.github/9
ISSUE_TEMPLATE/3bug_report.md config.yml feature_request.md
workflows/3ci.yml clean-clone.yml release.yml
CODEOWNERS dependabot.yml pull_request_template.md
apps/630
api/250
src/243
common/30
__tests__/3authorize.test.ts bigint-serialization.interceptor.test.ts files-limits.test.ts
filters/1all-exceptions.filter.ts
guards/3auth.guard.ts internal-service.guard.ts rate-limit.guard.ts
interceptors/2actor-context.interceptor.ts bigint-serialization.interceptor.ts
middleware/2idempotency.test.ts idempotency.ts
pipes/1zod-validation.pipe.ts
actor-context.ts audit.service.ts cache.test.ts cache.ts cookies.ts core.module.ts deprecation.ts email.service.ts errors.ts event-bus.ts log-scrubber.test.ts log-scrubber.ts logger.ts permission-cache-bridge.ts prisma.service.ts redis.service.ts trace-metadata.ts types.ts
config/1env.ts
lib/11
__tests__/5crypto.test.ts filter-builder.test.ts jwt.test.ts pagination.test.ts positions.test.ts
authorize.ts crypto.ts filter-builder.ts jwt.ts pagination.ts positions.ts
modules/197
ai/28
__tests__/4ai-writing.test.ts database-ai.test.ts generate-executor.test.ts prompts.test.ts
generator/4executor.ts generate.controller.ts generate.dto.ts generate.service.ts
ai-chat.service.ts ai-rate-limit.ts ai-writing.controller.ts ai-writing.dto.ts ai-writing.service.ts ai.controller.ts ai.module.ts ai.service.ts budget-guard.service.ts database-ai.controller.ts database-ai.dto.ts database-ai.service.ts db-prompts.ts dto.ts gateway-provider.ts ledger.service.ts prompts.ts rag.controller.ts rag.dto.ts rag.service.ts
api-keys/3api-keys.controller.ts api-keys.module.ts api-keys.service.ts
audit/2audit-log.controller.ts audit.module.ts
auth/4auth.controller.ts auth.module.ts auth.service.ts dto.ts
automations/5automations.controller.ts automations.module.ts automations.service.ts dto.ts webhook-incoming.controller.ts
blocks/5
__tests__/1batch-version-cas.test.ts
block.service.ts blocks.controller.ts blocks.module.ts dto.ts
comments/4comments.controller.ts comments.module.ts comments.service.ts dto.ts
databases/9
__tests__/3normalize-select-options.test.ts option-id-canonicalization.test.ts record-open-query-shape.test.ts
database.service.ts databases.controller.ts databases.module.ts dto.ts property-cache-schema.ts query.service.ts
docs/2docs.controller.ts docs.module.ts
files/10
__tests__/1quota.test.ts
dto.ts files.controller.ts files.module.ts files.service.ts quota.ts record.ts s3.ts uploads.controller.ts uploads.service.ts
forms/18debug.controller.ts form.dto.ts forms.controller.ts forms.module.ts forms.service.ts markdown.test.ts markdown.ts optional-auth.guard.ts public-forms.controller.ts public-forms.service.ts question.dto.ts questions.service.ts rate-limit.ts recaptcha.ts slug.ts submissions.controller.ts submissions.service.ts submit-schema.ts
health/3health.controller.ts health.module.ts health.service.ts
integrations/3integrations.controller.ts integrations.module.ts integrations.service.ts
internal/12
__tests__/2flush-query-shape.test.ts internal.endpoints.test.ts
ai-test-seam.controller.ts ai-test-seam.dto.ts ai-test-seam.service.ts internal.controller.ts internal.dto.ts internal.module.ts internal.service.ts test-seam.controller.ts test-seam.dto.ts test-seam.service.ts
notifications/3notifications.controller.ts notifications.module.ts notifications.service.ts
oauth/6
__tests__/1oauth.scopes.test.ts
oauth.controller.ts oauth.module.ts oauth.service.ts scopes.ts token-keys.ts
org/15audit.service.ts embed-preview.controller.ts guards.ts members.service.ts org-admin.controller.ts org-policy.service.ts org.module.ts overview.service.ts policy-effective.controller.ts policy.schema.ts policy.test.ts retention.service.ts totp.test.ts totp.ts two-factor.service.ts
orgs/5dto.ts orgs.controller.ts orgs.module.ts orgs.service.ts util.ts
pages/4dto.ts page.service.ts pages.controller.ts pages.module.ts
scim/15
__tests__/3email-mapping.test.ts etag.test.ts filters.test.ts
etag.ts filters.ts groups.service.ts schemas.ts scim-admin.controller.ts scim-admin.service.ts scim-auth.guard.ts scim-auth.service.ts scim-errors.ts scim.controller.ts scim.module.ts users.service.ts
search/4
__tests__/1search.service.test.ts
search.controller.ts search.module.ts search.service.ts
sessions/3sessions.controller.ts sessions.module.ts sessions.service.ts
share/3share.controller.ts share.module.ts share.service.ts
sso/16
__tests__/4enforcement.test.ts oidc.verify.test.ts provisioning.test.ts saml.validate.test.ts
enforcement.ts oidc.controller.ts oidc.service.ts oidc.ts provisioning.service.ts saml.controller.ts saml.ts schemas.ts sso-admin.controller.ts sso-admin.service.ts sso-secrets.ts sso.module.ts
teams/4dto.ts teams.controller.ts teams.module.ts teams.service.ts
users/4dto.ts users.controller.ts users.module.ts users.service.ts
webhooks/3webhooks.controller.ts webhooks.module.ts webhooks.service.ts
workspaces/4dto.ts workspaces.controller.ts workspaces.module.ts workspaces.service.ts
types/1xmldom.d.ts
app.module.ts constants.ts main.ts
_probe_rawbody.cjs .env.example Dockerfile package.json README.md tsconfig.json vitest.config.ts
realtime/33
src/17auth.ts comments.ts config.ts event-source.ts http-store.ts index.ts metrics.ts notifications.ts page-updated-source.ts presence.ts prisma-store.ts protocol.ts rate-limit.ts room-key.ts rooms.ts server.ts update-log.ts
test/11auth.test.ts config.test.ts event-source.test.ts http-store.test.ts metrics-merge.test.ts page-updated-source.test.ts presence.test.ts protocol.test.ts rate-limit.test.ts rooms.test.ts task-0094-builders.test.ts
.env.example Dockerfile package.json README.md tsconfig.json
web/255
src/244
app/45
__tests__/1theme-tokens.test.ts
(app)/26
w/25
[workspace]/25
ask/1page.tsx
databases/1
[id]/1page.tsx
favorites/1page.tsx
files/1page.tsx
notifications/1page.tsx
page/4
[id]/4
[block]/1page.tsx
share/1page.tsx
layout.tsx page.tsx
search/1page.tsx
settings/10
appearance/1page.tsx
automations/3
[id]/1page.tsx
new/1page.tsx
page.tsx
billing/1page.tsx
developers/1page.tsx
members/1page.tsx
permissions/1page.tsx
teams/1page.tsx
page.tsx
shared/1page.tsx
templates/1page.tsx
trash/1page.tsx
layout.tsx page.tsx
layout.tsx
(auth)/4
login/1page.tsx
register/1page.tsx
reset-password/1page.tsx
layout.tsx
api/2
metrics/1route.ts
v1/1
health/1route.ts
f/1
[slug]/1page.tsx
health/1
ready/1route.ts
org/2
[orgId]/2
admin/2
integrations/1page.tsx
page.tsx
share/1
[id]/1page.tsx
error.tsx global-error.tsx globals.css layout.tsx not-found.tsx page.tsx providers.tsx
components/118
admin/10
org-admin/8ApiKeysTab.tsx AuditTab.tsx MembersTab.tsx OverviewTab.tsx PoliciesTab.tsx RetentionTab.tsx WebhooksTab.tsx WorkspacePicker.tsx
ScimCard.tsx SsoTabs.tsx
ai/6ai-panel.tsx ask-ai-submenu.tsx db-ai-dialog.tsx generate-page-dialog.tsx writing-actions-menu.tsx writing-diff.tsx
automations/5action-editor.tsx condition-editor.tsx lib.ts run-history.tsx trigger-editor.tsx
collaboration/6CursorLayer.tsx OfflineBanner.tsx PresenceBar.tsx README.md ReviewFailedDialog.tsx StatusChip.tsx
comments/3comment-panel.tsx comment-thread.tsx mention-input.tsx
database/27
__tests__/2cell-editor.test.tsx cell-renderer.test.tsx
AiExtractDialog.tsx AiFormulaDialog.tsx AiGenerateDialog.tsx board-view.tsx buttons-panel.tsx calendar-view.tsx cell-editor.tsx cell-renderer.tsx chart-view.tsx database-page-view.tsx database-toolbar.tsx database-view-renderer.tsx db-embedded.tsx filter-builder.tsx form-builder-view.tsx formula-editor.tsx gallery-view.tsx group-builder.tsx list-view.tsx property-editor.tsx record-header.tsx record-panel.tsx sort-builder.tsx table-view.tsx timeline-view.tsx
dialogs/7
__tests__/1move-to-dialog.test.tsx
create-dialog.tsx dialog.tsx file-permissions-dialog.tsx invite-dialog.tsx move-to-dialog.tsx share-dialog.tsx
editor/19
__tests__/3code-block-text.test.ts code-block.test.tsx expandable.test.tsx
block-card.tsx block-renderer.tsx block-save-queue.ts block-toolbar.tsx context-menu.tsx expandable.tsx history-popover.tsx inline-editor.tsx insert-dialog.tsx page-canvas.tsx page-spinner.tsx page-view.tsx selection-toolbar.tsx slash-menu.tsx template-dialog.tsx type-menu.tsx
files/4FileBlock.tsx FileBrowser.tsx FilePicker.tsx PreviewModal.tsx
form/4form-field.tsx form-renderer.tsx form-responses-view.tsx form-view.tsx
layout/8
__tests__/1page-tree-icon.test.tsx
command-palette.tsx factory-logo.tsx page-tree-node.tsx sidebar.tsx theme-provider.tsx topbar.tsx workspace-shell.tsx
mentions/3
__tests__/1mention-model.test.ts
mention-model.ts MentionPopup.tsx
search/3ask-ai-search.tsx results-list.tsx search-dialog.tsx
settings/10
api-keys/4ApiKeyCreateDialog.tsx ApiKeyRevealDialog.tsx ApiKeyTable.tsx ScopesBadge.tsx
webhooks/5CurlExamples.tsx DeliveryInspector.tsx EventExplorer.tsx OverviewTab.tsx WebhooksTab.tsx
settings-shell.tsx
ui/3confirm-dialog.tsx form.tsx toaster.tsx
hooks/21
__tests__/4realtime-frame-handler.test.ts use-blocks.test.ts use-database-options.test.ts use-workspace.test.tsx
realtime-frame-handler.ts use-ai-stream.ts use-auth.ts use-blocks.ts use-database.ts use-db-ai-stream.ts use-forms.ts use-members.ts use-mention-sources.ts use-notifications.ts use-offline-status.ts use-pages.ts use-presence-announcer.ts use-presence-toasts.ts use-presence.ts use-realtime.ts use-workspace.ts
lib/50
__tests__/21ai-database.test.ts ai-generate.test.ts ai-sse.test.ts ai-writing.test.ts api-keys.test.ts api-members.test.ts auth-refresh.test.ts auth-store.test.ts batch-queue-drop.test.ts block-content.test.ts clipboard-import.test.ts code-highlight.test.ts offline-status.test.ts presence-announcer.test.ts presence-color.test.ts presence-cursor.test.ts presence-emitter.test.ts realtime-url.test.ts search.test.ts utils.test.ts writing-diff.test.ts
files/7
__tests__/2picker.test.ts upload.test.ts
api.ts picker.ts upload-store.ts upload.ts use-file-uploads.ts
ai-database.ts ai.ts api-keys.ts api.ts auth-store.ts batch-queue.ts block-content.ts clipboard-import.ts code-highlight.ts constants.ts offline-status.ts org-admin.ts presence-announcer.ts presence-color.ts presence-cursor.ts presence-editor-views.ts presence-emitter.ts query-client.ts realtime.ts search.ts types.ts utils.ts
stores/8
__tests__/1block-patch-store.test.ts
block-patch-store.ts comment-pill-store.ts database-store.ts editor-store.ts offlineQueue.ts presence-store.ts ui-store.ts
instrumentation.ts middleware.ts
.env.example Dockerfile next-env.d.ts next.config.mjs package.json postcss.config.mjs README.md tailwind.config.ts tsconfig.json tsconfig.tsbuildinfo vitest.config.ts
worker/92
src/87
__tests__/13ai-embedding.test.ts ai-request.test.ts ai-usage-budget.test.ts audit-export.test.ts email-decision.test.ts encrypt.test.ts health-search-index-lag.test.ts mime.test.ts retention.test.ts s3-sign.test.ts search-index.test.ts smtp.test.ts webhook-sign.test.ts
health/1server.ts
lib/12
__tests__/1clamav.test.ts
clamav.ts encrypt.ts job-otel.ts mime.ts notify.ts prisma.ts redis-stream.ts redis.ts s3.ts sharp.ts smtp.ts
processors/56
automations/28
__tests__/3condition-evaluator.spec.ts executors.spec.ts run-pipeline.spec.ts
executors/17assign-user.ts call-integration.ts change-status.ts classify.ts create-notification.ts create-page.ts email-cap.ts helpers.ts integration-registry.ts permission.ts record-write.ts registry.ts run-ai-action.ts send-email.ts send-webhook.ts types.ts update-record.ts
condition-evaluator.ts dead-letter.ts log.ts matcher.ts pipeline.ts run-state.ts schedule.ts watchdog.ts
files/15
__tests__/4errors.test.ts scan-gate.test.ts status.test.ts temp.test.ts
steps/4finalize.ts metadata.ts scan.ts thumbnails.ts
cleanup.ts errors.ts generateThumbs.ts processor.ts status.ts temp.ts types.ts
search-index/4builders.ts grants.ts index.ts processor.ts
ai-embedding.ts ai.processor.ts audit-export.ts automations.processor.ts email-decision.processor.ts email.processor.ts maintenance.processor.ts retention.ts webhooks.processor.ts
config.ts events.ts index.ts logger.ts queues.ts
.env.example Dockerfile package.json README.md tsconfig.json
docs/73
api/17ai.md authentication.md automations.md blocks.md comments.md databases.md files.md forms.md openapi.yaml org-admin.md pages.md scim.md search.md sso.md versioning.md webhooks.md workspaces.md
architecture/10ai-architecture.md automation.md backend-architecture.md database-architecture.md frontend-architecture.md realtime-architecture.md search-architecture.md security-architecture.md storage.md system-architecture.md
database/2database-architecture.md redis-keys.md
development/8admin-guide.md coding-standards.md deployment.md development-guide.md files-pipeline.md perf-report.md performance.md testing.md
product/4functional-requirements.md non-functional-requirements.md product-requirements.md ux-requirements.md
requirements/1requirements-matrix.md
security/5audit-report.md files.md forms.md license-allowlist.md security-architecture.md
tasks/16phase-00-foundation.md phase-01-authentication.md phase-02-workspace.md phase-03-pages.md phase-04-editor.md phase-05-databases.md phase-06-relations.md phase-07-collaboration.md phase-08-search.md phase-09-files.md phase-10-forms.md phase-11-automation.md phase-12-ai.md phase-13-api.md phase-14-enterprise.md phase-15-production.md
ui/9ai.md automations.md design-system.md developers.md files.md forms.md org-admin.md scim.md sso.md
support-matrix.md
infrastructure/71
caddy/1Caddyfile
docker/6
init/101-extensions.sql
opensearch/1init-indices.json
.env.docker docker-compose.yml opensearch-init.sh README.md
kubernetes/40
observability/20
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1sync-sources.mjs
alertmanager.yaml grafana.yaml kustomization.yaml namespace.yaml node-exporter.yaml otel-collector.yaml prometheus.yaml
postgres/7configmap.yaml cronjob-backup.yaml cronjob-opensearch-snapshot.yaml pgbouncer.yaml replica.yaml statefulset.yaml walg-iam-policy.yaml
api.yaml configmap.yaml ingress.yaml kustomization.yaml namespace.yaml networkpolicy.yaml pdb.yaml README.md realtime.yaml redis.yaml secret.example.yaml web.yaml worker.yaml
nginx/2nginx.conf README.md
observability/14
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1dashboard-completeness.mjs
README.md
terraform/8compute.tf data.tf main.tf network.tf outputs.tf README.md security.tf variables.tf
packages/289
ai/24
src/20
providers/7anthropic.ts google.ts http.ts index.ts openai-compatible.ts rag.test.ts writing.test.ts
schemas/2database.ts generate.ts
adapters.test.ts config.test.ts config.ts gateway.test.ts gateway.ts index.ts tools.test.ts tools.ts types.ts usage.test.ts usage.ts
package.json README.md tsconfig.json vitest.config.ts
auth/11
src/7index.ts passwords.test.ts passwords.ts sessions.test.ts sessions.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
collaboration/17
src/13awareness.ts codec.test.ts codec.ts index.ts lww.test.ts lww.ts mapping.ts offline.ts outbox.test.ts outbox.ts provider.test.ts provider.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
config/7
src/4eslint.base.js index.ts prettier.base.json tsconfig.base.json
package.json README.md tsconfig.json
database-client/5
src/2index.test.ts index.ts
package.json README.md tsconfig.json
database-engine/33
src/29
filter/2filter.test.ts index.ts
formula/8evaluator.ts formula.test.ts functions.ts index.ts lexer.ts parser.ts types.ts validate.ts
query/6group.ts index.ts paginate.ts pivot.ts query.test.ts sort.ts
relations/2index.ts relations.test.ts
rollups/2index.ts rollups.test.ts
values/5dates.ts index.ts registry.ts types.ts values.test.ts
views/3buttons.ts index.ts views.test.ts
index.ts
package.json README.md tsconfig.json vitest.config.ts
editor/25
src/21
marks/1highlight.ts
nodes/2equation.ts mention.ts
autosave.test.ts autosave.ts clipboard.test.ts clipboard.ts codec.test.ts codec.ts context-menu.test.ts context-menu.ts diff.test.ts diff.ts factory.ts history.test.ts history.ts index.ts positions.ts slash.test.ts slash.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
observability/20
src/16
__tests__/8db-redis-metrics.test.ts metrics.test.ts otel.test.ts pino-otel.test.ts prometheus.test.ts resource.test.ts scrubber.test.ts traceparent.test.ts
index.ts metrics.ts otel.ts pino-otel.ts prometheus.ts resource.ts scrubber.ts traceparent.ts
package.json README.md tsconfig.json vitest.config.ts
permissions/11
src/7index.ts levels.test.ts levels.ts resolve.test.ts resolve.ts rules.test.ts rules.ts
package.json README.md tsconfig.json vitest.config.ts
search/22
src/18chunking.test.ts chunking.ts client.test.ts client.ts embeddings.test.ts embeddings.ts hybrid.test.ts hybrid.ts index.ts indexer.test.ts indexer.ts mapping.test.ts mapping.ts queries.test.ts queries.ts rrf.test.ts rrf.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
shared/61
src/57
dto/17ai.dto.ts auth.dto.test.ts auth.dto.ts block.dto.ts browser-safety.test.ts comment.dto.ts database.dto.test.ts database.dto.ts file.dto.ts formula-ast.ts index.ts notification.dto.ts page.dto.ts search.dto.ts share.dto.ts webhook.dto.ts workspace.dto.ts
events/8audit-catalog.test.ts audit-catalog.ts audit-csv.test.ts audit-csv.ts audit-scope.test.ts audit-scope.ts catalog.test.ts catalog.ts
realtime/6comment-events.ts frames.test.ts frames.ts notification-policy.ts notification-prefs.ts task-0094.test.ts
automation.test.ts automation.ts constants.ts crypto.test.ts crypto.ts env.ts errors.ts events.test.ts events.ts forms.test.ts forms.ts index.ts markdown.test.ts markdown.ts mentions.test.ts mentions.ts net.test.ts net.ts positions.test.ts positions.ts presence.test.ts presence.ts security-headers.ts webhook-delivery.test.ts webhook-delivery.ts webhook-events.ts
package.json README.md tsconfig.json vitest.config.ts
storage/16
src/12client.test.ts client.ts index.ts keys.test.ts keys.ts mime.test.ts mime.ts presign.test.ts presign.ts thumbnails.test.ts thumbnails.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
ui/37
src/33
components/19avatar.tsx badge.tsx button.tsx checkbox.tsx command.tsx context-menu.tsx dialog.tsx dropdown-menu.tsx empty-state.tsx index.ts popover.tsx separator.tsx skeleton.tsx slider.tsx spinner.tsx switch.tsx tabs.tsx toast.tsx tooltip.tsx
color.ts command-filter.test.ts command-filter.ts focus.test.ts focus.ts formatters.test.ts formatters.ts index.ts theme.test.ts theme.ts toast-store.test.ts toast-store.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
prisma/16
migrations/13
20260927000000_init/1migration.sql
20260928000000_search_documents_content_hash/1migration.sql
20260929000000_workspace_usage_storage_bytes/1migration.sql
20260930000000_form_submitted_notification_type/1migration.sql
20261001000000_ai_phase12/1migration.sql
20261002000000_webhooks_secret_encrypted_dlq/1migration.sql
20261002111436_webhook_delivery_latency/1migration.sql
20261002120000_org_admin_2fa_retention_unique/1migration.sql
20261002215124_audit_export/1migration.sql
20261003210000_perf_indexes_hot_paths/1migration.sql
20261004004500_perf_indexes_hot_paths_2/1migration.sql
migration_lock.toml README.md
schema.prisma seed.ts verify.ts
scripts/7backup-health.ts docs-completeness.mjs generate-openapi.mjs minio-init.ts opensearch-reindex.ts storage-orphans.ts verify-clean-clone.sh
tests/169
e2e/63
.auth/1user.json
fixtures/7
files/6clip.mp4 clip.wav clip.webm hero-1400.png note.docx note.txt
realtime.ts
mocks/2oidc-issuer.ts saml-idp.ts
playwright-report/3
data/2a6557bae9c2714a658968d4eb8534195c08ebda8.png e3092d07d00a16063e46385c02a559a9054fa847.webm
index.html
reports/1files-security.json
specs/34ai-database.spec.ts ai-generate.spec.ts ai-rag.spec.ts ai-security.spec.ts ai-writing.spec.ts ai.spec.ts api-keys.spec.ts appearance-theme.spec.ts auth.spec.ts automation.spec.ts collab-two-tab.spec.ts comments.spec.ts databases.spec.ts default-workspace-alias.spec.ts developers.spec.ts editor.spec.ts files.spec.ts final-flow.spec.ts forms.spec.ts oauth.spec.ts org-admin.spec.ts pages.spec.ts performance.spec.ts presence-cursors.spec.ts qa-paste-toggle.spec.ts realtime.spec.ts scim.spec.ts search-index-lag.spec.ts search.spec.ts sharing.spec.ts sso.spec.ts task-0094-comments-notifications.spec.ts webhooks.spec.ts workspace.spec.ts
test-results-final-flow/3
final-flow-Release-v1-0-fi-6c06b-w-state-carries-sequential--chromium-desktop/2test-failed-1.png trace.zip
.last-run.json
utils/4api.ts dnd.ts search-lag.ts wait.ts
.gitignore fixtures.ts global-setup.ts package.json playwright.config.ts playwright.final-flow.config.ts README.md tsconfig.json
fixtures/1
files/1make-sparse.ts
integration/60
ai-capability/2ai-app.capability.test.ts gateway.capability.test.ts
drill/2seed-drill.mjs verify-restore.mjs
fixtures/5
files/5clip.mp4 clip.wav doc.pdf hero-1400.png note.txt
helpers/3api.ts containers.ts seed.ts
realtime/11control-channel.test.ts convergence.test.ts flush-rebase-http.test.ts handshake.test.ts heartbeat-limits.test.ts lifecycle.test.ts presence.test.ts rebase.test.ts resume.test.ts rt-helpers.ts task-0094-fanout.test.ts
ai-chat.integration.test.ts ai-database.integration.test.ts ai-generate.integration.test.ts ai-rag.integration.test.ts ai-writing.integration.test.ts api-keys.integration.test.ts audit-completeness.integration.test.ts audit-export-s3.integration.test.ts auth.integration.test.ts automation-pipeline.integration.test.ts automations.integration.test.ts blocks.integration.test.ts cache-invalidation.integration.test.ts comments.integration.test.ts databases.integration.test.ts files-lifecycle.integration.test.ts files-pipeline.integration.test.ts files-security.integration.test.ts files.integration.test.ts forms-responses.integration.test.ts forms.integration.test.ts oauth.integration.test.ts openapi.contract.test.ts org-admin.integration.test.ts package.json pages.integration.test.ts permissions.integration.test.ts README.md replica-routing.integration.test.ts retention-engine.integration.test.ts scim.integration.test.ts search.integration.test.ts sso.integration.test.ts tsconfig.json vitest.config.ts webhooks.integration.test.ts workspace.integration.test.ts
performance/28
fixtures/1search-100k.ts
helpers/2seed-100k.mjs seed-1k-load.mjs
reports/7api-load-smoke.json database-load-smoke.json editor-local.json realtime-load.json search-full.json search-last.json search-reduced.json
api-load.js api.js archive.tar database-load.js database.js editor-local.mjs extract-100k-env.mjs package.json README.md realtime-1k.js realtime-load.js realtime.js run-realtime-load.sh search-search.k6.js search-summary.json seed-100k.mjs threshold-breach-demo.js verify-zero-op-loss.mjs
security/16
helpers/1app.ts
audit-report.test.ts auth-paths.test.ts deps.test.ts formula-fuzz.test.ts gitleaks-config.test.ts headers.test.ts images.test.ts oracle.ts package.json permission-fuzz.test.ts rate-limits.test.ts README.md tsconfig.json upload-abuse.test.ts vitest.config.ts
README.md
.dockerignore .env .env.bak-task0125 .env.example .eslintrc.json .gitignore .npmrc .prettierignore .prettierrc.json AGENTS.md ARCHITECTURE.md CHANGELOG.md CONTRIBUTING.md gitleaks.toml LICENSE package.json pnpm-lock.yaml pnpm-workspace.yaml PRODUCT-SPEC.md README.md ROADMAP.md SECURITY.md tsconfig.json turbo.json
files.mddocs/api/files.md← api31,377 B← Back to the app

