@skrr-ai/auth-core 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +21 -0
  2. package/dist/cjs/aead.d.ts +24 -0
  3. package/dist/cjs/aead.js +65 -0
  4. package/dist/cjs/authExpiredDetector.d.ts +65 -0
  5. package/dist/cjs/authExpiredDetector.js +89 -0
  6. package/dist/cjs/authHelper.d.ts +51 -0
  7. package/dist/cjs/authHelper.js +321 -0
  8. package/dist/cjs/credentialEnvelope.d.ts +85 -0
  9. package/dist/cjs/credentialEnvelope.js +230 -0
  10. package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
  11. package/dist/cjs/credentialEnvelopeBridge.js +637 -0
  12. package/dist/cjs/credentialResolver.d.ts +278 -0
  13. package/dist/cjs/credentialResolver.js +423 -0
  14. package/dist/cjs/daemonPair.d.ts +172 -0
  15. package/dist/cjs/daemonPair.js +495 -0
  16. package/dist/cjs/daemonScopes.d.ts +74 -0
  17. package/dist/cjs/daemonScopes.js +84 -0
  18. package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
  19. package/dist/cjs/deviceIdentityBridge.js +591 -0
  20. package/dist/cjs/deviceKey.d.ts +167 -0
  21. package/dist/cjs/deviceKey.js +332 -0
  22. package/dist/cjs/fdToken.d.ts +17 -0
  23. package/dist/cjs/fdToken.js +238 -0
  24. package/dist/cjs/harnessTrust.d.ts +72 -0
  25. package/dist/cjs/harnessTrust.js +103 -0
  26. package/dist/cjs/headers-helper.d.ts +25 -0
  27. package/dist/cjs/headers-helper.js +323 -0
  28. package/dist/cjs/index.d.ts +37 -0
  29. package/dist/cjs/index.js +305 -0
  30. package/dist/cjs/jwtUtils.d.ts +35 -0
  31. package/dist/cjs/jwtUtils.js +61 -0
  32. package/dist/cjs/kek/index.d.ts +89 -0
  33. package/dist/cjs/kek/index.js +208 -0
  34. package/dist/cjs/kek/linux.d.ts +92 -0
  35. package/dist/cjs/kek/linux.js +596 -0
  36. package/dist/cjs/kek/macos.d.ts +112 -0
  37. package/dist/cjs/kek/macos.js +599 -0
  38. package/dist/cjs/kek/types.d.ts +111 -0
  39. package/dist/cjs/kek/types.js +43 -0
  40. package/dist/cjs/kek/windows.d.ts +71 -0
  41. package/dist/cjs/kek/windows.js +350 -0
  42. package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
  43. package/dist/cjs/kek/zeroize-registry.js +63 -0
  44. package/dist/cjs/legacyStatePreflight.d.ts +67 -0
  45. package/dist/cjs/legacyStatePreflight.js +78 -0
  46. package/dist/cjs/localIdentity.d.ts +54 -0
  47. package/dist/cjs/localIdentity.js +57 -0
  48. package/dist/cjs/loginLocalhost.d.ts +115 -0
  49. package/dist/cjs/loginLocalhost.js +368 -0
  50. package/dist/cjs/loginWithLocalhost.d.ts +131 -0
  51. package/dist/cjs/loginWithLocalhost.js +359 -0
  52. package/dist/cjs/machineId.d.ts +35 -0
  53. package/dist/cjs/machineId.js +239 -0
  54. package/dist/cjs/messages.d.ts +9 -0
  55. package/dist/cjs/messages.js +44 -0
  56. package/dist/cjs/package.json +3 -0
  57. package/dist/cjs/pkce.d.ts +62 -0
  58. package/dist/cjs/pkce.js +158 -0
  59. package/dist/cjs/recoveryCode.d.ts +61 -0
  60. package/dist/cjs/recoveryCode.js +213 -0
  61. package/dist/cjs/refresh.d.ts +215 -0
  62. package/dist/cjs/refresh.js +877 -0
  63. package/dist/cjs/refreshClassification.d.ts +53 -0
  64. package/dist/cjs/refreshClassification.js +105 -0
  65. package/dist/cjs/refreshScheduler.d.ts +53 -0
  66. package/dist/cjs/refreshScheduler.js +332 -0
  67. package/dist/cjs/runtime.d.ts +149 -0
  68. package/dist/cjs/runtime.js +136 -0
  69. package/dist/cjs/spawnEnv.d.ts +76 -0
  70. package/dist/cjs/spawnEnv.js +153 -0
  71. package/dist/cjs/ttlParser.d.ts +54 -0
  72. package/dist/cjs/ttlParser.js +87 -0
  73. package/dist/cjs/types.d.ts +91 -0
  74. package/dist/cjs/types.js +26 -0
  75. package/dist/esm/aead.d.ts +24 -0
  76. package/dist/esm/aead.js +57 -0
  77. package/dist/esm/authExpiredDetector.d.ts +65 -0
  78. package/dist/esm/authExpiredDetector.js +85 -0
  79. package/dist/esm/authHelper.d.ts +51 -0
  80. package/dist/esm/authHelper.js +311 -0
  81. package/dist/esm/credentialEnvelope.d.ts +85 -0
  82. package/dist/esm/credentialEnvelope.js +213 -0
  83. package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
  84. package/dist/esm/credentialEnvelopeBridge.js +620 -0
  85. package/dist/esm/credentialResolver.d.ts +278 -0
  86. package/dist/esm/credentialResolver.js +414 -0
  87. package/dist/esm/daemonPair.d.ts +172 -0
  88. package/dist/esm/daemonPair.js +476 -0
  89. package/dist/esm/daemonScopes.d.ts +74 -0
  90. package/dist/esm/daemonScopes.js +80 -0
  91. package/dist/esm/deviceIdentityBridge.d.ts +113 -0
  92. package/dist/esm/deviceIdentityBridge.js +572 -0
  93. package/dist/esm/deviceKey.d.ts +167 -0
  94. package/dist/esm/deviceKey.js +320 -0
  95. package/dist/esm/fdToken.d.ts +17 -0
  96. package/dist/esm/fdToken.js +231 -0
  97. package/dist/esm/harnessTrust.d.ts +72 -0
  98. package/dist/esm/harnessTrust.js +97 -0
  99. package/dist/esm/headers-helper.d.ts +25 -0
  100. package/dist/esm/headers-helper.js +316 -0
  101. package/dist/esm/index.d.ts +37 -0
  102. package/dist/esm/index.js +144 -0
  103. package/dist/esm/jwtUtils.d.ts +35 -0
  104. package/dist/esm/jwtUtils.js +57 -0
  105. package/dist/esm/kek/index.d.ts +89 -0
  106. package/dist/esm/kek/index.js +162 -0
  107. package/dist/esm/kek/linux.d.ts +92 -0
  108. package/dist/esm/kek/linux.js +585 -0
  109. package/dist/esm/kek/macos.d.ts +112 -0
  110. package/dist/esm/kek/macos.js +586 -0
  111. package/dist/esm/kek/types.d.ts +111 -0
  112. package/dist/esm/kek/types.js +39 -0
  113. package/dist/esm/kek/windows.d.ts +71 -0
  114. package/dist/esm/kek/windows.js +344 -0
  115. package/dist/esm/kek/zeroize-registry.d.ts +44 -0
  116. package/dist/esm/kek/zeroize-registry.js +58 -0
  117. package/dist/esm/legacyStatePreflight.d.ts +67 -0
  118. package/dist/esm/legacyStatePreflight.js +71 -0
  119. package/dist/esm/localIdentity.d.ts +54 -0
  120. package/dist/esm/localIdentity.js +54 -0
  121. package/dist/esm/loginLocalhost.d.ts +115 -0
  122. package/dist/esm/loginLocalhost.js +355 -0
  123. package/dist/esm/loginWithLocalhost.d.ts +131 -0
  124. package/dist/esm/loginWithLocalhost.js +353 -0
  125. package/dist/esm/machineId.d.ts +35 -0
  126. package/dist/esm/machineId.js +231 -0
  127. package/dist/esm/messages.d.ts +9 -0
  128. package/dist/esm/messages.js +40 -0
  129. package/dist/esm/pkce.d.ts +62 -0
  130. package/dist/esm/pkce.js +148 -0
  131. package/dist/esm/recoveryCode.d.ts +61 -0
  132. package/dist/esm/recoveryCode.js +207 -0
  133. package/dist/esm/refresh.d.ts +215 -0
  134. package/dist/esm/refresh.js +863 -0
  135. package/dist/esm/refreshClassification.d.ts +53 -0
  136. package/dist/esm/refreshClassification.js +106 -0
  137. package/dist/esm/refreshScheduler.d.ts +53 -0
  138. package/dist/esm/refreshScheduler.js +329 -0
  139. package/dist/esm/runtime.d.ts +149 -0
  140. package/dist/esm/runtime.js +121 -0
  141. package/dist/esm/spawnEnv.d.ts +76 -0
  142. package/dist/esm/spawnEnv.js +149 -0
  143. package/dist/esm/ttlParser.d.ts +54 -0
  144. package/dist/esm/ttlParser.js +83 -0
  145. package/dist/esm/types.d.ts +91 -0
  146. package/dist/esm/types.js +21 -0
  147. package/package.json +82 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Shared types & error classes used across refresh, refreshScheduler, and
