.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
security-architecture.mddocs/security/security-architecture.md← security59,567 B← Back to the app

Security Architecture — AI Software Factory

Deep security design for the platform. Requirements: PS§24 (product-requirements.md PR-112…120, PR-158), NFR-SE1…SE8 (non-functional-requirements.md §4); coding-level rules in coding-standards.md §10; schema backing in database-architecture.md (identity, permissions, audit, api_keys, webhooks tables); release gates in testing.md §7 and deployment.md §6.

Prime directive (AGENTS.md rule 19): never trust frontend authorization. The UI enforces permissions for speed; the API enforces them for truth. Every statement below that says "the client may X" is UX; the enforceable fact is always the server-side rule.


1. Security context & trust boundaries

 UNTRUSTED                            PARTIALLY TRUSTED                    TRUSTED
─────────────                         ──────────────────                   ───────
 browsers (any JS)      ──TLS──►      nginx / ingress                      api / worker /
 client JS bundles                              │                           realtime / web (SSR)
 uploaded file bytes      ─────────►            ▼                           ┌──────────────────┐
 import files (md/html)   ────────►      api (Zod at the door)              │ Postgres (PG)    │
 webhook payloads (in)    ────────►      services (authz engine)            │ Redis            │
 AI model output          ─────────►      repositories (tenant scope)        │ S3/MinIO        │
 email links (tokens)     ─────────►                                 │ OpenSearch       │
                                                                     └──────────────────┘

Boundaries and their controls:

Boundary Threat Control
Internet → app eavesdrop, tamper, clickjacking, downgrade TLS 1.2+ (1.3 preferred), HSTS, X-Frame-Options: DENY, CSP (§7)
Browser → API injection, CSRF, brute force, abuse Zod validation (§8), CSRF strategy (§10.2), rate limits (§10.1), auth (§2)
API → domain authorization bypass, cross-tenant leak permission engine on every op (§4), tenant-scoped repositories (database-architecture §10.1), cross-tenant test suite (CI gate)
API → stores SQLi, unsafe deserialization Prisma parameterization (coding-standards S3), typed JSONB via Zod (no JSON.parse into executable anything), no eval anywhere (S2)
Client → S3 object guessing, unauthorized read private buckets, presigned URLs with scoped keys, per-file permission re-check on download (§9)
Platform → external SSRF, secret exfil, open redirect URL validation + private-IP block (§10.4), secrets by reference (§11), redirect allow-list
AI → platform prompt injection → data exfil / destructive tool calls tool calls validated against Zod + executed with the user's permissions, no raw SQL, output is data (§8.5, §13 T9)
Operator → data insider, leak audit on every sensitive op (§12), secrets out of code (§11), least-privilege service accounts, admin actions audited

2. Authentication lifecycle (AuthN)

2.1 Token model

Token Format Lifetime Storage Rotation
Access JWT (HS256, JWT_SECRET), claims {sub, ws?, role, jti, iat, exp} 15 min (JWT_EXPIRES_IN) client memory (TanStack Query client); never localStorage none — short-lived; re-issued via refresh
Refresh opaque 256-bit random (URL-safe) 30 days (REFRESH_TOKEN_TTL_DAYS) HttpOnly; Secure; SameSite=Strict; Path=/api cookie factory_rt (and mobile-safe fallback: returned in body for non-browser clients, hashed at rest in refresh_tokens) every use (rotation, §2.3)
Session opaque id (groups a login episode) sliding 30 d sessions table (hashed) revoked via UI

Why this shape: the access JWT is bearer-but-short (15 min exposure window); the refresh credential is long but (a) cookie-scoped (no XSS read for the cookie-auth path — HttpOnly), (b) hashed at rest (a DB leak does not yield usable tokens), (c) single-use per rotation (theft is detected and the family burned, §2.3).

2.2 Sequence: register → verify → login → use

sequenceDiagram
    participant U as Browser
    participant A as API (auth module)
    participant P as Postgres
    participant M as Mail worker
    U->>A: POST /auth/register {email, password, name}
    A->>A: Zod validation (shared schema); bcrypt(cost=12)
    A->>P: INSERT user(UNVERIFIED) + personal workspace + OWNER member (1 txn)
    A->>M: enqueue email:verify (token, 24h, hashed in email_verifications)
    A-->>U: 201 (unverified session pinned with verify banner)
    Note over U,M: user clicks link
    U->>A: POST /auth/verify {token}
    A->>P: single-use consume; user → VERIFIED
    U->>A: POST /auth/login {email, password}
    A->>A: constant-time bcrypt compare; check status (SUSPENDED→403)
    A->>P: session row + refresh token row (tokenHash) + audit user.login.success
    A-->>U: 200 {accessToken(15m)} + Set-Cookie factory_rt (HttpOnly, 30d)
    loop every request
        U->>A: Authorization: Bearer <accessToken>
        A->>A: verify JWT (signature, exp); load ctx (user, ws membership, role)
    end
    U->>A: POST /auth/refresh (cookie)
    A->>P: lookup tokenHash; must be ACTIVE & unexpired
    A->>P: mark revoked + replacedBy=new; INSERT new token (same family)
    A-->>U: new access JWT + new cookie

Rules:

2.3 Refresh rotation & replay (FR-PR-003/004)

