@agent-native/core 0.120.4 → 0.121.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/durable-background.ts +12 -4
  5. package/corpus/core/src/agent/run-manager.ts +3 -0
  6. package/corpus/core/src/client/use-db-sync.ts +23 -21
  7. package/corpus/core/src/deploy/build.ts +1 -0
  8. package/corpus/core/src/server/core-routes-plugin.ts +3 -0
  9. package/corpus/core/src/server/gateway-access-check.ts +67 -0
  10. package/corpus/core/src/server/poll.ts +295 -9
  11. package/corpus/core/src/server/short-lived-token.ts +122 -0
  12. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +29 -4
  13. package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +365 -9
  14. package/corpus/templates/slides/app/components/editor/bullet-editing.ts +350 -0
  15. package/corpus/templates/slides/app/context/DeckContext.tsx +1 -3
  16. package/corpus/templates/slides/app/i18n/en-US.ts +2 -0
  17. package/corpus/templates/slides/app/pages/DeckEditor.tsx +5 -0
  18. package/corpus/templates/slides/changelog/2026-07-23-new-bullet-rows-created-with-enter-now-keep-the-list-item-s-.md +6 -0
  19. package/corpus/templates/slides/changelog/2026-07-23-pressing-enter-in-generated-checkbox-shape-marker-lists-now-.md +6 -0
  20. package/corpus/templates/slides/changelog/2026-07-24-typing-a-markdown-style-dash-space-at-the-start-of-a-text-bl.md +6 -0
  21. package/dist/agent/durable-background.d.ts.map +1 -1
  22. package/dist/agent/durable-background.js +16 -4
  23. package/dist/agent/durable-background.js.map +1 -1
  24. package/dist/agent/run-manager.d.ts.map +1 -1
  25. package/dist/agent/run-manager.js +6 -0
  26. package/dist/agent/run-manager.js.map +1 -1
  27. package/dist/client/use-db-sync.d.ts.map +1 -1
  28. package/dist/client/use-db-sync.js +27 -22
  29. package/dist/client/use-db-sync.js.map +1 -1
  30. package/dist/collab/awareness.d.ts +2 -2
  31. package/dist/collab/awareness.d.ts.map +1 -1
  32. package/dist/deploy/build.d.ts.map +1 -1
  33. package/dist/deploy/build.js +1 -0
  34. package/dist/deploy/build.js.map +1 -1
  35. package/dist/observability/routes.d.ts +3 -3
  36. package/dist/progress/routes.d.ts +1 -1
  37. package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
  38. package/dist/provider-api/actions/provider-api.d.ts +6 -6
  39. package/dist/resources/handlers.d.ts +1 -1
  40. package/dist/secrets/routes.d.ts +9 -9
  41. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  42. package/dist/server/core-routes-plugin.js +3 -0
  43. package/dist/server/core-routes-plugin.js.map +1 -1
  44. package/dist/server/gateway-access-check.d.ts +12 -0
  45. package/dist/server/gateway-access-check.d.ts.map +1 -0
  46. package/dist/server/gateway-access-check.js +49 -0
  47. package/dist/server/gateway-access-check.js.map +1 -0
  48. package/dist/server/poll.d.ts +55 -1
  49. package/dist/server/poll.d.ts.map +1 -1
  50. package/dist/server/poll.js +254 -7
  51. package/dist/server/poll.js.map +1 -1
  52. package/dist/server/realtime-token.d.ts +1 -1
  53. package/dist/server/short-lived-token.d.ts +28 -0
  54. package/dist/server/short-lived-token.d.ts.map +1 -1
  55. package/dist/server/short-lived-token.js +78 -0
  56. package/dist/server/short-lived-token.js.map +1 -1
  57. package/dist/server/transcribe-voice.d.ts +1 -1
  58. package/package.json +1 -1
  59. package/src/agent/durable-background.ts +12 -4
  60. package/src/agent/run-manager.ts +3 -0
  61. package/src/client/use-db-sync.ts +23 -21
  62. package/src/deploy/build.ts +1 -0
  63. package/src/server/core-routes-plugin.ts +3 -0
  64. package/src/server/gateway-access-check.ts +67 -0
  65. package/src/server/poll.ts +295 -9
  66. package/src/server/short-lived-token.ts +122 -0
