feat: improve API keys and build workflows

This commit is contained in:
2026-10-04 20:05:13 +00:00 Unverified
parent 62f2362a2e
commit e4623efe86
27 changed files with 2080 additions and 235 deletions
+83 -8
View File
@@ -136,9 +136,27 @@ describe("API keys", () => {
});
expect(differentWorkspace.status).toBe(403);
const noExpiry = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
});
expect(noExpiry.status).toBe(403);
expect((await noExpiry.json()) as { code: string }).toHaveProperty(
"code",
"API_KEY_DELEGATION_FORBIDDEN",
);
const laterExpiry = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-06T00:00:00.000Z",
});
expect(laterExpiry.status).toBe(403);
const subset = await create({
capabilities: ["kubernetes:read"],
workspace: "shop",
expiresAt: "2026-10-05T00:00:00.000Z",
});
expect(subset.status).toBe(201);
expect(
@@ -168,7 +186,7 @@ describe("API keys", () => {
event.spec.action === "api_key.create" &&
event.spec.outcome === "denied",
),
).toHaveLength(4);
).toHaveLength(6);
expect(JSON.stringify(denied)).not.toContain("delegation-parent");
await store.createApiKey({
@@ -184,7 +202,10 @@ describe("API keys", () => {
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt: "2026-09-20T00:00:00.000Z",
}),
},
"unscoped-delegation",
),
@@ -193,6 +214,33 @@ describe("API keys", () => {
expect(await unscopedSubset.json()).not.toHaveProperty("workspace");
});
test("allows a non-expiring parent to delegate a non-expiring child", async () => {
const { app, store } = await setup();
await store.createApiKey({
id: "key_nonexpiring_parent",
tokenHash: hashToken("nonexpiring-parent"),
username: "ci",
capabilities: ["users:write", "kubernetes:read"],
});
const created = await app(
request(
"/api/v2/users/ci/keys",
{
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
},
"nonexpiring-parent",
),
);
expect(created.status).toBe(201);
const child = (await created.json()) as { token: string };
expect(child).not.toHaveProperty("expiresAt");
expect((await app(request("/api/v2/me", {}, child.token))).status).toBe(
200,
);
});
test("rejects expired keys and expiry longer than 365 days", async () => {
const { app, store } = await setup();
await store.createApiKey({
@@ -221,7 +269,7 @@ describe("API keys", () => {
).toBe(400);
});
test("uses the default expiry, cleans up expired keys, and does not log keys out", async () => {
test("preserves non-expiring keys while cleaning up finite keys and sessions", async () => {
const { app, store } = await setup();
const created = await app(
request("/api/v2/users/ci/keys", {
@@ -230,17 +278,44 @@ describe("API keys", () => {
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
}),
);
const key = (await created.json()) as { token: string; expiresAt: string };
expect(key.expiresAt).toBe("2026-12-04T00:00:00.000Z");
expect(created.status).toBe(201);
const key = (await created.json()) as { token: string };
expect(key).not.toHaveProperty("expiresAt");
const listed = await app(request("/api/v2/users/ci/keys"));
const { items } = (await listed.json()) as { items: object[] };
expect(items).toHaveLength(1);
expect(items[0]).not.toHaveProperty("expiresAt");
const finite = await app(
request("/api/v2/users/ci/keys", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt: "2026-10-05T00:00:00.000Z",
}),
}),
);
expect(finite.status).toBe(201);
const finiteKey = (await finite.json()) as {
token: string;
expiresAt: string;
};
expect(finiteKey.expiresAt).toBe("2026-10-05T00:00:00.000Z");
expect(
(await app(request("/api/v2/logout", { method: "POST" }, key.token)))
.status,
).toBe(204);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(200);
expect(await cleanupExpiredSessions(store, Date.parse(key.expiresAt))).toBe(
2,
expect((await app(request("/api/v2/me", {}, finiteKey.token))).status).toBe(
200,
);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(401);
expect(
await cleanupExpiredSessions(store, Date.parse(finiteKey.expiresAt)),
).toBe(2);
expect((await app(request("/api/v2/me", {}, finiteKey.token))).status).toBe(
401,
);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(200);
});
test("denies a workspace-scoped key outside its workspace", async () => {
+179 -4
View File
@@ -238,9 +238,7 @@ describe("operation response safety", () => {
});
test("redacts database and storage reconciliation results immediately", async () => {
const workspaceStore = new MemoryWorkspaceStore({
uid: () => "workspace-uid",
});
const workspaceStore = new MemoryWorkspaceStore({ uid: () => "workspace-uid" });
await workspaceStore.create({
id: "demo",
source: { uri: "oci://example/demo", digest: "sha256:abc" },
@@ -381,6 +379,143 @@ describe("operation response safety", () => {
});
});
describe("API key route expiry", () => {
test("omitted expiry creates a non-expiring key that authenticates and survives cleanup", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "admin",
passwordHash: "hash",
roles: ["admin"],
});
await store.putUser({
username: "ci",
passwordHash: "hash",
roles: ["viewer"],
});
await store.putSession({
tokenHash: hashToken("admin-token"),
username: "admin",
authVersion: 1,
expiresAt: "2027-01-01T00:00:00.000Z",
});
let time = Date.parse("2026-10-05T00:00:00.000Z");
const app = createApp({ store, now: () => time });
const created = await app(
request(
"/api/v2/users/ci/keys",
{
method: "POST",
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
},
"admin-token",
),
);
expect(created.status).toBe(201);
const key = (await created.json()) as {
id: string;
token: string;
expiresAt?: string;
};
expect(key).not.toHaveProperty("expiresAt");
expect((await store.listApiKeys("ci"))[0]).not.toHaveProperty("expiresAt");
const listed = await app(
request("/api/v2/users/ci/keys", {}, "admin-token"),
);
const list = (await listed.json()) as { items: Record<string, unknown>[] };
expect(list.items).toEqual([expect.objectContaining({ id: key.id })]);
expect(list.items[0]).not.toHaveProperty("expiresAt");
time = Date.parse("2028-01-01T00:00:00.000Z");
expect(await cleanupExpiredSessions(store, time)).toBe(1);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(200);
});
test("explicit finite expiry is enforced and malformed expiry is rejected", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "admin",
passwordHash: "hash",
roles: ["admin"],
});
await store.putUser({
username: "ci",
passwordHash: "hash",
roles: ["viewer"],
});
await store.putSession({
tokenHash: hashToken("admin-token"),
username: "admin",
authVersion: 1,
expiresAt: "2027-01-01T00:00:00.000Z",
});
let time = Date.parse("2026-10-05T00:00:00.000Z");
const app = createApp({ store, now: () => time });
const create = (expiresAt: unknown) =>
app(
request(
"/api/v2/users/ci/keys",
{
method: "POST",
body: JSON.stringify({
capabilities: ["kubernetes:read"],
expiresAt,
}),
},
"admin-token",
),
);
const expiry = "2026-10-06T00:00:00.000Z";
const created = await create(expiry);
expect(created.status).toBe(201);
const key = (await created.json()) as { token: string; expiresAt: string };
expect(key.expiresAt).toBe(expiry);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(200);
for (const invalid of [
null,
0,
"",
"2026-10-05T00:00:00.000Z",
"2027-10-06T00:00:00.000Z",
])
expect((await create(invalid)).status).toBe(400);
time = Date.parse(expiry);
expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(401);
});
test("a finite parent API key cannot delegate a non-expiring child", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "ci",
passwordHash: "hash",
roles: ["viewer"],
});
await store.createApiKey({
id: "finite-parent-key-1",
tokenHash: hashToken("parent-token"),
username: "ci",
capabilities: ["users:write", "kubernetes:read"],
expiresAt: "2027-01-01T00:00:00.000Z",
});
const app = createApp({
store,
now: () => Date.parse("2026-10-05T00:00:00.000Z"),
});
const created = await app(
request(
"/api/v2/users/ci/keys",
{
method: "POST",
body: JSON.stringify({ capabilities: ["kubernetes:read"] }),
},
"parent-token",
),
);
expect(created.status).toBe(403);
expect((await created.json()) as { code: string }).toMatchObject({
code: "API_KEY_DELEGATION_FORBIDDEN",
});
});
});
async function authenticatedStore(role: "viewer" | "operator" | "admin") {
const store = new MemoryAuthStore();
await store.putUser({ username: role, passwordHash: "hash", roles: [role] });
@@ -1078,7 +1213,9 @@ describe("kuber v2 HTTP routes", () => {
});
test("a stalled observation-only wait does not block mutation and remains idempotent", async () => {
const workspaceStore = new MemoryWorkspaceStore({ uid: () => "workspace-uid" });
const workspaceStore = new MemoryWorkspaceStore({
uid: () => "workspace-uid",
});
await workspaceStore.create({
id: "demo",
source: { uri: "oci://example/demo", digest: "sha256:abc" },
@@ -1620,6 +1757,44 @@ describe("kuber v2 HTTP routes", () => {
).toBe("failed");
});
test("returns safe database phase and claim details on first and repeated failures", async () => {
const workspaceStore = new MemoryWorkspaceStore({ uid: () => "workspace-uid" });
await workspaceStore.create({
id: "demo",
source: { uri: "oci://example/demo", digest: "sha256:abc" },
});
const operationStore = new MemoryOperationStore(undefined, () => "db-failure");
const { DatabaseReconciliationError } = await import("../../lib/database");
const app = createApp({
store: await authenticatedStore("operator"),
workspaceStore,
operationStore,
management: {
reconcileDatabases: async () => {
throw new DatabaseReconciliationError(
"database apply",
new Error("Forbidden: DB_PASSWORD=private-value"),
{ service: "web", username: "web_role", database: "web_db", secretName: "web" },
);
},
} as unknown as ManagementService,
});
const reconcile = () => app(request(
"/api/v2/workspaces/demo/databases",
{ method: "POST", headers: { "idempotency-key": "db-failure" }, body: JSON.stringify({ compose: {} }) },
"token",
));
for (const result of [await reconcile(), await reconcile()]) {
expect(result.status).toBe(500);
const problem = await result.json() as { code: string; detail: string };
expect(problem.code).toBe("DATABASE_RECONCILE_FAILED");
expect(problem.detail).toContain("database apply for database web_db (service web, role web_role): Forbidden");
expect(JSON.stringify(problem)).not.toContain("private-value");
}
expect((await operationStore.get("operation-db-failure"))?.status.error?.message)
.toContain("database apply for database web_db");
});
test("routes workspace adoption and keeps platform adoption admin-only", async () => {
const workspaceStore = new MemoryWorkspaceStore({
uid: () => "workspace-uid",
+49
View File
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
import {
MemoryAuthStore,
hashToken,
normalizeApiKey,
tokenHashesEqual,
} from "../../server/auth";
import {
@@ -23,6 +24,54 @@ describe("authorization", () => {
});
describe("memory auth store", () => {
test("keeps non-expiring keys active and preserves finite expiry validation", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "alice",
passwordHash: "hash",
roles: ["viewer"],
});
const key = {
id: "never-expiring-key-123",
tokenHash: hashToken("never"),
username: "alice",
capabilities: ["kubernetes:read" as const],
};
await store.createApiKey(key);
expect(await store.getApiKey(key.tokenHash)).toMatchObject(key);
expect((await store.listApiKeys("alice"))[0]?.expiresAt).toBeUndefined();
await store.createApiKey({
...key,
id: "finite-expiry-key-123",
tokenHash: hashToken("finite"),
expiresAt: "2020-01-01T00:00:00.000Z",
});
expect(await store.deleteExpiredApiKeys()).toBe(1);
expect(await store.getApiKey(key.tokenHash)).toMatchObject(key);
expect(
normalizeApiKey({ ...key, expiresAt: undefined }).expiresAt,
).toBeUndefined();
for (const expiresAt of ["none", "not-a-date", "2026-09-03", ""]) {
expect(() => normalizeApiKey({ ...key, expiresAt })).toThrow(
"ISO timestamp",
);
}
await expect(
store.createApiKey({
...key,
id: "duplicate-key-id-123",
tokenHash: hashToken("duplicate"),
}),
).resolves.toBeUndefined();
expect(await store.revokeApiKey("bob", key.id)).toBe(false);
expect(await store.revokeApiKey("alice", key.id)).toBe(true);
expect(await store.getApiKey(key.tokenHash)).toBeUndefined();
await expect(
store.createApiKey({ ...key, username: "missing" }),
).rejects.toThrow("not active");
await store.updateUser("alice", { disabled: true });
await expect(store.createApiKey({ ...key })).rejects.toThrow("not active");
});
test("maintains the API-key hash index across key mutations", async () => {
const store = new MemoryAuthStore();
await store.putUser({
+119 -2
View File
@@ -167,7 +167,10 @@ async function fixture(
};
}
async function internalFixture(maxLogBytes = 1024) {
async function internalFixture(
maxLogBytes = 1024,
resolveDigest?: (image: string) => Promise<Sha256Digest>,
) {
const root = await mkdtemp(join(tmpdir(), "kuber-controller-internal-"));
roots.push(root);
const cas = new FilesystemCas(join(root, "cas"));
@@ -205,7 +208,7 @@ async function internalFixture(maxLogBytes = 1024) {
pushRegistryInsecure: true,
maxLogBytes,
now: () => new Date(Date.UTC(2026, 8, 2, 0, 0, now++)),
resolveDigest: async () => `sha256:${"f".repeat(64)}`,
resolveDigest: resolveDigest ?? (async () => `sha256:${"f".repeat(64)}`),
});
const request: BuildRequest = {
version: BUILD_PROTOCOL_VERSION,
@@ -233,6 +236,86 @@ async function internalFixture(maxLogBytes = 1024) {
}
describe("build controller", () => {
test("publishes multiple service destinations in one Job and resolves every canonical reference", async () => {
const resolved: string[] = [];
const { controller, request, kubernetes } = await internalFixture(
1024,
async (image) => {
resolved.push(image);
return `sha256:${"f".repeat(64)}`;
},
);
request.destinations = [
{ service: "worker", image: "external.example/other:latest" },
];
await controller.submitBuild(request);
expect(kubernetes.jobs).toHaveLength(1);
expect(
(kubernetes.jobs[0]!.spec as any).template.spec.containers[0].args,
).toContain(
'--output=type=image,"name=cncf-distribution-svc.registry.svc.cluster.local:5000/kuber/demo-web:latest,cncf-distribution-svc.registry.svc.cluster.local:5000/kuber/demo-worker:latest",push=true,registry.insecure=true',
);
kubernetes.observation = { phase: "succeeded" };
expect(await controller.reconcileBuild(request.id)).toMatchObject({
state: "succeeded",
});
expect(resolved).toEqual([
"registry.neko-piranha.ts.net/kuber/demo-web:latest",
"registry.neko-piranha.ts.net/kuber/demo-worker:latest",
]);
expect(await controller.getBuildResult(request.id)).toMatchObject({
reference: `registry.neko-piranha.ts.net/kuber/demo-web@sha256:${"f".repeat(64)}`,
references: {
worker: `registry.neko-piranha.ts.net/kuber/demo-worker@sha256:${"f".repeat(64)}`,
},
});
});
test("fails the shared job if an alias resolves to a different digest", async () => {
const { controller, request, kubernetes } = await internalFixture(
1024,
async (image) =>
`sha256:${(image.includes("worker") ? "e" : "f").repeat(64)}`,
);
request.destinations = [
{ service: "worker", image: "registry.test/worker:latest" },
];
await controller.submitBuild(request);
kubernetes.observation = { phase: "succeeded" };
expect(await controller.reconcileBuild(request.id)).toMatchObject({
state: "failed",
error: expect.stringContaining("different digest"),
});
await expect(controller.getBuildResult(request.id)).rejects.toBeInstanceOf(
BuildConflictError,
);
});
test("rejects duplicate alias destinations and unsafe exporter names", async () => {
const { controller, request, kubernetes } = await fixture();
request.destinations = [
{ service: "web", image: "registry.test/demo/other:latest" },
];
await expect(controller.submitBuild(request)).rejects.toBeInstanceOf(
BuildValidationError,
);
request.destinations = [
{ service: "worker", image: "registry.test/demo/web:latest" },
];
await expect(controller.submitBuild(request)).rejects.toBeInstanceOf(
BuildValidationError,
);
request.destinations = [
{
service: "worker",
image: "registry.test/demo/worker:latest,push=false",
},
];
await expect(controller.submitBuild(request)).rejects.toBeInstanceOf(
BuildValidationError,
);
expect(kubernetes.jobs).toHaveLength(0);
});
test("negotiates snapshots and resumes verified blob uploads", async () => {
const { controller, cas } = await fixture();
const content = Buffer.from("resumable");
@@ -365,6 +448,40 @@ describe("build controller", () => {
expect(await controller.reconcileBuild(replacement.id)).toEqual(status);
});
test("persists creating and starting phases without claiming the build has started", async () => {
const { controller, kubernetes, request } = await fixture();
expect(await controller.submitBuild(request)).toMatchObject({
state: "queued",
phase: "queued",
});
for (const phase of ["creating", "starting"] as const) {
kubernetes.observation = { phase };
expect(await controller.reconcileBuild(request.id)).toMatchObject({
state: "queued",
phase,
});
}
kubernetes.observation = { phase: "running" };
expect(await controller.reconcileBuild(request.id)).toMatchObject({
state: "running",
phase: "running",
});
kubernetes.observation = {
phase: "failed",
error: "buildkit crashed\nstack line",
};
expect(await controller.reconcileBuild(request.id)).toMatchObject({
state: "failed",
phase: "done",
error: "buildkit crashed\nstack line",
});
expect(
(await controller.getBuildEvents(request.id))
.filter((event) => event.type === "status")
.map((event) => event.type === "status" && event.status.phase),
).toEqual(["queued", "creating", "starting", "running", "done"]);
});
test("concurrent controllers converge on one same-ID record and Job", async () => {
const first = await fixture();
const second = new BuildController({
+14
View File
@@ -15,6 +15,20 @@ function spec(architecture: "arm64" | "amd64"): BuildSpec {
}
describe("BuildKit Job generation", () => {
test("quotes multiple image names as one BuildKit image exporter", () => {
const job = createBuildJob({
name: "build-multi",
namespace: "default",
spec: spec("amd64"),
workspaceClaimName: "workspace",
cacheImage: "registry.example.com/cache/demo-web",
pushImage: "registry.example.com/kuber/demo-web:latest",
pushImages: ["registry.example.com/kuber/demo-worker:latest"],
});
expect((job.spec as any).template.spec.containers[0].args).toContain(
'--output=type=image,"name=registry.example.com/kuber/demo-web:latest,registry.example.com/kuber/demo-worker:latest",push=true',
);
});
test.each(["arm64", "amd64"] as const)(
"generates a rootless %s job",
(architecture) => {
+54 -6
View File
@@ -6,6 +6,7 @@ import {
type BuildRecord,
} from "../../server/build-store";
import {
KubernetesBuildOperations,
KubernetesBuildStore,
type BuildObjectApi,
} from "../../server/build-kubernetes";
@@ -19,6 +20,50 @@ const namespace = "kuber-test";
const imageKey = "project\0service\0registry.test/app:latest";
const workspace = `sha256:${"a".repeat(64)}` as Sha256Digest;
test("observes the build container lifecycle rather than treating an active Job as running", async () => {
const job = {
status: { active: 1, startTime: new Date("2026-01-01T00:00:00Z") },
};
let pods: any[] = [];
const operations = new KubernetesBuildOperations(
{ readNamespacedJob: async () => job } as any,
{ listNamespacedPod: async () => ({ items: pods }) } as any,
);
expect((await operations.getJob("builds", "job"))?.phase).toBe("creating");
pods = [{ spec: {}, status: { phase: "Pending" } }];
expect((await operations.getJob("builds", "job"))?.phase).toBe("creating");
pods = [
{
spec: { nodeName: "node" },
status: {
phase: "Pending",
containerStatuses: [
{
name: "buildkit",
state: { waiting: { reason: "ContainerCreating" } },
},
],
},
},
];
expect((await operations.getJob("builds", "job"))?.phase).toBe("starting");
pods[0].status.phase = "Running";
expect((await operations.getJob("builds", "job"))?.phase).toBe("starting");
pods[0].status.containerStatuses[0].state = {
running: { startedAt: new Date() },
};
expect(await operations.getJob("builds", "job")).toMatchObject({
phase: "running",
});
(job.status as any).conditions = [
{ type: "Failed", status: "True", message: "crashed" },
];
expect(await operations.getJob("builds", "job")).toMatchObject({
phase: "failed",
error: "crashed",
});
});
function validLabelValue(value: string): boolean {
return (
value.length <= 63 &&
@@ -221,11 +266,12 @@ describe("KubernetesBuildStore", () => {
new Date(Date.UTC(2026, 0, 1, 0, 0, i)).toISOString(),
);
record.status.state = i === 0 ? "queued" : "failed";
if (i === 1) record.status.reconcileLease = {
holder: "worker",
token: "active-lease",
expiresAt: new Date(Date.now() + 60_000).toISOString(),
};
if (i === 1)
record.status.reconcileLease = {
holder: "worker",
token: "active-lease",
expiresAt: new Date(Date.now() + 60_000).toISOString(),
};
fake.maps.set(name("build", record.metadata.name), configMap(record));
}
const old = build(
@@ -253,7 +299,9 @@ describe("KubernetesBuildStore", () => {
expect(fake.maps.has(name("build", "protected-lock"))).toBe(true);
expect(fake.maps.has(name("build", "history-2"))).toBe(false);
expect(
[...fake.maps.values()].filter((value) => value.metadata.labels?.["kuber.astrxl.dev/type"] === "build"),
[...fake.maps.values()].filter(
(value) => value.metadata.labels?.["kuber.astrxl.dev/type"] === "build",
),
).toHaveLength(50);
expect(fake.deletes.length).toBeGreaterThan(0);
expect(
+60 -1
View File
@@ -26,7 +26,7 @@ function encode(value: string): string {
}
function secret(
recordType: "user" | "session",
recordType: "user" | "session" | "api-key",
name: string,
values: Record<string, string>,
): StoredSecret {
@@ -111,6 +111,65 @@ function setup(): {
}
describe("KubernetesAuthStore", () => {
test("persists absent expiry, reads finite legacy keys, and rejects malformed expiries", async () => {
const { fake, store } = setup();
await store.putUser({
username: "alice",
passwordHash: "hash",
roles: ["viewer"],
});
const never = {
id: "never-expiring-key-123",
tokenHash: hashToken("never"),
username: "alice",
capabilities: ["kubernetes:read" as const],
};
await store.createApiKey(never);
expect(fake.patches.at(-1)?.stringData).not.toHaveProperty("expiresAt");
expect(await store.getApiKey(never.tokenHash)).toMatchObject(never);
const finite = {
...never,
id: "finite-expiry-key-123",
tokenHash: hashToken("finite"),
expiresAt: "2020-01-01T00:00:00.000Z",
};
await store.createApiKey(finite);
expect(
(await store.listApiKeys("alice")).map((key) => key.expiresAt),
).toEqual([finite.expiresAt, undefined]);
expect(await store.deleteExpiredApiKeys()).toBe(1);
expect(await store.getApiKey(never.tokenHash)).toMatchObject(never);
const active = {
...finite,
id: "active-finite-key-123",
tokenHash: hashToken("active-finite"),
expiresAt: new Date(Date.now() + 60_000).toISOString(),
};
await store.createApiKey(active);
expect(await store.getApiKey(active.tokenHash)).toMatchObject(active);
const name = objectName("api-key", never.tokenHash);
for (const expiry of ["none", "", "2026-09-03"]) {
fake.secrets.set(
name,
secret("api-key", name, {
id: never.id,
tokenHash: never.tokenHash,
username: never.username,
capabilities: JSON.stringify(never.capabilities),
workspace: "",
disabled: "false",
expiresAt: expiry,
}),
);
expect(await store.getApiKey(never.tokenHash)).toBeUndefined();
}
expect(await store.revokeApiKey("bob", finite.id)).toBe(false);
await expect(
store.createApiKey({ ...never, username: "missing" }),
).rejects.toThrow("not active");
await store.updateUser("alice", { disabled: true });
await expect(store.createApiKey(never)).rejects.toThrow("not active");
});
test("validates the session and user from the store on every request", async () => {
const { fake, store } = setup();
const tokenHash = hashToken("fresh");
+53
View File
@@ -25,6 +25,59 @@ describe("OCI registry digest resolution", () => {
).toThrow("Invalid image reference");
});
test("accepts distribution tags and digest-pinned references", () => {
for (const tag of [
"latest",
"v1.2.3",
"Release_2026-10",
"_build",
"a".repeat(128),
]) {
expect(parseImageReference(`localhost:5000/team/my_image:${tag}`)).toEqual({
registry: "localhost:5000",
repository: "team/my_image",
reference: tag,
});
}
const digest = `sha256:${"a".repeat(64)}` as const;
expect(
parseImageReference(`registry.example.com/team/my-image:Release_1@${digest}`),
).toEqual({
registry: "registry.example.com",
repository: "team/my-image",
reference: digest,
digest,
});
expect(
parseImageReference(`registry.example.com/team/my-image@${digest}`).digest,
).toBe(digest);
});
test("rejects malformed tags and uppercase repository names", () => {
for (const tag of [
"bad+tag",
".leading",
"-leading",
"bad:tag",
"bad@tag",
"bad/tag",
"é",
"a".repeat(129),
"",
]) {
expect(() =>
parseImageReference(`registry.example.com/team/image:${tag}`),
).toThrow();
}
expect(() =>
parseImageReference("registry.example.com/Team/image:Release_1"),
).toThrow("Invalid image reference");
const digest = `sha256:${"a".repeat(64)}` as const;
expect(() =>
parseImageReference(`registry.example.com/team/image:bad+tag@${digest}`),
).toThrow("Invalid image reference");
});
test("resolves an anonymous manifest using the advertised digest", async () => {
const expected = `sha256:${"b".repeat(64)}` as const;
const calls: Array<{ url: string; authorization: string | null }> = [];