Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
4240a8b
docs(ai): design authenticated proposal gateway context
seonghobae Aug 4, 2026
0bbc7c4
docs(ai): plan authenticated proposal gateway context
seonghobae Aug 4, 2026
cc9f23d
test(ai): define authenticated service context contract
seonghobae Aug 4, 2026
81f1492
test(web): define authenticated AI proposal BFF
seonghobae Aug 4, 2026
16aa319
test(ai): require signed service context at HTTP boundary
seonghobae Aug 4, 2026
b04f7c0
feat(ai): verify method-bound signed gateway context
seonghobae Aug 4, 2026
627ae96
fix(ai): reject unsigned ownership context
seonghobae Aug 4, 2026
d1e912a
test(ai): authenticate no-mutation HTTP evidence
seonghobae Aug 4, 2026
148e8b6
feat(web): add authenticated AI proposal BFF
seonghobae Aug 4, 2026
78ec645
feat(web): expose AI proposal collection BFF
seonghobae Aug 4, 2026
7d38143
feat(web): expose AI proposal detail BFF
seonghobae Aug 4, 2026
1c9373b
feat(web): expose AI proposal decision BFF
seonghobae Aug 4, 2026
abb4048
docs(ai): configure authenticated AI BFF
seonghobae Aug 4, 2026
a27b30a
test(web): cover AI proposal route delegation
seonghobae Aug 4, 2026
a0f3950
chore(web): validate authenticated AI BFF files
seonghobae Aug 4, 2026
89da015
chore(ai): format-check authenticated gateway slice
seonghobae Aug 4, 2026
5ffacf4
docs(ai): document signed proxy trust contract
seonghobae Aug 4, 2026
cd41e62
docs(ai): record authenticated proposal gateway
seonghobae Aug 4, 2026
33f41d2
docs(ai): record gateway standards research
seonghobae Aug 4, 2026
a5086bb
chore(ai): format-check standards research
seonghobae Aug 4, 2026
38a80ac
test(ai): enforce complete service coverage
seonghobae Aug 4, 2026
14dacb8
test(ai): run complete coverage gate
seonghobae Aug 4, 2026
33314d7
test(web): harden AI BFF boundary cases
seonghobae Aug 4, 2026
3e98916
test(web): reject cross-scope AI responses
seonghobae Aug 4, 2026
4db14bf
test(web): include cross-scope AI regression
seonghobae Aug 4, 2026
d5a9f9a
refactor(web): isolate AI proposal transport core
seonghobae Aug 4, 2026
ff121bd
fix(web): reject cross-scope AI representations
seonghobae Aug 4, 2026
c0adf4a
chore(web): validate AI transport core
seonghobae Aug 4, 2026
5ce5880
test(web): add one-time authenticated scope patch
seonghobae Aug 4, 2026
814b10c
chore(ci): remove one-time branch mutation workflow
seonghobae Aug 4, 2026
5de3935
chore(ci): expose one-time AI scope patch on PR
seonghobae Aug 4, 2026
33680e9
chore(ci): remove one-time AI scope patch
seonghobae Aug 4, 2026
ff86f59
style(web): format AI scope regression
seonghobae Aug 4, 2026
eba59a4
fix(web): bound identity stream without response cloning
seonghobae Aug 4, 2026
f34324d
test(web): prevent identity stream clone buffering regression
seonghobae Aug 4, 2026
a90cca7
test(web): include identity stream regression gate
seonghobae Aug 4, 2026
4f2e15d
docs(security): record bounded identity stream handling
seonghobae Aug 4, 2026
ab545fe
fix(ai): automate gateway review repairs
seonghobae Aug 4, 2026
b0ca2df
ci(ai): run gateway review repair
seonghobae Aug 4, 2026
c234aed
ci: expedite reviewed AI gateway repair
seonghobae Aug 4, 2026
eaa734a
ci: guarantee AI gateway repair on pull request
seonghobae Aug 4, 2026
ec6e2f3
fix(ai): remove credential-shaped gateway fixtures
github-actions[bot] Aug 4, 2026
3123d47
chore(ai): remove incidental lockfile from gateway slice
seonghobae Aug 4, 2026
eadef96
ci: format AI gateway review evidence
seonghobae Aug 4, 2026
1a3c3b0
style(ai): format gateway boundary evidence
github-actions[bot] Aug 4, 2026
cb4b2c5
ci: remove temporary AI gateway format repair
seonghobae Aug 4, 2026
4337077
ci: repair exact optional AI gateway request types
seonghobae Aug 4, 2026
649cf09
ci: temporarily repair AI gateway formatting
seonghobae Aug 4, 2026
22a0921
style: format authenticated AI gateway slice
github-actions[bot] Aug 4, 2026
0e1ee1e
ci: remove temporary AI gateway type repair
seonghobae Aug 4, 2026
0e32d56
ci: retrigger exact-head validation
seonghobae Aug 4, 2026
342b2ae
ci: finalize exact-head validation trigger
seonghobae Aug 4, 2026
53680d3
ci: temporarily repair exact optional property types
seonghobae Aug 4, 2026
c6a13d3
ci: persist AI gateway type repairs without workflow mutation
seonghobae Aug 4, 2026
1591fb0
ci: retrigger repaired exact-head validation
seonghobae Aug 4, 2026
217227b
ci: finalize repaired exact-head trigger
seonghobae Aug 4, 2026
8492ee2
ci: apply AI request type repair immediately
seonghobae Aug 4, 2026
3a3ce24
fix(web): omit absent optional request properties
github-actions[bot] Aug 4, 2026
090ed4a
ci: remove temporary AI gateway type repair
seonghobae Aug 4, 2026
a5ffb18
ci: temporarily repair web formatting
seonghobae Aug 4, 2026
16c2f0b
ci: format AI gateway web boundary
seonghobae Aug 4, 2026
c88c62e
style(web): format AI gateway boundary
github-actions[bot] Aug 4, 2026
c3979aa
ci: remove temporary AI gateway formatter
seonghobae Aug 4, 2026
8d38a62
ci: remove temporary web formatter
seonghobae Aug 4, 2026
3da8cc5
ci: retrigger final exact-head validation
seonghobae Aug 4, 2026
e9f071a
ci: finalize exact-head validation trigger
seonghobae Aug 4, 2026
f2a315f
ci: reconcile authenticated gateway with main
seonghobae Aug 4, 2026
4b623a0
ci: capture merge conflicts for autonomous resolution
seonghobae Aug 4, 2026
f27e167
ci: capture main merge conflicts
github-actions[bot] Aug 4, 2026
eda8da5
ci: resolve reviewed AI service merge conflicts
seonghobae Aug 4, 2026
5875716
merge: align AI service package with audit assurance baseline
seonghobae Aug 4, 2026
fd08645
merge: align AI coverage reporting with audit assurance baseline
seonghobae Aug 4, 2026
acb9899
Merge branch 'main' into feat/ai-authenticated-gateway-context
github-actions[bot] Aug 4, 2026
dca478a
ci: remove resolved main conflict marker
seonghobae Aug 4, 2026
e6ca079
ci: remove temporary main reconciliation workflow
seonghobae Aug 4, 2026
aa124ac
ci: repair merged AI quality gateway compatibility
seonghobae Aug 4, 2026
69b45a3
ci: repair merged AI quality context tests
seonghobae Aug 4, 2026
3e88e0d
chore(ci): repair signed-context coverage tests
seonghobae Aug 4, 2026
6bca04f
ci: make AI context test repair deterministic
seonghobae Aug 4, 2026
f46f67d
test(ai): exercise signed context in complete coverage suite
github-actions[bot] Aug 4, 2026
697a9cf
chore(ci): remove temporary quality repair workflow
seonghobae Aug 4, 2026
b8c1c34
ci: strengthen AI audit redaction evidence
seonghobae Aug 4, 2026
79399bb
ci: finalize AI context coverage repair
seonghobae Aug 4, 2026
7278e6e
refactor(ai): remove redundant canonical path guard
seonghobae Aug 4, 2026
03af447
test(ai): reject noncanonical signed context encoding
seonghobae Aug 4, 2026
78b4db0
chore(ci): remove temporary AI coverage repair workflow
seonghobae Aug 4, 2026
4183b22
ci: finalize AI gateway coverage and redaction evidence
seonghobae Aug 4, 2026
8689c10
ci: restore immutable pull request validation
seonghobae Aug 4, 2026
5b69744
test(ai): narrow alternate signature character
seonghobae Aug 4, 2026
2885864
ci: apply reviewed AI audit redaction assertion
seonghobae Aug 4, 2026
60681ac
test(ai): inspect error details in redaction evidence
github-actions[bot] Aug 4, 2026
4caaa1b
ci: finalize AI audit fixture repair
seonghobae Aug 4, 2026
e2e0596
test(ai): exercise signed context in complete coverage suite
github-actions[bot] Aug 4, 2026
4ced7a2
ci: remove completed AI context repair workflow
seonghobae Aug 4, 2026
ab9137d
ci: finalize deterministic AI test repair
seonghobae Aug 4, 2026
2b9ca31
test(ai): narrow canonical signature tail
seonghobae Aug 4, 2026
fcd7dab
ci: finalize deterministic AI assurance fixtures
seonghobae Aug 4, 2026
8d641ba
test(ai): cover default PostgreSQL runtime adapters
seonghobae Aug 4, 2026
9b5687b
test(ai): cover production module provider projections
seonghobae Aug 4, 2026
3ecfda3
test(ai): cover default audit clock and decision identity
seonghobae Aug 4, 2026
fd43e75
test(ai): cover production runtime and module wiring
seonghobae Aug 4, 2026
0af0004
ci: format and verify AI assurance gap tests
seonghobae Aug 4, 2026
fe701fa
test(ai): keep provider coverage credential free
seonghobae Aug 4, 2026
6b1b549
test(ai): consolidate runtime coverage evidence
seonghobae Aug 4, 2026
48e058d
test(ai): consolidate provider coverage evidence
seonghobae Aug 4, 2026
4f171b7
test(ai): remove duplicate runtime coverage fixture
seonghobae Aug 4, 2026
ff32b3b
test(ai): consolidate audit default coverage evidence
seonghobae Aug 4, 2026
ecfb38b
ci: verify consolidated AI assurance coverage
seonghobae Aug 4, 2026
925cb6d
test(ai): restore consolidated runtime coverage evidence
seonghobae Aug 4, 2026
fe7c4fb
test(ai): cover all Nest runtime provider factories
seonghobae Aug 4, 2026
c648ad3
test(ai): close assurance coverage gaps
github-actions[bot] Aug 4, 2026
c97780c
test(ai): clarify production provider assurance
seonghobae Aug 4, 2026
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ CORS_ALLOWED_ORIGINS=http://localhost:3000
IDENTITY_SERVICE_ORIGIN=http://127.0.0.1:4101
PLANNING_SERVICE_ORIGIN=http://127.0.0.1:4102
PLANNING_GATEWAY_CONTEXT_SECRET=replace-with-at-least-32-random-bytes
AI_SERVICE_ORIGIN=http://127.0.0.1:4105
AI_GATEWAY_CONTEXT_SECRET=replace-with-at-least-32-random-bytes
SESSION_SECRET=replace-with-at-least-32-random-bytes
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable changes to LifeOS are documented in this file.
- A bounded notification runtime that composes one PostgreSQL pool, the reminder repository, the in-app gateway, and the scheduler with exactly-once pool shutdown.
- A production AI runtime that persists every inert proposal before returning it and exposes tenant-scoped proposal evidence and append-only accept/reject decision history.
- Replay-safe AI proposal decisions bound to the exact workspace, actor, proposal revision digest, UUIDv4 idempotency key, and decision timestamp.
- An authenticated same-origin AI proposal boundary that derives workspace and actor identity from the active session, signs the exact upstream method and path, and never forwards browser credentials to AI service.