@@ -1 +1 @@
1
- {"version":3,"file":"short-lived-token.js","sourceRoot":"","sources":["../../src/server/short-lived-token.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Short-lived HMAC-signed access tokens for media URLs.\n *\n * Used by clips and calls to mint a single-use bearer token after a password\n * gate passes, then bake `?t=<token>` into the video/blob URL handed to the\n * `<video>` element — instead of `?password=<plaintext>` (which ends up in\n * browser history, CDN logs, and Referer headers).\n *\n * Token shape: `<payloadB64Url>.<sigB64Url>`\n * payload = base64url(JSON.stringify({ resourceId, viewerEmail?, exp }))\n * sig = base64url(HMAC-SHA256(payload, key))\n *\n * Key resolution mirrors `google-oauth.ts:getStateSigningKey`:\n * 1. OAUTH_STATE_SECRET (preferred — dedicated to short-lived signing)\n * 2. BETTER_AUTH_SECRET (already used as a server secret)\n * 3. Hosted workspace deploys derive a per-purpose key from A2A_SECRET\n * 4. In dev only, an ephemeral random key (per-process)\n *\n * In production, throws if no usable server secret is set.\n */\n\nimport crypto from \"node:crypto\";\n\nimport { getWorkspaceA2ADerivedSecret } from \"./derived-secret.js\";\n\n/** Default token TTL, in seconds. 10 minutes covers a typical video session. */\nconst DEFAULT_TTL_SECONDS = 600;\n\n/**\n * Inputs for {@link signShortLivedToken}.\n */\nexport interface ShortLivedTokenClaims {\n /** Resource id the token authorises (recording id, call id, snippet id, …). */\n resourceId: string;\n /** Optional viewer email for audit / analytics — not used for authorisation. */\n viewerEmail?: string;\n /** Override default TTL (seconds). */\n ttlSeconds?: number;\n}\n\ninterface DecodedClaims {\n resourceId: string;\n viewerEmail?: string;\n exp: number;\n}\n\n/**\n * Result of {@link verifyShortLivedToken}. Discriminated by the literal\n * `ok` field so callers can `if (!result.ok) return …`.\n */\nexport type VerifyResult =\n | { ok: true; viewerEmail?: string }\n | { ok: false; reason: string };\n\nlet _devSigningKey: string | undefined;\n\nfunction getSigningKey(): string {\n const secret =\n process.env.OAUTH_STATE_SECRET ||\n process.env.BETTER_AUTH_SECRET ||\n getWorkspaceA2ADerivedSecret(\"short-lived-token\");\n if (secret) return secret;\n\n if (process.env.NODE_ENV === \"production\") {\n throw new Error(\n \"Short-lived token signing requires a server secret. \" +\n \"Set OAUTH_STATE_SECRET, BETTER_AUTH_SECRET, or A2A_SECRET in production workspace deploys.\",\n );\n }\n\n if (!_devSigningKey) {\n _devSigningKey = crypto.randomBytes(32).toString(\"hex\");\n }\n return _devSigningKey;\n}\n\nfunction base64UrlEncode(buf: Buffer | string): string {\n const b = typeof buf === \"string\" ? Buffer.from(buf, \"utf8\") : buf;\n return b\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n\nfunction base64UrlDecode(s: string): Buffer {\n // Re-pad to a multiple of 4 so Buffer.from('base64') decodes cleanly.\n const padded = s + \"=\".repeat((4 - (s.length % 4)) % 4);\n return Buffer.from(padded.replace(/-/g, \"+\").replace(/_/g, \"/\"), \"base64\");\n}\n\n/**\n * Mint a signed token authorising read access to `claims.resourceId` until\n * `exp = now + ttl`. The result is safe to drop into a query string —\n * `?t=<token>` — and verified by {@link verifyShortLivedToken} on the\n * downstream route.\n */\nexport function signShortLivedToken(claims: ShortLivedTokenClaims): string {\n const ttl = claims.ttlSeconds ?? DEFAULT_TTL_SECONDS;\n const payload: DecodedClaims = {\n resourceId: claims.resourceId,\n exp: Math.floor(Date.now() / 1000) + ttl,\n };\n if (claims.viewerEmail) payload.viewerEmail = claims.viewerEmail;\n\n const payloadStr = base64UrlEncode(JSON.stringify(payload));\n const sig = base64UrlEncode(\n crypto.createHmac(\"sha256\", getSigningKey()).update(payloadStr).digest(),\n );\n return `${payloadStr}.${sig}`;\n}\n\n/**\n * Verify a token previously produced by {@link signShortLivedToken}.\n *\n * Returns `{ ok: true, viewerEmail? }` only when:\n * - the token has the expected shape (`<payload>.<sig>`),\n * - the signature matches via constant-time comparison,\n * - the token has not expired,\n * - the embedded `resourceId` matches `expectedResourceId`.\n *\n * Otherwise returns `{ ok: false, reason: <error string> }`. Callers should\n * not surface the reason to viewers (it's useful for server-side logs only).\n */\nexport function verifyShortLivedToken(\n token: string,\n expectedResourceId: string,\n): VerifyResult {\n if (typeof token !== \"string\" || !token.includes(\".\")) {\n return { ok: false, reason: \"malformed\" };\n }\n const [payloadStr, sig] = token.split(\".\", 2);\n if (!payloadStr || !sig) return { ok: false, reason: \"malformed\" };\n\n const expected = base64UrlEncode(\n crypto.createHmac(\"sha256\", getSigningKey()).update(payloadStr).digest(),\n );\n\n // Constant-time compare. Length-mismatched inputs would throw under\n // `crypto.timingSafeEqual`, so we check length first and fall back to a\n // dummy compare to keep timing roughly constant on the failure path.\n const sigBuf = Buffer.from(sig, \"utf8\");\n const expBuf = Buffer.from(expected, \"utf8\");\n if (sigBuf.length !== expBuf.length) {\n crypto.timingSafeEqual(expBuf, expBuf); // burn ~equal cycles\n return { ok: false, reason: \"bad_signature\" };\n }\n if (!crypto.timingSafeEqual(sigBuf, expBuf)) {\n return { ok: false, reason: \"bad_signature\" };\n }\n\n let claims: DecodedClaims;\n try {\n claims = JSON.parse(base64UrlDecode(payloadStr).toString(\"utf8\"));\n } catch {\n return { ok: false, reason: \"bad_payload\" };\n }\n\n if (typeof claims.exp !== \"number\") {\n return { ok: false, reason: \"bad_payload\" };\n }\n if (claims.exp * 1000 < Date.now()) {\n return { ok: false, reason: \"expired\" };\n }\n if (claims.resourceId !== expectedResourceId) {\n return { ok: false, reason: \"wrong_resource\" };\n }\n\n return { ok: true, viewerEmail: claims.viewerEmail };\n}\n\n// ── Realtime subscribe tokens ────────────────────────────────────────────────\n//\n// An identity-bearing extension of the same HMAC discipline, used by the hosted\n// Realtime Gateway. Differs from the media token above in three ways the gateway\n// depends on:\n// 1. It is signed with a caller-supplied PER-PROJECT key (the app's HMAC\n// secret), not the single deployment-wide `getSigningKey()`. The app server\n// mints with it; the gateway (a second issuer for stream rotation) verifies\n// and re-mints with the same per-project key. A token minted for project A\n// fails signature verification against project B's key.\n// 2. It carries authorization-bearing identity (`owner`/`orgId`) that the\n// gateway feeds to `canSeeChangeForUser` — the media token's `viewerEmail`\n// is audit-only by contract, so a new field is required, not repurposed.\n// 3. It binds `projectId` as the channel and stamps a `typ` discriminator, so\n// it is verified against the connect channel and cannot be replayed as a\n// media token (or vice-versa) even if keys ever overlapped.\n\n/** Payload `typ` discriminator for realtime subscribe tokens. */\nexport const REALTIME_SUBSCRIBE_TOKEN_TYPE = \"rt-subscribe\";\nconst DEFAULT_REALTIME_TTL_SECONDS = 600;\n\n/** Inputs for {@link signRealtimeSubscribeToken}. */\nexport interface RealtimeSubscribeClaims {\n /** Channel — one Neon project per app. Verified at connect, not just carried. */\n projectId: string;\n /**\n * App end-user session email (NOT a Builder.io account — see the tech spec).\n * Fed to `canSeeChangeForUser`. Present in v0.\n */\n owner?: string;\n /** Framework org id for the app's end-user. */\n orgId?: string;\n /** Override default TTL (seconds). */\n ttlSeconds?: number;\n}\n\ninterface DecodedRealtimeClaims {\n typ: string;\n projectId: string;\n owner?: string;\n orgId?: string;\n exp: number;\n}\n\n/**\n * Result of {@link verifyRealtimeSubscribeToken}. On success it returns the\n * identity claims the gateway uses to scope delivery.\n */\nexport type RealtimeVerifyResult =\n | {\n ok: true;\n projectId: string;\n owner?: string;\n orgId?: string;\n exp: number;\n }\n | { ok: false; reason: string };\n\nfunction hmacB64(payloadStr: string, key: string): string {\n return base64UrlEncode(\n crypto.createHmac(\"sha256\", key).update(payloadStr).digest(),\n );\n}\n\nfunction timingSafeEqualB64(sig: string, expected: string): boolean {\n const sigBuf = Buffer.from(sig, \"utf8\");\n const expBuf = Buffer.from(expected, \"utf8\");\n if (sigBuf.length !== expBuf.length) {\n crypto.timingSafeEqual(expBuf, expBuf); // burn ~equal cycles\n return false;\n }\n return crypto.timingSafeEqual(sigBuf, expBuf);\n}\n\n/**\n * Mint a realtime subscribe token for `claims.projectId`, signed with the\n * app's per-project `key`. Safe to place on the connect query string (short\n * TTL, single-purpose, channel-bound). Verified by\n * {@link verifyRealtimeSubscribeToken} at connect.\n */\nexport function signRealtimeSubscribeToken(\n claims: RealtimeSubscribeClaims,\n key: string,\n): string {\n if (!key) throw new Error(\"signRealtimeSubscribeToken requires a key\");\n // Fail closed: a token with neither owner nor orgId carries no authorization\n // identity, so canSeeChangeForUser would only ever match global/unowned\n // events. Every issuer (mint endpoint + the gateway's rotation re-mint) must\n // supply at least one identity claim.\n if (!claims.owner && !claims.orgId) {\n throw new Error(\n \"signRealtimeSubscribeToken requires an owner or orgId claim\",\n );\n }\n const ttl = claims.ttlSeconds ?? DEFAULT_REALTIME_TTL_SECONDS;\n const payload: DecodedRealtimeClaims = {\n typ: REALTIME_SUBSCRIBE_TOKEN_TYPE,\n projectId: claims.projectId,\n exp: Math.floor(Date.now() / 1000) + ttl,\n };\n if (claims.owner) payload.owner = claims.owner;\n if (claims.orgId) payload.orgId = claims.orgId;\n\n const payloadStr = base64UrlEncode(JSON.stringify(payload));\n return `${payloadStr}.${hmacB64(payloadStr, key)}`;\n}\n\n/**\n * Verify a realtime subscribe token against the app's per-project `key` and the\n * connect channel `projectId`. Returns the identity claims only when the shape,\n * signature (constant-time), `typ`, `exp`, and `projectId` binding all hold.\n */\nexport function verifyRealtimeSubscribeToken(\n token: string,\n expected: { projectId: string; key: string },\n): RealtimeVerifyResult {\n if (!expected.key) return { ok: false, reason: \"no_key\" };\n if (typeof token !== \"string\" || !token.includes(\".\")) {\n return { ok: false, reason: \"malformed\" };\n }\n const [payloadStr, sig] = token.split(\".\", 2);\n if (!payloadStr || !sig) return { ok: false, reason: \"malformed\" };\n\n if (!timingSafeEqualB64(sig, hmacB64(payloadStr, expected.key))) {\n return { ok: false, reason: \"bad_signature\" };\n }\n\n let claims: DecodedRealtimeClaims;\n try {\n claims = JSON.parse(base64UrlDecode(payloadStr).toString(\"utf8\"));\n } catch {\n return { ok: false, reason: \"bad_payload\" };\n }\n\n if (claims.typ !== REALTIME_SUBSCRIBE_TOKEN_TYPE) {\n return { ok: false, reason: \"wrong_type\" };\n }\n if (typeof claims.exp !== \"number\") {\n return { ok: false, reason: \"bad_payload\" };\n }\n if (claims.exp * 1000 < Date.now()) {\n return { ok: false, reason: \"expired\" };\n }\n if (claims.projectId !== expected.projectId) {\n return { ok: false, reason: \"wrong_project\" };\n }\n\n return {\n ok: true,\n projectId: claims.projectId,\n owner: claims.owner,\n orgId: claims.orgId,\n exp: claims.exp,\n };\n}\n"]}
1
+ {"version":3,"file":"short-lived-token.js","sourceRoot":"","sources":["../../src/server/short-lived-token.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Short-lived HMAC-signed access tokens for media URLs.\n *\n * Used by clips and calls to mint a single-use bearer token after a password\n * gate passes, then bake `?t=<token>` into the video/blob URL handed to the\n * `<video>` element — instead of `?password=<plaintext>` (which ends up in\n * browser history, CDN logs, and Referer headers).\n *\n * Token shape: `<payloadB64Url>.<sigB64Url>`\n * payload = base64url(JSON.stringify({ resourceId, viewerEmail?, exp }))\n * sig = base64url(HMAC-SHA256(payload, key))\n *\n * Key resolution mirrors `google-oauth.ts:getStateSigningKey`:\n * 1. OAUTH_STATE_SECRET (preferred — dedicated to short-lived signing)\n * 2. BETTER_AUTH_SECRET (already used as a server secret)\n * 3. Hosted workspace deploys derive a per-purpose key from A2A_SECRET\n * 4. In dev only, an ephemeral random key (per-process)\n *\n * In production, throws if no usable server secret is set.\n */\n\nimport crypto from \"node:crypto\";\n\nimport { getWorkspaceA2ADerivedSecret } from \"./derived-secret.js\";\n\n/** Default token TTL, in seconds. 10 minutes covers a typical video session. */\nconst DEFAULT_TTL_SECONDS = 600;\n\n/**\n * Inputs for {@link signShortLivedToken}.\n */\nexport interface ShortLivedTokenClaims {\n /** Resource id the token authorises (recording id, call id, snippet id, …). */\n resourceId: string;\n /** Optional viewer email for audit / analytics — not used for authorisation. */\n viewerEmail?: string;\n /** Override default TTL (seconds). */\n ttlSeconds?: number;\n}\n\ninterface DecodedClaims {\n resourceId: string;\n viewerEmail?: string;\n exp: number;\n}\n\n/**\n * Result of {@link verifyShortLivedToken}. Discriminated by the literal\n * `ok` field so callers can `if (!result.ok) return …`.\n */\nexport type VerifyResult =\n | { ok: true; viewerEmail?: string }\n | { ok: false; reason: string };\n\nlet _devSigningKey: string | undefined;\n\nfunction getSigningKey(): string {\n const secret =\n process.env.OAUTH_STATE_SECRET ||\n process.env.BETTER_AUTH_SECRET ||\n getWorkspaceA2ADerivedSecret(\"short-lived-token\");\n if (secret) return secret;\n\n if (process.env.NODE_ENV === \"production\") {\n throw new Error(\n \"Short-lived token signing requires a server secret. \" +\n \"Set OAUTH_STATE_SECRET, BETTER_AUTH_SECRET, or A2A_SECRET in production workspace deploys.\",\n );\n }\n\n if (!_devSigningKey) {\n _devSigningKey = crypto.randomBytes(32).toString(\"hex\");\n }\n return _devSigningKey;\n}\n\nfunction base64UrlEncode(buf: Buffer | string): string {\n const b = typeof buf === \"string\" ? Buffer.from(buf, \"utf8\") : buf;\n return b\n .toString(\"base64\")\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=+$/g, \"\");\n}\n\nfunction base64UrlDecode(s: string): Buffer {\n // Re-pad to a multiple of 4 so Buffer.from('base64') decodes cleanly.\n const padded = s + \"=\".repeat((4 - (s.length % 4)) % 4);\n return Buffer.from(padded.replace(/-/g, \"+\").replace(/_/g, \"/\"), \"base64\");\n}\n\n/**\n * Mint a signed token authorising read access to `claims.resourceId` until\n * `exp = now + ttl`. The result is safe to drop into a query string —\n * `?t=<token>` — and verified by {@link verifyShortLivedToken} on the\n * downstream route.\n */\nexport function signShortLivedToken(claims: ShortLivedTokenClaims): string {\n const ttl = claims.ttlSeconds ?? DEFAULT_TTL_SECONDS;\n const payload: DecodedClaims = {\n resourceId: claims.resourceId,\n exp: Math.floor(Date.now() / 1000) + ttl,\n };\n if (claims.viewerEmail) payload.viewerEmail = claims.viewerEmail;\n\n const payloadStr = base64UrlEncode(JSON.stringify(payload));\n const sig = base64UrlEncode(\n crypto.createHmac(\"sha256\", getSigningKey()).update(payloadStr).digest(),\n );\n return `${payloadStr}.${sig}`;\n}\n\n/**\n * Verify a token previously produced by {@link signShortLivedToken}.\n *\n * Returns `{ ok: true, viewerEmail? }` only when:\n * - the token has the expected shape (`<payload>.<sig>`),\n * - the signature matches via constant-time comparison,\n * - the token has not expired,\n * - the embedded `resourceId` matches `expectedResourceId`.\n *\n * Otherwise returns `{ ok: false, reason: <error string> }`. Callers should\n * not surface the reason to viewers (it's useful for server-side logs only).\n */\nexport function verifyShortLivedToken(\n token: string,\n expectedResourceId: string,\n): VerifyResult {\n if (typeof token !== \"string\" || !token.includes(\".\")) {\n return { ok: false, reason: \"malformed\" };\n }\n const [payloadStr, sig] = token.split(\".\", 2);\n if (!payloadStr || !sig) return { ok: false, reason: \"malformed\" };\n\n const expected = base64UrlEncode(\n crypto.createHmac(\"sha256\", getSigningKey()).update(payloadStr).digest(),\n );\n\n // Constant-time compare. Length-mismatched inputs would throw under\n // `crypto.timingSafeEqual`, so we check length first and fall back to a\n // dummy compare to keep timing roughly constant on the failure path.\n const sigBuf = Buffer.from(sig, \"utf8\");\n const expBuf = Buffer.from(expected, \"utf8\");\n if (sigBuf.length !== expBuf.length) {\n crypto.timingSafeEqual(expBuf, expBuf); // burn ~equal cycles\n return { ok: false, reason: \"bad_signature\" };\n }\n if (!crypto.timingSafeEqual(sigBuf, expBuf)) {\n return { ok: false, reason: \"bad_signature\" };\n }\n\n let claims: DecodedClaims;\n try {\n claims = JSON.parse(base64UrlDecode(payloadStr).toString(\"utf8\"));\n } catch {\n return { ok: false, reason: \"bad_payload\" };\n }\n\n if (typeof claims.exp !== \"number\") {\n return { ok: false, reason: \"bad_payload\" };\n }\n if (claims.exp * 1000 < Date.now()) {\n return { ok: false, reason: \"expired\" };\n }\n if (claims.resourceId !== expectedResourceId) {\n return { ok: false, reason: \"wrong_resource\" };\n }\n\n return { ok: true, viewerEmail: claims.viewerEmail };\n}\n\n// ── Realtime subscribe tokens ────────────────────────────────────────────────\n//\n// An identity-bearing extension of the same HMAC discipline, used by the hosted\n// Realtime Gateway. Differs from the media token above in three ways the gateway\n// depends on:\n// 1. It is signed with a caller-supplied PER-PROJECT key (the app's HMAC\n// secret), not the single deployment-wide `getSigningKey()`. The app server\n// mints with it; the gateway (a second issuer for stream rotation) verifies\n// and re-mints with the same per-project key. A token minted for project A\n// fails signature verification against project B's key.\n// 2. It carries authorization-bearing identity (`owner`/`orgId`) that the\n// gateway feeds to `canSeeChangeForUser` — the media token's `viewerEmail`\n// is audit-only by contract, so a new field is required, not repurposed.\n// 3. It binds `projectId` as the channel and stamps a `typ` discriminator, so\n// it is verified against the connect channel and cannot be replayed as a\n// media token (or vice-versa) even if keys ever overlapped.\n\n/** Payload `typ` discriminator for realtime subscribe tokens. */\nexport const REALTIME_SUBSCRIBE_TOKEN_TYPE = \"rt-subscribe\";\nconst DEFAULT_REALTIME_TTL_SECONDS = 600;\n\n/** Inputs for {@link signRealtimeSubscribeToken}. */\nexport interface RealtimeSubscribeClaims {\n /** Channel — one Neon project per app. Verified at connect, not just carried. */\n projectId: string;\n /**\n * App end-user session email (NOT a Builder.io account — see the tech spec).\n * Fed to `canSeeChangeForUser`. Present in v0.\n */\n owner?: string;\n /** Framework org id for the app's end-user. */\n orgId?: string;\n /** Override default TTL (seconds). */\n ttlSeconds?: number;\n}\n\ninterface DecodedRealtimeClaims {\n typ: string;\n projectId: string;\n owner?: string;\n orgId?: string;\n exp: number;\n}\n\n/**\n * Result of {@link verifyRealtimeSubscribeToken}. On success it returns the\n * identity claims the gateway uses to scope delivery.\n */\nexport type RealtimeVerifyResult =\n | {\n ok: true;\n projectId: string;\n owner?: string;\n orgId?: string;\n exp: number;\n }\n | { ok: false; reason: string };\n\nfunction hmacB64(payloadStr: string, key: string): string {\n return base64UrlEncode(\n crypto.createHmac(\"sha256\", key).update(payloadStr).digest(),\n );\n}\n\nfunction timingSafeEqualB64(sig: string, expected: string): boolean {\n const sigBuf = Buffer.from(sig, \"utf8\");\n const expBuf = Buffer.from(expected, \"utf8\");\n if (sigBuf.length !== expBuf.length) {\n crypto.timingSafeEqual(expBuf, expBuf); // burn ~equal cycles\n return false;\n }\n return crypto.timingSafeEqual(sigBuf, expBuf);\n}\n\n/**\n * Mint a realtime subscribe token for `claims.projectId`, signed with the\n * app's per-project `key`. Safe to place on the connect query string (short\n * TTL, single-purpose, channel-bound). Verified by\n * {@link verifyRealtimeSubscribeToken} at connect.\n */\nexport function signRealtimeSubscribeToken(\n claims: RealtimeSubscribeClaims,\n key: string,\n): string {\n if (!key) throw new Error(\"signRealtimeSubscribeToken requires a key\");\n // Fail closed: a token with neither owner nor orgId carries no authorization\n // identity, so canSeeChangeForUser would only ever match global/unowned\n // events. Every issuer (mint endpoint + the gateway's rotation re-mint) must\n // supply at least one identity claim.\n if (!claims.owner && !claims.orgId) {\n throw new Error(\n \"signRealtimeSubscribeToken requires an owner or orgId claim\",\n );\n }\n const ttl = claims.ttlSeconds ?? DEFAULT_REALTIME_TTL_SECONDS;\n const payload: DecodedRealtimeClaims = {\n typ: REALTIME_SUBSCRIBE_TOKEN_TYPE,\n projectId: claims.projectId,\n exp: Math.floor(Date.now() / 1000) + ttl,\n };\n if (claims.owner) payload.owner = claims.owner;\n if (claims.orgId) payload.orgId = claims.orgId;\n\n const payloadStr = base64UrlEncode(JSON.stringify(payload));\n return `${payloadStr}.${hmacB64(payloadStr, key)}`;\n}\n\n/**\n * Verify a realtime subscribe token against the app's per-project `key` and the\n * connect channel `projectId`. Returns the identity claims only when the shape,\n * signature (constant-time), `typ`, `exp`, and `projectId` binding all hold.\n */\nexport function verifyRealtimeSubscribeToken(\n token: string,\n expected: { projectId: string; key: string },\n): RealtimeVerifyResult {\n if (!expected.key) return { ok: false, reason: \"no_key\" };\n if (typeof token !== \"string\" || !token.includes(\".\")) {\n return { ok: false, reason: \"malformed\" };\n }\n const [payloadStr, sig] = token.split(\".\", 2);\n if (!payloadStr || !sig) return { ok: false, reason: \"malformed\" };\n\n if (!timingSafeEqualB64(sig, hmacB64(payloadStr, expected.key))) {\n return { ok: false, reason: \"bad_signature\" };\n }\n\n let claims: DecodedRealtimeClaims;\n try {\n claims = JSON.parse(base64UrlDecode(payloadStr).toString(\"utf8\"));\n } catch {\n return { ok: false, reason: \"bad_payload\" };\n }\n\n if (claims.typ !== REALTIME_SUBSCRIBE_TOKEN_TYPE) {\n return { ok: false, reason: \"wrong_type\" };\n }\n if (typeof claims.exp !== \"number\") {\n return { ok: false, reason: \"bad_payload\" };\n }\n if (claims.exp * 1000 < Date.now()) {\n return { ok: false, reason: \"expired\" };\n }\n if (claims.projectId !== expected.projectId) {\n return { ok: false, reason: \"wrong_project\" };\n }\n\n return {\n ok: true,\n projectId: claims.projectId,\n owner: claims.owner,\n orgId: claims.orgId,\n exp: claims.exp,\n };\n}\n\n// ── Gateway access-check tokens ──────────────────────────────────────────────\n//\n// The hosted gateway has no access to an app's shareable-resource registry, so\n// it cannot resolve sharee visibility itself. It signs one of these with the\n// app's per-project key and calls the app's `/_agent-native/can-see`, which runs\n// `resolveAccess` and answers. The full access query is bound into the token so\n// the app authenticates the params, not merely the caller.\n\n/** Payload `typ` discriminator for gateway access-check tokens. */\nexport const GATEWAY_ACCESS_TOKEN_TYPE = \"rt-access-check\";\n/** Short TTL: minted per check, used immediately server-to-server. */\nconst DEFAULT_GATEWAY_ACCESS_TTL_SECONDS = 60;\n\n/** Inputs for {@link signGatewayAccessToken}. */\nexport interface GatewayAccessClaims {\n projectId: string;\n resourceType: string;\n resourceId: string;\n /** App end-user whose visibility of the resource is being checked. */\n userEmail: string;\n orgId?: string;\n ttlSeconds?: number;\n}\n\ninterface DecodedGatewayAccessClaims {\n typ: string;\n projectId: string;\n resourceType: string;\n resourceId: string;\n userEmail: string;\n orgId?: string;\n exp: number;\n}\n\n/** Result of {@link verifyGatewayAccessToken}; on success carries the bound query. */\nexport type GatewayAccessVerifyResult =\n | {\n ok: true;\n projectId: string;\n resourceType: string;\n resourceId: string;\n userEmail: string;\n orgId?: string;\n }\n | { ok: false; reason: string };\n\n/** Mint a gateway access-check token, signed with the app's per-project `key`. */\nexport function signGatewayAccessToken(\n claims: GatewayAccessClaims,\n key: string,\n): string {\n if (!key) throw new Error(\"signGatewayAccessToken requires a key\");\n const ttl = claims.ttlSeconds ?? DEFAULT_GATEWAY_ACCESS_TTL_SECONDS;\n const payload: DecodedGatewayAccessClaims = {\n typ: GATEWAY_ACCESS_TOKEN_TYPE,\n projectId: claims.projectId,\n resourceType: claims.resourceType,\n resourceId: claims.resourceId,\n userEmail: claims.userEmail,\n exp: Math.floor(Date.now() / 1000) + ttl,\n };\n if (claims.orgId) payload.orgId = claims.orgId;\n const payloadStr = base64UrlEncode(JSON.stringify(payload));\n return `${payloadStr}.${hmacB64(payloadStr, key)}`;\n}\n\n/** Verify a gateway access-check token against the app's per-project `key`. */\nexport function verifyGatewayAccessToken(\n token: string,\n key: string,\n expectedProjectId?: string,\n): GatewayAccessVerifyResult {\n if (!key) return { ok: false, reason: \"no_key\" };\n if (typeof token !== \"string\" || !token.includes(\".\")) {\n return { ok: false, reason: \"malformed\" };\n }\n const [payloadStr, sig] = token.split(\".\", 2);\n if (!payloadStr || !sig) return { ok: false, reason: \"malformed\" };\n\n if (!timingSafeEqualB64(sig, hmacB64(payloadStr, key))) {\n return { ok: false, reason: \"bad_signature\" };\n }\n\n let claims: DecodedGatewayAccessClaims;\n try {\n claims = JSON.parse(base64UrlDecode(payloadStr).toString(\"utf8\"));\n } catch {\n return { ok: false, reason: \"bad_payload\" };\n }\n\n if (claims.typ !== GATEWAY_ACCESS_TOKEN_TYPE) {\n return { ok: false, reason: \"wrong_type\" };\n }\n if (typeof claims.exp !== \"number\")\n return { ok: false, reason: \"bad_payload\" };\n if (claims.exp * 1000 < Date.now()) return { ok: false, reason: \"expired\" };\n if (\n !claims.projectId ||\n !claims.resourceType ||\n !claims.resourceId ||\n !claims.userEmail\n ) {\n return { ok: false, reason: \"bad_payload\" };\n }\n // Optional channel binding, mirroring verifyRealtimeSubscribeToken. The\n // per-project key already scopes verification to one app; this is belt-and-\n // suspenders for a future multi-tenant secret store. Skipped when the caller\n // can't cheaply resolve its own project id (scoped-secret apps).\n if (expectedProjectId && claims.projectId !== expectedProjectId) {\n return { ok: false, reason: \"wrong_project\" };\n }\n\n return {\n ok: true,\n projectId: claims.projectId,\n resourceType: claims.resourceType,\n resourceId: claims.resourceId,\n userEmail: claims.userEmail,\n orgId: claims.orgId,\n };\n}\n"]}
@@ -20,7 +20,7 @@ export declare function createTranscribeVoiceHandler(): import("h3").EventHandle
20
20
  error: string;
