@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
@@ -366,12 +366,76 @@ const stripWebp = (bytes: Uint8Array): Uint8Array => {
366
366
  return out.subarray(0, written);
367
367
  };
368
368
 
369
+ const stripGif = (bytes: Uint8Array): Uint8Array => {
370
+ const bad = () => new ImageMetadataError("GIF");
371
+ if (bytes.length < 13) throw bad();
372
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
373
+ if (!view.getUint16(6, true) || !view.getUint16(8, true)) throw bad();
374
+ let offset = 13 + (bytes[10]! & 0x80 ? 3 * (1 << ((bytes[10]! & 7) + 1)) : 0);
375
+ if (offset > bytes.length) throw bad();
376
+ const out = new Uint8Array(bytes.length);
377
+ let written = copy(out, 0, bytes, 0, offset);
378
+ let image = false;
379
+ const blocksEnd = (start: number) => {
380
+ let next = start;
381
+ while (next < bytes.length) {
382
+ const size = bytes[next++]!;
383
+ if (!size) return next;
384
+ if (next + size > bytes.length) throw bad();
385
+ next += size;
386
+ }
387
+ throw bad();
388
+ };
389
+ while (offset < bytes.length) {
390
+ const start = offset;
391
+ const type = bytes[offset++]!;
392
+ if (type === 0x3b) {
393
+ if (!image) throw bad();
394
+ out[written++] = type;
395
+ return out.subarray(0, written);
396
+ }
397
+ if (type === 0x2c) {
398
+ if (offset + 9 > bytes.length || !view.getUint16(offset + 4, true) || !view.getUint16(offset + 6, true)) throw bad();
399
+ const packed = bytes[offset + 8]!;
400
+ offset += 9 + (packed & 0x80 ? 3 * (1 << ((packed & 7) + 1)) : 0);
401
+ if (offset >= bytes.length || bytes[offset]! < 2 || bytes[offset]! > 8) throw bad();
402
+ offset = blocksEnd(offset + 1);
403
+ written = copy(out, written, bytes, start, offset);
404
+ image = true;
405
+ } else if (type === 0x21) {
406
+ if (offset >= bytes.length) throw bad();
407
+ const label = bytes[offset++]!;
408
+ const payload = offset;
409
+ offset = blocksEnd(offset);
410
+ if (label === 0xf9) {
411
+ if (bytes[payload] !== 4 || offset !== payload + 6) throw bad();
412
+ written = copy(out, written, bytes, start, offset);
413
+ } else if (label === 0x01) {
414
+ // Plain text extensions contribute visible content, unlike comments and private application blocks.
415
+ if (bytes[payload] !== 12) throw bad();
416
+ written = copy(out, written, bytes, start, offset);
417
+ } else if (
418
+ label === 0xff &&
419
+ bytes[payload] === 11 &&
420
+ offset === start + 19 &&
421
+ (startsWith(bytes, "NETSCAPE2.0", payload + 1) || startsWith(bytes, "ANIMEXTS1.0", payload + 1)) &&
422
+ bytes[payload + 12] === 3 &&
423
+ bytes[payload + 13] === 1
424
+ ) {
425
+ written = copy(out, written, bytes, start, offset);
426
+ }
427
+ } else throw bad();
428
+ }
429
+ throw bad();
430
+ };
431
+
369
432
  /**
370
- * Losslessly removes non-rendering metadata from JPEG, PNG and WebP, selected
433
+ * Losslessly removes non-rendering metadata from JPEG, PNG, WebP and GIF, selected
371
434
  * by magic bytes. Call only for uploads the application classifies as images.
372
435
  * Other formats are returned unchanged. No decoding; linear time and space.
373
436
  */