### Fixed

Expand All @@ -29,4 +30,5 @@ All notable changes to LifeOS are documented in this file.

- Planning-search upstream responses are stopped at a fixed byte limit before they can be fully buffered by the web boundary.
- Notification persistence stores SHA-256 idempotency digests instead of raw delivery keys, validates every untrusted row, and keeps all SQL tenant-scoped and parameterized.
- The AI production boundary accepts workspace and actor scope only through trusted headers, rejects ownership injection in decision bodies, returns credential-free problem details, and exposes no proposal apply or execution route.
- The AI production boundary rejects direct client-selected ownership headers, verifies a short-lived HMAC-SHA-256 context bound to workspace, actor, HTTP method, and exact path, returns credential-free problem details, and exposes no proposal apply or execution route.
- The AI web boundary consumes and bounds identity-session response streams exactly once, avoiding unbounded buffering from cloning an untrusted streamed response.
20 changes: 13 additions & 7 deletions apps/ai-service/migrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ The production module exposes the inert proposal-generation route together with
- `GET /v1/proposals/:proposalId/decisions`
- `POST /v1/proposals/:proposalId/decisions`

Every route derives workspace scope only from `x-workspace-id`. Decision append additionally requires `x-actor-id` and a closed JSON body containing `expectedContentDigest`, `idempotencyKey`, `decision`, optional `reason`, and `decidedAt`. Workspace and actor identifiers are trusted only when supplied by an authenticated gateway; direct public exposure of the AI service is not supported.
Every route requires a short-lived signed service context produced only after a trusted proxy authenticates a session and authorizes workspace membership. The headers are `x-life-os-workspace-id`, `x-life-os-actor-id`, `x-life-os-context-issued-at`, and `x-life-os-context-signature`. The HMAC-SHA-256 payload binds canonical workspace and actor UUIDv4 values to issuance time, uppercase HTTP method, and the exact `/v1/...` path. The shared `AI_GATEWAY_CONTEXT_SECRET` must contain 32–4096 UTF-8 bytes, remain server-only, and be identical in the trusted proxy and AI service.