21
21
  text?: undefined;
22
22
  } | {
23
- error?: undefined;
24
23
  text: string;
24
+ error?: undefined;
25
25
  }>>;
26
26
  //# sourceMappingURL=transcribe-voice.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.120.4",
3
+ "version": "0.121.1",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -114,10 +114,15 @@ function isNetlifyHostedRuntimeForDispatch(): boolean {
114
114
  if (process.env.NETLIFY_LOCAL === "true") return false;
115
115
  if (process.env.NETLIFY === "false") return false;
116
116
  if (process.env.NETLIFY && process.env.NETLIFY !== "false") return true;
117
- // Netlify sets AWS Lambda runtime env on deployed Functions, but the build-time
118
- // NETLIFY flag is not always present in the runtime isolate. Treat Lambda as
119
- // Netlify here unless Netlify was explicitly disabled above; non-Netlify AWS
120
- // falls back inline if the /.netlify/functions dispatch fast-fails.
117
+ // NETLIFY is a build-only read-only variable. In deployed Functions Netlify
118
+ // documents URL, SITE_NAME, and SITE_ID as the runtime read-only variables;
119
+ // SITE_ID is the unambiguous host marker. Lambda compatibility mode also
120
+ // exposes AWS runtime variables, so keep the function-name fallback for older
121
+ // deploys. Without either check a modern Netlify Function silently selects the
122
+ // portable framework route even though the emitted background function exists.
123
+ if (process.env.SITE_ID) return true; // guard:allow-env-credential -- Netlify's read-only public site identifier is a runtime host marker, not a user credential.
124
+ // Non-Netlify AWS falls back inline if the /.netlify/functions dispatch
125
+ // fast-fails.
121
126
  return Boolean(process.env.AWS_LAMBDA_FUNCTION_NAME);
122
127
  }