374
437
  export const stripImageMetadata = (bytes: Uint8Array): Uint8Array => {
438
+ if (startsWith(bytes, "GIF87a") || startsWith(bytes, "GIF89a")) return stripGif(bytes);
375
439
  if (bytes[0] === 0xff && bytes[1] === 0xd8) return stripJpeg(bytes);
376
440
  if (startsWith(bytes, "\x89PNG")) return stripPng(bytes);
377
441
  if (startsWith(bytes, "RIFF") && startsWith(bytes, "WEBP", 8)) return stripWebp(bytes);
@@ -104,7 +104,7 @@ export type {
104
104
  ServiceAccountCredentialOwner,
105
105
  ServiceAccountCredentialStatus,
106
106
  } from "./service-account-credentials";
107
- export { oauthTokens } from "./oauth-tokens";
107
+ export { isServiceAccountOAuthAuthorityCurrent, oauthTokens } from "./oauth-tokens";
108
108
  export type { AuthenticatedOAuthToken } from "./oauth-tokens";
109
109
  export { webauthn } from "./webauthn";
110
110
  export type { WebAuthnRp } from "./webauthn";
@@ -251,6 +251,7 @@ export {
251
251
  export type { NetworkLookup, PublicNetworkAddress } from "./network-security";
252
252
 
253
253
  export { geoip } from "./geoip";
254
+ export { appDetails } from "./app-details";
254
255
  export {
255
256
  knownBrowsers,
256
257
  KNOWN_BROWSER_LIMITS,
@@ -268,3 +269,25 @@ export { coreGeo, createGeoService } from "./geo/service";
268
269
  export { migrate as migrateGeo } from "./geo/migrate";
269
270
 
270
271
  export { coreGeoMap, createGeoMapService } from "./geo/map";
272
+
273
+ export {
274
+ AppAccessManagementError,
275
+ appAccessGrantCounts,
276
+ canManageAppAccess,
277
+ readAppAccess,
278
+ changeAppAccess,
279
+ canRepairAppAccess,
280
+ requireAppAccessManager,
281
+ type AppAccessSnapshot,
282
+ } from "./app-access-management";
283
+
284
+ export { storage } from "./object-store";
285
+ export type {
286
+ ObjectStoreFile,
287
+ ObjectVisibility,
288
+ ObjectWriteData,
289
+ ObjectWriteOptions,
290
+ ObjectPresignOptions,
291
+ ObjectListOptions,
292
+ ObjectListResult,
293
+ } from "./object-store";
@@ -1,4 +1,5 @@
1
1
  import { sql } from "bun";
2
+ import { coreUserEventPublisher } from "../../events/core-user-created";
2
3
  import { freeipa } from "../../server/services";
3
4
  import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
4
5
  import { calculateIpaProfileFromGroupNames, parseIpaAccountTransitionPolicy, parseIpaMatchMode } from "../account-model";
@@ -461,8 +462,11 @@ export const syncFromIpa = async (runtime: IpaSyncRuntime = {}): Promise<void> =
461
462
  RETURNING id, (xmax = 0) AS inserted`;
462
463
  await upsertUserIpaData(tx, { userId: upserted[0]!.id as string, ...u });
463
464
  upsertedUsersByUid += 1;
464
- if (Boolean(upserted[0]?.inserted)) insertedUsersByUid += 1;
465
- else updatedUsersByUid += 1;
465
+ if (Boolean(upserted[0]?.inserted)) {
466
+ insertedUsersByUid += 1;
467
+ const userId = upserted[0]!.id as string;
468
+ await coreUserEventPublisher().publish(tx, "user.created", { subject: userId, data: { userId, provider: "ipa" } });
469
+ } else updatedUsersByUid += 1;
466
470
  }
467
471
 
468
472
  for (let staleIndex = 0; staleIndex < staleLocalUsers.length; staleIndex += 1) {
@@ -1,6 +1,7 @@
1
1
  import { password } from "@k2b/stdlib";
2
2
  import { sql } from "bun";
3
3
  import type { MutationResult, UserProfile } from "../../contracts/shared";
4
+ import { coreUserEventPublisher } from "../../events/core-user-created";
4
5
  import { freeipa } from "../../server/services";
5
6
  import { writeDeletedAccountAudit } from "../account-lifecycle/audit";
6
7
  import { resolveProviderProfile } from "../accounts/base-user";
@@ -366,7 +367,7 @@ export const addIpa = async (params: {
366
367
  display_name = EXCLUDED.display_name,
367
368
  mail = EXCLUDED.mail,
368
369
  account_expires = EXCLUDED.account_expires
369
- RETURNING id
370
+ RETURNING id, (xmax = 0) AS inserted
370
371
  `;
371
372
  id = insertRows[0]!.id as string;
372
373
  await upsertUserIpaData(
@@ -381,6 +382,8 @@ export const addIpa = async (params: {
381
382
  },
382
383
  tx,
383
384
  );
385
+ if (insertRows[0]?.inserted)
386
+ await coreUserEventPublisher().publish(tx, "user.created", { subject: id, data: { userId: id, provider: "ipa" } });
384
387
  }
385
388
  } catch (dbError) {
386
389
  log.error("CRITICAL: FreeIPA user created but local DB update failed. Manual reconciliation needed.", {
@@ -194,6 +194,11 @@ export const isMandatePolicyNarrowing = (current: MandatePolicyV1, next: Mandate
194
194
  }),
195
195
  ));
196
196
 
197
+ /**
198
+ * True when a background mandate permits one operation. Unattended Actions must fit the live
199
+ * capability policy whenever the mandate has grants or preapproves Actions; a `require_approval`
200
+ * mandate without grants runs an Action only after its separate approval completed through Cloud.
201
+ */
197
202
  export const mandatePolicyAllows = (
198
203
  policy: MandatePolicyV1,
199
204
  input: {
@@ -224,14 +229,14 @@ export const mandatePolicyAllows = (
224
229
  )
225
230
  );
226
231
  }
232
+ const [operation, capabilityId] = input.operation.split(":");
233
+ const kind =
234
+ operation === "capability.query"
235
+ ? "query"
236
+ : operation === "capability.action.run" || operation === "capability.action.review"
237
+ ? "action"
238
+ : null;
227
239
  if (policy.grants !== undefined) {
228
- const [operation, capabilityId] = input.operation.split(":");
229
- const kind =
230
- operation === "capability.query"
231
- ? "query"
232
- : operation === "capability.action.run" || operation === "capability.action.review"
233
- ? "action"
234
- : null;
235
240
  if (
236
241
  !kind ||
237
242
  !capabilityId ||
@@ -240,11 +245,13 @@ export const mandatePolicyAllows = (
240
245
  )
241
246
  )
242
247
  return false;
243
- if (kind === "action") {
244
- if (input.capabilityPolicy) {
245
- if (!("silent" in input.capabilityPolicy) || input.capabilityPolicy.routines === false) return false;
246
- } else if (input.capabilityApproval !== "none" && input.capabilityApproval !== "rememberable") return false;
247
- }
248
+ }
249
+ // Routine grants and unattended preapproval must both fit the live capability policy.
250
+ // A no-grants require_approval mandate keeps its separately approved execution path.
251
+ if (kind === "action" && (policy.grants !== undefined || policy.actions === "preapproved")) {
252
+ if (input.capabilityPolicy) {
253
+ if (!("silent" in input.capabilityPolicy) || input.capabilityPolicy.routines === false) return false;
254
+ } else if (input.capabilityApproval !== "none" && input.capabilityApproval !== "rememberable") return false;
248
255
  }
249
256
  if (!input.operation.startsWith("capability.action.run:")) return true;
250
257
  if (policy.actions === "deny") return false;
@@ -29,6 +29,7 @@ type BrowserDeliveryPayload = {
29
29
  group?: string;
30
30
  badge?: number;
31
31
  createdAt?: number;
32
+ alert?: false;
32
33
  };
33
34
 
34
35
  // RFC 8030 section 7.2 guarantees 4,096 bytes; aes128gcm adds a header, tag, and padding delimiter.
@@ -44,6 +45,7 @@ const browserPushMessage = (payload: BrowserDeliveryPayload): string =>
44
45
  ...(payload.group !== undefined ? { group: payload.group } : {}),
45
46
  ...(payload.badge !== undefined ? { badge: payload.badge } : {}),
46
47
  ...(payload.createdAt !== undefined ? { createdAt: payload.createdAt } : {}),
48
+ ...(payload.alert === false ? { alert: false } : {}),
47
49
  });
