@k2b/cloud 0.42.0 → 0.44.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.
Files changed (352) hide show
  1. package/package.json +13 -5
  2. package/scripts/app-favicon.ts +7 -6
  3. package/scripts/build.ts +3 -0
  4. package/scripts/installable-assets.ts +61 -0
  5. package/scripts/preload.ts +3 -0
  6. package/src/_internal/app-access-path.ts +12 -0
  7. package/src/_internal/app-access-request.ts +13 -0
  8. package/src/_internal/app-access-resources.ts +141 -0
  9. package/src/_internal/app-event-catalog.ts +40 -0
  10. package/src/_internal/app-event-schemas.ts +127 -0
  11. package/src/_internal/application-package.ts +40 -0
  12. package/src/_internal/built-in-app-ids.json +27 -0
  13. package/src/_internal/capabilities.ts +185 -12
  14. package/src/_internal/capability-origin.ts +4 -0
  15. package/src/_internal/capability-streams.ts +10 -5
  16. package/src/_internal/define-app.ts +159 -45
  17. package/src/_internal/installable-assets.ts +90 -0
  18. package/src/_internal/installable-head.ts +22 -0
  19. package/src/_internal/object-store-area.ts +9 -0
  20. package/src/_internal/object-store-config.ts +101 -0
  21. package/src/_internal/object-store-key.ts +14 -0
  22. package/src/_internal/page-responses.ts +34 -3
  23. package/src/_internal/process-identity.ts +7 -2
  24. package/src/_internal/registry-validation.ts +15 -1
  25. package/src/_internal/registry.ts +44 -1
  26. package/src/_internal/runtime-context.ts +9 -1
  27. package/src/_internal/runtime-watcher.ts +8 -0
  28. package/src/_internal/status-preserving-ssr.ts +12 -1
  29. package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
  30. package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
  31. package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
  32. package/src/access/AppAccessEditor.tsx +380 -0
  33. package/src/access/PermissionEditor.browser-harness.tsx +22 -2
  34. package/src/access/PermissionEditor.tsx +386 -77
  35. package/src/access/PrincipalAvatar.tsx +63 -0
  36. package/src/access/PrincipalPicker.tsx +29 -8
  37. package/src/access/ResourceApiKeys.tsx +3 -4
  38. package/src/access/app-access-adapter.ts +51 -0
  39. package/src/access/app-access-messages.ts +172 -0
  40. package/src/access/messages.ts +14 -0
  41. package/src/access/ui.ts +4 -1
  42. package/src/account/Avatar.tsx +21 -3
  43. package/src/account/ProfilePictureDialog.tsx +699 -0
  44. package/src/account/profile-picture-messages.ts +269 -0
  45. package/src/account/ui.ts +8 -1
  46. package/src/ai/admin.ts +3 -0
  47. package/src/ai/app-frame.ts +12 -0
  48. package/src/ai/application-tasks.ts +101 -0
  49. package/src/ai/assistant-models.ts +126 -64
  50. package/src/ai/capabilities.ts +19 -4
  51. package/src/ai/capability-execution.ts +5 -2
  52. package/src/ai/chart-block.ts +63 -30
  53. package/src/ai/chat/chart-block.tsx +12 -3
  54. package/src/ai/chat/composer-adapter.ts +8 -7
  55. package/src/ai/chat/message-actions.tsx +1 -1
  56. package/src/ai/chat/message-utils.ts +2 -2
  57. package/src/ai/chat/presentation.tsx +22 -1
  58. package/src/ai/chat-quotas.ts +5 -4
  59. package/src/ai/chat-tasks.ts +2 -2
  60. package/src/ai/client/controller.ts +11 -10
  61. package/src/ai/client/projection.ts +3 -3
  62. package/src/ai/code-capability-routes.ts +1 -0
  63. package/src/ai/compaction.ts +3 -3
  64. package/src/ai/credentials.ts +13 -114
  65. package/src/ai/default-tool-contracts.ts +4 -3
  66. package/src/ai/dictation-runtime.ts +4 -4
  67. package/src/ai/enrich.ts +2 -2
  68. package/src/ai/executor.ts +38 -27
  69. package/src/ai/http.ts +17 -29
  70. package/src/ai/index.ts +11 -5
  71. package/src/ai/inference-calls.ts +6 -4
  72. package/src/ai/maintenance.ts +1 -1
  73. package/src/ai/memory-learning-runs.ts +5 -5
  74. package/src/ai/memory-learning.ts +3 -3
  75. package/src/ai/message-queue.ts +1 -2
  76. package/src/ai/migrate.ts +75 -42
  77. package/src/ai/model-access-migrate.ts +3 -17
  78. package/src/ai/model-access.ts +44 -85
  79. package/src/ai/model-card.tsx +134 -0
  80. package/src/ai/model-catalog.ts +359 -0
  81. package/src/ai/model-chooser-messages.ts +88 -0
  82. package/src/ai/model-chooser.tsx +577 -0
  83. package/src/ai/model-delete.ts +234 -0
  84. package/src/ai/model-fixture.ts +39 -0
  85. package/src/ai/model-icon.tsx +36 -0
  86. package/src/ai/model-overview.ts +53 -0
  87. package/src/ai/model-usage.ts +45 -0
  88. package/src/ai/openai-api-messages.ts +0 -4
  89. package/src/ai/openai-api-slots.ts +10 -2
  90. package/src/ai/openai-api.ts +83 -16
  91. package/src/ai/personal-agent.ts +1 -1
  92. package/src/ai/prefs.ts +12 -10
  93. package/src/ai/projects-routes.ts +2 -1
  94. package/src/ai/projects.ts +13 -12
  95. package/src/ai/protocol.ts +2 -2
  96. package/src/ai/provider-connections.ts +161 -0
  97. package/src/ai/provider-errors.ts +138 -0
  98. package/src/ai/provider-fetch.ts +4 -0
  99. package/src/ai/provider-retry.ts +1 -1
  100. package/src/ai/provider.ts +2 -1
  101. package/src/ai/quota-limits.ts +36 -0
  102. package/src/ai/quota-provider.ts +11 -2
  103. package/src/ai/quota-report.ts +7 -4
  104. package/src/ai/quotas-migrate.ts +1 -0
  105. package/src/ai/quotas.ts +19 -8
  106. package/src/ai/recent-resources.ts +6 -0
  107. package/src/ai/request-headers.ts +14 -87
  108. package/src/ai/resource-refs.ts +6 -2
  109. package/src/ai/routes.ts +17 -18
  110. package/src/ai/runtime.ts +50 -22
  111. package/src/ai/settings.ts +93 -580
  112. package/src/ai/store.ts +45 -23
  113. package/src/ai/stream.ts +1 -1
  114. package/src/ai/structured-runs.ts +2 -2
  115. package/src/ai/structured.ts +29 -46
  116. package/src/ai/task-contracts.ts +25 -5
  117. package/src/ai/task-execution.ts +15 -6
  118. package/src/ai/task-model.ts +9 -0
  119. package/src/ai/transcription.ts +37 -21
  120. package/src/ai/turn-failure.ts +24 -2
  121. package/src/ai/types.ts +50 -62
  122. package/src/ai/ui.tsx +13 -0
  123. package/src/ai/usage.ts +8 -8
  124. package/src/ai/validate.ts +8 -7
  125. package/src/ai/view-block.ts +2 -2
  126. package/src/api/accounts-entities.ts +40 -16
  127. package/src/api/admin-ai-quotas.ts +34 -56
  128. package/src/api/admin-ai-usage.ts +1 -1
  129. package/src/api/admin-ai.ts +173 -0
  130. package/src/api/admin-core-settings.ts +68 -244
  131. package/src/api/admin-directory-shares.ts +55 -0
  132. package/src/api/admin-identity.ts +81 -2
  133. package/src/api/app-access.ts +85 -0
  134. package/src/api/app-approval.ts +3 -1
  135. package/src/api/app-event-delivery.ts +51 -0
  136. package/src/api/app-events.ts +123 -0
  137. package/src/api/app-resource-access.ts +48 -0
  138. package/src/api/auth/account-mismatch.ts +24 -0
  139. package/src/api/auth.ts +26 -3
  140. package/src/api/capabilities.ts +127 -15
  141. package/src/api/capability-streams.ts +2 -5
  142. package/src/api/identity-child-invocation.ts +145 -0
  143. package/src/api/index.ts +11 -2
  144. package/src/api/mcp.ts +180 -5
  145. package/src/api/me.ts +38 -17
  146. package/src/api/pwa-phone.ts +87 -48
  147. package/src/api/pwa.ts +34 -8
  148. package/src/api/resource-links.ts +83 -6
  149. package/src/api/resource-resolve.ts +50 -0
  150. package/src/api/search/schemas.ts +3 -1
  151. package/src/api/search.ts +9 -7
  152. package/src/api/widgets.ts +1 -0
  153. package/src/app-pwa/authority.ts +75 -0
  154. package/src/app-pwa/contracts.ts +177 -0
  155. package/src/app-pwa/core-query-error.ts +20 -0
  156. package/src/app-pwa/index.ts +23 -0
  157. package/src/app-pwa/protocol.ts +27 -0
  158. package/src/app-pwa/schema.ts +59 -0
  159. package/src/app-pwa/service.ts +861 -0
  160. package/src/app-pwa/transaction.ts +338 -0
  161. package/src/app-pwa/transport-input.ts +57 -0
  162. package/src/app-pwa/transport.ts +235 -0
  163. package/src/approvals/messages.ts +2 -2
  164. package/src/browser/app-access-recovery.ts +44 -0
  165. package/src/browser/app-session.ts +14 -3
  166. package/src/browser/cloud-id.ts +86 -0
  167. package/src/browser/live-websocket.ts +5 -0
  168. package/src/browser/pwa-drafts.ts +57 -0
  169. package/src/browser/resource-clipboard.ts +4 -0
  170. package/src/browser/resource-links.ts +52 -0
  171. package/src/browser/resource-picker.tsx +3 -2
  172. package/src/capabilities/calendar-sources.ts +302 -0
  173. package/src/capabilities/executions.ts +10 -3
  174. package/src/capabilities/migrate.ts +4 -0
  175. package/src/capabilities/nested.ts +43 -0
  176. package/src/capabilities/server-invoke.ts +202 -0
  177. package/src/capabilities/server.ts +26 -187
  178. package/src/capabilities/testing.ts +5 -2
  179. package/src/cli/access.ts +6 -5
  180. package/src/cli/account.ts +57 -0
  181. package/src/cli/admin/agents.ts +66 -0
  182. package/src/cli/admin/ai-models.ts +199 -0
  183. package/src/cli/admin/ai-quotas.ts +6 -102
  184. package/src/cli/admin/ai-usage.ts +4 -4
  185. package/src/cli/admin/app-credentials.ts +5 -3
  186. package/src/cli/admin/index.ts +7 -3
  187. package/src/cli/app-access.ts +60 -0
  188. package/src/cli/apps.ts +89 -26
  189. package/src/cli/avatar.ts +64 -0
  190. package/src/cli/capabilities.ts +22 -1
  191. package/src/cli/events.ts +320 -0
  192. package/src/cli/index.ts +1 -1
  193. package/src/config/env.ts +40 -0
  194. package/src/contracts/app-access.ts +52 -5
  195. package/src/contracts/app-approval.ts +8 -1
  196. package/src/contracts/app-details.ts +89 -0
  197. package/src/contracts/app-events.ts +51 -0
  198. package/src/contracts/app.ts +24 -5
  199. package/src/contracts/calendar-source.ts +274 -0
  200. package/src/contracts/calendar-view.ts +35 -0
  201. package/src/contracts/capabilities.ts +164 -3
  202. package/src/contracts/geo-admin.ts +2 -0
  203. package/src/contracts/geo-route.ts +0 -11
  204. package/src/contracts/geo.ts +14 -2
  205. package/src/contracts/index.ts +6 -0
  206. package/src/contracts/installable.ts +40 -0
  207. package/src/contracts/notification-types.ts +36 -9
  208. package/src/contracts/profile.ts +32 -0
  209. package/src/contracts/pwa-builds.ts +14 -0
  210. package/src/contracts/pwa-location.ts +38 -0
  211. package/src/contracts/pwa.ts +24 -3
  212. package/src/contracts/registry.ts +8 -0
  213. package/src/contracts/resource-clipboard.ts +35 -0
  214. package/src/contracts/resource-pages.ts +140 -0
  215. package/src/contracts/resource-resolution.ts +13 -0
  216. package/src/contracts/shared.ts +4 -0
  217. package/src/contracts/user-notifications.ts +2 -0
  218. package/src/events/app-event-delivery.ts +100 -0
  219. package/src/events/app-events.testing.ts +65 -0
  220. package/src/events/app-events.ts +134 -0
  221. package/src/events/core-user-created.ts +24 -0
  222. package/src/events/index.ts +9 -0
  223. package/src/events/testing.ts +2 -0
  224. package/src/index.ts +2 -1
  225. package/src/server/api/respond.ts +8 -3
  226. package/src/server/index.ts +1 -0
  227. package/src/server/middleware/app-access.ts +7 -6
  228. package/src/server/middleware/auth.ts +168 -66
  229. package/src/server/middleware/index.ts +1 -0
  230. package/src/server/middleware/invocation.ts +4 -2
  231. package/src/server/middleware/workload.ts +3 -3
  232. package/src/server/oauth-scope.ts +10 -0
  233. package/src/server/public-storage.ts +106 -0
  234. package/src/server/services/access.ts +8 -0
  235. package/src/services/accounts/app.ts +35 -3
  236. package/src/services/accounts/avatar.ts +342 -35
  237. package/src/services/accounts/base-group.ts +4 -5
  238. package/src/services/accounts/directory-shares.ts +157 -0
  239. package/src/services/accounts/entities.ts +50 -6
  240. package/src/services/accounts/groups.ts +3 -1
  241. package/src/services/accounts/identities.ts +64 -12
  242. package/src/services/accounts/local-groups.ts +1 -1
  243. package/src/services/accounts/users.ts +23 -15
  244. package/src/services/app-access-management.ts +519 -0
  245. package/src/services/app-access.ts +25 -7
  246. package/src/services/app-approval.ts +15 -4
  247. package/src/services/app-details.ts +224 -0
  248. package/src/services/app-discovery.ts +2 -1
  249. package/src/services/app-event-shapes.ts +139 -0
  250. package/src/services/app-events/accept.ts +116 -0
  251. package/src/services/app-events/admin.ts +250 -0
  252. package/src/services/app-events/declarations.ts +187 -0
  253. package/src/services/app-events/deliver.ts +218 -0
  254. package/src/services/app-events.ts +92 -0
  255. package/src/services/config-conflict.ts +11 -0
  256. package/src/services/core-user-events.ts +1 -0
  257. package/src/services/freeipa-config.ts +9 -0
  258. package/src/services/geo/admin.ts +4 -1
  259. package/src/services/geo/ledger.ts +47 -14
  260. package/src/services/geo/route-chain.ts +40 -18
  261. package/src/services/geo/route-estimate.ts +19 -0
  262. package/src/services/geo/route-plan.ts +0 -2
  263. package/src/services/geo/route-service.ts +57 -39
  264. package/src/services/geo/service.ts +51 -25
  265. package/src/services/identity/core-origin.ts +12 -0
  266. package/src/services/identity/index.ts +2 -1
  267. package/src/services/identity/invocation-actor.ts +50 -2
  268. package/src/services/identity/invocation-authority.ts +38 -1
  269. package/src/services/identity/invocation-token.ts +255 -50
  270. package/src/services/identity/issue-invocation.ts +9 -1
  271. package/src/services/identity/models-only-auth.ts +13 -4
  272. package/src/services/identity/nested-invocation.ts +128 -0
  273. package/src/services/image-metadata.ts +65 -1
  274. package/src/services/index.ts +24 -1
  275. package/src/services/ipa/sync.ts +6 -2
  276. package/src/services/ipa/users.ts +4 -1
  277. package/src/services/mandates/policy.ts +19 -12
  278. package/src/services/notifications/browser.ts +4 -0
  279. package/src/services/notifications/catalog.ts +5 -2
  280. package/src/services/notifications/platform.ts +7 -2
  281. package/src/services/notifications/user.ts +18 -7
  282. package/src/services/oauth-tokens.ts +28 -2
  283. package/src/services/object-store.ts +217 -0
  284. package/src/services/providers/local/users.ts +5 -0
  285. package/src/services/pwa-devices.ts +147 -37
  286. package/src/services/service-account-credentials.ts +5 -2
  287. package/src/services/session/index.ts +14 -0
  288. package/src/services/settings/core-settings.ts +91 -47
  289. package/src/services/settings/index.ts +8 -0
  290. package/src/services/settings/versions.ts +26 -0
  291. package/src/shared/ai-models.ts +274 -0
  292. package/src/shared/ai-quotas.ts +2 -2
  293. package/src/shared/ai-usage.ts +2 -2
  294. package/src/shared/index.ts +2 -0
  295. package/src/shared-text/documents.ts +25 -8
  296. package/src/shared-text/log.ts +1 -1
  297. package/src/shared-text/text-diff.ts +75 -0
  298. package/src/ssr/AdminArea.tsx +131 -0
  299. package/src/ssr/AdminLayout.tsx +11 -3
  300. package/src/ssr/AdminSidebar.tsx +7 -7
  301. package/src/ssr/AppAccessDenied.tsx +114 -0
  302. package/src/ssr/AppBadge.tsx +14 -0
  303. package/src/ssr/AppLaunchpadPanel.tsx +1 -1
  304. package/src/ssr/AppPwaRuntime.island.tsx +82 -0
  305. package/src/ssr/CloudShell.island.tsx +25 -4
  306. package/src/ssr/Layout.tsx +15 -7
  307. package/src/ssr/PwaLayout.tsx +73 -8
  308. package/src/ssr/PwaPartCount.island.tsx +23 -0
  309. package/src/ssr/PwaRuntime.island.tsx +85 -6
  310. package/src/ssr/SettingsSaveOutcome.tsx +58 -0
  311. package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
  312. package/src/ssr/ShellRuntime.island.tsx +5 -0
  313. package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
  314. package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
  315. package/src/ssr/admin-active-link.ts +16 -0
  316. package/src/ssr/admin-navigation.ts +26 -20
  317. package/src/ssr/app-badges.ts +31 -7
  318. package/src/ssr/app-pwa-document.tsx +16 -0
  319. package/src/ssr/app-pwa-messages.ts +17 -0
  320. package/src/ssr/index.ts +8 -0
  321. package/src/ssr/layout-context.ts +1 -1
  322. package/src/ssr/page-trail-runtime.ts +3 -1
  323. package/src/ssr/platform-messages.ts +9 -1
  324. package/src/ssr/pwa-messages.ts +10 -2
  325. package/src/ssr/rail-navigation.ts +5 -1
  326. package/src/ssr/settings-draft.ts +153 -0
  327. package/src/ssr/shell-data.tsx +2 -0
  328. package/src/styles/global.css +1 -0
  329. package/src/styles/profile-picture.css +270 -0
  330. package/src/styles/utilities-navigation.css +6 -0
  331. package/src/workflows/ai/policy.ts +49 -0
  332. package/src/workflows/ai/runtime.ts +131 -21
  333. package/src/workflows/ai/store.ts +283 -35
  334. package/src/workflows/ai/types.ts +8 -2
  335. package/src/workflows/ai-actions.ts +148 -120
  336. package/src/workflows/authoring.ts +31 -0
  337. package/src/workflows/contracts.ts +1 -0
  338. package/src/workflows/definition.ts +2 -0
  339. package/src/workflows/editor.ts +15 -0
  340. package/src/workflows/index.ts +2 -1
  341. package/src/workflows/language/compiler.ts +34 -3
  342. package/src/workflows/runtime/executor.ts +132 -33
  343. package/src/workflows/runtime/index.ts +2 -0
  344. package/src/workflows/runtime/ports.ts +5 -1
  345. package/src/workflows/store/actions.ts +1 -1
  346. package/src/workflows/store/definitions.ts +5 -0
  347. package/src/workflows/store/runs.ts +5 -3
  348. package/src/workflows/store/worker.ts +7 -2
  349. package/src/ai/model-pricing.ts +0 -43
  350. package/src/ai/model-request-settings.ts +0 -98
  351. package/src/ai/settings-messages.ts +0 -129
  352. package/src/shared/ai-model-request-settings.ts +0 -21
