@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.
- package/README.md +21 -0
- package/dist/cjs/aead.d.ts +24 -0
- package/dist/cjs/aead.js +65 -0
- package/dist/cjs/authExpiredDetector.d.ts +65 -0
- package/dist/cjs/authExpiredDetector.js +89 -0
- package/dist/cjs/authHelper.d.ts +51 -0
- package/dist/cjs/authHelper.js +321 -0
- package/dist/cjs/credentialEnvelope.d.ts +85 -0
- package/dist/cjs/credentialEnvelope.js +230 -0
- package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/cjs/credentialEnvelopeBridge.js +637 -0
- package/dist/cjs/credentialResolver.d.ts +278 -0
- package/dist/cjs/credentialResolver.js +423 -0
- package/dist/cjs/daemonPair.d.ts +172 -0
- package/dist/cjs/daemonPair.js +495 -0
- package/dist/cjs/daemonScopes.d.ts +74 -0
- package/dist/cjs/daemonScopes.js +84 -0
- package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
- package/dist/cjs/deviceIdentityBridge.js +591 -0
- package/dist/cjs/deviceKey.d.ts +167 -0
- package/dist/cjs/deviceKey.js +332 -0
- package/dist/cjs/fdToken.d.ts +17 -0
- package/dist/cjs/fdToken.js +238 -0
- package/dist/cjs/harnessTrust.d.ts +72 -0
- package/dist/cjs/harnessTrust.js +103 -0
- package/dist/cjs/headers-helper.d.ts +25 -0
- package/dist/cjs/headers-helper.js +323 -0
- package/dist/cjs/index.d.ts +37 -0
- package/dist/cjs/index.js +305 -0
- package/dist/cjs/jwtUtils.d.ts +35 -0
- package/dist/cjs/jwtUtils.js +61 -0
- package/dist/cjs/kek/index.d.ts +89 -0
- package/dist/cjs/kek/index.js +208 -0
- package/dist/cjs/kek/linux.d.ts +92 -0
- package/dist/cjs/kek/linux.js +596 -0
- package/dist/cjs/kek/macos.d.ts +112 -0
- package/dist/cjs/kek/macos.js +599 -0
- package/dist/cjs/kek/types.d.ts +111 -0
- package/dist/cjs/kek/types.js +43 -0
- package/dist/cjs/kek/windows.d.ts +71 -0
- package/dist/cjs/kek/windows.js +350 -0
- package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
- package/dist/cjs/kek/zeroize-registry.js +63 -0
- package/dist/cjs/legacyStatePreflight.d.ts +67 -0
- package/dist/cjs/legacyStatePreflight.js +78 -0
- package/dist/cjs/localIdentity.d.ts +54 -0
- package/dist/cjs/localIdentity.js +57 -0
- package/dist/cjs/loginLocalhost.d.ts +115 -0
- package/dist/cjs/loginLocalhost.js +368 -0
- package/dist/cjs/loginWithLocalhost.d.ts +131 -0
- package/dist/cjs/loginWithLocalhost.js +359 -0
- package/dist/cjs/machineId.d.ts +35 -0
- package/dist/cjs/machineId.js +239 -0
- package/dist/cjs/messages.d.ts +9 -0
- package/dist/cjs/messages.js +44 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pkce.d.ts +62 -0
- package/dist/cjs/pkce.js +158 -0
- package/dist/cjs/recoveryCode.d.ts +61 -0
- package/dist/cjs/recoveryCode.js +213 -0
- package/dist/cjs/refresh.d.ts +215 -0
- package/dist/cjs/refresh.js +877 -0
- package/dist/cjs/refreshClassification.d.ts +53 -0
- package/dist/cjs/refreshClassification.js +105 -0
- package/dist/cjs/refreshScheduler.d.ts +53 -0
- package/dist/cjs/refreshScheduler.js +332 -0
- package/dist/cjs/runtime.d.ts +149 -0
- package/dist/cjs/runtime.js +136 -0
- package/dist/cjs/spawnEnv.d.ts +76 -0
- package/dist/cjs/spawnEnv.js +153 -0
- package/dist/cjs/ttlParser.d.ts +54 -0
- package/dist/cjs/ttlParser.js +87 -0
- package/dist/cjs/types.d.ts +91 -0
- package/dist/cjs/types.js +26 -0
- package/dist/esm/aead.d.ts +24 -0
- package/dist/esm/aead.js +57 -0
- package/dist/esm/authExpiredDetector.d.ts +65 -0
- package/dist/esm/authExpiredDetector.js +85 -0
- package/dist/esm/authHelper.d.ts +51 -0
- package/dist/esm/authHelper.js +311 -0
- package/dist/esm/credentialEnvelope.d.ts +85 -0
- package/dist/esm/credentialEnvelope.js +213 -0
- package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/esm/credentialEnvelopeBridge.js +620 -0
- package/dist/esm/credentialResolver.d.ts +278 -0
- package/dist/esm/credentialResolver.js +414 -0
- package/dist/esm/daemonPair.d.ts +172 -0
- package/dist/esm/daemonPair.js +476 -0
- package/dist/esm/daemonScopes.d.ts +74 -0
- package/dist/esm/daemonScopes.js +80 -0
- package/dist/esm/deviceIdentityBridge.d.ts +113 -0
- package/dist/esm/deviceIdentityBridge.js +572 -0
- package/dist/esm/deviceKey.d.ts +167 -0
- package/dist/esm/deviceKey.js +320 -0
- package/dist/esm/fdToken.d.ts +17 -0
- package/dist/esm/fdToken.js +231 -0
- package/dist/esm/harnessTrust.d.ts +72 -0
- package/dist/esm/harnessTrust.js +97 -0
- package/dist/esm/headers-helper.d.ts +25 -0
- package/dist/esm/headers-helper.js +316 -0
- package/dist/esm/index.d.ts +37 -0
- package/dist/esm/index.js +144 -0
- package/dist/esm/jwtUtils.d.ts +35 -0
- package/dist/esm/jwtUtils.js +57 -0
- package/dist/esm/kek/index.d.ts +89 -0
- package/dist/esm/kek/index.js +162 -0
- package/dist/esm/kek/linux.d.ts +92 -0
- package/dist/esm/kek/linux.js +585 -0
- package/dist/esm/kek/macos.d.ts +112 -0
- package/dist/esm/kek/macos.js +586 -0
- package/dist/esm/kek/types.d.ts +111 -0
- package/dist/esm/kek/types.js +39 -0
- package/dist/esm/kek/windows.d.ts +71 -0
- package/dist/esm/kek/windows.js +344 -0
- package/dist/esm/kek/zeroize-registry.d.ts +44 -0
- package/dist/esm/kek/zeroize-registry.js +58 -0
- package/dist/esm/legacyStatePreflight.d.ts +67 -0
- package/dist/esm/legacyStatePreflight.js +71 -0
- package/dist/esm/localIdentity.d.ts +54 -0
- package/dist/esm/localIdentity.js +54 -0
- package/dist/esm/loginLocalhost.d.ts +115 -0
- package/dist/esm/loginLocalhost.js +355 -0
- package/dist/esm/loginWithLocalhost.d.ts +131 -0
- package/dist/esm/loginWithLocalhost.js +353 -0
- package/dist/esm/machineId.d.ts +35 -0
- package/dist/esm/machineId.js +231 -0
- package/dist/esm/messages.d.ts +9 -0
- package/dist/esm/messages.js +40 -0
- package/dist/esm/pkce.d.ts +62 -0
- package/dist/esm/pkce.js +148 -0
- package/dist/esm/recoveryCode.d.ts +61 -0
- package/dist/esm/recoveryCode.js +207 -0
- package/dist/esm/refresh.d.ts +215 -0
- package/dist/esm/refresh.js +863 -0
- package/dist/esm/refreshClassification.d.ts +53 -0
- package/dist/esm/refreshClassification.js +106 -0
- package/dist/esm/refreshScheduler.d.ts +53 -0
- package/dist/esm/refreshScheduler.js +329 -0
- package/dist/esm/runtime.d.ts +149 -0
- package/dist/esm/runtime.js +121 -0
- package/dist/esm/spawnEnv.d.ts +76 -0
- package/dist/esm/spawnEnv.js +149 -0
- package/dist/esm/ttlParser.d.ts +54 -0
- package/dist/esm/ttlParser.js +83 -0
- package/dist/esm/types.d.ts +91 -0
- package/dist/esm/types.js +21 -0
- 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;
|
package/dist/esm/aead.js
ADDED
|
@@ -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;
|