@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,902 @@
|
|
|
1
|
+
import { type SQL, type SQLQuery, sql } from "bun";
|
|
2
|
+
import type { CloudResourceRef } from "../contracts/capabilities";
|
|
3
|
+
import type { AccessSubject } from "../server";
|
|
4
|
+
import {
|
|
5
|
+
buildAccessPrincipalCondition,
|
|
6
|
+
createAccess,
|
|
7
|
+
deleteAccess,
|
|
8
|
+
hasPermission,
|
|
9
|
+
type PermissionLevel,
|
|
10
|
+
type Principal,
|
|
11
|
+
} from "../server/services/access";
|
|
12
|
+
import { toPgUuidArray } from "../services/postgres";
|
|
13
|
+
import { mountAiProjectFilePath } from "./file-mount";
|
|
14
|
+
import { withAiShortIdForDb } from "./short-id";
|
|
15
|
+
import type { AiProjectPromptSnapshot } from "./types";
|
|
16
|
+
|
|
17
|
+
export const AI_PROJECT_NAME_MAX_CHARS = 120;
|
|
18
|
+
export const AI_PROJECT_DESCRIPTION_MAX_CHARS = 500;
|
|
19
|
+
export const AI_PROJECT_INSTRUCTIONS_MAX_CHARS = 16_000;
|
|
20
|
+
export const AI_PROJECT_KNOWLEDGE_MAX_CHARS = 100_000;
|
|
21
|
+
export const AI_PROJECT_FILE_MAX_BYTES = 10 * 1024 * 1024;
|
|
22
|
+
const AI_PROJECT_PROMPT_MANIFEST_MAX_CHARS = 20_000;
|
|
23
|
+
|
|
24
|
+
export type AiProjectPermission = Exclude<PermissionLevel, "none">;
|
|
25
|
+
|
|
26
|
+
export class AiProjectLastAdminError extends Error {
|
|
27
|
+
constructor() {
|
|
28
|
+
super("A Project must keep at least one admin access entry.");
|
|
29
|
+
this.name = "AiProjectLastAdminError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type AiProject = {
|
|
34
|
+
id: string;
|
|
35
|
+
shortId: string;
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
icon: string;
|
|
39
|
+
instructions: string;
|
|
40
|
+
defaultModelProfileId: string | null;
|
|
41
|
+
permission: AiProjectPermission;
|
|
42
|
+
revision: number;
|
|
43
|
+
createdAt: string;
|
|
44
|
+
updatedAt: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type AiProjectKnowledge = {
|
|
48
|
+
id: string;
|
|
49
|
+
shortId: string;
|
|
50
|
+
projectId: string;
|
|
51
|
+
title: string;
|
|
52
|
+
content: string;
|
|
53
|
+
createdAt: string;
|
|
54
|
+
updatedAt: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type AiProjectFile = {
|
|
58
|
+
id: string;
|
|
59
|
+
shortId: string;
|
|
60
|
+
projectId: string;
|
|
61
|
+
path: string;
|
|
62
|
+
mediaType: string;
|
|
63
|
+
size: number;
|
|
64
|
+
updatedAt: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type AiProjectReference = {
|
|
68
|
+
id: string;
|
|
69
|
+
shortId: string;
|
|
70
|
+
projectId: string;
|
|
71
|
+
ref: CloudResourceRef;
|
|
72
|
+
label: string;
|
|
73
|
+
createdAt: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type AiProjectAccess = {
|
|
77
|
+
id: string;
|
|
78
|
+
shortId: string;
|
|
79
|
+
principal: Principal;
|
|
80
|
+
permission: AiProjectPermission;
|
|
81
|
+
displayName?: string;
|
|
82
|
+
createdAt: string;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type AiProjectAdminListItem = {
|
|
86
|
+
id: string;
|
|
87
|
+
shortId: string;
|
|
88
|
+
name: string;
|
|
89
|
+
description: string;
|
|
90
|
+
icon: string;
|
|
91
|
+
accessCount: number;
|
|
92
|
+
adminCount: number;
|
|
93
|
+
createdAt: string;
|
|
94
|
+
updatedAt: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type AiProjectAdminSummary = {
|
|
98
|
+
total: number;
|
|
99
|
+
unmanaged: number;
|
|
100
|
+
totalAccess: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
type ProjectRow = {
|
|
104
|
+
id: string;
|
|
105
|
+
short_id: string;
|
|
106
|
+
name: string;
|
|
107
|
+
description: string;
|
|
108
|
+
icon: string;
|
|
109
|
+
instructions: string;
|
|
110
|
+
default_model_profile_id: string | null;
|
|
111
|
+
revision: number;
|
|
112
|
+
created_at: Date | string;
|
|
113
|
+
updated_at: Date | string;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type KnowledgeRow = {
|
|
117
|
+
id: string;
|
|
118
|
+
short_id: string;
|
|
119
|
+
project_id: string;
|
|
120
|
+
title: string;
|
|
121
|
+
content: string;
|
|
122
|
+
created_at: Date | string;
|
|
123
|
+
updated_at: Date | string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type FileRow = {
|
|
127
|
+
id: string;
|
|
128
|
+
short_id: string;
|
|
129
|
+
project_id: string;
|
|
130
|
+
path: string;
|
|
131
|
+
media_type: string;
|
|
132
|
+
size: number;
|
|
133
|
+
updated_at: Date | string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type ReferenceRow = {
|
|
137
|
+
id: string;
|
|
138
|
+
short_id: string;
|
|
139
|
+
project_id: string;
|
|
140
|
+
resource_type: string;
|
|
141
|
+
resource_id: string;
|
|
142
|
+
label: string;
|
|
143
|
+
created_at: Date | string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type ProjectAccessRow = {
|
|
147
|
+
short_id: string;
|
|
148
|
+
user_id: string | null;
|
|
149
|
+
group_id: string | null;
|
|
150
|
+
service_account_id: string | null;
|
|
151
|
+
authenticated_only: boolean;
|
|
152
|
+
permission: AiProjectPermission;
|
|
153
|
+
created_at: Date | string;
|
|
154
|
+
display_name: string | null;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
type AdminProjectRow = ProjectRow & {
|
|
158
|
+
access_count: number;
|
|
159
|
+
admin_count: number;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const iso = (value: Date | string): string => (value instanceof Date ? value.toISOString() : new Date(value).toISOString());
|
|
163
|
+
const actorUserId = (subject: AccessSubject | null): string | null => (subject?.type === "user" ? subject.userId : null);
|
|
164
|
+
|
|
165
|
+
const principalForSubject = (subject: AccessSubject): Principal =>
|
|
166
|
+
subject.type === "user"
|
|
167
|
+
? { type: "user", userId: subject.userId }
|
|
168
|
+
: { type: "service_account", serviceAccountId: subject.serviceAccountId };
|
|
169
|
+
|
|
170
|
+
const accessMatch = (subject: AccessSubject | null): SQLQuery =>
|
|
171
|
+
buildAccessPrincipalCondition({
|
|
172
|
+
subject,
|
|
173
|
+
columns: {
|
|
174
|
+
userId: sql`access.user_id`,
|
|
175
|
+
groupId: sql`access.group_id`,
|
|
176
|
+
serviceAccountId: sql`access.service_account_id`,
|
|
177
|
+
authenticatedOnly: sql`access.authenticated_only`,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const permissionFor = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProjectPermission | "none"> => {
|
|
182
|
+
const match = accessMatch(subject);
|
|
183
|
+
const rows = await db<{ permission: AiProjectPermission }[]>`
|
|
184
|
+
SELECT access.permission
|
|
185
|
+
FROM ai.project_access project_access
|
|
186
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
187
|
+
WHERE project_access.project_id = ${row.id}::uuid AND ${match}
|
|
188
|
+
ORDER BY CASE access.permission WHEN 'admin' THEN 3 WHEN 'write' THEN 2 WHEN 'read' THEN 1 ELSE 0 END DESC
|
|
189
|
+
LIMIT 1
|
|
190
|
+
`;
|
|
191
|
+
return rows[0]?.permission ?? "none";
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const toProject = async (row: ProjectRow, subject: AccessSubject | null, db: SQL = sql): Promise<AiProject | null> => {
|
|
195
|
+
const permission = await permissionFor(row, subject, db);
|
|
196
|
+
if (permission === "none") return null;
|
|
197
|
+
return {
|
|
198
|
+
id: row.id,
|
|
199
|
+
shortId: row.short_id,
|
|
200
|
+
name: row.name,
|
|
201
|
+
description: row.description,
|
|
202
|
+
icon: row.icon,
|
|
203
|
+
instructions: row.instructions,
|
|
204
|
+
defaultModelProfileId: row.default_model_profile_id,
|
|
205
|
+
permission,
|
|
206
|
+
revision: Number(row.revision),
|
|
207
|
+
createdAt: iso(row.created_at),
|
|
208
|
+
updatedAt: iso(row.updated_at),
|
|
209
|
+
};
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const toKnowledge = (row: KnowledgeRow): AiProjectKnowledge => ({
|
|
213
|
+
id: row.id,
|
|
214
|
+
shortId: row.short_id,
|
|
215
|
+
projectId: row.project_id,
|
|
216
|
+
title: row.title,
|
|
217
|
+
content: row.content,
|
|
218
|
+
createdAt: iso(row.created_at),
|
|
219
|
+
updatedAt: iso(row.updated_at),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const toFile = (row: FileRow): AiProjectFile => ({
|
|
223
|
+
id: row.id,
|
|
224
|
+
shortId: row.short_id,
|
|
225
|
+
projectId: row.project_id,
|
|
226
|
+
path: row.path,
|
|
227
|
+
mediaType: row.media_type,
|
|
228
|
+
size: Number(row.size),
|
|
229
|
+
updatedAt: iso(row.updated_at),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const toReference = (row: ReferenceRow): AiProjectReference => ({
|
|
233
|
+
id: row.id,
|
|
234
|
+
shortId: row.short_id,
|
|
235
|
+
projectId: row.project_id,
|
|
236
|
+
ref: { type: row.resource_type, id: row.resource_id },
|
|
237
|
+
label: row.label,
|
|
238
|
+
createdAt: iso(row.created_at),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const toProjectAccess = (row: ProjectAccessRow): AiProjectAccess => ({
|
|
242
|
+
id: row.short_id,
|
|
243
|
+
shortId: row.short_id,
|
|
244
|
+
principal: row.user_id
|
|
245
|
+
? { type: "user", userId: row.user_id }
|
|
246
|
+
: row.group_id
|
|
247
|
+
? { type: "group", groupId: row.group_id }
|
|
248
|
+
: row.service_account_id
|
|
249
|
+
? { type: "service_account", serviceAccountId: row.service_account_id }
|
|
250
|
+
: row.authenticated_only
|
|
251
|
+
? { type: "authenticated" }
|
|
252
|
+
: { type: "public" },
|
|
253
|
+
permission: row.permission,
|
|
254
|
+
displayName: row.display_name ?? undefined,
|
|
255
|
+
createdAt: iso(row.created_at),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const toAdminProject = (row: AdminProjectRow): AiProjectAdminListItem => ({
|
|
259
|
+
id: row.id,
|
|
260
|
+
shortId: row.short_id,
|
|
261
|
+
name: row.name,
|
|
262
|
+
description: row.description,
|
|
263
|
+
icon: row.icon,
|
|
264
|
+
accessCount: Number(row.access_count),
|
|
265
|
+
adminCount: Number(row.admin_count),
|
|
266
|
+
createdAt: iso(row.created_at),
|
|
267
|
+
updatedAt: iso(row.updated_at),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const getRow = async (projectId: string): Promise<ProjectRow | null> => {
|
|
271
|
+
const rows = await sql<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid`;
|
|
272
|
+
return rows[0] ?? null;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const getRowByShortId = async (shortId: string): Promise<ProjectRow | null> => {
|
|
276
|
+
const rows = await sql<ProjectRow[]>`SELECT * FROM ai.projects WHERE short_id = ${shortId}`;
|
|
277
|
+
return rows[0] ?? null;
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const listProjectAccess = async (projectId: string, db: SQL = sql): Promise<AiProjectAccess[]> => {
|
|
281
|
+
const rows = await db<ProjectAccessRow[]>`
|
|
282
|
+
SELECT project_access.short_id, access.user_id, access.group_id, access.service_account_id, access.authenticated_only,
|
|
283
|
+
access.permission, access.created_at,
|
|
284
|
+
COALESCE(users.display_name, groups.name, service_accounts.name,
|
|
285
|
+
CASE WHEN access.authenticated_only THEN 'All authenticated users' ELSE 'Public' END) AS display_name
|
|
286
|
+
FROM ai.project_access project_access
|
|
287
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
288
|
+
LEFT JOIN auth.users users ON users.id = access.user_id
|
|
289
|
+
LEFT JOIN auth.groups groups ON groups.id = access.group_id
|
|
290
|
+
LEFT JOIN auth.service_accounts service_accounts ON service_accounts.id = access.service_account_id
|
|
291
|
+
WHERE project_access.project_id = ${projectId}::uuid
|
|
292
|
+
ORDER BY access.created_at, access.id
|
|
293
|
+
`;
|
|
294
|
+
return rows.map(toProjectAccess);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const createProjectAccess = async (
|
|
298
|
+
projectId: string,
|
|
299
|
+
input: { principal: Principal; permission: AiProjectPermission },
|
|
300
|
+
db: SQL,
|
|
301
|
+
): Promise<AiProjectAccess> => {
|
|
302
|
+
const created = await createAccess(input, db);
|
|
303
|
+
if (!created.ok) throw new Error(created.error.message);
|
|
304
|
+
let shortId = "";
|
|
305
|
+
await withAiShortIdForDb(db, "idx_ai_project_access_short_id", (attempt, candidate) => {
|
|
306
|
+
shortId = candidate;
|
|
307
|
+
return attempt`
|
|
308
|
+
INSERT INTO ai.project_access (project_id, access_id, short_id)
|
|
309
|
+
VALUES (${projectId}::uuid, ${created.data.id}::uuid, ${candidate})
|
|
310
|
+
`;
|
|
311
|
+
});
|
|
312
|
+
const entry = (await listProjectAccess(projectId, db)).find((item) => item.shortId === shortId);
|
|
313
|
+
if (!entry) throw new Error("Failed to read the created Project access entry.");
|
|
314
|
+
return entry;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const updateProjectAccess = async (projectId: string, accessId: string, permission: AiProjectPermission, db: SQL): Promise<boolean> => {
|
|
318
|
+
const [bound] = await db<{ id: string; permission: AiProjectPermission }[]>`
|
|
319
|
+
SELECT access.id, access.permission
|
|
320
|
+
FROM ai.project_access project_access
|
|
321
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
322
|
+
WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
|
|
323
|
+
`;
|
|
324
|
+
if (!bound) return false;
|
|
325
|
+
if (bound.permission === "admin" && permission !== "admin") {
|
|
326
|
+
const [admins] = await db<{ count: number }[]>`
|
|
327
|
+
SELECT count(*)::int AS count FROM ai.project_access project_access
|
|
328
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
329
|
+
WHERE project_access.project_id = ${projectId}::uuid AND access.permission = 'admin'
|
|
330
|
+
`;
|
|
331
|
+
if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
|
|
332
|
+
}
|
|
333
|
+
const result = await db`UPDATE auth.access SET permission = ${permission}::auth.permission_level WHERE id = ${bound.id}::uuid`;
|
|
334
|
+
return result.count === 1;
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const revokeProjectAccess = async (projectId: string, accessId: string, db: SQL): Promise<boolean> => {
|
|
338
|
+
const [bound] = await db<{ access_id: string; permission: AiProjectPermission }[]>`
|
|
339
|
+
SELECT project_access.access_id, access.permission
|
|
340
|
+
FROM ai.project_access project_access
|
|
341
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
342
|
+
WHERE project_access.project_id = ${projectId}::uuid AND project_access.short_id = ${accessId}
|
|
343
|
+
`;
|
|
344
|
+
if (!bound) return false;
|
|
345
|
+
if (bound.permission === "admin") {
|
|
346
|
+
const [admins] = await db<{ count: number }[]>`
|
|
347
|
+
SELECT count(*)::int AS count FROM ai.project_access project_access
|
|
348
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
349
|
+
WHERE project_access.project_id = ${projectId}::uuid AND access.permission = 'admin'
|
|
350
|
+
`;
|
|
351
|
+
if ((admins?.count ?? 0) <= 1) throw new AiProjectLastAdminError();
|
|
352
|
+
}
|
|
353
|
+
const deleted = await deleteAccess({ id: bound.access_id }, db);
|
|
354
|
+
if (!deleted.ok) throw new Error(deleted.error.message);
|
|
355
|
+
return true;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const requireProject = async (
|
|
359
|
+
projectId: string,
|
|
360
|
+
subject: AccessSubject | null,
|
|
361
|
+
permission: AiProjectPermission = "read",
|
|
362
|
+
): Promise<AiProject | null> => {
|
|
363
|
+
const row = await getRow(projectId);
|
|
364
|
+
if (!row) return null;
|
|
365
|
+
const project = await toProject(row, subject);
|
|
366
|
+
return project && hasPermission(project.permission, permission) ? project : null;
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const requireProjectByShortId = async (
|
|
370
|
+
shortId: string,
|
|
371
|
+
subject: AccessSubject | null,
|
|
372
|
+
permission: AiProjectPermission = "read",
|
|
373
|
+
): Promise<AiProject | null> => {
|
|
374
|
+
const row = await getRowByShortId(shortId);
|
|
375
|
+
if (!row) return null;
|
|
376
|
+
const project = await toProject(row, subject);
|
|
377
|
+
return project && hasPermission(project.permission, permission) ? project : null;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const touchProject = async (db: SQL, projectId: string): Promise<void> => {
|
|
381
|
+
await db`UPDATE ai.projects SET revision = revision + 1, updated_at = now() WHERE id = ${projectId}::uuid`;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const normalizeProjectPath = (value: string): string => {
|
|
385
|
+
const path = value.trim().replace(/\\/g, "/").replace(/^\/+/, "");
|
|
386
|
+
if (!path || path.length > 500 || path.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
387
|
+
throw new Error("Invalid project file path.");
|
|
388
|
+
}
|
|
389
|
+
return path;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const projectSearchPattern = (value: string | undefined): string | null => {
|
|
393
|
+
const query = value?.trim().toLowerCase();
|
|
394
|
+
return query ? `%${query.replace(/[\\%_]/g, (character) => `\\${character}`)}%` : null;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
export const aiProjects = {
|
|
398
|
+
async create(input: {
|
|
399
|
+
subject: AccessSubject;
|
|
400
|
+
name: string;
|
|
401
|
+
description?: string;
|
|
402
|
+
icon?: string;
|
|
403
|
+
instructions?: string;
|
|
404
|
+
defaultModelProfileId?: string | null;
|
|
405
|
+
}): Promise<AiProject> {
|
|
406
|
+
return sql.begin(async (tx) => {
|
|
407
|
+
const rows = await withAiShortIdForDb(
|
|
408
|
+
tx,
|
|
409
|
+
"idx_ai_projects_short_id",
|
|
410
|
+
(attempt, shortId) => attempt<ProjectRow[]>`
|
|
411
|
+
INSERT INTO ai.projects (short_id, name, description, icon, instructions, default_model_profile_id)
|
|
412
|
+
VALUES (
|
|
413
|
+
${shortId}, ${input.name.trim()}, ${input.description?.trim() ?? ""}, ${input.icon?.trim() || "ti ti-folders"},
|
|
414
|
+
${input.instructions?.trim() ?? ""}, ${input.defaultModelProfileId?.trim() || null}
|
|
415
|
+
)
|
|
416
|
+
RETURNING *
|
|
417
|
+
`,
|
|
418
|
+
);
|
|
419
|
+
const created = await createAccess({ principal: principalForSubject(input.subject), permission: "admin" }, tx);
|
|
420
|
+
if (!created.ok) throw new Error(created.error.message);
|
|
421
|
+
await withAiShortIdForDb(
|
|
422
|
+
tx,
|
|
423
|
+
"idx_ai_project_access_short_id",
|
|
424
|
+
(attempt, shortId) => attempt`
|
|
425
|
+
INSERT INTO ai.project_access (project_id, access_id, short_id)
|
|
426
|
+
VALUES (${rows[0]!.id}::uuid, ${created.data.id}::uuid, ${shortId})
|
|
427
|
+
`,
|
|
428
|
+
);
|
|
429
|
+
return { ...(await toProject(rows[0]!, input.subject, tx))!, permission: "admin" };
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
async list(subject: AccessSubject | null): Promise<AiProject[]> {
|
|
434
|
+
const match = accessMatch(subject);
|
|
435
|
+
const rows = await sql<ProjectRow[]>`
|
|
436
|
+
SELECT DISTINCT project.*, lower(project.name) AS sort_name
|
|
437
|
+
FROM ai.projects project
|
|
438
|
+
JOIN ai.project_access project_access ON project_access.project_id = project.id
|
|
439
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
440
|
+
WHERE access.permission <> 'none' AND ${match}
|
|
441
|
+
ORDER BY lower(project.name), project.id
|
|
442
|
+
LIMIT 200
|
|
443
|
+
`;
|
|
444
|
+
return (await Promise.all(rows.map((row) => toProject(row, subject)))).filter((project): project is AiProject => Boolean(project));
|
|
445
|
+
},
|
|
446
|
+
|
|
447
|
+
async resolveShortIds(projectIds: readonly string[], subject: AccessSubject | null): Promise<Map<string, string>> {
|
|
448
|
+
const ids = [...new Set(projectIds)];
|
|
449
|
+
if (!ids.length) return new Map();
|
|
450
|
+
const match = accessMatch(subject);
|
|
451
|
+
const rows = await sql<{ id: string; short_id: string }[]>`
|
|
452
|
+
SELECT DISTINCT project.id, project.short_id
|
|
453
|
+
FROM ai.projects project
|
|
454
|
+
JOIN ai.project_access project_access ON project_access.project_id = project.id
|
|
455
|
+
JOIN auth.access access ON access.id = project_access.access_id
|
|
456
|
+
WHERE project.id = ANY(${toPgUuidArray(ids)}::uuid[])
|
|
457
|
+
AND access.permission <> 'none' AND ${match}
|
|
458
|
+
`;
|
|
459
|
+
return new Map(rows.map((row) => [row.id, row.short_id]));
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
async scopeVersion(subject: AccessSubject | null): Promise<string> {
|
|
463
|
+
const projects = await this.list(subject);
|
|
464
|
+
return projects
|
|
465
|
+
.map((project) => `${project.shortId}:${project.permission}`)
|
|
466
|
+
.sort()
|
|
467
|
+
.join("|");
|
|
468
|
+
},
|
|
469
|
+
|
|
470
|
+
get: requireProject,
|
|
471
|
+
getByShortId: requireProjectByShortId,
|
|
472
|
+
|
|
473
|
+
async update(
|
|
474
|
+
projectId: string,
|
|
475
|
+
subject: AccessSubject | null,
|
|
476
|
+
input: Partial<Pick<AiProject, "name" | "description" | "icon" | "instructions" | "defaultModelProfileId">>,
|
|
477
|
+
): Promise<AiProject | null> {
|
|
478
|
+
const changesTrustedInstructions = Object.hasOwn(input, "instructions") || Object.hasOwn(input, "defaultModelProfileId");
|
|
479
|
+
if (!(await requireProject(projectId, subject, changesTrustedInstructions ? "admin" : "write"))) return null;
|
|
480
|
+
const rows = await sql<ProjectRow[]>`
|
|
481
|
+
UPDATE ai.projects
|
|
482
|
+
SET name = COALESCE(${input.name?.trim() ?? null}, name),
|
|
483
|
+
description = COALESCE(${input.description?.trim() ?? null}, description),
|
|
484
|
+
icon = COALESCE(${input.icon?.trim() ?? null}, icon),
|
|
485
|
+
instructions = COALESCE(${input.instructions?.trim() ?? null}, instructions),
|
|
486
|
+
default_model_profile_id = CASE
|
|
487
|
+
WHEN ${Object.hasOwn(input, "defaultModelProfileId")} THEN ${input.defaultModelProfileId?.trim() || null}
|
|
488
|
+
ELSE default_model_profile_id
|
|
489
|
+
END,
|
|
490
|
+
revision = revision + 1,
|
|
491
|
+
updated_at = now()
|
|
492
|
+
WHERE id = ${projectId}::uuid
|
|
493
|
+
RETURNING *
|
|
494
|
+
`;
|
|
495
|
+
return rows[0] ? toProject(rows[0], subject) : null;
|
|
496
|
+
},
|
|
497
|
+
|
|
498
|
+
async delete(projectId: string, subject: AccessSubject | null): Promise<boolean> {
|
|
499
|
+
return sql.begin(async (tx) => {
|
|
500
|
+
const [project] = await tx<ProjectRow[]>`
|
|
501
|
+
SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
|
|
502
|
+
`;
|
|
503
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "admin")) return false;
|
|
504
|
+
const accessIds = (
|
|
505
|
+
await tx<{ access_id: string }[]>`SELECT access_id FROM ai.project_access WHERE project_id = ${projectId}::uuid`
|
|
506
|
+
).map((row) => row.access_id);
|
|
507
|
+
await tx`DELETE FROM ai.projects WHERE id = ${projectId}::uuid`;
|
|
508
|
+
if (accessIds.length) await tx`DELETE FROM auth.access WHERE id = ANY(${toPgUuidArray(accessIds)}::uuid[])`;
|
|
509
|
+
return true;
|
|
510
|
+
});
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
async snapshot(projectId: string, subject: AccessSubject | null): Promise<AiProjectPromptSnapshot | null> {
|
|
514
|
+
return sql.begin(async (tx) => {
|
|
515
|
+
const [row] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR SHARE`;
|
|
516
|
+
if (!row || !hasPermission(await permissionFor(row, subject, tx), "read")) return null;
|
|
517
|
+
const project = await toProject(row, subject, tx);
|
|
518
|
+
if (!project) return null;
|
|
519
|
+
const [knowledgeRows, fileRows, referenceRows] = await Promise.all([
|
|
520
|
+
tx<KnowledgeRow[]>`SELECT * FROM ai.project_knowledge WHERE project_id = ${projectId}::uuid ORDER BY updated_at DESC, id LIMIT 100`,
|
|
521
|
+
tx<
|
|
522
|
+
FileRow[]
|
|
523
|
+
>`SELECT id, short_id, project_id, path, media_type, octet_length(bytes)::int AS size, updated_at FROM ai.project_files WHERE project_id = ${projectId}::uuid ORDER BY updated_at DESC, id`,
|
|
524
|
+
tx<ReferenceRow[]>`SELECT * FROM ai.project_resource_refs WHERE project_id = ${projectId}::uuid ORDER BY created_at, id LIMIT 500`,
|
|
525
|
+
]);
|
|
526
|
+
const knowledge = knowledgeRows.map(toKnowledge);
|
|
527
|
+
const files = fileRows.map(toFile);
|
|
528
|
+
const references = referenceRows.map(toReference);
|
|
529
|
+
const lines = [
|
|
530
|
+
`Project: ${project.name} (${project.shortId}, revision ${project.revision})`,
|
|
531
|
+
project.description ? `Description: ${project.description}` : null,
|
|
532
|
+
knowledge.length ? `Knowledge entries:\n${knowledge.map((item) => `- ${item.title} [${item.shortId}]`).join("\n")}` : null,
|
|
533
|
+
files.length
|
|
534
|
+
? `Files (read-only below /project):\n${files
|
|
535
|
+
.map((file) => `- ${mountAiProjectFilePath(file.path)} (${file.mediaType}, ${file.size} bytes) [${file.shortId}]`)
|
|
536
|
+
.join("\n")}`
|
|
537
|
+
: null,
|
|
538
|
+
references.length
|
|
539
|
+
? `Cloud references (metadata only; use authorized app capabilities to read the source):\n${references
|
|
540
|
+
.map((reference) => `- ${reference.label || reference.ref.id}: ${reference.ref.type}/${reference.ref.id}`)
|
|
541
|
+
.join("\n")}`
|
|
542
|
+
: null,
|
|
543
|
+
].filter(Boolean);
|
|
544
|
+
return {
|
|
545
|
+
id: project.shortId,
|
|
546
|
+
name: project.name,
|
|
547
|
+
revision: project.revision,
|
|
548
|
+
instructions: project.instructions,
|
|
549
|
+
context: lines.join("\n").slice(0, AI_PROJECT_PROMPT_MANIFEST_MAX_CHARS),
|
|
550
|
+
references: references.map((reference) => reference.ref),
|
|
551
|
+
defaultModelProfileId: project.defaultModelProfileId,
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
async listAccess(projectId: string, subject: AccessSubject | null): Promise<AiProjectAccess[] | null> {
|
|
557
|
+
if (!(await requireProject(projectId, subject, "admin"))) return null;
|
|
558
|
+
return listProjectAccess(projectId);
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
async grantAccess(
|
|
562
|
+
projectId: string,
|
|
563
|
+
subject: AccessSubject | null,
|
|
564
|
+
input: { principal: Principal; permission: AiProjectPermission },
|
|
565
|
+
): Promise<AiProjectAccess | null> {
|
|
566
|
+
return sql.begin(async (tx) => {
|
|
567
|
+
const [row] = await tx<ProjectRow[]>`
|
|
568
|
+
SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
|
|
569
|
+
`;
|
|
570
|
+
if (!row || !hasPermission(await permissionFor(row, subject, tx), "admin")) return null;
|
|
571
|
+
return createProjectAccess(projectId, input, tx);
|
|
572
|
+
});
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
async updateAccess(
|
|
576
|
+
projectId: string,
|
|
577
|
+
accessId: string,
|
|
578
|
+
subject: AccessSubject | null,
|
|
579
|
+
permission: AiProjectPermission,
|
|
580
|
+
): Promise<boolean> {
|
|
581
|
+
return sql.begin(async (tx) => {
|
|
582
|
+
const [project] = await tx<ProjectRow[]>`
|
|
583
|
+
SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
|
|
584
|
+
`;
|
|
585
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "admin")) return false;
|
|
586
|
+
return updateProjectAccess(projectId, accessId, permission, tx);
|
|
587
|
+
});
|
|
588
|
+
},
|
|
589
|
+
|
|
590
|
+
async revokeAccess(projectId: string, accessId: string, subject: AccessSubject | null): Promise<boolean> {
|
|
591
|
+
return sql.begin(async (tx) => {
|
|
592
|
+
const [project] = await tx<ProjectRow[]>`
|
|
593
|
+
SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE
|
|
594
|
+
`;
|
|
595
|
+
if (!project || !hasPermission(await permissionFor(project, subject, tx), "admin")) return false;
|
|
596
|
+
return revokeProjectAccess(projectId, accessId, tx);
|
|
597
|
+
});
|
|
598
|
+
},
|
|
599
|
+
|
|
600
|
+
admin: {
|
|
601
|
+
async list(params: {
|
|
602
|
+
search?: string;
|
|
603
|
+
page?: number;
|
|
604
|
+
perPage?: number;
|
|
605
|
+
}): Promise<{ items: AiProjectAdminListItem[]; total: number; page: number; perPage: number }> {
|
|
606
|
+
const perPage = Math.min(Math.max(params.perPage ?? 100, 1), 500);
|
|
607
|
+
const page = Math.max(params.page ?? 1, 1);
|
|
608
|
+
const offset = (page - 1) * perPage;
|
|
609
|
+
const pattern = projectSearchPattern(params.search);
|
|
610
|
+
const [countRow] = await sql<{ total: number }[]>`
|
|
611
|
+
SELECT count(*)::int AS total
|
|
612
|
+
FROM ai.projects project
|
|
613
|
+
WHERE ${pattern}::text IS NULL
|
|
614
|
+
OR lower(project.name) LIKE ${pattern} ESCAPE '\\'
|
|
615
|
+
OR lower(project.description) LIKE ${pattern} ESCAPE '\\'
|
|
616
|
+
OR lower(project.short_id) LIKE ${pattern} ESCAPE '\\'
|
|
617
|
+
`;
|
|
618
|
+
const rows = await sql<AdminProjectRow[]>`
|
|
619
|
+
SELECT project.*,
|
|
620
|
+
count(project_access.access_id)::int AS access_count,
|
|
621
|
+
count(project_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
|
|
622
|
+
FROM ai.projects project
|
|
623
|
+
LEFT JOIN ai.project_access project_access ON project_access.project_id = project.id
|
|
624
|
+
LEFT JOIN auth.access access ON access.id = project_access.access_id
|
|
625
|
+
WHERE ${pattern}::text IS NULL
|
|
626
|
+
OR lower(project.name) LIKE ${pattern} ESCAPE '\\'
|
|
627
|
+
OR lower(project.description) LIKE ${pattern} ESCAPE '\\'
|
|
628
|
+
OR lower(project.short_id) LIKE ${pattern} ESCAPE '\\'
|
|
629
|
+
GROUP BY project.id
|
|
630
|
+
ORDER BY project.updated_at DESC, project.id
|
|
631
|
+
LIMIT ${perPage} OFFSET ${offset}
|
|
632
|
+
`;
|
|
633
|
+
return { items: rows.map(toAdminProject), total: countRow?.total ?? 0, page, perPage };
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
async summary(params: { search?: string } = {}): Promise<AiProjectAdminSummary> {
|
|
637
|
+
const pattern = projectSearchPattern(params.search);
|
|
638
|
+
const [row] = await sql<{ total: number; unmanaged: number; total_access: number }[]>`
|
|
639
|
+
WITH filtered AS (
|
|
640
|
+
SELECT project.id,
|
|
641
|
+
count(project_access.access_id)::int AS access_count,
|
|
642
|
+
count(project_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
|
|
643
|
+
FROM ai.projects project
|
|
644
|
+
LEFT JOIN ai.project_access project_access ON project_access.project_id = project.id
|
|
645
|
+
LEFT JOIN auth.access access ON access.id = project_access.access_id
|
|
646
|
+
WHERE ${pattern}::text IS NULL
|
|
647
|
+
OR lower(project.name) LIKE ${pattern} ESCAPE '\\'
|
|
648
|
+
OR lower(project.description) LIKE ${pattern} ESCAPE '\\'
|
|
649
|
+
OR lower(project.short_id) LIKE ${pattern} ESCAPE '\\'
|
|
650
|
+
GROUP BY project.id
|
|
651
|
+
)
|
|
652
|
+
SELECT count(*)::int AS total,
|
|
653
|
+
count(*) FILTER (WHERE admin_count = 0)::int AS unmanaged,
|
|
654
|
+
coalesce(sum(access_count), 0)::int AS total_access
|
|
655
|
+
FROM filtered
|
|
656
|
+
`;
|
|
657
|
+
return { total: row?.total ?? 0, unmanaged: row?.unmanaged ?? 0, totalAccess: row?.total_access ?? 0 };
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
async getByShortId(shortId: string): Promise<AiProjectAdminListItem | null> {
|
|
661
|
+
const rows = await sql<AdminProjectRow[]>`
|
|
662
|
+
SELECT project.*,
|
|
663
|
+
count(project_access.access_id)::int AS access_count,
|
|
664
|
+
count(project_access.access_id) FILTER (WHERE access.permission = 'admin')::int AS admin_count
|
|
665
|
+
FROM ai.projects project
|
|
666
|
+
LEFT JOIN ai.project_access project_access ON project_access.project_id = project.id
|
|
667
|
+
LEFT JOIN auth.access access ON access.id = project_access.access_id
|
|
668
|
+
WHERE project.short_id = ${shortId}
|
|
669
|
+
GROUP BY project.id
|
|
670
|
+
`;
|
|
671
|
+
return rows[0] ? toAdminProject(rows[0]) : null;
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
listAccess: listProjectAccess,
|
|
675
|
+
|
|
676
|
+
async grantAccess(
|
|
677
|
+
projectId: string,
|
|
678
|
+
input: { principal: Principal; permission: AiProjectPermission },
|
|
679
|
+
): Promise<AiProjectAccess | null> {
|
|
680
|
+
return sql.begin(async (tx) => {
|
|
681
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
682
|
+
return project ? createProjectAccess(projectId, input, tx) : null;
|
|
683
|
+
});
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
async updateAccess(projectId: string, accessId: string, permission: AiProjectPermission): Promise<boolean> {
|
|
687
|
+
return sql.begin(async (tx) => {
|
|
688
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
689
|
+
return project ? updateProjectAccess(projectId, accessId, permission, tx) : false;
|
|
690
|
+
});
|
|
691
|
+
},
|
|
692
|
+
|
|
693
|
+
async revokeAccess(projectId: string, accessId: string): Promise<boolean> {
|
|
694
|
+
return sql.begin(async (tx) => {
|
|
695
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
696
|
+
return project ? revokeProjectAccess(projectId, accessId, tx) : false;
|
|
697
|
+
});
|
|
698
|
+
},
|
|
699
|
+
|
|
700
|
+
async delete(projectId: string): Promise<boolean> {
|
|
701
|
+
return sql.begin(async (tx) => {
|
|
702
|
+
const [project] = await tx<ProjectRow[]>`SELECT * FROM ai.projects WHERE id = ${projectId}::uuid FOR UPDATE`;
|
|
703
|
+
if (!project) return false;
|
|
704
|
+
const accessIds = (
|
|
705
|
+
await tx<{ access_id: string }[]>`SELECT access_id FROM ai.project_access WHERE project_id = ${projectId}::uuid`
|
|
706
|
+
).map((row) => row.access_id);
|
|
707
|
+
await tx`DELETE FROM ai.projects WHERE id = ${projectId}::uuid`;
|
|
708
|
+
if (accessIds.length) await tx`DELETE FROM auth.access WHERE id = ANY(${toPgUuidArray(accessIds)}::uuid[])`;
|
|
709
|
+
return true;
|
|
710
|
+
});
|
|
711
|
+
},
|
|
712
|
+
},
|
|
713
|
+
|
|
714
|
+
async listKnowledge(projectId: string, subject: AccessSubject | null, query?: string): Promise<AiProjectKnowledge[]> {
|
|
715
|
+
if (!(await requireProject(projectId, subject, "read"))) return [];
|
|
716
|
+
const q = query?.trim() || null;
|
|
717
|
+
const pattern = q ? `%${q.toLowerCase().replace(/[\\%_]/g, (char) => `\\${char}`)}%` : null;
|
|
718
|
+
const rows = await sql<KnowledgeRow[]>`
|
|
719
|
+
SELECT * FROM ai.project_knowledge
|
|
720
|
+
WHERE project_id = ${projectId}::uuid
|
|
721
|
+
AND (${q}::text IS NULL OR lower(title) LIKE ${pattern} ESCAPE '\\' OR lower(content) LIKE ${pattern} ESCAPE '\\'
|
|
722
|
+
OR search_document @@ websearch_to_tsquery('simple', ${q ?? ""}))
|
|
723
|
+
ORDER BY updated_at DESC, id
|
|
724
|
+
LIMIT 100
|
|
725
|
+
`;
|
|
726
|
+
return rows.map(toKnowledge);
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
async getKnowledgeByShortId(projectId: string, shortId: string, subject: AccessSubject | null): Promise<AiProjectKnowledge | null> {
|
|
730
|
+
if (!(await requireProject(projectId, subject, "read"))) return null;
|
|
731
|
+
const rows = await sql<KnowledgeRow[]>`
|
|
732
|
+
SELECT * FROM ai.project_knowledge
|
|
733
|
+
WHERE project_id = ${projectId}::uuid AND short_id = ${shortId}
|
|
734
|
+
LIMIT 1
|
|
735
|
+
`;
|
|
736
|
+
return rows[0] ? toKnowledge(rows[0]) : null;
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
async createKnowledge(
|
|
740
|
+
projectId: string,
|
|
741
|
+
subject: AccessSubject | null,
|
|
742
|
+
input: { title: string; content: string },
|
|
743
|
+
): Promise<AiProjectKnowledge | null> {
|
|
744
|
+
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
745
|
+
return sql.begin(async (tx) => {
|
|
746
|
+
const rows = await withAiShortIdForDb(
|
|
747
|
+
tx,
|
|
748
|
+
"idx_ai_project_knowledge_short_id",
|
|
749
|
+
(attempt, shortId) => attempt<KnowledgeRow[]>`
|
|
750
|
+
INSERT INTO ai.project_knowledge (short_id, project_id, title, content, created_by_user_id)
|
|
751
|
+
VALUES (${shortId}, ${projectId}::uuid, ${input.title.trim()}, ${input.content.trim()}, ${actorUserId(subject)}::uuid)
|
|
752
|
+
RETURNING *
|
|
753
|
+
`,
|
|
754
|
+
);
|
|
755
|
+
await touchProject(tx, projectId);
|
|
756
|
+
return toKnowledge(rows[0]!);
|
|
757
|
+
});
|
|
758
|
+
},
|
|
759
|
+
|
|
760
|
+
async updateKnowledge(
|
|
761
|
+
projectId: string,
|
|
762
|
+
knowledgeId: string,
|
|
763
|
+
subject: AccessSubject | null,
|
|
764
|
+
input: { title?: string; content?: string },
|
|
765
|
+
): Promise<AiProjectKnowledge | null> {
|
|
766
|
+
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
767
|
+
return sql.begin(async (tx) => {
|
|
768
|
+
const rows = await tx<KnowledgeRow[]>`
|
|
769
|
+
UPDATE ai.project_knowledge
|
|
770
|
+
SET title = COALESCE(${input.title?.trim() ?? null}, title), content = COALESCE(${input.content?.trim() ?? null}, content), updated_at = now()
|
|
771
|
+
WHERE short_id = ${knowledgeId} AND project_id = ${projectId}::uuid
|
|
772
|
+
RETURNING *
|
|
773
|
+
`;
|
|
774
|
+
if (rows[0]) await touchProject(tx, projectId);
|
|
775
|
+
return rows[0] ? toKnowledge(rows[0]) : null;
|
|
776
|
+
});
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
async deleteKnowledge(projectId: string, knowledgeId: string, subject: AccessSubject | null): Promise<boolean> {
|
|
780
|
+
if (!(await requireProject(projectId, subject, "write"))) return false;
|
|
781
|
+
return sql.begin(async (tx) => {
|
|
782
|
+
const rows = await tx<{ id: string }[]>`
|
|
783
|
+
DELETE FROM ai.project_knowledge WHERE short_id = ${knowledgeId} AND project_id = ${projectId}::uuid RETURNING id
|
|
784
|
+
`;
|
|
785
|
+
if (rows[0]) await touchProject(tx, projectId);
|
|
786
|
+
return Boolean(rows[0]);
|
|
787
|
+
});
|
|
788
|
+
},
|
|
789
|
+
|
|
790
|
+
async listFiles(projectId: string, subject: AccessSubject | null): Promise<AiProjectFile[]> {
|
|
791
|
+
if (!(await requireProject(projectId, subject, "read"))) return [];
|
|
792
|
+
return (
|
|
793
|
+
await sql<
|
|
794
|
+
FileRow[]
|
|
795
|
+
>`SELECT id, short_id, project_id, path, media_type, size, updated_at FROM ai.project_files WHERE project_id = ${projectId}::uuid ORDER BY path LIMIT 500`
|
|
796
|
+
).map(toFile);
|
|
797
|
+
},
|
|
798
|
+
|
|
799
|
+
async writeFile(
|
|
800
|
+
projectId: string,
|
|
801
|
+
subject: AccessSubject | null,
|
|
802
|
+
input: { path: string; mediaType: string; bytes: Uint8Array },
|
|
803
|
+
): Promise<AiProjectFile | null> {
|
|
804
|
+
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
805
|
+
if (input.bytes.byteLength > AI_PROJECT_FILE_MAX_BYTES) throw new Error("Project file exceeds the size limit.");
|
|
806
|
+
const path = normalizeProjectPath(input.path);
|
|
807
|
+
return sql.begin(async (tx) => {
|
|
808
|
+
const rows = await withAiShortIdForDb(
|
|
809
|
+
tx,
|
|
810
|
+
"idx_ai_project_files_short_id",
|
|
811
|
+
(attempt, shortId) => attempt<FileRow[]>`
|
|
812
|
+
INSERT INTO ai.project_files (short_id, project_id, path, media_type, bytes, size, created_by_user_id)
|
|
813
|
+
VALUES (${shortId}, ${projectId}::uuid, ${path}, ${input.mediaType.trim() || "application/octet-stream"}, ${input.bytes}, ${input.bytes.byteLength}, ${actorUserId(subject)}::uuid)
|
|
814
|
+
ON CONFLICT (project_id, path) DO UPDATE SET
|
|
815
|
+
media_type = EXCLUDED.media_type, bytes = EXCLUDED.bytes, size = EXCLUDED.size, updated_at = now()
|
|
816
|
+
RETURNING id, short_id, project_id, path, media_type, size, updated_at
|
|
817
|
+
`,
|
|
818
|
+
);
|
|
819
|
+
await touchProject(tx, projectId);
|
|
820
|
+
return toFile(rows[0]!);
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
async readFile(
|
|
825
|
+
projectId: string,
|
|
826
|
+
fileId: string,
|
|
827
|
+
subject: AccessSubject | null,
|
|
828
|
+
): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
|
|
829
|
+
if (!(await requireProject(projectId, subject, "read"))) return null;
|
|
830
|
+
const rows = await sql<(FileRow & { bytes: Uint8Array })[]>`
|
|
831
|
+
SELECT id, short_id, project_id, path, media_type, size, updated_at, bytes FROM ai.project_files
|
|
832
|
+
WHERE short_id = ${fileId} AND project_id = ${projectId}::uuid
|
|
833
|
+
`;
|
|
834
|
+
return rows[0] ? { ...toFile(rows[0]), bytes: rows[0].bytes } : null;
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
async readFileByPath(
|
|
838
|
+
projectId: string,
|
|
839
|
+
path: string,
|
|
840
|
+
subject: AccessSubject | null,
|
|
841
|
+
): Promise<(AiProjectFile & { bytes: Uint8Array }) | null> {
|
|
842
|
+
if (!(await requireProject(projectId, subject, "read"))) return null;
|
|
843
|
+
const normalized = normalizeProjectPath(path);
|
|
844
|
+
const rows = await sql<(FileRow & { bytes: Uint8Array })[]>`
|
|
845
|
+
SELECT id, short_id, project_id, path, media_type, size, updated_at, bytes FROM ai.project_files
|
|
846
|
+
WHERE path = ${normalized} AND project_id = ${projectId}::uuid
|
|
847
|
+
`;
|
|
848
|
+
return rows[0] ? { ...toFile(rows[0]), bytes: rows[0].bytes } : null;
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
async deleteFile(projectId: string, fileId: string, subject: AccessSubject | null): Promise<boolean> {
|
|
852
|
+
if (!(await requireProject(projectId, subject, "write"))) return false;
|
|
853
|
+
return sql.begin(async (tx) => {
|
|
854
|
+
const rows = await tx<{ id: string }[]>`
|
|
855
|
+
DELETE FROM ai.project_files WHERE short_id = ${fileId} AND project_id = ${projectId}::uuid RETURNING id
|
|
856
|
+
`;
|
|
857
|
+
if (rows[0]) await touchProject(tx, projectId);
|
|
858
|
+
return Boolean(rows[0]);
|
|
859
|
+
});
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
async listReferences(projectId: string, subject: AccessSubject | null): Promise<AiProjectReference[]> {
|
|
863
|
+
if (!(await requireProject(projectId, subject, "read"))) return [];
|
|
864
|
+
return (
|
|
865
|
+
await sql<
|
|
866
|
+
ReferenceRow[]
|
|
867
|
+
>`SELECT * FROM ai.project_resource_refs WHERE project_id = ${projectId}::uuid ORDER BY created_at, id LIMIT 500`
|
|
868
|
+
).map(toReference);
|
|
869
|
+
},
|
|
870
|
+
|
|
871
|
+
async createReference(
|
|
872
|
+
projectId: string,
|
|
873
|
+
subject: AccessSubject | null,
|
|
874
|
+
input: { ref: CloudResourceRef; label?: string },
|
|
875
|
+
): Promise<AiProjectReference | null> {
|
|
876
|
+
if (!(await requireProject(projectId, subject, "write"))) return null;
|
|
877
|
+
return sql.begin(async (tx) => {
|
|
878
|
+
const rows = await withAiShortIdForDb(
|
|
879
|
+
tx,
|
|
880
|
+
"idx_ai_project_resource_refs_short_id",
|
|
881
|
+
(attempt, shortId) => attempt<ReferenceRow[]>`
|
|
882
|
+
INSERT INTO ai.project_resource_refs (short_id, project_id, resource_type, resource_id, label)
|
|
883
|
+
VALUES (${shortId}, ${projectId}::uuid, ${input.ref.type}, ${input.ref.id}, ${input.label?.trim() ?? ""})
|
|
884
|
+
RETURNING *
|
|
885
|
+
`,
|
|
886
|
+
);
|
|
887
|
+
await touchProject(tx, projectId);
|
|
888
|
+
return toReference(rows[0]!);
|
|
889
|
+
});
|
|
890
|
+
},
|
|
891
|
+
|
|
892
|
+
async deleteReference(projectId: string, referenceId: string, subject: AccessSubject | null): Promise<boolean> {
|
|
893
|
+
if (!(await requireProject(projectId, subject, "write"))) return false;
|
|
894
|
+
return sql.begin(async (tx) => {
|
|
895
|
+
const rows = await tx<{ id: string }[]>`
|
|
896
|
+
DELETE FROM ai.project_resource_refs WHERE short_id = ${referenceId} AND project_id = ${projectId}::uuid RETURNING id
|
|
897
|
+
`;
|
|
898
|
+
if (rows[0]) await touchProject(tx, projectId);
|
|
899
|
+
return Boolean(rows[0]);
|
|
900
|
+
});
|
|
901
|
+
},
|
|
902
|
+
};
|