Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/app/src/agents/site-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,11 @@ describe("project context", () => {
expect(context).toContain("assets/poster.png");
expect(context).toContain("inspect_image");
expect(context).toContain("read_url");
expect(context).not.toContain("Uploaded documents in the project: assets/legacy.docx");
expect(context).not.toContain("supported documents");
// Only PDFs are extractable; an unsupported upload must not be offered to
// extract_document_text.
const extractionGuidance = context.split("\n").find((line) => line.includes("extract_document_text"));
expect(extractionGuidance).toContain("assets/cv.pdf");
expect(extractionGuidance).not.toContain("legacy.docx");
});
});

Expand Down
3 changes: 1 addition & 2 deletions packages/app/src/boundary-logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
emitDiagnostic,
errorCodeFrom,
mintCorrelation,
outcomeForStatus,
principalForOperationalSubject,
terminalForStatus,
withCorrelationFetch,
Expand Down Expand Up @@ -477,7 +476,7 @@ describe("service-local diagnostics and helpers", () => {
});

it("maps terminal outcomes and principals without treating legacy ids as subjects", () => {
expect(outcomeForStatus(200)).toBe("ok");
expect(terminalForStatus(200).outcome).toBe("ok");
expect(terminalForStatus(401)).toEqual({ outcome: "denied", reason: "denied" });
expect(terminalForStatus(429)).toEqual({ outcome: "denied", reason: "rate_limited" });
expect(terminalForStatus(503)).toEqual({ outcome: "error", reason: "application_failure" });
Expand Down
11 changes: 0 additions & 11 deletions packages/app/src/fleet-projection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
CAIL_ANALYTICS_ENGINE_BLOBS,
CAIL_ANALYTICS_ENGINE_DOUBLES,
CAIL_EVENTS,
ROUTE_TEMPLATE_RE,
type CailAnalyticsEngineDataPoint,
} from "@cuny-ai-lab/cail-log";
import { OBSERVABILITY_CONTRACT } from "./lib/observability/contract";
Expand Down Expand Up @@ -82,14 +81,4 @@ describe("CAIL fleet projection boundary", () => {
consoleLog.mockRestore();
}
});

it("recognizes every canonical action route and keeps build/publish distinct", () => {
expect(Object.entries(OBSERVABILITY_CONTRACT.actions)).toEqual([
["build", { route: "/api/agents/site-builder/{project_id}", method: "POST" }],
["publish", { route: "/api/projects/{id}/publish", method: "POST" }],
]);
for (const action of Object.values(OBSERVABILITY_CONTRACT.actions)) {
expect(ROUTE_TEMPLATE_RE.test(action.route)).toBe(true);
}
});
});
60 changes: 33 additions & 27 deletions packages/app/src/lib/cail-identity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from "@cuny-ai-lab/cail-identity/testing";
import {
cailAuthRequiredResponse,
getRequestIdentity,
resolveRequestIdentity,
} from "./cail-identity";

Expand Down Expand Up @@ -131,57 +130,64 @@ beforeAll(async () => {
};
});

