@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
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { defineCapabilities } from "../contracts/capabilities";
3
+ import { appEvents, bindAppEvents } from "./app-events";
4
+
5
+ export const coreUserEventTypes = {
6
+ user: { title: "User", description: "A Cloud user identity.", icon: "ti ti-user" },
7
+ } as const;
8
+ export const coreUserEvents = {
9
+ "user.created": {
10
+ version: 1,
11
+ title: "User created",
12
+ description: "A user identity was first created in Cloud.",
13
+ subject: "user",
14
+ schema: z.object({ userId: z.uuid(), provider: z.enum(["local", "ipa"]) }).strict(),
15
+ },
16
+ } as const;
17
+ const declarations = defineCapabilities({ protocolVersion: 2, types: coreUserEventTypes, events: coreUserEvents });
18
+ const publisher = appEvents(declarations);
19
+
20
+ /** Creation services explicitly bind Core's shared event declarations before publishing. */
21
+ export const coreUserEventPublisher = () => {
22
+ bindAppEvents("core", declarations);
23
+ return publisher;
24
+ };
@@ -1,2 +1,11 @@
1
+ export type {
2
+ AppEventDefinition,
3
+ AppEventDeliveryContext,
4
+ AppEventEnvelope,
5
+ AppEventSubscriptionDefinition,
6
+ } from "../contracts/app-events";
7
+ export { APP_EVENT_MAX_BYTES, AppEventEnvelopeSchema, appEventSubscriptionId } from "../contracts/app-events";
8
+
9
+ export { appEvents } from "./app-events";
1
10
  export type { LiveChannel, LiveViewer } from "./live";
2
11
  export { defineLive } from "./live";
@@ -1,6 +1,8 @@
1
1
  import { CursorMismatchError, RetentionGapError, type TopicEvent } from "@k2b/sync";
2
2
  import type { LiveEnvelope, LiveTopic } from "./live-engine";
3
3
 
4
+ export { appEventFixture, createAppEventTestHarness, deliverAppEvent } from "./app-events.testing";
5
+
4
6
  /** Publishes an application's real outbox without starting its application. */
5
7
  export { startLiveOutbox } from "./live";