present(token T):
  row = lookup(tokenHash(T))
  if !row:            → 401 (unknown token) → client: clear cookie, redirect to login
  if row.revokedAt:   → REPLAY: revoke ALL tokens of row's family (UPDATE
                       refresh_tokens SET revokedAt=now() WHERE family = row.family)
                       audit auth.refresh.replayed {userId, ip, ua}
                       → 401 (client clears cookie; user is signed out everywhere)
  if row.expiresAt < now: → 401 (expired; client re-login)
  new = create token (same family, +30d from ORIGINAL issue)
  mark row revoked, replacedBy = new
  issue new access JWT (new jti)

The replay branch is the theft detector: a stolen token used after the victim's legitimate rotation invalidates the thief's copy AND the victim's current family (the victim re-logins; the thief gets nothing). This is deliberately over-eager — one false positive costs a re-login; the alternative (undetected account takeover) is the incident.

2.4 Session revocation (FR-PR-005)

2.5 Enterprise SSO (PR-011/012 — Phase 14)

OIDC (auth code + PKCE, state + nonce, id_token signature verified against JWKS, iss/aud pinned) and SAML 2.0 (assertion signature, NameID → sso_identities.externalId, ACS endpoint). Identity linking: email match → link; else JIT-create a passwordless verified user. enforce=true → password login for the org returns 403 SSO_REQUIRED. SCIM deactivate → SUSPENDED + full revocation (AN3). All SSO logins audited (sso.login.*). The trust model and threat catalog below cover the SAML/OIDC handshake; SCIM is TASK-0132.

Trust boundaries. The SP (this platform) places NO trust in: the IdP's transport, the assertion/ID-token content, or the browser mid- handshake (the ACS is session-less by design — the signature IS the authentication). The SP pins trust to exactly three things: (a) the org-configured IdP signing certificate (SAML) or the IdP's JWKS (OIDC), (b) the Audience = the SP entityID, (c) the single-use Response ID / nonce.

Threat → control (SAML).

Threat Control
Forged / tampered assertion Enveloped RSA-SHA256 signature over the Assertion (exc-C14N); bad_signature
Assertion from a different IdP / attacker's IdP Certificate binding — the KeyInfo X509Certificate must equal the org's current OR previous configured cert, checked before signature verification, so a foreign key is never even evaluated (cert_mismatch)
Replayed valid assertion (capture the form-POST) Response ID single-use for 10 min (Redis SET NX EX 600); replayed. A forged assertion can't consume a replay slot (the replay check runs after full validation)
Assertion issued for another SP Audience must include the SP entityID (audience_mismatch) — the entityID embeds the org slug, so cross-tenant reuse fails
Assertion delivered to the wrong endpoint Destination === the ACS URL (destination_mismatch)
Expired session Conditions.NotOnOrAfter with ±60 s skew (expired)
Wrong outcome Status must be Success (status_not_success)
Issuer spoofing Optional idpEntityId pin (issuer_mismatch)
XML bombs / XXE <!DOCTYPE + external entities rejected before parsing (doctype_forbidden)
Malformed / non-XML body Parse-failure reason before any crypto (malformed_xml)

Threat → control (OIDC).

Threat Control
Authorization-code theft (open redirector) PKCE S256 — the exchange requires the original verifier; mismatch → token_exchange_failed
state CSRF / response injection state is 128-bit, Redis-bound, single-use, TTL 10 min (state_invalid / state_replayed)
Forged / algorithm-confused id_token JWKS-fetched RSA key; alg must be RS256 with a matching kid — none is refused (unsupported_alg)
Token from another tenant/issuer iss pinned to the configured issuer; aud must include the client id
Stale / future token exp, nbf (+60 s skew)
nonce reuse across sessions nonce must equal the per-authorization value (nonce_mismatch)

Certificate rotation (SAML). PATCH /orgs/:id/sso with a new certificate moves current → previous; the validator accepts either, so an IdP key rollover has a grace window without a login blackout. The cert PEM is AES-256-GCM encrypted at rest (org secrets store — same pattern as TASK-0130 webhook secrets); the admin API returns only certificateConfigured: true (reveal-once).

Enforce mode + lockout. ssoEnforced is mirrored from SsoProvider.enforce into Organization.policy (single source of truth plus a queryable mirror). Password login for a member of an enforcing org is refused before bcrypt; unknown emails are rejected the same way (no enumeration timing). Lockout runbook: the SSO path is never blocked by enforcement. An owner whose IdP is down either (a) signs in via SSO once the IdP recovers (/sso/start/<slug>), or (b) turns enforce off with a pre-enforcement session. There is deliberately no elevated service-account credential — the enforcement state is plain data, so recovery needs no second trust path (tested in tests/e2e/specs/sso.spec.ts, "enforce mode…").

Discovery (anti-enumeration). GET /auth/sso/options?email=… reveals only that SSO exists for orgs the email already belongs to; an unknown (or not-yet-JIT-provisioned) email gets { orgs: [] }. First-time SSO users enter via the org-provided link /sso/start/<slug>, public by design (it only initiates a redirect to the org's own IdP — the IdP's login page is the first user-facing surface).

JIT provisioning boundary. A JIT/linked user gets membership ONLY in the SSO org: role = policy.ssoDefaultRole capped at MEMBER (owner / admin are never granted by SSO, existing roles are never demoted), auto-joined to the org's non-personal workspaces; other orgs are untouched. Assertion emails are treated as verified — the IdP vouches for the address, which is exactly why discovery stays minimal-info.

3. Authorization engine (AuthZ)

3.1 The two layers

Layer 1 — RBAC (workspace roles): the base capability matrix (product-requirements §4) maps each role to a base level per resource kind:

