@k2b/cloud 0.6.0-rc.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/README.md +98 -0
- package/package.json +128 -0
- package/public/logo.svg +1 -0
- package/scripts/app-favicon.test.ts +49 -0
- package/scripts/app-favicon.ts +51 -0
- package/scripts/build.ts +205 -0
- package/scripts/preload.ts +164 -0
- package/scripts/sync-recovery-smoke.ts +200 -0
- package/src/_internal/app-favicon.ts +2 -0
- package/src/_internal/app-presentation.ts +61 -0
- package/src/_internal/app-workspace-state.ts +24 -0
- package/src/_internal/bounded-json.ts +47 -0
- package/src/_internal/build-metadata.ts +9 -0
- package/src/_internal/capabilities.ts +1118 -0
- package/src/_internal/define-app.ts +801 -0
- package/src/_internal/heartbeat.ts +167 -0
- package/src/_internal/help-catalog.ts +241 -0
- package/src/_internal/help.ts +95 -0
- package/src/_internal/nats-connection.ts +34 -0
- package/src/_internal/page-responses.ts +40 -0
- package/src/_internal/process-sync.ts +117 -0
- package/src/_internal/readiness.ts +3 -0
- package/src/_internal/registry-validation.ts +136 -0
- package/src/_internal/registry-watch.ts +52 -0
- package/src/_internal/registry.ts +338 -0
- package/src/_internal/runtime-compatibility.ts +64 -0
- package/src/_internal/runtime-context.ts +84 -0
- package/src/_internal/runtime-watcher.ts +78 -0
- package/src/_internal/static-assets.ts +86 -0
- package/src/_internal/status-preserving-ssr.ts +49 -0
- package/src/access/PermissionEditor.tsx +486 -0
- package/src/access/ResourceApiKeys.tsx +271 -0
- package/src/access/messages.ts +121 -0
- package/src/access/ui.ts +16 -0
- package/src/account/Avatar.tsx +28 -0
- package/src/account/EntitySearch.tsx +363 -0
- package/src/account/avatar-upload-dialog.tsx +197 -0
- package/src/account/avatar-upload.ts +149 -0
- package/src/account/ui.ts +3 -0
- package/src/ai/admin.ts +47 -0
- package/src/ai/approval-routes.ts +130 -0
- package/src/ai/approvals.ts +139 -0
- package/src/ai/assistant-models.ts +61 -0
- package/src/ai/attachments.ts +40 -0
- package/src/ai/browser.ts +92 -0
- package/src/ai/capabilities.ts +714 -0
- package/src/ai/capability-execution.ts +151 -0
- package/src/ai/chat/blocks.render.test.tsx +1108 -0
- package/src/ai/chat/blocks.tsx +730 -0
- package/src/ai/chat/builtin-tools.tsx +428 -0
- package/src/ai/chat/capability-table.tsx +87 -0
- package/src/ai/chat/composer-adapter.ts +169 -0
- package/src/ai/chat/file-tools.tsx +81 -0
- package/src/ai/chat/message-actions.tsx +370 -0
- package/src/ai/chat/message-utils.ts +479 -0
- package/src/ai/chat/messages.ts +43 -0
- package/src/ai/chat/presentation.tsx +395 -0
- package/src/ai/chat/primitives.tsx +3 -0
- package/src/ai/chat/tool-disclosure.tsx +41 -0
- package/src/ai/chat/turn-navigator-utils.ts +28 -0
- package/src/ai/chat/turn-navigator.tsx +164 -0
- package/src/ai/chat/user-message.tsx +149 -0
- package/src/ai/chat/visual-tools.tsx +435 -0
- package/src/ai/chat/web-tools.tsx +202 -0
- package/src/ai/chat-task-contracts.ts +113 -0
- package/src/ai/chat-tasks.ts +952 -0
- package/src/ai/client/controller.ts +1211 -0
- package/src/ai/client/file-source.ts +95 -0
- package/src/ai/client/live-connection.ts +140 -0
- package/src/ai/client/projection.ts +178 -0
- package/src/ai/client/transport.ts +111 -0
- package/src/ai/compaction.ts +182 -0
- package/src/ai/credentials.ts +124 -0
- package/src/ai/default-tool-contracts.ts +108 -0
- package/src/ai/default-tools.ts +127 -0
- package/src/ai/enrich.ts +280 -0
- package/src/ai/executor.ts +1624 -0
- package/src/ai/fetch-file-tool.ts +313 -0
- package/src/ai/file-context.ts +92 -0
- package/src/ai/file-media-type.ts +28 -0
- package/src/ai/file-mount.ts +16 -0
- package/src/ai/file-tools.ts +566 -0
- package/src/ai/files-store.ts +390 -0
- package/src/ai/firecrawl-tools.ts +305 -0
- package/src/ai/grids-skill.ts +126 -0
- package/src/ai/http.ts +265 -0
- package/src/ai/index.ts +337 -0
- package/src/ai/kit-skill.ts +22 -0
- package/src/ai/limits.ts +5 -0
- package/src/ai/live-events.ts +289 -0
- package/src/ai/live-messages.ts +45 -0
- package/src/ai/live-outbox.ts +102 -0
- package/src/ai/live-routes.ts +412 -0
- package/src/ai/live.ts +2 -0
- package/src/ai/maintenance.ts +302 -0
- package/src/ai/markdown-pdf-tool.ts +95 -0
- package/src/ai/memories.ts +605 -0
- package/src/ai/memory-learning-runs.ts +222 -0
- package/src/ai/memory-learning.ts +619 -0
- package/src/ai/memory-tool.ts +120 -0
- package/src/ai/memory-workflow-evidence.ts +158 -0
- package/src/ai/migrate.ts +2082 -0
- package/src/ai/model-access-migrate.ts +30 -0
- package/src/ai/model-access.ts +191 -0
- package/src/ai/personal-agent.ts +6 -0
- package/src/ai/prefs.ts +72 -0
- package/src/ai/project-tool.ts +94 -0
- package/src/ai/projects-routes.ts +277 -0
- package/src/ai/projects.ts +902 -0
- package/src/ai/protocol.ts +279 -0
- package/src/ai/provider.ts +73 -0
- package/src/ai/public-projection.ts +25 -0
- package/src/ai/resource-markers.ts +31 -0
- package/src/ai/resource-refs.ts +70 -0
- package/src/ai/routes.ts +1131 -0
- package/src/ai/runtime-tools.ts +39 -0
- package/src/ai/runtime.ts +544 -0
- package/src/ai/settings.ts +529 -0
- package/src/ai/short-id.ts +45 -0
- package/src/ai/skill-catalog.ts +73 -0
- package/src/ai/skill-format.ts +145 -0
- package/src/ai/skill-seeds.ts +430 -0
- package/src/ai/skill-tool.ts +81 -0
- package/src/ai/skills-routes.ts +143 -0
- package/src/ai/skills.ts +934 -0
- package/src/ai/solid.ts +27 -0
- package/src/ai/store.ts +3113 -0
- package/src/ai/stream.ts +308 -0
- package/src/ai/structured-runs.ts +66 -0
- package/src/ai/structured.ts +186 -0
- package/src/ai/system-prompt.ts +170 -0
- package/src/ai/task-prompt.ts +22 -0
- package/src/ai/timeline.ts +148 -0
- package/src/ai/tool-audit.ts +177 -0
- package/src/ai/tool-result-budget.ts +16 -0
- package/src/ai/tools.ts +155 -0
- package/src/ai/types.ts +967 -0
- package/src/ai/ui.tsx +39 -0
- package/src/ai/usage-migrate.ts +81 -0
- package/src/ai/usage.ts +315 -0
- package/src/ai/validate.ts +54 -0
- package/src/ai/vision-tool.ts +79 -0
- package/src/api/accounts-entities.ts +174 -0
- package/src/api/admin-ai-projects.ts +74 -0
- package/src/api/admin-ai-skills.ts +76 -0
- package/src/api/admin-ai-usage.ts +42 -0
- package/src/api/admin-core-settings.ts +508 -0
- package/src/api/admin-identity.ts +148 -0
- package/src/api/admin-lifecycle.ts +206 -0
- package/src/api/admin-linux-identities.ts +66 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/app-approval.ts +129 -0
- package/src/api/apps.ts +51 -0
- package/src/api/auth/schemas.ts +54 -0
- package/src/api/auth.ts +370 -0
- package/src/api/capabilities.ts +658 -0
- package/src/api/help-messages.ts +19 -0
- package/src/api/help.ts +78 -0
- package/src/api/index.ts +108 -0
- package/src/api/mcp.ts +744 -0
- package/src/api/me-rail.ts +45 -0
- package/src/api/me.ts +753 -0
- package/src/api/search/schemas.ts +79 -0
- package/src/api/search.bench.ts +185 -0
- package/src/api/search.ts +407 -0
- package/src/api/sync-ops.ts +79 -0
- package/src/api/widgets.ts +143 -0
- package/src/browser/CloudResourceSearch.behavior.test.tsx +74 -0
- package/src/browser/CloudResourceSearch.tsx +791 -0
- package/src/browser/app-approval-vault.ts +305 -0
- package/src/browser/app-approval.ts +234 -0
- package/src/browser/live-websocket.ts +268 -0
- package/src/browser/locale-preference.ts +25 -0
- package/src/browser/notification-target.ts +27 -0
- package/src/browser/notification-timeout.ts +14 -0
- package/src/browser/notifications.ts +181 -0
- package/src/browser/resource-clipboard.ts +70 -0
- package/src/browser/resource-picker.tsx +39 -0
- package/src/browser/resource-search-messages.ts +55 -0
- package/src/browser/resource-search.ts +27 -0
- package/src/capabilities/client.ts +149 -0
- package/src/capabilities/response.ts +39 -0
- package/src/capabilities/server.ts +215 -0
- package/src/capabilities/signals.ts +4 -0
- package/src/capabilities/testing.ts +12 -0
- package/src/capabilities/types.ts +32 -0
- package/src/cli/access.ts +499 -0
- package/src/cli/account.ts +382 -0
- package/src/cli/admin/account-administration.ts +45 -0
- package/src/cli/admin/account-categories.ts +49 -0
- package/src/cli/admin/ai-usage.ts +118 -0
- package/src/cli/admin/app-sign-in.ts +49 -0
- package/src/cli/admin/data.ts +343 -0
- package/src/cli/admin/documentation.ts +34 -0
- package/src/cli/admin/gateway.ts +155 -0
- package/src/cli/admin/index.ts +61 -0
- package/src/cli/admin/instance.ts +250 -0
- package/src/cli/admin/jobs.ts +223 -0
- package/src/cli/admin/legal.ts +122 -0
- package/src/cli/admin/linux.ts +53 -0
- package/src/cli/admin/logs.ts +324 -0
- package/src/cli/admin/metrics.ts +142 -0
- package/src/cli/admin/nats.ts +167 -0
- package/src/cli/admin/notifications.ts +516 -0
- package/src/cli/admin/shared.ts +113 -0
- package/src/cli/admin/sync.ts +188 -0
- package/src/cli/admin/telemetry.ts +314 -0
- package/src/cli/admin/webhooks.ts +222 -0
- package/src/cli/admin/workflows.ts +367 -0
- package/src/cli/apps.ts +40 -0
- package/src/cli/capabilities.ts +239 -0
- package/src/cli/commands.ts +604 -0
- package/src/cli/index.ts +61 -0
- package/src/cli/locale.ts +31 -0
- package/src/cli/output.ts +58 -0
- package/src/clients/core.ts +20 -0
- package/src/config/env.ts +54 -0
- package/src/config/index.ts +6 -0
- package/src/contracts/account-categories.ts +62 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app-approval.ts +197 -0
- package/src/contracts/app.ts +162 -0
- package/src/contracts/capabilities.ts +484 -0
- package/src/contracts/index.ts +14 -0
- package/src/contracts/notification-live.ts +66 -0
- package/src/contracts/notification-types.ts +235 -0
- package/src/contracts/posix.ts +69 -0
- package/src/contracts/profile.ts +85 -0
- package/src/contracts/rail-preferences.ts +53 -0
- package/src/contracts/registry.ts +110 -0
- package/src/contracts/resource-clipboard.ts +33 -0
- package/src/contracts/settings-types.ts +98 -0
- package/src/contracts/shared.ts +408 -0
- package/src/contracts/user-notifications.ts +101 -0
- package/src/contracts/widgets.ts +183 -0
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/index.ts +58 -0
- package/src/server/actor.ts +41 -0
- package/src/server/api/index.ts +2 -0
- package/src/server/api/respond.ts +96 -0
- package/src/server/api-client.ts +54 -0
- package/src/server/app-context.ts +39 -0
- package/src/server/help.ts +187 -0
- package/src/server/index.ts +89 -0
- package/src/server/locale.ts +55 -0
- package/src/server/middleware/auth.ts +386 -0
- package/src/server/middleware/index.ts +28 -0
- package/src/server/middleware/invocation.ts +55 -0
- package/src/server/middleware/middleware.ts +65 -0
- package/src/server/middleware/openapi.ts +126 -0
- package/src/server/middleware/rate-limit.ts +125 -0
- package/src/server/middleware/request-logger.ts +41 -0
- package/src/server/middleware/route-template.ts +46 -0
- package/src/server/middleware/runtime.ts +22 -0
- package/src/server/middleware/settings.ts +55 -0
- package/src/server/middleware/validator.ts +49 -0
- package/src/server/middleware/workload.ts +22 -0
- package/src/server/ratelimit.ts +151 -0
- package/src/server/request-cookies.ts +18 -0
- package/src/server/services/access.ts +644 -0
- package/src/server/services/freeipa/client.ts +137 -0
- package/src/server/services/freeipa/index.ts +9 -0
- package/src/server/services/freeipa/session.ts +123 -0
- package/src/server/services/freeipa/test-certificates.ts +44 -0
- package/src/server/services/freeipa/tls.ts +44 -0
- package/src/server/services/freeipa/transport.ts +116 -0
- package/src/server/services/freeipa/util.ts +59 -0
- package/src/server/services/geo.ts +198 -0
- package/src/server/services/index.ts +48 -0
- package/src/server/services/pagination.ts +22 -0
- package/src/server/services/services.ts +13 -0
- package/src/server/time.ts +33 -0
- package/src/services/account-category-policy.ts +51 -0
- package/src/services/account-lifecycle/audit.ts +41 -0
- package/src/services/account-lifecycle/index.ts +940 -0
- package/src/services/account-lifecycle/ipa-backfill.ts +152 -0
- package/src/services/account-lifecycle/notification-sender.ts +13 -0
- package/src/services/account-lifecycle/scheduler.ts +506 -0
- package/src/services/account-model.ts +22 -0
- package/src/services/accounts/app.ts +1671 -0
- package/src/services/accounts/authz.ts +44 -0
- package/src/services/accounts/avatar.ts +120 -0
- package/src/services/accounts/base-group.ts +11 -0
- package/src/services/accounts/base-user.ts +46 -0
- package/src/services/accounts/duplicate-emails.ts +82 -0
- package/src/services/accounts/entities.ts +622 -0
- package/src/services/accounts/group-sql.ts +106 -0
- package/src/services/accounts/groups.ts +272 -0
- package/src/services/accounts/index.ts +14 -0
- package/src/services/accounts/ipa-data.ts +66 -0
- package/src/services/accounts/lifecycle.ts +2 -0
- package/src/services/accounts/local-groups.ts +499 -0
- package/src/services/accounts/model.ts +129 -0
- package/src/services/accounts/notification-sender.ts +31 -0
- package/src/services/accounts/posix.ts +386 -0
- package/src/services/accounts/request-policy.ts +11 -0
- package/src/services/accounts/switching.ts +126 -0
- package/src/services/accounts/users.ts +745 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/app-approval.ts +498 -0
- package/src/services/audit/index.ts +438 -0
- package/src/services/auth-flows/index.ts +15 -0
- package/src/services/auth-flows/ipa.ts +197 -0
- package/src/services/auth-flows/magic-link.ts +185 -0
- package/src/services/auth-flows/notification-sender.ts +10 -0
- package/src/services/auth-flows/password-reset.ts +268 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/document-extraction.ts +148 -0
- package/src/services/freeipa-config.ts +179 -0
- package/src/services/gateway.ts +186 -0
- package/src/services/identity/constants.ts +18 -0
- package/src/services/identity/index.ts +52 -0
- package/src/services/identity/invocation-actor.ts +120 -0
- package/src/services/identity/invocation-authority.ts +53 -0
- package/src/services/identity/invocation-operations.ts +15 -0
- package/src/services/identity/invocation-token.ts +276 -0
- package/src/services/identity/key-config.ts +41 -0
- package/src/services/identity/key-ring.ts +530 -0
- package/src/services/identity/metrics.ts +29 -0
- package/src/services/identity/routes.ts +18 -0
- package/src/services/identity/runtime-config.ts +88 -0
- package/src/services/identity/session-token.ts +152 -0
- package/src/services/identity/signer-refresh.ts +44 -0
- package/src/services/identity/workload-auth.ts +64 -0
- package/src/services/index.ts +187 -0
- package/src/services/ipa/auth.ts +216 -0
- package/src/services/ipa/connection.ts +57 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/groups.ts +722 -0
- package/src/services/ipa/guard.ts +17 -0
- package/src/services/ipa/index.ts +17 -0
- package/src/services/ipa/posix.ts +19 -0
- package/src/services/ipa/profile.ts +132 -0
- package/src/services/ipa/runtime.ts +28 -0
- package/src/services/ipa/search.ts +154 -0
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.ts +100 -0
- package/src/services/ipa/sync.ts +862 -0
- package/src/services/ipa/users.ts +806 -0
- package/src/services/legal-consent.ts +76 -0
- package/src/services/logging/index.ts +477 -0
- package/src/services/logging/redaction.ts +20 -0
- package/src/services/logging/trace.ts +1231 -0
- package/src/services/mandates/index.ts +855 -0
- package/src/services/mandates/policy.ts +85 -0
- package/src/services/network-security.ts +31 -0
- package/src/services/notifications/batches.ts +722 -0
- package/src/services/notifications/browser.ts +252 -0
- package/src/services/notifications/catalog.ts +113 -0
- package/src/services/notifications/channels.ts +104 -0
- package/src/services/notifications/dispatcher.ts +139 -0
- package/src/services/notifications/email.ts +118 -0
- package/src/services/notifications/index.ts +633 -0
- package/src/services/notifications/live.ts +50 -0
- package/src/services/notifications/observability.ts +407 -0
- package/src/services/notifications/platform.ts +421 -0
- package/src/services/notifications/runtime.ts +120 -0
- package/src/services/notifications/user.ts +248 -0
- package/src/services/notifications/web-push-transport.ts +135 -0
- package/src/services/oauth-tokens.ts +209 -0
- package/src/services/outbox.ts +187 -0
- package/src/services/pdf/gotenberg.ts +290 -0
- package/src/services/pdf/index.ts +43 -0
- package/src/services/pdf/markdown.ts +209 -0
- package/src/services/pdf/template-preview.ts +122 -0
- package/src/services/postgres.ts +76 -0
- package/src/services/providers/index.ts +27 -0
- package/src/services/providers/local/auth.ts +51 -0
- package/src/services/providers/local/index.ts +4 -0
- package/src/services/providers/local/users.ts +263 -0
- package/src/services/rail-preferences.ts +27 -0
- package/src/services/runtime-lifecycle.ts +191 -0
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.ts +891 -0
- package/src/services/service-accounts.ts +184 -0
- package/src/services/session/index.ts +236 -0
- package/src/services/session/test-fixture.ts +18 -0
- package/src/services/session/user.ts +144 -0
- package/src/services/settings/api.ts +61 -0
- package/src/services/settings/app.ts +86 -0
- package/src/services/settings/core-settings.ts +796 -0
- package/src/services/settings/crypto.ts +69 -0
- package/src/services/settings/defaults.ts +374 -0
- package/src/services/settings/index.ts +215 -0
- package/src/services/settings/namespace.ts +9 -0
- package/src/services/settings/snapshot.ts +49 -0
- package/src/services/settings/store.ts +241 -0
- package/src/services/settings/templates.ts +4 -0
- package/src/services/sync-ops.ts +403 -0
- package/src/services/topic-cursor.ts +22 -0
- package/src/services/weather/forecast.ts +323 -0
- package/src/services/weather/geo.ts +119 -0
- package/src/services/weather/index.ts +67 -0
- package/src/services/weather/location.ts +24 -0
- package/src/services/weather/locations.ts +133 -0
- package/src/services/weather/migrate.ts +45 -0
- package/src/services/weather/settings.ts +66 -0
- package/src/services/weather/short-id.ts +57 -0
- package/src/services/weather/types.ts +61 -0
- package/src/services/weather/ui.ts +50 -0
- package/src/services/webauthn.ts +385 -0
- package/src/shared/account-action-notice.ts +70 -0
- package/src/shared/account-display.ts +15 -0
- package/src/shared/account-session.ts +15 -0
- package/src/shared/ai-platform-prompt.ts +134 -0
- package/src/shared/ai-usage.ts +49 -0
- package/src/shared/app-presentation.ts +48 -0
- package/src/shared/app-url.ts +12 -0
- package/src/shared/branding.ts +14 -0
- package/src/shared/capability-messages.ts +70 -0
- package/src/shared/email-html.ts +54 -0
- package/src/shared/format.ts +91 -0
- package/src/shared/help.ts +68 -0
- package/src/shared/icons.ts +400 -0
- package/src/shared/index.ts +54 -0
- package/src/shared/locale.ts +48 -0
- package/src/shared/login-method.ts +44 -0
- package/src/shared/markdown/client.ts +130 -0
- package/src/shared/markdown/extensions/code.ts +105 -0
- package/src/shared/markdown/extensions/guided-help.ts +100 -0
- package/src/shared/markdown/extensions/images.ts +79 -0
- package/src/shared/markdown/extensions/info-blocks.ts +97 -0
- package/src/shared/markdown/extensions/katex.ts +120 -0
- package/src/shared/markdown/extensions/links.ts +43 -0
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +109 -0
- package/src/shared/markdown/extensions/task-list.ts +53 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +193 -0
- package/src/shared/markdown/shared.ts +36 -0
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/network-address.ts +106 -0
- package/src/shared/redirect.ts +56 -0
- package/src/shared/template-rendering.ts +140 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +53 -0
- package/src/ssr/AdminSidebar.tsx +50 -0
- package/src/ssr/AppLaunchpad.island.tsx +236 -0
- package/src/ssr/AppWorkspaceController.island.tsx +21 -0
- package/src/ssr/BrowserNotifications.island.tsx +135 -0
- package/src/ssr/GlobalAnnouncements.island.tsx +142 -0
- package/src/ssr/GlobalSearchDialog.behavior.test.tsx +96 -0
- package/src/ssr/GlobalSearchDialog.tsx +35 -0
- package/src/ssr/GlobalSearchHelpDialog.tsx +104 -0
- package/src/ssr/GlobalSearchTrigger.island.tsx +55 -0
- package/src/ssr/HotkeysHelpRail.island.tsx +55 -0
- package/src/ssr/Layout.render.test.tsx +148 -0
- package/src/ssr/Layout.tsx +292 -0
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutFooter.tsx +22 -0
- package/src/ssr/LayoutHeader.tsx +81 -0
- package/src/ssr/LayoutHelp.tsx +869 -0
- package/src/ssr/LayoutPreferences.island.tsx +36 -0
- package/src/ssr/LayoutRail.tsx +58 -0
- package/src/ssr/MinimalLayout.tsx +46 -0
- package/src/ssr/PageError.tsx +29 -0
- package/src/ssr/ProfilePreferences.island.tsx +92 -0
- package/src/ssr/RailApps.island.tsx +61 -0
- package/src/ssr/RailEditor.tsx +293 -0
- package/src/ssr/RegisteredHelpDocuments.island.tsx +3 -0
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/admin-active-link.ts +47 -0
- package/src/ssr/admin-navigation.ts +78 -0
- package/src/ssr/app-appearance.ts +23 -0
- package/src/ssr/app-navigation.ts +41 -0
- package/src/ssr/help-messages.ts +104 -0
- package/src/ssr/index.ts +14 -0
- package/src/ssr/islands/SearchBar.island.tsx +80 -0
- package/src/ssr/islands/index.ts +16 -0
- package/src/ssr/layout-context.ts +17 -0
- package/src/ssr/layout-help-markdown.ts +9 -0
- package/src/ssr/layout-help-navigation.ts +24 -0
- package/src/ssr/layout-help-url.ts +5 -0
- package/src/ssr/layout-runtime.ts +22 -0
- package/src/ssr/page-error-messages.ts +25 -0
- package/src/ssr/platform-messages.ts +135 -0
- package/src/ssr/preference-controller.ts +44 -0
- package/src/ssr/profile-preferences-messages.ts +29 -0
- package/src/ssr/rail-context.ts +21 -0
- package/src/ssr/rail-messages.ts +66 -0
- package/src/ssr/rail-navigation.ts +60 -0
- package/src/ssr/runtime.ts +31 -0
- package/src/ssr/url-filter.ts +127 -0
- package/src/styles/base-popover.css +36 -0
- package/src/styles/effects.css +851 -0
- package/src/styles/global.css +268 -0
- package/src/styles/input.css +58 -0
- package/src/styles/tokens.css +230 -0
- package/src/styles/utilities-buttons.css +405 -0
- package/src/styles/utilities-data.css +33 -0
- package/src/styles/utilities-feedback.css +232 -0
- package/src/styles/utilities-layout.css +603 -0
- package/src/styles/utilities-markdown-editor.css +525 -0
- package/src/styles/utilities-markdown-table.css +234 -0
- package/src/styles/utilities-navigation.css +1054 -0
- package/src/types/ambient.d.ts +24 -0
- package/src/workflows/ai/index.ts +17 -0
- package/src/workflows/ai/runtime.ts +286 -0
- package/src/workflows/ai/store.ts +314 -0
- package/src/workflows/ai/types.ts +111 -0
- package/src/workflows/ai-actions.ts +269 -0
- package/src/workflows/authoring.ts +88 -0
- package/src/workflows/builtins.ts +193 -0
- package/src/workflows/contracts.ts +280 -0
- package/src/workflows/definition.ts +334 -0
- package/src/workflows/editor.ts +113 -0
- package/src/workflows/index.ts +71 -0
- package/src/workflows/language/binder.ts +45 -0
- package/src/workflows/language/canonical.ts +29 -0
- package/src/workflows/language/compiler.ts +396 -0
- package/src/workflows/language/expressions.ts +34 -0
- package/src/workflows/language/index.ts +20 -0
- package/src/workflows/language/references.ts +55 -0
- package/src/workflows/language/schema.ts +134 -0
- package/src/workflows/language/strict-yaml.ts +131 -0
- package/src/workflows/manifest.ts +24 -0
- package/src/workflows/module.ts +12 -0
- package/src/workflows/runtime/coordinator.ts +115 -0
- package/src/workflows/runtime/executor.ts +1003 -0
- package/src/workflows/runtime/index.ts +60 -0
- package/src/workflows/runtime/instant.ts +8 -0
- package/src/workflows/runtime/ports.ts +213 -0
- package/src/workflows/runtime/schedule.ts +203 -0
- package/src/workflows/runtime/trigger.ts +37 -0
- package/src/workflows/runtime/wake.ts +13 -0
- package/src/workflows/store/actions.ts +438 -0
- package/src/workflows/store/budget.ts +145 -0
- package/src/workflows/store/definitions.ts +345 -0
- package/src/workflows/store/events.ts +383 -0
- package/src/workflows/store/index.ts +107 -0
- package/src/workflows/store/observability.ts +594 -0
- package/src/workflows/store/runs.ts +975 -0
- package/src/workflows/store/transaction.ts +23 -0
- package/src/workflows/store/worker.ts +439 -0
- package/src/workflows/testing/conformance.ts +72 -0
- package/src/workflows/testing/index.ts +11 -0
- package/src/workflows/testing/process-fixtures.ts +275 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { crypto } from "@k2b/stdlib";
|
|
2
|
+
import { sql } from "bun";
|
|
3
|
+
import { CompactSign, compactVerify, exportJWK, generateKeyPair, importJWK, type JWK } from "jose";
|
|
4
|
+
import { logger } from "../logging";
|
|
5
|
+
import {
|
|
6
|
+
CLOUD_IDENTITY_ALGORITHM,
|
|
7
|
+
IDENTITY_ACTIVATION_LEAD_MS,
|
|
8
|
+
IDENTITY_CLOCK_TOLERANCE_SECONDS,
|
|
9
|
+
IDENTITY_ROLLOUT_MARGIN_MS,
|
|
10
|
+
IDENTITY_ROTATION_AGE_MS,
|
|
11
|
+
IDENTITY_SIGNING_CACHE_MS,
|
|
12
|
+
} from "./constants";
|
|
13
|
+
import { type IdentityKeyEncryptionConfig, readIdentityKeyEncryptionConfig } from "./key-config";
|
|
14
|
+
import { identityMetrics } from "./metrics";
|
|
15
|
+
import { getIdentityRuntimeConfig } from "./runtime-config";
|
|
16
|
+
import { SignerRefreshes } from "./signer-refresh";
|
|
17
|
+
|
|
18
|
+
export type SigningKeyPurpose = "session" | "invocation" | "oauth";
|
|
19
|
+
export type SigningKeyState = "pending" | "active" | "retired" | "revoked";
|
|
20
|
+
const SIGNING_KEY_PURPOSES = ["session", "invocation", "oauth"] as const satisfies readonly SigningKeyPurpose[];
|
|
21
|
+
|
|
22
|
+
type SigningKeyRow = {
|
|
23
|
+
id: string;
|
|
24
|
+
purpose: SigningKeyPurpose;
|
|
25
|
+
state: SigningKeyState;
|
|
26
|
+
kid: string;
|
|
27
|
+
alg: "RS256";
|
|
28
|
+
public_jwk: JWK | string;
|
|
29
|
+
encrypted_private_jwk: string;
|
|
30
|
+
encryption_key_id: string;
|
|
31
|
+
created_at: Date | string;
|
|
32
|
+
activate_at: Date | string;
|
|
33
|
+
activated_at: Date | string | null;
|
|
34
|
+
sign_until: Date | string;
|
|
35
|
+
retired_at: Date | string | null;
|
|
36
|
+
verify_until: Date | string;
|
|
37
|
+
revoked_at: Date | string | null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type PreparedIdentitySigner = {
|
|
41
|
+
kid: string;
|
|
42
|
+
key: CryptoKey;
|
|
43
|
+
signUntil: Date;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type IdentitySigningKeyStatus = {
|
|
47
|
+
purpose: SigningKeyPurpose;
|
|
48
|
+
state: SigningKeyState;
|
|
49
|
+
kid: string;
|
|
50
|
+
alg: "RS256";
|
|
51
|
+
encryptionKeyId: string;
|
|
52
|
+
createdAt: string;
|
|
53
|
+
activateAt: string;
|
|
54
|
+
signUntil: string;
|
|
55
|
+
verifyUntil: string;
|
|
56
|
+
revokedAt: string | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type GeneratedKey = {
|
|
60
|
+
kid: string;
|
|
61
|
+
publicJwk: JWK;
|
|
62
|
+
encryptedPrivateJwk: string;
|
|
63
|
+
encryptionKeyId: string;
|
|
64
|
+
privateKey: CryptoKey;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const log = logger("core:identity-keyring");
|
|
68
|
+
const encoder = new TextEncoder();
|
|
69
|
+
const PURPOSE_FALLBACK_TOKEN_TTL_MS: Record<SigningKeyPurpose, number> = {
|
|
70
|
+
session: 24 * 60 * 60_000,
|
|
71
|
+
invocation: 30_000,
|
|
72
|
+
oauth: 2 * 60 * 60_000,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const date = (value: Date | string): Date => (value instanceof Date ? value : new Date(value));
|
|
76
|
+
const publicJwk = (value: JWK | string): JWK => (typeof value === "string" ? (JSON.parse(value) as JWK) : value);
|
|
77
|
+
const serializedSize = (value: unknown): number => encoder.encode(JSON.stringify(value)).byteLength;
|
|
78
|
+
|
|
79
|
+
const assertRsaJwk = (value: JWK, privateRequired: boolean): void => {
|
|
80
|
+
if (value.kty !== "RSA" || typeof value.n !== "string" || typeof value.e !== "string") {
|
|
81
|
+
throw new Error("Cloud identity key is not an RSA JWK");
|
|
82
|
+
}
|
|
83
|
+
if (privateRequired && typeof value.d !== "string") throw new Error("Cloud identity private JWK is incomplete");
|
|
84
|
+
if (value.alg && value.alg !== CLOUD_IDENTITY_ALGORITHM) throw new Error("Cloud identity key uses an unsupported algorithm");
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const verifyPair = async (privateJwk: JWK, expectedPublicJwk: JWK): Promise<CryptoKey> => {
|
|
88
|
+
assertRsaJwk(privateJwk, true);
|
|
89
|
+
assertRsaJwk(expectedPublicJwk, false);
|
|
90
|
+
if (privateJwk.n !== expectedPublicJwk.n || privateJwk.e !== expectedPublicJwk.e) {
|
|
91
|
+
throw new Error("Cloud identity private/public key pair does not match");
|
|
92
|
+
}
|
|
93
|
+
const importedPrivate = await importJWK(privateJwk, CLOUD_IDENTITY_ALGORITHM);
|
|
94
|
+
const importedPublic = await importJWK(expectedPublicJwk, CLOUD_IDENTITY_ALGORITHM);
|
|
95
|
+
if (!(importedPrivate instanceof CryptoKey) || !(importedPublic instanceof CryptoKey)) {
|
|
96
|
+
throw new Error("Cloud identity RSA JWK imported as an unexpected key type");
|
|
97
|
+
}
|
|
98
|
+
const probe = encoder.encode("cloud-identity-key-pair-check");
|
|
99
|
+
const signature = await new CompactSign(probe).setProtectedHeader({ alg: CLOUD_IDENTITY_ALGORITHM }).sign(importedPrivate);
|
|
100
|
+
const verified = await compactVerify(signature, importedPublic, { algorithms: [CLOUD_IDENTITY_ALGORITHM] });
|
|
101
|
+
if (new TextDecoder().decode(verified.payload) !== "cloud-identity-key-pair-check") {
|
|
102
|
+
throw new Error("Cloud identity key-pair verification failed");
|
|
103
|
+
}
|
|
104
|
+
return importedPrivate;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const decryptPrivateJwk = async (row: SigningKeyRow, keys: IdentityKeyEncryptionConfig): Promise<JWK> => {
|
|
108
|
+
const candidate =
|
|
109
|
+
row.encryption_key_id === keys.current.id
|
|
110
|
+
? keys.current
|
|
111
|
+
: row.encryption_key_id === keys.previous?.id
|
|
112
|
+
? keys.previous
|
|
113
|
+
: row.encryption_key_id === keys.next?.id
|
|
114
|
+
? keys.next
|
|
115
|
+
: null;
|
|
116
|
+
if (!candidate) throw new Error(`No configured KEK can decrypt Cloud identity key ${row.kid}`);
|
|
117
|
+
const plaintext = await crypto.symmetric.decrypt({ payload: row.encrypted_private_jwk, key: candidate.key });
|
|
118
|
+
const parsed = JSON.parse(plaintext) as JWK;
|
|
119
|
+
if (serializedSize(parsed) > 16_384) throw new Error("Cloud identity private JWK exceeds its size bound");
|
|
120
|
+
return parsed;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const generateEncryptedKey = async (keys: IdentityKeyEncryptionConfig): Promise<GeneratedKey> => {
|
|
124
|
+
const { privateKey, publicKey } = await generateKeyPair(CLOUD_IDENTITY_ALGORITHM, { extractable: true, modulusLength: 2048 });
|
|
125
|
+
const kid = crypto.common.uuid();
|
|
126
|
+
const [rawPrivate, rawPublic] = await Promise.all([exportJWK(privateKey), exportJWK(publicKey)]);
|
|
127
|
+
const privateJwk: JWK = { ...rawPrivate, alg: CLOUD_IDENTITY_ALGORITHM, kid, use: "sig" };
|
|
128
|
+
const exposedPublicJwk: JWK = { ...rawPublic, alg: CLOUD_IDENTITY_ALGORITHM, kid, use: "sig" };
|
|
129
|
+
assertRsaJwk(privateJwk, true);
|
|
130
|
+
assertRsaJwk(exposedPublicJwk, false);
|
|
131
|
+
if (serializedSize(exposedPublicJwk) > 16_384 || serializedSize(privateJwk) > 16_384) {
|
|
132
|
+
throw new Error("Generated Cloud identity JWK exceeds its size bound");
|
|
133
|
+
}
|
|
134
|
+
const encryptedPrivateJwk = await crypto.symmetric.encrypt({
|
|
135
|
+
payload: JSON.stringify(privateJwk),
|
|
136
|
+
key: keys.current.key,
|
|
137
|
+
stretched: false,
|
|
138
|
+
});
|
|
139
|
+
if (encoder.encode(encryptedPrivateJwk).byteLength > 32_768) {
|
|
140
|
+
throw new Error("Encrypted Cloud identity private JWK exceeds its size bound");
|
|
141
|
+
}
|
|
142
|
+
return { kid, publicJwk: exposedPublicJwk, encryptedPrivateJwk, encryptionKeyId: keys.current.id, privateKey };
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const signingDeadline = (activateAt: Date): Date =>
|
|
146
|
+
new Date(activateAt.getTime() + IDENTITY_ROTATION_AGE_MS + IDENTITY_ACTIVATION_LEAD_MS + IDENTITY_SIGNING_CACHE_MS);
|
|
147
|
+
|
|
148
|
+
const cache = new Map<SigningKeyPurpose, { signer: PreparedIdentitySigner; expiresAt: number }>();
|
|
149
|
+
const signerRefreshes = new SignerRefreshes<SigningKeyPurpose, PreparedIdentitySigner>();
|
|
150
|
+
let maintenanceTimer: ReturnType<typeof setInterval> | null = null;
|
|
151
|
+
|
|
152
|
+
const importRow = async (row: SigningKeyRow, keys: IdentityKeyEncryptionConfig): Promise<PreparedIdentitySigner> => {
|
|
153
|
+
if (row.alg !== CLOUD_IDENTITY_ALGORITHM || row.state !== "active") throw new Error("Cloud identity signer row is not active RS256");
|
|
154
|
+
const privateJwk = await decryptPrivateJwk(row, keys);
|
|
155
|
+
const key = await verifyPair(privateJwk, publicJwk(row.public_jwk));
|
|
156
|
+
return { kid: row.kid, key, signUntil: date(row.sign_until) };
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const selectRows = async (purpose: SigningKeyPurpose): Promise<SigningKeyRow[]> =>
|
|
160
|
+
sql<SigningKeyRow[]>`
|
|
161
|
+
SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
|
|
162
|
+
created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
|
|
163
|
+
FROM auth.signing_keys
|
|
164
|
+
WHERE purpose = ${purpose} AND state IN ('pending', 'active')
|
|
165
|
+
ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END, created_at DESC
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const insertKey = async (
|
|
169
|
+
db: typeof sql,
|
|
170
|
+
purpose: SigningKeyPurpose,
|
|
171
|
+
generated: GeneratedKey,
|
|
172
|
+
state: "pending" | "active",
|
|
173
|
+
now: Date,
|
|
174
|
+
): Promise<void> => {
|
|
175
|
+
const activateAt = state === "active" ? now : new Date(now.getTime() + IDENTITY_ACTIVATION_LEAD_MS);
|
|
176
|
+
const signUntil = signingDeadline(activateAt);
|
|
177
|
+
const purposeTtl = PURPOSE_FALLBACK_TOKEN_TTL_MS[purpose];
|
|
178
|
+
const verifyUntil = new Date(signUntil.getTime() + purposeTtl + IDENTITY_CLOCK_TOLERANCE_SECONDS * 1_000 + IDENTITY_ROLLOUT_MARGIN_MS);
|
|
179
|
+
await db`
|
|
180
|
+
INSERT INTO auth.signing_keys (
|
|
181
|
+
purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
|
|
182
|
+
created_at, activate_at, activated_at, sign_until, verify_until
|
|
183
|
+
) VALUES (
|
|
184
|
+
${purpose}, ${state}, ${generated.kid}, ${CLOUD_IDENTITY_ALGORITHM}, ${JSON.stringify(generated.publicJwk)}::jsonb,
|
|
185
|
+
${generated.encryptedPrivateJwk}, ${generated.encryptionKeyId}, ${now}, ${activateAt},
|
|
186
|
+
${state === "active" ? now : null}, ${signUntil}, ${verifyUntil}
|
|
187
|
+
)
|
|
188
|
+
`;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const maintainPurpose = async (purpose: SigningKeyPurpose): Promise<SigningKeyRow> => {
|
|
192
|
+
const keys = await readIdentityKeyEncryptionConfig();
|
|
193
|
+
const before = await selectRows(purpose);
|
|
194
|
+
const beforeActive = before.find((row) => row.state === "active");
|
|
195
|
+
const beforePending = before.find((row) => row.state === "pending");
|
|
196
|
+
const nowMs = Date.now();
|
|
197
|
+
const activeIsFresh =
|
|
198
|
+
beforeActive &&
|
|
199
|
+
nowMs < date(beforeActive.sign_until).getTime() - IDENTITY_ACTIVATION_LEAD_MS &&
|
|
200
|
+
nowMs < date(beforeActive.created_at).getTime() + IDENTITY_ROTATION_AGE_MS;
|
|
201
|
+
if (activeIsFresh && (!beforePending || date(beforePending.activate_at).getTime() > nowMs)) return beforeActive;
|
|
202
|
+
|
|
203
|
+
const generated =
|
|
204
|
+
!beforeActive || date(beforeActive.sign_until).getTime() <= nowMs || (!beforePending && !activeIsFresh)
|
|
205
|
+
? await generateEncryptedKey(keys)
|
|
206
|
+
: null;
|
|
207
|
+
const active = await sql.begin(async (tx) => {
|
|
208
|
+
await tx`SELECT pg_advisory_xact_lock(hashtextextended(${`cloud:identity-keyring:${purpose}`}, 0))`;
|
|
209
|
+
const rows = await tx<SigningKeyRow[]>`
|
|
210
|
+
SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
|
|
211
|
+
created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
|
|
212
|
+
FROM auth.signing_keys
|
|
213
|
+
WHERE purpose = ${purpose} AND state IN ('pending', 'active')
|
|
214
|
+
ORDER BY CASE state WHEN 'active' THEN 0 ELSE 1 END, created_at DESC
|
|
215
|
+
FOR UPDATE
|
|
216
|
+
`;
|
|
217
|
+
let current = rows.find((row) => row.state === "active");
|
|
218
|
+
let pending = rows.find((row) => row.state === "pending");
|
|
219
|
+
const now = new Date();
|
|
220
|
+
|
|
221
|
+
// Long downtime can outlive both signing deadlines. Preserve their public
|
|
222
|
+
// verification windows, but never promote or return an expired signer.
|
|
223
|
+
for (const row of [current, pending]) {
|
|
224
|
+
if (!row || date(row.sign_until).getTime() > now.getTime()) continue;
|
|
225
|
+
await tx`UPDATE auth.signing_keys SET state = 'retired', retired_at = ${now} WHERE id = ${row.id}`;
|
|
226
|
+
if (row === current) current = undefined;
|
|
227
|
+
if (row === pending) pending = undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (pending && date(pending.activate_at).getTime() <= now.getTime()) {
|
|
231
|
+
if (current) {
|
|
232
|
+
const overlapSignUntil = new Date(now.getTime() + IDENTITY_SIGNING_CACHE_MS);
|
|
233
|
+
await tx`
|
|
234
|
+
UPDATE auth.signing_keys
|
|
235
|
+
SET state = 'retired', retired_at = ${now}, sign_until = ${overlapSignUntil},
|
|
236
|
+
verify_until = GREATEST(verify_until, ${new Date(
|
|
237
|
+
overlapSignUntil.getTime() +
|
|
238
|
+
PURPOSE_FALLBACK_TOKEN_TTL_MS[purpose] +
|
|
239
|
+
IDENTITY_CLOCK_TOLERANCE_SECONDS * 1_000 +
|
|
240
|
+
IDENTITY_ROLLOUT_MARGIN_MS,
|
|
241
|
+
)})
|
|
242
|
+
WHERE id = ${current.id}
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
const [promoted] = await tx<SigningKeyRow[]>`
|
|
246
|
+
UPDATE auth.signing_keys
|
|
247
|
+
SET state = 'active', activated_at = ${now}
|
|
248
|
+
WHERE id = ${pending.id}
|
|
249
|
+
RETURNING id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
|
|
250
|
+
created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
|
|
251
|
+
`;
|
|
252
|
+
if (!promoted) throw new Error(`Failed to promote ${purpose} signing key`);
|
|
253
|
+
current = promoted;
|
|
254
|
+
identityMetrics.increment("rotation_success");
|
|
255
|
+
log.info("Cloud identity signing key promoted", { purpose, kid: promoted.kid });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!current) {
|
|
259
|
+
const candidate = generated ?? (await generateEncryptedKey(keys));
|
|
260
|
+
await insertKey(tx as typeof sql, purpose, candidate, "active", now);
|
|
261
|
+
const [created] = await tx<SigningKeyRow[]>`
|
|
262
|
+
SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
|
|
263
|
+
created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
|
|
264
|
+
FROM auth.signing_keys WHERE purpose = ${purpose} AND state = 'active'
|
|
265
|
+
`;
|
|
266
|
+
if (!created) throw new Error(`Failed to create initial ${purpose} signing key`);
|
|
267
|
+
current = created;
|
|
268
|
+
identityMetrics.increment("rotation_success");
|
|
269
|
+
log.info("Initial Cloud identity signing key created", { purpose, kid: created.kid });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const currentDue =
|
|
273
|
+
date(current.created_at).getTime() + IDENTITY_ROTATION_AGE_MS <= now.getTime() ||
|
|
274
|
+
date(current.sign_until).getTime() - IDENTITY_ACTIVATION_LEAD_MS <= now.getTime();
|
|
275
|
+
if (!pending && currentDue) {
|
|
276
|
+
const candidate = generated ?? (await generateEncryptedKey(keys));
|
|
277
|
+
await insertKey(tx as typeof sql, purpose, candidate, "pending", now);
|
|
278
|
+
log.info("Cloud identity signing key pre-published", { purpose, kid: candidate.kid });
|
|
279
|
+
}
|
|
280
|
+
return current;
|
|
281
|
+
});
|
|
282
|
+
return active;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const prepareIdentitySigner = async (purpose: SigningKeyPurpose): Promise<PreparedIdentitySigner> => {
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const cached = cache.get(purpose);
|
|
288
|
+
if (cached && cached.expiresAt > now && cached.signer.signUntil.getTime() > now) return cached.signer;
|
|
289
|
+
return signerRefreshes.run(
|
|
290
|
+
purpose,
|
|
291
|
+
async () => {
|
|
292
|
+
try {
|
|
293
|
+
const keys = await readIdentityKeyEncryptionConfig();
|
|
294
|
+
const row = await maintainPurpose(purpose);
|
|
295
|
+
const signer = await importRow(row, keys);
|
|
296
|
+
if (signer.signUntil.getTime() <= now) throw new Error(`Active ${purpose} signing key is past sign_until`);
|
|
297
|
+
return signer;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
identityMetrics.increment("rotation_failure");
|
|
300
|
+
log.error("Cloud identity signer refresh failed", { purpose, error: error instanceof Error ? error.message : String(error) });
|
|
301
|
+
if (cached && cached.signer.signUntil.getTime() > now) return cached.signer;
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
(signer) => {
|
|
306
|
+
cache.set(purpose, { signer, expiresAt: Math.min(Date.now() + IDENTITY_SIGNING_CACHE_MS, signer.signUntil.getTime()) });
|
|
307
|
+
},
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Derives a local result while the selected key is still active in Postgres.
|
|
313
|
+
* The shared row lock makes emergency revocation wait for already-started
|
|
314
|
+
* issuance and prevents replicas from releasing a new result under that key
|
|
315
|
+
* after the revocation commits.
|
|
316
|
+
*/
|
|
317
|
+
export const withActiveIdentitySigner = async <T>(
|
|
318
|
+
purpose: SigningKeyPurpose,
|
|
319
|
+
callback: (signer: PreparedIdentitySigner & { issuer: string }, db: typeof sql) => Promise<T>,
|
|
320
|
+
options: { pool?: typeof sql; signal?: AbortSignal; timeoutMs?: number } = {},
|
|
321
|
+
): Promise<T> => {
|
|
322
|
+
const timeoutMs = options.timeoutMs ?? 30_000;
|
|
323
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new Error("Identity issuance timeout must be positive");
|
|
324
|
+
const deadline = Date.now() + timeoutMs;
|
|
325
|
+
const checkDeadline = () => {
|
|
326
|
+
options.signal?.throwIfAborted();
|
|
327
|
+
if (Date.now() >= deadline) throw new DOMException("Identity issuance deadline exceeded", "TimeoutError");
|
|
328
|
+
};
|
|
329
|
+
// Preparation can refresh/rotate keys using the pool. Never do it while
|
|
330
|
+
// holding a caller's transaction: every issuance needs just one connection.
|
|
331
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
332
|
+
checkDeadline();
|
|
333
|
+
const signer = await prepareIdentitySigner(purpose);
|
|
334
|
+
const { issuer } = await getIdentityRuntimeConfig();
|
|
335
|
+
checkDeadline();
|
|
336
|
+
const run = async (db: typeof sql): Promise<{ active: false } | { active: true; result: T }> => {
|
|
337
|
+
checkDeadline();
|
|
338
|
+
// A JS deadline alone cannot cancel a blocked PostgreSQL query. Bound
|
|
339
|
+
// each statement (including lock waits) on this connection, transaction-local.
|
|
340
|
+
await db`SELECT set_config('statement_timeout', ${`${Math.max(1, Math.floor(deadline - Date.now()))}ms`}, true)`;
|
|
341
|
+
const [active] = await db<Array<{ kid: string }>>`
|
|
342
|
+
SELECT kid
|
|
343
|
+
FROM auth.signing_keys
|
|
344
|
+
WHERE kid = ${signer.kid}
|
|
345
|
+
AND purpose = ${purpose}
|
|
346
|
+
AND state = 'active'
|
|
347
|
+
AND sign_until > now()
|
|
348
|
+
FOR SHARE
|
|
349
|
+
`;
|
|
350
|
+
checkDeadline();
|
|
351
|
+
if (!active) return { active: false };
|
|
352
|
+
const result = await callback({ ...signer, issuer }, db);
|
|
353
|
+
return { active: true, result };
|
|
354
|
+
};
|
|
355
|
+
const checked = await (options.pool ?? sql).begin(run);
|
|
356
|
+
checkDeadline();
|
|
357
|
+
if (checked.active) return checked.result;
|
|
358
|
+
invalidateIdentitySignerCache(purpose, signer.kid);
|
|
359
|
+
}
|
|
360
|
+
throw new Error(`No active ${purpose} identity signer is available`);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
export const initializeIdentityAuthority = async (): Promise<void> => {
|
|
364
|
+
for (const purpose of SIGNING_KEY_PURPOSES) await prepareIdentitySigner(purpose);
|
|
365
|
+
const keys = await readIdentityKeyEncryptionConfig();
|
|
366
|
+
if (keys.previous) await rewrapIdentitySigningKeys();
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export const listIdentityJwks = async (purpose: "session" | "invocation"): Promise<{ keys: JWK[]; etag: string }> => {
|
|
370
|
+
const rows = await sql<Array<{ public_jwk: JWK | string }>>`
|
|
371
|
+
SELECT public_jwk
|
|
372
|
+
FROM auth.signing_keys
|
|
373
|
+
WHERE purpose = ${purpose}
|
|
374
|
+
AND state IN ('pending', 'active', 'retired')
|
|
375
|
+
AND verify_until > now()
|
|
376
|
+
ORDER BY created_at
|
|
377
|
+
`;
|
|
378
|
+
const keys = rows.map((row) => publicJwk(row.public_jwk));
|
|
379
|
+
return { keys, etag: `"${(await crypto.common.hash(JSON.stringify({ purpose, keys }))).slice(0, 32)}"` };
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
export const revokeIdentitySigningKey = async (params: { kid: string; reason: string }): Promise<boolean> => {
|
|
383
|
+
const reason = params.reason.trim().slice(0, 500);
|
|
384
|
+
if (!reason) throw new Error("Emergency key revocation requires a reason");
|
|
385
|
+
const rows = await sql<Array<{ purpose: SigningKeyPurpose }>>`
|
|
386
|
+
UPDATE auth.signing_keys
|
|
387
|
+
SET state = 'revoked', revoked_at = now(), revoke_reason = ${reason}
|
|
388
|
+
WHERE kid = ${params.kid} AND state <> 'revoked'
|
|
389
|
+
RETURNING purpose
|
|
390
|
+
`;
|
|
391
|
+
for (const row of rows) invalidateIdentitySignerCache(row.purpose);
|
|
392
|
+
if (rows.length > 0) {
|
|
393
|
+
const purpose = rows[0]!.purpose;
|
|
394
|
+
log.warn("Cloud identity signing key revoked", { kid: params.kid, purpose, reason });
|
|
395
|
+
await prepareIdentitySigner(purpose);
|
|
396
|
+
}
|
|
397
|
+
return rows.length > 0;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const getIdentitySigningKeyStatus = async (): Promise<IdentitySigningKeyStatus[]> => {
|
|
401
|
+
const rows = await sql<SigningKeyRow[]>`
|
|
402
|
+
SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
|
|
403
|
+
created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
|
|
404
|
+
FROM auth.signing_keys
|
|
405
|
+
ORDER BY purpose, created_at DESC
|
|
406
|
+
`;
|
|
407
|
+
return rows.map((row) => ({
|
|
408
|
+
purpose: row.purpose,
|
|
409
|
+
state: row.state,
|
|
410
|
+
kid: row.kid,
|
|
411
|
+
alg: row.alg,
|
|
412
|
+
encryptionKeyId: row.encryption_key_id,
|
|
413
|
+
createdAt: date(row.created_at).toISOString(),
|
|
414
|
+
activateAt: date(row.activate_at).toISOString(),
|
|
415
|
+
signUntil: date(row.sign_until).toISOString(),
|
|
416
|
+
verifyUntil: date(row.verify_until).toISOString(),
|
|
417
|
+
revokedAt: row.revoked_at ? date(row.revoked_at).toISOString() : null,
|
|
418
|
+
}));
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
export const rewrapIdentitySigningKeys = async (): Promise<number> => {
|
|
422
|
+
const keys = await readIdentityKeyEncryptionConfig();
|
|
423
|
+
const rows = await sql<SigningKeyRow[]>`
|
|
424
|
+
SELECT id, purpose, state, kid, alg, public_jwk, encrypted_private_jwk, encryption_key_id,
|
|
425
|
+
created_at, activate_at, activated_at, sign_until, retired_at, verify_until, revoked_at
|
|
426
|
+
FROM auth.signing_keys
|
|
427
|
+
ORDER BY created_at
|
|
428
|
+
`;
|
|
429
|
+
const replacements: Array<{ id: string; encrypted: string }> = [];
|
|
430
|
+
for (const row of rows) {
|
|
431
|
+
const privateJwk = await decryptPrivateJwk(row, keys);
|
|
432
|
+
await verifyPair(privateJwk, publicJwk(row.public_jwk));
|
|
433
|
+
if (row.encryption_key_id === keys.current.id) continue;
|
|
434
|
+
const encrypted = await crypto.symmetric.encrypt({ payload: JSON.stringify(privateJwk), key: keys.current.key, stretched: false });
|
|
435
|
+
const verifiedPlaintext = await crypto.symmetric.decrypt({ payload: encrypted, key: keys.current.key });
|
|
436
|
+
await verifyPair(JSON.parse(verifiedPlaintext) as JWK, publicJwk(row.public_jwk));
|
|
437
|
+
replacements.push({
|
|
438
|
+
id: row.id,
|
|
439
|
+
encrypted,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
if (replacements.length === 0) return 0;
|
|
443
|
+
await sql.begin(async (tx) => {
|
|
444
|
+
for (const replacement of replacements) {
|
|
445
|
+
await tx`
|
|
446
|
+
UPDATE auth.signing_keys
|
|
447
|
+
SET encrypted_private_jwk = ${replacement.encrypted}, encryption_key_id = ${keys.current.id}
|
|
448
|
+
WHERE id = ${replacement.id}
|
|
449
|
+
`;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
cache.clear();
|
|
453
|
+
signerRefreshes.invalidateAll();
|
|
454
|
+
log.info("Cloud identity signing keys rewrapped", { count: replacements.length, encryptionKeyId: keys.current.id });
|
|
455
|
+
return replacements.length;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const maintainVerificationWindows = async (): Promise<void> => {
|
|
459
|
+
await sql`
|
|
460
|
+
UPDATE auth.signing_keys key
|
|
461
|
+
SET verify_until = GREATEST(
|
|
462
|
+
key.verify_until,
|
|
463
|
+
family.latest_expiry + (${IDENTITY_CLOCK_TOLERANCE_SECONDS}::int * INTERVAL '1 second') + (${Math.floor(
|
|
464
|
+
IDENTITY_ROLLOUT_MARGIN_MS / 1_000,
|
|
465
|
+
)}::int * INTERVAL '1 second')
|
|
466
|
+
)
|
|
467
|
+
FROM (
|
|
468
|
+
SELECT signing_kid, max(expires_at) AS latest_expiry
|
|
469
|
+
FROM auth.session_families
|
|
470
|
+
GROUP BY signing_kid
|
|
471
|
+
) family
|
|
472
|
+
WHERE key.kid = family.signing_kid AND key.purpose = 'session'
|
|
473
|
+
`;
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
export const runIdentityKeyMaintenance = async (): Promise<void> => {
|
|
477
|
+
for (const purpose of SIGNING_KEY_PURPOSES) await maintainPurpose(purpose);
|
|
478
|
+
await maintainVerificationWindows();
|
|
479
|
+
await sql`
|
|
480
|
+
DELETE FROM auth.session_families
|
|
481
|
+
WHERE sid IN (
|
|
482
|
+
SELECT sid FROM auth.session_families
|
|
483
|
+
WHERE expires_at < now() - INTERVAL '24 hours'
|
|
484
|
+
OR revoked_at < now() - INTERVAL '24 hours'
|
|
485
|
+
ORDER BY COALESCE(revoked_at, expires_at)
|
|
486
|
+
LIMIT 500
|
|
487
|
+
)
|
|
488
|
+
`;
|
|
489
|
+
await sql`
|
|
490
|
+
DELETE FROM auth.signing_keys
|
|
491
|
+
WHERE id IN (
|
|
492
|
+
SELECT key.id FROM auth.signing_keys key
|
|
493
|
+
WHERE key.state = 'retired' AND key.verify_until < now()
|
|
494
|
+
AND NOT EXISTS (
|
|
495
|
+
SELECT 1 FROM auth.session_families family
|
|
496
|
+
WHERE family.signing_kid = key.kid
|
|
497
|
+
)
|
|
498
|
+
ORDER BY verify_until
|
|
499
|
+
LIMIT 100
|
|
500
|
+
)
|
|
501
|
+
`;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export const startIdentityKeyMaintenance = (): (() => void) => {
|
|
505
|
+
if (maintenanceTimer) return () => undefined;
|
|
506
|
+
maintenanceTimer = setInterval(() => {
|
|
507
|
+
void runIdentityKeyMaintenance().catch((error) => {
|
|
508
|
+
identityMetrics.increment("rotation_failure");
|
|
509
|
+
log.error("Cloud identity key maintenance failed", { error: error instanceof Error ? error.message : String(error) });
|
|
510
|
+
});
|
|
511
|
+
}, IDENTITY_SIGNING_CACHE_MS);
|
|
512
|
+
maintenanceTimer.unref?.();
|
|
513
|
+
return () => {
|
|
514
|
+
if (maintenanceTimer) clearInterval(maintenanceTimer);
|
|
515
|
+
maintenanceTimer = null;
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
export const clearIdentityKeyCachesForTest = (): void => {
|
|
520
|
+
cache.clear();
|
|
521
|
+
signerRefreshes.invalidateAll();
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
export const invalidateIdentitySignerCache = (purpose: SigningKeyPurpose, kid?: string): void => {
|
|
525
|
+
const cached = cache.get(purpose);
|
|
526
|
+
if (!kid || cached?.signer.kid === kid) {
|
|
527
|
+
cache.delete(purpose);
|
|
528
|
+
signerRefreshes.invalidate(purpose);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type IdentityMetricName =
|
|
2
|
+
| "sign_success"
|
|
3
|
+
| "sign_failure"
|
|
4
|
+
| "verify_success"
|
|
5
|
+
| "verify_failure"
|
|
6
|
+
| "unknown_kid_refresh"
|
|
7
|
+
| "rotation_success"
|
|
8
|
+
| "rotation_failure";
|
|
9
|
+
|
|
10
|
+
const names: IdentityMetricName[] = [
|
|
11
|
+
"sign_success",
|
|
12
|
+
"sign_failure",
|
|
13
|
+
"verify_success",
|
|
14
|
+
"verify_failure",
|
|
15
|
+
"unknown_kid_refresh",
|
|
16
|
+
"rotation_success",
|
|
17
|
+
"rotation_failure",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const counters = new Map<IdentityMetricName, number>(names.map((name) => [name, 0]));
|
|
21
|
+
|
|
22
|
+
export const identityMetrics = {
|
|
23
|
+
increment(name: IdentityMetricName): void {
|
|
24
|
+
counters.set(name, (counters.get(name) ?? 0) + 1);
|
|
25
|
+
},
|
|
26
|
+
snapshot(): Readonly<Record<IdentityMetricName, number>> {
|
|
27
|
+
return Object.freeze(Object.fromEntries(names.map((name) => [name, counters.get(name) ?? 0])) as Record<IdentityMetricName, number>);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Context, Hono } from "hono";
|
|
2
|
+
import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_SESSION_JWKS_PATH, IDENTITY_JWKS_MAX_AGE_SECONDS } from "./constants";
|
|
3
|
+
import { listIdentityJwks } from "./key-ring";
|
|
4
|
+
|
|
5
|
+
const serveJwks = (purpose: "session" | "invocation") => async (c: Context) => {
|
|
6
|
+
const jwks = await listIdentityJwks(purpose);
|
|
7
|
+
if (c.req.header("if-none-match") === jwks.etag) {
|
|
8
|
+
c.header("ETag", jwks.etag);
|
|
9
|
+
c.header("Cache-Control", `public, max-age=${IDENTITY_JWKS_MAX_AGE_SECONDS}, must-revalidate`);
|
|
10
|
+
return c.body(null, 304);
|
|
11
|
+
}
|
|
12
|
+
c.header("ETag", jwks.etag);
|
|
13
|
+
c.header("Cache-Control", `public, max-age=${IDENTITY_JWKS_MAX_AGE_SECONDS}, must-revalidate`);
|
|
14
|
+
return c.json({ keys: jwks.keys });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const createIdentityPublicRoutes = (): Hono =>
|
|
18
|
+
new Hono().get(CLOUD_SESSION_JWKS_PATH, serveJwks("session")).get(CLOUD_INVOCATION_JWKS_PATH, serveJwks("invocation"));
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { publicCloudOrigin } from "../../shared/app-url";
|
|
2
|
+
import { get as getSetting } from "../settings";
|
|
3
|
+
import { CLOUD_INVOCATION_JWKS_PATH, CLOUD_OAUTH_JWKS_PATH, CLOUD_SESSION_JWKS_PATH } from "./constants";
|
|
4
|
+
|
|
5
|
+
const RUNTIME_CONFIG_TTL_MS = 60_000;
|
|
6
|
+
|
|
7
|
+
export type IdentityRuntimeConfig = {
|
|
8
|
+
issuer: string;
|
|
9
|
+
sessionJwksUrl: URL;
|
|
10
|
+
invocationJwksUrl: URL;
|
|
11
|
+
oauthJwksUrl: URL;
|
|
12
|
+
groupsAdmin: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let cached: { value: IdentityRuntimeConfig; expiresAt: number } | null = null;
|
|
16
|
+
let loading: Promise<IdentityRuntimeConfig> | null = null;
|
|
17
|
+
|
|
18
|
+
const normalizeGroups = (value: unknown): string[] => {
|
|
19
|
+
if (!Array.isArray(value)) return ["admins"];
|
|
20
|
+
const groups = [
|
|
21
|
+
...new Set(
|
|
22
|
+
value
|
|
23
|
+
.filter((item): item is string => typeof item === "string")
|
|
24
|
+
.map((item) => item.trim())
|
|
25
|
+
.filter(Boolean),
|
|
26
|
+
),
|
|
27
|
+
];
|
|
28
|
+
return groups.length > 0 ? groups : ["admins"];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const resolveCoreJwksUrl = (issuer: string, path: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL => {
|
|
32
|
+
if (!transportOrigin) return new URL(path, issuer);
|
|
33
|
+
const origin = new URL(transportOrigin);
|
|
34
|
+
if (origin.protocol !== "http:" && origin.protocol !== "https:") {
|
|
35
|
+
throw new Error("CLOUD_IDENTITY_JWKS_ORIGIN must use http or https");
|
|
36
|
+
}
|
|
37
|
+
return new URL(path, origin);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const resolveSessionJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
|
|
41
|
+
resolveCoreJwksUrl(issuer, CLOUD_SESSION_JWKS_PATH, transportOrigin);
|
|
42
|
+
|
|
43
|
+
export const resolveInvocationJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_IDENTITY_JWKS_ORIGIN?.trim()): URL =>
|
|
44
|
+
resolveCoreJwksUrl(issuer, CLOUD_INVOCATION_JWKS_PATH, transportOrigin);
|
|
45
|
+
|
|
46
|
+
export const resolveOAuthJwksUrl = (issuer: string, transportOrigin = process.env.CLOUD_OAUTH_JWKS_ORIGIN?.trim()): URL => {
|
|
47
|
+
if (!transportOrigin) return new URL(CLOUD_OAUTH_JWKS_PATH, issuer);
|
|
48
|
+
|
|
49
|
+
const url = new URL(CLOUD_OAUTH_JWKS_PATH, transportOrigin);
|
|
50
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
51
|
+
throw new Error("CLOUD_OAUTH_JWKS_ORIGIN must use http or https");
|
|
52
|
+
}
|
|
53
|
+
return url;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const load = async (): Promise<IdentityRuntimeConfig> => {
|
|
57
|
+
const [appUrl, rawGroupsAdmin] = await Promise.all([getSetting<string>("app.url"), getSetting<unknown>("freeipa.groups.admin")]);
|
|
58
|
+
const issuer = publicCloudOrigin(appUrl);
|
|
59
|
+
if (process.env.NODE_ENV === "production" && new URL(issuer).protocol !== "https:") {
|
|
60
|
+
throw new Error("Cloud identity issuer must use HTTPS in production");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
issuer,
|
|
64
|
+
sessionJwksUrl: resolveSessionJwksUrl(issuer),
|
|
65
|
+
invocationJwksUrl: resolveInvocationJwksUrl(issuer),
|
|
66
|
+
oauthJwksUrl: resolveOAuthJwksUrl(issuer),
|
|
67
|
+
groupsAdmin: normalizeGroups(rawGroupsAdmin),
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const getIdentityRuntimeConfig = async (): Promise<IdentityRuntimeConfig> => {
|
|
72
|
+
const now = Date.now();
|
|
73
|
+
if (cached && cached.expiresAt > now) return cached.value;
|
|
74
|
+
if (loading) return loading;
|
|
75
|
+
loading = load()
|
|
76
|
+
.then((value) => {
|
|
77
|
+
cached = { value, expiresAt: Date.now() + RUNTIME_CONFIG_TTL_MS };
|
|
78
|
+
return value;
|
|
79
|
+
})
|
|
80
|
+
.finally(() => {
|
|
81
|
+
loading = null;
|
|
82
|
+
});
|
|
83
|
+
return loading;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const invalidateIdentityRuntimeConfig = (): void => {
|
|
87
|
+
cached = null;
|
|
88
|
+
};
|