From c0d60ed84efa561b9ca469fd42fa24c75674c2e9 Mon Sep 17 00:00:00 2001 From: Callan Logan Date: Fri, 29 May 2026 23:48:15 -0500 Subject: [PATCH] Fix auth JSON datetime decoding --- packages/contracts/src/auth.test.ts | 89 +++++++++++++++++++++++++++++ packages/contracts/src/auth.ts | 22 +++---- 2 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 packages/contracts/src/auth.test.ts diff --git a/packages/contracts/src/auth.test.ts b/packages/contracts/src/auth.test.ts new file mode 100644 index 00000000000..e539708ba2b --- /dev/null +++ b/packages/contracts/src/auth.test.ts @@ -0,0 +1,89 @@ +import * as Schema from "effect/Schema"; +import { describe, expect, it } from "vitest"; + +import { + AuthAccessSnapshot, + AuthBearerBootstrapResult, + AuthSessionState, + AuthWebSocketTokenResult, +} from "./auth.ts"; + +const decodeBearerBootstrap = Schema.decodeUnknownSync(AuthBearerBootstrapResult); +const encodeBearerBootstrap = Schema.encodeUnknownSync(AuthBearerBootstrapResult); +const decodeSessionState = Schema.decodeUnknownSync(AuthSessionState); +const decodeWebSocketToken = Schema.decodeUnknownSync(AuthWebSocketTokenResult); +const decodeAccessSnapshot = Schema.decodeUnknownSync(AuthAccessSnapshot); + +describe("auth contracts", () => { + it("decodes bearer bootstrap JSON timestamps from remote HTTP responses", () => { + const payload = { + authenticated: true, + role: "client", + sessionMethod: "bearer-session-token", + expiresAt: "2026-06-29T04:36:01.577Z", + sessionToken: "session-token", + }; + + const decoded = decodeBearerBootstrap(payload); + const encoded = encodeBearerBootstrap(decoded); + + expect(encoded).toEqual(payload); + }); + + it("decodes auth JSON timestamps across session and access payloads", () => { + const expiresAt = "2026-06-29T04:36:01.577Z"; + + expect( + decodeSessionState({ + authenticated: true, + auth: { + policy: "remote-reachable", + bootstrapMethods: ["one-time-token"], + sessionMethods: ["bearer-session-token"], + sessionCookieName: "t3_session_3773", + }, + role: "client", + sessionMethod: "bearer-session-token", + expiresAt, + }).authenticated, + ).toBe(true); + + expect( + decodeWebSocketToken({ + token: "ws-token", + expiresAt, + }), + ).toBeDefined(); + + expect( + decodeAccessSnapshot({ + pairingLinks: [ + { + id: "pairing-link", + credential: "pairing-credential", + role: "client", + subject: "pairing-subject", + createdAt: "2026-05-29T04:36:01.577Z", + expiresAt, + }, + ], + clientSessions: [ + { + sessionId: "session-id", + subject: "client-subject", + role: "client", + method: "bearer-session-token", + client: { + deviceType: "desktop", + }, + issuedAt: "2026-05-29T04:36:01.577Z", + expiresAt, + lastConnectedAt: null, + connected: false, + current: false, + }, + ], + }).clientSessions, + ).toHaveLength(1); + }); +}); diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index 8439d12b069..6fa86d9070e 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -2,6 +2,8 @@ import * as Schema from "effect/Schema"; import { AuthSessionId, TrimmedNonEmptyString } from "./baseSchemas.ts"; +const AuthDateTimeUtc = Schema.DateTimeUtcFromString; + /** * Declares the server's overall authentication posture. * @@ -109,7 +111,7 @@ export const AuthBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: ServerAuthSessionMethod, - expiresAt: Schema.DateTimeUtc, + expiresAt: AuthDateTimeUtc, }); export type AuthBootstrapResult = typeof AuthBootstrapResult.Type; @@ -117,14 +119,14 @@ export const AuthBearerBootstrapResult = Schema.Struct({ authenticated: Schema.Literal(true), role: AuthSessionRole, sessionMethod: Schema.Literal("bearer-session-token"), - expiresAt: Schema.DateTimeUtc, + expiresAt: AuthDateTimeUtc, sessionToken: TrimmedNonEmptyString, }); export type AuthBearerBootstrapResult = typeof AuthBearerBootstrapResult.Type; export const AuthWebSocketTokenResult = Schema.Struct({ token: TrimmedNonEmptyString, - expiresAt: Schema.DateTimeUtc, + expiresAt: AuthDateTimeUtc, }); export type AuthWebSocketTokenResult = typeof AuthWebSocketTokenResult.Type; @@ -132,7 +134,7 @@ export const AuthPairingCredentialResult = Schema.Struct({ id: TrimmedNonEmptyString, credential: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - expiresAt: Schema.DateTimeUtc, + expiresAt: AuthDateTimeUtc, }); export type AuthPairingCredentialResult = typeof AuthPairingCredentialResult.Type; @@ -142,8 +144,8 @@ export const AuthPairingLink = Schema.Struct({ role: AuthSessionRole, subject: TrimmedNonEmptyString, label: Schema.optionalKey(TrimmedNonEmptyString), - createdAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, + createdAt: AuthDateTimeUtc, + expiresAt: AuthDateTimeUtc, }); export type AuthPairingLink = typeof AuthPairingLink.Type; @@ -172,9 +174,9 @@ export const AuthClientSession = Schema.Struct({ role: AuthSessionRole, method: ServerAuthSessionMethod, client: AuthClientMetadata, - issuedAt: Schema.DateTimeUtc, - expiresAt: Schema.DateTimeUtc, - lastConnectedAt: Schema.NullOr(Schema.DateTimeUtc), + issuedAt: AuthDateTimeUtc, + expiresAt: AuthDateTimeUtc, + lastConnectedAt: Schema.NullOr(AuthDateTimeUtc), connected: Schema.Boolean, current: Schema.Boolean, }); @@ -261,6 +263,6 @@ export const AuthSessionState = Schema.Struct({ auth: ServerAuthDescriptor, role: Schema.optionalKey(AuthSessionRole), sessionMethod: Schema.optionalKey(ServerAuthSessionMethod), - expiresAt: Schema.optionalKey(Schema.DateTimeUtc), + expiresAt: Schema.optionalKey(AuthDateTimeUtc), }); export type AuthSessionState = typeof AuthSessionState.Type;