Resource kind Owner Admin Member Guest
workspace (settings/members) FULL_ACCESS FULL_ACCESS NONE NONE
teamspace (create/manage) FULL_ACCESS FULL_ACCESS NONE NONE
page / database (content) FULL_ACCESS FULL_ACCESS EDIT NONE
file (read) FULL_ACCESS FULL_ACCESS VIEW NONE
audit log (view) FULL_ACCESS VIEW NONE NONE
api keys / webhooks (manage) FULL_ACCESS FULL_ACCESS NONE NONE

Layer 2 — resource grants (permissions table): explicit (grantee, resource, level) rows, where grantee ∈ {user, team, role} and level ∈ {NONE, VIEW, COMMENT, EDIT, FULL_ACCESS}. Grants live at: workspace, teamspace, page, database, record, file.

3.2 Level semantics

Level Grants
NONE nothing — and as an explicit grant, it is a deny override (beats inheritance at its level, §3.3)
VIEW read the resource (page/blocks/properties/records as rendered to viewers)
COMMENT VIEW + create/reply/resolve comments, mention
EDIT COMMENT + create/modify/delete content (blocks, records, properties values, views)
FULL_ACCESS EDIT + manage permissions on the resource, delete, share, export, manage database schema

Required level per operation (the guard's input): read = VIEW; comment = COMMENT; any content write = EDIT; schema change / grant change / delete / share / export = FULL_ACCESS (Owner/Admin baseline may lower the baseline requirement per §3.1 but never bypass an explicit NONE — deny always wins).

3.3 Evaluation algorithm

can(ctx, resource, requiredLevel) -> bool:
  # ctx: {userId, workspaceRole, teamIds, workspaceId}
  # resource: {kind, id, workspaceId, pagePath?}  (pagePath = [root..resource] for pages)

  base = roleBaseLevel(ctx.workspaceRole, resource.kind)     # §3.1 table

  grants = collectGrants(ctx, resource)
    # rows where granteeType/granteeId ∈ {("user", uid), ("team", t) for t in teamIds,
    #                                       ("role", ctx.workspaceRole)}
    # AND resource matches:
    #   - exact (resourceType, resourceId) = resource, OR
    #   - (for pages) an ANCESTOR in pagePath, OR
    #   - (for records/files) the parent page/database ancestor chain, OR
    #   - workspace/team grants (kind ∈ {workspace, teamspace}) apply to ALL
    #     content inside that team/workspace unless a closer grant says otherwise

  explicit = max(level of grants matching this resource exactly)   # NONE counts as "present"
  inherited = max(level of ancestor grants)                        # walk DOWN the page tree

  if explicit is present:
     effective = explicit            # an explicit grant on the resource wins (including NONE = deny)
  else:
     effective = max(base, inherited)

  # deny-override from a closer ancestor beats a farther ancestor:
  #   the walk keeps the NEAREST ancestor grant that is NONE → effective = NONE

  return effective >= requiredLevel   # NONE < VIEW < COMMENT < EDIT < FULL_ACCESS

Worked examples (all unit-tested in the decision matrix — testing.md §3.2):

  1. Member, no grants, any page → base( Member, page ) = EDIT → can edit.
  2. Guest, explicit VIEW grant on page P → effective VIEW → can read P, cannot comment.
  3. Member with an explicit NONE on team T → everything under T: effective NONE → the member is removed from their own team's space (deny beats the role baseline). This is the "lock a team away from most members" tool.
  4. Page Q under page P (P has explicit EDIT for user U); Q has no grant → Q inherits EDIT from P.
  5. Q under P (P: EDIT for U), Q has explicit VIEW for U → Q's explicit VIEW wins → U can read Q but not edit it (explicit narrows inheritance).
  6. Record R in database D under page P; grant EDIT on P → R inherits EDIT (records inherit through their page; the database's own grants also apply — max of the two paths).

3.4 Guard integration (NestJS)

// controller
@Post()
@RequirePermission("page", "EDIT")          // declares (resourceType, requiredLevel)
async update(@Body() dto: UpdatePageDto, @CurrentUser() ctx: RequestContext) {
  return this.pagesService.update(ctx, dto);  // service re-checks with the RESOLVED id
}

3.5 Decision cache & invalidation

4. Multi-tenancy (the isolation contract)

  1. Scope at the repository (database-architecture §10.1): no tenant-scoped table is queryable by bare id; every read is findFirst({where: {id, workspaceId}}). The cross-tenant matrix (50 random cross-workspace pairs, 0 leaks) is a CI gate on every PR (NFR-P1).
  2. No client-controlled tenant switch: the workspace is bound to the access token (ws claim) or resolved from the route (/w/<slug> → membership check) — a user can only act in workspaces they belong to.
  3. Search isolation at query time (permission snapshot per document, functional §11) — a leaked index document cannot be read by the query filter; the snapshot is refreshed on every permission change event.
  4. Realtime isolation: rooms are (workspace, docId); join validates token workspace == room workspace AND can(view) on the doc (edit level required to send updates — VIEW peers receive but their updates are rejected with a typed frame error).
  5. Queue isolation: every domain event payload carries workspaceId; consumers re-scope on the row they touch (worker code uses the same tenant-scoped repositories — the worker is not a back door).
  6. File isolation: object keys embed the workspace id (<ws>/<file>/<version>); the presigner only mints URLs for keys it verified belong to the caller's workspace (per-file check, §9).

5. Transport & headers

TLS: 1.2+ (1.3 preferred) at the edge (nginx §deployment.5); internal service traffic in k8s is plaintext-on-trusted-network (mTLS is a Phase-15 enterprise option, not v1). The exact header set (nginx + API agree; the API sets CSP, nginx sets the rest — see the split rationale in deployment §5):

Header Value Set by
Strict-Transport-Security max-age=31536000; includeSubDomains nginx (always)
X-Content-Type-Options nosniff nginx
X-Frame-Options DENY nginx (no embeddable surface — public shares are full pages)
Referrer-Policy strict-origin-when-cross-origin nginx
Permissions-Policy camera=(), microphone=(), geolocation=() nginx
Content-Security-Policy default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: <s3-host>; connect-src 'self' ws: wss: <s3-host>; frame-src https://www.youtube.com https://player.vimeo.com https://*.loom.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self' API/Next (per-route; share/public routes drop style-src 'unsafe-inline' is kept for the editor's inline styles — inline scripts are never allowed; unsafe-inline for style is the documented exception: the editor sets per-block color inline, and a nonce-based policy is a Phase-15 refinement)
Cache-Control (API JSON) no-store (authed), public, max-age=31536000, immutable (hashed static assets) API / nginx
X-Request-Id echo/generate (correlation, coding-standards L2) API
Set-Cookie per §2.1 (refresh: HttpOnly; Secure; SameSite=Strict; Path=/api; CSRF: Secure; SameSite=Lax; Path=/; session display cookies: HttpOnly where possible) API
Cross-Origin-Opener-Policy / Cross-Origin-Embedder-Policy not set (they break embeds + some CDNs; revisited if we ship SharedArrayBuffer features) —

CORS: single allow-list (WEB_URL + the API docs origin), credentials allowed only for that origin, methods limited per route, Vary: Origin. No * anywhere. WS origin check: the gateway validates the Origin header against WEB_URL on connect (rejects cross-origin sockets before auth).

6. Cookies, sessions & CSRF

v1 cookie flags (TASK-0134 — single source). Every Set-Cookie in the API is emitted from one place, apps/api/src/common/cookies.ts: the refresh cookie factory_refresh carries Path=/; HttpOnly; SameSite=Lax (+ Secure in production only, since dev runs plain-HTTP on :4000) plus a Max-Age. tests/security/auth-paths.test.ts asserts these flags on every Set-Cookie in the live auth flow (the "single source" is what makes that assertion meaningful). The factory_rt/factory_csrf names in the table above are the design target; v1 uses factory_refresh.

7. Validation & sanitization

7.1 The Zod pipeline (coding-standards §5)

Every untrusted byte passes a named Zod schema at the boundary before any logic runs:

Boundary Schema home Notes
HTTP bodies/params/query packages/shared/schemas/*.ts (per endpoint) .strict() on all external input; 400 with field map on failure
WS frames packages/shared/schemas/realtime.ts (frame union) bad frame → typed error frame + connection counter; 10 bad frames = disconnect
Queue payloads per-event schema in shared/schemas/events.ts a malformed event is dead-lettered, never partially applied
Webhook inbound bodies shared/schemas/webhook-inbound.ts signature check first, then schema
Import files parser-specific (md/html/csv/json/notion-zip) → normalized block-tree schema hostile inputs are a first-class test category (testing.md §3.6)
AI tool-call results shared/schemas/ai-tools.ts a rejected tool result is fed back, max 3 retries, then the op fails cleanly
Env apps/api/src/config/env.ts boot fails on invalid env (no half-configured service)

7.2 Block content sanitization

Block content is structured JSON (Zod per type) — the primary defense is there is no HTML field. The residual HTML surfaces:

Surface Sanitization
Rich text (RICH_TEXT property, block text) stored as markdown source; rendered client-side by the markdown pipeline → DOMPurify allow-list (tags: `p,br,strong,em,u,s,code,pre,a[href^=https
Search snippets server-side <mark> highlighting → client strips all tags except mark → re-renders (no raw HTML path)
Import HTML full pipeline: parse (readability-style extraction) → strip (all scripts, styles, iframes, event attrs, javascript:/data: (except data:image for images ≤ 5 MB base64) URLs) → normalize to blocks; the output is the block schema (Zod) — anything non-conformant is dropped, not escaped-and-kept
Markdown import the markdown parser is configured with no raw-HTML pass-through (raw HTML blocks in the source are dropped with a report entry); link protocols allow-listed as above
Comments same rich-text pipeline as RICH_TEXT
User-generated URLs (bookmark, URL property, embed) protocol allow-list (http/https only; embed additionally provider-allow-listed); stored as-is (display-only), rendered via <a>/iframe sandbox (functional §4.2 embed sandbox attrs)
Filenames / titles / names length-capped, control chars stripped (log-safe + render-safe); display-only (never executed, never in paths — object keys are server-generated)

7.3 Output encoding

React escapes by default; the only dangerouslySetInnerHTML uses are the sanitized-markdown render and KaTeX output (KaTeX's own trust config is left false). The CI security suite (testing.md §7) feeds XSS payloads into every string surface (title, comment, block text, property value, filename, share token, URL) and asserts no script executes / no raw tag renders.

7.4 AI output (the new untrusted surface)

AI responses are data: chat text renders through the markdown pipeline (same sanitizer); generation (pages/databases) arrives as tool calls validated by Zod before any write; the executor runs with the invoking user's permissions (the AI cannot grant itself access — §13 T9). No model output ever reaches a prompt-injection-sensitive sink (no "ignore previous instructions" surface: the platform's own prompts are static templates with a # User content (untrusted) delimiter, and retrieval results are injected as data blocks with the same marking).

8. File pipeline security

 prepare (API: size/mime/workspace checks) ─► presigned PUT (15 min, exact key)
   ─► complete (API) ─► worker: [1] ClamAV scan → [2] magic-byte MIME sniff
      → [3] metadata (sha256, dimensions, duration) → [4] thumbnail
      → [5] status=READY (+ file.uploaded event)
Control Rule
Size limits per-file max 500 MB (plan-configurable); images 25 MB; form files 25 MB; avatar 5 MB; total per workspace from plan (usageLimits.storageMb, NFR-S6). Limits enforced twice: at prepare (metadata) and at complete (actual object size from S3 HEAD — a client can PUT more than it declared).
MIME magic-byte sniff is authoritative (never the extension): the pipeline re-derives mimeType from bytes; extension/sniff mismatch → FAILED + report (spoofed .png that is actually .exe is caught here even before ClamAV). Per-plan mime allow-list; executables (.exe, .msi, .bat, .cmd, .sh, .app, mach-o, pe32+) blocked platform-wide.
Virus scan ClamAV worker stage, synchronous in the pipeline (file is not READY until clean); verdict VIRUS_DETECTED → object moved to a quarantine prefix, never served, admin report shows signature + hash; scan failures (engine down) → FAILED (fail-closed, not fail-open). EICAR test in CI (NFR-SE6).
Signed URLs downloads/preview via presigned GET, 15 min TTL, scoped to the exact key; uploads via presigned PUT, 15 min, exact key + content-length range; URLs are single-object (no prefix wildcards).
Private buckets the uploads bucket has no public ACLs, no anonymous access, no public bucket policy (MinIO: mc anonymous set none); the only read paths are presigned URLs and the API download endpoint (which re-checks permission, §4.4 below).
Per-file permission check a download is: (1) can(file.page/page, VIEW) via the engine, (2) the file's own override (files.permissions) may restrict further (a file can be hidden from editors), never grant more, (3) then the signed URL is minted. Public shares: only files whose page is publicly shared (and allowComments-irrelevant) are downloadable by share visitors — the share route mints URLs only for files passing the share scope check.
Keys server-generated <wsId>/<fileId>/<version> (user filenames are display-only, sanitized for logs); no user input in any path segment.
Rendering image previews served as image/* (browser sandboxed by content-type); PDFs via the native viewer (X-Content-Type-Options: nosniff + correct content-type); HTML-type files are never served as HTML (served as attachment) — stored HTML files download, they don't render (the XSS-surface kill switch).
Upload abuse presign rate limit (20/min/user); orphaned uploads (prepared, never completed) are reaped after 24 h (object + row); concurrent uploads per user capped at 4.

9. Abuse protection

9.1 Rate-limit tiers (Redis sliding window)

Per (user, tier) and per (IP, tier); the stricter of the two applies. Headers: X-RateLimit-Limit/Remaining/Reset; on limit → 429 RATE_LIMITED

Tier Endpoints Limit (default, plan-tunable)
auth-strict login, forgot-password, verify, reset 5/min/IP; 3/day/IP registration; 3/hour email-send
auth refresh, logout, sessions 60/min
write all state-changing API routes 300/min/user (RATE_LIMIT_MAX)
read all other API routes 600/min/user
search /search, suggestions 60/min/user
ai AI endpoints 20/min/user + workspace token budget (functional §16)
upload file prepare/complete 20/min/user
public-form public form submissions 10/15 min/IP
public-share /share/:token reads 120/min/IP/token (throttle link sharing abuse)
api-key public API v1 plan default (workspace.usageLimits.apiRps) or per-key rateLimit
ws realtime messages 60 msg/s/conn sustained (burst 100); join: 30/min/conn

Implementation: one Redis Lua script (sliding window, atomic), keys rl:{tier}:{scope}; the guard caches the "not limited" verdict 1 s to avoid a Redis round-trip per request at scale (NFR-S3 headroom). NFR-SE5 load test: 5k credential-stuffing attempts → 0 bypasses.

v1 implemented buckets (TASK-0134 — the authoritative, tested set). The tier table above is the design target. v1 ships a smaller subset; the exact defaults below are pinned in tests/security/rate-limits.test.ts (a change without a finding + justification fails CI). The two new buckets (incoming-webhook, public-form) were added by TASK-0134 because those public third-party endpoints were previously unbounded:

Bucket Key v1 default Notes
API (per credential/IP) rl:api:<user|ip> 100/min (RATE_LIMIT_API) generic window
Auth (login/register/reset) rl:auth:<user|ip> 10/min (RATE_LIMIT_AUTH) credential endpoints
Per API key window per key 600/min (RATE_LIMIT_API_KEY) or key.rateLimit the key's value wins
Incoming webhook (NEW) auto:webhook-rate:<webhookId> 60/min (WEBHOOK_RATE_PER_MINUTE) checked before signature verification
Public form (NEW) form:rl:{slug}:{ip} 5/min (FORM_SUBMIT_LIMIT, 60 s) per IP+form; stricter than the spec's 20/min example (delta recorded, AGENTS rule 29)

The design tiers not yet implemented in v1 (per-tier write/read/ search/ai/upload/ws limits) are tracked as follow-ups, not audit findings.

9.2 CSRF

§6 (double-submit X-CSRF-Token for browser state changes; API keys and public forms exempt by design, documented).

v1 implemented model (TASK-0134, finding F-107 — recorded deviation, AGENTS rule 29). v1 does not ship the double-submit factory_csrf token. Instead: every state-changing web request is authenticated with an in-memory Bearer access token (never a cookie), and the only cookie-authenticated route is POST /api/v1/auth/refresh (the 401 retry), whose cookie factory_refresh is HttpOnly; SameSite=Lax. This is CSRF-safe: a cross-origin page cannot read the in-memory Bearer, and a SameSite=Lax cookie is not sent on cross-site POST. The double-submit design in §6 remains the target model; the 100% coverage of the state-changing surface (all routes through the Bearer client, refresh exempt, OAuth/SAML callbacks using state/nonce) is proven by the route-walk in tests/security/auth-paths.test.ts.

9.3 Secure cookies & CORS

Cookie attributes per §2.1/§6 (Secure always in prod — the cookie is simply not set over http in production; HttpOnly on everything except factory_csrf/factory_theme; SameSite Strict for auth, Lax for CSRF). CORS allow-list per §5. No cookie is readable by JS except the two non-sensitive ones.

9.4 External URLs & SSRF

Any URL the platform will request (webhook targets, integration calls, AI custom endpoints, bookmark OpenGraph fetch, embed validation):

  1. parse + scheme allow-list (https: in prod; http: only for loopback in dev);
  2. hostname: DNS-resolve before the request; reject if any resolved IP is private/loopback/link-local/reserved (10/8, 172.16/12, 192.168/16, 127/8, 169.254/16, ::1, fc00::/7, 0.0.0.0) — re-resolve and re-check at dispatch time (DNS-rebinding window is still closed by connecting to the checked IP via a dial callback);
  3. redirect policy: max 3 hops, every hop re-validated (a public URL that 302s to http://169.254.169.254 is rejected);
  4. egress: in k8s, webhook/integration egress goes through the worker's NetworkPolicy-allowed CIDRs only (the api pod has no direct internet egress except the AI provider + S3 — egress allow-list is the second line). User-provided URLs that are only displayed (bookmark cards, URL properties) are not fetched server-side in v1 except bookmark OpenGraph (fetch is best-effort, failures render a plain link — the fetch path goes through the same SSRF guard).

10. Secrets management

10.1 Inventory & flow

Secret Source Storage at rest Rotation
JWT_SECRET, REFRESH_TOKEN_SECRET, SESSION_SECRET env (Vault/SOPS) never in DB dual-key overlap window (*_PREV) for rolling restarts (deployment §3.1)
MASTER_ENCRYPTION_KEY (envelope DEK) env (Vault/SOPS), 64 hex never in DB §10.3
S3/SendGrid/SMTP/OpenSearch/AI provider creds env / secret store Vault KV / SOPS per-provider SLA; app reads at boot (restart = new creds)
User passwords user input bcrypt hash in users.passwordHash n/a (hash)
Refresh/session/verify/reset/invite tokens generated SHA-256 hash only single-use / TTL / rotation
API keys generated SHA-256 hash + display prefix (fk_live_ab12) raw shown once at creation; user self-rotates anytime
Webhook signing secrets generated raw kept envelope-encrypted (dispatcher must HMAC) + secretHash integrity column raw shown once; rotation re-keys deliveries
Integration credentials (Slack tokens etc.) user input envelope-encrypted in integrations.config user-managed; status=error on provider 401
Share passwords user input bcrypt hash in shares.passwordHash n/a
SSO client secrets / SAML certs admin input envelope-encrypted (by reference in sso_providers.config) admin-managed

10.2 Envelope encryption design

DEK (data key)  = MASTER_ENCRYPTION_KEY (env) — AES-256-GCM
value → payload = { v: 1, alg: "A256GCM",
                    nonce: 12B-random,
                    ct: AES-GCM(DEK, plaintext),
                    tag: 16B }

10.3 API keys (FR-PR-149)

fk_live_ + 32 random bytes (256-bit entropy); stored: SHA-256 hash (keyHash) + first 12 chars (prefix, for identification); the raw key appears exactly once (creation response) — the UI marks it "copy now, you won't see it again". Auth: lookup by hash (unique index), then scope check (READ/WRITE/ADMIN vs method), expiry/revocation check, per-key rate limit, lastUsedAt update (throttled 1/min). Audit: api_key.created / api_key.revoked mandatory.

10.4 Webhook HMAC (FR-PR-151)

Webhooks sign both directions with one scheme: X-Factory-Signature: t=<unix-seconds>,v1=<hex> (comma-separated, no space), where v1 = HMAC-SHA256(signingSecret, t + "." + rawBody), lowercase hex. The t and the raw request bytes are joined with a literal dot — the verifier must hash the exact received body (never re-serialized JSON). The scheme is the single shared implementation (@factory/shared webhook-delivery.ts), so outbound deliveries and inbound triggers verify against the same code.

Outbound (our → receiver): every delivery carries the signature. Receivers verify by recomputing with the raw body, comparing constant-time, and rejecting if |now − t| > 300 s (5-min replay window — a stale signature must not verify). The delivery also carries X-Factory-Event / X-Factory-Event-Id / X-Factory-Delivery-Id / X-Factory-Attempt for dedupe and ordering.

Inbound (integrator → our POST /webhooks/incoming/:webhookId): the public trigger endpoint is gated by the same signature — no credentials are sent; the signature is the only gate. Verification order: (1) v1 matches → else 401 WEBHOOK_SIGNATURE_INVALID; (2) |now − t| ≤ 300 s → else 401 WEBHOOK_TIMESTAMP_STALE; (3) replay protection — the (t, sha256(body)) pair is a Redis nonce (SET NX EX 600, 10-min window = 2× the timestamp tolerance); a second identical request → 401 WEBHOOK_REPLAY_DETECTED. Unknown webhookId → 404 (no existence leak beyond the signature failure). The resulting automation run is audited with the external integrator as the INTEGRATION actor (NFR-SE7).

Signing-secret storage: a webhook's raw signing secret is returned exactly once (create / rotate) and then stored AES-256-GCM encrypted (secretEncrypted, keyed by MASTER_ENCRYPTION_KEY) so the worker can re-sign retries, alongside a SHA-256 secretHash for integrity and legacy hash-only rows. The raw secret is never read back; only a leak-free secretPrefix (whsec_ + first 4 hash chars) is exposed on list/get. Incoming-webhook trigger secrets use the same envelope under AUTOMATION_WEBHOOK_KMS_KEY (falling back to MASTER_ENCRYPTION_KEY).

Interoperability vector is committed (testing.md §3.6 — the signature test uses a fixed secret/body and must never change without a scheme version bump v2).

10.5 SCIM bearer token (TASK-0132)

The IdP→Factory provisioning credential (scim_ + 32 random bytes) follows the API-key pattern, not the webhook pattern: it is a long-lived bearer token, so it is stored SHA-256-hashed only (scimConfig.bearerTokenHash) — there is no plaintext or ciphertext to leak, and it is revealed exactly once (enable/rotate). Auth: the guard hashes the presented token and looks it up in a Redis hash index (scim:index → orgId, maintained on enable/rotate/disable; cold-cache rebuild from the DB) — no per-request DB scan. The token acts as the org (actorType: INTEGRATION, actorId: orgId), which keeps the audit trail attributable to the IdP integration even when individual IdP admins differ.

Threat-model notes:

11. Audit architecture

11.1 The contract

Area Events (dot-notation, audit_logs.action)
auth user.registered, user.login.success, user.login.failed, user.logout, user.password.reset, session.revoked, auth.refresh.replayed, sso.login
workspace workspace.created, workspace.deleted, workspace.role.changed, workspace.invitation.sent, workspace.invitation.accepted, workspace.invitation.revoked
pages page.created, page.updated, page.deleted, page.restored, page.purged, page.shared, page.exported
permissions permission.changed (metadata: before/after grant), share.created, share.revoked
databases database.created, database.property.changed, record.created, record.updated, record.deleted, database.button.executed
files file.uploaded, file.virus_detected, file.purged (file.downloaded sampled 1/100 for volume control)
comments comment.created (only for admin-visible moderation views; not every comment — volume; the comment rows themselves are the record)
automation automation.created, automation.executed (metadata: status, duration, action)
API api_key.created, api_key.revoked, webhook.created, webhook.deleted
forms form.published, form.submitted (public submissions are audited — anonymous actor)
AI ai.generation.completed (metadata: kind, tokens, model)
enterprise scim.user.updated, scim.user.deactivated, retention.purged, org.policy.changed

11.2 Retention & export

11.3 The org-admin surface (threat model)

The org admin console (TASK-0133) concentrates the org's sensitive operations behind one gate — the requireOrgAdmin check (OWNER/ADMIN membership; everyone else, including members of OTHER orgs, gets the same 403 requires_org_admin, so cross-org enumeration is impossible).

11.4 Log hygiene — no secrets in logs (TASK-0134, finding F-121)

The API logs structured JSON (apps/api/src/common/logger.ts; Nest's built-in logger is disabled). The only request-coupled log paths are the global exception filter (req.originalUrl + exception message/stack) and ad-hoc logger.* calls. A secret-bearing request can therefore carry credentials into a log line via the URL (e.g. ?token=…), an exception message, or an echoed header. apps/api/src/common/log-scrubber.ts (scrubSecrets) redacts, at that boundary: Authorization: Bearer …, JWT-shaped tokens, factory/third-party token prefixes (fk_/whsec_/apk_/ ghp_/xox…/sk-/AKIA…), exact credential query params (?token=, ?api_key=, ?secret=, …), and Cookie:/Set-Cookie: values. It is wired into AllExceptionsFilter; the regression test is apps/api/src/common/log-scrubber.test.ts. The web/worker/realtime apps were audited for the same risk and have no request-coupled HTTP log path that emits headers/cookies/auth values (the worker logs job/processor lifecycle; realtime logs channel lifecycle), so the scrubber is applied where the exposure exists (the API).

12. Threat model (top 10)

# Threat Scenario Mitigations (where) Residual risk / notes
T1 Account takeover (credential theft) stolen password / phished login bcrypt-12 + breach-list check (§2.4); rate limits §9.1; refresh replay detection §2.3 (a stolen refresh token burns the family); session visibility (user sees foreign UA/IP and revokes); SSO enforcement for enterprise (§2.5); audit on every login §11 no MFA in v1 (Phase-14 enterprise gap — documented; TOTP is the planned control)
T2 Cross-tenant data leak crafted id / query bug exposes another workspace's row tenant scope in every repository (database-arch §10.1); 404-not-403 for foreign ids (§3.4); cross-tenant CI matrix gate (NFR-P1); search permission snapshots (§4.3); realtime room isolation (§4.4); object keys embed ws id (§8) RLS not used (documented decision, database-arch §10.2) — the CI gate is the standing control
T3 Authorization bypass role/level confusion (guest edits, member shares) engine decision matrix 100% tested (testing §3.2); guard + service double-check (§3.4); explicit-NONE deny override (§3.3); permission writes bounded by granter's level (§3.4) the matrix is the spec — a new resource kind must add matrix cells in the same PR
T4 XSS / stored injection malicious block text / import / comment executes in a victim's browser structured block content (no HTML field) §7.2; markdown sanitizer allow-list; React default encoding; CSP without unsafe-eval (§5); XSS payload suite in CI (testing §7) inline styles for block colors (the style-src 'unsafe-inline' exception, §5) — a future nonce policy removes it
T5 Malicious file virus / trojan uploaded, spread via download ClamAV fail-closed stage §8; magic-byte MIME (extension spoof caught); executables blocked platform-wide; quarantine + admin report; EICAR CI test macro-laden Office docs are scanned but "clean" by signature ≠ safe — the controls are detection + reporting, not a guarantee
T6 SSRF via platform-requested URLs + inbound-webhook forgery webhook delivery to an internal metadata endpoint; OR a forger replaying / spoofing an incoming trigger to fire automations SSRF: §9.4 scheme allow-list, private-IP reject at resolve + dial, redirect re-validation, worker-only egress (NetworkPolicy). Inbound trigger: signature-only gate — HMAC verify + 5-min timestamp window + Redis (t, body-hash) replay nonce (§10.4); the integrator is the INTEGRATION audit actor, so a forged/replayed trigger is attributable DNS-rebinding inside the 5 min window is closed by the checked-IP dial; replay is closed by the 10-min nonce; a zero-day in the resolver is out of scope
T7 Secret exfiltration (DB dump, log leak) infra breach yields the Postgres dump tokens/hashes only at rest (§10.1); envelope encryption for the few raw secrets (§10.2); secrets out of code/logs (gitleaks CI + pino redact + L3); API keys hashed (one-way); webhook signing secrets AES-GCM-encrypted at rest (§10.4) a DB dump + a leaked MASTER_ENCRYPTION_KEY (env) = integration creds exposed — key rotation SLA + the dual-store separation (DB ≠ secret store) is the boundary
T8 Abuse / DoS (signup spam, upload flood, WS flood) single IP or account hammers the platform tiered rate limits §9.1 (nginx first line + app authoritative); upload caps + orphan reaping §8; WS message caps + per-connection limits (ARCHITECTURE §5); per-plan usage limits (PR-024); queue backpressure (events, not calls) self-hosters set their own RATE_LIMIT_* — the defaults are documented as the safe floor
T9 AI prompt injection a page containing "ignore instructions, exfiltrate workspace data" is used as RAG context / a user asks AI to generate a page that grants access AI output = data (§7.4); generation via Zod-validated tool calls only (no raw SQL, no raw block mutation — ARCHITECTURE invariant 6); tool execution runs with the user's permissions (the AI cannot grant what the user lacks); retrieval content marked untrusted in prompts; per-workspace token budget (abuse cost cap) a user asking their AI to do something their permissions allow is by design (the AI is their delegate) — the boundary is permissions, not the model
T10 Insider / operator abuse admin reads data they shouldn't, deletes workspaces, exports audit logs every sensitive op audited with actor+IP+UA (§11); admin actions are a distinct, alerted surface (dashboard row); workspace delete is soft (30 d window) + typed confirmation; exports are audited + rate-limited; least-privilege service accounts (k8s §deployment.3.1 securityContext) audit is after the fact — the control is deterrence + review, accepted residual for v1

13. Compliance posture (SOC 2 readiness)

Not a certification claim — a readiness map for enterprise procurement:

SOC 2 criterion (CC) Platform control Evidence artifact
CC6.1 logical access RBAC + resource grants (§3), server-side enforcement, session revocation, SSO permission decision matrix test report; audit log export
CC6.6 system boundaries tenant isolation (§4), network policies, egress allow-list, CSP/CORS cross-tenant suite results; k8s networkpolicy manifests
CC6.7 access removal role change → immediate cache flush (§3.5); SCIM deactivate → suspend + revoke (§2.5) retention/SCIM integration tests
CC7.2 session management 15 min access / rotating refresh / replay detection (§2) auth integration suite
CC8.1 change management migration procedure (deployment §7), release process (§6), backward-compat rule release checklist + migration records in task files
CC8.1 p (vulnerability mgmt) SCA gate (0 critical), ZAP baseline, ASVS L2 audit (Phase 15), patch SLA (D5) SCA reports; pen-test report (Phase 15)
CC9.2 monitoring observability stack (deployment §9): SLO burn, alerts §9.3, dashboards Grafana dashboards (as code), alert rules
CC9.4 risk response threat model (this §12), DR drills (deployment §8.4), RPO 5 min / RTO 1 h DR drill reports (docs/ops/dr-reports/)
CC12.3/CC11 (audit logs) mandatory event catalog, 400 d retention, export (§11) audit viewer + export; retention policy config
Privacy (A2) data minimization (DTO projections), no tracking cookies, PII scrubbed from logs (NFR-P2), retention defaults (NFR-P3), GDPR-style export/delete (NFR-P5) log-scrubber test; retention integration test

Gaps to close for a formal audit (post-v1): TOTP MFA (T1), centralized key management with automatic rotation, pen-test remediation cycle documentation, and a formal risk register. The architecture is deliberately SOC 2-shaped now (auditability, separation of duties via roles, evidence as code) so that those closures are configuration, not redesign.