@@ -1,3 +1,5 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isDeepStrictEqual } from "node:util";
1
3
  import { createRemoteJWKSet, decodeProtectedHeader, errors, type JWTVerifyGetKey, jwtVerify, SignJWT } from "jose";
2
4
  import { z } from "zod";
3
5
  import {
@@ -9,6 +11,7 @@ import {
9
11
  IDENTITY_MAX_COMPACT_TOKEN_BYTES,
10
12
  INVOCATION_CLOCK_TOLERANCE_SECONDS,
11
13
  } from "./constants";
14
+ import { authorityFromParentInvocation } from "./invocation-authority";
12
15
  import { type PreparedIdentitySigner, prepareIdentitySigner } from "./key-ring";
13
16
  import { identityMetrics } from "./metrics";
14
17
  import { getIdentityRuntimeConfig } from "./runtime-config";
@@ -44,7 +47,7 @@ export const normalizeInvocationRequestId = (value: unknown): string | undefined
44
47
  return parsed.success ? parsed.data : undefined;
45
48
  };
46
49
 
47
- const InvocationPayloadSchema = z
50
+ const InvocationPayloadBaseSchema = z
48
51
  .object({
49
52
  iss: z.string().url(),
50
53
  aud: z.string().regex(/^app:[a-z][a-z0-9-]{0,79}$/),
@@ -59,6 +62,8 @@ const InvocationPayloadSchema = z
59
62
  /** Only for an app session of the mobile app, which never acts as an administrator. */
60
63
  session_kind: z.literal("app").optional(),
61
64
  credential_id: BoundedIdentifierSchema.optional(),
65
+ oauth_client_id: z.string().min(1).max(200).optional(),
66
+ credential_expires_at: z.iso.datetime().optional(),
62
67
  scopes: z
63
68
  .array(ScopeSchema)
64
69
  .max(100)
@@ -71,50 +76,72 @@ const InvocationPayloadSchema = z
71
76
  mandate_revision: z.number().int().positive().optional(),
72
77
  workload_type: BoundedIdentifierSchema.optional(),
73
78
  workload_id: BoundedIdentifierSchema.optional(),
79
+ parent_invocation_id: z.uuid().optional(),
74
80
  jti: z.string().uuid(),
75
81
  iat: z.number().int().nonnegative(),
76
82
  nbf: z.number().int().nonnegative(),
77
83
  exp: z.number().int().positive(),
78
84
  })
