@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,891 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { crypto, err, fail, ok, type PageParams, type Paginated, type Result } from "@k2b/stdlib";
|
|
3
|
+
import { type SQL, sql } from "bun";
|
|
4
|
+
import type { User } from "../contracts/shared";
|
|
5
|
+
import { accounts } from "./accounts";
|
|
6
|
+
import { audit } from "./audit";
|
|
7
|
+
import { isUniqueViolation, toPgTextArray } from "./postgres";
|
|
8
|
+
import { type ServiceAccount, serviceAccounts } from "./service-accounts";
|
|
9
|
+
|
|
10
|
+
export type ServiceAccountCredentialStatus = "active" | "revoked";
|
|
11
|
+
export type ServiceAccountCredentialKind = "api_token";
|
|
12
|
+
|
|
13
|
+
export type ServiceAccountCredential = {
|
|
14
|
+
id: string;
|
|
15
|
+
serviceAccountId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
kind: ServiceAccountCredentialKind;
|
|
18
|
+
status: ServiceAccountCredentialStatus;
|
|
19
|
+
tokenPrefix: string;
|
|
20
|
+
scopes: string[];
|
|
21
|
+
expiresAt: string | null;
|
|
22
|
+
lastUsedAt: string | null;
|
|
23
|
+
createdBy: string | null;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
revokedAt: string | null;
|
|
26
|
+
revokedBy: string | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AuthenticatedServiceAccountCredential = {
|
|
30
|
+
credential: ServiceAccountCredential;
|
|
31
|
+
serviceAccount: ServiceAccount;
|
|
32
|
+
delegatedUser: User | null;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ServiceAccountCredentialOwner =
|
|
36
|
+
| {
|
|
37
|
+
type: "user";
|
|
38
|
+
userId: string;
|
|
39
|
+
uid: string;
|
|
40
|
+
displayName: string;
|
|
41
|
+
mail: string | null;
|
|
42
|
+
avatarHash: string | null;
|
|
43
|
+
}
|
|
44
|
+
| {
|
|
45
|
+
type: "resource";
|
|
46
|
+
appId: string;
|
|
47
|
+
resourceType: string;
|
|
48
|
+
resourceId: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type ServiceAccountCredentialOverview = ServiceAccountCredential & {
|
|
52
|
+
serviceAccount: ServiceAccount;
|
|
53
|
+
owner: ServiceAccountCredentialOwner;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type DbCredentialRow = {
|
|
57
|
+
id: string;
|
|
58
|
+
service_account_id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
kind: ServiceAccountCredentialKind;
|
|
61
|
+
status: ServiceAccountCredentialStatus;
|
|
62
|
+
token_prefix: string;
|
|
63
|
+
scopes: string[];
|
|
64
|
+
expires_at: Date | null;
|
|
65
|
+
last_used_at: Date | null;
|
|
66
|
+
created_by: string | null;
|
|
67
|
+
created_at: Date;
|
|
68
|
+
revoked_at: Date | null;
|
|
69
|
+
revoked_by: string | null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type DbCredentialWithSecretRow = DbCredentialRow & {
|
|
73
|
+
activity_due: boolean;
|
|
74
|
+
secret_hash: string;
|
|
75
|
+
} & DbCredentialServiceAccountFields;
|
|
76
|
+
|
|
77
|
+
type DbCredentialServiceAccountFields = {
|
|
78
|
+
service_account_id: string;
|
|
79
|
+
service_account_name: string;
|
|
80
|
+
service_account_kind: ServiceAccount["kind"];
|
|
81
|
+
service_account_status: ServiceAccount["status"];
|
|
82
|
+
delegated_user_id: string | null;
|
|
83
|
+
app_id: string | null;
|
|
84
|
+
resource_type: string | null;
|
|
85
|
+
resource_id: string | null;
|
|
86
|
+
service_account_created_by: string | null;
|
|
87
|
+
service_account_created_at: Date;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type SqlRunner = SQL;
|
|
91
|
+
type DbCredentialOverviewRow = DbCredentialRow &
|
|
92
|
+
DbCredentialServiceAccountFields & {
|
|
93
|
+
delegated_uid: string | null;
|
|
94
|
+
delegated_display_name: string | null;
|
|
95
|
+
delegated_mail: string | null;
|
|
96
|
+
delegated_avatar_hash: string | null;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const TOKEN_PREFIX = "cld";
|
|
100
|
+
const TOKEN_PATTERN = /^cld_([0-9a-f]{24})_([0-9a-f]{64})$/i;
|
|
101
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
102
|
+
const SECRET_HASH_PATTERN = /^sha256:([0-9a-f]{64})$/;
|
|
103
|
+
const SUCCESS_ACTIVITY_INTERVAL_MS = 60_000;
|
|
104
|
+
|
|
105
|
+
const isForeignKeyViolation = (error: unknown): boolean => {
|
|
106
|
+
if (!error || typeof error !== "object") return false;
|
|
107
|
+
const e = error as { code?: string; errno?: string };
|
|
108
|
+
return e.code === "23503" || e.errno === "23503";
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const USER_DELEGATED_UNIQUE_CONSTRAINT = "uniq_service_accounts_user_delegated";
|
|
112
|
+
|
|
113
|
+
const mapCredential = (row: DbCredentialRow): ServiceAccountCredential => ({
|
|
114
|
+
id: row.id,
|
|
115
|
+
serviceAccountId: row.service_account_id,
|
|
116
|
+
name: row.name,
|
|
117
|
+
kind: row.kind,
|
|
118
|
+
status: row.status,
|
|
119
|
+
tokenPrefix: row.token_prefix,
|
|
120
|
+
scopes: row.scopes ?? [],
|
|
121
|
+
expiresAt: row.expires_at?.toISOString() ?? null,
|
|
122
|
+
lastUsedAt: row.last_used_at?.toISOString() ?? null,
|
|
123
|
+
createdBy: row.created_by,
|
|
124
|
+
createdAt: row.created_at.toISOString(),
|
|
125
|
+
revokedAt: row.revoked_at?.toISOString() ?? null,
|
|
126
|
+
revokedBy: row.revoked_by,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const mapServiceAccount = (row: DbCredentialServiceAccountFields): ServiceAccount => ({
|
|
130
|
+
id: row.service_account_id,
|
|
131
|
+
name: row.service_account_name,
|
|
132
|
+
kind: row.service_account_kind,
|
|
133
|
+
status: row.service_account_status,
|
|
134
|
+
delegatedUserId: row.delegated_user_id,
|
|
135
|
+
appId: row.app_id,
|
|
136
|
+
resourceType: row.resource_type,
|
|
137
|
+
resourceId: row.resource_id,
|
|
138
|
+
createdBy: row.service_account_created_by,
|
|
139
|
+
createdAt: row.service_account_created_at.toISOString(),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const mapCredentialOverview = (row: DbCredentialOverviewRow): ServiceAccountCredentialOverview => {
|
|
143
|
+
const serviceAccount = mapServiceAccount(row);
|
|
144
|
+
return {
|
|
145
|
+
...mapCredential(row),
|
|
146
|
+
serviceAccount,
|
|
147
|
+
owner:
|
|
148
|
+
serviceAccount.kind === "user_delegated" && serviceAccount.delegatedUserId
|
|
149
|
+
? {
|
|
150
|
+
type: "user",
|
|
151
|
+
userId: serviceAccount.delegatedUserId,
|
|
152
|
+
uid: row.delegated_uid ?? serviceAccount.delegatedUserId,
|
|
153
|
+
displayName: row.delegated_display_name ?? "",
|
|
154
|
+
mail: row.delegated_mail,
|
|
155
|
+
avatarHash: row.delegated_avatar_hash,
|
|
156
|
+
}
|
|
157
|
+
: {
|
|
158
|
+
type: "resource",
|
|
159
|
+
appId: serviceAccount.appId ?? "",
|
|
160
|
+
resourceType: serviceAccount.resourceType ?? "",
|
|
161
|
+
resourceId: serviceAccount.resourceId ?? "",
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const actorForUser = (user: Pick<User, "id" | "uid" | "provider" | "roles">) => ({
|
|
167
|
+
userId: user.id,
|
|
168
|
+
uid: user.uid,
|
|
169
|
+
provider: user.provider,
|
|
170
|
+
roles: user.roles,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const normalizeName = (value: string): string => value.trim();
|
|
174
|
+
|
|
175
|
+
const parseToken = (token: string): { tokenPrefix: string; secret: string } | null => {
|
|
176
|
+
const match = token.match(TOKEN_PATTERN);
|
|
177
|
+
if (!match) return null;
|
|
178
|
+
return { tokenPrefix: match[1]!.toLowerCase(), secret: match[2]!.toLowerCase() };
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const generateTokenParts = (): { tokenPrefix: string; secret: string; token: string } => {
|
|
182
|
+
const tokenPrefix = crypto.common.generateKey(12);
|
|
183
|
+
const secret = crypto.common.generateKey(32);
|
|
184
|
+
return { tokenPrefix, secret, token: `${TOKEN_PREFIX}_${tokenPrefix}_${secret}` };
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// API token secrets carry 256 bits of entropy, so a fast digest is sufficient and avoids password-KDF cost per request.
|
|
188
|
+
const hashApiTokenSecret = (secret: string): string => `sha256:${createHash("sha256").update(secret).digest("hex")}`;
|
|
189
|
+
|
|
190
|
+
const verifyApiTokenSecret = async (secret: string, storedHash: string): Promise<boolean> => {
|
|
191
|
+
const match = storedHash.match(SECRET_HASH_PATTERN);
|
|
192
|
+
if (match) {
|
|
193
|
+
const actual = Buffer.from(hashApiTokenSecret(secret).slice("sha256:".length), "hex");
|
|
194
|
+
const expected = Buffer.from(match[1]!, "hex");
|
|
195
|
+
return timingSafeEqual(actual, expected);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
return await Bun.password.verify(secret, storedHash);
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const generateUniqueTokenParts = async (): Promise<{ tokenPrefix: string; secret: string; token: string }> => {
|
|
206
|
+
for (let i = 0; i < 5; i += 1) {
|
|
207
|
+
const parts = generateTokenParts();
|
|
208
|
+
const [row] = await sql<{ exists: boolean }[]>`
|
|
209
|
+
SELECT EXISTS(
|
|
210
|
+
SELECT 1 FROM auth.service_account_credentials WHERE token_prefix = ${parts.tokenPrefix}
|
|
211
|
+
) AS exists
|
|
212
|
+
`;
|
|
213
|
+
if (!row?.exists) return parts;
|
|
214
|
+
}
|
|
215
|
+
throw new Error("Failed to generate unique API token prefix");
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const isApiToken = (token: string | null | undefined): boolean => Boolean(token && TOKEN_PATTERN.test(token));
|
|
219
|
+
|
|
220
|
+
export const getOrCreateUserDelegatedServiceAccount = async (params: {
|
|
221
|
+
userId: string;
|
|
222
|
+
createdBy?: string | null;
|
|
223
|
+
}): Promise<Result<ServiceAccount>> => {
|
|
224
|
+
const [existing] = await sql<
|
|
225
|
+
{
|
|
226
|
+
id: string;
|
|
227
|
+
name: string;
|
|
228
|
+
kind: ServiceAccount["kind"];
|
|
229
|
+
status: ServiceAccount["status"];
|
|
230
|
+
delegated_user_id: string | null;
|
|
231
|
+
app_id: string | null;
|
|
232
|
+
resource_type: string | null;
|
|
233
|
+
resource_id: string | null;
|
|
234
|
+
created_by: string | null;
|
|
235
|
+
created_at: Date;
|
|
236
|
+
}[]
|
|
237
|
+
>`
|
|
238
|
+
SELECT id, name, kind, status, delegated_user_id, app_id, resource_type, resource_id, created_by, created_at
|
|
239
|
+
FROM auth.service_accounts
|
|
240
|
+
WHERE kind = 'user_delegated'
|
|
241
|
+
AND delegated_user_id = ${params.userId}::uuid
|
|
242
|
+
ORDER BY created_at ASC
|
|
243
|
+
LIMIT 1
|
|
244
|
+
`;
|
|
245
|
+
if (existing) {
|
|
246
|
+
return ok({
|
|
247
|
+
id: existing.id,
|
|
248
|
+
name: existing.name,
|
|
249
|
+
kind: existing.kind,
|
|
250
|
+
status: existing.status,
|
|
251
|
+
delegatedUserId: existing.delegated_user_id,
|
|
252
|
+
appId: existing.app_id,
|
|
253
|
+
resourceType: existing.resource_type,
|
|
254
|
+
resourceId: existing.resource_id,
|
|
255
|
+
createdBy: existing.created_by,
|
|
256
|
+
createdAt: existing.created_at.toISOString(),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
return await serviceAccounts.createUserDelegated({
|
|
262
|
+
name: "Personal API keys",
|
|
263
|
+
delegatedUserId: params.userId,
|
|
264
|
+
createdBy: params.createdBy ?? params.userId,
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (!isUniqueViolation(error, USER_DELEGATED_UNIQUE_CONSTRAINT)) throw error;
|
|
268
|
+
const [row] = await sql<
|
|
269
|
+
{
|
|
270
|
+
id: string;
|
|
271
|
+
name: string;
|
|
272
|
+
kind: ServiceAccount["kind"];
|
|
273
|
+
status: ServiceAccount["status"];
|
|
274
|
+
delegated_user_id: string | null;
|
|
275
|
+
app_id: string | null;
|
|
276
|
+
resource_type: string | null;
|
|
277
|
+
resource_id: string | null;
|
|
278
|
+
created_by: string | null;
|
|
279
|
+
created_at: Date;
|
|
280
|
+
}[]
|
|
281
|
+
>`
|
|
282
|
+
SELECT id, name, kind, status, delegated_user_id, app_id, resource_type, resource_id, created_by, created_at
|
|
283
|
+
FROM auth.service_accounts
|
|
284
|
+
WHERE kind = 'user_delegated'
|
|
285
|
+
AND delegated_user_id = ${params.userId}::uuid
|
|
286
|
+
LIMIT 1
|
|
287
|
+
`;
|
|
288
|
+
if (!row) return fail(err.internal("Failed to load user service account"));
|
|
289
|
+
return ok({
|
|
290
|
+
id: row.id,
|
|
291
|
+
name: row.name,
|
|
292
|
+
kind: row.kind,
|
|
293
|
+
status: row.status,
|
|
294
|
+
delegatedUserId: row.delegated_user_id,
|
|
295
|
+
appId: row.app_id,
|
|
296
|
+
resourceType: row.resource_type,
|
|
297
|
+
resourceId: row.resource_id,
|
|
298
|
+
createdBy: row.created_by,
|
|
299
|
+
createdAt: row.created_at.toISOString(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const insertApiToken = async (
|
|
305
|
+
db: SqlRunner,
|
|
306
|
+
params: {
|
|
307
|
+
serviceAccountId: string;
|
|
308
|
+
name: string;
|
|
309
|
+
createdBy?: string | null;
|
|
310
|
+
expiresAt?: string | null;
|
|
311
|
+
scopes?: string[];
|
|
312
|
+
},
|
|
313
|
+
): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => {
|
|
314
|
+
if (!UUID_PATTERN.test(params.serviceAccountId)) return fail(err.notFound("Service account"));
|
|
315
|
+
const name = normalizeName(params.name);
|
|
316
|
+
if (!name) return fail(err.badInput("API key name is required"));
|
|
317
|
+
if (name.length > 120) return fail(err.badInput("API key name must be 120 characters or fewer"));
|
|
318
|
+
|
|
319
|
+
let expiresAt: Date | null = null;
|
|
320
|
+
if (params.expiresAt) {
|
|
321
|
+
expiresAt = new Date(params.expiresAt);
|
|
322
|
+
if (Number.isNaN(expiresAt.getTime())) return fail(err.badInput("Invalid expiry date"));
|
|
323
|
+
if (expiresAt.getTime() <= Date.now()) return fail(err.badInput("Expiry must be in the future"));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const parts = await generateUniqueTokenParts();
|
|
327
|
+
const secretHash = hashApiTokenSecret(parts.secret);
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const [row] = await db<DbCredentialRow[]>`
|
|
331
|
+
INSERT INTO auth.service_account_credentials (
|
|
332
|
+
service_account_id,
|
|
333
|
+
name,
|
|
334
|
+
token_prefix,
|
|
335
|
+
secret_hash,
|
|
336
|
+
scopes,
|
|
337
|
+
expires_at,
|
|
338
|
+
created_by
|
|
339
|
+
)
|
|
340
|
+
VALUES (
|
|
341
|
+
${params.serviceAccountId}::uuid,
|
|
342
|
+
${name},
|
|
343
|
+
${parts.tokenPrefix},
|
|
344
|
+
${secretHash},
|
|
345
|
+
${toPgTextArray(params.scopes ?? [])}::text[],
|
|
346
|
+
${expiresAt},
|
|
347
|
+
${params.createdBy ?? null}::uuid
|
|
348
|
+
)
|
|
349
|
+
RETURNING id, service_account_id, name, kind, status, token_prefix, scopes, expires_at, last_used_at, created_by, created_at, revoked_at, revoked_by
|
|
350
|
+
`;
|
|
351
|
+
if (!row) return fail(err.internal("Failed to create API key"));
|
|
352
|
+
return ok({ credential: mapCredential(row), token: parts.token });
|
|
353
|
+
} catch (error) {
|
|
354
|
+
if (isForeignKeyViolation(error)) return fail(err.notFound("Service account"));
|
|
355
|
+
if (isUniqueViolation(error)) return fail(err.conflict("API key"));
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
export const createApiToken = (params: {
|
|
361
|
+
serviceAccountId: string;
|
|
362
|
+
name: string;
|
|
363
|
+
createdBy?: string | null;
|
|
364
|
+
expiresAt?: string | null;
|
|
365
|
+
scopes?: string[];
|
|
366
|
+
}): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => insertApiToken(sql, params);
|
|
367
|
+
|
|
368
|
+
export const createUserApiToken = async (params: {
|
|
369
|
+
user: User;
|
|
370
|
+
name: string;
|
|
371
|
+
expiresAt?: string | null;
|
|
372
|
+
}): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => {
|
|
373
|
+
const serviceAccountResult = await getOrCreateUserDelegatedServiceAccount({
|
|
374
|
+
userId: params.user.id,
|
|
375
|
+
createdBy: params.user.id,
|
|
376
|
+
});
|
|
377
|
+
if (!serviceAccountResult.ok) return fail(serviceAccountResult.error);
|
|
378
|
+
|
|
379
|
+
return sql.begin(async (tx) => {
|
|
380
|
+
const result = await insertApiToken(tx, {
|
|
381
|
+
serviceAccountId: serviceAccountResult.data.id,
|
|
382
|
+
name: params.name,
|
|
383
|
+
expiresAt: params.expiresAt,
|
|
384
|
+
createdBy: params.user.id,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return audit.recordResult({
|
|
388
|
+
action: "service_account_credential.create",
|
|
389
|
+
actor: actorForUser(params.user),
|
|
390
|
+
target: { type: "service_account_credential", id: result.ok ? result.data.credential.id : null, label: params.name },
|
|
391
|
+
metadata: {
|
|
392
|
+
serviceAccountId: serviceAccountResult.data.id,
|
|
393
|
+
kind: "api_token",
|
|
394
|
+
expiresAt: params.expiresAt ?? null,
|
|
395
|
+
},
|
|
396
|
+
result,
|
|
397
|
+
db: tx,
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export const createResourceApiToken = async (params: {
|
|
403
|
+
serviceAccountId: string;
|
|
404
|
+
actor: User;
|
|
405
|
+
name: string;
|
|
406
|
+
expiresAt?: string | null;
|
|
407
|
+
scopes?: string[];
|
|
408
|
+
}): Promise<Result<{ credential: ServiceAccountCredential; token: string }>> => {
|
|
409
|
+
const serviceAccount = await serviceAccounts.get({ id: params.serviceAccountId });
|
|
410
|
+
if (!serviceAccount || serviceAccount.kind !== "resource_bound") return fail(err.notFound("Resource service account"));
|
|
411
|
+
if (serviceAccount.status !== "active") return fail(err.badInput("Resource service account is disabled"));
|
|
412
|
+
|
|
413
|
+
return sql.begin(async (tx) => {
|
|
414
|
+
const result = await insertApiToken(tx, {
|
|
415
|
+
serviceAccountId: serviceAccount.id,
|
|
416
|
+
name: params.name,
|
|
417
|
+
expiresAt: params.expiresAt,
|
|
418
|
+
createdBy: params.actor.id,
|
|
419
|
+
scopes: params.scopes,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
return audit.recordResult({
|
|
423
|
+
action: "service_account_credential.create",
|
|
424
|
+
actor: actorForUser(params.actor),
|
|
425
|
+
target: { type: "service_account_credential", id: result.ok ? result.data.credential.id : null, label: params.name },
|
|
426
|
+
metadata: {
|
|
427
|
+
serviceAccountId: serviceAccount.id,
|
|
428
|
+
kind: "api_token",
|
|
429
|
+
serviceAccountKind: serviceAccount.kind,
|
|
430
|
+
appId: serviceAccount.appId,
|
|
431
|
+
resourceType: serviceAccount.resourceType,
|
|
432
|
+
resourceId: serviceAccount.resourceId,
|
|
433
|
+
expiresAt: params.expiresAt ?? null,
|
|
434
|
+
},
|
|
435
|
+
result,
|
|
436
|
+
db: tx,
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
export const listForDelegatedUser = async (params: { userId: string }): Promise<ServiceAccountCredential[]> => {
|
|
442
|
+
const rows = await sql<DbCredentialRow[]>`
|
|
443
|
+
SELECT c.id, c.service_account_id, c.name, c.kind, c.status, c.token_prefix, c.scopes, c.expires_at,
|
|
444
|
+
c.last_used_at, c.created_by, c.created_at, c.revoked_at, c.revoked_by
|
|
445
|
+
FROM auth.service_account_credentials c
|
|
446
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
447
|
+
WHERE sa.kind = 'user_delegated'
|
|
448
|
+
AND sa.delegated_user_id = ${params.userId}::uuid
|
|
449
|
+
AND c.status = 'active'
|
|
450
|
+
ORDER BY c.created_at DESC
|
|
451
|
+
`;
|
|
452
|
+
return rows.map(mapCredential);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
export const listOverview = async (config?: {
|
|
456
|
+
pagination?: PageParams;
|
|
457
|
+
filter?: {
|
|
458
|
+
search?: string;
|
|
459
|
+
serviceAccountKind?: ServiceAccount["kind"];
|
|
460
|
+
credentialStatus?: ServiceAccountCredentialStatus;
|
|
461
|
+
userId?: string;
|
|
462
|
+
appId?: string;
|
|
463
|
+
resourceType?: string;
|
|
464
|
+
resourceId?: string;
|
|
465
|
+
serviceAccountId?: string;
|
|
466
|
+
};
|
|
467
|
+
}): Promise<Paginated<ServiceAccountCredentialOverview>> => {
|
|
468
|
+
const page = Math.max(1, config?.pagination?.page ?? 1);
|
|
469
|
+
const perPage = Math.max(1, Math.min(config?.pagination?.perPage ?? 100, 500));
|
|
470
|
+
const offset = (page - 1) * perPage;
|
|
471
|
+
const search = config?.filter?.search?.trim() || null;
|
|
472
|
+
const serviceAccountKind = config?.filter?.serviceAccountKind ?? null;
|
|
473
|
+
const credentialStatus = config?.filter?.credentialStatus ?? null;
|
|
474
|
+
const userId = config?.filter?.userId ?? null;
|
|
475
|
+
const appId = config?.filter?.appId ?? null;
|
|
476
|
+
const resourceType = config?.filter?.resourceType ?? null;
|
|
477
|
+
const resourceId = config?.filter?.resourceId ?? null;
|
|
478
|
+
const serviceAccountId = config?.filter?.serviceAccountId ?? null;
|
|
479
|
+
|
|
480
|
+
const [countRow] = await sql<{ count: string }[]>`
|
|
481
|
+
SELECT COUNT(*)::text AS count
|
|
482
|
+
FROM auth.service_account_credentials c
|
|
483
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
484
|
+
LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
|
|
485
|
+
WHERE (${serviceAccountKind}::text IS NULL OR sa.kind = ${serviceAccountKind})
|
|
486
|
+
AND (${credentialStatus}::text IS NULL OR c.status = ${credentialStatus})
|
|
487
|
+
AND (${userId}::uuid IS NULL OR sa.delegated_user_id = ${userId}::uuid)
|
|
488
|
+
AND (${serviceAccountId}::uuid IS NULL OR sa.id = ${serviceAccountId}::uuid)
|
|
489
|
+
AND (${appId}::text IS NULL OR sa.app_id = ${appId})
|
|
490
|
+
AND (${resourceType}::text IS NULL OR sa.resource_type = ${resourceType})
|
|
491
|
+
AND (${resourceId}::text IS NULL OR sa.resource_id = ${resourceId})
|
|
492
|
+
AND (
|
|
493
|
+
${search}::text IS NULL
|
|
494
|
+
OR c.name ILIKE '%' || ${search} || '%'
|
|
495
|
+
OR c.token_prefix ILIKE '%' || ${search} || '%'
|
|
496
|
+
OR sa.name ILIKE '%' || ${search} || '%'
|
|
497
|
+
OR du.uid ILIKE '%' || ${search} || '%'
|
|
498
|
+
OR du.display_name ILIKE '%' || ${search} || '%'
|
|
499
|
+
OR du.mail ILIKE '%' || ${search} || '%'
|
|
500
|
+
OR sa.app_id ILIKE '%' || ${search} || '%'
|
|
501
|
+
OR sa.resource_type ILIKE '%' || ${search} || '%'
|
|
502
|
+
OR sa.resource_id ILIKE '%' || ${search} || '%'
|
|
503
|
+
)
|
|
504
|
+
`;
|
|
505
|
+
|
|
506
|
+
const rows = await sql<DbCredentialOverviewRow[]>`
|
|
507
|
+
SELECT
|
|
508
|
+
c.id,
|
|
509
|
+
c.service_account_id,
|
|
510
|
+
c.name,
|
|
511
|
+
c.kind,
|
|
512
|
+
c.status,
|
|
513
|
+
c.token_prefix,
|
|
514
|
+
c.scopes,
|
|
515
|
+
c.expires_at,
|
|
516
|
+
c.last_used_at,
|
|
517
|
+
c.created_by,
|
|
518
|
+
c.created_at,
|
|
519
|
+
c.revoked_at,
|
|
520
|
+
c.revoked_by,
|
|
521
|
+
sa.name AS service_account_name,
|
|
522
|
+
sa.kind AS service_account_kind,
|
|
523
|
+
sa.status AS service_account_status,
|
|
524
|
+
sa.delegated_user_id,
|
|
525
|
+
sa.app_id,
|
|
526
|
+
sa.resource_type,
|
|
527
|
+
sa.resource_id,
|
|
528
|
+
sa.created_by AS service_account_created_by,
|
|
529
|
+
sa.created_at AS service_account_created_at,
|
|
530
|
+
du.uid AS delegated_uid,
|
|
531
|
+
du.display_name AS delegated_display_name,
|
|
532
|
+
du.mail AS delegated_mail,
|
|
533
|
+
du.avatar_hash AS delegated_avatar_hash
|
|
534
|
+
FROM auth.service_account_credentials c
|
|
535
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
536
|
+
LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
|
|
537
|
+
WHERE (${serviceAccountKind}::text IS NULL OR sa.kind = ${serviceAccountKind})
|
|
538
|
+
AND (${credentialStatus}::text IS NULL OR c.status = ${credentialStatus})
|
|
539
|
+
AND (${userId}::uuid IS NULL OR sa.delegated_user_id = ${userId}::uuid)
|
|
540
|
+
AND (${serviceAccountId}::uuid IS NULL OR sa.id = ${serviceAccountId}::uuid)
|
|
541
|
+
AND (${appId}::text IS NULL OR sa.app_id = ${appId})
|
|
542
|
+
AND (${resourceType}::text IS NULL OR sa.resource_type = ${resourceType})
|
|
543
|
+
AND (${resourceId}::text IS NULL OR sa.resource_id = ${resourceId})
|
|
544
|
+
AND (
|
|
545
|
+
${search}::text IS NULL
|
|
546
|
+
OR c.name ILIKE '%' || ${search} || '%'
|
|
547
|
+
OR c.token_prefix ILIKE '%' || ${search} || '%'
|
|
548
|
+
OR sa.name ILIKE '%' || ${search} || '%'
|
|
549
|
+
OR du.uid ILIKE '%' || ${search} || '%'
|
|
550
|
+
OR du.display_name ILIKE '%' || ${search} || '%'
|
|
551
|
+
OR du.mail ILIKE '%' || ${search} || '%'
|
|
552
|
+
OR sa.app_id ILIKE '%' || ${search} || '%'
|
|
553
|
+
OR sa.resource_type ILIKE '%' || ${search} || '%'
|
|
554
|
+
OR sa.resource_id ILIKE '%' || ${search} || '%'
|
|
555
|
+
)
|
|
556
|
+
ORDER BY c.created_at DESC
|
|
557
|
+
LIMIT ${perPage}
|
|
558
|
+
OFFSET ${offset}
|
|
559
|
+
`;
|
|
560
|
+
|
|
561
|
+
const total = Number.parseInt(countRow?.count ?? "0", 10);
|
|
562
|
+
return {
|
|
563
|
+
items: rows.map(mapCredentialOverview),
|
|
564
|
+
page,
|
|
565
|
+
perPage,
|
|
566
|
+
total,
|
|
567
|
+
hasNext: page * perPage < total,
|
|
568
|
+
};
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
export const getOverview = async (params: { id: string }): Promise<ServiceAccountCredentialOverview | null> => {
|
|
572
|
+
if (!UUID_PATTERN.test(params.id)) return null;
|
|
573
|
+
const [row] = await sql<DbCredentialOverviewRow[]>`
|
|
574
|
+
SELECT
|
|
575
|
+
c.id,
|
|
576
|
+
c.service_account_id,
|
|
577
|
+
c.name,
|
|
578
|
+
c.kind,
|
|
579
|
+
c.status,
|
|
580
|
+
c.token_prefix,
|
|
581
|
+
c.scopes,
|
|
582
|
+
c.expires_at,
|
|
583
|
+
c.last_used_at,
|
|
584
|
+
c.created_by,
|
|
585
|
+
c.created_at,
|
|
586
|
+
c.revoked_at,
|
|
587
|
+
c.revoked_by,
|
|
588
|
+
sa.name AS service_account_name,
|
|
589
|
+
sa.kind AS service_account_kind,
|
|
590
|
+
sa.status AS service_account_status,
|
|
591
|
+
sa.delegated_user_id,
|
|
592
|
+
sa.app_id,
|
|
593
|
+
sa.resource_type,
|
|
594
|
+
sa.resource_id,
|
|
595
|
+
sa.created_by AS service_account_created_by,
|
|
596
|
+
sa.created_at AS service_account_created_at,
|
|
597
|
+
du.uid AS delegated_uid,
|
|
598
|
+
du.display_name AS delegated_display_name,
|
|
599
|
+
du.mail AS delegated_mail,
|
|
600
|
+
du.avatar_hash AS delegated_avatar_hash
|
|
601
|
+
FROM auth.service_account_credentials c
|
|
602
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
603
|
+
LEFT JOIN auth.users du ON du.id = sa.delegated_user_id
|
|
604
|
+
WHERE c.id = ${params.id}::uuid
|
|
605
|
+
LIMIT 1
|
|
606
|
+
`;
|
|
607
|
+
return row ? mapCredentialOverview(row) : null;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
export const revokeForDelegatedUser = async (params: { credentialId: string; user: User }): Promise<Result<void>> => {
|
|
611
|
+
if (!UUID_PATTERN.test(params.credentialId)) return fail(err.notFound("API key"));
|
|
612
|
+
|
|
613
|
+
return sql.begin(async (tx) => {
|
|
614
|
+
const [row] = await tx<DbCredentialRow[]>`
|
|
615
|
+
UPDATE auth.service_account_credentials c
|
|
616
|
+
SET status = 'revoked',
|
|
617
|
+
revoked_at = now(),
|
|
618
|
+
revoked_by = ${params.user.id}::uuid
|
|
619
|
+
FROM auth.service_accounts sa
|
|
620
|
+
WHERE c.id = ${params.credentialId}::uuid
|
|
621
|
+
AND c.service_account_id = sa.id
|
|
622
|
+
AND sa.kind = 'user_delegated'
|
|
623
|
+
AND sa.delegated_user_id = ${params.user.id}::uuid
|
|
624
|
+
AND c.status = 'active'
|
|
625
|
+
RETURNING c.id, c.service_account_id, c.name, c.kind, c.status, c.token_prefix, c.scopes, c.expires_at,
|
|
626
|
+
c.last_used_at, c.created_by, c.created_at, c.revoked_at, c.revoked_by
|
|
627
|
+
`;
|
|
628
|
+
|
|
629
|
+
const result = row ? ok() : fail(err.notFound("API key"));
|
|
630
|
+
return audit.recordResult({
|
|
631
|
+
action: "service_account_credential.revoke",
|
|
632
|
+
actor: actorForUser(params.user),
|
|
633
|
+
target: { type: "service_account_credential", id: params.credentialId, label: row?.name ?? null },
|
|
634
|
+
metadata: { serviceAccountId: row?.service_account_id ?? null },
|
|
635
|
+
result,
|
|
636
|
+
db: tx,
|
|
637
|
+
});
|
|
638
|
+
});
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
type RevokeCredentialInput =
|
|
642
|
+
| { credentialId: string; actor: User; system?: never }
|
|
643
|
+
| { credentialId: string; actor?: never; system: { service: string; reason: string } };
|
|
644
|
+
|
|
645
|
+
export const revoke = async (params: RevokeCredentialInput, options: { db?: SQL } = {}): Promise<Result<void>> => {
|
|
646
|
+
if (!UUID_PATTERN.test(params.credentialId)) return fail(err.notFound("API key"));
|
|
647
|
+
|
|
648
|
+
const run = async (db: SQL): Promise<Result<void>> => {
|
|
649
|
+
const [row] = await db<DbCredentialRow[]>`
|
|
650
|
+
UPDATE auth.service_account_credentials
|
|
651
|
+
SET status = 'revoked',
|
|
652
|
+
revoked_at = now(),
|
|
653
|
+
revoked_by = ${params.actor?.id ?? null}::uuid
|
|
654
|
+
WHERE id = ${params.credentialId}::uuid
|
|
655
|
+
AND status = 'active'
|
|
656
|
+
RETURNING id, service_account_id, name, kind, status, token_prefix, scopes, expires_at,
|
|
657
|
+
last_used_at, created_by, created_at, revoked_at, revoked_by
|
|
658
|
+
`;
|
|
659
|
+
|
|
660
|
+
const [existing] = row
|
|
661
|
+
? [row]
|
|
662
|
+
: await db<DbCredentialRow[]>`
|
|
663
|
+
SELECT id, service_account_id, name, kind, status, token_prefix, scopes, expires_at,
|
|
664
|
+
last_used_at, created_by, created_at, revoked_at, revoked_by
|
|
665
|
+
FROM auth.service_account_credentials
|
|
666
|
+
WHERE id = ${params.credentialId}::uuid
|
|
667
|
+
LIMIT 1
|
|
668
|
+
`;
|
|
669
|
+
const result = row || existing?.status === "revoked" ? ok() : fail(err.notFound("API key"));
|
|
670
|
+
return audit.recordResult({
|
|
671
|
+
action: "service_account_credential.revoke",
|
|
672
|
+
actor: params.actor ? actorForUser(params.actor) : null,
|
|
673
|
+
target: { type: "service_account_credential", id: params.credentialId, label: row?.name ?? existing?.name ?? null },
|
|
674
|
+
metadata: {
|
|
675
|
+
serviceAccountId: row?.service_account_id ?? existing?.service_account_id ?? null,
|
|
676
|
+
...(params.actor ? { adminAction: true } : { provenance: "system", service: params.system.service, reason: params.system.reason }),
|
|
677
|
+
},
|
|
678
|
+
result,
|
|
679
|
+
db,
|
|
680
|
+
});
|
|
681
|
+
};
|
|
682
|
+
return options.db ? run(options.db) : sql.begin(run);
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const findActiveByTokenPrefix = async (tokenPrefix: string): Promise<DbCredentialWithSecretRow | null> => {
|
|
686
|
+
const [row] = await sql<DbCredentialWithSecretRow[]>`
|
|
687
|
+
SELECT
|
|
688
|
+
c.id,
|
|
689
|
+
c.service_account_id,
|
|
690
|
+
c.name,
|
|
691
|
+
c.kind,
|
|
692
|
+
c.status,
|
|
693
|
+
c.token_prefix,
|
|
694
|
+
c.secret_hash,
|
|
695
|
+
c.scopes,
|
|
696
|
+
c.expires_at,
|
|
697
|
+
c.last_used_at,
|
|
698
|
+
(
|
|
699
|
+
c.last_used_at IS NULL
|
|
700
|
+
OR c.last_used_at < now() - (${SUCCESS_ACTIVITY_INTERVAL_MS} * interval '1 millisecond')
|
|
701
|
+
) AS activity_due,
|
|
702
|
+
c.created_by,
|
|
703
|
+
c.created_at,
|
|
704
|
+
c.revoked_at,
|
|
705
|
+
c.revoked_by,
|
|
706
|
+
sa.name AS service_account_name,
|
|
707
|
+
sa.kind AS service_account_kind,
|
|
708
|
+
sa.status AS service_account_status,
|
|
709
|
+
sa.delegated_user_id,
|
|
710
|
+
sa.app_id,
|
|
711
|
+
sa.resource_type,
|
|
712
|
+
sa.resource_id,
|
|
713
|
+
sa.created_by AS service_account_created_by,
|
|
714
|
+
sa.created_at AS service_account_created_at
|
|
715
|
+
FROM auth.service_account_credentials c
|
|
716
|
+
JOIN auth.service_accounts sa ON sa.id = c.service_account_id
|
|
717
|
+
WHERE c.token_prefix = ${tokenPrefix}
|
|
718
|
+
AND c.status = 'active'
|
|
719
|
+
AND sa.status = 'active'
|
|
720
|
+
AND (c.expires_at IS NULL OR c.expires_at > now())
|
|
721
|
+
LIMIT 1
|
|
722
|
+
`;
|
|
723
|
+
return row ?? null;
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
const recordSuccessfulAuthentication = async (params: {
|
|
727
|
+
row: DbCredentialWithSecretRow;
|
|
728
|
+
secret: string;
|
|
729
|
+
serviceAccount: ServiceAccount;
|
|
730
|
+
delegatedUser: User | null;
|
|
731
|
+
}): Promise<void> => {
|
|
732
|
+
const upgradedSecretHash = SECRET_HASH_PATTERN.test(params.row.secret_hash) ? null : hashApiTokenSecret(params.secret);
|
|
733
|
+
if (!upgradedSecretHash && !params.row.activity_due) return;
|
|
734
|
+
|
|
735
|
+
if (upgradedSecretHash) {
|
|
736
|
+
await sql`
|
|
737
|
+
UPDATE auth.service_account_credentials
|
|
738
|
+
SET secret_hash = ${upgradedSecretHash}
|
|
739
|
+
WHERE id = ${params.row.id}::uuid
|
|
740
|
+
AND status = 'active'
|
|
741
|
+
AND secret_hash = ${params.row.secret_hash}
|
|
742
|
+
`;
|
|
743
|
+
}
|
|
744
|
+
if (!params.row.activity_due) return;
|
|
745
|
+
|
|
746
|
+
await sql.begin(async (tx) => {
|
|
747
|
+
// Authentication telemetry is best-effort under contention. SKIP LOCKED
|
|
748
|
+
// keeps one replica from queuing live requests behind another replica.
|
|
749
|
+
const [activity] = await tx<{ last_used_at: Date }[]>`
|
|
750
|
+
WITH claimed AS (
|
|
751
|
+
SELECT id
|
|
752
|
+
FROM auth.service_account_credentials
|
|
753
|
+
WHERE id = ${params.row.id}::uuid
|
|
754
|
+
AND status = 'active'
|
|
755
|
+
AND (
|
|
756
|
+
last_used_at IS NULL
|
|
757
|
+
OR last_used_at < now() - (${SUCCESS_ACTIVITY_INTERVAL_MS} * interval '1 millisecond')
|
|
758
|
+
)
|
|
759
|
+
FOR UPDATE SKIP LOCKED
|
|
760
|
+
)
|
|
761
|
+
UPDATE auth.service_account_credentials
|
|
762
|
+
SET last_used_at = now()
|
|
763
|
+
FROM claimed
|
|
764
|
+
WHERE auth.service_account_credentials.id = claimed.id
|
|
765
|
+
RETURNING auth.service_account_credentials.last_used_at
|
|
766
|
+
`;
|
|
767
|
+
if (!activity) return;
|
|
768
|
+
|
|
769
|
+
await audit.record(
|
|
770
|
+
{
|
|
771
|
+
action: "service_account_credential.authenticate",
|
|
772
|
+
outcome: "allowed",
|
|
773
|
+
actor: params.delegatedUser ? actorForUser(params.delegatedUser) : null,
|
|
774
|
+
target: { type: "service_account_credential", id: params.row.id, label: params.row.name },
|
|
775
|
+
metadata: {
|
|
776
|
+
serviceAccountId: params.row.service_account_id,
|
|
777
|
+
serviceAccountKind: params.serviceAccount.kind,
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
tx,
|
|
781
|
+
);
|
|
782
|
+
});
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const pendingSecretVerifications = new Map<string, Promise<boolean>>();
|
|
786
|
+
|
|
787
|
+
const verifyApiTokenSecretOnce = async (secret: string, row: DbCredentialWithSecretRow): Promise<boolean> => {
|
|
788
|
+
const key = `${row.id}:${row.secret_hash}:${hashApiTokenSecret(secret)}`;
|
|
789
|
+
let pending = pendingSecretVerifications.get(key);
|
|
790
|
+
if (!pending) {
|
|
791
|
+
pending = verifyApiTokenSecret(secret, row.secret_hash);
|
|
792
|
+
pendingSecretVerifications.set(key, pending);
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
return await pending;
|
|
796
|
+
} finally {
|
|
797
|
+
if (pendingSecretVerifications.get(key) === pending) pendingSecretVerifications.delete(key);
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const recordDeniedAuthentication = async (params: {
|
|
802
|
+
reason: string;
|
|
803
|
+
tokenPrefix: string;
|
|
804
|
+
row?: DbCredentialWithSecretRow;
|
|
805
|
+
}): Promise<null> => {
|
|
806
|
+
await audit.record({
|
|
807
|
+
action: "service_account_credential.authenticate",
|
|
808
|
+
outcome: "denied",
|
|
809
|
+
...(params.row ? { target: { type: "service_account_credential" as const, id: params.row.id, label: params.row.name } } : {}),
|
|
810
|
+
reason: params.reason,
|
|
811
|
+
metadata: {
|
|
812
|
+
tokenPrefix: params.tokenPrefix,
|
|
813
|
+
...(params.row ? { serviceAccountId: params.row.service_account_id } : {}),
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
return null;
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
export const authenticateApiToken = async (token: string): Promise<AuthenticatedServiceAccountCredential | null> => {
|
|
820
|
+
const parsed = parseToken(token);
|
|
821
|
+
if (!parsed) return null;
|
|
822
|
+
|
|
823
|
+
let row = await findActiveByTokenPrefix(parsed.tokenPrefix);
|
|
824
|
+
if (!row) {
|
|
825
|
+
return recordDeniedAuthentication({
|
|
826
|
+
reason: "API key not found, inactive, or expired",
|
|
827
|
+
tokenPrefix: parsed.tokenPrefix,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (!(await verifyApiTokenSecretOnce(parsed.secret, row))) {
|
|
831
|
+
return recordDeniedAuthentication({
|
|
832
|
+
reason: "Invalid API key secret",
|
|
833
|
+
tokenPrefix: parsed.tokenPrefix,
|
|
834
|
+
row,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Verification may be expensive. Refresh authorization afterwards so a
|
|
839
|
+
// request cannot join work that began before a credential was revoked.
|
|
840
|
+
const current = await findActiveByTokenPrefix(parsed.tokenPrefix);
|
|
841
|
+
if (!current || current.id !== row.id) {
|
|
842
|
+
return recordDeniedAuthentication({
|
|
843
|
+
reason: "API key not found, inactive, or expired",
|
|
844
|
+
tokenPrefix: parsed.tokenPrefix,
|
|
845
|
+
row,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
if (current.secret_hash !== row.secret_hash && !(await verifyApiTokenSecretOnce(parsed.secret, current))) {
|
|
849
|
+
return recordDeniedAuthentication({
|
|
850
|
+
reason: "Invalid API key secret",
|
|
851
|
+
tokenPrefix: parsed.tokenPrefix,
|
|
852
|
+
row: current,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
row = current;
|
|
856
|
+
|
|
857
|
+
const serviceAccount = mapServiceAccount(row);
|
|
858
|
+
const delegatedUser = serviceAccount.delegatedUserId ? await accounts.users.get({ id: serviceAccount.delegatedUserId }) : null;
|
|
859
|
+
if (delegatedUser && !(await isAccountCategoryAllowed(delegatedUser))) return null;
|
|
860
|
+
if (serviceAccount.kind === "user_delegated" && !delegatedUser) {
|
|
861
|
+
return recordDeniedAuthentication({
|
|
862
|
+
reason: "Delegated user is missing",
|
|
863
|
+
tokenPrefix: parsed.tokenPrefix,
|
|
864
|
+
row,
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
await recordSuccessfulAuthentication({ row, secret: parsed.secret, serviceAccount, delegatedUser });
|
|
869
|
+
|
|
870
|
+
return {
|
|
871
|
+
credential: mapCredential({ ...row, last_used_at: new Date() }),
|
|
872
|
+
serviceAccount,
|
|
873
|
+
delegatedUser,
|
|
874
|
+
};
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
export const serviceAccountCredentials = {
|
|
878
|
+
isApiToken,
|
|
879
|
+
getOrCreateUserDelegatedServiceAccount,
|
|
880
|
+
createApiToken,
|
|
881
|
+
createUserApiToken,
|
|
882
|
+
createResourceApiToken,
|
|
883
|
+
listForDelegatedUser,
|
|
884
|
+
listOverview,
|
|
885
|
+
getOverview,
|
|
886
|
+
revokeForDelegatedUser,
|
|
887
|
+
revoke,
|
|
888
|
+
authenticateApiToken,
|
|
889
|
+
};
|
|
890
|
+
|
|
891
|
+
import { isAccountCategoryAllowed } from "./account-category-policy";
|