-
Notifications
You must be signed in to change notification settings - Fork 0
feat(planning): add durable Today synchronization #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8b1da24
d371945
0b49064
7b612f1
a0683aa
d59254c
68eebee
f96671e
74304cd
ab09f5d
32774fa
185c3b6
c22afed
864eaea
bf04a5f
a202eae
fdc2ef0
ec00594
89a5065
8e0758e
ce60e74
28df9b5
83df8e2
f9653ce
28f8a27
1f0754a
ac0218f
8edf7f3
02f9a28
8cc9d42
c6303a5
64218f4
2c15d21
71a3ae4
ea633c3
bb06f3a
3c46feb
c3fa5a7
607ddb4
3b093be
4e473d4
0eadfb6
d77e30c
9727606
79555c3
a9ae43d
57110d1
a5a2586
86e9507
5939d11
4f1c251
24dd54e
c087cdf
ec3235b
eb13ee3
a30e0b2
1df3a2d
451ffcb
3ca84c6
ceb638e
6807483
0984658
19aaa88
e9602d6
be5e6d9
f8575d7
cb69c71
931b783
67f2e96
386d631
e38cdc4
aed4878
8ec3175
17a0de7
2714b6d
e53349e
09d65b9
518fdce
e9a68b1
935a8b9
bc3af48
12de7f7
ece37c5
f5f57eb
d73afed
945ada5
e4076fc
2250163
23f768b
c3b7f21
f3f2fcf
ea52117
a9b4b9d
14e700f
fec9ce3
a8e8361
a5f53a2
a4213da
37f91b7
e490723
b3a4021
8b928af
5767be7
2969193
39fc503
63842d0
55d9a51
2cd8c76
9e2ac42
d9002ae
9cc7f5e
297057a
8260c6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| CREATE TABLE identity.data_rights_requests ( | ||
| request_id uuid PRIMARY KEY, | ||
| -- Deliberately not a foreign key: completed request evidence must survive | ||
| -- source-workspace erasure for bounded audit and reconciliation retention. | ||
| workspace_id uuid NOT NULL, | ||
| -- Deliberately not a foreign key: erasing the identity source record must | ||
| -- neither delete this receipt nor make user erasure impossible. | ||
| requested_by_user_id uuid NOT NULL, | ||
| request_kind text NOT NULL, | ||
| idempotency_key uuid NOT NULL, | ||
| request_digest character(64) NOT NULL, | ||
| request_status text NOT NULL DEFAULT 'pending', | ||
| receipt_digest character(64), | ||
| requested_at timestamptz NOT NULL, | ||
| completed_at timestamptz, | ||
| CONSTRAINT data_rights_request_kind_valid | ||
| CHECK (request_kind IN ('export', 'erasure')), | ||
| CONSTRAINT data_rights_request_digest_valid | ||
| CHECK (request_digest ~ '^[0-9a-f]{64}$'), | ||
| CONSTRAINT data_rights_request_status_valid | ||
| CHECK (request_status IN ('pending', 'completed')), | ||
| CONSTRAINT data_rights_receipt_digest_valid | ||
| CHECK (receipt_digest IS NULL OR receipt_digest ~ '^[0-9a-f]{64}$'), | ||
| CONSTRAINT data_rights_request_completion_consistent | ||
| CHECK ( | ||
| (request_status = 'pending' AND receipt_digest IS NULL AND completed_at IS NULL) | ||
| OR | ||
| (request_status = 'completed' AND receipt_digest IS NOT NULL AND completed_at IS NOT NULL) | ||
| ), | ||
| CONSTRAINT data_rights_request_time_order | ||
| CHECK (completed_at IS NULL OR completed_at >= requested_at), | ||
| CONSTRAINT data_rights_workspace_idempotency_unique | ||
| UNIQUE (workspace_id, idempotency_key) | ||
| ); | ||
|
|
||
| CREATE FUNCTION identity.preserve_completed_data_rights_receipt() | ||
| RETURNS trigger | ||
| LANGUAGE plpgsql | ||
| AS $$ | ||
| BEGIN | ||
| IF OLD.request_status = 'completed' | ||
| AND ( | ||
| NEW.request_status IS DISTINCT FROM OLD.request_status | ||
| OR NEW.receipt_digest IS DISTINCT FROM OLD.receipt_digest | ||
| OR NEW.completed_at IS DISTINCT FROM OLD.completed_at | ||
| ) THEN | ||
| RETURN NULL; | ||
| END IF; | ||
| RETURN NEW; | ||
| END; | ||
| $$; | ||
|
|
||
| CREATE TRIGGER data_rights_receipt_immutable_guard | ||
| BEFORE UPDATE ON identity.data_rights_requests | ||
| FOR EACH ROW | ||
| EXECUTE FUNCTION identity.preserve_completed_data_rights_receipt(); | ||
|
|
||
| CREATE INDEX data_rights_requests_workspace_time_idx | ||
| ON identity.data_rights_requests (workspace_id, requested_at DESC); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| const SESSION_BODY = Object.freeze({ | ||
| sessionId: '11111111-1111-4111-8111-111111111111', | ||
| userId: '22222222-2222-4222-8222-222222222222', | ||
| workspaceId: '33333333-3333-4333-8333-333333333333', | ||
| authenticatedAt: '2026-08-09T17:55:00.000Z', | ||
| createdAt: '2026-08-09T17:56:00.000Z', | ||
| expiresAt: '2026-08-10T17:56:00.000Z', | ||
| }); | ||
|
|
||
| interface AuthenticatedApplicationConstructor { | ||
| new ( | ||
| sessions: { | ||
| introspectSession(cookieHeader: string | undefined): Promise<{ | ||
| statusCode: 200; | ||
| body: typeof SESSION_BODY; | ||
| }>; | ||
| }, | ||
| dataRights: { | ||
| exportWorkspace(context: { | ||
| readonly workspaceId: string; | ||
| readonly actorUserId: string; | ||
| }): Promise<unknown>; | ||
| }, | ||
| options: { | ||
| readonly now: () => Date; | ||
| readonly maximumAgeMs: number; | ||
| }, | ||
| ): { | ||
| exportWorkspace(cookieHeader: string | undefined): Promise<unknown>; | ||
| }; | ||
| } | ||
|
|
||
| async function applicationConstructor(): Promise<AuthenticatedApplicationConstructor> { | ||
| const modulePath = './data-rights-authenticated-application'; | ||
| const module = (await import(modulePath).catch(() => ({}))) as Readonly< | ||
| Record<string, unknown> | ||
| >; | ||
| expect(typeof module.AuthenticatedDataRightsApplication).toBe('function'); | ||
| return module.AuthenticatedDataRightsApplication as AuthenticatedApplicationConstructor; | ||
| } | ||
|
|
||
| describe('AuthenticatedDataRightsApplication', () => { | ||
| it('derives export ownership only from the authenticated recent session', async () => { | ||
| const AuthenticatedDataRightsApplication = await applicationConstructor(); | ||
| const contexts: unknown[] = []; | ||
| const sessions = { | ||
| async introspectSession(cookieHeader: string | undefined) { | ||
| expect(cookieHeader).toBe('life_os_session=opaque-session'); | ||
| return { statusCode: 200 as const, body: SESSION_BODY }; | ||
| }, | ||
| }; | ||
| const dataRights = { | ||
| async exportWorkspace(context: { | ||
| readonly workspaceId: string; | ||
| readonly actorUserId: string; | ||
| }) { | ||
| contexts.push(context); | ||
| return { schemaVersion: 'life-os.data-export.v1' }; | ||
| }, | ||
| }; | ||
| const application = new AuthenticatedDataRightsApplication( | ||
| sessions, | ||
| dataRights, | ||
| { | ||
| now: () => new Date('2026-08-09T18:00:00.000Z'), | ||
| maximumAgeMs: 10 * 60 * 1000, | ||
| }, | ||
| ); | ||
|
|
||
| await expect( | ||
| application.exportWorkspace('life_os_session=opaque-session'), | ||
| ).resolves.toEqual({ schemaVersion: 'life-os.data-export.v1' }); | ||
| expect(contexts).toEqual([ | ||
| { | ||
| workspaceId: SESSION_BODY.workspaceId, | ||
| actorUserId: SESSION_BODY.userId, | ||
| }, | ||
| ]); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import type { DataRightsWorkspaceContext } from './data-rights'; | ||
| import { requireRecentAuthentication } from './oauth-http-boundary'; | ||
|
|
||
| interface SessionView { | ||
| readonly userId: string; | ||
| readonly workspaceId: string; | ||
| readonly authenticatedAt: string; | ||
| } | ||
|
|
||
| interface SessionIntrospectionApplication { | ||
| introspectSession(cookieHeader: string | undefined): Promise<{ | ||
| readonly statusCode: number; | ||
| readonly body: SessionView; | ||
| }>; | ||
| } | ||
|
|
||
| interface DataRightsExportApplication { | ||
| exportWorkspace(context: DataRightsWorkspaceContext): Promise<unknown>; | ||
| } | ||
|
|
||
| interface RecentAuthenticationOptions { | ||
| readonly now: () => Date; | ||
| readonly maximumAgeMs: number; | ||
| } | ||
|
|
||
| /** | ||
| * Establishes the authenticated application boundary for data-rights exports. | ||
| * Workspace and actor ownership are derived exclusively from the opaque session, | ||
| * and the request is rejected before data-rights work when authentication is stale. | ||
| */ | ||
| export class AuthenticatedDataRightsApplication { | ||
| constructor( | ||
| private readonly sessions: SessionIntrospectionApplication, | ||
| private readonly dataRights: DataRightsExportApplication, | ||
| private readonly options: RecentAuthenticationOptions, | ||
| ) {} | ||
|
|
||
| /** | ||
| * Exports the session-owned workspace after enforcing the configured recent-authentication window. | ||
| */ | ||
| async exportWorkspace(cookieHeader: string | undefined): Promise<unknown> { | ||
| const session = await this.sessions.introspectSession(cookieHeader); | ||
| if (session.statusCode !== 200) { | ||
| throw new Error('Authentication is required'); | ||
| } | ||
| requireRecentAuthentication({ | ||
| authenticatedAt: session.body.authenticatedAt, | ||
| now: this.options.now(), | ||
| maximumAgeMs: this.options.maximumAgeMs, | ||
| }); | ||
| return this.dataRights.exportWorkspace( | ||
| Object.freeze({ | ||
| workspaceId: session.body.workspaceId, | ||
| actorUserId: session.body.userId, | ||
| }), | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { describe, expect, it } from 'vitest'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import * as oauthBoundary from './oauth-http-boundary'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type RecentAuthenticationGate = (input: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly authenticatedAt: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly now: Date; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly maximumAgeMs: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) => string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function recentAuthenticationGate(): RecentAuthenticationGate { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const candidate = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| oauthBoundary as unknown as Readonly<Record<string, unknown>> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).requireRecentAuthentication; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(typeof candidate).toBe('function'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return candidate as RecentAuthenticationGate; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('data-rights recent authentication gate', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('accepts an authentication instant at the exact maximum age boundary', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requireRecentAuthentication = recentAuthenticationGate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireRecentAuthentication({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticatedAt: '2026-08-09T17:50:00.000Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now: new Date('2026-08-09T18:00:00.000Z'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maximumAgeMs: 10 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).toBe('2026-08-09T17:50:00.000Z'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('rejects a stale authentication instant even when the session itself is still valid', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requireRecentAuthentication = recentAuthenticationGate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireRecentAuthentication({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticatedAt: '2026-08-09T17:49:59.999Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now: new Date('2026-08-09T18:00:00.000Z'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maximumAgeMs: 10 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).toThrow('Recent authentication is required'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('fails closed on future, malformed, or invalid policy timestamps', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requireRecentAuthentication = recentAuthenticationGate(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireRecentAuthentication({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticatedAt: '2026-08-09T18:00:00.001Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now: new Date('2026-08-09T18:00:00.000Z'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maximumAgeMs: 10 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).toThrow('Authentication provenance is invalid'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireRecentAuthentication({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticatedAt: 'not-an-instant', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now: new Date('2026-08-09T18:00:00.000Z'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maximumAgeMs: 10 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).toThrow('Authentication provenance is invalid'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireRecentAuthentication({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticatedAt: '2026-08-09T17:55:00.000Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now: new Date('invalid'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maximumAgeMs: 10 * 60 * 1000, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).toThrow('Recent authentication policy is invalid'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(() => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requireRecentAuthentication({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticatedAt: '2026-08-09T17:55:00.000Z', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| now: new Date('2026-08-09T18:00:00.000Z'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| maximumAgeMs: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).toThrow('Recent authentication policy is invalid'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+43
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 정규화되지 않은 시각 문자열 사례를 추가하십시오.
💚 추가 사례 제안 expect(() =>
requireRecentAuthentication({
authenticatedAt: 'not-an-instant',
now: new Date('2026-08-09T18:00:00.000Z'),
maximumAgeMs: 10 * 60 * 1000,
}),
).toThrow('Authentication provenance is invalid');
+ expect(() =>
+ requireRecentAuthentication({
+ authenticatedAt: '2026-08-09T17:55:00Z',
+ now: new Date('2026-08-09T18:00:00.000Z'),
+ maximumAgeMs: 10 * 60 * 1000,
+ }),
+ ).toThrow('Authentication provenance is invalid');📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
실패 경로 테스트를 추가하십시오.
이 파일은 성공 경로만 검증합니다.
session.statusCode !== 200분기와 오래된 인증 거부 분기는 실행되지 않습니다. 두 분기는 이 경계의 보안 계약입니다. 또한 인증이 실패하면dataRights.exportWorkspace가 호출되지 않아야 한다는 점도 검증해야 합니다.As per coding guidelines: "Tests must prove realistic domain accuracy and failure behavior, not only mocked call counts." 및 "Packages that enforce coverage gates must retain 100% statement, branch, function, and line coverage."
💚 추가 테스트 제안
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Coding guidelines