There is deliberately no apply, execute, command, or user-data mutation route. Proposal generation persists the complete verified audit record before returning the proposal. Validation, not-found, stale-digest, conflicting replay, persistence, and unknown failures are mapped to bounded credential-free problem details.
Direct `x-workspace-id` and `x-actor-id` headers never authorize a route. Browser cookies, bearer material, client-selected ownership fields, and the HMAC secret must never be forwarded to or exposed by AI service. Missing, malformed, stale, future-dated, method-replayed, path-replayed, or forged context fails closed before the proposal or audit application receives tenant scope.

Decision append accepts a closed JSON body containing `expectedContentDigest`, `idempotencyKey`, `decision`, optional `reason`, and `decidedAt`; actor identity comes only from the verified service context. There is deliberately no apply, execute, command, or user-data mutation route. Proposal generation persists the complete verified audit record before returning the proposal. Validation, not-found, stale-digest, conflicting replay, persistence, and unknown failures are mapped to bounded credential-free problem details.

## Trust boundary

The default LifeOS composition exposes same-origin `/api/ai/...` web routes. That BFF sends the opaque browser cookie only to identity-service `GET /v1/session`, derives `workspaceId` and `userId` from the validated session response, signs the exact AI request, and calls AI service without forwarding the cookie. AI service remains independently deployable behind another compatible private proxy that implements the same versioned contract.