describe("getRequestIdentity", () => {
const INVALID = { status: "invalid" } as const;

describe("resolveRequestIdentity token verification", () => {
it("accepts a valid canonical identity token", async () => {
const identity = await getRequestIdentity(requestWithToken(await mintJwt()), currentEnv);
const token = await mintJwt();
const result = await resolveRequestIdentity(requestWithToken(token), currentEnv);

expect(identity).toEqual({
subject: TEST_SUBJECTS.alice,
email: "someone@gc.cuny.edu",
name: "Some One",
entitlements: ["site-studio"],
expect(result).toEqual({
status: "verified",
token,
identity: {
subject: TEST_SUBJECTS.alice,
email: "someone@gc.cuny.edu",
name: "Some One",
entitlements: ["site-studio"],
},
});
});

it("rejects a token signed by a key outside the configured JWKS", async () => {
const otherIssuer = await createTestIdentityIssuer({ kid: "other" });
const token = await otherIssuer.mintIdentityJwt({ audience: AUDIENCE });
expect(await getRequestIdentity(requestWithToken(token), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(token), currentEnv)).toEqual(INVALID);
});

it("rejects an algorithm other than RS256", async () => {
// Sanity: the same local key verifies when the header is untampered.
expect(await getRequestIdentity(requestWithToken(await signLocalJwt(localKey)), localEnv))
.not.toBeNull();
expect(await resolveRequestIdentity(requestWithToken(await signLocalJwt(localKey)), localEnv))
.toMatchObject({ status: "verified" });
const token = await signLocalJwt(localKey, {}, {
alg: "none",
typ: "JWT",
kid: localKey.jwk.kid,
});
expect(await getRequestIdentity(requestWithToken(token), localEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(token), localEnv)).toEqual(INVALID);
});

it("rejects an expired token", async () => {
// exp = (now - 3720) + 3600 = 120 seconds in the past.
const token = await mintJwt({ now: Math.floor(Date.now() / 1000) - 3720 });
expect(await getRequestIdentity(requestWithToken(token), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(token), currentEnv)).toEqual(INVALID);
});

it("rejects the wrong audience", async () => {
const token = await mintJwt({ audience: "cail:another-service" });
expect(await getRequestIdentity(requestWithToken(token), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(token), currentEnv)).toEqual(INVALID);
});

it("rejects array-valued audiences, including a one-element array", async () => {
const token = await mintJwt({ audience: [AUDIENCE] });
expect(await getRequestIdentity(requestWithToken(token), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(token), currentEnv)).toEqual(INVALID);
});

it("preserves the verified canonical subject byte-for-byte as the durable owner key", async () => {
const subject = canonicalTestSubject("durable-owner");
const token = await mintJwt({ subject });
await expect(getRequestIdentity(requestWithToken(token), currentEnv))
.resolves.toMatchObject({ subject });
await expect(resolveRequestIdentity(requestWithToken(token), currentEnv))
.resolves.toMatchObject({ status: "verified", identity: { subject } });
});

it("rejects non-canonical subjects (v4 accepts only cail-<32 lowercase hex>)", async () => {
Expand All @@ -192,16 +198,16 @@ describe("getRequestIdentity", () => {
"someone@gc.cuny.edu",
]) {
const token = await mintJwt({ subject });
expect(await getRequestIdentity(requestWithToken(token), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(token), currentEnv)).toEqual(INVALID);
}
});

it("rejects non-allowlisted and look-alike issuers", async () => {
const untrusted = await mintJwt({ issuer: "https://evil.example/not-cail" });
const lookAlike = await mintJwt({ issuer: "https://evil.example/cail-sso" });

expect(await getRequestIdentity(requestWithToken(untrusted), currentEnv)).toBeNull();
expect(await getRequestIdentity(requestWithToken(lookAlike), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(untrusted), currentEnv)).toEqual(INVALID);
expect(await resolveRequestIdentity(requestWithToken(lookAlike), currentEnv)).toEqual(INVALID);
});

it("rejects a self-consistent attacker issuer, JWKS, and token configuration", async () => {
Expand All @@ -212,10 +218,10 @@ describe("getRequestIdentity", () => {
issuer: attackerIssuer,
});

await expect(getRequestIdentity(requestWithToken(token), {
await expect(resolveRequestIdentity(requestWithToken(token), {
CAIL_IDENTITY_JWKS: attacker.jwksJson,
CAIL_IDENTITY_ISSUER: attackerIssuer,
})).resolves.toBeNull();
})).resolves.toEqual(INVALID);
});

it("fails closed when the configured issuer is not CAIL's canonical issuer", async () => {
Expand All @@ -225,19 +231,19 @@ describe("getRequestIdentity", () => {
{ ...currentEnv, CAIL_IDENTITY_ISSUER: undefined },
{ ...currentEnv, CAIL_IDENTITY_ISSUER: "https://evil.example/cail-sso" },
]) {
await expect(getRequestIdentity(requestWithToken(productionToken), env))
.resolves.toBeNull();
await expect(resolveRequestIdentity(requestWithToken(productionToken), env))
.resolves.toEqual(INVALID);
}
});

it("rejects an empty subject", async () => {
const token = await mintJwt({ subject: "" });
expect(await getRequestIdentity(requestWithToken(token), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken(token), currentEnv)).toEqual(INVALID);
});

it("rejects malformed tokens", async () => {
expect(await getRequestIdentity(requestWithToken("not-a-jwt"), currentEnv)).toBeNull();
expect(await getRequestIdentity(requestWithToken("a.b"), currentEnv)).toBeNull();
expect(await resolveRequestIdentity(requestWithToken("not-a-jwt"), currentEnv)).toEqual(INVALID);
expect(await resolveRequestIdentity(requestWithToken("a.b"), currentEnv)).toEqual(INVALID);
});
});

Expand Down
13 changes: 0 additions & 13 deletions packages/app/src/lib/cail-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,19 +103,6 @@ export async function resolveRequestIdentity(
: { status: "invalid" };
}

/**
* Verify the selected CAIL identity carried on a request, if any. Returns the
* identity or `null`; callers that must distinguish invalid credentials from absence
* use `resolveRequestIdentity`. Never throws.
*/
export async function getRequestIdentity(
request: Request,
env: IdentityVerificationEnv
): Promise<CailIdentity | null> {
const resolution = await resolveRequestIdentity(request, env);
return resolution.status === "verified" ? resolution.identity : null;
}

/**
* The CAIL `authentication_required` envelope (docs/security-and-recovery.md,
* identity and ownership), returned verbatim so the frontend treats worker-issued
Expand Down
7 changes: 1 addition & 6 deletions packages/app/src/lib/document.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import { extractDocumentText, supportsDocumentExtraction, type DocumentExtractor } from "./document";
import { extractDocumentText, type DocumentExtractor } from "./document";

describe("document extraction", () => {
const mockedExtractText = vi.fn<DocumentExtractor["extractText"]>();
Expand All @@ -12,11 +12,6 @@ describe("document extraction", () => {
mockedGetMeta.mockReset();
});

it("marks PDFs as supported for extraction", () => {
expect(supportsDocumentExtraction("paper.pdf")).toBe(true);
expect(supportsDocumentExtraction("notes.md")).toBe(false);
});

it("extracts text and metadata from PDFs", async () => {
mockedExtractText.mockResolvedValue({
totalPages: 2,
Expand Down
4 changes: 0 additions & 4 deletions packages/app/src/lib/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,6 @@ function formatPdfPages(pages: string[]): FormattedPdfPages {
return { text, warnings };
}

export function supportsDocumentExtraction(filePath: string): boolean {
return getContentType(filePath) === "application/pdf";
}

export async function extractDocumentText(
filePath: string,
data: Uint8Array,
Expand Down
63 changes: 0 additions & 63 deletions packages/app/src/lib/http.test.ts

This file was deleted.

8 changes: 0 additions & 8 deletions packages/app/src/lib/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ export async function readFormData(request: Request): Promise<FormData> {
}
}

export function jsonHeaders(extra?: HeadersInit): Headers {
const headers = new Headers(extra);
if (!headers.has("Content-Type")) {
headers.set("Content-Type", "application/json; charset=utf-8");
}
return headers;
}

export function binaryBody(data: Uint8Array): Blob {
return new Blob([new Uint8Array(data)]);
}
4 changes: 0 additions & 4 deletions packages/app/src/lib/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,6 @@ export function terminalForStatus(status: number): CailTerminalFields {
return { outcome: "ok", reason: "completed" };
}

export function outcomeForStatus(status: number): CailTerminalFields["outcome"] {
return terminalForStatus(status).outcome;
}

/** Stable machine type derived only from the exception class, never its message. */
export function errorCodeFrom(cause: unknown): string {
const name = cause instanceof Error && cause.name ? cause.name : "error";
Expand Down
Loading
Loading