79
- .strict()
80
- .superRefine((claims, context) => {
81
- if (claims.exp - claims.iat !== CLOUD_INVOCATION_TOKEN_TTL_SECONDS || claims.nbf !== claims.iat) {
82
- context.addIssue({ code: "custom", message: "Invocation lifetime is invalid" });
83
- }
85
+ .strict();
84
86
 
85
- if (claims.principal_type === "user") {
86
- if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.sub || claims.delegated_user_id !== undefined) {
87
- context.addIssue({ code: "custom", message: "User invocation authority is inconsistent" });
88
- }
89
- if (claims.credential_kind === "api_key") {
90
- context.addIssue({ code: "custom", message: "An API key must act through a service account" });
91
- }
92
- } else if (claims.delegated_user_id) {
93
- if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.delegated_user_id) {
94
- context.addIssue({ code: "custom", message: "Delegated invocation authority is inconsistent" });
95
- }
96
- } else if (claims.access_subject_type !== "service_account" || claims.access_subject_id !== claims.sub) {
97
- context.addIssue({ code: "custom", message: "Service-account invocation authority is inconsistent" });
98
- }
99
- if (claims.credential_kind === "session" && claims.principal_type !== "user") {
100
- context.addIssue({ code: "custom", message: "A session invocation must use a user principal" });
101
- }
102
- if (claims.session_kind && claims.credential_kind !== "session") {
103
- context.addIssue({ code: "custom", message: "Only a session invocation has a session kind" });
87
+ const validateInvocationClaims = (claims: z.infer<typeof InvocationPayloadBaseSchema>, context: z.RefinementCtx, subjectProbe = false) => {
88
+ if (
89
+ (claims.parent_invocation_id
90
+ ? claims.exp <= claims.iat ||
91
+ claims.exp - claims.iat > CLOUD_INVOCATION_TOKEN_TTL_SECONDS ||
92
+ !claims.op.startsWith("capability.query:")
93
+ : claims.exp - claims.iat !== CLOUD_INVOCATION_TOKEN_TTL_SECONDS) ||
94
+ claims.nbf !== claims.iat
95
+ ) {
96
+ context.addIssue({ code: "custom", message: "Invocation lifetime is invalid" });
97
+ }
98
+
99
+ if (claims.principal_type === "user") {
100
+ if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.sub || claims.delegated_user_id !== undefined) {
101
+ context.addIssue({ code: "custom", message: "User invocation authority is inconsistent" });
104
102
  }
105
- if (claims.credential_kind === "session" && claims.scopes.length > 0) {
106
- context.addIssue({ code: "custom", message: "A session invocation cannot carry credential scopes" });
103
+ if (claims.credential_kind === "api_key") {
104
+ context.addIssue({ code: "custom", message: "An API key must act through a service account" });
107
105
  }
108
- if (claims.credential_kind === "api_key" && !claims.credential_id) {
109
- context.addIssue({ code: "custom", message: "An API-key invocation requires credential provenance" });
106
+ } else if (claims.delegated_user_id) {
107
+ if (claims.access_subject_type !== "user" || claims.access_subject_id !== claims.delegated_user_id) {
108
+ context.addIssue({ code: "custom", message: "Delegated invocation authority is inconsistent" });
110
109
  }
110
+ } else if (claims.access_subject_type !== "service_account" || claims.access_subject_id !== claims.sub) {
111
+ context.addIssue({ code: "custom", message: "Service-account invocation authority is inconsistent" });
112
+ }
113
+ if (claims.credential_kind === "session" && claims.principal_type !== "user") {
114
+ context.addIssue({ code: "custom", message: "A session invocation must use a user principal" });
115
+ }
116
+ if (claims.session_kind && claims.credential_kind !== "session") {
117
+ context.addIssue({ code: "custom", message: "Only a session invocation has a session kind" });
118
+ }
119
+ if (claims.credential_kind === "session" && claims.scopes.length > 0) {
120
+ context.addIssue({ code: "custom", message: "A session invocation cannot carry credential scopes" });
121
+ }
122
+ if (claims.credential_kind === "api_key" && !claims.credential_id) {
123
+ context.addIssue({ code: "custom", message: "An API-key invocation requires credential provenance" });
124
+ }
111
125
 
112
- const mandateFields = [claims.mandate_id, claims.mandate_revision, claims.workload_type, claims.workload_id];
113
- const mandateFieldCount = mandateFields.filter((value) => value !== undefined).length;
114
- if (claims.credential_kind === "mandate" ? mandateFieldCount !== mandateFields.length : mandateFieldCount !== 0) {
115
- context.addIssue({ code: "custom", message: "Mandate provenance is incomplete or unexpected" });
116
- }
117
- });
126
+ const mandateFields = [claims.mandate_id, claims.mandate_revision, claims.workload_type, claims.workload_id];
127
+ const mandateFieldCount = mandateFields.filter((value) => value !== undefined).length;
128
+ if (
129
+ claims.credential_kind === "mandate"
130
+ ? mandateFieldCount !== mandateFields.length && !(subjectProbe && mandateFieldCount === 0)
131
+ : mandateFieldCount !== 0
132
+ ) {
133
+ context.addIssue({ code: "custom", message: "Mandate provenance is incomplete or unexpected" });
134
+ }
135
+ };
136
+
137
+ const InvocationPayloadSchema = InvocationPayloadBaseSchema.superRefine((claims, context) => validateInvocationClaims(claims, context));
138
+ const AppAccessProbeSchema = InvocationPayloadBaseSchema.extend({
139
+ token_use: z.literal("app_access_probe"),
140
+ act: z.object({ sub: z.literal("app:core") }).strict(),
141
+ op: z.literal("app-access.check"),
142
+ schema_hash: z.null(),
143
+ parent_invocation_id: z.never().optional(),
144
+ }).superRefine((claims, context) => validateInvocationClaims({ ...claims, token_use: "invocation" }, context, true));
118
145
 