The audit schema stores only validated proposal requests, model identity, inert proposed operations, explanatory rationale, canonical SHA-256 digests, timestamps, and explicit user decisions. It has no foreign key, repository dependency, database privilege, or command surface for planning, calendar, habit, identity, notification, or other user-owned state mutation.

Every proposal, workspace, decision, actor, and idempotency identifier is constrained to UUIDv4. Decision ownership carries `(proposal_id, workspace_id, proposal_content_digest)` through a composite foreign key so an accept/reject event cannot silently target another tenant or a stale proposal revision.
Expand All @@ -52,12 +56,14 @@ Destructive schema setup is permitted only through `AI_TEST_DATABASE_URL`. The U

## Validation evidence

CI supplies separate application and disposable-test variables, applies the migration to an ephemeral PostgreSQL service, and verifies restart durability, deterministic reads, tenant isolation, exact decision replay, stale-digest rejection, conflicting replay rejection, append-only enforcement, bounded runtime configuration, retryable exactly-once successful shutdown, idle-client error handling, and the absence of proposal execution routes. All SQL values are parameterized and stored JSON is treated as untrusted evidence on read.
CI supplies separate application and disposable-test variables, applies the migration to an ephemeral PostgreSQL service, and verifies restart durability, deterministic reads, tenant isolation, exact decision replay, stale-digest rejection, conflicting replay rejection, append-only enforcement, bounded runtime configuration, retryable exactly-once successful shutdown, idle-client error handling, unsigned ownership rejection, method/path replay rejection, and the absence of proposal execution routes. All SQL values are parameterized and stored JSON is treated as untrusted evidence on read.

