@tangle-network/agent-core 0.5.4 → 0.6.0

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.
@@ -146,6 +146,39 @@ declare function issueBatchScopedToken(signingSecret: string, payload: Omit<Batc
146
146
  * policy (route maps + authorization check) in the sidecar-auth package.
147
147
  */
148
148
  type SidecarCapability = "computer_use" | "read" | "debug" | "terminal" | "workspace" | "control" | "raw_input";
149
+ /**
150
+ * The accepted values of the `sst` claim. The exported union is derived from
151
+ * this list so the type a caller programs against and the set the verifier
152
+ * enforces cannot drift apart.
153
+ */
154
+ declare const SIDECAR_SID_SCOPES: readonly ["session", "project"];
155
+ /**
156
+ * What a sidecar token's `sid` claim holds, carried in the `sst` claim.
157
+ *
158
+ * `sid` is minted with two different meanings: for a session-runtime token it
159
+ * is a session id, and for a project-scoped read-only token it is a project
160
+ * ref. Nothing on the token distinguished them, so a consumer could not tell
161
+ * whether comparing `sid` against a session id in a request path was an
162
+ * authorization check or a guaranteed mismatch.
163
+ *
164
+ * A consumer must treat the claim's absence as "the meaning of `sid` is
165
+ * unknown, do not enforce a session comparison" — tokens minted before this
166
+ * claim existed carry no `sst`.
167
+ *
168
+ * A consumer must also treat any value other than `"session"` as "this is not
169
+ * a session token", and deny it on a session-addressed route. Reading the
170
+ * claim as `sst === "session" ? compare : allow` grants a project-scoped token
171
+ * the reach the comparison exists to remove.
172
+ *
173
+ * The verifier does not check that `sid` looks like what `sst` declares. That
174
+ * is a semantic judgement about ids this layer cannot make; it belongs to the
175
+ * consumer that knows the id namespaces.
176
+ *
177
+ * Adding a value here is a breaking change for verifiers: `verifySidecarToken`
178
+ * rejects an `sst` it does not recognise, so every verifier must ship the new
179
+ * value before any issuer stamps it.
180
+ */
181
+ type SidecarSidScope = (typeof SIDECAR_SID_SCOPES)[number];
149
182
  /**
150
183
  * Generate an Ed25519 key pair for per-session JWT signing.
151
184
  * Private key stays in orchestrator memory. Public key is injected into
@@ -172,6 +205,13 @@ declare function issueSidecarAccessToken(privateKey: string, payload: {
172
205
  pid: string;
173
206
  cid: string;
174
207
  sid?: string;
208
+ /**
209
+ * What `sid` holds. Stamp it whenever `sid` is set, so a consumer can tell
210
+ * a session-bound token from a project-bound one instead of guessing from
211
+ * the capability list. Omitting it leaves consumers unable to enforce a
212
+ * session comparison.
213
+ */
214
+ sst?: SidecarSidScope;
175
215
  /**
176
216
  * Capability allowlist. When present, the token is restricted to
177
217
  * routes whose required capability is in this list, as enforced by the
@@ -189,6 +229,11 @@ declare function verifySidecarToken(token: string, publicKey: string, containerI
189
229
  pid: string;
190
230
  cid: string;
191
231
  sid?: string;
232
+ /**
233
+ * What `sid` holds. Absent on tokens minted before the claim existed —
234
+ * treat that as "unknown", never as a licence to skip a check.
235
+ */
236
+ sst?: SidecarSidScope;
192
237
  typ: string;
193
238
  jti?: string;
194
239
  exp: number;
@@ -347,5 +392,5 @@ declare class ProductTokenIssuer {
347
392
  getTtlMinutes(tier?: "free" | "pro" | "enterprise"): number;
348
393
  }
349
394
  //#endregion
350
- export { type AuthProvider, BatchScopedTokenPayload, BatchScopedTokenPayloadSchema, CallbackAuth, ChannelAccessResult, CreateSessionRequest, CreateSessionRequestSchema, CreateSessionResponse, EnvTokenAuth, NoAuth, Product, ProductAuth, ProductAuthInfo, ProductSchema, ProductSecrets, ProductTokenIssuer, ProjectScopedTokenPayload, ProjectScopedTokenPayloadSchema, ReadTokenAuth, ReadTokenPayload, ReadTokenPayloadSchema, RefreshableTokenAuth, SendMessageRequest, SendMessageRequestSchema, SessionScopedTokenPayload, SessionScopedTokenPayloadSchema, SidecarAuth, SidecarCapability, StaticTokenAuth, TokenScope, TokenValidationResult, canBatchTokenAccessChannel, canProjectTokenAccessChannel, canSessionTokenAccessChannel, canTokenAccessChannel, decodeToken, extractProjectFromChannel, extractSessionFromChannel, generateApiKey, generateSecureToken, generateSidecarKeyPair, generateSigningSecret, getAllowedChannelPatterns, getTokenScope, getTokenTTL, hashApiKey, hashApiKeyWithSalt, isBatchScopedToken, isProjectScopedToken, isSessionScopedToken, isTokenExpiringSoon, issueBatchScopedToken, issueProjectScopedToken, issueReadToken, issueSessionScopedToken, issueSidecarAccessToken, validateChannelSubscription, validateTokenScope, verifyApiKey, verifyReadToken, verifySidecarToken };
395
+ export { type AuthProvider, BatchScopedTokenPayload, BatchScopedTokenPayloadSchema, CallbackAuth, ChannelAccessResult, CreateSessionRequest, CreateSessionRequestSchema, CreateSessionResponse, EnvTokenAuth, NoAuth, Product, ProductAuth, ProductAuthInfo, ProductSchema, ProductSecrets, ProductTokenIssuer, ProjectScopedTokenPayload, ProjectScopedTokenPayloadSchema, ReadTokenAuth, ReadTokenPayload, ReadTokenPayloadSchema, RefreshableTokenAuth, SendMessageRequest, SendMessageRequestSchema, SessionScopedTokenPayload, SessionScopedTokenPayloadSchema, SidecarAuth, SidecarCapability, SidecarSidScope, StaticTokenAuth, TokenScope, TokenValidationResult, canBatchTokenAccessChannel, canProjectTokenAccessChannel, canSessionTokenAccessChannel, canTokenAccessChannel, decodeToken, extractProjectFromChannel, extractSessionFromChannel, generateApiKey, generateSecureToken, generateSidecarKeyPair, generateSigningSecret, getAllowedChannelPatterns, getTokenScope, getTokenTTL, hashApiKey, hashApiKeyWithSalt, isBatchScopedToken, isProjectScopedToken, isSessionScopedToken, isTokenExpiringSoon, issueBatchScopedToken, issueProjectScopedToken, issueReadToken, issueSessionScopedToken, issueSidecarAccessToken, validateChannelSubscription, validateTokenScope, verifyApiKey, verifyReadToken, verifySidecarToken };
351
396
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/auth/channel-access.ts","../../src/auth/tokens.ts","../../src/auth/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/auth/channel-access.ts","../../src/auth/tokens.ts","../../src/auth/index.ts"],"mappings":"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"}
@@ -170,6 +170,12 @@ function jwtHeaderEddsa() {
170
170
  return jwtHeaderEddsaCache;
171
171
  }
172
172
  /**
173
+ * The accepted values of the `sst` claim. The exported union is derived from
174
+ * this list so the type a caller programs against and the set the verifier
175
+ * enforces cannot drift apart.
176
+ */
177
+ const SIDECAR_SID_SCOPES = ["session", "project"];
178
+ /**
173
179
  * Generate an Ed25519 key pair for per-session JWT signing.
174
180
  * Private key stays in orchestrator memory. Public key is injected into
175
181
  * the sidecar container. Even if the sidecar is fully compromised,
@@ -204,6 +210,10 @@ function generateSidecarKeyPair() {
204
210
  */
205
211
  function issueSidecarAccessToken(privateKey, payload, ttlMinutes) {
206
212
  if (!privateKey.includes("PRIVATE KEY")) throw new Error("issueSidecarAccessToken requires an Ed25519 private key (PEM). HMAC fallback has been removed — generate a key pair with generateSidecarKeyPair().");
213
+ if (payload.sst !== void 0) {
214
+ if (!SIDECAR_SID_SCOPES.includes(payload.sst)) throw new Error(`issueSidecarAccessToken: sst must be one of ${SIDECAR_SID_SCOPES.join(", ")}.`);
215
+ if (!payload.sid || payload.sid.trim().length === 0) throw new Error("issueSidecarAccessToken: sst describes the sid claim and cannot be set without a non-blank sid.");
216
+ }
207
217
  const now = Math.floor(Date.now() / 1e3);
208
218
  const { randomBytes, sign } = nodeCrypto();
209
219
  const jti = `${payload.cid}:${now}:${randomBytes(8).toString("hex")}`;
@@ -240,6 +250,10 @@ function verifySidecarToken(token, publicKey, containerId) {
240
250
  const now = Math.floor(Date.now() / 1e3);
241
251
  if (typeof payload.exp !== "number" || payload.exp < now) return null;
242
252
  if (typeof payload.sub !== "string" || payload.sub.length === 0) return null;
253
+ if (payload.sst !== void 0) {
254
+ if (!SIDECAR_SID_SCOPES.includes(payload.sst)) return null;
255
+ if (typeof payload.sid !== "string" || payload.sid.trim().length === 0) return null;
256
+ }
243
257
  return payload;
244
258
  } catch {
245
259
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/auth/tokens.ts","../../src/auth/channel-access.ts","../../src/auth/index.ts"],"sourcesContent":["/**\n * JWT Token Utilities\n *\n * Token generation and verification for multi-tenant auth.\n * - Read/product tokens: HMAC-SHA256 (symmetric, fast)\n * - Sidecar tokens: Ed25519 (asymmetric — sidecar cannot forge tokens)\n */\n\nimport type {\n BatchScopedTokenPayload,\n ProductAuthInfo,\n ProjectScopedTokenPayload,\n ReadTokenPayload,\n SessionScopedTokenPayload,\n TokenScope,\n TokenValidationResult,\n} from \"./types.js\";\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\ntype NodeCrypto = typeof import(\"node:crypto\");\nlet nodeCryptoCache: NodeCrypto | undefined;\n\n/**\n * Resolve Node's `crypto` lazily and synchronously. Signing, verification, and\n * key generation are server-only, but this module must stay importable in a\n * browser bundle — a static `import \"node:crypto\"` forces bundlers to resolve\n * the builtin at load and blanks the page. `process.getBuiltinModule` exists\n * only on a Node runtime (Node >= 22.3); in a browser `globalThis.process` is\n * undefined, so callers get a clear server-only error instead of a crash.\n */\nfunction nodeCrypto(): NodeCrypto {\n if (nodeCryptoCache) return nodeCryptoCache;\n const resolved = (\n globalThis as {\n process?: { getBuiltinModule?: (id: string) => unknown };\n }\n ).process?.getBuiltinModule?.(\"node:crypto\") as NodeCrypto | undefined;\n if (!resolved) {\n throw new Error(\n \"@tangle-network/agent-core/auth: token signing, verification, and key generation are server-only (Node.js crypto) and cannot run in a browser.\",\n );\n }\n nodeCryptoCache = resolved;\n return resolved;\n}\n\n/**\n * Generate a cryptographically secure random string.\n * @param prefix - Prefix for the generated string (e.g., \"orch_prod_\")\n * @param bytes - Number of random bytes (default: 32 = 256 bits)\n */\nexport function generateSecureToken(prefix: string, bytes = 32): string {\n return `${prefix}${base64UrlEncode(nodeCrypto().randomBytes(bytes))}`;\n}\n\n/**\n * Generate a product API key.\n */\nexport function generateApiKey(): string {\n return generateSecureToken(\"orch_prod_\");\n}\n\n/**\n * Generate a signing secret.\n */\nexport function generateSigningSecret(): string {\n return generateSecureToken(\"orch_sign_\");\n}\n\n/**\n * Base64URL encode (RFC 7515). Isomorphic: `btoa` + `TextEncoder` exist in Node\n * and browsers, so — unlike `Buffer` — this never blanks a browser page that\n * transitively imports the module.\n */\nfunction base64UrlEncode(data: string | Uint8Array): string {\n const bytes = typeof data === \"string\" ? textEncoder.encode(data) : data;\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/**\n * Base64URL decode (RFC 7515) to raw bytes.\n */\nfunction base64UrlToBytes(data: string): Uint8Array {\n const padded = data + \"=\".repeat((4 - (data.length % 4)) % 4);\n const binary = atob(padded.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\n/**\n * Base64URL decode to a UTF-8 string.\n */\nfunction base64UrlDecode(data: string): string {\n return textDecoder.decode(base64UrlToBytes(data));\n}\n\n/**\n * Create HMAC-SHA256 signature.\n */\nfunction createSignature(data: string, secret: string): string {\n return base64UrlEncode(\n nodeCrypto().createHmac(\"sha256\", secret).update(data).digest(),\n );\n}\n\n/**\n * Verify signature using timing-safe comparison on raw HMAC bytes.\n *\n * Decoding both sides to raw bytes before the compare strips the\n * length-of-base64url-string oracle: every legitimate signature\n * decodes to exactly 32 bytes (HMAC-SHA256 digest size), so a\n * length mismatch on the decoded buffer means malformed input,\n * not a partial match. Equivalent to the pattern used in\n * `products/sandbox/sdk/src/auth/tokens.ts`.\n */\nfunction verifySignature(\n data: string,\n signature: string,\n secret: string,\n): boolean {\n const { createHmac, timingSafeEqual } = nodeCrypto();\n const expectedBuf = createHmac(\"sha256\", secret).update(data).digest();\n let providedBuf: Uint8Array;\n try {\n providedBuf = base64UrlToBytes(signature);\n } catch {\n return false;\n }\n if (providedBuf.length !== expectedBuf.length) return false;\n try {\n return timingSafeEqual(providedBuf, expectedBuf);\n } catch {\n return false;\n }\n}\n\n/**\n * Base64URL-encoded JWT header `{\"alg\":\"HS256\",\"typ\":\"JWT\"}`. Computed on first\n * use (not at module load) and memoized.\n */\nlet jwtHeaderCache: string | undefined;\nfunction jwtHeader(): string {\n if (jwtHeaderCache === undefined) {\n jwtHeaderCache = base64UrlEncode(\n JSON.stringify({ alg: \"HS256\", typ: \"JWT\" }),\n );\n }\n return jwtHeaderCache;\n}\n\n/**\n * Issue a read token (JWT) for WebSocket authentication.\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload (without iat/exp, those are added)\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueReadToken(\n signingSecret: string,\n payload: Omit<ReadTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n const now = Math.floor(Date.now() / 1000);\n\n // Type assertion is safe here: payload already satisfies one branch of the\n // ReadTokenPayload union (minus iat/exp/typ), and we're adding those fields.\n // TypeScript can't infer which union branch we're creating from a spread.\n const fullPayload = {\n ...payload,\n typ: \"read\" as const,\n iat: now,\n exp: now + ttlMinutes * 60,\n } as ReadTokenPayload;\n\n const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));\n const data = `${jwtHeader()}.${encodedPayload}`;\n const signature = createSignature(data, signingSecret);\n\n return `${data}.${signature}`;\n}\n\n/**\n * Issue a session-scoped token (JWT) for WebSocket authentication.\n * Grants access to a single session's events.\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload with session ID\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueSessionScopedToken(\n signingSecret: string,\n payload: Omit<SessionScopedTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n return issueReadToken(signingSecret, payload, ttlMinutes);\n}\n\n/**\n * Issue a project-scoped token (JWT) for WebSocket authentication.\n * Grants access to all sessions within a single project.\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload with project ID\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueProjectScopedToken(\n signingSecret: string,\n payload: Omit<ProjectScopedTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n return issueReadToken(\n signingSecret,\n payload as Omit<ReadTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes,\n );\n}\n\n/**\n * Issue a batch-scoped token (JWT) for WebSocket authentication.\n * Grants access to multiple projects (organization-level access).\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload with project IDs array\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueBatchScopedToken(\n signingSecret: string,\n payload: Omit<BatchScopedTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n return issueReadToken(\n signingSecret,\n payload as Omit<ReadTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes,\n );\n}\n\n// Ed25519 JWT header (asymmetric — sidecar cannot forge tokens); computed on\n// first use, not at module load, so the module stays browser-importable.\nlet jwtHeaderEddsaCache: string | undefined;\nfunction jwtHeaderEddsa(): string {\n if (jwtHeaderEddsaCache === undefined) {\n jwtHeaderEddsaCache = base64UrlEncode(\n JSON.stringify({ alg: \"EdDSA\", typ: \"JWT\" }),\n );\n }\n return jwtHeaderEddsaCache;\n}\n\n/**\n * Capability strings carried in a sidecar token's `cap` claim. This is the\n * shared vocabulary the crypto layer stamps into the JWT (issueSidecarAccessToken)\n * and reads back out (verifySidecarToken). The route→capability authorization\n * policy that enforces these claims lives in the sidecar layer, not here.\n * Tokens with a `cap` claim are strictly limited to those capabilities; tokens\n * without a `cap` claim are full-scope (legacy orchestrator-internal use).\n *\n * Adding a new capability: extend this union, then update the enforcement\n * policy (route maps + authorization check) in the sidecar-auth package.\n */\nexport type SidecarCapability =\n | \"computer_use\"\n | \"read\"\n | \"debug\"\n | \"terminal\"\n | \"workspace\"\n | \"control\"\n | \"raw_input\";\n\n/**\n * Generate an Ed25519 key pair for per-session JWT signing.\n * Private key stays in orchestrator memory. Public key is injected into\n * the sidecar container. Even if the sidecar is fully compromised,\n * the attacker cannot forge JWTs for any container.\n *\n * @returns { privateKey, publicKey } as PEM strings\n */\nexport function generateSidecarKeyPair(): {\n privateKey: string;\n publicKey: string;\n} {\n const { privateKey, publicKey } = nodeCrypto().generateKeyPairSync(\n \"ed25519\",\n {\n publicKeyEncoding: { type: \"spki\", format: \"pem\" },\n privateKeyEncoding: { type: \"pkcs8\", format: \"pem\" },\n },\n );\n return { privateKey, publicKey };\n}\n\n/**\n * Issue a sidecar access token (JWT) using Ed25519 signing.\n * Scoped to a specific container via the required `cid` claim.\n *\n * @param privateKey - Ed25519 private key (PEM). If not provided, falls back to HMAC.\n * @param hmacFallbackSecret - HMAC secret for backward compat (used when no Ed25519 key)\n * @param payload - Token payload with container ID\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueSidecarAccessToken(\n privateKey: string,\n payload: {\n sub: string;\n pid: string;\n cid: string;\n sid?: string;\n /**\n * Capability allowlist. When present, the token is restricted to\n * routes whose required capability is in this list, as enforced by the\n * sidecar-auth policy layer. Absent = full scope (legacy\n * orchestrator-internal tokens).\n */\n cap?: SidecarCapability[];\n },\n ttlMinutes: number,\n): string {\n if (!privateKey.includes(\"PRIVATE KEY\")) {\n throw new Error(\n \"issueSidecarAccessToken requires an Ed25519 private key (PEM). \" +\n \"HMAC fallback has been removed — generate a key pair with generateSidecarKeyPair().\",\n );\n }\n\n const now = Math.floor(Date.now() / 1000);\n // jti (JWT ID) enables token revocation — orchestrator adds jti to blocklist\n // on sandbox delete, sidecar checks blocklist on every request.\n const { randomBytes, sign } = nodeCrypto();\n const jti = `${payload.cid}:${now}:${randomBytes(8).toString(\"hex\")}`;\n const fullPayload = {\n ...payload,\n typ: \"sidecar\" as const,\n jti,\n iat: now,\n exp: now + ttlMinutes * 60,\n };\n const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));\n const data = `${jwtHeaderEddsa()}.${encodedPayload}`;\n const signature = base64UrlEncode(\n sign(null, textEncoder.encode(data), privateKey),\n );\n return `${data}.${signature}`;\n}\n\n/**\n * Verify a sidecar JWT using Ed25519 public key. Rejects non-EdDSA tokens.\n * Returns decoded payload on success, null on failure.\n */\nexport function verifySidecarToken(\n token: string,\n publicKey: string,\n containerId: string,\n): {\n sub: string;\n pid: string;\n cid: string;\n sid?: string;\n typ: string;\n jti?: string;\n exp: number;\n cap?: SidecarCapability[];\n} | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n\n const headerJson = base64UrlDecode(parts[0]);\n const header = JSON.parse(headerJson);\n if (header.alg !== \"EdDSA\") return null;\n\n // SECURITY: verify signature BEFORE inspecting claims.\n // Parsing claims before verification creates a timing oracle that\n // leaks valid container IDs via early-return timing differences.\n const data = `${parts[0]}.${parts[1]}`;\n const signatureBuffer = base64UrlToBytes(parts[2]);\n const valid = nodeCrypto().verify(\n null,\n textEncoder.encode(data),\n publicKey,\n signatureBuffer,\n );\n if (!valid) return null;\n\n // Signature verified — now safe to parse and inspect claims\n const payload = JSON.parse(base64UrlDecode(parts[1]));\n\n if (payload.typ !== \"sidecar\") return null;\n if (typeof payload.jti !== \"string\" || payload.jti.length === 0) {\n return null;\n }\n const exactContainerMatch = payload.cid === containerId;\n const shortDockerIdMatch = isDockerShortIdMatch(payload.cid, containerId);\n if (!exactContainerMatch && !shortDockerIdMatch) return null;\n\n // Require exp — tokens without expiration are rejected (no immortal tokens)\n const now = Math.floor(Date.now() / 1000);\n if (typeof payload.exp !== \"number\" || payload.exp < now) return null;\n\n // Require non-empty sub and cid\n if (typeof payload.sub !== \"string\" || payload.sub.length === 0)\n return null;\n\n return payload;\n } catch {\n return null;\n }\n}\n\nfunction isDockerShortIdMatch(\n payloadCid: unknown,\n containerId: unknown,\n): boolean {\n if (typeof payloadCid !== \"string\" || typeof containerId !== \"string\") {\n return false;\n }\n if (!/^[a-f0-9]{64}$/i.test(payloadCid)) {\n return false;\n }\n if (!/^[a-f0-9]{12}$/i.test(containerId)) {\n return false;\n }\n return payloadCid.startsWith(containerId);\n}\n\n/**\n * Check if a token payload is session-scoped.\n * Session-scoped tokens have a sid claim and no projectId/projectIds.\n */\nexport function isSessionScopedToken(\n payload: ReadTokenPayload,\n): payload is SessionScopedTokenPayload {\n return (\n \"sid\" in payload &&\n typeof payload.sid === \"string\" &&\n payload.sid.length > 0 &&\n !(\"projectId\" in payload && payload.projectId) &&\n !(\"projectIds\" in payload && payload.projectIds)\n );\n}\n\n/**\n * Check if a token payload is project-scoped.\n * Project-scoped tokens have a projectId claim and no sid/projectIds.\n */\nexport function isProjectScopedToken(\n payload: ReadTokenPayload,\n): payload is ProjectScopedTokenPayload {\n return (\n \"projectId\" in payload &&\n typeof payload.projectId === \"string\" &&\n payload.projectId.length > 0 &&\n !(\"sid\" in payload && payload.sid) &&\n !(\"projectIds\" in payload && payload.projectIds)\n );\n}\n\n/**\n * Check if a token payload is batch-scoped.\n * Batch-scoped tokens have a projectIds claim and no sid/projectId.\n */\nexport function isBatchScopedToken(\n payload: ReadTokenPayload,\n): payload is BatchScopedTokenPayload {\n return (\n \"projectIds\" in payload &&\n Array.isArray(payload.projectIds) &&\n payload.projectIds.length > 0 &&\n !(\"sid\" in payload && payload.sid) &&\n !(\"projectId\" in payload && payload.projectId)\n );\n}\n\n/**\n * Get the scope of a token payload.\n * Returns null if the token has an invalid/ambiguous scope.\n */\nexport function getTokenScope(payload: ReadTokenPayload): TokenScope | null {\n if (isSessionScopedToken(payload)) return \"session\";\n if (isProjectScopedToken(payload)) return \"project\";\n if (isBatchScopedToken(payload)) return \"batch\";\n return null;\n}\n\n/**\n * Validate that a token payload has exactly one valid scope.\n * Returns an error message if invalid, null if valid.\n */\nexport function validateTokenScope(payload: ReadTokenPayload): string | null {\n const hasSid = \"sid\" in payload && payload.sid;\n const hasProjectId = \"projectId\" in payload && payload.projectId;\n const hasProjectIds =\n \"projectIds\" in payload &&\n Array.isArray(payload.projectIds) &&\n payload.projectIds.length > 0;\n\n const scopeCount = [hasSid, hasProjectId, hasProjectIds].filter(\n Boolean,\n ).length;\n\n if (scopeCount === 0) {\n return \"Token must have one of: sid (session), projectId (project), or projectIds (batch)\";\n }\n\n if (scopeCount > 1) {\n return \"Token cannot have multiple scope claims (sid, projectId, projectIds are mutually exclusive)\";\n }\n\n return null;\n}\n\n/**\n * Decode a JWT without verification (to extract claims for lookup).\n * Returns null if the token is malformed.\n */\nexport function decodeToken(token: string): ReadTokenPayload | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n\n const payload = JSON.parse(base64UrlDecode(parts[1]));\n return payload as ReadTokenPayload;\n } catch {\n return null;\n }\n}\n\n/**\n * Verify a read token against a product's signing secrets.\n *\n * @param token - The JWT token to verify\n * @param product - Product auth info containing secrets\n */\nexport function verifyReadToken(\n token: string,\n product: ProductAuthInfo,\n): TokenValidationResult {\n // Check product status\n if (product.status !== \"active\") {\n return {\n valid: false,\n error: \"Product is suspended\",\n errorCode: \"PRODUCT_SUSPENDED\",\n };\n }\n\n // Parse token\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return {\n valid: false,\n error: \"Invalid token format\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n const [header, encodedPayload, signature] = parts;\n const data = `${header}.${encodedPayload}`;\n\n // Pin algorithm to HS256 — defense-in-depth against alg-confusion\n // attacks that swap a forged header (e.g. `alg: \"none\"`) onto an\n // otherwise valid token. Fast path: when the header bytes match the\n // canonical `JWT_HEADER` verbatim, we skip the parse. Slow path:\n // decode the header and reject anything whose `alg` claim is not\n // `\"HS256\"`. Non-canonical-but-valid headers (e.g. `alg: \"HS256\"`\n // with an extra `kid` claim) fall through to signature verification,\n // which covers the actual header bytes — so any tampering with the\n // header still fails the signature check.\n if (header !== jwtHeader()) {\n let parsedAlg: string | null = null;\n try {\n const decoded = JSON.parse(base64UrlDecode(header)) as Record<\n string,\n unknown\n >;\n if (typeof decoded.alg === \"string\") parsedAlg = decoded.alg;\n } catch {\n // Header didn't decode cleanly; treat as TOKEN_INVALID below.\n }\n if (parsedAlg !== \"HS256\") {\n return {\n valid: false,\n error: \"Unsupported token algorithm\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n }\n\n // Verify signature with current secret\n let signatureValid = verifySignature(\n data,\n signature,\n product.secrets.current.secret,\n );\n\n // If failed and we have a previous secret in grace period, try it\n if (!signatureValid && product.secrets.previous) {\n if (Date.now() < product.secrets.previous.expires_at) {\n signatureValid = verifySignature(\n data,\n signature,\n product.secrets.previous.secret,\n );\n }\n }\n\n if (!signatureValid) {\n return {\n valid: false,\n error: \"Invalid signature\",\n errorCode: \"SIGNATURE_INVALID\",\n };\n }\n\n // Parse payload\n let payload: ReadTokenPayload;\n try {\n payload = JSON.parse(base64UrlDecode(encodedPayload));\n } catch {\n return {\n valid: false,\n error: \"Invalid token payload\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n // Check token type\n if (payload.typ !== \"read\") {\n return {\n valid: false,\n error: \"Not a read token\",\n errorCode: \"TOKEN_WRONG_TYPE\",\n };\n }\n\n // Check expiration\n const now = Math.floor(Date.now() / 1000);\n if (payload.exp < now) {\n return {\n valid: false,\n error: \"Token expired\",\n errorCode: \"TOKEN_EXPIRED\",\n };\n }\n\n // Check product ID matches\n if (payload.pid !== product.product_id) {\n return {\n valid: false,\n error: \"Product ID mismatch\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n // Validate token scope (must have exactly one of sid, projectId, projectIds)\n const scopeError = validateTokenScope(payload);\n if (scopeError) {\n return {\n valid: false,\n error: scopeError,\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n return {\n valid: true,\n payload,\n scope: getTokenScope(payload) ?? undefined,\n };\n}\n\n/**\n * Get time until token expires (in seconds).\n * Returns negative if expired.\n */\nexport function getTokenTTL(payload: ReadTokenPayload): number {\n const now = Math.floor(Date.now() / 1000);\n return payload.exp - now;\n}\n\n/**\n * Check if token is expiring soon (within buffer seconds).\n */\nexport function isTokenExpiringSoon(\n payload: ReadTokenPayload,\n bufferSeconds = 60,\n): boolean {\n return getTokenTTL(payload) <= bufferSeconds;\n}\n\n/**\n * Default salt for API key hashing (development only).\n * Production MUST set API_KEY_HASH_SALT environment variable.\n * WARNING: Changing the salt will invalidate all existing API key hashes.\n */\nconst DEFAULT_API_KEY_SALT = \"api_key_hash_salt\";\n\nlet _saltWarned = false;\n\n/**\n * Get the API key hash salt from environment.\n * Logs a warning if falling back to the weak default (development only).\n */\nfunction getApiKeyHashSalt(): string {\n if (typeof process !== \"undefined\" && process.env?.API_KEY_HASH_SALT) {\n return process.env.API_KEY_HASH_SALT;\n }\n if (\n !_saltWarned &&\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"production\"\n ) {\n _saltWarned = true;\n console.error(\n \"[SECURITY] API_KEY_HASH_SALT not set in production. \" +\n \"Using weak default salt. Set API_KEY_HASH_SALT to a cryptographically random value.\",\n );\n }\n return DEFAULT_API_KEY_SALT;\n}\n\n/**\n * Hash an API key for storage/lookup.\n * Uses HMAC-SHA256 for consistent, fast hashing.\n *\n * The salt can be configured via API_KEY_HASH_SALT environment variable.\n * WARNING: Changing the salt will invalidate all existing API key hashes.\n */\nexport function hashApiKey(apiKey: string): string {\n return nodeCrypto()\n .createHmac(\"sha256\", getApiKeyHashSalt())\n .update(apiKey)\n .digest(\"hex\");\n}\n\n/**\n * Hash an API key with an explicit salt.\n * Use this when you need to verify against a specific salt.\n */\nexport function hashApiKeyWithSalt(apiKey: string, salt: string): string {\n return nodeCrypto().createHmac(\"sha256\", salt).update(apiKey).digest(\"hex\");\n}\n\n/**\n * Verify an API key using timing-safe comparison.\n */\nexport function verifyApiKey(provided: string, stored: string): boolean {\n if (provided.length !== stored.length) {\n return false;\n }\n try {\n return nodeCrypto().timingSafeEqual(\n textEncoder.encode(provided),\n textEncoder.encode(stored),\n );\n } catch {\n return false;\n }\n}\n","/**\n * Channel Access Control\n *\n * Determines what channels a token can access based on its scope.\n * Used by SessionGateway to validate subscriptions and event delivery.\n *\n * Channel patterns:\n * - session:{sessionId} - Events for a specific session\n * - agent:{sessionId} - Agent events for a specific session\n * - project:{projectId} - All events for a project\n * - system - System-wide events (heartbeat, etc.)\n */\n\nimport {\n isBatchScopedToken,\n isProjectScopedToken,\n isSessionScopedToken,\n} from \"./tokens.js\";\nimport type {\n BatchScopedTokenPayload,\n ProjectScopedTokenPayload,\n ReadTokenPayload,\n SessionScopedTokenPayload,\n} from \"./types.js\";\n\n/**\n * Result of channel access check.\n */\nexport interface ChannelAccessResult {\n /** Whether access is allowed */\n allowed: boolean;\n /** Reason for denial (if not allowed) */\n reason?: string;\n}\n\n/**\n * Extract session ID from a session channel pattern.\n * Returns null if not a session channel.\n */\nexport function extractSessionFromChannel(channel: string): string | null {\n if (channel.startsWith(\"session:\")) {\n return channel.slice(8);\n }\n if (channel.startsWith(\"agent:\")) {\n return channel.slice(6);\n }\n return null;\n}\n\n/**\n * Extract project ID from a project channel pattern.\n * Returns null if not a project channel.\n */\nexport function extractProjectFromChannel(channel: string): string | null {\n if (channel.startsWith(\"project:\")) {\n return channel.slice(8);\n }\n return null;\n}\n\n/**\n * Check if a session-scoped token can access a channel.\n *\n * Session tokens can access:\n * - session:{their-sid}\n * - agent:{their-sid}\n * - system (always allowed)\n * - Wildcard patterns matching their session\n */\nexport function canSessionTokenAccessChannel(\n payload: SessionScopedTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // System channels are always allowed\n if (channel === \"system\" || channel.startsWith(\"system.\")) {\n return { allowed: true };\n }\n\n // Global wildcard requires higher scope\n if (channel === \"*\") {\n return {\n allowed: false,\n reason: \"Session-scoped tokens cannot subscribe to all channels\",\n };\n }\n\n // Check session channel match\n const sessionId = extractSessionFromChannel(channel);\n if (sessionId !== null) {\n if (sessionId === payload.sid) {\n return { allowed: true };\n }\n // Wildcard session channels not allowed for session tokens\n if (sessionId === \"*\") {\n return {\n allowed: false,\n reason: \"Session-scoped tokens cannot subscribe to all sessions\",\n };\n }\n return {\n allowed: false,\n reason: `Token only has access to session ${payload.sid}`,\n };\n }\n\n // Project channels require project scope\n if (channel.startsWith(\"project:\")) {\n return {\n allowed: false,\n reason: \"Session-scoped tokens cannot access project channels\",\n };\n }\n\n // Default: check if channel matches session pattern\n // Allow channels that don't have explicit scope (backward compatibility)\n return { allowed: true };\n}\n\n/**\n * Check if a project-scoped token can access a channel.\n *\n * Project tokens can access:\n * - project:{their-projectId}\n * - session:* (all sessions within project - requires validation by caller)\n * - agent:* (all agent events within project)\n * - system (always allowed)\n */\nexport function canProjectTokenAccessChannel(\n payload: ProjectScopedTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // System channels are always allowed\n if (channel === \"system\" || channel.startsWith(\"system.\")) {\n return { allowed: true };\n }\n\n // Global wildcard requires batch scope or higher\n if (channel === \"*\") {\n return {\n allowed: false,\n reason:\n \"Project-scoped tokens cannot subscribe to all channels across projects\",\n };\n }\n\n // Project channel access\n const projectId = extractProjectFromChannel(channel);\n if (projectId !== null) {\n if (projectId === payload.projectId) {\n return { allowed: true };\n }\n return {\n allowed: false,\n reason: `Token only has access to project ${payload.projectId}`,\n };\n }\n\n // Session channels - project tokens can access any session in their project\n // The actual validation that session belongs to project must be done by the caller\n // who has access to the session-to-project mapping\n if (channel.startsWith(\"session:\") || channel.startsWith(\"agent:\")) {\n // Wildcard session channels allowed for project tokens\n const sessionId = extractSessionFromChannel(channel);\n if (sessionId === \"*\") {\n return { allowed: true };\n }\n // Specific session - allowed but caller must verify session is in project\n return { allowed: true };\n }\n\n // Default: allow general channels (backward compatibility)\n return { allowed: true };\n}\n\n/**\n * Check if a batch-scoped token can access a channel.\n *\n * Batch tokens can access:\n * - project:{any of their projectIds}\n * - session:* (all sessions within their projects)\n * - agent:* (all agent events within their projects)\n * - system (always allowed)\n * - * (global wildcard - for organization-wide subscriptions)\n */\nexport function canBatchTokenAccessChannel(\n payload: BatchScopedTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // System channels are always allowed\n if (channel === \"system\" || channel.startsWith(\"system.\")) {\n return { allowed: true };\n }\n\n // Global wildcard allowed for batch tokens\n if (channel === \"*\") {\n return { allowed: true };\n }\n\n // Project channel access\n const projectId = extractProjectFromChannel(channel);\n if (projectId !== null) {\n // Wildcard project access allowed\n if (projectId === \"*\") {\n return { allowed: true };\n }\n // Specific project must be in the list\n if (payload.projectIds.includes(projectId)) {\n return { allowed: true };\n }\n return {\n allowed: false,\n reason: `Token does not have access to project ${projectId}`,\n };\n }\n\n // Session/agent channels - batch tokens can access any session in their projects\n // Caller must verify session belongs to one of the token's projects\n if (channel.startsWith(\"session:\") || channel.startsWith(\"agent:\")) {\n return { allowed: true };\n }\n\n // Default: allow general channels (backward compatibility)\n return { allowed: true };\n}\n\n/**\n * Check if a token can access a channel.\n * Dispatches to scope-specific functions based on token type.\n *\n * @param payload - The token payload\n * @param channel - The channel to check access for\n * @returns Access result with allowed boolean and optional reason\n */\nexport function canTokenAccessChannel(\n payload: ReadTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // Use type guards for clean scope detection and type narrowing\n if (isSessionScopedToken(payload)) {\n return canSessionTokenAccessChannel(payload, channel);\n }\n\n if (isProjectScopedToken(payload)) {\n return canProjectTokenAccessChannel(payload, channel);\n }\n\n if (isBatchScopedToken(payload)) {\n return canBatchTokenAccessChannel(payload, channel);\n }\n\n // Invalid token scope\n return {\n allowed: false,\n reason: \"Token has no valid scope (missing sid, projectId, or projectIds)\",\n };\n}\n\n/**\n * Get the list of allowed channel patterns for a token.\n * Useful for informing clients what they can subscribe to.\n *\n * @param payload - The token payload\n * @returns Array of allowed channel patterns\n */\nexport function getAllowedChannelPatterns(payload: ReadTokenPayload): string[] {\n const patterns: string[] = [\"system\", \"system.*\"];\n\n // Use type guards for clean scope detection\n if (isSessionScopedToken(payload)) {\n patterns.push(`session:${payload.sid}`, `agent:${payload.sid}`);\n return patterns;\n }\n\n if (isProjectScopedToken(payload)) {\n patterns.push(\n `project:${payload.projectId}`,\n \"session:*\", // Within project\n \"agent:*\", // Within project\n );\n return patterns;\n }\n\n if (isBatchScopedToken(payload)) {\n patterns.push(\n \"*\", // Global wildcard\n \"project:*\", // All projects in batch\n ...payload.projectIds.map((id) => `project:${id}`),\n \"session:*\",\n \"agent:*\",\n );\n return patterns;\n }\n\n return patterns;\n}\n\n/**\n * Validate channel subscription request.\n * Checks multiple channels and returns aggregated result.\n *\n * @param payload - The token payload\n * @param channels - Channels to validate\n * @returns Object with allowed channels and denied channels with reasons\n */\nexport function validateChannelSubscription(\n payload: ReadTokenPayload,\n channels: string[],\n): {\n allowed: string[];\n denied: Array<{ channel: string; reason: string }>;\n} {\n const allowed: string[] = [];\n const denied: Array<{ channel: string; reason: string }> = [];\n\n for (const channel of channels) {\n const result = canTokenAccessChannel(payload, channel);\n if (result.allowed) {\n allowed.push(channel);\n } else {\n denied.push({ channel, reason: result.reason ?? \"Access denied\" });\n }\n }\n\n return { allowed, denied };\n}\n","/**\n * Authentication Configuration\n *\n * Token management with refresh hooks and expiration handling.\n */\n\nimport { issueReadToken } from \"./tokens.js\";\n\n// Re-export channel access utilities\nexport * from \"./channel-access.js\";\n// Re-export shared auth providers (eliminates 185 lines of duplication with browser.ts)\nexport type { AuthProvider } from \"./shared-providers.js\";\nexport {\n CallbackAuth,\n NoAuth,\n ReadTokenAuth,\n RefreshableTokenAuth,\n StaticTokenAuth,\n} from \"./shared-providers.js\";\nexport * from \"./tokens.js\";\n// Re-export types and token utilities\nexport * from \"./types.js\";\n\n/**\n * Environment-based auth that reads from env vars.\n * Matches the server-side patterns used in sidecar/orchestrator.\n */\nexport class EnvTokenAuth {\n private envVar: string;\n private fallback?: string;\n\n constructor(options: { envVar: string; fallback?: string }) {\n this.envVar = options.envVar;\n this.fallback = options.fallback;\n }\n\n async getToken(): Promise<string | null> {\n // Works in Node.js; in browser, fallback would be used\n if (typeof process !== \"undefined\" && process.env) {\n return process.env[this.envVar] ?? this.fallback ?? null;\n }\n return this.fallback ?? null;\n }\n}\n\n/**\n * Pre-configured auth for sidecar connections.\n * Uses SIDECAR_AUTH_TOKEN env var (matches server expectation).\n */\nexport class SidecarAuth extends EnvTokenAuth {\n constructor(token?: string) {\n super({ envVar: \"SIDECAR_AUTH_TOKEN\", fallback: token });\n }\n}\n\n/**\n * Product-based auth for multi-tenant orchestrator connections.\n * Uses ORCHESTRATOR_API_SECRET_KEY env var.\n */\nexport class ProductAuth extends EnvTokenAuth {\n constructor(apiKey?: string) {\n super({ envVar: \"ORCHESTRATOR_API_SECRET_KEY\", fallback: apiKey });\n }\n}\n\n/**\n * Product token issuer for backend services.\n *\n * Use this in product backends to issue read tokens for WebSocket connections.\n *\n * @example\n * ```typescript\n * import { ProductTokenIssuer } from \"@tangle-network/agent-core\";\n *\n * const issuer = new ProductTokenIssuer({\n * productId: \"vibecode\",\n * signingSecret: process.env.ORCHESTRATOR_SIGNING_SECRET!,\n * ttlMinutes: { free: 15, pro: 240 },\n * });\n *\n * // Issue a token for a user session\n * const token = issuer.issue({\n * userId: \"user_123\",\n * sessionId: \"sess_abc\",\n * tier: \"pro\",\n * });\n *\n * // Return token to frontend for WebSocket connection\n * res.json({\n * readToken: token.token,\n * expiresAt: token.expiresAt,\n * websocketUrl: `wss://orchestrator.example.com/session?token=${token.token}`,\n * });\n * ```\n */\nexport class ProductTokenIssuer {\n private readonly productId: string;\n private readonly signingSecret: string;\n private readonly ttlMinutes: {\n free: number;\n pro: number;\n enterprise?: number;\n };\n\n constructor(config: {\n productId: string;\n signingSecret: string;\n /** TTL in minutes for each tier (default: { free: 15, pro: 240 }) */\n ttlMinutes?: { free?: number; pro?: number; enterprise?: number };\n }) {\n this.productId = config.productId;\n this.signingSecret = config.signingSecret;\n this.ttlMinutes = {\n free: config.ttlMinutes?.free ?? 15,\n pro: config.ttlMinutes?.pro ?? 240,\n enterprise: config.ttlMinutes?.enterprise ?? 480,\n };\n }\n\n /**\n * Issue a read token for a user session.\n */\n issue(params: {\n userId: string;\n sessionId: string;\n tier?: \"free\" | \"pro\" | \"enterprise\";\n sidecarId?: string;\n }): { token: string; expiresAt: number } {\n const tier = params.tier ?? \"free\";\n const ttl = this.ttlMinutes[tier] ?? this.ttlMinutes.free;\n\n const token = issueReadToken(\n this.signingSecret,\n {\n sub: params.userId,\n sid: params.sessionId,\n pid: this.productId,\n cid: params.sidecarId,\n },\n ttl,\n );\n\n const expiresAt = Math.floor(Date.now() / 1000) + ttl * 60;\n\n return { token, expiresAt };\n }\n\n /**\n * Get the TTL in minutes for a tier.\n */\n getTtlMinutes(tier: \"free\" | \"pro\" | \"enterprise\" = \"free\"): number {\n return this.ttlMinutes[tier] ?? this.ttlMinutes.free;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/auth/tokens.ts","../../src/auth/channel-access.ts","../../src/auth/index.ts"],"sourcesContent":["/**\n * JWT Token Utilities\n *\n * Token generation and verification for multi-tenant auth.\n * - Read/product tokens: HMAC-SHA256 (symmetric, fast)\n * - Sidecar tokens: Ed25519 (asymmetric — sidecar cannot forge tokens)\n */\n\nimport type {\n BatchScopedTokenPayload,\n ProductAuthInfo,\n ProjectScopedTokenPayload,\n ReadTokenPayload,\n SessionScopedTokenPayload,\n TokenScope,\n TokenValidationResult,\n} from \"./types.js\";\n\nconst textEncoder = new TextEncoder();\nconst textDecoder = new TextDecoder();\n\ntype NodeCrypto = typeof import(\"node:crypto\");\nlet nodeCryptoCache: NodeCrypto | undefined;\n\n/**\n * Resolve Node's `crypto` lazily and synchronously. Signing, verification, and\n * key generation are server-only, but this module must stay importable in a\n * browser bundle — a static `import \"node:crypto\"` forces bundlers to resolve\n * the builtin at load and blanks the page. `process.getBuiltinModule` exists\n * only on a Node runtime (Node >= 22.3); in a browser `globalThis.process` is\n * undefined, so callers get a clear server-only error instead of a crash.\n */\nfunction nodeCrypto(): NodeCrypto {\n if (nodeCryptoCache) return nodeCryptoCache;\n const resolved = (\n globalThis as {\n process?: { getBuiltinModule?: (id: string) => unknown };\n }\n ).process?.getBuiltinModule?.(\"node:crypto\") as NodeCrypto | undefined;\n if (!resolved) {\n throw new Error(\n \"@tangle-network/agent-core/auth: token signing, verification, and key generation are server-only (Node.js crypto) and cannot run in a browser.\",\n );\n }\n nodeCryptoCache = resolved;\n return resolved;\n}\n\n/**\n * Generate a cryptographically secure random string.\n * @param prefix - Prefix for the generated string (e.g., \"orch_prod_\")\n * @param bytes - Number of random bytes (default: 32 = 256 bits)\n */\nexport function generateSecureToken(prefix: string, bytes = 32): string {\n return `${prefix}${base64UrlEncode(nodeCrypto().randomBytes(bytes))}`;\n}\n\n/**\n * Generate a product API key.\n */\nexport function generateApiKey(): string {\n return generateSecureToken(\"orch_prod_\");\n}\n\n/**\n * Generate a signing secret.\n */\nexport function generateSigningSecret(): string {\n return generateSecureToken(\"orch_sign_\");\n}\n\n/**\n * Base64URL encode (RFC 7515). Isomorphic: `btoa` + `TextEncoder` exist in Node\n * and browsers, so — unlike `Buffer` — this never blanks a browser page that\n * transitively imports the module.\n */\nfunction base64UrlEncode(data: string | Uint8Array): string {\n const bytes = typeof data === \"string\" ? textEncoder.encode(data) : data;\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary)\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/, \"\");\n}\n\n/**\n * Base64URL decode (RFC 7515) to raw bytes.\n */\nfunction base64UrlToBytes(data: string): Uint8Array {\n const padded = data + \"=\".repeat((4 - (data.length % 4)) % 4);\n const binary = atob(padded.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n}\n\n/**\n * Base64URL decode to a UTF-8 string.\n */\nfunction base64UrlDecode(data: string): string {\n return textDecoder.decode(base64UrlToBytes(data));\n}\n\n/**\n * Create HMAC-SHA256 signature.\n */\nfunction createSignature(data: string, secret: string): string {\n return base64UrlEncode(\n nodeCrypto().createHmac(\"sha256\", secret).update(data).digest(),\n );\n}\n\n/**\n * Verify signature using timing-safe comparison on raw HMAC bytes.\n *\n * Decoding both sides to raw bytes before the compare strips the\n * length-of-base64url-string oracle: every legitimate signature\n * decodes to exactly 32 bytes (HMAC-SHA256 digest size), so a\n * length mismatch on the decoded buffer means malformed input,\n * not a partial match. Equivalent to the pattern used in\n * `products/sandbox/sdk/src/auth/tokens.ts`.\n */\nfunction verifySignature(\n data: string,\n signature: string,\n secret: string,\n): boolean {\n const { createHmac, timingSafeEqual } = nodeCrypto();\n const expectedBuf = createHmac(\"sha256\", secret).update(data).digest();\n let providedBuf: Uint8Array;\n try {\n providedBuf = base64UrlToBytes(signature);\n } catch {\n return false;\n }\n if (providedBuf.length !== expectedBuf.length) return false;\n try {\n return timingSafeEqual(providedBuf, expectedBuf);\n } catch {\n return false;\n }\n}\n\n/**\n * Base64URL-encoded JWT header `{\"alg\":\"HS256\",\"typ\":\"JWT\"}`. Computed on first\n * use (not at module load) and memoized.\n */\nlet jwtHeaderCache: string | undefined;\nfunction jwtHeader(): string {\n if (jwtHeaderCache === undefined) {\n jwtHeaderCache = base64UrlEncode(\n JSON.stringify({ alg: \"HS256\", typ: \"JWT\" }),\n );\n }\n return jwtHeaderCache;\n}\n\n/**\n * Issue a read token (JWT) for WebSocket authentication.\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload (without iat/exp, those are added)\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueReadToken(\n signingSecret: string,\n payload: Omit<ReadTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n const now = Math.floor(Date.now() / 1000);\n\n // Type assertion is safe here: payload already satisfies one branch of the\n // ReadTokenPayload union (minus iat/exp/typ), and we're adding those fields.\n // TypeScript can't infer which union branch we're creating from a spread.\n const fullPayload = {\n ...payload,\n typ: \"read\" as const,\n iat: now,\n exp: now + ttlMinutes * 60,\n } as ReadTokenPayload;\n\n const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));\n const data = `${jwtHeader()}.${encodedPayload}`;\n const signature = createSignature(data, signingSecret);\n\n return `${data}.${signature}`;\n}\n\n/**\n * Issue a session-scoped token (JWT) for WebSocket authentication.\n * Grants access to a single session's events.\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload with session ID\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueSessionScopedToken(\n signingSecret: string,\n payload: Omit<SessionScopedTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n return issueReadToken(signingSecret, payload, ttlMinutes);\n}\n\n/**\n * Issue a project-scoped token (JWT) for WebSocket authentication.\n * Grants access to all sessions within a single project.\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload with project ID\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueProjectScopedToken(\n signingSecret: string,\n payload: Omit<ProjectScopedTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n return issueReadToken(\n signingSecret,\n payload as Omit<ReadTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes,\n );\n}\n\n/**\n * Issue a batch-scoped token (JWT) for WebSocket authentication.\n * Grants access to multiple projects (organization-level access).\n *\n * @param signingSecret - The product's signing secret\n * @param payload - Token payload with project IDs array\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueBatchScopedToken(\n signingSecret: string,\n payload: Omit<BatchScopedTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes: number,\n): string {\n return issueReadToken(\n signingSecret,\n payload as Omit<ReadTokenPayload, \"iat\" | \"exp\" | \"typ\">,\n ttlMinutes,\n );\n}\n\n// Ed25519 JWT header (asymmetric — sidecar cannot forge tokens); computed on\n// first use, not at module load, so the module stays browser-importable.\nlet jwtHeaderEddsaCache: string | undefined;\nfunction jwtHeaderEddsa(): string {\n if (jwtHeaderEddsaCache === undefined) {\n jwtHeaderEddsaCache = base64UrlEncode(\n JSON.stringify({ alg: \"EdDSA\", typ: \"JWT\" }),\n );\n }\n return jwtHeaderEddsaCache;\n}\n\n/**\n * Capability strings carried in a sidecar token's `cap` claim. This is the\n * shared vocabulary the crypto layer stamps into the JWT (issueSidecarAccessToken)\n * and reads back out (verifySidecarToken). The route→capability authorization\n * policy that enforces these claims lives in the sidecar layer, not here.\n * Tokens with a `cap` claim are strictly limited to those capabilities; tokens\n * without a `cap` claim are full-scope (legacy orchestrator-internal use).\n *\n * Adding a new capability: extend this union, then update the enforcement\n * policy (route maps + authorization check) in the sidecar-auth package.\n */\nexport type SidecarCapability =\n | \"computer_use\"\n | \"read\"\n | \"debug\"\n | \"terminal\"\n | \"workspace\"\n | \"control\"\n | \"raw_input\";\n\n/**\n * The accepted values of the `sst` claim. The exported union is derived from\n * this list so the type a caller programs against and the set the verifier\n * enforces cannot drift apart.\n */\nconst SIDECAR_SID_SCOPES = [\"session\", \"project\"] as const;\n\n/**\n * What a sidecar token's `sid` claim holds, carried in the `sst` claim.\n *\n * `sid` is minted with two different meanings: for a session-runtime token it\n * is a session id, and for a project-scoped read-only token it is a project\n * ref. Nothing on the token distinguished them, so a consumer could not tell\n * whether comparing `sid` against a session id in a request path was an\n * authorization check or a guaranteed mismatch.\n *\n * A consumer must treat the claim's absence as \"the meaning of `sid` is\n * unknown, do not enforce a session comparison\" — tokens minted before this\n * claim existed carry no `sst`.\n *\n * A consumer must also treat any value other than `\"session\"` as \"this is not\n * a session token\", and deny it on a session-addressed route. Reading the\n * claim as `sst === \"session\" ? compare : allow` grants a project-scoped token\n * the reach the comparison exists to remove.\n *\n * The verifier does not check that `sid` looks like what `sst` declares. That\n * is a semantic judgement about ids this layer cannot make; it belongs to the\n * consumer that knows the id namespaces.\n *\n * Adding a value here is a breaking change for verifiers: `verifySidecarToken`\n * rejects an `sst` it does not recognise, so every verifier must ship the new\n * value before any issuer stamps it.\n */\nexport type SidecarSidScope = (typeof SIDECAR_SID_SCOPES)[number];\n\n/**\n * Generate an Ed25519 key pair for per-session JWT signing.\n * Private key stays in orchestrator memory. Public key is injected into\n * the sidecar container. Even if the sidecar is fully compromised,\n * the attacker cannot forge JWTs for any container.\n *\n * @returns { privateKey, publicKey } as PEM strings\n */\nexport function generateSidecarKeyPair(): {\n privateKey: string;\n publicKey: string;\n} {\n const { privateKey, publicKey } = nodeCrypto().generateKeyPairSync(\n \"ed25519\",\n {\n publicKeyEncoding: { type: \"spki\", format: \"pem\" },\n privateKeyEncoding: { type: \"pkcs8\", format: \"pem\" },\n },\n );\n return { privateKey, publicKey };\n}\n\n/**\n * Issue a sidecar access token (JWT) using Ed25519 signing.\n * Scoped to a specific container via the required `cid` claim.\n *\n * @param privateKey - Ed25519 private key (PEM). If not provided, falls back to HMAC.\n * @param hmacFallbackSecret - HMAC secret for backward compat (used when no Ed25519 key)\n * @param payload - Token payload with container ID\n * @param ttlMinutes - Token TTL in minutes\n */\nexport function issueSidecarAccessToken(\n privateKey: string,\n payload: {\n sub: string;\n pid: string;\n cid: string;\n sid?: string;\n /**\n * What `sid` holds. Stamp it whenever `sid` is set, so a consumer can tell\n * a session-bound token from a project-bound one instead of guessing from\n * the capability list. Omitting it leaves consumers unable to enforce a\n * session comparison.\n */\n sst?: SidecarSidScope;\n /**\n * Capability allowlist. When present, the token is restricted to\n * routes whose required capability is in this list, as enforced by the\n * sidecar-auth policy layer. Absent = full scope (legacy\n * orchestrator-internal tokens).\n */\n cap?: SidecarCapability[];\n },\n ttlMinutes: number,\n): string {\n if (!privateKey.includes(\"PRIVATE KEY\")) {\n throw new Error(\n \"issueSidecarAccessToken requires an Ed25519 private key (PEM). \" +\n \"HMAC fallback has been removed — generate a key pair with generateSidecarKeyPair().\",\n );\n }\n\n // Reject at mint time what no verifier accepts. Either mistake otherwise\n // produces a token that fails everywhere it is presented, debugged inside a\n // sidecar rather than in the issuer that made it. A caller reaching this\n // function from JavaScript, or through a value typed `string`, gets no help\n // from the parameter type.\n if (payload.sst !== undefined) {\n if (!SIDECAR_SID_SCOPES.includes(payload.sst)) {\n throw new Error(\n `issueSidecarAccessToken: sst must be one of ${SIDECAR_SID_SCOPES.join(\", \")}.`,\n );\n }\n // `sst` names what `sid` holds, so it is meaningless without one. A\n // whitespace-only sid is the same mistake wearing a disguise: it passes a\n // truthiness test, then a consumer enforces a session match against a\n // degenerate value.\n if (!payload.sid || payload.sid.trim().length === 0) {\n throw new Error(\n \"issueSidecarAccessToken: sst describes the sid claim and cannot be set without a non-blank sid.\",\n );\n }\n }\n\n const now = Math.floor(Date.now() / 1000);\n // jti (JWT ID) enables token revocation — orchestrator adds jti to blocklist\n // on sandbox delete, sidecar checks blocklist on every request.\n const { randomBytes, sign } = nodeCrypto();\n const jti = `${payload.cid}:${now}:${randomBytes(8).toString(\"hex\")}`;\n const fullPayload = {\n ...payload,\n typ: \"sidecar\" as const,\n jti,\n iat: now,\n exp: now + ttlMinutes * 60,\n };\n const encodedPayload = base64UrlEncode(JSON.stringify(fullPayload));\n const data = `${jwtHeaderEddsa()}.${encodedPayload}`;\n const signature = base64UrlEncode(\n sign(null, textEncoder.encode(data), privateKey),\n );\n return `${data}.${signature}`;\n}\n\n/**\n * Verify a sidecar JWT using Ed25519 public key. Rejects non-EdDSA tokens.\n * Returns decoded payload on success, null on failure.\n */\nexport function verifySidecarToken(\n token: string,\n publicKey: string,\n containerId: string,\n): {\n sub: string;\n pid: string;\n cid: string;\n sid?: string;\n /**\n * What `sid` holds. Absent on tokens minted before the claim existed —\n * treat that as \"unknown\", never as a licence to skip a check.\n */\n sst?: SidecarSidScope;\n typ: string;\n jti?: string;\n exp: number;\n cap?: SidecarCapability[];\n} | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n\n const headerJson = base64UrlDecode(parts[0]);\n const header = JSON.parse(headerJson);\n if (header.alg !== \"EdDSA\") return null;\n\n // SECURITY: verify signature BEFORE inspecting claims.\n // Parsing claims before verification creates a timing oracle that\n // leaks valid container IDs via early-return timing differences.\n const data = `${parts[0]}.${parts[1]}`;\n const signatureBuffer = base64UrlToBytes(parts[2]);\n const valid = nodeCrypto().verify(\n null,\n textEncoder.encode(data),\n publicKey,\n signatureBuffer,\n );\n if (!valid) return null;\n\n // Signature verified — now safe to parse and inspect claims\n const payload = JSON.parse(base64UrlDecode(parts[1]));\n\n if (payload.typ !== \"sidecar\") return null;\n if (typeof payload.jti !== \"string\" || payload.jti.length === 0) {\n return null;\n }\n const exactContainerMatch = payload.cid === containerId;\n const shortDockerIdMatch = isDockerShortIdMatch(payload.cid, containerId);\n if (!exactContainerMatch && !shortDockerIdMatch) return null;\n\n // Require exp — tokens without expiration are rejected (no immortal tokens)\n const now = Math.floor(Date.now() / 1000);\n if (typeof payload.exp !== \"number\" || payload.exp < now) return null;\n\n // Require non-empty sub and cid\n if (typeof payload.sub !== \"string\" || payload.sub.length === 0)\n return null;\n\n // `sst` decides whether a consumer runs an authorization check, so an\n // unrecognised or unusable value must reject the token rather than pass\n // through. Returning it would leave the consumer's `sst === \"session\"`\n // test false and silently skip the check the issuer asked for.\n if (payload.sst !== undefined) {\n // `includes` compares by SameValueZero, so this rejects every non-string\n // a JSON claim can hold as well as an unrecognised string.\n if (!SIDECAR_SID_SCOPES.includes(payload.sst)) return null;\n // Blank counts as absent. The trim applies only on this branch, so it\n // cannot reject a token minted before the claim existed.\n if (typeof payload.sid !== \"string\" || payload.sid.trim().length === 0) {\n return null;\n }\n }\n\n return payload;\n } catch {\n return null;\n }\n}\n\n/**\n * Compile-time assertions that keep the `sst` claim narrow on both halves of\n * the round trip. They are load-bearing: this package type-checks `src` only,\n * so widening either field, or dropping the `as const` that derives\n * `SidecarSidScope`, otherwise collapses the union to `string` and passes\n * every gate here — leaving a consumer free to test `sst` against a value the\n * verifier rejects.\n */\ntype AssertTrue<T extends true> = T;\ntype _IssuerTakesNarrowSst = AssertTrue<\n string extends NonNullable<Parameters<typeof issueSidecarAccessToken>[1][\"sst\"]>\n ? false\n : true\n>;\ntype _VerifierReturnsNarrowSst = AssertTrue<\n string extends NonNullable<\n NonNullable<ReturnType<typeof verifySidecarToken>>[\"sst\"]\n >\n ? false\n : true\n>;\n\nfunction isDockerShortIdMatch(\n payloadCid: unknown,\n containerId: unknown,\n): boolean {\n if (typeof payloadCid !== \"string\" || typeof containerId !== \"string\") {\n return false;\n }\n if (!/^[a-f0-9]{64}$/i.test(payloadCid)) {\n return false;\n }\n if (!/^[a-f0-9]{12}$/i.test(containerId)) {\n return false;\n }\n return payloadCid.startsWith(containerId);\n}\n\n/**\n * Check if a token payload is session-scoped.\n * Session-scoped tokens have a sid claim and no projectId/projectIds.\n */\nexport function isSessionScopedToken(\n payload: ReadTokenPayload,\n): payload is SessionScopedTokenPayload {\n return (\n \"sid\" in payload &&\n typeof payload.sid === \"string\" &&\n payload.sid.length > 0 &&\n !(\"projectId\" in payload && payload.projectId) &&\n !(\"projectIds\" in payload && payload.projectIds)\n );\n}\n\n/**\n * Check if a token payload is project-scoped.\n * Project-scoped tokens have a projectId claim and no sid/projectIds.\n */\nexport function isProjectScopedToken(\n payload: ReadTokenPayload,\n): payload is ProjectScopedTokenPayload {\n return (\n \"projectId\" in payload &&\n typeof payload.projectId === \"string\" &&\n payload.projectId.length > 0 &&\n !(\"sid\" in payload && payload.sid) &&\n !(\"projectIds\" in payload && payload.projectIds)\n );\n}\n\n/**\n * Check if a token payload is batch-scoped.\n * Batch-scoped tokens have a projectIds claim and no sid/projectId.\n */\nexport function isBatchScopedToken(\n payload: ReadTokenPayload,\n): payload is BatchScopedTokenPayload {\n return (\n \"projectIds\" in payload &&\n Array.isArray(payload.projectIds) &&\n payload.projectIds.length > 0 &&\n !(\"sid\" in payload && payload.sid) &&\n !(\"projectId\" in payload && payload.projectId)\n );\n}\n\n/**\n * Get the scope of a token payload.\n * Returns null if the token has an invalid/ambiguous scope.\n */\nexport function getTokenScope(payload: ReadTokenPayload): TokenScope | null {\n if (isSessionScopedToken(payload)) return \"session\";\n if (isProjectScopedToken(payload)) return \"project\";\n if (isBatchScopedToken(payload)) return \"batch\";\n return null;\n}\n\n/**\n * Validate that a token payload has exactly one valid scope.\n * Returns an error message if invalid, null if valid.\n */\nexport function validateTokenScope(payload: ReadTokenPayload): string | null {\n const hasSid = \"sid\" in payload && payload.sid;\n const hasProjectId = \"projectId\" in payload && payload.projectId;\n const hasProjectIds =\n \"projectIds\" in payload &&\n Array.isArray(payload.projectIds) &&\n payload.projectIds.length > 0;\n\n const scopeCount = [hasSid, hasProjectId, hasProjectIds].filter(\n Boolean,\n ).length;\n\n if (scopeCount === 0) {\n return \"Token must have one of: sid (session), projectId (project), or projectIds (batch)\";\n }\n\n if (scopeCount > 1) {\n return \"Token cannot have multiple scope claims (sid, projectId, projectIds are mutually exclusive)\";\n }\n\n return null;\n}\n\n/**\n * Decode a JWT without verification (to extract claims for lookup).\n * Returns null if the token is malformed.\n */\nexport function decodeToken(token: string): ReadTokenPayload | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return null;\n }\n\n const payload = JSON.parse(base64UrlDecode(parts[1]));\n return payload as ReadTokenPayload;\n } catch {\n return null;\n }\n}\n\n/**\n * Verify a read token against a product's signing secrets.\n *\n * @param token - The JWT token to verify\n * @param product - Product auth info containing secrets\n */\nexport function verifyReadToken(\n token: string,\n product: ProductAuthInfo,\n): TokenValidationResult {\n // Check product status\n if (product.status !== \"active\") {\n return {\n valid: false,\n error: \"Product is suspended\",\n errorCode: \"PRODUCT_SUSPENDED\",\n };\n }\n\n // Parse token\n const parts = token.split(\".\");\n if (parts.length !== 3) {\n return {\n valid: false,\n error: \"Invalid token format\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n const [header, encodedPayload, signature] = parts;\n const data = `${header}.${encodedPayload}`;\n\n // Pin algorithm to HS256 — defense-in-depth against alg-confusion\n // attacks that swap a forged header (e.g. `alg: \"none\"`) onto an\n // otherwise valid token. Fast path: when the header bytes match the\n // canonical `JWT_HEADER` verbatim, we skip the parse. Slow path:\n // decode the header and reject anything whose `alg` claim is not\n // `\"HS256\"`. Non-canonical-but-valid headers (e.g. `alg: \"HS256\"`\n // with an extra `kid` claim) fall through to signature verification,\n // which covers the actual header bytes — so any tampering with the\n // header still fails the signature check.\n if (header !== jwtHeader()) {\n let parsedAlg: string | null = null;\n try {\n const decoded = JSON.parse(base64UrlDecode(header)) as Record<\n string,\n unknown\n >;\n if (typeof decoded.alg === \"string\") parsedAlg = decoded.alg;\n } catch {\n // Header didn't decode cleanly; treat as TOKEN_INVALID below.\n }\n if (parsedAlg !== \"HS256\") {\n return {\n valid: false,\n error: \"Unsupported token algorithm\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n }\n\n // Verify signature with current secret\n let signatureValid = verifySignature(\n data,\n signature,\n product.secrets.current.secret,\n );\n\n // If failed and we have a previous secret in grace period, try it\n if (!signatureValid && product.secrets.previous) {\n if (Date.now() < product.secrets.previous.expires_at) {\n signatureValid = verifySignature(\n data,\n signature,\n product.secrets.previous.secret,\n );\n }\n }\n\n if (!signatureValid) {\n return {\n valid: false,\n error: \"Invalid signature\",\n errorCode: \"SIGNATURE_INVALID\",\n };\n }\n\n // Parse payload\n let payload: ReadTokenPayload;\n try {\n payload = JSON.parse(base64UrlDecode(encodedPayload));\n } catch {\n return {\n valid: false,\n error: \"Invalid token payload\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n // Check token type\n if (payload.typ !== \"read\") {\n return {\n valid: false,\n error: \"Not a read token\",\n errorCode: \"TOKEN_WRONG_TYPE\",\n };\n }\n\n // Check expiration\n const now = Math.floor(Date.now() / 1000);\n if (payload.exp < now) {\n return {\n valid: false,\n error: \"Token expired\",\n errorCode: \"TOKEN_EXPIRED\",\n };\n }\n\n // Check product ID matches\n if (payload.pid !== product.product_id) {\n return {\n valid: false,\n error: \"Product ID mismatch\",\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n // Validate token scope (must have exactly one of sid, projectId, projectIds)\n const scopeError = validateTokenScope(payload);\n if (scopeError) {\n return {\n valid: false,\n error: scopeError,\n errorCode: \"TOKEN_INVALID\",\n };\n }\n\n return {\n valid: true,\n payload,\n scope: getTokenScope(payload) ?? undefined,\n };\n}\n\n/**\n * Get time until token expires (in seconds).\n * Returns negative if expired.\n */\nexport function getTokenTTL(payload: ReadTokenPayload): number {\n const now = Math.floor(Date.now() / 1000);\n return payload.exp - now;\n}\n\n/**\n * Check if token is expiring soon (within buffer seconds).\n */\nexport function isTokenExpiringSoon(\n payload: ReadTokenPayload,\n bufferSeconds = 60,\n): boolean {\n return getTokenTTL(payload) <= bufferSeconds;\n}\n\n/**\n * Default salt for API key hashing (development only).\n * Production MUST set API_KEY_HASH_SALT environment variable.\n * WARNING: Changing the salt will invalidate all existing API key hashes.\n */\nconst DEFAULT_API_KEY_SALT = \"api_key_hash_salt\";\n\nlet _saltWarned = false;\n\n/**\n * Get the API key hash salt from environment.\n * Logs a warning if falling back to the weak default (development only).\n */\nfunction getApiKeyHashSalt(): string {\n if (typeof process !== \"undefined\" && process.env?.API_KEY_HASH_SALT) {\n return process.env.API_KEY_HASH_SALT;\n }\n if (\n !_saltWarned &&\n typeof process !== \"undefined\" &&\n process.env?.NODE_ENV === \"production\"\n ) {\n _saltWarned = true;\n console.error(\n \"[SECURITY] API_KEY_HASH_SALT not set in production. \" +\n \"Using weak default salt. Set API_KEY_HASH_SALT to a cryptographically random value.\",\n );\n }\n return DEFAULT_API_KEY_SALT;\n}\n\n/**\n * Hash an API key for storage/lookup.\n * Uses HMAC-SHA256 for consistent, fast hashing.\n *\n * The salt can be configured via API_KEY_HASH_SALT environment variable.\n * WARNING: Changing the salt will invalidate all existing API key hashes.\n */\nexport function hashApiKey(apiKey: string): string {\n return nodeCrypto()\n .createHmac(\"sha256\", getApiKeyHashSalt())\n .update(apiKey)\n .digest(\"hex\");\n}\n\n/**\n * Hash an API key with an explicit salt.\n * Use this when you need to verify against a specific salt.\n */\nexport function hashApiKeyWithSalt(apiKey: string, salt: string): string {\n return nodeCrypto().createHmac(\"sha256\", salt).update(apiKey).digest(\"hex\");\n}\n\n/**\n * Verify an API key using timing-safe comparison.\n */\nexport function verifyApiKey(provided: string, stored: string): boolean {\n if (provided.length !== stored.length) {\n return false;\n }\n try {\n return nodeCrypto().timingSafeEqual(\n textEncoder.encode(provided),\n textEncoder.encode(stored),\n );\n } catch {\n return false;\n }\n}\n","/**\n * Channel Access Control\n *\n * Determines what channels a token can access based on its scope.\n * Used by SessionGateway to validate subscriptions and event delivery.\n *\n * Channel patterns:\n * - session:{sessionId} - Events for a specific session\n * - agent:{sessionId} - Agent events for a specific session\n * - project:{projectId} - All events for a project\n * - system - System-wide events (heartbeat, etc.)\n */\n\nimport {\n isBatchScopedToken,\n isProjectScopedToken,\n isSessionScopedToken,\n} from \"./tokens.js\";\nimport type {\n BatchScopedTokenPayload,\n ProjectScopedTokenPayload,\n ReadTokenPayload,\n SessionScopedTokenPayload,\n} from \"./types.js\";\n\n/**\n * Result of channel access check.\n */\nexport interface ChannelAccessResult {\n /** Whether access is allowed */\n allowed: boolean;\n /** Reason for denial (if not allowed) */\n reason?: string;\n}\n\n/**\n * Extract session ID from a session channel pattern.\n * Returns null if not a session channel.\n */\nexport function extractSessionFromChannel(channel: string): string | null {\n if (channel.startsWith(\"session:\")) {\n return channel.slice(8);\n }\n if (channel.startsWith(\"agent:\")) {\n return channel.slice(6);\n }\n return null;\n}\n\n/**\n * Extract project ID from a project channel pattern.\n * Returns null if not a project channel.\n */\nexport function extractProjectFromChannel(channel: string): string | null {\n if (channel.startsWith(\"project:\")) {\n return channel.slice(8);\n }\n return null;\n}\n\n/**\n * Check if a session-scoped token can access a channel.\n *\n * Session tokens can access:\n * - session:{their-sid}\n * - agent:{their-sid}\n * - system (always allowed)\n * - Wildcard patterns matching their session\n */\nexport function canSessionTokenAccessChannel(\n payload: SessionScopedTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // System channels are always allowed\n if (channel === \"system\" || channel.startsWith(\"system.\")) {\n return { allowed: true };\n }\n\n // Global wildcard requires higher scope\n if (channel === \"*\") {\n return {\n allowed: false,\n reason: \"Session-scoped tokens cannot subscribe to all channels\",\n };\n }\n\n // Check session channel match\n const sessionId = extractSessionFromChannel(channel);\n if (sessionId !== null) {\n if (sessionId === payload.sid) {\n return { allowed: true };\n }\n // Wildcard session channels not allowed for session tokens\n if (sessionId === \"*\") {\n return {\n allowed: false,\n reason: \"Session-scoped tokens cannot subscribe to all sessions\",\n };\n }\n return {\n allowed: false,\n reason: `Token only has access to session ${payload.sid}`,\n };\n }\n\n // Project channels require project scope\n if (channel.startsWith(\"project:\")) {\n return {\n allowed: false,\n reason: \"Session-scoped tokens cannot access project channels\",\n };\n }\n\n // Default: check if channel matches session pattern\n // Allow channels that don't have explicit scope (backward compatibility)\n return { allowed: true };\n}\n\n/**\n * Check if a project-scoped token can access a channel.\n *\n * Project tokens can access:\n * - project:{their-projectId}\n * - session:* (all sessions within project - requires validation by caller)\n * - agent:* (all agent events within project)\n * - system (always allowed)\n */\nexport function canProjectTokenAccessChannel(\n payload: ProjectScopedTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // System channels are always allowed\n if (channel === \"system\" || channel.startsWith(\"system.\")) {\n return { allowed: true };\n }\n\n // Global wildcard requires batch scope or higher\n if (channel === \"*\") {\n return {\n allowed: false,\n reason:\n \"Project-scoped tokens cannot subscribe to all channels across projects\",\n };\n }\n\n // Project channel access\n const projectId = extractProjectFromChannel(channel);\n if (projectId !== null) {\n if (projectId === payload.projectId) {\n return { allowed: true };\n }\n return {\n allowed: false,\n reason: `Token only has access to project ${payload.projectId}`,\n };\n }\n\n // Session channels - project tokens can access any session in their project\n // The actual validation that session belongs to project must be done by the caller\n // who has access to the session-to-project mapping\n if (channel.startsWith(\"session:\") || channel.startsWith(\"agent:\")) {\n // Wildcard session channels allowed for project tokens\n const sessionId = extractSessionFromChannel(channel);\n if (sessionId === \"*\") {\n return { allowed: true };\n }\n // Specific session - allowed but caller must verify session is in project\n return { allowed: true };\n }\n\n // Default: allow general channels (backward compatibility)\n return { allowed: true };\n}\n\n/**\n * Check if a batch-scoped token can access a channel.\n *\n * Batch tokens can access:\n * - project:{any of their projectIds}\n * - session:* (all sessions within their projects)\n * - agent:* (all agent events within their projects)\n * - system (always allowed)\n * - * (global wildcard - for organization-wide subscriptions)\n */\nexport function canBatchTokenAccessChannel(\n payload: BatchScopedTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // System channels are always allowed\n if (channel === \"system\" || channel.startsWith(\"system.\")) {\n return { allowed: true };\n }\n\n // Global wildcard allowed for batch tokens\n if (channel === \"*\") {\n return { allowed: true };\n }\n\n // Project channel access\n const projectId = extractProjectFromChannel(channel);\n if (projectId !== null) {\n // Wildcard project access allowed\n if (projectId === \"*\") {\n return { allowed: true };\n }\n // Specific project must be in the list\n if (payload.projectIds.includes(projectId)) {\n return { allowed: true };\n }\n return {\n allowed: false,\n reason: `Token does not have access to project ${projectId}`,\n };\n }\n\n // Session/agent channels - batch tokens can access any session in their projects\n // Caller must verify session belongs to one of the token's projects\n if (channel.startsWith(\"session:\") || channel.startsWith(\"agent:\")) {\n return { allowed: true };\n }\n\n // Default: allow general channels (backward compatibility)\n return { allowed: true };\n}\n\n/**\n * Check if a token can access a channel.\n * Dispatches to scope-specific functions based on token type.\n *\n * @param payload - The token payload\n * @param channel - The channel to check access for\n * @returns Access result with allowed boolean and optional reason\n */\nexport function canTokenAccessChannel(\n payload: ReadTokenPayload,\n channel: string,\n): ChannelAccessResult {\n // Use type guards for clean scope detection and type narrowing\n if (isSessionScopedToken(payload)) {\n return canSessionTokenAccessChannel(payload, channel);\n }\n\n if (isProjectScopedToken(payload)) {\n return canProjectTokenAccessChannel(payload, channel);\n }\n\n if (isBatchScopedToken(payload)) {\n return canBatchTokenAccessChannel(payload, channel);\n }\n\n // Invalid token scope\n return {\n allowed: false,\n reason: \"Token has no valid scope (missing sid, projectId, or projectIds)\",\n };\n}\n\n/**\n * Get the list of allowed channel patterns for a token.\n * Useful for informing clients what they can subscribe to.\n *\n * @param payload - The token payload\n * @returns Array of allowed channel patterns\n */\nexport function getAllowedChannelPatterns(payload: ReadTokenPayload): string[] {\n const patterns: string[] = [\"system\", \"system.*\"];\n\n // Use type guards for clean scope detection\n if (isSessionScopedToken(payload)) {\n patterns.push(`session:${payload.sid}`, `agent:${payload.sid}`);\n return patterns;\n }\n\n if (isProjectScopedToken(payload)) {\n patterns.push(\n `project:${payload.projectId}`,\n \"session:*\", // Within project\n \"agent:*\", // Within project\n );\n return patterns;\n }\n\n if (isBatchScopedToken(payload)) {\n patterns.push(\n \"*\", // Global wildcard\n \"project:*\", // All projects in batch\n ...payload.projectIds.map((id) => `project:${id}`),\n \"session:*\",\n \"agent:*\",\n );\n return patterns;\n }\n\n return patterns;\n}\n\n/**\n * Validate channel subscription request.\n * Checks multiple channels and returns aggregated result.\n *\n * @param payload - The token payload\n * @param channels - Channels to validate\n * @returns Object with allowed channels and denied channels with reasons\n */\nexport function validateChannelSubscription(\n payload: ReadTokenPayload,\n channels: string[],\n): {\n allowed: string[];\n denied: Array<{ channel: string; reason: string }>;\n} {\n const allowed: string[] = [];\n const denied: Array<{ channel: string; reason: string }> = [];\n\n for (const channel of channels) {\n const result = canTokenAccessChannel(payload, channel);\n if (result.allowed) {\n allowed.push(channel);\n } else {\n denied.push({ channel, reason: result.reason ?? \"Access denied\" });\n }\n }\n\n return { allowed, denied };\n}\n","/**\n * Authentication Configuration\n *\n * Token management with refresh hooks and expiration handling.\n */\n\nimport { issueReadToken } from \"./tokens.js\";\n\n// Re-export channel access utilities\nexport * from \"./channel-access.js\";\n// Re-export shared auth providers (eliminates 185 lines of duplication with browser.ts)\nexport type { AuthProvider } from \"./shared-providers.js\";\nexport {\n CallbackAuth,\n NoAuth,\n ReadTokenAuth,\n RefreshableTokenAuth,\n StaticTokenAuth,\n} from \"./shared-providers.js\";\nexport * from \"./tokens.js\";\n// Re-export types and token utilities\nexport * from \"./types.js\";\n\n/**\n * Environment-based auth that reads from env vars.\n * Matches the server-side patterns used in sidecar/orchestrator.\n */\nexport class EnvTokenAuth {\n private envVar: string;\n private fallback?: string;\n\n constructor(options: { envVar: string; fallback?: string }) {\n this.envVar = options.envVar;\n this.fallback = options.fallback;\n }\n\n async getToken(): Promise<string | null> {\n // Works in Node.js; in browser, fallback would be used\n if (typeof process !== \"undefined\" && process.env) {\n return process.env[this.envVar] ?? this.fallback ?? null;\n }\n return this.fallback ?? null;\n }\n}\n\n/**\n * Pre-configured auth for sidecar connections.\n * Uses SIDECAR_AUTH_TOKEN env var (matches server expectation).\n */\nexport class SidecarAuth extends EnvTokenAuth {\n constructor(token?: string) {\n super({ envVar: \"SIDECAR_AUTH_TOKEN\", fallback: token });\n }\n}\n\n/**\n * Product-based auth for multi-tenant orchestrator connections.\n * Uses ORCHESTRATOR_API_SECRET_KEY env var.\n */\nexport class ProductAuth extends EnvTokenAuth {\n constructor(apiKey?: string) {\n super({ envVar: \"ORCHESTRATOR_API_SECRET_KEY\", fallback: apiKey });\n }\n}\n\n/**\n * Product token issuer for backend services.\n *\n * Use this in product backends to issue read tokens for WebSocket connections.\n *\n * @example\n * ```typescript\n * import { ProductTokenIssuer } from \"@tangle-network/agent-core\";\n *\n * const issuer = new ProductTokenIssuer({\n * productId: \"vibecode\",\n * signingSecret: process.env.ORCHESTRATOR_SIGNING_SECRET!,\n * ttlMinutes: { free: 15, pro: 240 },\n * });\n *\n * // Issue a token for a user session\n * const token = issuer.issue({\n * userId: \"user_123\",\n * sessionId: \"sess_abc\",\n * tier: \"pro\",\n * });\n *\n * // Return token to frontend for WebSocket connection\n * res.json({\n * readToken: token.token,\n * expiresAt: token.expiresAt,\n * websocketUrl: `wss://orchestrator.example.com/session?token=${token.token}`,\n * });\n * ```\n */\nexport class ProductTokenIssuer {\n private readonly productId: string;\n private readonly signingSecret: string;\n private readonly ttlMinutes: {\n free: number;\n pro: number;\n enterprise?: number;\n };\n\n constructor(config: {\n productId: string;\n signingSecret: string;\n /** TTL in minutes for each tier (default: { free: 15, pro: 240 }) */\n ttlMinutes?: { free?: number; pro?: number; enterprise?: number };\n }) {\n this.productId = config.productId;\n this.signingSecret = config.signingSecret;\n this.ttlMinutes = {\n free: config.ttlMinutes?.free ?? 15,\n pro: config.ttlMinutes?.pro ?? 240,\n enterprise: config.ttlMinutes?.enterprise ?? 480,\n };\n }\n\n /**\n * Issue a read token for a user session.\n */\n issue(params: {\n userId: string;\n sessionId: string;\n tier?: \"free\" | \"pro\" | \"enterprise\";\n sidecarId?: string;\n }): { token: string; expiresAt: number } {\n const tier = params.tier ?? \"free\";\n const ttl = this.ttlMinutes[tier] ?? this.ttlMinutes.free;\n\n const token = issueReadToken(\n this.signingSecret,\n {\n sub: params.userId,\n sid: params.sessionId,\n pid: this.productId,\n cid: params.sidecarId,\n },\n ttl,\n );\n\n const expiresAt = Math.floor(Date.now() / 1000) + ttl * 60;\n\n return { token, expiresAt };\n }\n\n /**\n * Get the TTL in minutes for a tier.\n */\n getTtlMinutes(tier: \"free\" | \"pro\" | \"enterprise\" = \"free\"): number {\n return this.ttlMinutes[tier] ?? this.ttlMinutes.free;\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-core",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "Core transport interfaces, event utilities, auth, SSE parsing, and resilience primitives for talking to Tangle agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -119,7 +119,7 @@
119
119
  "LICENSE"
120
120
  ],
121
121
  "dependencies": {
122
- "@tangle-network/agent-interface": "0.46.1",
122
+ "@tangle-network/agent-interface": "0.47.0",
123
123
  "zod": "4.4.3"
124
124
  },
125
125
  "devDependencies": {