6
8
  export {
package/src/index.ts CHANGED
@@ -36,9 +36,10 @@ export type { RuntimeCompatibilityIssue } from "./_internal/runtime-compatibilit
36
36
  export { assessRuntimeCompatibility } from "./_internal/runtime-compatibility";
37
37
  export { buildRuntimeFromRegistry } from "./_internal/runtime-context";
38
38
  export { type AppSkillDefinition, skill } from "./ai/app-skills";
39
- export type { AppAccessDeclaration, AppAccessRule, FixedAppAccessRule } from "./contracts/app-access";
39
+ export type { AppAccessDeclaration, AppAccessRule, FixedAppAccessRule, ResourceAppAccessAdapter } from "./contracts/app-access";
40
40
  export { appAccess } from "./contracts/app-access";
41
41
  export { defineCapabilities } from "./contracts/capabilities";
42
+ export type { AppInstallable } from "./contracts/installable";
42
43
  export type {
43
44
  AnyBoundNotificationDefinition,
44
45
  BoundNotificationDefinition,
@@ -43,6 +43,11 @@ const toErrorResponse = (result: AnyResult): [ApiErrorBody, number] => {
43
43
  return [{ message: "Internal server error", code: "INTERNAL" }, 500];
44
44
  };
45
45
 
46
+ export async function respond<T>(
47
+ c: Context,
48
+ resultOrFn: ResultOrFn<AnyResult<T>>,
49
+ successStatus: 202,
50
+ ): Promise<JsonTypedResponse<T, 202> | ApiErrorResponse>;
46
51
  export async function respond<E extends ServiceError>(
47
52
  c: Context,
48
53
  resultOrFn: ResultOrFn<Result<never, E>>,
@@ -71,8 +76,8 @@ export async function respond<T>(
71
76
  export async function respond<T>(
72
77
  c: Context,
73
78
  resultOrFn: ResultOrFn<AnyResult<T>>,
74
- successStatus: SuccessStatus = 200,
75
- ): Promise<JsonTypedResponse<T, SuccessStatus> | ApiErrorResponse> {
79
+ successStatus: SuccessStatus | 202 = 200,
80
+ ): Promise<JsonTypedResponse<T, SuccessStatus | 202> | ApiErrorResponse> {
76
81
  const result = typeof resultOrFn === "function" ? await resultOrFn() : await resultOrFn;
77
82
 
78
83
  if (!result.ok) {
@@ -80,7 +85,7 @@ export async function respond<T>(
80
85
  return c.json(body, status as ApiErrorStatus) as ApiErrorResponse;
81
86
  }
82
87
 
83
- return c.json(result.data, successStatus as 200 | 201) as JsonTypedResponse<T, SuccessStatus>;
88
+ return c.json(result.data, successStatus) as JsonTypedResponse<T, SuccessStatus | 202>;
84
89
  }
85
90
 
86
91
  export const respondMessage = (c: Context, resultPromise: Promise<Result<void>>, message: string) =>
@@ -26,6 +26,7 @@ export type {
26
26
  export {
27
27
  auth,
28
28
  imageResponse,
29
+ isRequestCredentialCurrent,
29
30
  jsonResponse,
30
31
  middleware,
31
32
  openApiMeta,
@@ -8,7 +8,7 @@ import type { AppMeta } from "../../contracts/app";
8
8
  import { appAccessAuthority, canUseApp } from "../../services/app-access";
9
9
  import { recordAppAccessDenied } from "../../services/app-access-metrics";
10
10
  import { createLoginRedirectUrl } from "../../shared/redirect";
11
- import { type AuthContext, auth, type RequestAuthority } from "./auth";
11
+ import { type AuthContext, auth, loadAuthenticatedActor, type RequestAuthority } from "./auth";
12
12
 
13
13
  type AccessApp = Pick<AppMeta, "id" | "access"> & { name?: string };
14
14
  export const APP_ACCESS_RECHECK_MS = 50_000;
@@ -22,7 +22,7 @@ export const stopAppAccessConnections = (): void => {
22
22
  export const appAccessDeniedBody = (app: AccessApp) => ({
23
23
  code: "app_access_denied" as const,
24
24
  app: app.id,
25
- message: `You cannot use ${app.name ?? app.id}. ${app.access?.kind === "grants" ? "Ask someone who manages access to this app." : "Ask your Cloud administrator about access."}`,
25
+ message: `You cannot use ${app.name ?? app.id}. ${app.access?.kind === "grants" ? "Ask someone who manages access to this app." : app.access?.kind === "resources" ? "Ask someone who manages resource permissions in this app." : "Ask your Cloud administrator about access."}`,
26
26
  });
27
27
 
28
28
  export class AppAccessDeniedError extends Error {
@@ -280,10 +280,10 @@ export const requireAppAccess = (
280
280
  const isPublic = appPublicRouteMatcher(app);
281
281
  return createMiddleware<AuthContext>(async (c, next) => {
282
282
  if (!app.access || isPublic(c.req.path)) return next();
283
- let resolved: RequestAuthority | null = null;
284
- await auth.requireRole("*")(c, async () => {
285
- resolved = appAccessAuthority(c);
286
- });
283
+ // The framework checks application entry before the app router is matched.
284
+ // Route-level OAuth policy belongs to the app authentication middleware.
285
+ await loadAuthenticatedActor(c);
286
+ const resolved = appAccessAuthority(c);
287
287
  const browser =
288
288
  c.req.method === "GET" && c.req.header("accept")?.includes("text/html") && c.req.header("upgrade")?.toLowerCase() !== "websocket";
289
289
  if (!resolved) {
@@ -304,6 +304,7 @@ export const requireAppAccess = (
304
304
  const body = appAccessDeniedBody(app);
305
305
  const response = browser ? await renderDenied(c, body.message) : c.json(body, 403);
306
306
  response.headers.set("Cache-Control", "private, no-store");
307
+ response.headers.set("X-Cloud-App-Access-Denied", app.id);
307
308
  return response;
308
309
  }
309
310
  await next();
@@ -1,5 +1,8 @@
1
- import type { Context } from "hono";
1
+ import type { Context, Env, MiddlewareHandler } from "hono";
2
2
  import { createMiddleware } from "hono/factory";
3
+ import { matchedRoutes } from "hono/route";
4
+ import { findTargetHandler } from "hono/utils/handler";
5
+ import { bindAppAccessRequest } from "../../_internal/app-access-request";
3
6
  import { measureServerPhase } from "../../_internal/server-timing";
4
7
  import {
5
8
  AI_INFERENCE_SCOPE,
@@ -29,6 +32,7 @@ export type InvocationProvenance = {
29
32
  callingAppId: string;
30
33
  credentialKind: "session" | "oauth" | "api_key" | "mandate";
31
34
  invocationId: string;
35
+ parentInvocationId?: string;
32
36
  requestId?: string | null;
33
37
  mandateId?: string;
34
38
  mandateRevision?: number;
@@ -55,6 +59,8 @@ export type ServiceAccountRequestActor =
55
59
  delegatedUser: User;
56
60
  scopes: string[];
57
61
  credentialId?: string | null;
62
+ /** Public OAuth client identifier; safe to retain, never the bearer token. */
63
+ oauthClientId?: string | null;
58
64
  credentialExpiresAt?: string | null;
59
65
  delegation?: InvocationProvenance;
60
66
  }
@@ -64,6 +70,8 @@ export type ServiceAccountRequestActor =
64
70
  delegatedUser: null;
65
71
  scopes: string[];
66
72
  credentialId?: string | null;
73
+ /** Public OAuth client identifier; safe to retain, never the bearer token. */
74
+ oauthClientId?: string | null;
67
75
  credentialExpiresAt?: string | null;
68
76
  delegation?: InvocationProvenance;
69
77
  };
@@ -72,7 +80,14 @@ export type RequestActor = UserRequestActor | ServiceAccountRequestActor;
72
80
 
73
81
  export type RequestCredentialKind = "session" | "oauth" | "api_key" | "invocation";
74
82
 
83
+ /** Core keeps only revocation coordinates, never the original bearer credential. */
84
+ export type RequestCredentialProof =
85
+ | { kind: "session"; userId: string; sid: string; authEpoch: number; expiresAt: string }
86
+ | { kind: "oauth"; payload: import("jose").JWTPayload }
87
+ | { kind: "api_key"; credentialId: string };
88
+
75
89
  export type RequestAuthority = {
90
+ credentialProof?: RequestCredentialProof;
76
91
  actor: RequestActor;
77
92
  accessSubject: AccessSubject;
78
93
  credentialKind: RequestCredentialKind;
@@ -92,6 +107,7 @@ export type AuthContext = {
92
107
  sessionKind?: SessionKind;
93
108
  /** Constraints carried by OAuth/API credentials; absent sessions are unrestricted by transport scope. */
94
109
  credentialScopes?: string[];
110
+ credentialProof?: RequestCredentialProof;
95
111
  /** OAuth scopes for bearer-token requests. Absent for sessions and API credentials. */
96
112
  oauthScopes?: string[];
97
113
  };
@@ -159,6 +175,13 @@ const loadAuthenticatedActorUncached = async (
159
175
  c.set("sessionToken", token);
160
176
  c.set("credentialKind", "session");
161
177
  c.set("sessionKind", authenticatedSession.data.kind);
178
+ c.set("credentialProof", {
179
+ kind: "session",
180
+ userId: user.id,
181
+ sid: authenticatedSession.data.sid,
182
+ authEpoch: authenticatedSession.data.authEpoch,
183
+ expiresAt: authenticatedSession.data.expiresAt,
184
+ });
162
185
  return { token, user, actor };
163
186
  }
164
187
 
@@ -185,6 +208,7 @@ const loadAuthenticatedActorUncached = async (
185
208
  };
186
209
  c.set("actor", actor);
187
210
  c.set("credentialKind", "api_key");
211
+ c.set("credentialProof", { kind: "api_key", credentialId: authResult.credential.id });
188
212
  c.set("credentialScopes", authResult.credential.scopes);
189
213
  if (authResult.delegatedUser) {
190
214
  c.set("accessSubject", { type: "user", userId: authResult.delegatedUser.id });
@@ -200,6 +224,14 @@ const loadAuthenticatedActorUncached = async (
200
224
  const authResult = await oauthTokens.verifyAccessToken(bearer, expectedAudience);
201
225
  if (!authResult) return { token: null, user: null, actor: null };
202
226
 
227
+ c.set("credentialProof", {
228
+ kind: "oauth",
229
+ payload: Object.fromEntries(
230
+ ["sub", "id", "uid", "client_id", "auth_epoch", "service_account_id", "scope", "exp"].flatMap((key) =>
231
+ authResult.payload[key] === undefined ? [] : [[key, authResult.payload[key]]],
232
+ ),
233
+ ),
234
+ });
203
235
  if (authResult.kind === "user") {
204
236
  if (isAccountExpired(authResult.user.accountExpires)) return { token: null, user: null, actor: null };
205
237
  const actor: RequestActor = { kind: "user", user: authResult.user };
@@ -221,6 +253,7 @@ const loadAuthenticatedActorUncached = async (
221
253
  delegatedUser: authResult.delegatedUser,
222
254
  scopes: authResult.scopes,
223
255
  credentialId: null,
256
+ oauthClientId: authResult.oauthClientId,
224
257
  credentialExpiresAt: typeof authResult.payload.exp === "number" ? new Date(authResult.payload.exp * 1_000).toISOString() : null,
225
258
  };
226
259
  c.set("actor", actor);
@@ -239,7 +272,7 @@ const loadAuthenticatedActorUncached = async (
239
272
  return { token: null, user: null, actor: null };
240
273
  };
241
274
 
242
- const loadAuthenticatedActor = (
275
+ export const loadAuthenticatedActor = (
243
276
  c: Context<AuthContext>,
244
277
  options: Pick<RoleOptions, "oauthAudience"> = {},
245
278
  ): Promise<AuthenticatedActorResult> => {
@@ -289,8 +322,24 @@ export const isRequestCredentialCurrent = async (
289
322
  return false;
290
323
  };
291
324
 
325
+ const readOnlyMiddlewares = new WeakSet<object>();
326
+
327
+ /**
328
+ * Declare middleware that only authenticates, authorizes, validates or reads.
329
+ * A read-only route cannot exempt another unmarked ALL handler: Hono uses
330
+ * ALL for both `.use()` middleware and terminal `.all()` routes.
331
+ * This declaration never exempts an endpoint from the default write requirement.
332
+ */
333
+ const readOnlyMiddleware = <E extends Env = AuthContext>(handler: MiddlewareHandler<E>): MiddlewareHandler<E> => {
334
+ readOnlyMiddlewares.add(handler);
335
+ return handler;
336
+ };
337
+
292
338
  /**
293
339
  * Universal auth middleware. Handles authentication AND authorization.
340
+ * OAuth requests require `write` or `admin` scope for methods other than GET,
341
+ * HEAD and OPTIONS, unless the route explicitly declares `auth.readOnly`.
342
+ * Browser sessions and personal API keys keep their existing authority.
294
343
  *
295
344
  * @param args - Roles to check (OR logic) + optional RoleOptions at the end. Special roles:
296
345
  * - "*": No check, always passes (like optionalAuth)
@@ -320,45 +369,49 @@ const requireRole = (...args: (RoleOrSpecial | RoleOptions)[]) => {
320
369
  const options: RoleOptions = hasOptions ? (args.pop() as RoleOptions) : {};
321
370
  const roles = args as RoleOrSpecial[];
322
371
 
323
- return createMiddleware<AuthContext>(async (c, next) => {
324
- // "*" = no check at all, pass through (but try to load user)
325
- if (roles.includes("*")) {
326
- await loadAuthenticatedActor(c, options);
327
- return next();
328
- }
372
+ return readOnlyMiddleware(
373
+ createMiddleware<AuthContext>(async (c, next) => {
374
+ const continueRequest = () => requireOAuthMutationScope(c, next);
329
375
 
330
- const { user, actor } = await loadAuthenticatedActor(c, options);
376
+ // "*" skips role checks but still resolves any credential and its scopes.
377
+ if (roles.includes("*")) {
378
+ await loadAuthenticatedActor(c, options);
379
+ return continueRequest();
380
+ }
331
381
 
332
- // "anonymous" = must NOT be logged in
333
- if (roles.includes("anonymous")) {
334
- if (actor) {
335
- return handleReject(c, options, "forbidden");
382
+ const { user, actor } = await loadAuthenticatedActor(c, options);
383
+
384
+ // "anonymous" = must NOT be logged in
385
+ if (roles.includes("anonymous")) {
386
+ if (actor) {
387
+ return handleReject(c, options, "forbidden");
388
+ }
389
+ return continueRequest();
336
390
  }
337
- return next();
338
- }
339
391
 
340
- // All other roles require authentication
341
- if (!actor) {
342
- return handleReject(c, options, "unauthenticated");
343
- }
392
+ // All other roles require authentication
393
+ if (!actor) {
394
+ return handleReject(c, options, "unauthenticated");
395
+ }
344
396
 
345
- // "authenticated" = any logged-in user
346
- if (roles.includes("authenticated")) {
347
- return next();
348
- }
397
+ // "authenticated" = any logged-in user
398
+ if (roles.includes("authenticated")) {
399
+ return continueRequest();
400
+ }
349
401
 
350
- if (!user) {
351
- return handleReject(c, options, "forbidden");
352
- }
402
+ if (!user) {
403
+ return handleReject(c, options, "forbidden");
404
+ }
353
405
 
354
- // Check if user has at least one required role
355
- const hasRequiredRole = roles.some((role) => user.roles.includes(role as Role));
356
- if (!hasRequiredRole) {
357
- return handleReject(c, options, "forbidden");
358
- }
406
+ // Check if user has at least one required role
407
+ const hasRequiredRole = roles.some((role) => user.roles.includes(role as Role));
408
+ if (!hasRequiredRole) {
409
+ return handleReject(c, options, "forbidden");
410
+ }
359
411
 
360
- return next();
361
- });
412
+ return continueRequest();
413
+ }),
414
+ );
362
415
  };
363
416
 
364
417
  /**
@@ -376,35 +429,80 @@ const requireRole = (...args: (RoleOrSpecial | RoleOptions)[]) => {
376
429
  * turns into further down.
377
430
  */
378
431
  const requireUser = (options: RoleOptions = {}) =>
379
- createMiddleware<AuthContext>(async (c, next) => {
380
- const { user } = await loadAuthenticatedActor(c);
381
- if (user) return next();
382
- // A caller that reaches here is authenticated but has no user, so the
383
- // generic "Insufficient permissions" would send them looking for a missing
384
- // role. `onReject` still wins, which is what keeps SSR redirects working.
385
- if (options.onReject) return handleReject(c, options, "forbidden");
386
- return c.json({ message: "Self-service endpoints require a user-backed actor", code: "FORBIDDEN" }, 403);
387
- });
432
+ readOnlyMiddleware(
433
+ createMiddleware<AuthContext>(async (c, next) => {
434
+ const { user } = await loadAuthenticatedActor(c);
435
+ if (user) return requireOAuthMutationScope(c, next);
436
+ // A caller that reaches here is authenticated but has no user, so the
437
+ // generic "Insufficient permissions" would send them looking for a missing
438
+ // role. `onReject` still wins, which is what keeps SSR redirects working.
439
+ if (options.onReject) return handleReject(c, options, "forbidden");
440
+ return c.json({ message: "Self-service endpoints require a user-backed actor", code: "FORBIDDEN" }, 403);
441
+ }),
442
+ );
388
443
 
389
444
  /** Require one OAuth scope when the caller uses OAuth. Sessions and API credentials are unaffected. */
390
445
  const requireOAuthScope = (...requiredScopes: string[]) =>
391
- createMiddleware<AuthContext>(async (c, next) => {
392
- const scopes = c.get("oauthScopes");
393
- if (!scopes || requiredScopes.some((scope) => scopes.includes(scope))) return next();
394
- return c.json({ code: "FORBIDDEN", message: `OAuth scope ${requiredScopes.join(" or ")} is required` }, 403);
395
- });
446
+ readOnlyMiddleware(
447
+ createMiddleware<AuthContext>(async (c, next) => {
448
+ const scopes = c.get("oauthScopes");
449
+ if (!scopes || requiredScopes.some((scope) => scopes.includes(scope))) return next();
450
+ return c.json({ code: "FORBIDDEN", message: `OAuth scope ${requiredScopes.join(" or ")} is required` }, 403);
451
+ }),
452
+ );
453
+
454
+ /**
455
+ * Explicitly declare a read-only POST route. Register on the route itself,
456
+ * as its first handler, never with `.use()`: authentication can see the declaration before it runs.
457
+ * This does not replace resource authorization or make a handler read-only.
458
+ */
459
+ const readOnly = requireOAuthScope("read", "admin");
460
+
461
+ /** Internal protocol dispatchers check scope against the resolved operation. */
462
+ export const oauthScopeByOperation = createMiddleware<AuthContext>(async (_c, next) => next());
463
+
464
+ export const requireOAuthMutationScope = createMiddleware<AuthContext>(async (c, next) => {
465
+ if (["GET", "HEAD", "OPTIONS"].includes(c.req.method)) return next();
466
+ // Only an explicit method registration can relax the default. A declaration
467
+ // accidentally mounted with `.use()` or on a wildcard cannot exempt a subtree.
468
+ const matched = matchedRoutes(c);
469
+ const lastMethod = matched.findLastIndex((route) => route.method !== "ALL");
470
+ const unguardedAllHandler = matched
471
+ .slice(c.req.routeIndex + 1, lastMethod)
472
+ .some((route) => route.method === "ALL" && !readOnlyMiddlewares.has(findTargetHandler(route.handler)));
473
+ const routes = matched.filter((route) => route.method !== "ALL");
474
+ const first = routes[0];
475
+ const handler = first && findTargetHandler(first.handler);
476
+ const declared =
477
+ !unguardedAllHandler &&
478
+ first?.method === c.req.method &&
479
+ !first.path.includes("*") &&
480
+ (handler === readOnly || handler === oauthScopeByOperation) &&
481
+ routes.every((route) => route.method === first.method && route.path === first.path);
482
+ if (declared) return handler === readOnly ? requireOAuthScope("read", "admin")(c, next) : next();
483
+ return requireOAuthScope("write", "admin")(c, next);
484
+ });
396
485
 
397
486
  const getAuthority = <E extends AuthContext>(c: Context<E>): RequestAuthority => {
398
487
  const actor = c.get("actor");
399
488
  const accessSubject = c.get("accessSubject");
400
489
  const credentialKind = c.get("credentialKind");
401
490
  if (!actor || !accessSubject || !credentialKind) throw new Error("Request authority has not been resolved");
402
- return {
403
- actor,
404
- accessSubject,
405
- credentialKind,
406
- scopes: [...(c.get("credentialScopes") ?? [])],
407
- };
491
+ return bindAppAccessRequest(
492
+ {
493
+ actor,
494
+ accessSubject,
495
+ credentialKind,
496
+ scopes: [...(c.get("credentialScopes") ?? [])],
497
+ ...(c.get("credentialProof") ? { credentialProof: c.get("credentialProof") } : {}),
498
+ },
499
+ c.req.raw,
500
+ credentialKind === "session"
501
+ ? c.get("sessionToken")
502
+ : credentialKind !== "invocation"
503
+ ? (session.getBearerToken(c) ?? undefined)
504
+ : undefined,
505
+ );
408
506
  };
409
507
 
410
508
  /**
@@ -444,23 +542,25 @@ const redirectToLogin: RoleOptions = {
444
542
  };
445
543
 
446
544
  const requireAccount = (options: AccountOptions) =>
447
- createMiddleware<AuthContext>(async (c, next) => {
448
- const { user } = await loadAuthenticatedActor(c);
545
+ readOnlyMiddleware(
546
+ createMiddleware<AuthContext>(async (c, next) => {
547
+ const { user } = await loadAuthenticatedActor(c);
449
548
 
450
- if (!user) {
451
- return handleReject(c, options, "unauthenticated");
452
- }
549
+ if (!user) {
550
+ return handleReject(c, options, "unauthenticated");
551
+ }
453
552
 
454
- if (options.provider && user.provider !== options.provider) {
455
- return handleReject(c, options, "forbidden");
456
- }
553
+ if (options.provider && user.provider !== options.provider) {
554
+ return handleReject(c, options, "forbidden");
555
+ }
457
556
 
458
- if (options.profile && user.profile !== options.profile) {
459
- return handleReject(c, options, "forbidden");
460
- }
557
+ if (options.profile && user.profile !== options.profile) {
558
+ return handleReject(c, options, "forbidden");
559
+ }
461
560
 
462
- return next();
463
- });
561
+ return requireOAuthMutationScope(c, next);
562
+ }),
563
+ );
464
564
 
465
565
  // ==========================
466
566
  // Export
@@ -474,6 +574,8 @@ export const auth = {
474
574
  browserSignIn,
475
575
  rejectAppSession,
476
576
  requireOAuthScope,
577
+ readOnly,
578
+ readOnlyMiddleware,
477
579
  getAuthority,
478
580
  requireAccount,
479
581
  redirect,
@@ -4,6 +4,7 @@ export {
4
4
  type AuthContext,
5
5
  auth,
6
6
  type InvocationProvenance,
7
+ isRequestCredentialCurrent,
7
8
  type RequestActor,
8
9
  type RequestAuthority,
9
10
  type RequestCredentialKind,
@@ -1,5 +1,6 @@
1
1
  import type { Context, MiddlewareHandler } from "hono";
2
2
  import { createMiddleware } from "hono/factory";
3
+ import { bindAppAccessRequest } from "../../_internal/app-access-request";
3
4
  import { getApp } from "../../_internal/registry";
4
5
  import type { AppMeta } from "../../contracts/app";
5
6
  import { canUseApp } from "../../services/app-access";
@@ -7,7 +8,7 @@ import { recordAppAccessDenied } from "../../services/app-access-metrics";
7
8
  import { type ResolvedInvocationAuthority, resolveInvocationAuthority } from "../../services/identity/invocation-actor";
8
9
  import { type CloudInvocationClaims, isInvocationJwtCandidate, verifyInvocationToken } from "../../services/identity/invocation-token";
9
10
  import { appAccessDeniedBody } from "./app-access";
10
- import type { AuthContext } from "./auth";
11
+ import { type AuthContext, requireOAuthMutationScope } from "./auth";
11
12
 
12
13
  export type InvocationExpectation = {
13
14
  targetAppId: string;
@@ -53,6 +54,7 @@ export const requireInvocation = (
53
54
  if (!claims) return reject(c);
54
55
  const authority = await (dependencies.resolve ?? resolveInvocationAuthority)(claims);
55
56
  if (!authority) return reject(c);
57
+ bindAppAccessRequest(authority, c.req.raw);
56
58
  const app = await (dependencies.getApp ?? getApp)(expectation.targetAppId).catch(() => null);
57
59
  if (!app || !(await canUseApp(authority, app))) {
58
60
  if (app) recordAppAccessDenied(app.id);
@@ -62,5 +64,5 @@ export const requireInvocation = (
62
64
  return c.json({ code: "SCHEMA_MISMATCH", message: "The invocation schema no longer matches the target" }, 409);
63
65
  }
64
66
  installAuthority(c, claims, authority);
65
- return next();
67
+ return requireOAuthMutationScope(c, next);
66
68
  });
@@ -1,7 +1,7 @@
1
1
  import type { Context, MiddlewareHandler } from "hono";
2
2
  import { createMiddleware } from "hono/factory";
3
3
  import { hasReservedWorkloadScope } from "../../services/identity/workload-auth";
4
- import type { AuthContext } from "./auth";
4
+ import { type AuthContext, auth } from "./auth";
5
5
 
6
6
  /** Reserved app workload keys are valid only at their dedicated Core authority routes. */
7
7
  export const isReservedWorkloadCredential = (c: Context<AuthContext>): boolean => {
@@ -17,6 +17,6 @@ export const isReservedWorkloadCredential = (c: Context<AuthContext>): boolean =
17
17
  const reservedWorkloadResponse = (c: Context<AuthContext>): Response =>
18
18
  c.json({ code: "FORBIDDEN", message: "App workload credentials require a dedicated Core authority route" }, 403);
19
19
 
20
- export const rejectReservedWorkloadCredential: MiddlewareHandler<AuthContext> = createMiddleware<AuthContext>(async (c, next) =>
21
- isReservedWorkloadCredential(c) ? reservedWorkloadResponse(c) : await next(),
20
+ export const rejectReservedWorkloadCredential: MiddlewareHandler<AuthContext> = auth.readOnlyMiddleware(
21
+ createMiddleware<AuthContext>(async (c, next) => (isReservedWorkloadCredential(c) ? reservedWorkloadResponse(c) : await next())),
22
22
  );
@@ -0,0 +1,10 @@
1
+ import type { RequestAuthority } from "./middleware/auth";
2
+
3
+ /** Credential ceilings survive inter-app invocation; sessions and API keys retain their own contracts. */
4
+ export const hasOAuthScope = (authority: RequestAuthority | undefined, ...required: string[]): boolean => {
5
+ if (!authority) return true;
6
+ const oauth =
7
+ authority.credentialKind === "oauth" ||
8
+ (authority.credentialKind === "invocation" && authority.actor.delegation?.credentialKind === "oauth");
9
+ return !oauth || required.some((scope) => authority.scopes.includes(scope));
10
+ };