## Deferred work
## Secret rotation and rollback

Authenticated workspace and actor derivation belongs at the gateway. External model transport, prompt and context redaction, policy evaluation, model-quality evaluation, and separately authorized action execution remain independent reviewed capabilities. The audit service must not gain planning, calendar, habit, identity, notification, or generic command dependencies when those slices are added.
This contract currently supports one active secret. Rotation requires a coordinated trusted-proxy and AI-service deployment; zero-downtime overlapping verification keys are deferred. If signer and verifier become incompatible, disable external AI proposal traffic rather than falling back to unsigned ownership headers. Secret compromise requires coordinated replacement, waiting at least the 60-second context lifetime before treating old tags as expired, and reviewing proposal/decision audit evidence for forged activity.

## Rollback
The database migration is forward-only in automated environments. An operator-approved rollback must export and verify proposal and decision evidence before dropping `ai.proposal_decision_events`, `ai.proposal_audit_records`, `ai.reject_proposal_audit_mutation()`, and the `ai` schema. Do not roll back after recording production decisions unless legal, retention, and audit requirements have been reviewed and documented.

## Deferred work

This migration is forward-only in automated environments. An operator-approved rollback must export and verify proposal and decision evidence before dropping `ai.proposal_decision_events`, `ai.proposal_audit_records`, `ai.reject_proposal_audit_mutation()`, and the `ai` schema. Do not roll back after recording production decisions unless legal, retention, and audit requirements have been reviewed and documented.
External model transport, prompt and context redaction, policy evaluation, model-quality evaluation, multi-secret rotation windows, asymmetric workload identity, and separately authorized action execution remain independent reviewed capabilities. The audit service must not gain planning, calendar, habit, identity, notification, or generic command dependencies when those slices are added.
297 changes: 297 additions & 0 deletions apps/ai-service/src/ai-http-boundary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import { createHmac } from 'node:crypto';
import { HttpException } from '@nestjs/common';
import { describe, expect, it } from 'vitest';
import {
requireTrustedAiContext,
type TrustedAiContextHeaders,
} from './ai-http-boundary';

const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
const ACTOR_ID = '22222222-2222-4222-8222-222222222222';
const PROPOSAL_ID = '33333333-3333-4333-8333-333333333333';
const GATEWAY_SECRET = 'trusted-ai-gateway-context-secret-32-bytes';
const NOW_SECONDS = 1_785_806_400;