123
128
 
@@ -202,6 +207,9 @@ export const AGENT_CHAT_BACKGROUND_RUN_FIELD = "__backgroundRun";
202
207
  * what "hosted" means.
203
208
  */
204
209
  export function isHostedRuntimeForDurableBackground(): boolean {
210
+ if (process.env.NETLIFY_LOCAL === "true") return false;
211
+ if (process.env.NETLIFY === "false") return false;
212
+ if (process.env.SITE_ID) return true; // guard:allow-env-credential -- Netlify's read-only public site identifier is a runtime host marker, not a user credential.
205
213
  if (
206
214
  process.env.NETLIFY &&
207
215
  process.env.NETLIFY !== "false" &&
@@ -319,6 +319,9 @@ export interface ResolveRunSoftTimeoutOptions {
319
319
  * that clamp (and the platform wall behind it) exists.
320
320
  */
321
321
  export function isHostedRuntime(): boolean {
322
+ if (process.env.NETLIFY_LOCAL === "true") return false;
323
+ if (process.env.NETLIFY === "false") return false;
324
+ if (process.env.SITE_ID) return true; // guard:allow-env-credential -- Netlify's read-only public site identifier is a runtime host marker, not a user credential.
322
325
  if (
323
326
  process.env.NETLIFY &&
324
327
  process.env.NETLIFY !== "false" &&
@@ -330,9 +330,11 @@ class SyncTransport {
330
330
 
331
331
  private get activeSseUrl(): string | false {
332
332
  if (this.mode === "hosted" && this.gateway) {
333
- return this.token
334
- ? `${this.gateway.sseUrl}?token=${encodeURIComponent(this.token)}`
335
- : this.gateway.sseUrl;
333
+ if (!this.token) return this.gateway.sseUrl;
334
+ const base = `${this.gateway.sseUrl}?token=${encodeURIComponent(this.token)}`;
335
+ // Cursor lets the gateway replay the reconnect gap on connect instead of
336
+ // deferring it to the next poll; 0 on first connect means nothing to replay.
337
+ return this.versionRef > 0 ? `${base}&since=${this.versionRef}` : base;
336
338
  }
337
339
  return this.sseUrl;
338
340
  }
@@ -656,27 +658,27 @@ class SyncTransport {
656
658
  this.schedulePoll();
657
659
  };
658
660
  source.onerror = () => {
661
+ // A replaced/closed source can still fire late; ignore it so it can't
662
+ // flip the connected state or tear down the current stream.
663
+ if (this.eventSource !== source) return;
659
664
  this.setSseConnected(false);
660
- // When the browser gives up permanently (HTTP error → readyState
661
- // CLOSED), it won't auto-reconnect. Drop the ref so a later
662
- // connectEvents() (on focus/visibility) can establish a fresh stream;
663
- // otherwise the non-null closed `eventSource` blocks reconnection and
664
- // we'd be stuck on polling-only forever.
665
+ if (this.mode === "hosted" && this.gateway) {
666
+ // Browser auto-reconnect reuses the URL frozen at construction, so it
667
+ // would replay from a stale `since`. Own the reconnect so the next
668
+ // connect rebuilds activeSseUrl from the current versionRef. CLOSED also
669
+ // refreshes the token (expired/rotated/deploy); CONNECTING keeps it. A
670
+ // successful reconnect resets the count in onopen; a hard-down gateway
671
+ // trips the threshold and health-gates to local.
672
+ if (source.readyState === EventSource.CLOSED) this.token = null;
673
+ this.closeEvents();
674
+ this.onGatewayTransientFailure();
675
+ if (this.mode === "hosted") this.scheduleGatewayReconnect();
676
+ return;
677
+ }
678
+ // Local mode: native EventSource reconnect is fine. Drop a CLOSED ref so a
679
+ // later connectEvents() (focus/visibility) can establish a fresh stream.
665
680
  if (source.readyState === EventSource.CLOSED) {
666
681
  this.eventSource = null;
667
- if (this.mode === "hosted" && this.gateway) {
668
- // A closed gateway stream is most likely an expired token or a
669
- // request-timeout/deploy cycle. Re-mint and reconnect with jitter;
670
- // this is NOT the poll-401 cooldown path. Each closed stream counts
671
- // toward the unhealthy threshold so a hard-down gateway (or one
672
- // rejecting our tokens) health-gates to local instead of looping
673
- // mint+connect forever; a successful reconnect resets the count in
674
- // onopen above.
675
- this.token = null;
676
- this.onGatewayTransientFailure();
677
- if (this.mode === "hosted") this.scheduleGatewayReconnect();
678
- return;
679
- }
680
682
  }
681
683
  this.schedulePoll();
682
684
  };
@@ -2447,6 +2447,7 @@ export const config = {
2447
2447
  name: "agent-native server keep warm",
2448
2448
  generator: "agent-native build",
2449
2449
  schedule: "* * * * *",
2450
+ nodeBundler: "none",
2450
2451
  };
2451
2452
  `;
2452
2453
 
@@ -159,6 +159,7 @@ import {
159
159
  markDefaultPluginProvided,
160
160
  trackPluginInit,
161
161
  } from "./framework-request-handler.js";
162
+ import { createGatewayAccessCheckHandler } from "./gateway-access-check.js";
162
163
  import { getAppBasePath, getOrigin } from "./google-oauth.js";
163
164
  import { createGoogleRealtimeSessionHandler } from "./google-realtime-session.js";
164
165
  import {
@@ -1429,6 +1430,8 @@ export function createCoreRoutesPlugin(
1429
1430
  `${P}/realtime-token`,
1430
1431
  createRealtimeTokenHandler(),
1431
1432
  );
1433
+ // Sharee visibility check for the hosted gateway
1434
+ getH3App(nitroApp).use(`${P}/can-see`, createGatewayAccessCheckHandler());
1432
1435
 
1433
1436
  // SSE
1434
1437
  if (!options.disableSSE) {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `GET /_agent-native/can-see` — the hosted Realtime Gateway's sharee-visibility
3
+ * check. Verifies a gateway access-check token (rationale in
4
+ * short-lived-token.ts), runs the app's own `resolveAccess`, answers
5
+ * `{ allowed }`, and fails closed.
6
+ */
7
+
8
+ import {
9
+ defineEventHandler,
10
+ getMethod,
11
+ getQuery,
12
+ type H3Event,
13
+ setResponseHeader,
14
+ setResponseStatus,
15
+ } from "h3";
16
+
17
+ import { resolveAccess } from "../sharing/access.js";
18
+ import { getBuilderBranchProjectId } from "./builder-browser.js";
19
+ import { getRealtimeSigningSecret } from "./realtime-token.js";
20
+ import { runWithRequestContext } from "./request-context.js";
21
+ import { verifyGatewayAccessToken } from "./short-lived-token.js";
22
+
23
+ export function createGatewayAccessCheckHandler() {
24
+ return defineEventHandler(async (event: H3Event) => {
25
+ setResponseHeader(event, "Cache-Control", "private, no-store");
26
+
27
+ if (getMethod(event) !== "GET") {
28
+ setResponseStatus(event, 405);
29
+ return { error: "Method not allowed" };
30
+ }
31
+
32
+ const secret = getRealtimeSigningSecret();
33
+ if (!secret) {
34
+ setResponseStatus(event, 404);
35
+ return { error: "Realtime gateway not configured" };
36
+ }
37
+
38
+ const token = getQuery(event).token;
39
+ // Sync, env-only: binds the token's channel when this app's project id is
40
+ // known, and no-ops (undefined) for scoped-secret apps where it isn't.
41
+ const expectedProjectId = getBuilderBranchProjectId() || undefined;
42
+ const verified =
43
+ typeof token === "string"
44
+ ? verifyGatewayAccessToken(token, secret, expectedProjectId)
45
+ : ({ ok: false, reason: "missing" } as const);
46
+ if (!verified.ok) {
47
+ setResponseStatus(event, 401);
48
+ return { error: "Unauthorized" };
49
+ }
50
+
51
+ const { resourceType, resourceId, userEmail, orgId } = verified;
52
+ return runWithRequestContext({ userEmail, orgId }, async () => {
53
+ try {
54
+ const access = await resolveAccess(
55
+ resourceType,
56
+ resourceId,
57
+ { userEmail, orgId },
58
+ { skipResourceBody: true },
59
+ );
60
+ return { allowed: access != null };
61
+ } catch {
62
+ // Unknown resource type or lookup failure: fail closed.
63
+ return { allowed: false };
64
+ }
65
+ });
66
+ });
67
+ }