48
50
 
49
51
  const BrowserDeliveryPayloadSchema = z.object({
@@ -59,6 +61,7 @@ const BrowserDeliveryPayloadSchema = z.object({
59
61
  .optional(),
60
62
  badge: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
61
63
  createdAt: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).optional(),
64
+ alert: z.literal(false).optional(),
62
65
  });
63
66
 
64
67
  const BrowserDestinationContextSchema = z.object({
@@ -192,6 +195,7 @@ const browserDriver = {
192
195
  ...(group !== undefined ? { group } : {}),
193
196
  ...(presentation.badge !== undefined ? { badge: presentation.badge } : {}),
194
197
  ...(presentation.group !== undefined || presentation.badge !== undefined ? { createdAt: Date.now() } : {}),
198
+ ...(presentation.alert === false ? { alert: false } : {}),
195
199
  });
196
200
  if (payload.preview !== undefined && Buffer.byteLength(browserPushMessage(payload), "utf8") > WEB_PUSH_PLAINTEXT_BUDGET) {
197
201
  const { preview: _preview, ...titleOnlyPayload } = payload;
@@ -17,15 +17,16 @@ const isSchemaUnavailable = (error: unknown): boolean => {
17
17
  const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db: typeof sql = sql): Promise<void> => {
18
18
  const recommended = toPgTextArray([...(definition.delivery?.recommended ?? [])]);
19
19
  const required = toPgTextArray([...(definition.delivery?.required ?? [])]);
20
+ const excluded = toPgTextArray([...(definition.delivery?.excluded ?? [])]);
20
21
  await db`
21
22
  INSERT INTO notifications.definitions (
22
23
  id, app_id, kind, label, description, presentation, recipient_kind,
23
- recommended_channels, required_channels, active, last_seen_at, updated_at
24
+ recommended_channels, required_channels, excluded_channels, excluded_reason, active, last_seen_at, updated_at
24
25
  ) VALUES (
25
26
  ${definition.id}, ${definition.appId}, ${definition.key}, ${definition.label},
26
27
  ${definition.description}, (${definition.presentation ? JSON.stringify(definition.presentation) : null}::text)::jsonb,
27
28
  ${definition.recipient}, ${recommended}::text[],
28
- ${required}::text[], true, now(), now()
29
+ ${required}::text[], ${excluded}::text[], ${definition.excludedReason ?? null}, true, now(), now()
29
30
  )
30
31
  ON CONFLICT (id) DO UPDATE SET
31
32
  app_id = EXCLUDED.app_id,
@@ -36,6 +37,8 @@ const upsertDefinition = async (definition: AnyBoundNotificationDefinition, db:
36
37
  recipient_kind = EXCLUDED.recipient_kind,
37
38
  recommended_channels = EXCLUDED.recommended_channels,
38
39
  required_channels = EXCLUDED.required_channels,
40
+ excluded_channels = EXCLUDED.excluded_channels,
41
+ excluded_reason = EXCLUDED.excluded_reason,
39
42
  active = true,
40
43
  last_seen_at = now(),
41
44
  updated_at = now()
@@ -95,6 +95,9 @@ const validatePresentation = (presentation: NotificationPresentation, appId: str
95
95
  if (presentation.badge !== undefined && (!Number.isSafeInteger(presentation.badge) || presentation.badge < 0)) {
96
96
  throw new Error("Notification badge must be a non-negative safe integer");
97
97
  }
98
+ if (presentation.alert !== undefined && typeof presentation.alert !== "boolean") {
99
+ throw new Error("Notification alert must be a boolean");
100
+ }
98
101
  return {
99
102
  title,
100
103
  ...(body ? { body } : {}),
@@ -102,6 +105,7 @@ const validatePresentation = (presentation: NotificationPresentation, appId: str
102
105
  ...(targetHref ? { targetHref } : {}),
103
106
  ...(presentation.group !== undefined ? { group: presentation.group } : {}),
104
107
  ...(presentation.badge !== undefined ? { badge: presentation.badge } : {}),
108
+ ...(presentation.alert === false ? { alert: false } : {}),
105
109
  };
106
110
  };
107
111
 
@@ -274,10 +278,11 @@ export const sendTypedNotification = async <
274
278
  const candidateEventId = crypto.randomUUID();
275
279
  await ensureNotificationDefinition(definition);
276
280
 
277
- const requiredChannels = unique([...(definition.delivery?.required ?? [])]);
281
+ const excluded = new Set<string>(definition.delivery?.excluded ?? []);
282
+ const requiredChannels = unique([...(definition.delivery?.required ?? [])]).filter((channel) => !excluded.has(channel));
278
283
  const requiredChannelSet = new Set<string>(requiredChannels);
279
284
  const preference = await preferredChannels(definition.id, resolved.recipient, definition.delivery?.recommended ?? []);
280
- const preferred = preference.channels.filter((channel) => !requiredChannelSet.has(channel));
285
+ const preferred = preference.channels.filter((channel) => !requiredChannelSet.has(channel) && !excluded.has(channel));
281
286
 
282
287
  const identity = await sql.begin(async (tx) => {
283
288
  const eventRows = await tx<{ id: string }[]>`
@@ -25,6 +25,8 @@ type PreferenceRow = {
25
25
  presentation: NotificationDefinitionPresentationCatalog | null;
26
26
  recommended_channels: string[];
27
27
  required_channels: string[];
28
+ excluded_channels: string[];
29
+ excluded_reason: string | null;
28
30
  channels: string[] | null;
29
31
  customized: boolean;
30
32
  };
@@ -57,12 +59,14 @@ const definitionPresentation = (
57
59
  ): { label: string; description: string } => resolveNotificationDefinitionPresentation(row, locale);
58
60
 
59
61
  const mapPreference = (row: PreferenceRow, locale?: string): UserNotificationPreference => {
60
- const requiredChannels = unique(row.required_channels);
62
+ const excludedChannels = unique(row.excluded_channels);
63
+ const excluded = new Set(excludedChannels);
64
+ const requiredChannels = unique(row.required_channels).filter((channel) => !excluded.has(channel));
61
65
  const required = new Set(requiredChannels);
62
66
  const selectedChannels = unique(row.customized ? (row.channels ?? []) : row.recommended_channels).filter(
63
- (channel) => !required.has(channel),
67
+ (channel) => !required.has(channel) && !excluded.has(channel),
64
68
  );
65
- const presentation = definitionPresentation(row, locale);
69
+ const presentation = resolveNotificationDefinitionPresentation({ ...row, excludedReason: row.excluded_reason ?? undefined }, locale);
66
70
  return {
67
71
  id: row.id,
68
72
  appId: row.app_id,
@@ -71,6 +75,8 @@ const mapPreference = (row: PreferenceRow, locale?: string): UserNotificationPre
71
75
  description: presentation.description,
72
76
  recommendedChannels: unique(row.recommended_channels),
73
77
  requiredChannels,
78
+ excludedChannels,
79
+ excludedReason: presentation.excludedReason ?? null,
74
80
  selectedChannels,
75
81
  effectiveChannels: unique([...requiredChannels, ...selectedChannels]),
76
82
  customized: row.customized,
@@ -80,7 +86,7 @@ const mapPreference = (row: PreferenceRow, locale?: string): UserNotificationPre
80
86
  const listPreferences = async (userId: string, locale?: string): Promise<UserNotificationPreferencesResponse> => {
81
87
  const rows = await sql<PreferenceRow[]>`
82
88
  SELECT d.id, d.app_id, d.kind, d.label, d.description, d.presentation,
83
- d.recommended_channels, d.required_channels, p.channels,
89
+ d.recommended_channels, d.required_channels, d.excluded_channels, d.excluded_reason, p.channels,
84
90
  (p.user_id IS NOT NULL) AS customized
85
91
  FROM notifications.definitions d
86
92
  LEFT JOIN notifications.preferences p
@@ -110,8 +116,8 @@ const setPreference = async (config: {
110
116
  channels: string[];
111
117
  locale?: string;
112
118
  }): Promise<MutationResult<UserNotificationPreference>> => {
113
- const rows = await sql<{ required_channels: string[] }[]>`
114
- SELECT required_channels
119
+ const rows = await sql<{ required_channels: string[]; excluded_channels: string[] }[]>`
120
+ SELECT required_channels, excluded_channels
115
121
  FROM notifications.definitions
116
122
  WHERE id = ${config.definitionId} AND active = true AND recipient_kind = 'user'
117
123
  LIMIT 1
@@ -119,8 +125,13 @@ const setPreference = async (config: {
119
125
  const definition = rows[0];
120
126
  if (!definition) return { ok: false, error: "Notification preference not found", status: 404 };
121
127
 
128
+ const requested = unique(config.channels.map((channel) => channel.trim()).filter(Boolean));
129
+ const excluded = requested.filter((channel) => definition.excluded_channels.includes(channel));
130
+ if (excluded.length > 0) {
131
+ return { ok: false, error: `Excluded notification channel: ${excluded.join(", ")}`, status: 400 };
132
+ }
122
133
  const required = new Set(definition.required_channels);
123
- const channels = unique(config.channels.map((channel) => channel.trim()).filter(Boolean)).filter((channel) => !required.has(channel));
134
+ const channels = requested.filter((channel) => !required.has(channel));
124
135
  const available = new Set(listNotificationChannels());
125
136
  const unavailable = channels.filter((channel) => !available.has(channel));
126
137
  if (unavailable.length > 0) {
@@ -4,7 +4,7 @@ import type { User } from "../contracts/shared";
4
4
  import { isAccountCategoryAllowed } from "./account-category-policy";
5
5
  import { isAccountExpired } from "./account-model";
6
6
  import { IDENTITY_JWKS_MAX_AGE_SECONDS } from "./identity/constants";
7
- import { getIdentityRuntimeConfig } from "./identity/runtime-config";
7
+ import { getIdentityRuntimeConfig, readIdentityGroupsAdmin } from "./identity/runtime-config";
8
8
  import type { ServiceAccount } from "./service-accounts";
9
9
  import { buildProjectedUser, userProjectionJoin, userProjectionSql } from "./session/user";
10
10
 
@@ -59,6 +59,7 @@ export type AuthenticatedOAuthToken =
59
59
  kind: "service_account";
60
60
  payload: JWTPayload;
61
61
  serviceAccount: ServiceAccount;
62
+ oauthClientId: string;
62
63
  delegatedUser: User | null;
63
64
  scopes: string[];
64
65
  };
@@ -138,7 +139,7 @@ export const resolveOAuthTokenActor = async (
138
139
  if (serviceAccount.kind === "user_delegated" && !delegatedUser) return null;
139
140
  if (delegatedUser && isAccountExpired(delegatedUser.accountExpires)) return null;
140
141
  if (delegatedUser && !(await isAccountCategoryAllowed(delegatedUser, query))) return null;
141
- return { kind: "service_account", payload, serviceAccount, delegatedUser, scopes };
142
+ return { kind: "service_account", payload, serviceAccount, oauthClientId: clientId, delegatedUser, scopes };
142
143
  }
143
144
 
144
145
  const rawUserId = getStringClaim(payload, "id");
@@ -161,6 +162,31 @@ export const resolveOAuthTokenActor = async (
161
162
  return { kind: "user", payload, user, scopes };
162
163
  };
163
164
 
165
+ /**
166
+ * Revalidate a retained, previously authenticated service-account OAuth reference.
167
+ * Identifiers are not credentials: callers must protect their provenance and keep
168
+ * the captured scopes and expiry as caps, then authorize the domain operation.
169
+ */
170
+ export const isServiceAccountOAuthAuthorityCurrent = async (
171
+ authority: {
172
+ serviceAccountId: string;
173
+ clientId: string;
174
+ expiresAt: string | null | undefined;
175
+ },
176
+ options: { query?: typeof sql; groupsAdmin?: readonly string[] } = {},
177
+ ): Promise<boolean> => {
178
+ const expiry = authority.expiresAt ? Date.parse(authority.expiresAt) : NaN;
179
+ if (!Number.isFinite(expiry) || expiry <= Date.now()) return false;
180
+ const groupsAdmin =
181
+ options.groupsAdmin ?? (options.query ? await readIdentityGroupsAdmin(options.query) : (await getIdentityRuntimeConfig()).groupsAdmin);
182
+ const current = await resolveOAuthTokenActor(
183
+ { client_id: authority.clientId, service_account_id: authority.serviceAccountId },
184
+ [...groupsAdmin],
185
+ options.query ?? sql,
186
+ );
187
+ return current?.kind === "service_account";
188
+ };
189
+
164
190
  export const verifyAccessToken = async (
165
191
  token: string,
166
192
  expectedAudience: string | string[] = "cloud",
@@ -0,0 +1,217 @@
1
+ import type { S3ListObjectsResponse, S3Stats } from "bun";
2
+ import { objectStoreArea } from "../_internal/object-store-area";
3
+ import { objectStoreBackend } from "../_internal/object-store-config";
4
+ import { validateObjectKey as validateKey } from "../_internal/object-store-key";
5
+ import { publicCloudOrigin } from "../shared/app-url";
6
+ import { get } from "./settings";
7
+
8
+ export type ObjectVisibility = "private" | "public";
9
+ export type ObjectWriteData = string | ArrayBuffer | ArrayBufferView | Blob | Request | Response;
10
+ export type ObjectWriteOptions = { type?: string };
11
+ export type ObjectPresignOptions = { method?: "GET" | "HEAD" | "DELETE"; expiresIn?: number };
12
+ export type ObjectListOptions = {
13
+ prefix?: string;
14
+ startAfter?: string;
15
+ continuationToken?: string;
16
+ maxKeys?: number;
17
+ };
18
+ export type ObjectListResult = Pick<S3ListObjectsResponse, "contents" | "isTruncated" | "nextContinuationToken">;
19
+
20
+ export interface ObjectStoreFile {
21
+ readonly key: string;
22
+ readonly size: Promise<number>;
23
+ stat(): Promise<S3Stats>;
24
+ write(data: ObjectWriteData, options?: ObjectWriteOptions): Promise<number>;
25
+ text(): Promise<string>;
26
+ bytes(): Promise<Uint8Array>;
27
+ stream(): ReadableStream<Uint8Array>;
28
+ exists(): Promise<boolean>;
29
+ delete(): Promise<void>;
30
+ copy(destinationKey: string): Promise<number>;
31
+ presign(options?: ObjectPresignOptions): Promise<string>;
32
+ readonly url: Promise<string>;
33
+ }
34
+
35
+ const assertSize = (size: number, limit: number): void => {
36
+ if (!Number.isSafeInteger(size) || size < 0 || size > limit) throw new RangeError(`Object exceeds the ${limit}-byte limit.`);
37
+ };
38
+
39
+ /** Pull one chunk at a time, propagate cancellation, and reject before buffering past the budget. */
40
+ const boundedStream = (source: ReadableStream<Uint8Array>, limit: number): ReadableStream<Uint8Array> => {
41
+ const reader = source.getReader();
42
+ let size = 0;
43
+ return new ReadableStream<Uint8Array>(
44
+ {
45
+ async pull(controller) {
46
+ try {
47
+ const next = await reader.read();
48
+ if (next.done) {
49
+ reader.releaseLock();
50
+ controller.close();
51
+ return;
52
+ }
53
+ size += next.value.byteLength;
54
+ assertSize(size, limit);
55
+ controller.enqueue(next.value);
56
+ } catch (error) {
57
+ await reader.cancel(error).catch(() => {});
58
+ reader.releaseLock();
59
+ controller.error(error);
60
+ }
61
+ },
62
+ async cancel(reason) {
63
+ try {
64
+ await reader.cancel(reason);
65
+ } finally {
66
+ reader.releaseLock();
67
+ }
68
+ },
69
+ },
70
+ { highWaterMark: 0 },
71
+ );
72
+ };
73
+
74
+ const uploadData = async (data: ObjectWriteData, limit: number): Promise<string | ArrayBuffer | Uint8Array<ArrayBufferLike>> => {
75
+ if (typeof data === "string") {
76
+ assertSize(Buffer.byteLength(data), limit);
77
+ return data;
78
+ }
79
+ if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
80
+ assertSize(data.byteLength, limit);
81
+ return ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : data;
82
+ }
83
+ if (data instanceof Blob || data instanceof Request || data instanceof Response) {
84
+ if (data instanceof Blob) assertSize(await data.size, limit);
85
+ const stream = data instanceof Blob ? data.stream() : data.body;
86
+ if (!stream) return new Uint8Array();
87
+ return new Response(boundedStream(stream, limit)).bytes();
88
+ }
89
+ throw new TypeError("Unsupported object data.");
90
+ };
91
+
92
+ const file = (key: string, visibility: ObjectVisibility = "private"): ObjectStoreFile => {
93
+ const prefix = objectStoreArea(visibility);
94
+ validateKey(key, prefix);
95
+ const path = prefix + key;
96
+ const stream = () => {
97
+ let reader: Promise<ReadableStreamDefaultReader<Uint8Array>> | undefined;
98
+ const source = () =>
99
+ (reader ??= objectStoreBackend().then(({ client, limit }) => boundedStream(client.file(path).stream(), limit).getReader()));
100
+ return new ReadableStream<Uint8Array>(
101
+ {
102
+ async pull(controller) {
103
+ try {
104
+ const current = await source();
105
+ const next = await current.read();
106
+ if (next.done) {
107
+ current.releaseLock();
108
+ controller.close();
109
+ } else controller.enqueue(next.value);
110
+ } catch (error) {
111
+ void reader?.then(
112
+ (current) => current.releaseLock(),
113
+ () => {},
114
+ );
115
+ controller.error(error);
116
+ }
117
+ },
118
+ async cancel(reason) {
119
+ if (!reader) return;
120
+ const current = await reader;
121
+ try {
122
+ await current.cancel(reason);
123
+ } finally {
124
+ current.releaseLock();
125
+ }
126
+ },
127
+ },
128
+ { highWaterMark: 0 },
129
+ );
130
+ };
131
+ return Object.freeze({
132
+ key,
133
+ get size() {
134
+ return objectStoreBackend().then(({ client }) => client.size(path));
135
+ },
136
+ async stat() {
137
+ return (await objectStoreBackend()).client.stat(path);
138
+ },
139
+ async write(data: ObjectWriteData, options: ObjectWriteOptions = {}) {
140
+ const { client, limit } = await objectStoreBackend();
141
+ const content = await uploadData(data, limit);
142
+ const inheritedType =
143
+ data instanceof Blob
144
+ ? data.type
145
+ : data instanceof Request || data instanceof Response
146
+ ? data.headers.get("content-type")
147
+ : undefined;
148
+ return client.file(path).write(content, { type: options.type ?? (inheritedType || undefined) });
149
+ },
150
+ text: () => new Response(stream()).text(),
151
+ bytes: () => new Response(stream()).bytes(),
152
+ stream,
153
+ async exists() {
154
+ return (await objectStoreBackend()).client.file(path).exists();
155
+ },
156
+ async delete() {
157
+ return (await objectStoreBackend()).client.file(path).delete();
158
+ },
159
+ async copy(destinationKey: string) {
160
+ validateKey(destinationKey, prefix);
161
+ const { client, limit } = await objectStoreBackend();
162
+ const metadata = await client.stat(path);
163
+ assertSize(metadata.size, limit);
164
+ const content = await new Response(boundedStream(client.file(path).stream(), limit)).bytes();
165
+ return client.file(prefix + destinationKey).write(content, { type: metadata.type });
166
+ },
167
+ presign(options: ObjectPresignOptions = {}) {
168
+ if (visibility !== "private") throw new Error("Use url for public objects.");
169
+ const expiresIn = options.expiresIn ?? 300;
170
+ const method = options.method ?? "GET";
171
+ if (!Number.isInteger(expiresIn) || expiresIn < 1 || expiresIn > 900) throw new RangeError("Presign expiry must be 1–900 seconds.");
172
+ if (!["GET", "HEAD", "DELETE"].includes(method))
173
+ throw new TypeError("Presign supports GET, HEAD, and DELETE; upload through write().");
174
+ return objectStoreBackend().then(({ client }) => client.file(path).presign({ method, expiresIn }));
175
+ },
176
+ get url() {
177
+ if (visibility !== "public") throw new Error("Private objects have no public URL.");
178
+ return get<string>("app.url").then(
179
+ (base) => `${publicCloudOrigin(base)}/public/storage/${path.slice("public/".length).split("/").map(encodeURIComponent).join("/")}`,
180
+ );
181
+ },
182
+ });
183
+ };
184
+
185
+ /** App-bound S3 operations. Identity comes from app.start(), never from an API parameter. */
186
+ const list = async (options: ObjectListOptions = {}, visibility: ObjectVisibility = "private"): Promise<ObjectListResult> => {
187
+ const prefix = objectStoreArea(visibility);
188
+ const relative = validateKey(options.prefix ?? "", prefix, true);
189
+ const startAfter = options.startAfter === undefined ? undefined : prefix + validateKey(options.startAfter, prefix);
190
+ const maxKeys = options.maxKeys ?? 1000;
191
+ if (!Number.isInteger(maxKeys) || maxKeys < 1 || maxKeys > 1000) throw new RangeError("List maxKeys must be 1–1,000.");
192
+ if (
193
+ options.continuationToken !== undefined &&
194
+ (typeof options.continuationToken !== "string" || options.continuationToken.length > 8192)
195
+ ) {
196
+ throw new TypeError("Invalid S3 continuation token.");
197
+ }
198
+ const { client } = await objectStoreBackend();
199
+ const result = await client.list({ prefix: prefix + relative, startAfter, maxKeys, continuationToken: options.continuationToken });
200
+ const contents = result.contents?.flatMap((object) => {
201
+ if (!object.key.startsWith(prefix + relative)) throw new Error("S3 returned an object outside the requested area.");
202
+ try {
203
+ return [{ ...object, key: validateKey(object.key.slice(prefix.length), prefix) }];
204
+ } catch {
205
+ // Console folder markers and externally written nonconforming names are
206
+ // inaccessible through this API, but must not break paging for valid keys.
207
+ return [];
208
+ }
209
+ });
210
+ return { contents, isTruncated: result.isTruncated, nextContinuationToken: result.nextContinuationToken };
211
+ };
212
+
213
+ export const storage = Object.freeze({
214
+ file: (key: string) => file(key),
215
+ list: (options?: ObjectListOptions) => list(options),
216
+ public: Object.freeze({ file: (key: string) => file(key, "public"), list: (options?: ObjectListOptions) => list(options, "public") }),
217
+ });