@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.
- package/package.json +13 -5
- package/scripts/app-favicon.ts +7 -6
- package/scripts/build.ts +3 -0
- package/scripts/installable-assets.ts +61 -0
- package/scripts/preload.ts +3 -0
- package/src/_internal/app-access-path.ts +12 -0
- package/src/_internal/app-access-request.ts +13 -0
- package/src/_internal/app-access-resources.ts +141 -0
- package/src/_internal/app-event-catalog.ts +40 -0
- package/src/_internal/app-event-schemas.ts +127 -0
- package/src/_internal/application-package.ts +40 -0
- package/src/_internal/built-in-app-ids.json +27 -0
- package/src/_internal/capabilities.ts +185 -12
- package/src/_internal/capability-origin.ts +4 -0
- package/src/_internal/capability-streams.ts +10 -5
- package/src/_internal/define-app.ts +159 -45
- package/src/_internal/installable-assets.ts +90 -0
- package/src/_internal/installable-head.ts +22 -0
- package/src/_internal/object-store-area.ts +9 -0
- package/src/_internal/object-store-config.ts +101 -0
- package/src/_internal/object-store-key.ts +14 -0
- package/src/_internal/page-responses.ts +34 -3
- package/src/_internal/process-identity.ts +7 -2
- package/src/_internal/registry-validation.ts +15 -1
- package/src/_internal/registry.ts +44 -1
- package/src/_internal/runtime-context.ts +9 -1
- package/src/_internal/runtime-watcher.ts +8 -0
- package/src/_internal/status-preserving-ssr.ts +12 -1
- package/src/access/AppAccessEditor.browser-fixture.ts +429 -0
- package/src/access/AppAccessEditor.browser-frame.tsx +68 -0
- package/src/access/AppAccessEditor.browser-harness.tsx +65 -0
- package/src/access/AppAccessEditor.tsx +380 -0
- package/src/access/PermissionEditor.browser-harness.tsx +22 -2
- package/src/access/PermissionEditor.tsx +386 -77
- package/src/access/PrincipalAvatar.tsx +63 -0
- package/src/access/PrincipalPicker.tsx +29 -8
- package/src/access/ResourceApiKeys.tsx +3 -4
- package/src/access/app-access-adapter.ts +51 -0
- package/src/access/app-access-messages.ts +172 -0
- package/src/access/messages.ts +14 -0
- package/src/access/ui.ts +4 -1
- package/src/account/Avatar.tsx +21 -3
- package/src/account/ProfilePictureDialog.tsx +699 -0
- package/src/account/profile-picture-messages.ts +269 -0
- package/src/account/ui.ts +8 -1
- package/src/ai/admin.ts +3 -0
- package/src/ai/app-frame.ts +12 -0
- package/src/ai/application-tasks.ts +101 -0
- package/src/ai/assistant-models.ts +126 -64
- package/src/ai/capabilities.ts +19 -4
- package/src/ai/capability-execution.ts +5 -2
- package/src/ai/chart-block.ts +63 -30
- package/src/ai/chat/chart-block.tsx +12 -3
- package/src/ai/chat/composer-adapter.ts +8 -7
- package/src/ai/chat/message-actions.tsx +1 -1
- package/src/ai/chat/message-utils.ts +2 -2
- package/src/ai/chat/presentation.tsx +22 -1
- package/src/ai/chat-quotas.ts +5 -4
- package/src/ai/chat-tasks.ts +2 -2
- package/src/ai/client/controller.ts +11 -10
- package/src/ai/client/projection.ts +3 -3
- package/src/ai/code-capability-routes.ts +1 -0
- package/src/ai/compaction.ts +3 -3
- package/src/ai/credentials.ts +13 -114
- package/src/ai/default-tool-contracts.ts +4 -3
- package/src/ai/dictation-runtime.ts +4 -4
- package/src/ai/enrich.ts +2 -2
- package/src/ai/executor.ts +38 -27
- package/src/ai/http.ts +17 -29
- package/src/ai/index.ts +11 -5
- package/src/ai/inference-calls.ts +6 -4
- package/src/ai/maintenance.ts +1 -1
- package/src/ai/memory-learning-runs.ts +5 -5
- package/src/ai/memory-learning.ts +3 -3
- package/src/ai/message-queue.ts +1 -2
- package/src/ai/migrate.ts +75 -42
- package/src/ai/model-access-migrate.ts +3 -17
- package/src/ai/model-access.ts +44 -85
- package/src/ai/model-card.tsx +134 -0
- package/src/ai/model-catalog.ts +359 -0
- package/src/ai/model-chooser-messages.ts +88 -0
- package/src/ai/model-chooser.tsx +577 -0
- package/src/ai/model-delete.ts +234 -0
- package/src/ai/model-fixture.ts +39 -0
- package/src/ai/model-icon.tsx +36 -0
- package/src/ai/model-overview.ts +53 -0
- package/src/ai/model-usage.ts +45 -0
- package/src/ai/openai-api-messages.ts +0 -4
- package/src/ai/openai-api-slots.ts +10 -2
- package/src/ai/openai-api.ts +83 -16
- package/src/ai/personal-agent.ts +1 -1
- package/src/ai/prefs.ts +12 -10
- package/src/ai/projects-routes.ts +2 -1
- package/src/ai/projects.ts +13 -12
- package/src/ai/protocol.ts +2 -2
- package/src/ai/provider-connections.ts +161 -0
- package/src/ai/provider-errors.ts +138 -0
- package/src/ai/provider-fetch.ts +4 -0
- package/src/ai/provider-retry.ts +1 -1
- package/src/ai/provider.ts +2 -1
- package/src/ai/quota-limits.ts +36 -0
- package/src/ai/quota-provider.ts +11 -2
- package/src/ai/quota-report.ts +7 -4
- package/src/ai/quotas-migrate.ts +1 -0
- package/src/ai/quotas.ts +19 -8
- package/src/ai/recent-resources.ts +6 -0
- package/src/ai/request-headers.ts +14 -87
- package/src/ai/resource-refs.ts +6 -2
- package/src/ai/routes.ts +17 -18
- package/src/ai/runtime.ts +50 -22
- package/src/ai/settings.ts +93 -580
- package/src/ai/store.ts +45 -23
- package/src/ai/stream.ts +1 -1
- package/src/ai/structured-runs.ts +2 -2
- package/src/ai/structured.ts +29 -46
- package/src/ai/task-contracts.ts +25 -5
- package/src/ai/task-execution.ts +15 -6
- package/src/ai/task-model.ts +9 -0
- package/src/ai/transcription.ts +37 -21
- package/src/ai/turn-failure.ts +24 -2
- package/src/ai/types.ts +50 -62
- package/src/ai/ui.tsx +13 -0
- package/src/ai/usage.ts +8 -8
- package/src/ai/validate.ts +8 -7
- package/src/ai/view-block.ts +2 -2
- package/src/api/accounts-entities.ts +40 -16
- package/src/api/admin-ai-quotas.ts +34 -56
- package/src/api/admin-ai-usage.ts +1 -1
- package/src/api/admin-ai.ts +173 -0
- package/src/api/admin-core-settings.ts +68 -244
- package/src/api/admin-directory-shares.ts +55 -0
- package/src/api/admin-identity.ts +81 -2
- package/src/api/app-access.ts +85 -0
- package/src/api/app-approval.ts +3 -1
- package/src/api/app-event-delivery.ts +51 -0
- package/src/api/app-events.ts +123 -0
- package/src/api/app-resource-access.ts +48 -0
- package/src/api/auth/account-mismatch.ts +24 -0
- package/src/api/auth.ts +26 -3
- package/src/api/capabilities.ts +127 -15
- package/src/api/capability-streams.ts +2 -5
- package/src/api/identity-child-invocation.ts +145 -0
- package/src/api/index.ts +11 -2
- package/src/api/mcp.ts +180 -5
- package/src/api/me.ts +38 -17
- package/src/api/pwa-phone.ts +87 -48
- package/src/api/pwa.ts +34 -8
- package/src/api/resource-links.ts +83 -6
- package/src/api/resource-resolve.ts +50 -0
- package/src/api/search/schemas.ts +3 -1
- package/src/api/search.ts +9 -7
- package/src/api/widgets.ts +1 -0
- package/src/app-pwa/authority.ts +75 -0
- package/src/app-pwa/contracts.ts +177 -0
- package/src/app-pwa/core-query-error.ts +20 -0
- package/src/app-pwa/index.ts +23 -0
- package/src/app-pwa/protocol.ts +27 -0
- package/src/app-pwa/schema.ts +59 -0
- package/src/app-pwa/service.ts +861 -0
- package/src/app-pwa/transaction.ts +338 -0
- package/src/app-pwa/transport-input.ts +57 -0
- package/src/app-pwa/transport.ts +235 -0
- package/src/approvals/messages.ts +2 -2
- package/src/browser/app-access-recovery.ts +44 -0
- package/src/browser/app-session.ts +14 -3
- package/src/browser/cloud-id.ts +86 -0
- package/src/browser/live-websocket.ts +5 -0
- package/src/browser/pwa-drafts.ts +57 -0
- package/src/browser/resource-clipboard.ts +4 -0
- package/src/browser/resource-links.ts +52 -0
- package/src/browser/resource-picker.tsx +3 -2
- package/src/capabilities/calendar-sources.ts +302 -0
- package/src/capabilities/executions.ts +10 -3
- package/src/capabilities/migrate.ts +4 -0
- package/src/capabilities/nested.ts +43 -0
- package/src/capabilities/server-invoke.ts +202 -0
- package/src/capabilities/server.ts +26 -187
- package/src/capabilities/testing.ts +5 -2
- package/src/cli/access.ts +6 -5
- package/src/cli/account.ts +57 -0
- package/src/cli/admin/agents.ts +66 -0
- package/src/cli/admin/ai-models.ts +199 -0
- package/src/cli/admin/ai-quotas.ts +6 -102
- package/src/cli/admin/ai-usage.ts +4 -4
- package/src/cli/admin/app-credentials.ts +5 -3
- package/src/cli/admin/index.ts +7 -3
- package/src/cli/app-access.ts +60 -0
- package/src/cli/apps.ts +89 -26
- package/src/cli/avatar.ts +64 -0
- package/src/cli/capabilities.ts +22 -1
- package/src/cli/events.ts +320 -0
- package/src/cli/index.ts +1 -1
- package/src/config/env.ts +40 -0
- package/src/contracts/app-access.ts +52 -5
- package/src/contracts/app-approval.ts +8 -1
- package/src/contracts/app-details.ts +89 -0
- package/src/contracts/app-events.ts +51 -0
- package/src/contracts/app.ts +24 -5
- package/src/contracts/calendar-source.ts +274 -0
- package/src/contracts/calendar-view.ts +35 -0
- package/src/contracts/capabilities.ts +164 -3
- package/src/contracts/geo-admin.ts +2 -0
- package/src/contracts/geo-route.ts +0 -11
- package/src/contracts/geo.ts +14 -2
- package/src/contracts/index.ts +6 -0
- package/src/contracts/installable.ts +40 -0
- package/src/contracts/notification-types.ts +36 -9
- package/src/contracts/profile.ts +32 -0
- package/src/contracts/pwa-builds.ts +14 -0
- package/src/contracts/pwa-location.ts +38 -0
- package/src/contracts/pwa.ts +24 -3
- package/src/contracts/registry.ts +8 -0
- package/src/contracts/resource-clipboard.ts +35 -0
- package/src/contracts/resource-pages.ts +140 -0
- package/src/contracts/resource-resolution.ts +13 -0
- package/src/contracts/shared.ts +4 -0
- package/src/contracts/user-notifications.ts +2 -0
- package/src/events/app-event-delivery.ts +100 -0
- package/src/events/app-events.testing.ts +65 -0
- package/src/events/app-events.ts +134 -0
- package/src/events/core-user-created.ts +24 -0
- package/src/events/index.ts +9 -0
- package/src/events/testing.ts +2 -0
- package/src/index.ts +2 -1
- package/src/server/api/respond.ts +8 -3
- package/src/server/index.ts +1 -0
- package/src/server/middleware/app-access.ts +7 -6
- package/src/server/middleware/auth.ts +168 -66
- package/src/server/middleware/index.ts +1 -0
- package/src/server/middleware/invocation.ts +4 -2
- package/src/server/middleware/workload.ts +3 -3
- package/src/server/oauth-scope.ts +10 -0
- package/src/server/public-storage.ts +106 -0
- package/src/server/services/access.ts +8 -0
- package/src/services/accounts/app.ts +35 -3
- package/src/services/accounts/avatar.ts +342 -35
- package/src/services/accounts/base-group.ts +4 -5
- package/src/services/accounts/directory-shares.ts +157 -0
- package/src/services/accounts/entities.ts +50 -6
- package/src/services/accounts/groups.ts +3 -1
- package/src/services/accounts/identities.ts +64 -12
- package/src/services/accounts/local-groups.ts +1 -1
- package/src/services/accounts/users.ts +23 -15
- package/src/services/app-access-management.ts +519 -0
- package/src/services/app-access.ts +25 -7
- package/src/services/app-approval.ts +15 -4
- package/src/services/app-details.ts +224 -0
- package/src/services/app-discovery.ts +2 -1
- package/src/services/app-event-shapes.ts +139 -0
- package/src/services/app-events/accept.ts +116 -0
- package/src/services/app-events/admin.ts +250 -0
- package/src/services/app-events/declarations.ts +187 -0
- package/src/services/app-events/deliver.ts +218 -0
- package/src/services/app-events.ts +92 -0
- package/src/services/config-conflict.ts +11 -0
- package/src/services/core-user-events.ts +1 -0
- package/src/services/freeipa-config.ts +9 -0
- package/src/services/geo/admin.ts +4 -1
- package/src/services/geo/ledger.ts +47 -14
- package/src/services/geo/route-chain.ts +40 -18
- package/src/services/geo/route-estimate.ts +19 -0
- package/src/services/geo/route-plan.ts +0 -2
- package/src/services/geo/route-service.ts +57 -39
- package/src/services/geo/service.ts +51 -25
- package/src/services/identity/core-origin.ts +12 -0
- package/src/services/identity/index.ts +2 -1
- package/src/services/identity/invocation-actor.ts +50 -2
- package/src/services/identity/invocation-authority.ts +38 -1
- package/src/services/identity/invocation-token.ts +255 -50
- package/src/services/identity/issue-invocation.ts +9 -1
- package/src/services/identity/models-only-auth.ts +13 -4
- package/src/services/identity/nested-invocation.ts +128 -0
- package/src/services/image-metadata.ts +65 -1
- package/src/services/index.ts +24 -1
- package/src/services/ipa/sync.ts +6 -2
- package/src/services/ipa/users.ts +4 -1
- package/src/services/mandates/policy.ts +19 -12
- package/src/services/notifications/browser.ts +4 -0
- package/src/services/notifications/catalog.ts +5 -2
- package/src/services/notifications/platform.ts +7 -2
- package/src/services/notifications/user.ts +18 -7
- package/src/services/oauth-tokens.ts +28 -2
- package/src/services/object-store.ts +217 -0
- package/src/services/providers/local/users.ts +5 -0
- package/src/services/pwa-devices.ts +147 -37
- package/src/services/service-account-credentials.ts +5 -2
- package/src/services/session/index.ts +14 -0
- package/src/services/settings/core-settings.ts +91 -47
- package/src/services/settings/index.ts +8 -0
- package/src/services/settings/versions.ts +26 -0
- package/src/shared/ai-models.ts +274 -0
- package/src/shared/ai-quotas.ts +2 -2
- package/src/shared/ai-usage.ts +2 -2
- package/src/shared/index.ts +2 -0
- package/src/shared-text/documents.ts +25 -8
- package/src/shared-text/log.ts +1 -1
- package/src/shared-text/text-diff.ts +75 -0
- package/src/ssr/AdminArea.tsx +131 -0
- package/src/ssr/AdminLayout.tsx +11 -3
- package/src/ssr/AdminSidebar.tsx +7 -7
- package/src/ssr/AppAccessDenied.tsx +114 -0
- package/src/ssr/AppBadge.tsx +14 -0
- package/src/ssr/AppLaunchpadPanel.tsx +1 -1
- package/src/ssr/AppPwaRuntime.island.tsx +82 -0
- package/src/ssr/CloudShell.island.tsx +25 -4
- package/src/ssr/Layout.tsx +15 -7
- package/src/ssr/PwaLayout.tsx +73 -8
- package/src/ssr/PwaPartCount.island.tsx +23 -0
- package/src/ssr/PwaRuntime.island.tsx +85 -6
- package/src/ssr/SettingsSaveOutcome.tsx +58 -0
- package/src/ssr/SettingsSaveStatus.island.tsx +16 -0
- package/src/ssr/ShellRuntime.island.tsx +5 -0
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +3 -3
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +1 -1
- package/src/ssr/admin-active-link.ts +16 -0
- package/src/ssr/admin-navigation.ts +26 -20
- package/src/ssr/app-badges.ts +31 -7
- package/src/ssr/app-pwa-document.tsx +16 -0
- package/src/ssr/app-pwa-messages.ts +17 -0
- package/src/ssr/index.ts +8 -0
- package/src/ssr/layout-context.ts +1 -1
- package/src/ssr/page-trail-runtime.ts +3 -1
- package/src/ssr/platform-messages.ts +9 -1
- package/src/ssr/pwa-messages.ts +10 -2
- package/src/ssr/rail-navigation.ts +5 -1
- package/src/ssr/settings-draft.ts +153 -0
- package/src/ssr/shell-data.tsx +2 -0
- package/src/styles/global.css +1 -0
- package/src/styles/profile-picture.css +270 -0
- package/src/styles/utilities-navigation.css +6 -0
- package/src/workflows/ai/policy.ts +49 -0
- package/src/workflows/ai/runtime.ts +131 -21
- package/src/workflows/ai/store.ts +283 -35
- package/src/workflows/ai/types.ts +8 -2
- package/src/workflows/ai-actions.ts +148 -120
- package/src/workflows/authoring.ts +31 -0
- package/src/workflows/contracts.ts +1 -0
- package/src/workflows/definition.ts +2 -0
- package/src/workflows/editor.ts +15 -0
- package/src/workflows/index.ts +2 -1
- package/src/workflows/language/compiler.ts +34 -3
- package/src/workflows/runtime/executor.ts +132 -33
- package/src/workflows/runtime/index.ts +2 -0
- package/src/workflows/runtime/ports.ts +5 -1
- package/src/workflows/store/actions.ts +1 -1
- package/src/workflows/store/definitions.ts +5 -0
- package/src/workflows/store/runs.ts +5 -3
- package/src/workflows/store/worker.ts +7 -2
- package/src/ai/model-pricing.ts +0 -43
- package/src/ai/model-request-settings.ts +0 -98
- package/src/ai/settings-messages.ts +0 -129
- 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
|
|
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);
|
package/src/services/index.ts
CHANGED
|
@@ -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";
|
package/src/services/ipa/sync.ts
CHANGED
|
@@ -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))
|
|
465
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
+
});
|