3
+ * fdToken modules. Lives in its own file so consumers can `import type` from
4
+ * the package without dragging in side-effecting modules.
5
+ */
6
+ export interface TokenPair {
7
+ /**
8
+ * Legacy daemon/cli/web access JWT. Null after Stage-E #1 step 4
9
+ * for daemon-scope refresh responses: the JWT-daemon substrate has
10
+ * been retired and only `opaqueAccessToken` is shipped. Non-daemon
11
+ * scopes (cli, web) still receive a non-null JWT here.
12
+ */
13
+ accessToken: string | null;
14
+ /** Epoch ms. */
15
+ accessExpiresAt: number;
16
+ refreshToken: string;
17
+ /** Epoch ms. */
18
+ refreshExpiresAt: number;
19
+ /**
20
+ * Stage-E #1 — opaque `osk_dmn_*` daemon access token co-issued
21
+ * with the JWT for daemon-scope sessions. Optional during the
22
+ * dark-ship migration window:
23
+ *
24
+ * - `undefined` / absent: server didn't co-issue (older API,
25
+ * non-daemon scope, or `OVERSKY_DMN_OPAQUE_DISABLED=1` set).
26
+ * Daemon authenticates with the JWT (legacy behavior).
27
+ * - `null`: server co-issued but the value is explicitly null
28
+ * (kill switch tripped mid-rotation, or write failed). Daemon
29
+ * callers MUST treat this as "clear any cached opaque slot
30
+ * so we don't keep using a stale opaque after its row was
31
+ * family-revoked".
32
+ * - `string`: opaque plaintext. Daemon stores in keychain and
33
+ * prefers over JWT for the next handshake (gated by
34
+ * `OVERSKY_PREFER_OPAQUE_DAEMON_TOKEN`, default on).
35
+ *
36
+ * Expires in lockstep with `accessExpiresAt` (server-side they're
37
+ * minted in the same transaction with identical TTL). The pair
38
+ * intentionally does NOT carry a separate `opaqueAccessExpiresAt`.
39
+ */
40
+ opaqueAccessToken?: string | null;
41
+ /**
42
+ * Production-hardening 2026-05-06 — soft backoff hint from the server
43
+ * indicating the fleet refresh-QPS is high and the daemon SHOULD
44
+ * defer its next proactive refresh by this many seconds (plus its
45
+ * own jitter). Distinct from a 429 Retry-After: this is advisory,
46
+ * the rotation already succeeded, and the daemon's correctness is
47
+ * not at risk if it ignores the hint. Honoring it lets a fleet
48
+ * stretch its refresh-cadence under load WITHOUT any rejected
49
+ * requests.
50
+ *
51
+ * Sourced from the response's `X-Refresh-Backoff` header (or
52
+ * `Retry-After` when present on a 200). Absent when the server
53
+ * isn't backpressuring or the header parser failed.
54
+ */
55
+ softBackoffSeconds?: number;
56
+ }
57
+ /**
58
+ * Server-emitted permanent-failure codes.
59
+ *
60
+ * Three groups, all permanent (no amount of retry helps):
61
+ * - REFRESH_* — refresh-token specific (most common path).
62
+ * - TOKEN_* — access-token specific (server pushes 4402 close;
63
+ * daemon classifies as TOKEN_INVALID/TOKEN_EXPIRED).
64
+ * - SESSION_REVOKED — admin/forensic revoke; whole family burned.
65
+ * - UNKNOWN_401 — server returned 401 with a code we don't
66
+ * recognize. Treated as permanent (better to prompt
67
+ * re-auth than to loop forever) but tagged
68
+ * distinctly so monitoring can surface server-side
69
+ * bugs separately from real expirations.
70
+ *
71
+ * Production-hardening 2026-05-06 — widened from the original 4-value
72
+ * union to include `TOKEN_INVALID`, `TOKEN_EXPIRED`, and `UNKNOWN_401`
73
+ * to match the codes the daemon and client UX actually emit/render.
74
+ * Without this, the client `describeAuthFailure` exhaustiveness check
75
+ * was silently broken for the non-REFRESH branches.
76
+ */
77
+ export type PermanentAuthReason = 'REFRESH_INVALID' | 'REFRESH_REUSED' | 'REFRESH_EXPIRED' | 'SESSION_REVOKED' | 'TOKEN_INVALID' | 'TOKEN_EXPIRED' | 'UNKNOWN_401';
78
+ export declare class PermanentAuthFailure extends Error {
79
+ readonly code: PermanentAuthReason;
80
+ constructor(code: PermanentAuthReason, message: string);
81
+ }
82
+ export declare class TransientAuthFailure extends Error {
83
+ readonly cause?: unknown;
84
+ constructor(message: string, cause?: unknown);
85
+ }
86
+ export interface ReauthState {
87
+ code: string;
88
+ message?: string;
89
+ /** Epoch ms. */
90
+ at: number;
91
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * Shared types & error classes used across refresh, refreshScheduler, and
4
+ * fdToken modules. Lives in its own file so consumers can `import type` from
5
+ * the package without dragging in side-effecting modules.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.TransientAuthFailure = exports.PermanentAuthFailure = void 0;
9
+ class PermanentAuthFailure extends Error {
10
+ code;
11
+ constructor(code, message) {
12
+ super(message);
13
+ this.name = 'PermanentAuthFailure';
14
+ this.code = code;
15
+ }
16
+ }
17
+ exports.PermanentAuthFailure = PermanentAuthFailure;
18
+ class TransientAuthFailure extends Error {
19
+ cause;
20
+ constructor(message, cause) {
21
+ super(message);
22
+ this.name = 'TransientAuthFailure';
23
+ this.cause = cause;
24
+ }
25
+ }
26
+ exports.TransientAuthFailure = TransientAuthFailure;
@@ -0,0 +1,24 @@
1
+ /** Key length in bytes (256 bits). */
2
+ export declare const AEAD_KEY_LEN = 32;
3
+ /** Nonce length in bytes (96-bit GCM standard). */
4
+ export declare const AEAD_NONCE_LEN = 12;
5
+ /** GCM authentication tag length in bytes (128 bits). */
6
+ export declare const AEAD_TAG_LEN = 16;
7
+ /** Result of an AEAD seal operation. */
8
+ export interface AeadSealed {
9
+ readonly nonce: Buffer;
10
+ readonly ct: Buffer;
11
+ readonly tag: Buffer;
12
+ }
13
+ /**
14
+ * Seal `plaintext` under `key` with optional `aad`. A fresh 96-bit nonce is
15
+ * generated per call — callers must never reuse nonces for the same key.
16
+ *
17
+ * Throws if `key` is not exactly 32 bytes.
18
+ */
19
+ export declare function aeadSeal(plaintext: Buffer, key: Buffer, aad?: Buffer): AeadSealed;
20
+ /**
21
+ * Open a sealed AEAD blob. Throws on any authentication failure (wrong key,
22
+ * wrong AAD, truncated ciphertext). Never silently returns garbage.
23
+ */
24
+ export declare function aeadOpen(sealed: AeadSealed, key: Buffer, aad?: Buffer): Buffer;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * aead.ts — shared AES-256-GCM primitive for auth-core envelope formats.
3
+ *
4
+ * CredentialEnvelope (L12 at-rest) uses AES-256-GCM. This module owns the
5
+ * bare cipher operation; framing, versioning, and serialization live in its
6
+ * envelope module.
7
+ */
8
+ import crypto from 'node:crypto';
9
+ /** AEAD algorithm. */
10
+ const ALG = 'aes-256-gcm';
11
+ /** Key length in bytes (256 bits). */
12
+ export const AEAD_KEY_LEN = 32;
13
+ /** Nonce length in bytes (96-bit GCM standard). */
14
+ export const AEAD_NONCE_LEN = 12;
15
+ /** GCM authentication tag length in bytes (128 bits). */
16
+ export const AEAD_TAG_LEN = 16;
17
+ /**
18
+ * Seal `plaintext` under `key` with optional `aad`. A fresh 96-bit nonce is
19
+ * generated per call — callers must never reuse nonces for the same key.
20
+ *
21
+ * Throws if `key` is not exactly 32 bytes.
22
+ */
23
+ export function aeadSeal(plaintext, key, aad) {
24
+ if (!Buffer.isBuffer(key) || key.length !== AEAD_KEY_LEN) {
25
+ throw new TypeError(`aeadSeal: key must be a ${AEAD_KEY_LEN}-byte Buffer`);
26
+ }
27
+ const nonce = crypto.randomBytes(AEAD_NONCE_LEN);
28
+ const cipher = crypto.createCipheriv(ALG, key, nonce);
29
+ if (aad && aad.length > 0)
30
+ cipher.setAAD(aad);
31
+ const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
32
+ const tag = cipher.getAuthTag();
33
+ return { nonce, ct, tag };
34
+ }
35
+ /**
36
+ * Open a sealed AEAD blob. Throws on any authentication failure (wrong key,
37
+ * wrong AAD, truncated ciphertext). Never silently returns garbage.
38
+ */
39
+ export function aeadOpen(sealed, key, aad) {
40
+ if (!Buffer.isBuffer(key) || key.length !== AEAD_KEY_LEN) {
41
+ throw new TypeError(`aeadOpen: key must be a ${AEAD_KEY_LEN}-byte Buffer`);
42
+ }
43
+ if (!Buffer.isBuffer(sealed.nonce) || sealed.nonce.length !== AEAD_NONCE_LEN) {
44
+ throw new Error('aeadOpen: invalid nonce length');
45
+ }
46
+ if (!Buffer.isBuffer(sealed.tag) || sealed.tag.length !== AEAD_TAG_LEN) {
47
+ throw new Error('aeadOpen: invalid auth tag length');
48
+ }
49
+ if (!Buffer.isBuffer(sealed.ct)) {
50
+ throw new Error('aeadOpen: ciphertext must be a Buffer');
51
+ }
52
+ const decipher = crypto.createDecipheriv(ALG, key, sealed.nonce);
53
+ if (aad && aad.length > 0)
54
+ decipher.setAAD(aad);
55
+ decipher.setAuthTag(sealed.tag);
56
+ return Buffer.concat([decipher.update(sealed.ct), decipher.final()]);
57
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * authExpiredDetector.ts — mid-stream auth-expired SSE detector.
3
+ *
4
+ * HTTP auth is checked BEFORE the body stream starts — a 401 mid-stream
5
+ * is technically impossible. But long-running SSE / Socket.IO sessions
6
+ * (chat with tool calls, agent steps, voice) can see the access token
7
+ * expire mid-body, because the server-side auth check fires per-event
8
+ * on every internal step. The server's contract: emit a terminal event
9
+ * with `type: 'error'` + `error.code === 'auth_expired'` (or the legacy
10
+ * forms `event: 'auth:expired'`, `event: 'auth_expired'`, `code:
11
+ * 'auth_expired'`) signalling the client to refresh and restart.
12
+ *
13
+ * This module recognises every documented shape and exposes a small
14
+ * orchestration helper that runs the caller-supplied refresher, then
15
+ * fires a user-visible message via the callbacks bag.
16
+ *
17
+ * Pure — no React, no fetch, no platform code. Suitable for daemon,
18
+ * desktop, web, and mobile clients. The mobile React Native client
19
+ * keeps a byte-equal mirror at
20
+ * `mobile/react-native/src/chat/streaming/authExpiredDetector.ts`
21
+ * (separate repo, can't import this package today). Treat that file
22
+ * as auth-tier canonical: any change here MUST be ported there.
23
+ */
24
+ /** Minimal shape of a parsed SSE event this module recognises. */
25
+ export interface ParsedSseLike {
26
+ type?: unknown;
27
+ event?: unknown;
28
+ error?: unknown;
29
+ code?: unknown;
30
+ }
31
+ /**
32
+ * Return true when `parsed` looks like the server's mid-stream auth
33
+ * expiry signal. The check is broad on purpose — multiple server
34
+ * generations use slightly different shapes and we want to catch them
35
+ * all rather than drop into the generic-error branch and confuse the
36
+ * user.
37
+ *
38
+ * Accepted shapes (any one is enough):
39
+ * - `{ type: 'error', error: { code: 'auth_expired' } }`
40
+ * - `{ event: 'auth:expired' }`
41
+ * - `{ event: 'auth_expired' }`
42
+ * - `{ code: 'auth_expired' }`
43
+ * - `{ type: 'error', error: 'auth_expired' }`
44
+ */
45
+ export declare function isAuthExpiredSseEvent(parsed: unknown): boolean;
46
+ /**
47
+ * Integration helper — call from the SSE parser when `isAuthExpiredSseEvent`
48
+ * returns true. Calls the supplied refresher, fires callbacks.onError
49
+ * with a user-visible message, and returns the new token (or null on
50
+ * failure) so the caller can decide whether to auto-retry.
51
+ *
52
+ * Pure in the sense that it does NOT drive a fetch / socket itself —
53
+ * that is the caller's responsibility, because retry semantics differ
54
+ * between SSE (re-POST) and Socket.IO (disconnect+reconnect).
55
+ */
56
+ export interface AuthExpiredCallbacks {
57
+ onError?: (message: string) => void;
58
+ }
59
+ export interface HandleMidStreamAuthExpiryResult {
60
+ /** True when the refresher returned a fresh token. */
61
+ refreshed: boolean;
62
+ /** The fresh token when refreshed===true, else null. */
63
+ token: string | null;
64
+ }
65
+ export declare function handleMidStreamAuthExpiry(callbacks: AuthExpiredCallbacks, refresher: () => Promise<string | null>): Promise<HandleMidStreamAuthExpiryResult>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * authExpiredDetector.ts — mid-stream auth-expired SSE detector.
3
+ *
4
+ * HTTP auth is checked BEFORE the body stream starts — a 401 mid-stream
5
+ * is technically impossible. But long-running SSE / Socket.IO sessions
6
+ * (chat with tool calls, agent steps, voice) can see the access token
7
+ * expire mid-body, because the server-side auth check fires per-event
8
+ * on every internal step. The server's contract: emit a terminal event
9
+ * with `type: 'error'` + `error.code === 'auth_expired'` (or the legacy
10
+ * forms `event: 'auth:expired'`, `event: 'auth_expired'`, `code:
11
+ * 'auth_expired'`) signalling the client to refresh and restart.
12
+ *
13
+ * This module recognises every documented shape and exposes a small
14
+ * orchestration helper that runs the caller-supplied refresher, then
15
+ * fires a user-visible message via the callbacks bag.
16
+ *
17
+ * Pure — no React, no fetch, no platform code. Suitable for daemon,
18
+ * desktop, web, and mobile clients. The mobile React Native client
19
+ * keeps a byte-equal mirror at
20
+ * `mobile/react-native/src/chat/streaming/authExpiredDetector.ts`
21
+ * (separate repo, can't import this package today). Treat that file
22
+ * as auth-tier canonical: any change here MUST be ported there.
23
+ */
24
+ /**
25
+ * Return true when `parsed` looks like the server's mid-stream auth
26
+ * expiry signal. The check is broad on purpose — multiple server
27
+ * generations use slightly different shapes and we want to catch them
28
+ * all rather than drop into the generic-error branch and confuse the
29
+ * user.
30
+ *
31
+ * Accepted shapes (any one is enough):
32
+ * - `{ type: 'error', error: { code: 'auth_expired' } }`
33
+ * - `{ event: 'auth:expired' }`
34
+ * - `{ event: 'auth_expired' }`
35
+ * - `{ code: 'auth_expired' }`
36
+ * - `{ type: 'error', error: 'auth_expired' }`
37
+ */
38
+ export function isAuthExpiredSseEvent(parsed) {
39
+ if (!parsed || typeof parsed !== 'object')
40
+ return false;
41
+ const p = parsed;
42
+ const eventStr = typeof p.event === 'string' ? p.event.toLowerCase() : '';
43
+ if (eventStr === 'auth:expired' || eventStr === 'auth_expired')
44
+ return true;
45
+ const codeStr = typeof p.code === 'string' ? p.code.toLowerCase() : '';
46
+ if (codeStr === 'auth_expired')
47
+ return true;
48
+ const typeStr = typeof p.type === 'string' ? p.type.toLowerCase() : '';
49
+ if (typeStr === 'error' && isAuthExpiredError(p.error)) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+ function isAuthExpiredError(err) {
55
+ if (!err)
56
+ return false;
57
+ if (typeof err === 'string')
58
+ return err.toLowerCase() === 'auth_expired';
59
+ if (typeof err !== 'object')
60
+ return false;
61
+ const maybe = err.code;
62
+ if (typeof maybe === 'string' && maybe.toLowerCase() === 'auth_expired') {
63
+ return true;
64
+ }
65
+ const message = err.message;
66
+ if (typeof message === 'string' && message.toLowerCase().includes('auth_expired')) {
67
+ return true;
68
+ }
69
+ return false;
70
+ }
71
+ export async function handleMidStreamAuthExpiry(callbacks, refresher) {
72
+ let token = null;
73
+ try {
74
+ token = await refresher();
75
+ }
76
+ catch {
77
+ token = null;
78
+ }
79
+ if (token) {
80
+ callbacks.onError?.('Your session was refreshed. Tap retry to resume the response.');
81
+ return { refreshed: true, token };
82
+ }
83
+ callbacks.onError?.('Session expired. Please sign in again.');
84
+ return { refreshed: false, token: null };
85
+ }
@@ -0,0 +1,51 @@
1
+ export interface HelperResult {
2
+ token: string;
3
+ /** ms-epoch absolute time. When omitted, callers default to TTL window. */
4
+ expiresAt?: number;
5
+ }
6
+ /**
7
+ * L6.3 — diagnostic taxonomy for the cold-cache failure sentinel.
8
+ *
9
+ * - 'disabled' — kill switch / config ruled out the helper before
10
+ * any I/O. Resolver: fall through (no sentinel).
11
+ * - 'invalid-path' — relative path, not-a-file, non-existent. The
12
+ * helper isn't really "configured" so the resolver
13
+ * falls through to the next slot.
14
+ * - 'not-executable'— exec-bit unset. Same fall-through semantics.
15
+ * - 'spawn-error' — Node failed to spawn (ENOMEM, EACCES at exec
16
+ * time, etc.). Counts as "helper attempted and
17
+ * failed" → sentinel.
18
+ * - 'timeout' — helper exceeded `OVERSKY_AUTH_HELPER_TIMEOUT_MS`
19
+ * and we SIGKILL'd it → sentinel.
20
+ * - 'non-zero-exit' — script exited with non-zero code → sentinel.
21
+ * - 'oversized' — stdout exceeded 64 KiB cap → sentinel.
22
+ * - 'empty-output' — script exited 0 with no stdout → sentinel.
23
+ * - 'parse-error' — JSON-shaped stdout but malformed, OR bare-token
24
+ * output had embedded whitespace → sentinel.
25
+ */
26
+ export type AuthHelperFailureReason = 'disabled' | 'invalid-path' | 'not-executable' | 'spawn-error' | 'timeout' | 'non-zero-exit' | 'oversized' | 'empty-output' | 'parse-error';
27
+ /**
28
+ * L6.3 — failure outcomes that should TRIGGER the cold-cache sentinel.
29
+ * Validation failures (`disabled`, `invalid-path`, `not-executable`) are
30
+ * deliberately excluded: those mean "helper isn't really wired up" and
31
+ * the resolver chain should fall through to the next slot, not fail
32
+ * closed.
33
+ *
34
+ * Exported as a `const` set so the resolver can do
35
+ * `SENTINEL_TRIGGERING_FAILURES.has(reason)` without re-encoding the
36
+ * policy in two places.
37
+ */
38
+ export declare const SENTINEL_TRIGGERING_FAILURES: ReadonlySet<AuthHelperFailureReason>;
39
+ export interface InvokeAuthHelperOptions {
40
+ /** Override the timeout (defaults to env or 10 s). */
41
+ timeoutMs?: number;
42
+ }
43
+ export declare function getLastAuthHelperFailureReason(): AuthHelperFailureReason | null;
44
+ /** @internal test-only — clear the failure-reason latch between cases. */
45
+ export declare function __resetAuthHelperFailureReasonForTest(): void;
46
+ /**
47
+ * Invoke the helper script and return its parsed token, or null on any
48
+ * failure. Never throws.
49
+ */
50
+ export declare function invokeAuthHelper(scriptPath: string, options?: InvokeAuthHelperOptions): Promise<HelperResult | null>;
51
+ export declare function getDefaultHelperTtlMs(): number;