119
146
  export type CloudInvocationClaims = z.infer<typeof InvocationPayloadSchema>;
120
147
  export type InvocationAuthority = Pick<
@@ -127,6 +154,8 @@ export type InvocationAuthority = Pick<
127
154
  | "credential_kind"
128
155
  | "session_kind"
129
156
  | "credential_id"
157
+ | "oauth_client_id"
158
+ | "credential_expires_at"
130
159
  | "scopes"
131
160
  | "mandate_id"
132
161
  | "mandate_revision"
@@ -181,11 +210,23 @@ export const signInvocationToken = async (params: {
181
210
  schemaHash: string | null;
182
211
  authority: InvocationAuthority;
183
212
  requestId?: string;
213
+ parent?: CloudInvocationClaims;
214
+ expiresAt?: number;
184
215
  issuedAt?: Date;
185
216
  jti?: string;
186
217
  signer?: PreparedIdentitySigner;
187
218
  issuer?: string;
188
219
  }): Promise<{ token: string; kid: string; claims: CloudInvocationClaims }> => {
220
+ if (
221
+ params.parent &&
222
+ (params.parent.parent_invocation_id ||
223
+ params.parent.aud !== `app:${params.callingAppId}` ||
224
+ params.parent.aud === `app:${params.targetAppId}` ||
225
+ !params.operation.startsWith("capability.query:") ||
226
+ !isDeepStrictEqual(authorityFromParentInvocation(params.parent), params.authority))
227
+ ) {
228
+ throw new Error("Child invocation cannot widen or change its parent authority");
229
+ }
189
230
  const issuer = params.issuer ?? (await getIdentityRuntimeConfig()).issuer;
190
231
  const signer = params.signer ?? (await prepareIdentitySigner("invocation"));
191
232
  const issuedAt = params.issuedAt ?? new Date();
@@ -204,7 +245,8 @@ export const signInvocationToken = async (params: {
204
245
  jti: params.jti ?? crypto.randomUUID(),
205
246
  iat: issuedAtSeconds,
206
247
  nbf: issuedAtSeconds,
207
- exp: issuedAtSeconds + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
248
+ ...(params.parent ? { parent_invocation_id: params.parent.jti } : {}),
249
+ exp: Math.min(issuedAtSeconds + CLOUD_INVOCATION_TOKEN_TTL_SECONDS, params.parent?.exp ?? Infinity, params.expiresAt ?? Infinity),
208
250
  });
209
251
 
210
252
  try {
@@ -222,6 +264,26 @@ export const signInvocationToken = async (params: {
222
264
  }
223
265
  };
224
266
 
267
+ async function verifyWithKeyRefresh(
268
+ verify: (key: JWTVerifyGetKey) => ReturnType<typeof jwtVerify>,
269
+ key: JWTVerifyGetKey,
270
+ remote: { url: URL; state: RemoteSetState } | null,
271
+ ) {
272
+ try {
273
+ return await verify(key);
274
+ } catch (error) {
275
+ if (!(error instanceof errors.JWKSNoMatchingKey) || !remote) throw error;
276
+ const now = Date.now();
277
+ if (now - remote.state.lastForcedRefreshAt < 30_000) throw error;
278
+ remote.state.lastForcedRefreshAt = now;
279
+ identityMetrics.increment("unknown_kid_refresh");
280
+ const refreshed = createRemoteSet(remote.url);
281
+ const verified = await verify(refreshed);
282
+ remote.state.key = refreshed;
283
+ return verified;
284
+ }
285
+ }
286
+
225
287
  /** With deferSchemaBinding, the caller must compare schema_hash after resolving live authority. */
226
288
  export const verifyInvocationToken = async (
227
289
  token: string,
@@ -239,7 +301,7 @@ export const verifyInvocationToken = async (
239
301
  const jwksUrl = options.jwksUrl ?? runtime?.invocationJwksUrl;
240
302
  const state = options.key || !jwksUrl ? null : remoteSet(jwksUrl);
241
303
  if (state && jwksUrl) remote = { url: jwksUrl, state };
242
- let key = options.key ?? state?.key;
304
+ const key = options.key ?? state?.key;
243
305
  if (!key) throw new Error("Cloud invocation verifier has no public key source");
244
306
  assertProtectedHeader(decodeProtectedHeader(token));
245
307
  const verify = (candidate: JWTVerifyGetKey) =>
@@ -251,19 +313,7 @@ export const verifyInvocationToken = async (
251
313
  clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
252
314
  currentDate: options.now,
253
315
  });
254
- let verified: Awaited<ReturnType<typeof jwtVerify>>;
255
- try {
256
- verified = await verify(key);
257
- } catch (error) {
258
- if (!(error instanceof errors.JWKSNoMatchingKey) || !remote) throw error;
259
- const now = Date.now();
260
- if (now - remote.state.lastForcedRefreshAt < 30_000) throw error;
261
- remote.state.lastForcedRefreshAt = now;
262
- identityMetrics.increment("unknown_kid_refresh");
263
- key = createRemoteSet(remote.url);
264
- verified = await verify(key);
265
- remote.state.key = key;
266
- }
316
+ const verified = await verifyWithKeyRefresh(verify, key, remote);
267
317
  const parsed = InvocationPayloadSchema.safeParse(verified.payload);
268
318
  if (!parsed.success || parsed.data.op !== operation || (!options.deferSchemaBinding && parsed.data.schema_hash !== schemaHash)) {
269
319
  throw new Error("Cloud invocation JWT claims do not match the target operation");
@@ -280,3 +330,158 @@ export const verifyInvocationToken = async (
280
330
  export const clearInvocationVerifierCachesForTest = (): void => {
281
331
  remoteSets.clear();
282
332
  };
333
+
334
+ const AppEventInvocationClaimsSchema = z
335
+ .object({
336
+ iss: z.string().url(),
337
+ aud: z.string(),
338
+ token_use: z.literal("app_event_delivery"),
339
+ act: z.literal("app:core"),
340
+ op: z.literal("app-event.deliver"),
341
+ subscription_id: z.string().min(1).max(400),
342
+ event_id: z.uuid(),
343
+ payload_hash: z.string().regex(/^[a-f0-9]{64}$/),
344
+ iat: z.number().int(),
345
+ nbf: z.number().int(),
346
+ exp: z.number().int(),
347
+ jti: z.uuid(),
348
+ })
349
+ .strict();
350
+
351
+ export const appEventPayloadHash = (payload: unknown): string => createHash("sha256").update(JSON.stringify(payload)).digest("hex");
352
+
353
+ /** Core-to-app invocation, bound to one subscription, event and exact request body. */
354
+ export const signAppEventInvocation = async (input: {
355
+ appId: string;
356
+ subscriptionId: string;
357
+ eventId: string;
358
+ payload: unknown;
359
+ issuer?: string;
360
+ signer?: PreparedIdentitySigner;
361
+ now?: Date;
362
+ jti?: string;
363
+ }) => {
364
+ const issuer = input.issuer ?? (await getIdentityRuntimeConfig()).issuer;
365
+ const signer = input.signer ?? (await prepareIdentitySigner("invocation"));
366
+ const now = Math.floor((input.now?.getTime() ?? Date.now()) / 1_000);
367
+ if (signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
368
+ throw new Error("Invocation signing key expires before the delivery token");
369
+ const claims = AppEventInvocationClaimsSchema.parse({
370
+ iss: issuer,
371
+ aud: `app:${AppIdSchema.parse(input.appId)}`,
372
+ token_use: "app_event_delivery",
373
+ act: "app:core",
374
+ op: "app-event.deliver",
375
+ subscription_id: input.subscriptionId,
376
+ event_id: input.eventId,
377
+ payload_hash: appEventPayloadHash(input.payload),
378
+ iat: now,
379
+ nbf: now,
380
+ exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
381
+ jti: input.jti ?? crypto.randomUUID(),
382
+ });
383
+ return new SignJWT(claims)
384
+ .setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: signer.kid })
385
+ .sign(signer.key);
386
+ };
387
+
388
+ export const verifyAppEventInvocation = async (
389
+ token: string,
390
+ input: { appId: string; subscriptionId: string; eventId: string; payload: unknown },
391
+ options: { issuer?: string; key?: JWTVerifyGetKey; jwksUrl?: URL; now?: Date } = {},
392
+ ): Promise<boolean> => {
393
+ if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return false;
394
+ try {
395
+ assertProtectedHeader(decodeProtectedHeader(token));
396
+ const runtime = options.issuer && (options.key || options.jwksUrl) ? null : await getIdentityRuntimeConfig();
397
+ const issuer = options.issuer ?? runtime!.issuer;
398
+ const url = options.jwksUrl ?? runtime?.invocationJwksUrl;
399
+ const state = options.key || !url ? null : remoteSet(url);
400
+ const key = options.key ?? state?.key;
401
+ if (!key) return false;
402
+ const verify = (key: JWTVerifyGetKey) =>
403
+ jwtVerify(token, key, {
404
+ algorithms: [CLOUD_IDENTITY_ALGORITHM],
405
+ audience: `app:${AppIdSchema.parse(input.appId)}`,
406
+ issuer,
407
+ typ: CLOUD_INVOCATION_TOKEN_TYPE,
408
+ clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
409
+ currentDate: options.now,
410
+ });
411
+ const result = await verifyWithKeyRefresh(verify, key, state && url ? { url, state } : null);
412
+ const claims = AppEventInvocationClaimsSchema.parse(result.payload);
413
+ return (
414
+ claims.exp - claims.iat === CLOUD_INVOCATION_TOKEN_TTL_SECONDS &&
415
+ claims.nbf === claims.iat &&
416
+ claims.subscription_id === input.subscriptionId &&
417
+ claims.event_id === input.eventId &&
418
+ claims.payload_hash === appEventPayloadHash(input.payload)
419
+ );
420
+ } catch {
421
+ return false;
422
+ }
423
+ };
424
+
425
+ /** Core-only, target-bound existence proof. It cannot authorize an application operation. */
426
+ export const signAppAccessProbe = async (input: {
427
+ appId: string;
428
+ authority: InvocationAuthority;
429
+ issuer?: string;
430
+ signer?: PreparedIdentitySigner;
431
+ now?: Date;
432
+ }) => {
433
+ const issuer = input.issuer ?? (await getIdentityRuntimeConfig()).issuer;
434
+ const signer = input.signer ?? (await prepareIdentitySigner("invocation"));
435
+ const now = Math.floor((input.now?.getTime() ?? Date.now()) / 1_000);
436
+ if (signer.signUntil.getTime() < (now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS) * 1000)
437
+ throw new Error("Invocation signing key expires before the app-access probe");
438
+ const claims = AppAccessProbeSchema.parse({
439
+ ...input.authority,
440
+ iss: issuer,
441
+ aud: `app:${AppIdSchema.parse(input.appId)}`,
442
+ token_use: "app_access_probe",
443
+ act: { sub: "app:core" },
444
+ op: "app-access.check",
445
+ schema_hash: null,
446
+ ver: CLOUD_INVOCATION_PROTOCOL_VERSION,
447
+ iat: now,
448
+ nbf: now,
449
+ exp: now + CLOUD_INVOCATION_TOKEN_TTL_SECONDS,
450
+ jti: crypto.randomUUID(),
451
+ });
452
+ const token = await new SignJWT(claims)
453
+ .setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM, typ: CLOUD_INVOCATION_TOKEN_TYPE, kid: signer.kid })
454
+ .sign(signer.key);
455
+ if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) throw new Error("App-access probe exceeds 4 KiB");
456
+ return token;
457
+ };
458
+
459
+ export const verifyAppAccessProbe = async (
460
+ token: string,
461
+ appId: string,
462
+ options: { issuer?: string; key?: JWTVerifyGetKey; jwksUrl?: URL; now?: Date } = {},
463
+ ): Promise<z.infer<typeof AppAccessProbeSchema> | null> => {
464
+ if (new TextEncoder().encode(token).byteLength > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
465
+ try {
466
+ assertProtectedHeader(decodeProtectedHeader(token));
467
+ const runtime = options.issuer && (options.key || options.jwksUrl) ? null : await getIdentityRuntimeConfig();
468
+ const issuer = options.issuer ?? runtime!.issuer;
469
+ const url = options.jwksUrl ?? runtime?.invocationJwksUrl;
470
+ const state = options.key || !url ? null : remoteSet(url);
471
+ const key = options.key ?? state?.key;
472
+ if (!key) return null;
473
+ const verify = (key: JWTVerifyGetKey) =>
474
+ jwtVerify(token, key, {
475
+ algorithms: [CLOUD_IDENTITY_ALGORITHM],
476
+ audience: `app:${AppIdSchema.parse(appId)}`,
477
+ issuer,
478
+ typ: CLOUD_INVOCATION_TOKEN_TYPE,
479
+ clockTolerance: INVOCATION_CLOCK_TOLERANCE_SECONDS,
480
+ currentDate: options.now,
481
+ });
482
+ const result = await verifyWithKeyRefresh(verify, key, state && url ? { url, state } : null);
483
+ return AppAccessProbeSchema.parse(result.payload);
484
+ } catch {
485
+ return null;
486
+ }
487
+ };
@@ -7,6 +7,7 @@ import { canUseApp } from "../app-access";
7
7
  import { recordAppAccessDenied } from "../app-access-metrics";
8
8
  import { CLOUD_INVOCATION_TOKEN_TTL_SECONDS } from "./constants";
9
9
  import { resolveInvocationAuthority } from "./invocation-actor";
10
+ import { matchesInvocationSubject } from "./invocation-authority";
10
11
  import { type InvocationAuthority, signInvocationToken } from "./invocation-token";
11
12
  import { readIdentityGroupsAdmin } from "./runtime-config";
12
13
 
@@ -32,6 +33,7 @@ export const resolveIssuanceAuthority = async (
32
33
 
33
34
  type RegisteredAppAccessOptions = {
34
35
  db?: SQL;
36
+ signal?: AbortSignal;
35
37
  getApp?: (id: string) => Promise<Pick<AppMeta, "id" | "access"> | null>;
36
38
  };
37
39
 
@@ -40,8 +42,10 @@ export const requireRegisteredAppAccess = async (
40
42
  appId: string,
41
43
  options: RegisteredAppAccessOptions = {},
42
44
  ): Promise<void> => {
45
+ options.signal?.throwIfAborted();
43
46
  const app = await (options.getApp ?? getApp)(appId).catch(() => null);
44
- if (!app || !authority || !(await canUseApp(authority, app, { db: options.db }))) {
47
+ if (!app || !authority || !(await canUseApp(authority, app, { db: options.db, signal: options.signal }))) {
48
+ options.signal?.throwIfAborted();
45
49
  if (app) recordAppAccessDenied(app.id);
46
50
  throw new AppAccessDeniedError(app ?? { id: appId });
47
51
  }
@@ -56,12 +60,16 @@ export const issueInvocationToken = async (
56
60
  sign?: typeof signInvocationToken;
57
61
  } = {},
58
62
  ): ReturnType<typeof signInvocationToken> => {
63
+ dependencies.signal?.throwIfAborted();
59
64
  const authority = await (dependencies.resolve ?? resolveIssuanceAuthority)(
60
65
  params.authority,
61
66
  params.callingAppId,
62
67
  dependencies.db,
63
68
  dependencies.groupsAdmin,
64
69
  );
70
+ if (params.parent && (!authority || !matchesInvocationSubject(params.parent, authority)))
71
+ throw new AppAccessDeniedError({ id: params.targetAppId });
65
72
  await requireRegisteredAppAccess(authority, params.targetAppId, dependencies);
73
+ dependencies.signal?.throwIfAborted();
66
74
  return (dependencies.sign ?? signInvocationToken)(params);
67
75
  };
@@ -1,10 +1,12 @@
1
- import { AI_INFERENCE_SCOPE, type User } from "../../contracts/shared";
1
+ import { AI_INFERENCE_SCOPE } from "../../contracts/shared";
2
+ import type { AccessSubject } from "../../server/services/access";
2
3
  import { isAccountExpired } from "../account-model";
3
4
  import { serviceAccountCredentials } from "../service-account-credentials";
5
+ import { isStandaloneServiceAccountKind } from "../service-accounts";
4
6
 
5
7
  export const authenticateModelsOnlyCredential = async (
6
8
  token: string | null | undefined,
7
- ): Promise<{ user: User; credentialId: string } | null> => {
9
+ ): Promise<{ subject: AccessSubject; credentialId: string } | null> => {
8
10
  if (!token || !serviceAccountCredentials.isApiToken(token)) return null;
9
11
  const authenticated = await serviceAccountCredentials.authenticateApiToken(token);
10
12
  if (!authenticated) return null;
@@ -14,12 +16,19 @@ export const authenticateModelsOnlyCredential = async (
14
16
  credential.status !== "active" ||
15
17
  credential.serviceAccountId !== serviceAccount.id ||
16
18
  serviceAccount.status !== "active" ||
19
+ credential.scopes.length !== 1 ||
20
+ credential.scopes[0] !== AI_INFERENCE_SCOPE
21
+ )
22
+ return null;
23
+ if (isStandaloneServiceAccountKind(serviceAccount.kind)) {
24
+ return { subject: { type: "service_account", serviceAccountId: serviceAccount.id }, credentialId: credential.id };
25
+ }
26
+ if (
17
27
  serviceAccount.kind !== "user_delegated" ||
18
28
  !delegatedUser ||
19
29
  serviceAccount.delegatedUserId !== delegatedUser.id ||
20
- !credential.scopes.includes(AI_INFERENCE_SCOPE) ||
21
30
  isAccountExpired(delegatedUser.accountExpires)
22
31
  )
23
32
  return null;
24
- return { user: delegatedUser, credentialId: credential.id };
33
+ return { subject: { type: "user", userId: delegatedUser.id }, credentialId: credential.id };
25
34
  };
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import type { RequestCredentialProof } from "../../server/middleware/auth";
3
+ import { resolveOAuthTokenActor } from "../oauth-tokens";
4
+ import { requestCacheRedis } from "../request-cache-redis";
5
+ import { getOverview } from "../service-account-credentials";
6
+ import { loadJwtSessionIdentity } from "../session/user";
7
+ import { CLOUD_INVOCATION_TOKEN_TTL_SECONDS, IDENTITY_MAX_COMPACT_TOKEN_BYTES } from "./constants";
8
+ import type { CloudInvocationClaims } from "./invocation-token";
9
+ import { getIdentityRuntimeConfig } from "./runtime-config";
10
+
11
+ // The first consumer performs one resource-reader check. Budget exactly that
12
+ // dependency: one Query, one hop, no recursive fan-out or automatic retries.
13
+ // Optional lease setup may spend at most 1% of the parent's 30-second token
14
+ // lifetime (300 ms), taken from the original operation's remaining budget.
15
+ export const NESTED_INVOCATION_SETUP_BUDGET_MS = (CLOUD_INVOCATION_TOKEN_TTL_SECONDS * 1000) / 100;
16
+
17
+ const ProofSchema = z.discriminatedUnion("kind", [
18
+ z
19
+ .object({
20
+ kind: z.literal("session"),
21
+ userId: z.uuid(),
22
+ sid: z.uuid(),
23
+ authEpoch: z.number().int().nonnegative(),
24
+ expiresAt: z.iso.datetime(),
25
+ })
26
+ .strict(),
27
+ z.object({ kind: z.literal("oauth"), payload: z.record(z.string(), z.unknown()) }).strict(),
28
+ z.object({ kind: z.literal("api_key"), credentialId: z.uuid() }).strict(),
29
+ ]);
30
+ const LeaseSchema = z
31
+ .object({
32
+ appId: z.string(),
33
+ deadline: z.number().int().positive(),
34
+ origin: z.enum(["http", "app", "assistant", "mcp"]),
35
+ proof: ProofSchema.optional(),
36
+ assistantTurn: z.object({ conversationId: z.uuid(), turnId: z.uuid() }).strict().optional(),
37
+ })
38
+ .strict()
39
+ .refine((lease) => (lease.origin === "assistant") === !!lease.assistantTurn, { message: "Turn authority requires the Assistant origin" });
40
+ export type NestedInvocationLease = z.infer<typeof LeaseSchema>;
41
+
42
+ const key = (id: string) => `identity:nested:v1:${id}`;
43
+ const TAKE = `local value = redis.call('GET', KEYS[1])
44
+ if not value or redis.call('EXISTS', KEYS[2]) == 1 then return nil end
45
+ redis.call('SET', KEYS[2], '1', 'PX', ARGV[1])
46
+ return value`;
47
+
48
+ export const nestedInvocationLeases = {
49
+ async create(claims: CloudInvocationClaims, lease: NestedInvocationLease, signal?: AbortSignal): Promise<void> {
50
+ const value = LeaseSchema.parse(lease);
51
+ const ttl = Math.min(claims.exp * 1000, value.deadline) - Date.now();
52
+ if (ttl <= 0) throw new Error("Invocation expired");
53
+ const json = JSON.stringify(value);
54
+ if (Buffer.byteLength(json) > IDENTITY_MAX_COMPACT_TOKEN_BYTES)
55
+ throw new Error("Invocation lease exceeds the identity metadata budget");
56
+ const client = await requestCacheRedis();
57
+ signal?.throwIfAborted();
58
+ await client.send("SET", [key(claims.jti), json, "PX", String(ttl), "NX"]);
59
+ // A command already sent cannot be cancelled. Remove its late result when
60
+ // the bounded setup has released the original dispatch in the meantime.
61
+ if (signal?.aborted) {
62
+ await client.send("DEL", [key(claims.jti)]);
63
+ signal.throwIfAborted();
64
+ }
65
+ },
66
+ async read(id: string): Promise<NestedInvocationLease | null> {
67
+ const value = await (await requestCacheRedis()).send("GET", [key(id)]);
68
+ if (typeof value !== "string" || Buffer.byteLength(value) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
69
+ const parsed = LeaseSchema.safeParse(JSON.parse(value));
70
+ return parsed.success && parsed.data.deadline > Date.now() ? parsed.data : null;
71
+ },
72
+ async take(id: string): Promise<NestedInvocationLease | null> {
73
+ const value = await (await requestCacheRedis()).send("EVAL", [
74
+ TAKE,
75
+ "2",
76
+ key(id),
77
+ `${key(id)}:used`,
78
+ String(CLOUD_INVOCATION_TOKEN_TTL_SECONDS * 1000),
79
+ ]);
80
+ if (typeof value !== "string" || Buffer.byteLength(value) > IDENTITY_MAX_COMPACT_TOKEN_BYTES) return null;
81
+ const parsed = LeaseSchema.safeParse(JSON.parse(value));
82
+ return parsed.success && parsed.data.deadline > Date.now() ? parsed.data : null;
83
+ },
84
+ async revoke(id: string): Promise<void> {
85
+ await (await requestCacheRedis()).send("DEL", [key(id)]);
86
+ },
87
+ };
88
+
89
+ /** Recheck the source's revocation coordinates; no reusable credential is stored. */
90
+ export const isNestedSourceCurrent = async (
91
+ proof: RequestCredentialProof,
92
+ parent: Pick<CloudInvocationClaims, "sub" | "credential_kind" | "credential_id" | "principal_type" | "scopes">,
93
+ ): Promise<boolean> => {
94
+ if (proof.kind !== parent.credential_kind) return false;
95
+ if (proof.kind === "session") {
96
+ return (
97
+ proof.userId === parent.sub &&
98
+ Date.parse(proof.expiresAt) > Date.now() &&
99
+ !!(await loadJwtSessionIdentity({ userId: proof.userId, sid: proof.sid, authEpoch: proof.authEpoch }))
100
+ );
101
+ }
102
+ if (proof.kind === "api_key") {
103
+ if (proof.credentialId !== parent.credential_id) return false;
104
+ const credential = await getOverview({ id: proof.credentialId });
105
+ return (
106
+ !!credential &&
107
+ credential.status === "active" &&
108
+ credential.serviceAccountId === parent.sub &&
109
+ (!credential.expiresAt || Date.parse(credential.expiresAt) > Date.now()) &&
110
+ parent.scopes.length === credential.scopes.length &&
111
+ parent.scopes.every((scope) => credential.scopes.includes(scope))
112
+ );
113
+ }
114
+ if (typeof proof.payload.exp !== "number" || proof.payload.exp * 1000 <= Date.now()) return false;
115
+ const current = await resolveOAuthTokenActor(proof.payload, (await getIdentityRuntimeConfig()).groupsAdmin);
116
+ return (
117
+ !!current &&
118
+ current.kind === parent.principal_type &&
119
+ (current.kind === "user" ? current.user.id : current.serviceAccount.id) === parent.sub &&
120
+ parent.scopes.every((scope) => current.scopes.includes(scope))
121
+ );
122
+ };
123
+
124
+ export const nestedSourceDeadline = (proof: RequestCredentialProof | undefined): number => {
125
+ if (proof?.kind === "session") return Date.parse(proof.expiresAt);
126
+ if (proof?.kind === "oauth") return typeof proof.payload.exp === "number" ? proof.payload.exp * 1000 : 0;
127
+ return Infinity;
128
+ };