/** Creates the exact versioned HMAC expected by the AI service boundary. */
function signContext(
input: {
workspaceId?: string;
actorId?: string;
issuedAt?: string;
method?: string;
path?: string;
secret?: string;
} = {},
): string {
const workspaceId = (input.workspaceId ?? WORKSPACE_ID).toLowerCase();
const actorId = (input.actorId ?? ACTOR_ID).toLowerCase();
const issuedAt = input.issuedAt ?? String(NOW_SECONDS);
const method = input.method ?? 'POST';
const path = input.path ?? '/v1/proposals';
const secret = input.secret ?? GATEWAY_SECRET;
return createHmac('sha256', secret)
.update(
`life-os.ai-context.v1\n${workspaceId}\n${actorId}\n${issuedAt}\n${method}\n${path}`,
'utf8',
)
.digest('base64url');
}

/** Returns one complete context header object with optional field overrides. */
function contextHeaders(
overrides: Partial<TrustedAiContextHeaders> = {},
): TrustedAiContextHeaders {
return {
workspaceId: WORKSPACE_ID,
actorId: ACTOR_ID,
issuedAt: String(NOW_SECONDS),
signature: signContext(),
...overrides,
};
}

/** Asserts one stable credential-free problem response. */
function expectProblem(
operation: () => unknown,
expected: { status: number; title: string; code: string },
): void {
try {
operation();
throw new Error('Expected trusted AI context validation to fail');
} catch (error) {
expect(error).toBeInstanceOf(HttpException);
const exception = error as HttpException;
expect(exception.getStatus()).toBe(expected.status);
expect(exception.getResponse()).toEqual({
type: 'about:blank',
...expected,
});
}
}