Files API

⚠️ Draft above / implemented contract below. Sections 1–7 of this file predate the Phase-9 implementation and describe the proposed contract (POST /files, POST /files/:fileId/complete, raw key in the file object, GET /files?mimeType= …). The implemented API differs (endpoints, response envelope, error codes). The authoritative, implementation-verified contract is §8 (TASK-0103 addendum) and §9 (TASK-0104 addendum) below — written against apps/api/src/modules/files/* + apps/api/src/lib/authorize.ts and verified by the integration suites (tests/integration/files.integration.test.ts, files-lifecycle.integration.test.ts). The draft is kept for reference only; the conflicts are recorded in docs/tasks/phase-09-files.md (Phase 9 Notes).

File upload and download go through presigned object-storage URLs: the API mints a short-lived PUT URL for the client to upload to directly, then the client calls complete, which triggers the processing pipeline (→ virus scan → metadata extraction → thumbnail generation → file.uploaded event). Downloads are served via short-lived signed GET URLs.

Shared conventions (auth, error model, pagination, rate limits): authentication.md.

Permission note: uploading requires EDIT on the anchoring block/page (or the workspace root); downloading requires VIEW on the file's workspace plus file-level permission overrides when set. Public share links can expose files without auth (see pages sharing).

File object:

{
  "id": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4",
  "workspaceId": "wks_01J9ZKP5C9H7J9K1L3M5N7P9Q1",
  "blockId": "blk_01J9ZKQ6D3E5F7A9B1C3D5E7F9",
  "pageId": "pg_01J9ZKQ4F8H5I7J9L1M3N5P7Q9",
  "name": "architecture-v2.pdf",
  "key": "wks_01J9ZKP5C9H7J9K1L3M5N7P9Q1/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4",
  "url": "https://factory.example.com/files/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4",
  "size": 482113,
  "mimeType": "application/pdf",
  "status": "READY",
  "metadata": {
    "pageCount": 14,
    "hashSha256": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b"
  },
  "thumbnailKey": "wks_01J9ZKP5C9H7J9K1L3M5N7P9Q1/thumbs/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4.png",
  "thumbnailUrl": "https://factory.example.com/files/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4/thumb",
  "scanReport": { "engine": "clamav-1.3.2", "result": "clean", "finishedAt": "2025-01-02T10:21:04.113Z" },
  "uploadedBy": "usr_01J9ZKQX4V2M8N3P5R7T9W1Y3A",
  "version": 1,
  "createdAt": "2025-01-02T10:20:58.000Z",
  "updatedAt": "2025-01-02T10:21:05.410Z"
}

status lifecycle:

UPLOADING → SCANNING → READY
                ├─────► VIRUS_DETECTED   (file hidden, not downloadable)
                └─────► FAILED           (pipeline error)

1. Upload

1.1 POST /files

Create an upload session. Returns a presigned PUT URL (valid 15 minutes, single use) and the fileId. The file record exists immediately with status: "UPLOADING" — until complete succeeds it is not searchable or downloadable, and an abandoned upload is garbage-collected after 24 h.

Request:

{
  "name": "architecture-v2.pdf",
  "size": 482113,
  "mimeType": "application/pdf",
  "pageId": "pg_01J9ZKQ4F8H5I7J9L1M3N5P7Q9",
  "blockId": "blk_01J9ZKQ6D3E5F7A9B1C3D5E7F9",
  "fileId": null
}
Field Required Notes
name yes (1–255) Display name (extension is parsed from it if no mimeType).
size yes (int > 0) Must match the uploaded object exactly; mismatch on complete → 422.
mimeType yes Must pass the allow-list (§5 Limits); unknown/application/octet-stream is accepted but always scanned and blocked for web-served types.
pageId no Anchor page (for permission inheritance).
blockId no Anchor block (implies its page).
fileId no When set: this is a new version of an existing file — see §4 Versions.

201 Created

{
  "data": {
    "fileId": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4",
    "uploadUrl": "https://factory-media.example.com/bucket/wks_01J9ZKP5C9H7J9K1L3M5N7P9Q1%2Ffil_01J9ZKP3D8F0A2B4C6D8E0F2A4?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=…&X-Amz-Date=20250102T102058Z&X-Amz-Expires=900&X-Amz-Signature=…",
    "method": "PUT",
    "headers": { "Content-Type": "application/pdf" },
    "expiresAt": "2025-01-02T10:35:58.000Z",
    "maxBytes": 482113
  }
}

Upload with curl:

curl -s -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @architecture-v2.pdf

Errors

Status Code When
400 VALIDATION_ERROR Missing/bad fields.
403 FORBIDDEN No EDIT on anchor; MIME blocked by allow-list (details: [{field: "mimeType", issue: "mime_not_allowed"}]).
404 NOT_FOUND Anchor page/block invisible.
413 PAYLOAD_TOO_LARGE size exceeds per-type or plan limit (details: [{maxBytes}]).
409 CONFLICT fileId targets a file you cannot write, or the file was deleted.

Events emitted: none yet (the file is not complete). file.upload_session_created is audit-logged only.


1.2 POST /files/:fileId/complete

Call after the PUT succeeds (even if the PUT partially failed — the worker verifies object size/hash). Triggers the pipeline:

complete → SCANNING (ClamAV) → metadata (mime, dimensions, pages, duration, sha256)
         → thumbnail/preview → READY → file.uploaded event

Request (optional):

{
  "expectedSha256": "3a7bd3e2360a3d29eea436fcfb7e44c735d117c42d1c1835420b6b9942dd4f1b"
}

Providing expectedSha256 makes the worker verify content integrity; a mismatch yields status: "FAILED", error.code: "HASH_MISMATCH".

202 Accepted (pipeline is async — poll §1.3):

{
  "data": {
    "fileId": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4",
    "status": "SCANNING",
    "stage": "virus_scan",
    "updatedAt": "2025-01-02T10:21:00.215Z"
  }
}

Errors

Status Code When
400 VALIDATION_ERROR Object not present in storage yet (details: [{issue: "object_not_found"}]).
404 NOT_FOUND Unknown file id.
409 CONFLICT Upload session expired (> 24 h old) or already completed.
422 UNPROCESSABLE_ENTITY Size mismatch vs. declared size (details: [{issue: "size_mismatch", expected, actual}]).

Events emitted: file.uploaded when the pipeline reaches READY (async — not on this call's return).


1.3 GET /files/:fileId — Status & Metadata

200 OK (in flight):

{
  "data": {
    "id": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4",
    "name": "architecture-v2.pdf",
    "size": 482113,
    "mimeType": "application/pdf",
    "status": "SCANNING",
    "stage": "thumbnail",
    "progress": 0.8,
    "createdAt": "2025-01-02T10:20:58.000Z",
    "updatedAt": "2025-01-02T10:21:03.902Z"
  }
}

When READY: the full file object (top of this doc) with metadata, thumbnailUrl, scanReport. When VIRUS_DETECTED: scanReport.result: "infected", scanReport.threats: ["Win.Troj.Agent.gen"], and no url/thumbnailUrl (downloads return 403). When FAILED: error: { code: "PIPELINE_FAILED", message }.

Errors: 401 UNAUTHORIZED, 403 FORBIDDEN, 404 NOT_FOUND.


2. List Files

GET /files

Query params: cursor, limit (default 20, max 100), pageId, blockId, mimeType (e.g. image/*), name (substring), status (default: hides VIRUS_DETECTED/FAILED), createdBy. Ordered createdAt DESC.

200 OK — { data: [ <file objects> ], nextCursor }.

Errors: 401 UNAUTHORIZED, 403 FORBIDDEN, 400 VALIDATION_ERROR (bad mimeType pattern).


3. Download & Delete

3.1 GET /files/:fileId/download

Returns a 302 redirect to a short-lived (30 min, single use) signed URL. The File object's url is a stable proxy path that performs the same redirect, so client code can use either.

HTTP/1.1 302 Found
Location: https://factory-media.example.com/bucket/…?X-Amz-Expires=1800&X-Amz-Signature=…
Content-Disposition: attachment; filename="architecture-v2.pdf"

Errors: 401 UNAUTHORIZED, 403 FORBIDDEN (not READY yet, or VIRUS_DETECTED), 404 NOT_FOUND, 410 GONE (file deleted).


3.2 GET /files/:fileId/thumbnail

302 redirect to the generated thumbnail (or 404 if none was generated, e.g. for application/zip). Same auth as download.


3.3 DELETE /files/:fileId

Deletes the file record and schedules object-storage deletion (object + thumbnails + all versions). Blocks referencing the file keep their id but render a "file deleted" placeholder.

202 Accepted

{ "data": { "id": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4", "status": "deleting" } }

Errors: 403 FORBIDDEN, 404 NOT_FOUND.

Events emitted: file.deleted.


4. Versions

A file can be replaced in place (same fileId — blocks that reference it keep working): each replacement creates a FileVersion row and keeps the previous object in storage.

POST /files/:fileId/versions

Upload a new version: same two-step flow, but POST /files with fileId: "<existing>" instead of a new file.

  1. POST /files → { "fileId": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4", "name": "architecture-v3.pdf", "size": 512000, "mimeType": "application/pdf" } → 201 with a fresh uploadUrl (the response includes "isVersion": true, "baseVersion": 1).
  2. PUT the object.
  3. POST /files/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4/complete → 202.

When the new version reaches READY: the file's size/mimeType/ metadata/key update, version bumps to 2, and the old object is retained as file_versions[{id, version: 1, key, size, createdBy, createdAt}].

Constraints: same mimeType as the base file (replacing a PDF with an image → 422 UNPROCESSABLE_ENTITY, details: [{issue: "mime_change_not_allowed"}]); max 10 versions per file (oldest is pruned when exceeded); the name may change per version.

Events emitted: file.version_added (carries baseVersion, newVersion).

GET /files/:fileId/versions

200 OK

{
  "data": [
    { "id": "fvr_01J9ZKPC4E1F3A5B7C9D1E3F5A", "fileId": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4", "version": 2, "key": "…/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4-v2", "size": 512000, "createdBy": "usr_01J9ZKQ2V4W6X8Y0Z2A4B6C8D0", "createdAt": "2025-01-04T09:00:00.000Z" },
    { "id": "fvr_01J9ZKPC3D0E2F4A6B8C0D2E4A", "fileId": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4", "version": 1, "key": "…/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4", "size": 482113, "createdBy": "usr_01J9ZKQX4V2M8N3P5R7T9W1Y3A", "createdAt": "2025-01-02T10:20:58.000Z" }
  ]
}

POST /files/:fileId/versions/:version/restore

Swap the current object back to this version (creates another version entry copying the old object — history is append-only).

200 OK — the file object with the new current key/size/version.

Events emitted: file.version_restored.


5. Sizes & MIME Limits

Class Allowed MIME (examples) Per-file max Plan default
Images image/png, image/jpeg, image/gif, image/webp, image/svg+xml, image/avif 25 MB (SVG 2 MB) 10 GB workspace
Documents application/pdf, application/msword, application/vnd.openxmlformats-officedocument.*, text/plain, text/markdown, text/csv, application/json 50 MB same
Video video/mp4, video/webm 2 GB 50 GB workspace (team plan)
Audio audio/mpeg, audio/ogg, audio/wav, audio/mp4 200 MB 20 GB workspace
Archives application/zip, application/gzip, application/x-tar 1 GB 50 GB workspace
Other anything else (always scanned, no inline preview) 50 MB counted to workspace storage

6. curl Examples

# 1. Create upload session
curl -s -X POST https://factory.example.com/api/v1/files \
  -H "Authorization: Bearer $ACCESS_TOKEN" -H 'Content-Type: application/json' \
  -d '{"name":"architecture-v2.pdf","size":482113,"mimeType":"application/pdf","pageId":"pg_01J9ZKQ4F8H5I7J9L1M3N5P7Q9"}'
# → { "data": { "fileId": "fil_01J9ZKP3D8F0A2B4C6D8E0F2A4", "uploadUrl": "https://…" } }

# 2. PUT the bytes
curl -s -X PUT "$UPLOAD_URL" -H "Content-Type: application/pdf" --data-binary @architecture-v2.pdf

# 3. Complete → triggers pipeline
curl -s -X POST https://factory.example.com/api/v1/files/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4/complete \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# 4. Poll status until READY
curl -s https://factory.example.com/api/v1/files/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4 \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# 5. Download (follow redirect)
curl -sL https://factory.example.com/api/v1/files/fil_01J9ZKP3D8F0A2B4C6D8E0F2A4/download \
  -H "Authorization: Bearer $ACCESS_TOKEN" -o architecture-v2.pdf

7. Events Summary

Operation Domain event(s)
Upload session created file.upload_session_created (audit only — not a webhook event)
Pipeline reaches READY file.uploaded (payload: full file object; drives search index + thumbnail consumers)
New version ready file.version_added
Version restored file.version_restored
File deleted file.deleted

file.uploaded is one of the subscribable webhook events.


8. TASK-0103 addendum — the implemented contract (authoritative)

Written against apps/api/src/modules/files/ (TASK-0101/0102/0103) and verified by tests/integration/files.integration.test.ts. Where this section and the draft (§1–7) disagree, this section wins.

8.0 File object (wire)

The file record never exposes the raw storage key and never a stable url field. Content is reached only via signed URLs minted per read.

{
  "id": "fil_…", "workspaceId": "wks_…", "blockId": "blk_…" , "pageId": "pg_…",
  "name": "architecture-v2.pdf", "size": 482113, "mimeType": "application/pdf",
  "status": "READY",
  "metadata": {
    "sha256": "3a7b…", "pdf": { "pages": 14 },
    "dimensions": { "width": 1400, "height": 933, "orientation": "landscape" },
    "media": { "duration": 12.4, "codec": "h264" },
    "pipeline": { "error": null }
  },
  "thumbnailKey": "…/thumbs/…", "thumbnailUrl": "https://…signed GET, 15 min…",
  "scanReport": { "engine": "clamav", "result": "clean" },
  "uploadedBy": "usr_…", "version": 1,
  "createdAt": "…", "updatedAt": "…"
}

status lifecycle: UPLOADING → SCANNING → READY | VIRUS_DETECTED | FAILED. metadata keys are optional and kind-specific: media (video/audio: duration, codec), pdf (pages), dimensions (image/video), pipeline.error (set on FAILED).

8.1 POST /files/uploads — create an upload session

Body: { name, mimeType, size, pageId?, blockId?, replaceFileId? } (replaceFileId is the TASK-0104 version path). The workspace resolves from the x-factory-workspace header, else claims.workspaces[0].

201 → { "upload": { "file": <file record, status UPLOADING>, "uploadUrl", "method": "PUT", "headers": { "Content-Type": <validated mime> }, "expiresAt" } } (the session expires in 900 s). The client must send the signed headers exactly on the PUT.

8.2 PUT /files/uploads/:id/complete

After the PUT to uploadUrl succeeds. Verifies the object (HEAD size vs declared), sniffs magic bytes (mismatch → FAILED MIME_MISMATCH, object deleted), then flips SCANNING + enqueues the worker pipeline.

8.3 POST /files/uploads/:id/cancel

Deletes the partial object + row. 200 → { deleted: true }.

8.4 GET /files — list (server-side permission-filtered, keyset-paginated)

Query params (validated, dto.ts ListFilesQuery):

Param Values Default
scope workspace | page workspace
pageId required with scope=page —
kind image | video | audio | pdf | doc | other all
status UPLOADING | SCANNING | READY | VIRUS_DETECTED | FAILED all
search substring on name (≤ 200) —
sort updated | name | size | date updated
cursor opaque (base64url {id, sortValue}) first page
limit 1–100 50

200 → { "data": [ <file record> + { uploadedByName, pageName, versionCount } ], "nextCursor" }.

8.5 GET /files/:id — detail + fresh signed URLs

200 → file record + { downloadUrl, previewUrl, expiresAt, versions[] }.

8.6 GET /files/:id/download / /files/:id/preview — 302 redirects

Server-side, permission-checked, audit-logged (file.downloaded). download → attachment; preview → inline. Short-lived signed Location. 403 when not READY or VIRUS_DETECTED; 404 unknown; 410 deleted.

8.7 PATCH /files/:id — rename

Body { name } (1–512). Renames the record only (storage key is immutably built from fileId). 200 → file record.

8.8 Error codes (implemented)

Code Status Where
FILE_TOO_LARGE 413 session create (plan limit)
QUOTA_EXCEEDED 413 session create (storage)
UPLOAD_EXPIRED 410 complete (expired / object missing)
UPLOAD_INCOMPLETE 422 complete (size mismatch)
MIME_MISMATCH 415 complete (magic-byte sniff vs declared)
FORBIDDEN 403 no VIEW/EDIT; blocked MIME at session

8.9 Upload-session curl (implemented)

# 1. session
curl -s -X POST $API/files/uploads -H "Authorization: Bearer $T" \
  -H "x-factory-workspace: $WS" -H 'Content-Type: application/json' \
  -d '{"name":"a.pdf","size":482113,"mimeType":"application/pdf"}'
# → { "upload": { "file": {...}, "uploadUrl": "...", "method":"PUT", "headers":{"Content-Type":"application/pdf"}, "expiresAt":"..." } }

# 2. PUT the bytes (send the signed headers EXACTLY)
curl -s -X PUT "$UPLOAD_URL" -H 'Content-Type: application/pdf' --data-binary @a.pdf

# 3. complete → SCANNING + worker pipeline
curl -s -X PUT $API/files/uploads/$FILE_ID/complete -H "Authorization: Bearer $T"

# 4. poll the detail (fresh signed URLs appear at READY)
curl -s $API/files/$FILE_ID -H "Authorization: Bearer $T"

# 5. download (302, audited) / list
curl -sL $API/files/$FILE_ID/download -H "Authorization: Bearer $T" -o a.pdf
curl -s "$API/files?scope=workspace&sort=updated&limit=50" -H "Authorization: Bearer $T"

Web UI client: apps/web/src/lib/files/api.ts (the filesApi) is the single source of truth for these endpoints; the legacy fileApi in lib/api.ts was removed (its endpoints 404'd) and uploadFile is now a thin adapter over lib/files/upload.ts. UI behavior is documented in ../ui/files.md.


9. TASK-0104 addendum — versions, trash, permissions, quota (authoritative)

Written against apps/api/src/modules/files/ + apps/api/src/lib/authorize.ts (TASK-0104) and verified by tests/integration/files-lifecycle.integration. test.ts. Where this section and the draft (§1–7, incl. its §3.3/§4) disagree, this section wins.

Supersedes the draft's versions/trash story. The draft §4 described POST /files/:fileId/versions + POST …/versions/:version/restore and §3.3 a DELETE → 202 {status:"deleting"} — none of those endpoints existed. TASK-0104 implements the spec's real contract: a version is a re-upload with replaceFileId, rollback is a key swap, delete is a soft trash with a separate ?purge=true hard-delete.

9.0 Route map (all under the /files controller; files/trash is

declared BEFORE files/:id so it is not swallowed by :id)

Method Path Purpose Min level
GET files list (see §8.4) VIEW
GET files/trash workspace trash VIEW
GET files/:id detail + signed URLs (§8.5) VIEW
GET files/:id/download / preview 302 signed (§8.6) VIEW
GET files/:id/versions version list (§9.1) VIEW
POST files/:id/rollback make version n current (§9.2) EDIT
POST files/:id/versions/prune delete old versions (§9.3) EDIT
DELETE files/:id trash (soft) (§9.4) EDIT
DELETE files/:id?purge=true purge (hard) (§9.5) FULL_ACCESS or ws Admin/Owner
POST files/:id/restore restore from trash (§9.6) EDIT
GET files/:id/permissions list explicit grants (§9.7) EDIT
PUT files/:id/permissions replace explicit grants (§9.7) EDIT
PATCH files/:id rename (§8.7) EDIT

9.1 Versions

A version is created by re-uploading with replaceFileId. POST /files/uploads body { name, mimeType, size, replaceFileId: "<fileId>" } → 201, same session shape as §8.1, but the session is anchored to the EXISTING file (its page/block are inherited; the request's anchors are ignored). On complete, the worker finalizes a FileVersion n+1 and the file re-enters SCANNING; when it reaches READY, the file's key/size/ mimeType/version re-point to the new object. The previous object is RETAINED (its FileVersion row stays until §9.3 or §9.5).

GET /files/:id/versions → bare array, ascending:

[
  { "id": "fvr_…", "version": 1, "size": 482113, "current": false,
    "createdBy": "usr_…", "createdByName": "Ada", "createdAt": "…" },
  { "id": "fvr_…", "version": 2, "size": 512000, "current": true,
    "createdBy": "usr_…", "createdByName": "Ada", "createdAt": "…" }
]

The raw storage key is never returned (spec §Security — signed URLs only). current is the version whose key equals the file's current key.

POST /files/:id/rollback body { version: <int> } → the file record. Key swap, not a copy: File.key/size are re-pointed at the target version's existing object (no new object, no re-scan). Audit file.rolled_back.

POST /files/:id/versions/prune → { "pruned": <int> }. Deletes every NON-CURRENT version's object + FileVersion row (keeps the current one), and decrements Workspace.usageStorageBytes by the pruned bytes. Audit file.versions_pruned { pruned }.

9.2 Trash → restore → purge

DELETE /files/:id → { "trashed": true, "deletedAt": "…" }. Soft delete: deletedAt is set; the object + version rows are RETAINED. The file leaves GET /files and the search index (the worker removes the doc on the file.deleted event, target < 30 s). Audit file.deleted; event file.deleted. Re-trashing a trashed file → 409 CONFLICT (already_trashed).

GET /files/trash → bare array of the workspace's trashed files (file record + { deletedAt, uploadedByName }, most-recent first, ≤ 200).

POST /files/:id/restore → the file record. Clears deletedAt; the file returns to GET /files + the search index (re-emits file.uploaded with restored: true). Audit file.restored. Storage counter is unchanged (the bytes were retained while trashed — no delta).

DELETE /files/:id?purge=true → { "purged": true }. Hard delete: every version object + the thumbnail + the File row + all FileVersion rows are removed (one transaction) and usageStorageBytes is decremented by the file's total bytes. Requires FULL_ACCESS on the file (explicit grant) OR a workspace Admin/Owner role; otherwise 403 + audit file.purge_denied. Audit file.purged { versions }. A purged file is gone from the bucket (pnpm storage:orphans --file <id> reports 0 for it).

9.3 File-level permissions (explicit grants + explicit deny)

Permissions resolve to an effective level per (actor, file): the most specific of the actor's explicit file grants; an explicit NONE grant is a hard deny that overrides any inherited level (file → block → page → team → workspace). No grant → inherit from the page/block/workspace (a non-member still sees nothing unless granted).

GET /files/:id/permissions → bare array of the file's explicit grants:

[ { "id": "perm_…", "granteeType": "user", "granteeId": "usr_…",
    "level": "NONE", "inheritsFrom": null,
    "granteeName": "Eve", "granteeEmail": "eve@…" } ]

granteeType ∈ user | team | role (role ⇒ granteeId is a role name: OWNER/ADMIN/MEMBER/GUEST).

PUT /files/:id/permissions body { "grants": [ { "granteeType", "granteeId", "level" } ] } → the updated bare array. This is a REPLACE (send the full desired set; omitted grants are deleted). level may be NONE (explicit deny). Audit file.permission_changed; event permission.changed (drives the search re-index so the doc is now visible/hidden to the grantee).

Access to read/download/preview a file requires effective level ≥ VIEW; a denial → 403 + audit file.download_denied { actor, ip, path }. A VIRUS_DETECTED file → 403 FILE_QUARANTINED on download/preview (the detail endpoint still returns the record with no signed URLs, §8.5).

9.4 Storage quota

Workspace.usageStorageBytes (BigInt, the event counter) is maintained transactionally with the byte-moving operation:

Operation Counter delta
complete (upload / new version) + size (bytes land in the bucket at complete)
versions/prune − Σ pruned bytes
purge − Σ all version bytes
restore ± 0 (bytes were retained)

The upload-session pre-check reads the counter O(1) (advisory-fast); only at the denial boundary is the authoritative ledger sum re-checked, and only a stale-LOW counter (drift > 1 GB) is corrected before a 413 QUOTA_EXCEEDED (naming used/limit in the message + details: { maxBytes, usedBytes, remainingBytes }). The workspace usage endpoint (GET /workspaces/:id/usage) returns the counter as usage.storageBytes (a decimal string) and the effective plan quota as limits.maxStorageBytes (min(plan quota, usageLimits override)).

9.5 Audit events (TASK-0104)

Action When Key metadata
file.version_created a replaceFileId upload completes version, name, size
file.rolled_back POST /:id/rollback version
file.versions_pruned POST /:id/versions/prune pruned
file.deleted DELETE /:id (soft) name, size, key
file.restored POST /:id/restore name, size, key
file.purge_denied purge without FULL_ACCESS/Admin name, key
file.purged DELETE /:id?purge=true key, thumbPrefix, versions
file.permission_changed PUT /:id/permissions name, grants
file.download_denied download/preview permission/quarantine fail actor, ip, path
file.orphan_purged worker orphan sweep deletes a stray object key, reason

9.6 Events (TASK-0104)

Operation Domain event
New version completes file.uploaded (carries version) — drives the search re-index
File trashed file.deleted (worker removes the search doc)
File restored file.uploaded (restored: true) — worker re-indexes
File renamed file.updated (carries name) — worker re-upserts the doc (title converges)
File permissions changed permission.changed — worker enqueues a workspace resync (runReindex)

file.updated is a convergence trigger (re-index on rename), NOT a webhook-catalog event — it is not a user-facing notification.

file.deleted / file.uploaded / file.updated / permission.changed are stream-internal (search-index consumers); the purge path deletes rows directly (the 15-min contentHash search reconcile backstops any doc whose row was removed). Note (rule-29 conflict): search visibility for a file doc is WORKSPACE-scoped (the workspace floor) — file-level Permission grants are enforced at read time, not search time, so permission.changed does not change whether a doc is returned to a grantee; it re-runs the resync so any converged metadata (e.g. workspace/teamspace/page-scoped changes) is current.

9.7 Orphan sweep

Objects under files/ with no matching File/FileVersion row are orphans (crashed finalizes, partial transactions). The daily maintenance:orphans worker job (7-day grace on the object's Last-Modified) and the manual pnpm storage:orphans CLI (dry-run by default; --purge deletes; --file <id> scopes) share apps/worker/src/processors/files/cleanup.ts. Each deletion is audited file.orphan_purged.