describe('trusted AI service context', () => {
it('accepts an exact fresh method-and-path-bound context', () => {
expect(
requireTrustedAiContext(
contextHeaders({
workspaceId: WORKSPACE_ID.toUpperCase(),
actorId: ACTOR_ID.toUpperCase(),
}),
GATEWAY_SECRET,
'POST',
'/v1/proposals',
NOW_SECONDS,
),
).toEqual({ workspaceId: WORKSPACE_ID, actorId: ACTOR_ID });
});

it('accepts proposal and decision paths at the documented time boundaries', () => {
for (const input of [
{
issuedAt: NOW_SECONDS - 60,
method: 'GET',
path: `/v1/proposals/${PROPOSAL_ID}`,
},
{
issuedAt: NOW_SECONDS + 5,
method: 'POST',
path: `/v1/proposals/${PROPOSAL_ID}/decisions`,
},
] as const) {
const issuedAt = String(input.issuedAt);
expect(
requireTrustedAiContext(
contextHeaders({
issuedAt,
signature: signContext({
issuedAt,
method: input.method,
path: input.path,
}),
}),
GATEWAY_SECRET,
input.method,
input.path,
NOW_SECONDS,
),
).toEqual({ workspaceId: WORKSPACE_ID, actorId: ACTOR_ID });
}
});

it.each([
undefined,
null,
'',
'too-short',
'x'.repeat(4097),
`x${String.fromCharCode(0)}y`,
])('fails closed when the gateway secret is unavailable: %#', (secret) => {
expectProblem(
() =>
requireTrustedAiContext(
contextHeaders(),
secret,
'POST',
'/v1/proposals',
NOW_SECONDS,
),
{
title: 'Trusted gateway context is unavailable',
status: 503,
code: 'gateway_context_unavailable',
},
);
});

it.each([
{ field: 'workspaceId', value: undefined },
{ field: 'workspaceId', value: 'workspace-a' },
{ field: 'actorId', value: null },
{ field: 'actorId', value: 'actor-a' },
{ field: 'issuedAt', value: `0${NOW_SECONDS}` },
{ field: 'issuedAt', value: 'not-a-time' },
{ field: 'issuedAt', value: '12345678901234' },
{ field: 'signature', value: undefined },
{ field: 'signature', value: 'invalid' },
{ field: 'signature', value: `${'a'.repeat(42)}!` },
])('rejects malformed context field $field: %#', ({ field, value }) => {
expectProblem(
() =>
requireTrustedAiContext(
contextHeaders({ [field]: value }),
GATEWAY_SECRET,
'POST',
'/v1/proposals',
NOW_SECONDS,
),
{
title: 'Trusted gateway context is invalid',
status: 401,
code: 'invalid_gateway_context',
},
);
});

it.each([
{ issuedAt: NOW_SECONDS - 61, nowSeconds: NOW_SECONDS },
{ issuedAt: NOW_SECONDS + 6, nowSeconds: NOW_SECONDS },
{ issuedAt: NOW_SECONDS, nowSeconds: -1 },
{ issuedAt: NOW_SECONDS, nowSeconds: Number.MAX_SAFE_INTEGER + 1 },
])(
'rejects stale, future, or invalid clock input %#',
({ issuedAt, nowSeconds }) => {
const issuedAtText = String(issuedAt);
expectProblem(
() =>
requireTrustedAiContext(
contextHeaders({
issuedAt: issuedAtText,
signature: signContext({ issuedAt: issuedAtText }),
}),
GATEWAY_SECRET,
'POST',
'/v1/proposals',
nowSeconds,
),
{
title: 'Trusted gateway context is invalid',
status: 401,
code: 'invalid_gateway_context',
},
);
},
);

it.each([
{ method: 'post', path: '/v1/proposals' },
{ method: 'PUT', path: '/v1/proposals' },
{ method: 42, path: '/v1/proposals' },
{ method: 'GET', path: '/v1/proposals/' },
{ method: 'GET', path: '/v1/proposals?workspace=other' },
{ method: 'GET', path: `/v1/proposals/${PROPOSAL_ID.toUpperCase()}` },
{ method: 'GET', path: '/v1/proposals/not-a-uuid' },
{ method: 'GET', path: '/v1/proposals/decisions' },
{ method: 'GET', path: '/v1/proposals\n' },
{ method: 'GET', path: 42 },
])('rejects noncanonical method or path %#', ({ method, path }) => {
expectProblem(
() =>
requireTrustedAiContext(
contextHeaders(),
GATEWAY_SECRET,
method,
path,
NOW_SECONDS,
),
{
title: 'Trusted gateway context is invalid',
status: 401,
code: 'invalid_gateway_context',
},
);
});

it('rejects a noncanonical base64url spelling of a 32-byte signature', () => {
const alphabet =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
const canonical = signContext();
const finalIndex = alphabet.indexOf(canonical[canonical.length - 1]!);
expect(finalIndex).toBeGreaterThanOrEqual(0);
expect(finalIndex % 4).toBe(0);
const alternateFinalCharacter = alphabet[finalIndex + 1]!;
expect(alternateFinalCharacter).toBeDefined();
const noncanonical = `${canonical.slice(0, -1)}${alternateFinalCharacter}`;

expectProblem(
() =>
requireTrustedAiContext(
contextHeaders({ signature: noncanonical }),
GATEWAY_SECRET,
'POST',
'/v1/proposals',
NOW_SECONDS,
),
{
title: 'Trusted gateway context is invalid',
status: 401,
code: 'invalid_gateway_context',
},
);
});

it.each([
{
method: 'GET',
path: '/v1/proposals',
signature: signContext({ method: 'POST' }),
},
{
method: 'POST',
path: `/v1/proposals/${PROPOSAL_ID}`,
signature: signContext({ method: 'POST', path: '/v1/proposals' }),
},
{
method: 'POST',
path: '/v1/proposals',
signature: signContext({
secret: 'another-gateway-secret-with-32-bytes',
}),
},
])('rejects method replay, path replay, or forged signature %#', (input) => {
expectProblem(
() =>
requireTrustedAiContext(
contextHeaders({ signature: input.signature }),
GATEWAY_SECRET,
input.method,
input.path,
NOW_SECONDS,
),
{
title: 'Trusted gateway context is invalid',
status: 401,
code: 'invalid_gateway_context',
},
);
});
});
Loading
Loading