diff --git a/.dockerignore b/.dockerignore index c21baed..39c3f05 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,7 +4,11 @@ dist out .turbo .git +.github .env +.env.* +!.env.example +!.env.docker.example coverage data *.log diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..ead9a7e --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,25 @@ +# 实验版本默认跟随仓库主开发分支。需要固定版本时改为对应的版本标签。 +NOTEGEN_SERVER_IMAGE=ghcr.io/codexu/note-gen-server:edge + +# PostgreSQL 密码。推荐使用 `openssl rand -hex 32` 生成,避免 URL 保留字符。 +POSTGRES_PASSWORD=replace-with-a-random-hex-password + +# 登录令牌和数据库内敏感配置的加密密钥。上线后不要随意更换。 +AUTH_SECRET=replace-with-at-least-32-random-characters + +# NoteGen 客户端实际访问的公开地址,不要带路径或末尾斜杠。 +# 本机实验可以使用 http://127.0.0.1:3789;远程部署必须使用 HTTPS 域名。 +PUBLIC_BASE_URL=http://127.0.0.1:3789 + +# 默认只监听宿主机回环地址,由 Caddy/Nginx/Traefik 反向代理。 +# 仅在受信局域网实验时改为 0.0.0.0,并同时配置防火墙。 +SERVER_BIND_ADDRESS=127.0.0.1 +SERVER_PORT=3789 + +# 只有外部无法绕过受信反向代理直连服务端口时才设置为 true。 +TRUST_PROXY=false + +LOG_LEVEL=info +METRICS_ENABLED=false +METRICS_TOKEN= +OPENAPI_ENABLED=false diff --git a/.env.example b/.env.example index 50dce35..20db803 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ PUBLIC_BASE_URL=https://sync.example.com # 登录令牌与数据库内敏感配置的加密密钥,至少 32 个随机字符;上线后不要更换。 AUTH_SECRET=replace-with-at-least-32-random-characters -# 部署模式、服务名称、管理员账号和注册策略在首次打开 Web 页面时设置; +# 服务名称、管理员账号和注册策略在首次打开 Web 页面时设置; # 业务限额、保留周期和邮件配置使用安全默认值,之后在 Web 管理后台维护。 +# SMTP 默认关闭且不是安装依赖;需要邀请邮件或密码找回时再由管理员配置。 # 空实例无需安装密钥,请先完成 Web 安装,再开放公网入口。 diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml new file mode 100644 index 0000000..d44ecb0 --- /dev/null +++ b/.github/workflows/publish-container.yml @@ -0,0 +1,76 @@ +name: Publish container image + +on: + push: + branches: + - feat/sync-foundation + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + packages: write + attestations: write + id-token: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/note-gen-server + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=edge,enable=${{ github.ref == 'refs/heads/feat/sync-foundation' }} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha- + labels: | + org.opencontainers.image.title=NoteGen Server + org.opencontainers.image.description=Self-hosted synchronization server and account portal for NoteGen + org.opencontainers.image.source=https://github.com/${{ github.repository }} + + - name: Build and push image + id: push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v3 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.gitignore b/.gitignore index f206946..bcbf237 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ out/ .env .env.* !.env.example +!.env.docker.example coverage/ data/ *.log diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7a4c226 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,73 @@ +# 参与 NoteGen Server 开发 + +NoteGen Server 处于实验阶段,欢迎通过 Issue、文档改进和代码贡献共同完善部署、同步协议、服务端 API 与账号管理 Web。 + +## 选择运行方式 + +### 源码开发 + +适合频繁修改、调试和提交代码。需要 Node.js 22 或更高版本、pnpm 10.20.0 和 PostgreSQL 17。 + +```bash +git clone https://github.com/codexu/note-gen-server.git +cd note-gen-server +pnpm install +cp .env.example .env +pnpm dev +``` + +首次创建本地数据库: + +```bash +createuser notegen +createdb -O notegen notegen +``` + +修改 `.env` 中的 `DATABASE_URL`、`AUTH_SECRET` 和 `PUBLIC_BASE_URL`。API 默认监听 `3789`,开发态 Web 默认监听 `3790`。 + +### 本地容器构建 + +适合复现 Docker 部署问题或验证镜像构建: + +```bash +cp .env.docker.example .env +docker compose -f compose.yaml -f compose.dev.yaml up -d --build +``` + +完整说明见 [Docker Compose 部署](docs/deployment/docker.md)。 + +## 仓库结构 + +```text +apps/server Fastify API、认证、同步和运维 CLI +apps/web 静态导出的账号管理 Web +packages/contracts Web 与服务端共享的协议类型 +docs 协议、部署和运维文档 +``` + +## 修改边界 + +- 数据库结构变化必须通过 Drizzle migration 表达,不手工改 migration journal。 +- 同步协议和 capabilities 采用显式版本与能力协商,不能只根据服务端版本猜测行为。 +- 不把访问令牌、密码、同步密文、封装密钥或真实 `.env` 提交到仓库。 +- Web 使用静态导出,生产环境由 Fastify 同源托管。 +- 新配置项需要同步更新 `.env.example`、部署文档和能力说明。 + +## 提交前检查 + +项目当前提供以下检查命令,按改动范围选择执行: + +```bash +pnpm typecheck +pnpm test +pnpm build +pnpm db:migrate +``` + +涉及真实同步链路时,可在服务启动后运行: + +```bash +INTEGRATION_BASE_URL=http://127.0.0.1:3789 pnpm test:integration +``` + +提交 Pull Request 时请说明问题、实现方式、兼容性影响,以及自己执行过的验证。涉及数据库迁移、协议、安全边界或备份恢复时,同时说明升级与回退约束。 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e6ed7e2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,66 @@ +FROM node:24-bookworm-slim AS base + +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH + +RUN apt-get update \ + && apt-get install -y --no-install-recommends g++ make python3 \ + && rm -rf /var/lib/apt/lists/* \ + && corepack enable \ + && corepack prepare pnpm@10.20.0 --activate + +WORKDIR /app + +FROM base AS dependencies + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ +COPY apps/server/package.json apps/server/package.json +COPY apps/web/package.json apps/web/package.json +COPY packages/contracts/package.json packages/contracts/package.json + +RUN pnpm install --frozen-lockfile + +FROM dependencies AS builder + +COPY . . + +RUN pnpm build + +FROM base AS production-dependencies + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./ +COPY apps/server/package.json apps/server/package.json +COPY apps/web/package.json apps/web/package.json +COPY packages/contracts/package.json packages/contracts/package.json + +RUN pnpm install --prod --frozen-lockfile + +FROM node:24-bookworm-slim AS runner + +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=3789 + +LABEL org.opencontainers.image.source="https://github.com/codexu/note-gen-server" +LABEL org.opencontainers.image.description="Self-hosted synchronization server and account portal for NoteGen" + +WORKDIR /app + +COPY --from=production-dependencies /app/node_modules ./node_modules +COPY --from=production-dependencies /app/apps/server/node_modules ./apps/server/node_modules +COPY --from=builder /app/apps/server/package.json ./apps/server/package.json +COPY --from=builder /app/apps/server/dist ./apps/server/dist +COPY --from=builder /app/apps/server/drizzle ./apps/server/drizzle +COPY --from=builder /app/apps/web/out ./apps/web/out + +RUN mkdir -p /app/data/blobs /app/data/backups \ + && chown -R node:node /app + +USER node + +EXPOSE 3789 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \ + CMD ["node", "-e", "fetch('http://127.0.0.1:3789/health/ready').then((response) => { if (!response.ok) process.exit(1) }).catch(() => process.exit(1))"] + +CMD ["node", "apps/server/dist/entrypoint.js"] diff --git a/README.md b/README.md index bdf003d..5d3c0c0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,52 @@ # NoteGen Sync Server -NoteGen Sync Server 是 NoteGen 客户端使用的同步服务,可用于自行部署,也可用于运营官方托管服务。 +NoteGen Sync Server 是 NoteGen 客户端使用的免费同步服务。官方公共测试服务和用户自行部署使用同一套独立实例运行方式。 + +> 自托管服务目前处于实验阶段,部署方式、配置和数据库结构仍可能变化。请保留 NoteGen 本地 Markdown,不要把实验实例作为重要数据的唯一副本。 项目包含同步 API 和账号管理 Web。Web 负责注册、登录、设备关联、同步统计和账号管理,不包含 Markdown 编辑器、AI 功能或多人协作功能。 +## 开始使用 + +### Docker Compose(推荐体验) + +适合快速搭建个人测试实例。官方 Compose 默认拉取 GHCR 的 `edge` 镜像,并启动 NoteGen Server 与 PostgreSQL 17: + +```bash +git clone https://github.com/codexu/note-gen-server.git +cd note-gen-server +cp .env.docker.example .env +# 编辑 .env,设置 POSTGRES_PASSWORD、AUTH_SECRET 和 PUBLIC_BASE_URL +docker compose up -d +docker compose ps +``` + +详细的首次初始化、HTTPS、日志和数据说明见 [Docker Compose 部署](docs/deployment/docker.md)。 + +### 从源码运行(开发者) + +适合调试服务端、修改功能和提交贡献。需要 Node.js 22 或更高版本、pnpm 10.20.0 和 PostgreSQL 17: + +```bash +git clone https://github.com/codexu/note-gen-server.git +cd note-gen-server +createuser notegen +createdb -O notegen notegen +pnpm install +cp .env.example .env +pnpm dev +``` + +贡献约定、仓库结构与本地容器构建方法见 [CONTRIBUTING.md](CONTRIBUTING.md)。 + +## 项目定位 + +- 项目免费且不以商业化为目标,不提供付费订阅、套餐或商业 SLA。 +- 官方只提供供社区体验的公共测试实例;它与用户自行部署使用相同代码和运行方式。 +- 每个实例由自己的管理员负责注册策略、邀请、数据保留、备份和滥用处理。 +- 邮件不是基础依赖。SMTP 默认关闭;未配置时仍可使用账号密码和一次性邀请链接。 +- 客户端应继续保留本地数据,公共测试实例不应被视为唯一备份。 + ## Monorepo 结构 ```text @@ -15,11 +58,15 @@ apps/web Next.js + shadcn/ui 账号管理页面 当前项目已完成首个可部署的服务端基线,详细方案见: +- [Docker Compose 部署](docs/deployment/docker.md) +- [容器镜像发布](docs/deployment/container-publishing.md) +- [参与服务端开发](CONTRIBUTING.md) - [同步服务技术规格](docs/sync-server-spec.md) - [本地运行与运维](docs/operations.md) - [NoteGen 客户端接入协议](docs/client-protocol.md) - [NoteGen 接入与配置同步体验](docs/notegen-integration.md) - [部署后的个人验收用例](docs/self-test.md) +- [公共测试实例上线清单](docs/production-checklist.md) - [账号服务开发计划总览](docs/account-service/README.md) ## 已确定技术栈 @@ -70,7 +117,7 @@ createuser notegen createdb -O notegen notegen ``` -复制并填写本地配置。`DATABASE_URL`、`AUTH_SECRET` 与 `SETUP_TOKEN` 必须设置为自己的值: +复制并填写本地配置。`DATABASE_URL`、`AUTH_SECRET` 与 `PUBLIC_BASE_URL` 必须设置为自己的值: ```bash cp .env.example .env diff --git a/apps/server/drizzle/0002_amazing_proemial_gods.sql b/apps/server/drizzle/0002_amazing_proemial_gods.sql new file mode 100644 index 0000000..caf876a --- /dev/null +++ b/apps/server/drizzle/0002_amazing_proemial_gods.sql @@ -0,0 +1,12 @@ +CREATE TABLE "sync_device_cursors" ( + "workspace_id" uuid NOT NULL, + "device_id" uuid NOT NULL, + "acknowledged_sequence" bigint DEFAULT 0 NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "sync_device_cursors_workspace_id_device_id_pk" PRIMARY KEY("workspace_id","device_id") +); +--> statement-breakpoint +ALTER TABLE "sync_commands" ADD COLUMN "sync_epoch" uuid;--> statement-breakpoint +ALTER TABLE "sync_device_cursors" ADD CONSTRAINT "sync_device_cursors_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sync_device_cursors" ADD CONSTRAINT "sync_device_cursors_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "sync_device_cursors_updated_idx" ON "sync_device_cursors" USING btree ("workspace_id","updated_at"); \ No newline at end of file diff --git a/apps/server/drizzle/0003_amused_tag.sql b/apps/server/drizzle/0003_amused_tag.sql new file mode 100644 index 0000000..ecf1806 --- /dev/null +++ b/apps/server/drizzle/0003_amused_tag.sql @@ -0,0 +1,10 @@ +CREATE TABLE "rate_limit_buckets" ( + "scope" text NOT NULL, + "rate_key" text NOT NULL, + "window_start" timestamp with time zone NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "hits" integer DEFAULT 0 NOT NULL, + CONSTRAINT "rate_limit_buckets_scope_rate_key_window_start_pk" PRIMARY KEY("scope","rate_key","window_start") +); +--> statement-breakpoint +CREATE INDEX "rate_limit_buckets_expiry_idx" ON "rate_limit_buckets" USING btree ("expires_at"); \ No newline at end of file diff --git a/apps/server/drizzle/0004_workspace_collaboration.sql b/apps/server/drizzle/0004_workspace_collaboration.sql new file mode 100644 index 0000000..ef9ba18 --- /dev/null +++ b/apps/server/drizzle/0004_workspace_collaboration.sql @@ -0,0 +1,53 @@ +CREATE TYPE "public"."workspace_type" AS ENUM('account-data', 'library'); +CREATE TYPE "public"."workspace_member_role" AS ENUM('viewer', 'editor', 'manager'); +CREATE TYPE "public"."workspace_invitation_kind" AS ENUM('account', 'link'); +CREATE TYPE "public"."workspace_invitation_status" AS ENUM('pending', 'accepted', 'revoked', 'expired'); +ALTER TYPE "public"."object_kind" ADD VALUE 'message'; + +ALTER TABLE "workspaces" ADD COLUMN "workspace_type" "workspace_type" DEFAULT 'library' NOT NULL; +UPDATE "workspaces" SET "workspace_type" = 'account-data' WHERE "is_default" = true; + +CREATE TABLE "workspace_members" ( + "workspace_id" uuid NOT NULL, + "account_id" uuid NOT NULL, + "role" "workspace_member_role" NOT NULL, + "capabilities" text[] NOT NULL, + "invited_by_account_id" uuid NOT NULL, + "joined_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "workspace_members_workspace_id_account_id_pk" PRIMARY KEY("workspace_id","account_id") +); + +CREATE TABLE "workspace_invitations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "workspace_id" uuid NOT NULL, + "kind" "workspace_invitation_kind" NOT NULL, + "invitee_account_id" uuid, + "token_hash" text, + "token_hint" text, + "role" "workspace_member_role" NOT NULL, + "capabilities" text[] NOT NULL, + "status" "workspace_invitation_status" DEFAULT 'pending' NOT NULL, + "invited_by_account_id" uuid NOT NULL, + "accepted_by_account_id" uuid, + "expires_at" timestamp with time zone NOT NULL, + "accepted_at" timestamp with time zone, + "revoked_at" timestamp with time zone, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); + +ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade; +ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade; +ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_invited_by_account_id_accounts_id_fk" FOREIGN KEY ("invited_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE restrict; +ALTER TABLE "workspace_invitations" ADD CONSTRAINT "workspace_invitations_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade; +ALTER TABLE "workspace_invitations" ADD CONSTRAINT "workspace_invitations_invitee_account_id_accounts_id_fk" FOREIGN KEY ("invitee_account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade; +ALTER TABLE "workspace_invitations" ADD CONSTRAINT "workspace_invitations_invited_by_account_id_accounts_id_fk" FOREIGN KEY ("invited_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE restrict; +ALTER TABLE "workspace_invitations" ADD CONSTRAINT "workspace_invitations_accepted_by_account_id_accounts_id_fk" FOREIGN KEY ("accepted_by_account_id") REFERENCES "public"."accounts"("id") ON DELETE set null; + +CREATE INDEX "workspace_members_account_idx" ON "workspace_members" USING btree ("account_id","updated_at"); +CREATE INDEX "workspace_invitations_workspace_idx" ON "workspace_invitations" USING btree ("workspace_id","status","created_at"); +CREATE INDEX "workspace_invitations_invitee_idx" ON "workspace_invitations" USING btree ("invitee_account_id","status","created_at"); +CREATE UNIQUE INDEX "workspace_invitations_token_unique" ON "workspace_invitations" USING btree ("token_hash") WHERE "token_hash" is not null; +CREATE UNIQUE INDEX "workspace_invitations_pending_account_unique" ON "workspace_invitations" USING btree ("workspace_id","invitee_account_id") WHERE "kind" = 'account' and "status" = 'pending'; +CREATE UNIQUE INDEX "workspaces_account_data_unique" ON "workspaces" USING btree ("account_id") WHERE "workspace_type" = 'account-data' and "deleted_at" is null; diff --git a/apps/server/drizzle/0005_workspace_object_boundaries.sql b/apps/server/drizzle/0005_workspace_object_boundaries.sql new file mode 100644 index 0000000..c142551 --- /dev/null +++ b/apps/server/drizzle/0005_workspace_object_boundaries.sql @@ -0,0 +1,14 @@ +-- Remove current objects written into the wrong workspace type by unpublished +-- protocol drafts. Immutable object versions remain available as retained +-- history, but the invalid objects no longer participate in bootstrap or +-- current-content accounting. +DELETE FROM "objects" AS "o" +USING "workspaces" AS "w" +WHERE "o"."workspace_id" = "w"."id" + AND ( + ("w"."workspace_type" = 'account-data' AND "o"."kind" IN ('note', 'folder')) + OR ( + "w"."workspace_type" = 'library' + AND "o"."kind" IN ('canvas', 'tag', 'mark', 'record', 'conversation', 'message', 'memory', 'setting') + ) + ); diff --git a/apps/server/drizzle/meta/0002_snapshot.json b/apps/server/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..cb98877 --- /dev/null +++ b/apps/server/drizzle/meta/0002_snapshot.json @@ -0,0 +1,11453 @@ +{ + "id": "a76dbd30-2474-48bd-adb7-f17389833a92", + "prevId": "03aca076-eb46-47a7-b0c4-a2bc8dfb9ee6", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account_action_tokens": { + "name": "account_action_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "identity_id": { + "name": "identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "account_action_token_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token_key_id": { + "name": "token_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_normalized": { + "name": "target_normalized", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "requested_ip_hash": { + "name": "requested_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_action_tokens_hash_unique": { + "name": "account_action_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_action_tokens_account_idx": { + "name": "account_action_tokens_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_action_tokens_account_id_accounts_id_fk": { + "name": "account_action_tokens_account_id_accounts_id_fk", + "tableFrom": "account_action_tokens", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_action_tokens_identity_id_account_identities_id_fk": { + "name": "account_action_tokens_identity_id_account_identities_id_fk", + "tableFrom": "account_action_tokens", + "tableTo": "account_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_deletion_cases": { + "name": "account_deletion_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "deletion_case_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cancel_until": { + "name": "cancel_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "purge_after": { + "name": "purge_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_credential_hash": { + "name": "cancel_credential_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purge_manifest_ref": { + "name": "purge_manifest_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purge_manifest_hash": { + "name": "purge_manifest_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purge_manifest": { + "name": "purge_manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_deletion_cases_active_account_unique": { + "name": "account_deletion_cases_active_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_deletion_cases\".\"account_id\" is not null and \"account_deletion_cases\".\"status\" in ('requested', 'cooling_off', 'scheduled', 'held', 'purging')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_deletion_cases_active_subject_unique": { + "name": "account_deletion_cases_active_subject_unique", + "columns": [ + { + "expression": "subject_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_deletion_cases\".\"status\" in ('requested', 'cooling_off', 'scheduled', 'held', 'purging')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_deletion_fences": { + "name": "account_deletion_fences", + "schema": "", + "columns": { + "account_uuid": { + "name": "account_uuid", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "state": { + "name": "state", + "type": "deletion_fence_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hold_revision": { + "name": "hold_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "blocks_domain_writes": { + "name": "blocks_domain_writes", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_identities": { + "name": "account_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "account_identity_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_identifier": { + "name": "normalized_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_identities_active_unique": { + "name": "account_identities_active_unique", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_identities\".\"disabled_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_identities_account_idx": { + "name": "account_identities_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_identities_account_id_accounts_id_fk": { + "name": "account_identities_account_id_accounts_id_fk", + "tableFrom": "account_identities", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_login_claim_conflicts": { + "name": "account_login_claim_conflicts", + "schema": "", + "columns": { + "normalized_login_key": { + "name": "normalized_login_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "candidate_account_id": { + "name": "candidate_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "candidate_identity_id": { + "name": "candidate_identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "candidate_kind": { + "name": "candidate_kind", + "type": "account_login_claim_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'quarantined'" + }, + "resolution_ref": { + "name": "resolution_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "account_login_claim_conflicts_status_idx": { + "name": "account_login_claim_conflicts_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_login_claim_conflicts_candidate_account_id_accounts_id_fk": { + "name": "account_login_claim_conflicts_candidate_account_id_accounts_id_fk", + "tableFrom": "account_login_claim_conflicts", + "tableTo": "accounts", + "columnsFrom": [ + "candidate_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_login_claim_conflicts_candidate_identity_id_account_identities_id_fk": { + "name": "account_login_claim_conflicts_candidate_identity_id_account_identities_id_fk", + "tableFrom": "account_login_claim_conflicts", + "tableTo": "account_identities", + "columnsFrom": [ + "candidate_identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_login_claim_conflicts_normalized_login_key_candidate_account_id_candidate_kind_pk": { + "name": "account_login_claim_conflicts_normalized_login_key_candidate_account_id_candidate_kind_pk", + "columns": [ + "normalized_login_key", + "candidate_account_id", + "candidate_kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_login_claims": { + "name": "account_login_claims", + "schema": "", + "columns": { + "normalized_login_key": { + "name": "normalized_login_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "account_login_claim_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reusable_after": { + "name": "reusable_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_login_claims_account_id_accounts_id_fk": { + "name": "account_login_claims_account_id_accounts_id_fk", + "tableFrom": "account_login_claims", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "account_login_claims_identity_id_account_identities_id_fk": { + "name": "account_login_claims_identity_id_account_identities_id_fk", + "tableFrom": "account_login_claims", + "tableTo": "account_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_service_audit_events": { + "name": "account_service_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_service_audit_events_actor_idx": { + "name": "account_service_audit_events_actor_idx", + "columns": [ + { + "expression": "actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_service_audit_events_target_idx": { + "name": "account_service_audit_events_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "account_service_audit_events_actor_type_check": { + "name": "account_service_audit_events_actor_type_check", + "value": "\"account_service_audit_events\".\"actor_type\" in ('account', 'staff', 'system', 'webhook')" + }, + "account_service_audit_events_action_check": { + "name": "account_service_audit_events_action_check", + "value": "length(\"account_service_audit_events\".\"action\") > 0" + }, + "account_service_audit_events_target_type_check": { + "name": "account_service_audit_events_target_type_check", + "value": "length(\"account_service_audit_events\".\"target_type\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.account_subscriptions": { + "name": "account_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_version_id": { + "name": "plan_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "account_subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "is_current": { + "name": "is_current", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_revision": { + "name": "provider_revision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "grace_ends_at": { + "name": "grace_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_subscriptions_provider_subscription_unique": { + "name": "account_subscriptions_provider_subscription_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_subscriptions_current_account_unique": { + "name": "account_subscriptions_current_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_subscriptions\".\"account_id\" is not null and \"account_subscriptions\".\"is_current\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_subscriptions_account_status_idx": { + "name": "account_subscriptions_account_status_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_subscriptions_account_id_accounts_id_fk": { + "name": "account_subscriptions_account_id_accounts_id_fk", + "tableFrom": "account_subscriptions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "account_subscriptions_plan_version_id_billing_plan_versions_id_fk": { + "name": "account_subscriptions_plan_version_id_billing_plan_versions_id_fk", + "tableFrom": "account_subscriptions", + "tableTo": "billing_plan_versions", + "columnsFrom": [ + "plan_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_usage": { + "name": "account_usage", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "active_object_bytes": { + "name": "active_object_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_crdt_bytes": { + "name": "active_crdt_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_blob_bytes": { + "name": "active_blob_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "reserved_blob_bytes": { + "name": "reserved_blob_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "retained_bytes": { + "name": "retained_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_objects": { + "name": "active_objects", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_devices": { + "name": "active_devices", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_workspaces": { + "name": "active_workspaces", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_usage_account_id_accounts_id_fk": { + "name": "account_usage_account_id_accounts_id_fk", + "tableFrom": "account_usage", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled_at": { + "name": "totp_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "identity_state": { + "name": "identity_state", + "type": "account_identity_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'legacy_migration'" + }, + "credential_epoch": { + "name": "credential_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_login_unique": { + "name": "accounts_login_unique", + "columns": [ + { + "expression": "lower(\"login\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_audit_logs": { + "name": "admin_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_audit_logs_actor_idx": { + "name": "admin_audit_logs_actor_idx", + "columns": [ + { + "expression": "actor_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_audit_logs_created_idx": { + "name": "admin_audit_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_audit_logs_actor_account_id_accounts_id_fk": { + "name": "admin_audit_logs_actor_account_id_accounts_id_fk", + "tableFrom": "admin_audit_logs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_backups": { + "name": "admin_backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "admin_backups_filename_unique": { + "name": "admin_backups_filename_unique", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_backups_job_id_admin_jobs_id_fk": { + "name": "admin_backups_job_id_admin_jobs_id_fk", + "tableFrom": "admin_backups", + "tableTo": "admin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_jobs": { + "name": "admin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_jobs_created_idx": { + "name": "admin_jobs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_jobs_status_idx": { + "name": "admin_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_jobs_actor_account_id_accounts_id_fk": { + "name": "admin_jobs_actor_account_id_accounts_id_fk", + "tableFrom": "admin_jobs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_test_objects": { + "name": "admin_test_objects", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_test_objects_created_idx": { + "name": "admin_test_objects_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_test_objects_workspace_id_workspaces_id_fk": { + "name": "admin_test_objects_workspace_id_workspaces_id_fk", + "tableFrom": "admin_test_objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admin_test_objects_created_by_account_id_accounts_id_fk": { + "name": "admin_test_objects_created_by_account_id_accounts_id_fk", + "tableFrom": "admin_test_objects", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "admin_test_objects_workspace_id_object_id_pk": { + "name": "admin_test_objects_workspace_id_object_id_pk", + "columns": [ + "workspace_id", + "object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_jobs": { + "name": "background_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_generation": { + "name": "queue_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "min_handler_version": { + "name": "min_handler_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_account_id": { + "name": "target_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "background_jobs_idempotency_unique": { + "name": "background_jobs_idempotency_unique", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_jobs_claim_idx": { + "name": "background_jobs_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_jobs_actor_account_id_accounts_id_fk": { + "name": "background_jobs_actor_account_id_accounts_id_fk", + "tableFrom": "background_jobs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "background_jobs_target_account_id_accounts_id_fk": { + "name": "background_jobs_target_account_id_accounts_id_fk", + "tableFrom": "background_jobs", + "tableTo": "accounts", + "columnsFrom": [ + "target_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "background_jobs_payload_version_check": { + "name": "background_jobs_payload_version_check", + "value": "\"background_jobs\".\"payload_version\" > 0" + }, + "background_jobs_queue_generation_check": { + "name": "background_jobs_queue_generation_check", + "value": "\"background_jobs\".\"queue_generation\" > 0" + }, + "background_jobs_min_handler_version_check": { + "name": "background_jobs_min_handler_version_check", + "value": "\"background_jobs\".\"min_handler_version\" > 0" + }, + "background_jobs_attempt_check": { + "name": "background_jobs_attempt_check", + "value": "\"background_jobs\".\"attempt\" >= 0" + }, + "background_jobs_max_attempts_check": { + "name": "background_jobs_max_attempts_check", + "value": "\"background_jobs\".\"max_attempts\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.backup_artifacts": { + "name": "backup_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_run_id": { + "name": "backup_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "relative_path": { + "name": "relative_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backup_artifacts_run_path_unique": { + "name": "backup_artifacts_run_path_unique", + "columns": [ + { + "expression": "backup_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "relative_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backup_artifacts_run_idx": { + "name": "backup_artifacts_run_idx", + "columns": [ + { + "expression": "backup_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "backup_artifacts_backup_run_id_backup_runs_id_fk": { + "name": "backup_artifacts_backup_run_id_backup_runs_id_fk", + "tableFrom": "backup_artifacts", + "tableTo": "backup_runs", + "columnsFrom": [ + "backup_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "backup_artifacts_size_check": { + "name": "backup_artifacts_size_check", + "value": "\"backup_artifacts\".\"size\" >= 0" + }, + "backup_artifacts_path_check": { + "name": "backup_artifacts_path_check", + "value": "length(\"backup_artifacts\".\"relative_path\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.backup_policies": { + "name": "backup_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_ref": { + "name": "target_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retention": { + "name": "retention", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encryption_key_id": { + "name": "encryption_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_runs": { + "name": "backup_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "generation": { + "name": "generation", + "type": "bigserial", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "backup_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "snapshot_at": { + "name": "snapshot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manifest_ref": { + "name": "manifest_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_bytes": { + "name": "database_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "blob_count": { + "name": "blob_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "blob_bytes": { + "name": "blob_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint": { + "name": "checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "backup_runs_generation_unique": { + "name": "backup_runs_generation_unique", + "columns": [ + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backup_runs_status_idx": { + "name": "backup_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "backup_runs_policy_id_backup_policies_id_fk": { + "name": "backup_runs_policy_id_backup_policies_id_fk", + "tableFrom": "backup_runs", + "tableTo": "backup_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_account_states": { + "name": "billing_account_states", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "current_subscription_id": { + "name": "current_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purchase_intent_id": { + "name": "purchase_intent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_account_states_account_id_accounts_id_fk": { + "name": "billing_account_states_account_id_accounts_id_fk", + "tableFrom": "billing_account_states", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_account_states_current_subscription_id_account_subscriptions_id_fk": { + "name": "billing_account_states_current_subscription_id_account_subscriptions_id_fk", + "tableFrom": "billing_account_states", + "tableTo": "account_subscriptions", + "columnsFrom": [ + "current_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "billing_account_states_purchase_intent_id_billing_checkout_sessions_id_fk": { + "name": "billing_account_states_purchase_intent_id_billing_checkout_sessions_id_fk", + "tableFrom": "billing_account_states", + "tableTo": "billing_checkout_sessions", + "columnsFrom": [ + "purchase_intent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_account_states_current_or_intent_check": { + "name": "billing_account_states_current_or_intent_check", + "value": "not (\"billing_account_states\".\"current_subscription_id\" is not null and \"billing_account_states\".\"purchase_intent_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.billing_checkout_sessions": { + "name": "billing_checkout_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_checkout_id": { + "name": "provider_checkout_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "price_mapping_id": { + "name": "price_mapping_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plan_version_id": { + "name": "plan_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_checkout_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending_provider'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_checkout_sessions_account_idempotency_unique": { + "name": "billing_checkout_sessions_account_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_sessions_provider_checkout_unique": { + "name": "billing_checkout_sessions_provider_checkout_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_sessions\".\"provider_checkout_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_sessions_active_account_unique": { + "name": "billing_checkout_sessions_active_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_sessions\".\"status\" in ('pending_provider','open','completed','reconciling')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_checkout_sessions_account_id_accounts_id_fk": { + "name": "billing_checkout_sessions_account_id_accounts_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_checkout_sessions_subscription_id_account_subscriptions_id_fk": { + "name": "billing_checkout_sessions_subscription_id_account_subscriptions_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "account_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "billing_checkout_sessions_price_mapping_id_billing_price_mappings_id_fk": { + "name": "billing_checkout_sessions_price_mapping_id_billing_price_mappings_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "billing_price_mappings", + "columnsFrom": [ + "price_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_sessions_plan_version_id_billing_plan_versions_id_fk": { + "name": "billing_checkout_sessions_plan_version_id_billing_plan_versions_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "billing_plan_versions", + "columnsFrom": [ + "plan_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_customers": { + "name": "billing_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_customers_provider_customer_unique": { + "name": "billing_customers_provider_customer_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_customers_account_unique": { + "name": "billing_customers_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_customers\".\"account_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_customers_account_id_accounts_id_fk": { + "name": "billing_customers_account_id_accounts_id_fk", + "tableFrom": "billing_customers", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_plan_versions": { + "name": "billing_plan_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_key": { + "name": "plan_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "interval": { + "name": "interval", + "type": "billing_plan_interval", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entitlement_schema_version": { + "name": "entitlement_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entitlements": { + "name": "entitlements", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "active_from": { + "name": "active_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_plan_versions_key_version_unique": { + "name": "billing_plan_versions_key_version_unique", + "columns": [ + { + "expression": "plan_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_plan_versions_amount_check": { + "name": "billing_plan_versions_amount_check", + "value": "\"billing_plan_versions\".\"amount_minor\" >= 0" + }, + "billing_plan_versions_version_check": { + "name": "billing_plan_versions_version_check", + "value": "\"billing_plan_versions\".\"version\" > 0" + }, + "billing_plan_versions_schema_check": { + "name": "billing_plan_versions_schema_check", + "value": "\"billing_plan_versions\".\"entitlement_schema_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_price_mappings": { + "name": "billing_price_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_version_id": { + "name": "plan_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_environment": { + "name": "provider_environment", + "type": "billing_provider_environment", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_price_id": { + "name": "provider_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval": { + "name": "interval", + "type": "billing_plan_interval", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_price_mappings_provider_price_unique": { + "name": "billing_price_mappings_provider_price_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_environment", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_price_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_price_mappings_plan_provider_unique": { + "name": "billing_price_mappings_plan_provider_unique", + "columns": [ + { + "expression": "plan_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_environment", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "currency", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "interval", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_price_mappings_plan_version_id_billing_plan_versions_id_fk": { + "name": "billing_price_mappings_plan_version_id_billing_plan_versions_id_fk", + "tableFrom": "billing_price_mappings", + "tableTo": "billing_plan_versions", + "columnsFrom": [ + "plan_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_webhook_events": { + "name": "billing_webhook_events", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature_verified_at": { + "name": "signature_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "payload_redacted": { + "name": "payload_redacted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_webhook_event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_webhook_events_claim_idx": { + "name": "billing_webhook_events_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_webhook_events_provider_provider_event_id_pk": { + "name": "billing_webhook_events_provider_provider_event_id_pk", + "columns": [ + "provider", + "provider_event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob_upload_parts": { + "name": "blob_upload_parts", + "schema": "", + "columns": { + "upload_id": { + "name": "upload_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "part_number": { + "name": "part_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "blob_upload_parts_upload_id_blob_uploads_id_fk": { + "name": "blob_upload_parts_upload_id_blob_uploads_id_fk", + "tableFrom": "blob_upload_parts", + "tableTo": "blob_uploads", + "columnsFrom": [ + "upload_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "blob_upload_parts_upload_id_part_number_pk": { + "name": "blob_upload_parts_upload_id_part_number_pk", + "columns": [ + "upload_id", + "part_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob_uploads": { + "name": "blob_uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blob_id": { + "name": "blob_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_reservation_id": { + "name": "usage_reservation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sync_epoch": { + "name": "sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expected_size": { + "name": "expected_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "received_size": { + "name": "received_size", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "completing_at": { + "name": "completing_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blob_uploads_workspace_blob_unique": { + "name": "blob_uploads_workspace_blob_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blob_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blob_uploads_workspace_blob_idx": { + "name": "blob_uploads_workspace_blob_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blob_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blob_uploads_expiry_idx": { + "name": "blob_uploads_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blob_uploads_usage_reservation_unique": { + "name": "blob_uploads_usage_reservation_unique", + "columns": [ + { + "expression": "usage_reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"blob_uploads\".\"usage_reservation_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blob_uploads_workspace_id_workspaces_id_fk": { + "name": "blob_uploads_workspace_id_workspaces_id_fk", + "tableFrom": "blob_uploads", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blob_uploads_usage_reservation_id_usage_reservations_id_fk": { + "name": "blob_uploads_usage_reservation_id_usage_reservations_id_fk", + "tableFrom": "blob_uploads", + "tableTo": "usage_reservations", + "columnsFrom": [ + "usage_reservation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blobs": { + "name": "blobs", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blob_id": { + "name": "blob_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "blob_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_referenced_at": { + "name": "last_referenced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blobs_storage_key_unique": { + "name": "blobs_storage_key_unique", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blobs_gc_idx": { + "name": "blobs_gc_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_referenced_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blobs_workspace_id_workspaces_id_fk": { + "name": "blobs_workspace_id_workspaces_id_fk", + "tableFrom": "blobs", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "blobs_workspace_id_blob_id_pk": { + "name": "blobs_workspace_id_blob_id_pk", + "columns": [ + "workspace_id", + "blob_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bootstrap_credentials": { + "name": "bootstrap_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source": { + "name": "source", + "type": "bootstrap_credential_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token_key_id": { + "name": "token_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hint": { + "name": "token_hint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bootstrap_credentials_token_hash_unique": { + "name": "bootstrap_credentials_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bootstrap_credentials_active_idx": { + "name": "bootstrap_credentials_active_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bootstrap_sessions": { + "name": "bootstrap_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_sequence": { + "name": "snapshot_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bootstrap_sessions_expiry_idx": { + "name": "bootstrap_sessions_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bootstrap_sessions_device_idx": { + "name": "bootstrap_sessions_device_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bootstrap_sessions_workspace_id_workspaces_id_fk": { + "name": "bootstrap_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "bootstrap_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bootstrap_sessions_device_id_devices_id_fk": { + "name": "bootstrap_sessions_device_id_devices_id_fk", + "tableFrom": "bootstrap_sessions", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenge_consumptions": { + "name": "challenge_consumptions", + "schema": "", + "columns": { + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "digest_key_id": { + "name": "digest_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_hostname": { + "name": "expected_hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_claims_hash": { + "name": "verified_claims_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changes": { + "name": "changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "change_type": { + "name": "change_type", + "type": "change_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "changes_workspace_sequence_unique": { + "name": "changes_workspace_sequence_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changes_workspace_object_idx": { + "name": "changes_workspace_object_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changes_created_idx": { + "name": "changes_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changes_workspace_id_workspaces_id_fk": { + "name": "changes_workspace_id_workspaces_id_fk", + "tableFrom": "changes", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changes_source_device_id_devices_id_fk": { + "name": "changes_source_device_id_devices_id_fk", + "tableFrom": "changes", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_requests": { + "name": "data_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_idempotency_key": { + "name": "client_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "data_request_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "request_channel": { + "name": "request_channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_ref": { + "name": "result_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_requests_subject_idempotency_unique": { + "name": "data_requests_subject_idempotency_unique", + "columns": [ + { + "expression": "subject_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_requests_status_idx": { + "name": "data_requests_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_requests_account_id_accounts_id_fk": { + "name": "data_requests_account_id_accounts_id_fk", + "tableFrom": "data_requests", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deletion_case_steps": { + "name": "deletion_case_steps", + "schema": "", + "columns": { + "deletion_case_id": { + "name": "deletion_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "handler": { + "name": "handler", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "deletion_step_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deletion_case_steps_deletion_case_id_account_deletion_cases_id_fk": { + "name": "deletion_case_steps_deletion_case_id_account_deletion_cases_id_fk", + "tableFrom": "deletion_case_steps", + "tableTo": "account_deletion_cases", + "columnsFrom": [ + "deletion_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deletion_case_steps_deletion_case_id_handler_pk": { + "name": "deletion_case_steps_deletion_case_id_handler_pk", + "columns": [ + "deletion_case_id", + "handler" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deletion_ledger": { + "name": "deletion_ledger", + "schema": "", + "columns": { + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "hash_key_id": { + "name": "hash_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deletion_case_id": { + "name": "deletion_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "minimum_backup_generation": { + "name": "minimum_backup_generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "minimum_database_lsn": { + "name": "minimum_database_lsn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "receipt_hash": { + "name": "receipt_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deletion_ledger_outbox": { + "name": "deletion_ledger_outbox", + "schema": "", + "columns": { + "deletion_case_id": { + "name": "deletion_case_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deletion_ledger_outbox_claim_idx": { + "name": "deletion_ledger_outbox_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deletion_ledger_outbox_deletion_case_id_account_deletion_cases_id_fk": { + "name": "deletion_ledger_outbox_deletion_case_id_account_deletion_cases_id_fk", + "tableFrom": "deletion_ledger_outbox", + "tableTo": "account_deletion_cases", + "columnsFrom": [ + "deletion_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deletion_ledger_outbox_idempotency_key_unique": { + "name": "deletion_ledger_outbox_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_settings": { + "name": "deployment_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "deployment_mode": { + "name": "deployment_mode", + "type": "deployment_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "registration_policy": { + "name": "registration_policy", + "type": "registration_policy", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "self_hosted_lifecycle": { + "name": "self_hosted_lifecycle", + "type": "self_hosted_lifecycle", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "admin_repair_required": { + "name": "admin_repair_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "instance_auth_epoch": { + "name": "instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "token_not_before": { + "name": "token_not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auth_epoch_enforced": { + "name": "auth_epoch_enforced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "runtime_configuration": { + "name": "runtime_configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "configuration_revision": { + "name": "configuration_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "1" + }, + "initialized_at": { + "name": "initialized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "initialized_by_account_id": { + "name": "initialized_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_authorizations": { + "name": "device_authorizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_name": { + "name": "device_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encryption_public_key": { + "name": "encryption_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "device_authorization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "approved_credential_epoch": { + "name": "approved_credential_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "approved_instance_auth_epoch": { + "name": "approved_instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "approved_issued_at": { + "name": "approved_issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_authorizations_device_code_unique": { + "name": "device_authorizations_device_code_unique", + "columns": [ + { + "expression": "device_code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_authorizations_user_code_unique": { + "name": "device_authorizations_user_code_unique", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_authorizations_expiry_idx": { + "name": "device_authorizations_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_authorizations_account_idx": { + "name": "device_authorizations_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_authorizations_account_id_accounts_id_fk": { + "name": "device_authorizations_account_id_accounts_id_fk", + "tableFrom": "device_authorizations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_cursors": { + "name": "device_cursors", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "acknowledged_sequence": { + "name": "acknowledged_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_cursors_workspace_id_workspaces_id_fk": { + "name": "device_cursors_workspace_id_workspaces_id_fk", + "tableFrom": "device_cursors", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "device_cursors_device_id_devices_id_fk": { + "name": "device_cursors_device_id_devices_id_fk", + "tableFrom": "device_cursors", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_cursors_workspace_id_device_id_pk": { + "name": "device_cursors_workspace_id_device_id_pk", + "columns": [ + "workspace_id", + "device_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_pairings": { + "name": "device_pairings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_epoch": { + "name": "credential_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "instance_auth_epoch": { + "name": "instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_pairings_token_hash_unique": { + "name": "device_pairings_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_pairings_account_idx": { + "name": "device_pairings_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_pairings_expiry_idx": { + "name": "device_pairings_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_pairings_account_id_accounts_id_fk": { + "name": "device_pairings_account_id_accounts_id_fk", + "tableFrom": "device_pairings", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encryption_public_key": { + "name": "encryption_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "devices_account_idx": { + "name": "devices_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "devices_account_id_accounts_id_fk": { + "name": "devices_account_id_accounts_id_fk", + "tableFrom": "devices", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entitlement_grants": { + "name": "entitlement_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "entitlement_grant_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entitlements": { + "name": "entitlements", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "entitlement_grants_source_unique": { + "name": "entitlement_grants_source_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entitlement_grants_active_idx": { + "name": "entitlement_grants_active_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlement_grants_account_id_accounts_id_fk": { + "name": "entitlement_grants_account_id_accounts_id_fk", + "tableFrom": "entitlement_grants", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entitlement_grants_created_by_accounts_id_fk": { + "name": "entitlement_grants_created_by_accounts_id_fk", + "tableFrom": "entitlement_grants", + "tableTo": "accounts", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "entitlement_grants_schema_check": { + "name": "entitlement_grants_schema_check", + "value": "\"entitlement_grants\".\"schema_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.legal_holds": { + "name": "legal_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authority": { + "name": "authority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'platform-admin'" + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_staff_id": { + "name": "approved_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "released_by": { + "name": "released_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_by_staff_id": { + "name": "released_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "release_reason_code": { + "name": "release_reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "legal_holds_active_account_unique": { + "name": "legal_holds_active_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"legal_holds\".\"released_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "legal_holds_account_idx": { + "name": "legal_holds_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "approved_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "legal_holds_account_id_accounts_id_fk": { + "name": "legal_holds_account_id_accounts_id_fk", + "tableFrom": "legal_holds", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_approved_by_accounts_id_fk": { + "name": "legal_holds_approved_by_accounts_id_fk", + "tableFrom": "legal_holds", + "tableTo": "accounts", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_approved_by_staff_id_staff_principals_id_fk": { + "name": "legal_holds_approved_by_staff_id_staff_principals_id_fk", + "tableFrom": "legal_holds", + "tableTo": "staff_principals", + "columnsFrom": [ + "approved_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_released_by_accounts_id_fk": { + "name": "legal_holds_released_by_accounts_id_fk", + "tableFrom": "legal_holds", + "tableTo": "accounts", + "columnsFrom": [ + "released_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_released_by_staff_id_staff_principals_id_fk": { + "name": "legal_holds_released_by_staff_id_staff_principals_id_fk", + "tableFrom": "legal_holds", + "tableTo": "staff_principals", + "columnsFrom": [ + "released_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail_secret_payloads": { + "name": "mail_secret_payloads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "erased_at": { + "name": "erased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mail_secret_payloads_expiry_idx": { + "name": "mail_secret_payloads_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mail_secret_payloads_version_check": { + "name": "mail_secret_payloads_version_check", + "value": "\"mail_secret_payloads\".\"payload_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.maintenance_state": { + "name": "maintenance_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "mode": { + "name": "mode", + "type": "maintenance_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entered_at": { + "name": "entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_versions": { + "name": "object_versions", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_object_id": { + "name": "parent_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name_ciphertext": { + "name": "name_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_blind_index": { + "name": "name_blind_index", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "blob_refs": { + "name": "blob_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_versions_created_idx": { + "name": "object_versions_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_versions_sequence_idx": { + "name": "object_versions_sequence_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_versions_workspace_id_workspaces_id_fk": { + "name": "object_versions_workspace_id_workspaces_id_fk", + "tableFrom": "object_versions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "object_versions_source_device_id_devices_id_fk": { + "name": "object_versions_source_device_id_devices_id_fk", + "tableFrom": "object_versions", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "object_versions_workspace_key_fk": { + "name": "object_versions_workspace_key_fk", + "tableFrom": "object_versions", + "tableTo": "workspace_keys", + "columnsFrom": [ + "workspace_id", + "key_version" + ], + "columnsTo": [ + "workspace_id", + "key_version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "object_versions_workspace_id_object_id_revision_pk": { + "name": "object_versions_workspace_id_object_id_revision_pk", + "columns": [ + "workspace_id", + "object_id", + "revision" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.objects": { + "name": "objects", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_object_id": { + "name": "parent_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name_ciphertext": { + "name": "name_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_blind_index": { + "name": "name_blind_index", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_revision": { + "name": "current_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "blob_refs": { + "name": "blob_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "objects_workspace_kind_idx": { + "name": "objects_workspace_kind_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_workspace_updated_idx": { + "name": "objects_workspace_updated_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_sibling_name_blind_idx": { + "name": "objects_sibling_name_blind_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name_blind_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"objects\".\"deleted_at\" is null and \"objects\".\"name_blind_index\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "objects_workspace_id_workspaces_id_fk": { + "name": "objects_workspace_id_workspaces_id_fk", + "tableFrom": "objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "objects_workspace_key_fk": { + "name": "objects_workspace_key_fk", + "tableFrom": "objects", + "tableTo": "workspace_keys", + "columnsFrom": [ + "workspace_id", + "key_version" + ], + "columnsTo": [ + "workspace_id", + "key_version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "objects_workspace_id_object_id_pk": { + "name": "objects_workspace_id_object_id_pk", + "columns": [ + "workspace_id", + "object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operations": { + "name": "operations", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_revision": { + "name": "result_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "result_sequence": { + "name": "result_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "operations_created_idx": { + "name": "operations_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "operations_workspace_id_workspaces_id_fk": { + "name": "operations_workspace_id_workspaces_id_fk", + "tableFrom": "operations", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "operations_source_device_id_devices_id_fk": { + "name": "operations_source_device_id_devices_id_fk", + "tableFrom": "operations", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "operations_workspace_id_operation_id_pk": { + "name": "operations_workspace_id_operation_id_pk", + "columns": [ + "workspace_id", + "operation_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_messages": { + "name": "outbox_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_or_event": { + "name": "template_or_event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_ref": { + "name": "recipient_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "secret_payload_ref": { + "name": "secret_payload_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "outbox_message_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_messages_idempotency_unique": { + "name": "outbox_messages_idempotency_unique", + "columns": [ + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_messages_claim_idx": { + "name": "outbox_messages_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_acceptances": { + "name": "policy_acceptances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_snapshot": { + "name": "subject_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "policy_document_id": { + "name": "policy_document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_prefix_hash": { + "name": "ip_prefix_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent_family": { + "name": "user_agent_family", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence_version": { + "name": "evidence_version", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "policy_acceptances_subject_idx": { + "name": "policy_acceptances_subject_idx", + "columns": [ + { + "expression": "subject_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_acceptances_account_id_accounts_id_fk": { + "name": "policy_acceptances_account_id_accounts_id_fk", + "tableFrom": "policy_acceptances", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "policy_acceptances_policy_document_id_policy_documents_id_fk": { + "name": "policy_acceptances_policy_document_id_policy_documents_id_fk", + "tableFrom": "policy_acceptances", + "tableTo": "policy_documents", + "columnsFrom": [ + "policy_document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_documents": { + "name": "policy_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_ref": { + "name": "content_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonicalization_version": { + "name": "canonicalization_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "requires_reacceptance": { + "name": "requires_reacceptance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "policy_documents_version_locale_unique": { + "name": "policy_documents_version_locale_unique", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "locale", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "issued_instance_auth_epoch": { + "name": "issued_instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotation_request_id": { + "name": "rotation_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rotation_response_ciphertext": { + "name": "rotation_response_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rotation_response_expires_at": { + "name": "rotation_response_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "refresh_tokens_hash_unique": { + "name": "refresh_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "refresh_tokens_device_idx": { + "name": "refresh_tokens_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "refresh_tokens_account_id_accounts_id_fk": { + "name": "refresh_tokens_account_id_accounts_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "refresh_tokens_device_id_devices_id_fk": { + "name": "refresh_tokens_device_id_devices_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_invitation_uses": { + "name": "registration_invitation_uses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invitation_id": { + "name": "invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "registration_invitation_uses_account_unique": { + "name": "registration_invitation_uses_account_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "registration_invitation_uses_invitation_idx": { + "name": "registration_invitation_uses_invitation_idx", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "registration_invitation_uses_invitation_id_registration_invitations_id_fk": { + "name": "registration_invitation_uses_invitation_id_registration_invitations_id_fk", + "tableFrom": "registration_invitation_uses", + "tableTo": "registration_invitations", + "columnsFrom": [ + "invitation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "registration_invitation_uses_account_id_accounts_id_fk": { + "name": "registration_invitation_uses_account_id_accounts_id_fk", + "tableFrom": "registration_invitation_uses", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_invitations": { + "name": "registration_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_key_id": { + "name": "token_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hint": { + "name": "token_hint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "invitation_actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_snapshot": { + "name": "creator_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "bound_email_normalized": { + "name": "bound_email_normalized", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sent_at": { + "name": "last_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replaces_invitation_id": { + "name": "replaces_invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "registration_invitations_token_hash_unique": { + "name": "registration_invitations_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "registration_invitations_active_idx": { + "name": "registration_invitations_active_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "registration_invitations_use_count_check": { + "name": "registration_invitations_use_count_check", + "value": "\"registration_invitations\".\"max_uses\" > 0 and \"registration_invitations\".\"use_count\" >= 0 and \"registration_invitations\".\"use_count\" <= \"registration_invitations\".\"max_uses\"" + } + }, + "isRLSEnabled": false + }, + "public.restore_credential_reviews": { + "name": "restore_credential_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "restore_marker_id": { + "name": "restore_marker_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operator_kind": { + "name": "operator_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local-operator'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "restore_credential_reviews_marker_account_unique": { + "name": "restore_credential_reviews_marker_account_unique", + "columns": [ + { + "expression": "restore_marker_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "restore_credential_reviews_restore_marker_id_restore_markers_id_fk": { + "name": "restore_credential_reviews_restore_marker_id_restore_markers_id_fk", + "tableFrom": "restore_credential_reviews", + "tableTo": "restore_markers", + "columnsFrom": [ + "restore_marker_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "restore_credential_reviews_account_id_accounts_id_fk": { + "name": "restore_credential_reviews_account_id_accounts_id_fk", + "tableFrom": "restore_credential_reviews", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restore_drills": { + "name": "restore_drills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_run_id": { + "name": "backup_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "restore_drill_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restore_drills_backup_run_id_backup_runs_id_fk": { + "name": "restore_drills_backup_run_id_backup_runs_id_fk", + "tableFrom": "restore_drills", + "tableTo": "backup_runs", + "columnsFrom": [ + "backup_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restore_markers": { + "name": "restore_markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "restore_marker_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "old_sync_epoch": { + "name": "old_sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "new_sync_epoch": { + "name": "new_sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "restored_through_sequence_by_workspace": { + "name": "restored_through_sequence_by_workspace", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sanitation_status": { + "name": "sanitation_status", + "type": "restore_sanitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "auth_epoch_after": { + "name": "auth_epoch_after", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "bootstrap_token_cutoff": { + "name": "bootstrap_token_cutoff", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bootstrap_reissue_required": { + "name": "bootstrap_reissue_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "restore_markers_new_epoch_unique": { + "name": "restore_markers_new_epoch_unique", + "columns": [ + { + "expression": "new_sync_epoch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_events": { + "name": "risk_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "identity_hash": { + "name": "identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ip_prefix_hash": { + "name": "ip_prefix_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent_family": { + "name": "user_agent_family", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_codes": { + "name": "reason_codes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_events_account_created_idx": { + "name": "risk_events_account_created_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_events_event_created_idx": { + "name": "risk_events_event_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_events_account_id_accounts_id_fk": { + "name": "risk_events_account_id_accounts_id_fk", + "tableFrom": "risk_events", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_provider_events": { + "name": "risk_provider_events", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature_verified_at": { + "name": "signature_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload_redacted": { + "name": "payload_redacted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "risk_provider_event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "risk_provider_events_claim_idx": { + "name": "risk_provider_events_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "risk_provider_events_provider_provider_event_id_pk": { + "name": "risk_provider_events_provider_provider_event_id_pk", + "columns": [ + "provider", + "provider_event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_restrictions": { + "name": "risk_restrictions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_type": { + "name": "subject_type", + "type": "risk_restriction_subject_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subject_ref": { + "name": "subject_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "risk_restriction_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "risk_restriction_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "risk_restriction_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_staff_id": { + "name": "created_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_by_staff_id": { + "name": "revoked_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_restrictions_active_unique": { + "name": "risk_restrictions_active_unique", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"risk_restrictions\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_restrictions_subject_idx": { + "name": "risk_restrictions_subject_idx", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_restrictions_created_by_staff_id_staff_principals_id_fk": { + "name": "risk_restrictions_created_by_staff_id_staff_principals_id_fk", + "tableFrom": "risk_restrictions", + "tableTo": "staff_principals", + "columnsFrom": [ + "created_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "risk_restrictions_revoked_by_staff_id_staff_principals_id_fk": { + "name": "risk_restrictions_revoked_by_staff_id_staff_principals_id_fk", + "tableFrom": "risk_restrictions", + "tableTo": "staff_principals", + "columnsFrom": [ + "revoked_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_metadata": { + "name": "server_metadata", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_principals": { + "name": "staff_principals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "external_issuer": { + "name": "external_issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_subject": { + "name": "external_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_login": { + "name": "local_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_password_hash": { + "name": "local_password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "staff_principals_issuer_subject_unique": { + "name": "staff_principals_issuer_subject_unique", + "columns": [ + { + "expression": "external_issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "staff_principals_local_login_unique": { + "name": "staff_principals_local_login_unique", + "columns": [ + { + "expression": "local_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"staff_principals\".\"local_login\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "staff_principals_active_idx": { + "name": "staff_principals_active_idx", + "columns": [ + { + "expression": "disabled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_role_assignments": { + "name": "staff_role_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_key": { + "name": "role_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_by_staff_id": { + "name": "assigned_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "staff_role_assignments_staff_role_idx": { + "name": "staff_role_assignments_staff_role_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "staff_role_assignments_active_idx": { + "name": "staff_role_assignments_active_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "staff_role_assignments_staff_id_staff_principals_id_fk": { + "name": "staff_role_assignments_staff_id_staff_principals_id_fk", + "tableFrom": "staff_role_assignments", + "tableTo": "staff_principals", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "staff_role_assignments_assigned_by_staff_id_staff_principals_id_fk": { + "name": "staff_role_assignments_assigned_by_staff_id_staff_principals_id_fk", + "tableFrom": "staff_role_assignments", + "tableTo": "staff_principals", + "columnsFrom": [ + "assigned_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_sessions": { + "name": "staff_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "auth_strength": { + "name": "auth_strength", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "csrf_token_hash": { + "name": "csrf_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "staff_sessions_active_idx": { + "name": "staff_sessions_active_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "staff_sessions_staff_id_staff_principals_id_fk": { + "name": "staff_sessions_staff_id_staff_principals_id_fk", + "tableFrom": "staff_sessions", + "tableTo": "staff_principals", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step_up_grants": { + "name": "step_up_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "digest_key_id": { + "name": "digest_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "step_up_actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_audience": { + "name": "action_audience", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_methods": { + "name": "auth_methods", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "step_up_grants_token_digest_unique": { + "name": "step_up_grants_token_digest_unique", + "columns": [ + { + "expression": "token_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "step_up_grants_actor_session_expiry_idx": { + "name": "step_up_grants_actor_session_expiry_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_cases": { + "name": "support_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_snapshot": { + "name": "account_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "category": { + "name": "category", + "type": "support_case_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "support_case_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "support_case_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "support_case_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_staff_id": { + "name": "assigned_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assigned_staff_snapshot": { + "name": "assigned_staff_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_cases_account_updated_idx": { + "name": "support_cases_account_updated_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_cases_queue_idx": { + "name": "support_cases_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_cases_account_id_accounts_id_fk": { + "name": "support_cases_account_id_accounts_id_fk", + "tableFrom": "support_cases", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_cases_assigned_staff_id_staff_principals_id_fk": { + "name": "support_cases_assigned_staff_id_staff_principals_id_fk", + "tableFrom": "support_cases", + "tableTo": "staff_principals", + "columnsFrom": [ + "assigned_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_diagnostic_grants": { + "name": "support_diagnostic_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_version": { + "name": "scope_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_ciphertext": { + "name": "snapshot_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_key_id": { + "name": "snapshot_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_encryption_version": { + "name": "snapshot_encryption_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_diagnostic_grants_case_idx": { + "name": "support_diagnostic_grants_case_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_diagnostic_grants_account_idx": { + "name": "support_diagnostic_grants_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_diagnostic_grants_case_id_support_cases_id_fk": { + "name": "support_diagnostic_grants_case_id_support_cases_id_fk", + "tableFrom": "support_diagnostic_grants", + "tableTo": "support_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "support_diagnostic_grants_account_id_accounts_id_fk": { + "name": "support_diagnostic_grants_account_id_accounts_id_fk", + "tableFrom": "support_diagnostic_grants", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "support_diagnostic_grants_scope_check": { + "name": "support_diagnostic_grants_scope_check", + "value": "length(\"support_diagnostic_grants\".\"scope_version\") > 0" + }, + "support_diagnostic_grants_encryption_version_check": { + "name": "support_diagnostic_grants_encryption_version_check", + "value": "\"support_diagnostic_grants\".\"snapshot_encryption_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.support_messages": { + "name": "support_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_type": { + "name": "author_type", + "type": "support_message_author_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "author_ref": { + "name": "author_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_snapshot": { + "name": "author_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "support_message_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'customer'" + }, + "body_ciphertext": { + "name": "body_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_key_id": { + "name": "body_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_encryption_version": { + "name": "body_encryption_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body_format": { + "name": "body_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'plain'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_messages_case_author_idempotency_unique": { + "name": "support_messages_case_author_idempotency_unique", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_messages_case_created_idx": { + "name": "support_messages_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_messages_case_id_support_cases_id_fk": { + "name": "support_messages_case_id_support_cases_id_fk", + "tableFrom": "support_messages", + "tableTo": "support_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "support_messages_encryption_version_check": { + "name": "support_messages_encryption_version_check", + "value": "\"support_messages\".\"body_encryption_version\" > 0" + }, + "support_messages_key_id_check": { + "name": "support_messages_key_id_check", + "value": "length(\"support_messages\".\"body_key_id\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.sync_bootstrap_objects": { + "name": "sync_bootstrap_objects", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_document_sequence": { + "name": "latest_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checkpoint_document_sequence": { + "name": "checkpoint_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "checkpoint_key_version": { + "name": "checkpoint_key_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext": { + "name": "checkpoint_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext_hash": { + "name": "checkpoint_ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialized_revision": { + "name": "materialized_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sync_bootstrap_objects_workspace_idx": { + "name": "sync_bootstrap_objects_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_bootstrap_objects_session_id_sync_bootstrap_sessions_id_fk": { + "name": "sync_bootstrap_objects_session_id_sync_bootstrap_sessions_id_fk", + "tableFrom": "sync_bootstrap_objects", + "tableTo": "sync_bootstrap_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_bootstrap_objects_workspace_id_workspaces_id_fk": { + "name": "sync_bootstrap_objects_workspace_id_workspaces_id_fk", + "tableFrom": "sync_bootstrap_objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_bootstrap_objects_session_id_object_id_pk": { + "name": "sync_bootstrap_objects_session_id_object_id_pk", + "columns": [ + "session_id", + "object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_bootstrap_sessions": { + "name": "sync_bootstrap_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_sequence": { + "name": "snapshot_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_bootstrap_sessions_workspace_idx": { + "name": "sync_bootstrap_sessions_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_bootstrap_sessions_workspace_id_workspaces_id_fk": { + "name": "sync_bootstrap_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "sync_bootstrap_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_checkpoints": { + "name": "sync_checkpoints", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "covers_document_sequence": { + "name": "covers_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_sequence": { + "name": "event_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "materialized_revision": { + "name": "materialized_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_checkpoints_document_idx": { + "name": "sync_checkpoints_document_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_checkpoints_workspace_id_workspaces_id_fk": { + "name": "sync_checkpoints_workspace_id_workspaces_id_fk", + "tableFrom": "sync_checkpoints", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_checkpoints_source_device_id_devices_id_fk": { + "name": "sync_checkpoints_source_device_id_devices_id_fk", + "tableFrom": "sync_checkpoints", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_checkpoints_workspace_id_checkpoint_id_pk": { + "name": "sync_checkpoints_workspace_id_checkpoint_id_pk", + "columns": [ + "workspace_id", + "checkpoint_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_commands": { + "name": "sync_commands", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_epoch": { + "name": "sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_commands_created_idx": { + "name": "sync_commands_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_commands_workspace_id_workspaces_id_fk": { + "name": "sync_commands_workspace_id_workspaces_id_fk", + "tableFrom": "sync_commands", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_commands_source_device_id_devices_id_fk": { + "name": "sync_commands_source_device_id_devices_id_fk", + "tableFrom": "sync_commands", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_commands_workspace_id_command_id_pk": { + "name": "sync_commands_workspace_id_command_id_pk", + "columns": [ + "workspace_id", + "command_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conflict_id": { + "name": "conflict_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "conflict_type": { + "name": "conflict_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unresolved'" + }, + "expected_revision": { + "name": "expected_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "expected_document_sequence": { + "name": "expected_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_sequence": { + "name": "created_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resolved_sequence": { + "name": "resolved_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "resolved_by_device_id": { + "name": "resolved_by_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_conflicts_status_idx": { + "name": "sync_conflicts_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_conflicts_object_idx": { + "name": "sync_conflicts_object_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_workspace_id_workspaces_id_fk": { + "name": "sync_conflicts_workspace_id_workspaces_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_conflicts_resolved_by_device_id_devices_id_fk": { + "name": "sync_conflicts_resolved_by_device_id_devices_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "devices", + "columnsFrom": [ + "resolved_by_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_workspace_id_conflict_id_pk": { + "name": "sync_conflicts_workspace_id_conflict_id_pk", + "columns": [ + "workspace_id", + "conflict_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_device_cursors": { + "name": "sync_device_cursors", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "acknowledged_sequence": { + "name": "acknowledged_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_device_cursors_updated_idx": { + "name": "sync_device_cursors_updated_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_device_cursors_workspace_id_workspaces_id_fk": { + "name": "sync_device_cursors_workspace_id_workspaces_id_fk", + "tableFrom": "sync_device_cursors", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_device_cursors_device_id_devices_id_fk": { + "name": "sync_device_cursors_device_id_devices_id_fk", + "tableFrom": "sync_device_cursors", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_device_cursors_workspace_id_device_id_pk": { + "name": "sync_device_cursors_workspace_id_device_id_pk", + "columns": [ + "workspace_id", + "device_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_documents": { + "name": "sync_documents", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "latest_document_sequence": { + "name": "latest_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "checkpoint_document_sequence": { + "name": "checkpoint_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "checkpoint_key_version": { + "name": "checkpoint_key_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext": { + "name": "checkpoint_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext_hash": { + "name": "checkpoint_ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialized_revision": { + "name": "materialized_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_documents_object_unique": { + "name": "sync_documents_object_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_documents_workspace_id_workspaces_id_fk": { + "name": "sync_documents_workspace_id_workspaces_id_fk", + "tableFrom": "sync_documents", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_documents_workspace_id_document_id_pk": { + "name": "sync_documents_workspace_id_document_id_pk", + "columns": [ + "workspace_id", + "document_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_events": { + "name": "sync_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_sequence": { + "name": "document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_events_workspace_sequence_unique": { + "name": "sync_events_workspace_sequence_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_events_workspace_event_unique": { + "name": "sync_events_workspace_event_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_events_document_idx": { + "name": "sync_events_document_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_events_created_idx": { + "name": "sync_events_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_events_workspace_id_workspaces_id_fk": { + "name": "sync_events_workspace_id_workspaces_id_fk", + "tableFrom": "sync_events", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_events_source_device_id_devices_id_fk": { + "name": "sync_events_source_device_id_devices_id_fk", + "tableFrom": "sync_events", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_resource_bindings": { + "name": "sync_resource_bindings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_object_id": { + "name": "owner_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_revision": { + "name": "owner_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resource_object_id": { + "name": "resource_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_revision": { + "name": "resource_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_resource_bindings_resource_idx": { + "name": "sync_resource_bindings_resource_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_resource_bindings_owner_idx": { + "name": "sync_resource_bindings_owner_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_resource_bindings_workspace_id_workspaces_id_fk": { + "name": "sync_resource_bindings_workspace_id_workspaces_id_fk", + "tableFrom": "sync_resource_bindings", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_resource_bindings_owner_version_fk": { + "name": "sync_resource_bindings_owner_version_fk", + "tableFrom": "sync_resource_bindings", + "tableTo": "object_versions", + "columnsFrom": [ + "workspace_id", + "owner_object_id", + "owner_revision" + ], + "columnsTo": [ + "workspace_id", + "object_id", + "revision" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_resource_bindings_resource_version_fk": { + "name": "sync_resource_bindings_resource_version_fk", + "tableFrom": "sync_resource_bindings", + "tableTo": "object_versions", + "columnsFrom": [ + "workspace_id", + "resource_object_id", + "resource_revision" + ], + "columnsTo": [ + "workspace_id", + "object_id", + "revision" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_resource_bindings_workspace_id_owner_object_id_owner_revision_resource_object_id_pk": { + "name": "sync_resource_bindings_workspace_id_owner_object_id_owner_revision_resource_object_id_pk", + "columns": [ + "workspace_id", + "owner_object_id", + "owner_revision", + "resource_object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_updates": { + "name": "sync_updates", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_sequence": { + "name": "document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "update_id": { + "name": "update_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_sequence": { + "name": "event_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_updates_id_unique": { + "name": "sync_updates_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_updates_event_idx": { + "name": "sync_updates_event_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_updates_workspace_id_workspaces_id_fk": { + "name": "sync_updates_workspace_id_workspaces_id_fk", + "tableFrom": "sync_updates", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_updates_source_device_id_devices_id_fk": { + "name": "sync_updates_source_device_id_devices_id_fk", + "tableFrom": "sync_updates", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_updates_workspace_id_document_id_document_sequence_pk": { + "name": "sync_updates_workspace_id_document_id_document_sequence_pk", + "columns": [ + "workspace_id", + "document_id", + "document_sequence" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_events": { + "name": "usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delta": { + "name": "delta", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resulting_value": { + "name": "resulting_value", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_period": { + "name": "billing_period", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "usage_events_workspace_idempotency_unique": { + "name": "usage_events_workspace_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_events\".\"workspace_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_events_account_idempotency_unique": { + "name": "usage_events_account_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_events\".\"workspace_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_events_account_occurred_idx": { + "name": "usage_events_account_occurred_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_events_account_id_accounts_id_fk": { + "name": "usage_events_account_id_accounts_id_fk", + "tableFrom": "usage_events", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_reservations": { + "name": "usage_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_upload_ref": { + "name": "provider_upload_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "usage_reservation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "usage_reservations_workspace_source_unique": { + "name": "usage_reservations_workspace_source_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_reservations\".\"workspace_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_reservations_account_source_unique": { + "name": "usage_reservations_account_source_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_reservations\".\"workspace_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_reservations_expiry_idx": { + "name": "usage_reservations_expiry_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_reservations_account_id_accounts_id_fk": { + "name": "usage_reservations_account_id_accounts_id_fk", + "tableFrom": "usage_reservations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.web_sessions": { + "name": "web_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "csrf_token_hash": { + "name": "csrf_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "issued_instance_auth_epoch": { + "name": "issued_instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_ip": { + "name": "last_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "web_sessions_token_hash_unique": { + "name": "web_sessions_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "web_sessions_account_idx": { + "name": "web_sessions_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "web_sessions_expiry_idx": { + "name": "web_sessions_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "web_sessions_account_id_accounts_id_fk": { + "name": "web_sessions_account_id_accounts_id_fk", + "tableFrom": "web_sessions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_key_envelopes": { + "name": "workspace_key_envelopes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "envelope_type": { + "name": "envelope_type", + "type": "key_envelope_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wrapped_key": { + "name": "wrapped_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kdf_salt": { + "name": "kdf_salt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kdf_params": { + "name": "kdf_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "key_envelope_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "replacement_idempotency_key": { + "name": "replacement_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replacement_request_hash": { + "name": "replacement_request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_key_envelopes_key_idx": { + "name": "workspace_key_envelopes_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_key_envelopes_active_recipient_unique": { + "name": "workspace_key_envelopes_active_recipient_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "envelope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"recipient_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_key_envelopes\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_key_envelopes_replacement_idempotency_unique": { + "name": "workspace_key_envelopes_replacement_idempotency_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "replacement_idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_key_envelopes\".\"replacement_idempotency_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_key_envelopes_key_fk": { + "name": "workspace_key_envelopes_key_fk", + "tableFrom": "workspace_key_envelopes", + "tableTo": "workspace_keys", + "columnsFrom": [ + "workspace_id", + "key_version" + ], + "columnsTo": [ + "workspace_id", + "key_version" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_keys": { + "name": "workspace_keys", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_keys_workspace_id_workspaces_id_fk": { + "name": "workspace_keys_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_keys", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_keys_workspace_id_key_version_pk": { + "name": "workspace_keys_workspace_id_key_version_pk", + "columns": [ + "workspace_id", + "key_version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name_ciphertext": { + "name": "name_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_idempotency_key": { + "name": "creation_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creation_request_hash": { + "name": "creation_request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latest_sequence": { + "name": "latest_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspaces_account_idx": { + "name": "workspaces_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_active_default_unique": { + "name": "workspaces_active_default_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspaces\".\"is_default\" = true and \"workspaces\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_account_creation_idempotency_unique": { + "name": "workspaces_account_creation_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "creation_idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspaces\".\"creation_idempotency_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_account_id_accounts_id_fk": { + "name": "workspaces_account_id_accounts_id_fk", + "tableFrom": "workspaces", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_action_token_purpose": { + "name": "account_action_token_purpose", + "schema": "public", + "values": [ + "verify_email", + "reset_password", + "change_email" + ] + }, + "public.account_identity_kind": { + "name": "account_identity_kind", + "schema": "public", + "values": [ + "username", + "email" + ] + }, + "public.account_identity_state": { + "name": "account_identity_state", + "schema": "public", + "values": [ + "pending_verification", + "active", + "legacy_migration" + ] + }, + "public.account_login_claim_kind": { + "name": "account_login_claim_kind", + "schema": "public", + "values": [ + "legacy_username", + "username", + "email" + ] + }, + "public.account_subscription_status": { + "name": "account_subscription_status", + "schema": "public", + "values": [ + "incomplete", + "trialing", + "active", + "past_due", + "grace", + "paused", + "ended", + "review" + ] + }, + "public.background_job_status": { + "name": "background_job_status", + "schema": "public", + "values": [ + "pending", + "running", + "succeeded", + "dead_letter", + "cancelled" + ] + }, + "public.backup_run_status": { + "name": "backup_run_status", + "schema": "public", + "values": [ + "queued", + "preparing", + "draining", + "dumping", + "copying", + "verifying", + "ready", + "failed", + "deleting" + ] + }, + "public.billing_checkout_status": { + "name": "billing_checkout_status", + "schema": "public", + "values": [ + "pending_provider", + "open", + "completed", + "linked", + "expired", + "failed", + "reconciling" + ] + }, + "public.billing_plan_interval": { + "name": "billing_plan_interval", + "schema": "public", + "values": [ + "month", + "year" + ] + }, + "public.billing_provider_environment": { + "name": "billing_provider_environment", + "schema": "public", + "values": [ + "test", + "live" + ] + }, + "public.billing_webhook_event_status": { + "name": "billing_webhook_event_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed", + "ignored", + "failed", + "dead_letter" + ] + }, + "public.blob_state": { + "name": "blob_state", + "schema": "public", + "values": [ + "uploading", + "ready", + "deleting" + ] + }, + "public.bootstrap_credential_source": { + "name": "bootstrap_credential_source", + "schema": "public", + "values": [ + "cli", + "legacy_environment" + ] + }, + "public.change_type": { + "name": "change_type", + "schema": "public", + "values": [ + "upsert", + "delete" + ] + }, + "public.data_request_status": { + "name": "data_request_status", + "schema": "public", + "values": [ + "submitted", + "identity_check", + "queued", + "processing", + "awaiting_user", + "completed", + "rejected", + "canceled", + "held", + "failed" + ] + }, + "public.data_request_type": { + "name": "data_request_type", + "schema": "public", + "values": [ + "access", + "export", + "correct", + "delete", + "restrict", + "object" + ] + }, + "public.deletion_case_status": { + "name": "deletion_case_status", + "schema": "public", + "values": [ + "requested", + "cooling_off", + "scheduled", + "held", + "purging", + "completed", + "canceled", + "failed" + ] + }, + "public.deletion_fence_state": { + "name": "deletion_fence_state", + "schema": "public", + "values": [ + "cooling_off", + "scheduled", + "purging", + "completed", + "canceled" + ] + }, + "public.deletion_step_state": { + "name": "deletion_step_state", + "schema": "public", + "values": [ + "pending", + "running", + "completed", + "failed", + "skipped" + ] + }, + "public.deployment_mode": { + "name": "deployment_mode", + "schema": "public", + "values": [ + "hosted", + "self-hosted" + ] + }, + "public.device_authorization_status": { + "name": "device_authorization_status", + "schema": "public", + "values": [ + "pending", + "approved", + "denied" + ] + }, + "public.entitlement_grant_source": { + "name": "entitlement_grant_source", + "schema": "public", + "values": [ + "promotion", + "support", + "migration", + "staff" + ] + }, + "public.invitation_actor_type": { + "name": "invitation_actor_type", + "schema": "public", + "values": [ + "account", + "staff", + "system" + ] + }, + "public.key_envelope_status": { + "name": "key_envelope_status", + "schema": "public", + "values": [ + "active", + "revoked" + ] + }, + "public.key_envelope_type": { + "name": "key_envelope_type", + "schema": "public", + "values": [ + "passphrase", + "recovery", + "device", + "managed" + ] + }, + "public.maintenance_mode": { + "name": "maintenance_mode", + "schema": "public", + "values": [ + "normal", + "read_only", + "write_drain", + "offline" + ] + }, + "public.object_kind": { + "name": "object_kind", + "schema": "public", + "values": [ + "note", + "folder", + "asset", + "canvas", + "record", + "tag", + "mark", + "conversation", + "memory", + "setting", + "yjs-checkpoint", + "yjs-update" + ] + }, + "public.outbox_message_status": { + "name": "outbox_message_status", + "schema": "public", + "values": [ + "pending", + "sending", + "sent", + "dead_letter", + "delivery_unknown" + ] + }, + "public.registration_policy": { + "name": "registration_policy", + "schema": "public", + "values": [ + "bootstrap", + "disabled", + "invitation", + "public" + ] + }, + "public.restore_drill_mode": { + "name": "restore_drill_mode", + "schema": "public", + "values": [ + "verify-only", + "isolated-restore", + "full-drill" + ] + }, + "public.restore_marker_mode": { + "name": "restore_marker_mode", + "schema": "public", + "values": [ + "preserve", + "clone" + ] + }, + "public.restore_sanitation_status": { + "name": "restore_sanitation_status", + "schema": "public", + "values": [ + "pending", + "running", + "complete", + "failed" + ] + }, + "public.risk_provider_event_status": { + "name": "risk_provider_event_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed", + "ignored", + "failed", + "dead_letter" + ] + }, + "public.risk_restriction_action": { + "name": "risk_restriction_action", + "schema": "public", + "values": [ + "challenge", + "lock", + "read_only", + "deny", + "review" + ] + }, + "public.risk_restriction_scope": { + "name": "risk_restriction_scope", + "schema": "public", + "values": [ + "registration", + "authentication", + "recovery", + "device", + "sync_write", + "blob", + "billing", + "all" + ] + }, + "public.risk_restriction_source": { + "name": "risk_restriction_source", + "schema": "public", + "values": [ + "automatic", + "staff", + "provider" + ] + }, + "public.risk_restriction_subject_type": { + "name": "risk_restriction_subject_type", + "schema": "public", + "values": [ + "account", + "identity", + "device", + "ip_prefix" + ] + }, + "public.self_hosted_lifecycle": { + "name": "self_hosted_lifecycle", + "schema": "public", + "values": [ + "uninitialized", + "ready" + ] + }, + "public.step_up_actor_type": { + "name": "step_up_actor_type", + "schema": "public", + "values": [ + "account", + "staff" + ] + }, + "public.support_case_category": { + "name": "support_case_category", + "schema": "public", + "values": [ + "account", + "sync", + "device", + "encryption", + "billing", + "privacy", + "abuse", + "other" + ] + }, + "public.support_case_severity": { + "name": "support_case_severity", + "schema": "public", + "values": [ + "normal", + "high", + "urgent" + ] + }, + "public.support_case_source": { + "name": "support_case_source", + "schema": "public", + "values": [ + "web", + "client", + "email", + "staff", + "external" + ] + }, + "public.support_case_status": { + "name": "support_case_status", + "schema": "public", + "values": [ + "open", + "waiting_for_support", + "waiting_for_user", + "resolved", + "closed", + "spam" + ] + }, + "public.support_message_author_type": { + "name": "support_message_author_type", + "schema": "public", + "values": [ + "account", + "staff", + "system" + ] + }, + "public.support_message_visibility": { + "name": "support_message_visibility", + "schema": "public", + "values": [ + "customer", + "internal" + ] + }, + "public.usage_reservation_status": { + "name": "usage_reservation_status", + "schema": "public", + "values": [ + "reserved", + "external_started", + "committed", + "released", + "expired", + "reconciling" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/drizzle/meta/0003_snapshot.json b/apps/server/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..2bd2e3f --- /dev/null +++ b/apps/server/drizzle/meta/0003_snapshot.json @@ -0,0 +1,11522 @@ +{ + "id": "7fc8704e-8d84-4c58-af1d-810964b970b4", + "prevId": "a76dbd30-2474-48bd-adb7-f17389833a92", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account_action_tokens": { + "name": "account_action_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "identity_id": { + "name": "identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purpose": { + "name": "purpose", + "type": "account_action_token_purpose", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token_key_id": { + "name": "token_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_normalized": { + "name": "target_normalized", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "requested_ip_hash": { + "name": "requested_ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_action_tokens_hash_unique": { + "name": "account_action_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_action_tokens_account_idx": { + "name": "account_action_tokens_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_action_tokens_account_id_accounts_id_fk": { + "name": "account_action_tokens_account_id_accounts_id_fk", + "tableFrom": "account_action_tokens", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_action_tokens_identity_id_account_identities_id_fk": { + "name": "account_action_tokens_identity_id_account_identities_id_fk", + "tableFrom": "account_action_tokens", + "tableTo": "account_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_deletion_cases": { + "name": "account_deletion_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "deletion_case_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'requested'" + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "cancel_until": { + "name": "cancel_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "purge_after": { + "name": "purge_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_credential_hash": { + "name": "cancel_credential_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purge_manifest_ref": { + "name": "purge_manifest_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purge_manifest_hash": { + "name": "purge_manifest_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "purge_manifest": { + "name": "purge_manifest", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_deletion_cases_active_account_unique": { + "name": "account_deletion_cases_active_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_deletion_cases\".\"account_id\" is not null and \"account_deletion_cases\".\"status\" in ('requested', 'cooling_off', 'scheduled', 'held', 'purging')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_deletion_cases_active_subject_unique": { + "name": "account_deletion_cases_active_subject_unique", + "columns": [ + { + "expression": "subject_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_deletion_cases\".\"status\" in ('requested', 'cooling_off', 'scheduled', 'held', 'purging')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_deletion_fences": { + "name": "account_deletion_fences", + "schema": "", + "columns": { + "account_uuid": { + "name": "account_uuid", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "generation": { + "name": "generation", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "state": { + "name": "state", + "type": "deletion_fence_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "hold_revision": { + "name": "hold_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "blocks_domain_writes": { + "name": "blocks_domain_writes", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_identities": { + "name": "account_identities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "account_identity_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_identifier": { + "name": "normalized_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_identities_active_unique": { + "name": "account_identities_active_unique", + "columns": [ + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_identities\".\"disabled_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_identities_account_idx": { + "name": "account_identities_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_identities_account_id_accounts_id_fk": { + "name": "account_identities_account_id_accounts_id_fk", + "tableFrom": "account_identities", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_login_claim_conflicts": { + "name": "account_login_claim_conflicts", + "schema": "", + "columns": { + "normalized_login_key": { + "name": "normalized_login_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "candidate_account_id": { + "name": "candidate_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "candidate_identity_id": { + "name": "candidate_identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "candidate_kind": { + "name": "candidate_kind", + "type": "account_login_claim_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'quarantined'" + }, + "resolution_ref": { + "name": "resolution_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "account_login_claim_conflicts_status_idx": { + "name": "account_login_claim_conflicts_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_login_claim_conflicts_candidate_account_id_accounts_id_fk": { + "name": "account_login_claim_conflicts_candidate_account_id_accounts_id_fk", + "tableFrom": "account_login_claim_conflicts", + "tableTo": "accounts", + "columnsFrom": [ + "candidate_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "account_login_claim_conflicts_candidate_identity_id_account_identities_id_fk": { + "name": "account_login_claim_conflicts_candidate_identity_id_account_identities_id_fk", + "tableFrom": "account_login_claim_conflicts", + "tableTo": "account_identities", + "columnsFrom": [ + "candidate_identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_login_claim_conflicts_normalized_login_key_candidate_account_id_candidate_kind_pk": { + "name": "account_login_claim_conflicts_normalized_login_key_candidate_account_id_candidate_kind_pk", + "columns": [ + "normalized_login_key", + "candidate_account_id", + "candidate_kind" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_login_claims": { + "name": "account_login_claims", + "schema": "", + "columns": { + "normalized_login_key": { + "name": "normalized_login_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "identity_id": { + "name": "identity_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "account_login_claim_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reusable_after": { + "name": "reusable_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_login_claims_account_id_accounts_id_fk": { + "name": "account_login_claims_account_id_accounts_id_fk", + "tableFrom": "account_login_claims", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "account_login_claims_identity_id_account_identities_id_fk": { + "name": "account_login_claims_identity_id_account_identities_id_fk", + "tableFrom": "account_login_claims", + "tableTo": "account_identities", + "columnsFrom": [ + "identity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_service_audit_events": { + "name": "account_service_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_service_audit_events_actor_idx": { + "name": "account_service_audit_events_actor_idx", + "columns": [ + { + "expression": "actor_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_service_audit_events_target_idx": { + "name": "account_service_audit_events_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "account_service_audit_events_actor_type_check": { + "name": "account_service_audit_events_actor_type_check", + "value": "\"account_service_audit_events\".\"actor_type\" in ('account', 'staff', 'system', 'webhook')" + }, + "account_service_audit_events_action_check": { + "name": "account_service_audit_events_action_check", + "value": "length(\"account_service_audit_events\".\"action\") > 0" + }, + "account_service_audit_events_target_type_check": { + "name": "account_service_audit_events_target_type_check", + "value": "length(\"account_service_audit_events\".\"target_type\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.account_subscriptions": { + "name": "account_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_version_id": { + "name": "plan_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "account_subscription_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "is_current": { + "name": "is_current", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_revision": { + "name": "provider_revision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_period_start": { + "name": "current_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_end": { + "name": "current_period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "grace_ends_at": { + "name": "grace_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_subscriptions_provider_subscription_unique": { + "name": "account_subscriptions_provider_subscription_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_subscriptions_current_account_unique": { + "name": "account_subscriptions_current_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"account_subscriptions\".\"account_id\" is not null and \"account_subscriptions\".\"is_current\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_subscriptions_account_status_idx": { + "name": "account_subscriptions_account_status_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_subscriptions_account_id_accounts_id_fk": { + "name": "account_subscriptions_account_id_accounts_id_fk", + "tableFrom": "account_subscriptions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "account_subscriptions_plan_version_id_billing_plan_versions_id_fk": { + "name": "account_subscriptions_plan_version_id_billing_plan_versions_id_fk", + "tableFrom": "account_subscriptions", + "tableTo": "billing_plan_versions", + "columnsFrom": [ + "plan_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account_usage": { + "name": "account_usage", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "active_object_bytes": { + "name": "active_object_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_crdt_bytes": { + "name": "active_crdt_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_blob_bytes": { + "name": "active_blob_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "reserved_blob_bytes": { + "name": "reserved_blob_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "retained_bytes": { + "name": "retained_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_objects": { + "name": "active_objects", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_devices": { + "name": "active_devices", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "active_workspaces": { + "name": "active_workspaces", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "reconciled_at": { + "name": "reconciled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_usage_account_id_accounts_id_fk": { + "name": "account_usage_account_id_accounts_id_fk", + "tableFrom": "account_usage", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "totp_secret": { + "name": "totp_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "totp_enabled_at": { + "name": "totp_enabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "identity_state": { + "name": "identity_state", + "type": "account_identity_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'legacy_migration'" + }, + "credential_epoch": { + "name": "credential_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_login_unique": { + "name": "accounts_login_unique", + "columns": [ + { + "expression": "lower(\"login\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_audit_logs": { + "name": "admin_audit_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_audit_logs_actor_idx": { + "name": "admin_audit_logs_actor_idx", + "columns": [ + { + "expression": "actor_account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_audit_logs_created_idx": { + "name": "admin_audit_logs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_audit_logs_actor_account_id_accounts_id_fk": { + "name": "admin_audit_logs_actor_account_id_accounts_id_fk", + "tableFrom": "admin_audit_logs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_backups": { + "name": "admin_backups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "admin_backups_filename_unique": { + "name": "admin_backups_filename_unique", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_backups_job_id_admin_jobs_id_fk": { + "name": "admin_backups_job_id_admin_jobs_id_fk", + "tableFrom": "admin_backups", + "tableTo": "admin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_jobs": { + "name": "admin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "progress": { + "name": "progress", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_jobs_created_idx": { + "name": "admin_jobs_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_jobs_status_idx": { + "name": "admin_jobs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_jobs_actor_account_id_accounts_id_fk": { + "name": "admin_jobs_actor_account_id_accounts_id_fk", + "tableFrom": "admin_jobs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_test_objects": { + "name": "admin_test_objects", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_by_account_id": { + "name": "created_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_test_objects_created_idx": { + "name": "admin_test_objects_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "admin_test_objects_workspace_id_workspaces_id_fk": { + "name": "admin_test_objects_workspace_id_workspaces_id_fk", + "tableFrom": "admin_test_objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "admin_test_objects_created_by_account_id_accounts_id_fk": { + "name": "admin_test_objects_created_by_account_id_accounts_id_fk", + "tableFrom": "admin_test_objects", + "tableTo": "accounts", + "columnsFrom": [ + "created_by_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "admin_test_objects_workspace_id_object_id_pk": { + "name": "admin_test_objects_workspace_id_object_id_pk", + "columns": [ + "workspace_id", + "object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.background_jobs": { + "name": "background_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "background_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "queue_generation": { + "name": "queue_generation", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "min_handler_version": { + "name": "min_handler_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "actor_account_id": { + "name": "actor_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_account_id": { + "name": "target_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "background_jobs_idempotency_unique": { + "name": "background_jobs_idempotency_unique", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "background_jobs_claim_idx": { + "name": "background_jobs_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "background_jobs_actor_account_id_accounts_id_fk": { + "name": "background_jobs_actor_account_id_accounts_id_fk", + "tableFrom": "background_jobs", + "tableTo": "accounts", + "columnsFrom": [ + "actor_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "background_jobs_target_account_id_accounts_id_fk": { + "name": "background_jobs_target_account_id_accounts_id_fk", + "tableFrom": "background_jobs", + "tableTo": "accounts", + "columnsFrom": [ + "target_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "background_jobs_payload_version_check": { + "name": "background_jobs_payload_version_check", + "value": "\"background_jobs\".\"payload_version\" > 0" + }, + "background_jobs_queue_generation_check": { + "name": "background_jobs_queue_generation_check", + "value": "\"background_jobs\".\"queue_generation\" > 0" + }, + "background_jobs_min_handler_version_check": { + "name": "background_jobs_min_handler_version_check", + "value": "\"background_jobs\".\"min_handler_version\" > 0" + }, + "background_jobs_attempt_check": { + "name": "background_jobs_attempt_check", + "value": "\"background_jobs\".\"attempt\" >= 0" + }, + "background_jobs_max_attempts_check": { + "name": "background_jobs_max_attempts_check", + "value": "\"background_jobs\".\"max_attempts\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.backup_artifacts": { + "name": "backup_artifacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_run_id": { + "name": "backup_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "relative_path": { + "name": "relative_path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "backup_artifacts_run_path_unique": { + "name": "backup_artifacts_run_path_unique", + "columns": [ + { + "expression": "backup_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "relative_path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backup_artifacts_run_idx": { + "name": "backup_artifacts_run_idx", + "columns": [ + { + "expression": "backup_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "backup_artifacts_backup_run_id_backup_runs_id_fk": { + "name": "backup_artifacts_backup_run_id_backup_runs_id_fk", + "tableFrom": "backup_artifacts", + "tableTo": "backup_runs", + "columnsFrom": [ + "backup_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "backup_artifacts_size_check": { + "name": "backup_artifacts_size_check", + "value": "\"backup_artifacts\".\"size\" >= 0" + }, + "backup_artifacts_path_check": { + "name": "backup_artifacts_path_check", + "value": "length(\"backup_artifacts\".\"relative_path\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.backup_policies": { + "name": "backup_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_ref": { + "name": "target_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retention": { + "name": "retention", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "encryption_key_id": { + "name": "encryption_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.backup_runs": { + "name": "backup_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "generation": { + "name": "generation", + "type": "bigserial", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "backup_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "snapshot_at": { + "name": "snapshot_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "manifest_ref": { + "name": "manifest_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "database_bytes": { + "name": "database_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "blob_count": { + "name": "blob_count", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "blob_bytes": { + "name": "blob_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint": { + "name": "checkpoint", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "backup_runs_generation_unique": { + "name": "backup_runs_generation_unique", + "columns": [ + { + "expression": "generation", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "backup_runs_status_idx": { + "name": "backup_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "backup_runs_policy_id_backup_policies_id_fk": { + "name": "backup_runs_policy_id_backup_policies_id_fk", + "tableFrom": "backup_runs", + "tableTo": "backup_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_account_states": { + "name": "billing_account_states", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "current_subscription_id": { + "name": "current_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "purchase_intent_id": { + "name": "purchase_intent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_account_states_account_id_accounts_id_fk": { + "name": "billing_account_states_account_id_accounts_id_fk", + "tableFrom": "billing_account_states", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_account_states_current_subscription_id_account_subscriptions_id_fk": { + "name": "billing_account_states_current_subscription_id_account_subscriptions_id_fk", + "tableFrom": "billing_account_states", + "tableTo": "account_subscriptions", + "columnsFrom": [ + "current_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "billing_account_states_purchase_intent_id_billing_checkout_sessions_id_fk": { + "name": "billing_account_states_purchase_intent_id_billing_checkout_sessions_id_fk", + "tableFrom": "billing_account_states", + "tableTo": "billing_checkout_sessions", + "columnsFrom": [ + "purchase_intent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_account_states_current_or_intent_check": { + "name": "billing_account_states_current_or_intent_check", + "value": "not (\"billing_account_states\".\"current_subscription_id\" is not null and \"billing_account_states\".\"purchase_intent_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.billing_checkout_sessions": { + "name": "billing_checkout_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_checkout_id": { + "name": "provider_checkout_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "price_mapping_id": { + "name": "price_mapping_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plan_version_id": { + "name": "plan_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_checkout_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending_provider'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_checkout_sessions_account_idempotency_unique": { + "name": "billing_checkout_sessions_account_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_sessions_provider_checkout_unique": { + "name": "billing_checkout_sessions_provider_checkout_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_checkout_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_sessions\".\"provider_checkout_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_sessions_active_account_unique": { + "name": "billing_checkout_sessions_active_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_sessions\".\"status\" in ('pending_provider','open','completed','reconciling')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_checkout_sessions_account_id_accounts_id_fk": { + "name": "billing_checkout_sessions_account_id_accounts_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_checkout_sessions_subscription_id_account_subscriptions_id_fk": { + "name": "billing_checkout_sessions_subscription_id_account_subscriptions_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "account_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "billing_checkout_sessions_price_mapping_id_billing_price_mappings_id_fk": { + "name": "billing_checkout_sessions_price_mapping_id_billing_price_mappings_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "billing_price_mappings", + "columnsFrom": [ + "price_mapping_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_sessions_plan_version_id_billing_plan_versions_id_fk": { + "name": "billing_checkout_sessions_plan_version_id_billing_plan_versions_id_fk", + "tableFrom": "billing_checkout_sessions", + "tableTo": "billing_plan_versions", + "columnsFrom": [ + "plan_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_customers": { + "name": "billing_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_customers_provider_customer_unique": { + "name": "billing_customers_provider_customer_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_customers_account_unique": { + "name": "billing_customers_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_customers\".\"account_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_customers_account_id_accounts_id_fk": { + "name": "billing_customers_account_id_accounts_id_fk", + "tableFrom": "billing_customers", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_plan_versions": { + "name": "billing_plan_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_key": { + "name": "plan_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "interval": { + "name": "interval", + "type": "billing_plan_interval", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "entitlement_schema_version": { + "name": "entitlement_schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entitlements": { + "name": "entitlements", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "active_from": { + "name": "active_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_plan_versions_key_version_unique": { + "name": "billing_plan_versions_key_version_unique", + "columns": [ + { + "expression": "plan_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_plan_versions_amount_check": { + "name": "billing_plan_versions_amount_check", + "value": "\"billing_plan_versions\".\"amount_minor\" >= 0" + }, + "billing_plan_versions_version_check": { + "name": "billing_plan_versions_version_check", + "value": "\"billing_plan_versions\".\"version\" > 0" + }, + "billing_plan_versions_schema_check": { + "name": "billing_plan_versions_schema_check", + "value": "\"billing_plan_versions\".\"entitlement_schema_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_price_mappings": { + "name": "billing_price_mappings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_version_id": { + "name": "plan_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_environment": { + "name": "provider_environment", + "type": "billing_provider_environment", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_price_id": { + "name": "provider_price_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "interval": { + "name": "interval", + "type": "billing_plan_interval", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_price_mappings_provider_price_unique": { + "name": "billing_price_mappings_provider_price_unique", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_environment", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_price_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_price_mappings_plan_provider_unique": { + "name": "billing_price_mappings_plan_provider_unique", + "columns": [ + { + "expression": "plan_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_environment", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "currency", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "interval", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_price_mappings_plan_version_id_billing_plan_versions_id_fk": { + "name": "billing_price_mappings_plan_version_id_billing_plan_versions_id_fk", + "tableFrom": "billing_price_mappings", + "tableTo": "billing_plan_versions", + "columnsFrom": [ + "plan_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_webhook_events": { + "name": "billing_webhook_events", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature_verified_at": { + "name": "signature_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "payload_redacted": { + "name": "payload_redacted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_webhook_event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_webhook_events_claim_idx": { + "name": "billing_webhook_events_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "billing_webhook_events_provider_provider_event_id_pk": { + "name": "billing_webhook_events_provider_provider_event_id_pk", + "columns": [ + "provider", + "provider_event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob_upload_parts": { + "name": "blob_upload_parts", + "schema": "", + "columns": { + "upload_id": { + "name": "upload_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "part_number": { + "name": "part_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "etag": { + "name": "etag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "blob_upload_parts_upload_id_blob_uploads_id_fk": { + "name": "blob_upload_parts_upload_id_blob_uploads_id_fk", + "tableFrom": "blob_upload_parts", + "tableTo": "blob_uploads", + "columnsFrom": [ + "upload_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "blob_upload_parts_upload_id_part_number_pk": { + "name": "blob_upload_parts_upload_id_part_number_pk", + "columns": [ + "upload_id", + "part_number" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blob_uploads": { + "name": "blob_uploads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blob_id": { + "name": "blob_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_upload_id": { + "name": "provider_upload_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_reservation_id": { + "name": "usage_reservation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sync_epoch": { + "name": "sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expected_size": { + "name": "expected_size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "received_size": { + "name": "received_size", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "completing_at": { + "name": "completing_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blob_uploads_workspace_blob_unique": { + "name": "blob_uploads_workspace_blob_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blob_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blob_uploads_workspace_blob_idx": { + "name": "blob_uploads_workspace_blob_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "blob_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blob_uploads_expiry_idx": { + "name": "blob_uploads_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blob_uploads_usage_reservation_unique": { + "name": "blob_uploads_usage_reservation_unique", + "columns": [ + { + "expression": "usage_reservation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"blob_uploads\".\"usage_reservation_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blob_uploads_workspace_id_workspaces_id_fk": { + "name": "blob_uploads_workspace_id_workspaces_id_fk", + "tableFrom": "blob_uploads", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "blob_uploads_usage_reservation_id_usage_reservations_id_fk": { + "name": "blob_uploads_usage_reservation_id_usage_reservations_id_fk", + "tableFrom": "blob_uploads", + "tableTo": "usage_reservations", + "columnsFrom": [ + "usage_reservation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.blobs": { + "name": "blobs", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blob_id": { + "name": "blob_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "blob_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_referenced_at": { + "name": "last_referenced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "blobs_storage_key_unique": { + "name": "blobs_storage_key_unique", + "columns": [ + { + "expression": "storage_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "blobs_gc_idx": { + "name": "blobs_gc_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_referenced_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "blobs_workspace_id_workspaces_id_fk": { + "name": "blobs_workspace_id_workspaces_id_fk", + "tableFrom": "blobs", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "blobs_workspace_id_blob_id_pk": { + "name": "blobs_workspace_id_blob_id_pk", + "columns": [ + "workspace_id", + "blob_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bootstrap_credentials": { + "name": "bootstrap_credentials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source": { + "name": "source", + "type": "bootstrap_credential_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token_key_id": { + "name": "token_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hint": { + "name": "token_hint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bootstrap_credentials_token_hash_unique": { + "name": "bootstrap_credentials_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bootstrap_credentials_active_idx": { + "name": "bootstrap_credentials_active_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bootstrap_sessions": { + "name": "bootstrap_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_sequence": { + "name": "snapshot_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bootstrap_sessions_expiry_idx": { + "name": "bootstrap_sessions_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "bootstrap_sessions_device_idx": { + "name": "bootstrap_sessions_device_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bootstrap_sessions_workspace_id_workspaces_id_fk": { + "name": "bootstrap_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "bootstrap_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bootstrap_sessions_device_id_devices_id_fk": { + "name": "bootstrap_sessions_device_id_devices_id_fk", + "tableFrom": "bootstrap_sessions", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.challenge_consumptions": { + "name": "challenge_consumptions", + "schema": "", + "columns": { + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "digest_key_id": { + "name": "digest_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expected_hostname": { + "name": "expected_hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_claims_hash": { + "name": "verified_claims_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.changes": { + "name": "changes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "change_type": { + "name": "change_type", + "type": "change_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "changes_workspace_sequence_unique": { + "name": "changes_workspace_sequence_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changes_workspace_object_idx": { + "name": "changes_workspace_object_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "changes_created_idx": { + "name": "changes_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "changes_workspace_id_workspaces_id_fk": { + "name": "changes_workspace_id_workspaces_id_fk", + "tableFrom": "changes", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "changes_source_device_id_devices_id_fk": { + "name": "changes_source_device_id_devices_id_fk", + "tableFrom": "changes", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.data_requests": { + "name": "data_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_idempotency_key": { + "name": "client_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "data_request_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "data_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'submitted'" + }, + "request_channel": { + "name": "request_channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_ref": { + "name": "result_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "data_requests_subject_idempotency_unique": { + "name": "data_requests_subject_idempotency_unique", + "columns": [ + { + "expression": "subject_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "client_idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "data_requests_status_idx": { + "name": "data_requests_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "data_requests_account_id_accounts_id_fk": { + "name": "data_requests_account_id_accounts_id_fk", + "tableFrom": "data_requests", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deletion_case_steps": { + "name": "deletion_case_steps", + "schema": "", + "columns": { + "deletion_case_id": { + "name": "deletion_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "handler": { + "name": "handler", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "deletion_step_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "deletion_case_steps_deletion_case_id_account_deletion_cases_id_fk": { + "name": "deletion_case_steps_deletion_case_id_account_deletion_cases_id_fk", + "tableFrom": "deletion_case_steps", + "tableTo": "account_deletion_cases", + "columnsFrom": [ + "deletion_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "deletion_case_steps_deletion_case_id_handler_pk": { + "name": "deletion_case_steps_deletion_case_id_handler_pk", + "columns": [ + "deletion_case_id", + "handler" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deletion_ledger": { + "name": "deletion_ledger", + "schema": "", + "columns": { + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "hash_key_id": { + "name": "hash_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deletion_case_id": { + "name": "deletion_case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "minimum_backup_generation": { + "name": "minimum_backup_generation", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "minimum_database_lsn": { + "name": "minimum_database_lsn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "receipt_hash": { + "name": "receipt_hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deletion_ledger_outbox": { + "name": "deletion_ledger_outbox", + "schema": "", + "columns": { + "deletion_case_id": { + "name": "deletion_case_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "deletion_ledger_outbox_claim_idx": { + "name": "deletion_ledger_outbox_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "deletion_ledger_outbox_deletion_case_id_account_deletion_cases_id_fk": { + "name": "deletion_ledger_outbox_deletion_case_id_account_deletion_cases_id_fk", + "tableFrom": "deletion_ledger_outbox", + "tableTo": "account_deletion_cases", + "columnsFrom": [ + "deletion_case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "deletion_ledger_outbox_idempotency_key_unique": { + "name": "deletion_ledger_outbox_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.deployment_settings": { + "name": "deployment_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "deployment_mode": { + "name": "deployment_mode", + "type": "deployment_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "registration_policy": { + "name": "registration_policy", + "type": "registration_policy", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "self_hosted_lifecycle": { + "name": "self_hosted_lifecycle", + "type": "self_hosted_lifecycle", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "admin_repair_required": { + "name": "admin_repair_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "instance_auth_epoch": { + "name": "instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "token_not_before": { + "name": "token_not_before", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "auth_epoch_enforced": { + "name": "auth_epoch_enforced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "runtime_configuration": { + "name": "runtime_configuration", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "configuration_revision": { + "name": "configuration_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "1" + }, + "initialized_at": { + "name": "initialized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "initialized_by_account_id": { + "name": "initialized_by_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_authorizations": { + "name": "device_authorizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_code_hash": { + "name": "device_code_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_name": { + "name": "device_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encryption_public_key": { + "name": "encryption_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "device_authorization_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "approved_credential_epoch": { + "name": "approved_credential_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "approved_instance_auth_epoch": { + "name": "approved_instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "approved_issued_at": { + "name": "approved_issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_authorizations_device_code_unique": { + "name": "device_authorizations_device_code_unique", + "columns": [ + { + "expression": "device_code_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_authorizations_user_code_unique": { + "name": "device_authorizations_user_code_unique", + "columns": [ + { + "expression": "user_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_authorizations_expiry_idx": { + "name": "device_authorizations_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_authorizations_account_idx": { + "name": "device_authorizations_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_authorizations_account_id_accounts_id_fk": { + "name": "device_authorizations_account_id_accounts_id_fk", + "tableFrom": "device_authorizations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_cursors": { + "name": "device_cursors", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "acknowledged_sequence": { + "name": "acknowledged_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "device_cursors_workspace_id_workspaces_id_fk": { + "name": "device_cursors_workspace_id_workspaces_id_fk", + "tableFrom": "device_cursors", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "device_cursors_device_id_devices_id_fk": { + "name": "device_cursors_device_id_devices_id_fk", + "tableFrom": "device_cursors", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "device_cursors_workspace_id_device_id_pk": { + "name": "device_cursors_workspace_id_device_id_pk", + "columns": [ + "workspace_id", + "device_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_pairings": { + "name": "device_pairings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "credential_epoch": { + "name": "credential_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "instance_auth_epoch": { + "name": "instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_pairings_token_hash_unique": { + "name": "device_pairings_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_pairings_account_idx": { + "name": "device_pairings_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_pairings_expiry_idx": { + "name": "device_pairings_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_pairings_account_id_accounts_id_fk": { + "name": "device_pairings_account_id_accounts_id_fk", + "tableFrom": "device_pairings", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encryption_public_key": { + "name": "encryption_public_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "devices_account_idx": { + "name": "devices_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "devices_account_id_accounts_id_fk": { + "name": "devices_account_id_accounts_id_fk", + "tableFrom": "devices", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.entitlement_grants": { + "name": "entitlement_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "entitlement_grant_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "entitlements": { + "name": "entitlements", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "entitlement_grants_source_unique": { + "name": "entitlement_grants_source_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "entitlement_grants_active_idx": { + "name": "entitlement_grants_active_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "entitlement_grants_account_id_accounts_id_fk": { + "name": "entitlement_grants_account_id_accounts_id_fk", + "tableFrom": "entitlement_grants", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entitlement_grants_created_by_accounts_id_fk": { + "name": "entitlement_grants_created_by_accounts_id_fk", + "tableFrom": "entitlement_grants", + "tableTo": "accounts", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "entitlement_grants_schema_check": { + "name": "entitlement_grants_schema_check", + "value": "\"entitlement_grants\".\"schema_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.legal_holds": { + "name": "legal_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "authority": { + "name": "authority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'platform-admin'" + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_staff_id": { + "name": "approved_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "released_by": { + "name": "released_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_by_staff_id": { + "name": "released_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "release_reason_code": { + "name": "release_reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "legal_holds_active_account_unique": { + "name": "legal_holds_active_account_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"legal_holds\".\"released_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "legal_holds_account_idx": { + "name": "legal_holds_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "approved_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "legal_holds_account_id_accounts_id_fk": { + "name": "legal_holds_account_id_accounts_id_fk", + "tableFrom": "legal_holds", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_approved_by_accounts_id_fk": { + "name": "legal_holds_approved_by_accounts_id_fk", + "tableFrom": "legal_holds", + "tableTo": "accounts", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_approved_by_staff_id_staff_principals_id_fk": { + "name": "legal_holds_approved_by_staff_id_staff_principals_id_fk", + "tableFrom": "legal_holds", + "tableTo": "staff_principals", + "columnsFrom": [ + "approved_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_released_by_accounts_id_fk": { + "name": "legal_holds_released_by_accounts_id_fk", + "tableFrom": "legal_holds", + "tableTo": "accounts", + "columnsFrom": [ + "released_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "legal_holds_released_by_staff_id_staff_principals_id_fk": { + "name": "legal_holds_released_by_staff_id_staff_principals_id_fk", + "tableFrom": "legal_holds", + "tableTo": "staff_principals", + "columnsFrom": [ + "released_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail_secret_payloads": { + "name": "mail_secret_payloads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key_id": { + "name": "key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "erased_at": { + "name": "erased_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mail_secret_payloads_expiry_idx": { + "name": "mail_secret_payloads_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "mail_secret_payloads_version_check": { + "name": "mail_secret_payloads_version_check", + "value": "\"mail_secret_payloads\".\"payload_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.maintenance_state": { + "name": "maintenance_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "boolean", + "primaryKey": true, + "notNull": true, + "default": true + }, + "mode": { + "name": "mode", + "type": "maintenance_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "generation": { + "name": "generation", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entered_at": { + "name": "entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.object_versions": { + "name": "object_versions", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_object_id": { + "name": "parent_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name_ciphertext": { + "name": "name_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_blind_index": { + "name": "name_blind_index", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "blob_refs": { + "name": "blob_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "object_versions_created_idx": { + "name": "object_versions_created_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "object_versions_sequence_idx": { + "name": "object_versions_sequence_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "object_versions_workspace_id_workspaces_id_fk": { + "name": "object_versions_workspace_id_workspaces_id_fk", + "tableFrom": "object_versions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "object_versions_source_device_id_devices_id_fk": { + "name": "object_versions_source_device_id_devices_id_fk", + "tableFrom": "object_versions", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "object_versions_workspace_key_fk": { + "name": "object_versions_workspace_key_fk", + "tableFrom": "object_versions", + "tableTo": "workspace_keys", + "columnsFrom": [ + "workspace_id", + "key_version" + ], + "columnsTo": [ + "workspace_id", + "key_version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "object_versions_workspace_id_object_id_revision_pk": { + "name": "object_versions_workspace_id_object_id_revision_pk", + "columns": [ + "workspace_id", + "object_id", + "revision" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.objects": { + "name": "objects", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_object_id": { + "name": "parent_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name_ciphertext": { + "name": "name_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name_blind_index": { + "name": "name_blind_index", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "current_revision": { + "name": "current_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "blob_refs": { + "name": "blob_refs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "objects_workspace_kind_idx": { + "name": "objects_workspace_kind_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_workspace_updated_idx": { + "name": "objects_workspace_updated_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "objects_sibling_name_blind_idx": { + "name": "objects_sibling_name_blind_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name_blind_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"objects\".\"deleted_at\" is null and \"objects\".\"name_blind_index\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "objects_workspace_id_workspaces_id_fk": { + "name": "objects_workspace_id_workspaces_id_fk", + "tableFrom": "objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "objects_workspace_key_fk": { + "name": "objects_workspace_key_fk", + "tableFrom": "objects", + "tableTo": "workspace_keys", + "columnsFrom": [ + "workspace_id", + "key_version" + ], + "columnsTo": [ + "workspace_id", + "key_version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "objects_workspace_id_object_id_pk": { + "name": "objects_workspace_id_object_id_pk", + "columns": [ + "workspace_id", + "object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.operations": { + "name": "operations", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "operation_id": { + "name": "operation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "result_revision": { + "name": "result_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "result_sequence": { + "name": "result_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "operations_created_idx": { + "name": "operations_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "operations_workspace_id_workspaces_id_fk": { + "name": "operations_workspace_id_workspaces_id_fk", + "tableFrom": "operations", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "operations_source_device_id_devices_id_fk": { + "name": "operations_source_device_id_devices_id_fk", + "tableFrom": "operations", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "operations_workspace_id_operation_id_pk": { + "name": "operations_workspace_id_operation_id_pk", + "columns": [ + "workspace_id", + "operation_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.outbox_messages": { + "name": "outbox_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_or_event": { + "name": "template_or_event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient_ref": { + "name": "recipient_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "payload_version": { + "name": "payload_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "secret_payload_ref": { + "name": "secret_payload_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "outbox_message_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "outbox_messages_idempotency_unique": { + "name": "outbox_messages_idempotency_unique", + "columns": [ + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbox_messages_claim_idx": { + "name": "outbox_messages_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_acceptances": { + "name": "policy_acceptances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_snapshot": { + "name": "subject_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "policy_document_id": { + "name": "policy_document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "ip_prefix_hash": { + "name": "ip_prefix_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent_family": { + "name": "user_agent_family", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "evidence_version": { + "name": "evidence_version", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "policy_acceptances_subject_idx": { + "name": "policy_acceptances_subject_idx", + "columns": [ + { + "expression": "subject_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "accepted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "policy_acceptances_account_id_accounts_id_fk": { + "name": "policy_acceptances_account_id_accounts_id_fk", + "tableFrom": "policy_acceptances", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "policy_acceptances_policy_document_id_policy_documents_id_fk": { + "name": "policy_acceptances_policy_document_id_policy_documents_id_fk", + "tableFrom": "policy_acceptances", + "tableTo": "policy_documents", + "columnsFrom": [ + "policy_document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.policy_documents": { + "name": "policy_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_ref": { + "name": "content_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "canonicalization_version": { + "name": "canonicalization_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "requires_reacceptance": { + "name": "requires_reacceptance", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "retired_at": { + "name": "retired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "policy_documents_version_locale_unique": { + "name": "policy_documents_version_locale_unique", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "locale", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_buckets": { + "name": "rate_limit_buckets", + "schema": "", + "columns": { + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rate_key": { + "name": "rate_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "hits": { + "name": "hits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "rate_limit_buckets_expiry_idx": { + "name": "rate_limit_buckets_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "rate_limit_buckets_scope_rate_key_window_start_pk": { + "name": "rate_limit_buckets_scope_rate_key_window_start_pk", + "columns": [ + "scope", + "rate_key", + "window_start" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "issued_instance_auth_epoch": { + "name": "issued_instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotation_request_id": { + "name": "rotation_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rotation_response_ciphertext": { + "name": "rotation_response_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rotation_response_expires_at": { + "name": "rotation_response_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "refresh_tokens_hash_unique": { + "name": "refresh_tokens_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "refresh_tokens_device_idx": { + "name": "refresh_tokens_device_idx", + "columns": [ + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "refresh_tokens_account_id_accounts_id_fk": { + "name": "refresh_tokens_account_id_accounts_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "refresh_tokens_device_id_devices_id_fk": { + "name": "refresh_tokens_device_id_devices_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_invitation_uses": { + "name": "registration_invitation_uses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invitation_id": { + "name": "invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "used_at": { + "name": "used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "registration_invitation_uses_account_unique": { + "name": "registration_invitation_uses_account_unique", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "registration_invitation_uses_invitation_idx": { + "name": "registration_invitation_uses_invitation_idx", + "columns": [ + { + "expression": "invitation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "registration_invitation_uses_invitation_id_registration_invitations_id_fk": { + "name": "registration_invitation_uses_invitation_id_registration_invitations_id_fk", + "tableFrom": "registration_invitation_uses", + "tableTo": "registration_invitations", + "columnsFrom": [ + "invitation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "registration_invitation_uses_account_id_accounts_id_fk": { + "name": "registration_invitation_uses_account_id_accounts_id_fk", + "tableFrom": "registration_invitation_uses", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.registration_invitations": { + "name": "registration_invitations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_key_id": { + "name": "token_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hint": { + "name": "token_hint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "invitation_actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_by_actor_id": { + "name": "created_by_actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_snapshot": { + "name": "creator_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "bound_email_normalized": { + "name": "bound_email_normalized", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sent_at": { + "name": "last_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replaces_invitation_id": { + "name": "replaces_invitation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "registration_invitations_token_hash_unique": { + "name": "registration_invitations_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "registration_invitations_active_idx": { + "name": "registration_invitations_active_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "registration_invitations_use_count_check": { + "name": "registration_invitations_use_count_check", + "value": "\"registration_invitations\".\"max_uses\" > 0 and \"registration_invitations\".\"use_count\" >= 0 and \"registration_invitations\".\"use_count\" <= \"registration_invitations\".\"max_uses\"" + } + }, + "isRLSEnabled": false + }, + "public.restore_credential_reviews": { + "name": "restore_credential_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "restore_marker_id": { + "name": "restore_marker_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "operator_kind": { + "name": "operator_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local-operator'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "restore_credential_reviews_marker_account_unique": { + "name": "restore_credential_reviews_marker_account_unique", + "columns": [ + { + "expression": "restore_marker_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "restore_credential_reviews_restore_marker_id_restore_markers_id_fk": { + "name": "restore_credential_reviews_restore_marker_id_restore_markers_id_fk", + "tableFrom": "restore_credential_reviews", + "tableTo": "restore_markers", + "columnsFrom": [ + "restore_marker_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "restore_credential_reviews_account_id_accounts_id_fk": { + "name": "restore_credential_reviews_account_id_accounts_id_fk", + "tableFrom": "restore_credential_reviews", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restore_drills": { + "name": "restore_drills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_run_id": { + "name": "backup_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "restore_drill_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checks": { + "name": "checks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "restore_drills_backup_run_id_backup_runs_id_fk": { + "name": "restore_drills_backup_run_id_backup_runs_id_fk", + "tableFrom": "restore_drills", + "tableTo": "backup_runs", + "columnsFrom": [ + "backup_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.restore_markers": { + "name": "restore_markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "backup_id": { + "name": "backup_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "restore_marker_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "old_sync_epoch": { + "name": "old_sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "new_sync_epoch": { + "name": "new_sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "restored_through_sequence_by_workspace": { + "name": "restored_through_sequence_by_workspace", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sanitation_status": { + "name": "sanitation_status", + "type": "restore_sanitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "auth_epoch_after": { + "name": "auth_epoch_after", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "bootstrap_token_cutoff": { + "name": "bootstrap_token_cutoff", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bootstrap_reissue_required": { + "name": "bootstrap_reissue_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "restore_markers_new_epoch_unique": { + "name": "restore_markers_new_epoch_unique", + "columns": [ + { + "expression": "new_sync_epoch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_events": { + "name": "risk_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "identity_hash": { + "name": "identity_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ip_prefix_hash": { + "name": "ip_prefix_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent_family": { + "name": "user_agent_family", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason_codes": { + "name": "reason_codes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "score": { + "name": "score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_events_account_created_idx": { + "name": "risk_events_account_created_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_events_event_created_idx": { + "name": "risk_events_event_created_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_events_account_id_accounts_id_fk": { + "name": "risk_events_account_id_accounts_id_fk", + "tableFrom": "risk_events", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_provider_events": { + "name": "risk_provider_events", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signature_verified_at": { + "name": "signature_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "payload_redacted": { + "name": "payload_redacted", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "risk_provider_event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "risk_provider_events_claim_idx": { + "name": "risk_provider_events_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "risk_provider_events_provider_provider_event_id_pk": { + "name": "risk_provider_events_provider_provider_event_id_pk", + "columns": [ + "provider", + "provider_event_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_restrictions": { + "name": "risk_restrictions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "subject_type": { + "name": "subject_type", + "type": "risk_restriction_subject_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subject_ref": { + "name": "subject_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "risk_restriction_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "risk_restriction_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "risk_restriction_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_staff_id": { + "name": "created_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "revoked_by_staff_id": { + "name": "revoked_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_restrictions_active_unique": { + "name": "risk_restrictions_active_unique", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"risk_restrictions\".\"revoked_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_restrictions_subject_idx": { + "name": "risk_restrictions_subject_idx", + "columns": [ + { + "expression": "subject_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_restrictions_created_by_staff_id_staff_principals_id_fk": { + "name": "risk_restrictions_created_by_staff_id_staff_principals_id_fk", + "tableFrom": "risk_restrictions", + "tableTo": "staff_principals", + "columnsFrom": [ + "created_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "risk_restrictions_revoked_by_staff_id_staff_principals_id_fk": { + "name": "risk_restrictions_revoked_by_staff_id_staff_principals_id_fk", + "tableFrom": "risk_restrictions", + "tableTo": "staff_principals", + "columnsFrom": [ + "revoked_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.server_metadata": { + "name": "server_metadata", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_principals": { + "name": "staff_principals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "external_issuer": { + "name": "external_issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_subject": { + "name": "external_subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_login": { + "name": "local_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_password_hash": { + "name": "local_password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_login_at": { + "name": "last_login_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "staff_principals_issuer_subject_unique": { + "name": "staff_principals_issuer_subject_unique", + "columns": [ + { + "expression": "external_issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_subject", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "staff_principals_local_login_unique": { + "name": "staff_principals_local_login_unique", + "columns": [ + { + "expression": "local_login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"staff_principals\".\"local_login\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "staff_principals_active_idx": { + "name": "staff_principals_active_idx", + "columns": [ + { + "expression": "disabled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_role_assignments": { + "name": "staff_role_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "role_key": { + "name": "role_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "assigned_by_staff_id": { + "name": "assigned_by_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "staff_role_assignments_staff_role_idx": { + "name": "staff_role_assignments_staff_role_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "staff_role_assignments_active_idx": { + "name": "staff_role_assignments_active_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "staff_role_assignments_staff_id_staff_principals_id_fk": { + "name": "staff_role_assignments_staff_id_staff_principals_id_fk", + "tableFrom": "staff_role_assignments", + "tableTo": "staff_principals", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "staff_role_assignments_assigned_by_staff_id_staff_principals_id_fk": { + "name": "staff_role_assignments_assigned_by_staff_id_staff_principals_id_fk", + "tableFrom": "staff_role_assignments", + "tableTo": "staff_principals", + "columnsFrom": [ + "assigned_by_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.staff_sessions": { + "name": "staff_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "staff_id": { + "name": "staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "auth_strength": { + "name": "auth_strength", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "csrf_token_hash": { + "name": "csrf_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "staff_sessions_active_idx": { + "name": "staff_sessions_active_idx", + "columns": [ + { + "expression": "staff_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "staff_sessions_staff_id_staff_principals_id_fk": { + "name": "staff_sessions_staff_id_staff_principals_id_fk", + "tableFrom": "staff_sessions", + "tableTo": "staff_principals", + "columnsFrom": [ + "staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.step_up_grants": { + "name": "step_up_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "digest_key_id": { + "name": "digest_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "step_up_actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action_audience": { + "name": "action_audience", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "auth_methods": { + "name": "auth_methods", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "step_up_grants_token_digest_unique": { + "name": "step_up_grants_token_digest_unique", + "columns": [ + { + "expression": "token_digest", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "step_up_grants_actor_session_expiry_idx": { + "name": "step_up_grants_actor_session_expiry_idx", + "columns": [ + { + "expression": "actor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_cases": { + "name": "support_cases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subject_hash": { + "name": "subject_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_snapshot": { + "name": "account_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "category": { + "name": "category", + "type": "support_case_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "support_case_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "support_case_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "support_case_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "assigned_staff_id": { + "name": "assigned_staff_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assigned_staff_snapshot": { + "name": "assigned_staff_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_cases_account_updated_idx": { + "name": "support_cases_account_updated_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_cases_queue_idx": { + "name": "support_cases_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_cases_account_id_accounts_id_fk": { + "name": "support_cases_account_id_accounts_id_fk", + "tableFrom": "support_cases", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_cases_assigned_staff_id_staff_principals_id_fk": { + "name": "support_cases_assigned_staff_id_staff_principals_id_fk", + "tableFrom": "support_cases", + "tableTo": "staff_principals", + "columnsFrom": [ + "assigned_staff_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_diagnostic_grants": { + "name": "support_diagnostic_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_version": { + "name": "scope_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_ciphertext": { + "name": "snapshot_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_key_id": { + "name": "snapshot_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snapshot_encryption_version": { + "name": "snapshot_encryption_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_diagnostic_grants_case_idx": { + "name": "support_diagnostic_grants_case_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_diagnostic_grants_account_idx": { + "name": "support_diagnostic_grants_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_diagnostic_grants_case_id_support_cases_id_fk": { + "name": "support_diagnostic_grants_case_id_support_cases_id_fk", + "tableFrom": "support_diagnostic_grants", + "tableTo": "support_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "support_diagnostic_grants_account_id_accounts_id_fk": { + "name": "support_diagnostic_grants_account_id_accounts_id_fk", + "tableFrom": "support_diagnostic_grants", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "support_diagnostic_grants_scope_check": { + "name": "support_diagnostic_grants_scope_check", + "value": "length(\"support_diagnostic_grants\".\"scope_version\") > 0" + }, + "support_diagnostic_grants_encryption_version_check": { + "name": "support_diagnostic_grants_encryption_version_check", + "value": "\"support_diagnostic_grants\".\"snapshot_encryption_version\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.support_messages": { + "name": "support_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "case_id": { + "name": "case_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_type": { + "name": "author_type", + "type": "support_message_author_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "author_ref": { + "name": "author_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_snapshot": { + "name": "author_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "support_message_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'customer'" + }, + "body_ciphertext": { + "name": "body_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_key_id": { + "name": "body_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body_encryption_version": { + "name": "body_encryption_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "body_format": { + "name": "body_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'plain'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_messages_case_author_idempotency_unique": { + "name": "support_messages_case_author_idempotency_unique", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_messages_case_created_idx": { + "name": "support_messages_case_created_idx", + "columns": [ + { + "expression": "case_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_messages_case_id_support_cases_id_fk": { + "name": "support_messages_case_id_support_cases_id_fk", + "tableFrom": "support_messages", + "tableTo": "support_cases", + "columnsFrom": [ + "case_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "support_messages_encryption_version_check": { + "name": "support_messages_encryption_version_check", + "value": "\"support_messages\".\"body_encryption_version\" > 0" + }, + "support_messages_key_id_check": { + "name": "support_messages_key_id_check", + "value": "length(\"support_messages\".\"body_key_id\") > 0" + } + }, + "isRLSEnabled": false + }, + "public.sync_bootstrap_objects": { + "name": "sync_bootstrap_objects", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_document_sequence": { + "name": "latest_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checkpoint_document_sequence": { + "name": "checkpoint_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "checkpoint_key_version": { + "name": "checkpoint_key_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext": { + "name": "checkpoint_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext_hash": { + "name": "checkpoint_ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialized_revision": { + "name": "materialized_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sync_bootstrap_objects_workspace_idx": { + "name": "sync_bootstrap_objects_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_bootstrap_objects_session_id_sync_bootstrap_sessions_id_fk": { + "name": "sync_bootstrap_objects_session_id_sync_bootstrap_sessions_id_fk", + "tableFrom": "sync_bootstrap_objects", + "tableTo": "sync_bootstrap_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_bootstrap_objects_workspace_id_workspaces_id_fk": { + "name": "sync_bootstrap_objects_workspace_id_workspaces_id_fk", + "tableFrom": "sync_bootstrap_objects", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_bootstrap_objects_session_id_object_id_pk": { + "name": "sync_bootstrap_objects_session_id_object_id_pk", + "columns": [ + "session_id", + "object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_bootstrap_sessions": { + "name": "sync_bootstrap_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_sequence": { + "name": "snapshot_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_bootstrap_sessions_workspace_idx": { + "name": "sync_bootstrap_sessions_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_bootstrap_sessions_workspace_id_workspaces_id_fk": { + "name": "sync_bootstrap_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "sync_bootstrap_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_checkpoints": { + "name": "sync_checkpoints", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "covers_document_sequence": { + "name": "covers_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_sequence": { + "name": "event_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "materialized_revision": { + "name": "materialized_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_checkpoints_document_idx": { + "name": "sync_checkpoints_document_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_checkpoints_workspace_id_workspaces_id_fk": { + "name": "sync_checkpoints_workspace_id_workspaces_id_fk", + "tableFrom": "sync_checkpoints", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_checkpoints_source_device_id_devices_id_fk": { + "name": "sync_checkpoints_source_device_id_devices_id_fk", + "tableFrom": "sync_checkpoints", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_checkpoints_workspace_id_checkpoint_id_pk": { + "name": "sync_checkpoints_workspace_id_checkpoint_id_pk", + "columns": [ + "workspace_id", + "checkpoint_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_commands": { + "name": "sync_commands", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_epoch": { + "name": "sync_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_commands_created_idx": { + "name": "sync_commands_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_commands_workspace_id_workspaces_id_fk": { + "name": "sync_commands_workspace_id_workspaces_id_fk", + "tableFrom": "sync_commands", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_commands_source_device_id_devices_id_fk": { + "name": "sync_commands_source_device_id_devices_id_fk", + "tableFrom": "sync_commands", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_commands_workspace_id_command_id_pk": { + "name": "sync_commands_workspace_id_command_id_pk", + "columns": [ + "workspace_id", + "command_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_conflicts": { + "name": "sync_conflicts", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conflict_id": { + "name": "conflict_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "conflict_type": { + "name": "conflict_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unresolved'" + }, + "expected_revision": { + "name": "expected_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "expected_document_sequence": { + "name": "expected_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_sequence": { + "name": "created_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resolved_sequence": { + "name": "resolved_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "resolved_by_device_id": { + "name": "resolved_by_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_conflicts_status_idx": { + "name": "sync_conflicts_status_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_conflicts_object_idx": { + "name": "sync_conflicts_object_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_conflicts_workspace_id_workspaces_id_fk": { + "name": "sync_conflicts_workspace_id_workspaces_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_conflicts_resolved_by_device_id_devices_id_fk": { + "name": "sync_conflicts_resolved_by_device_id_devices_id_fk", + "tableFrom": "sync_conflicts", + "tableTo": "devices", + "columnsFrom": [ + "resolved_by_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_conflicts_workspace_id_conflict_id_pk": { + "name": "sync_conflicts_workspace_id_conflict_id_pk", + "columns": [ + "workspace_id", + "conflict_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_device_cursors": { + "name": "sync_device_cursors", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "acknowledged_sequence": { + "name": "acknowledged_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_device_cursors_updated_idx": { + "name": "sync_device_cursors_updated_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_device_cursors_workspace_id_workspaces_id_fk": { + "name": "sync_device_cursors_workspace_id_workspaces_id_fk", + "tableFrom": "sync_device_cursors", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_device_cursors_device_id_devices_id_fk": { + "name": "sync_device_cursors_device_id_devices_id_fk", + "tableFrom": "sync_device_cursors", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_device_cursors_workspace_id_device_id_pk": { + "name": "sync_device_cursors_workspace_id_device_id_pk", + "columns": [ + "workspace_id", + "device_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_documents": { + "name": "sync_documents", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "object_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "latest_document_sequence": { + "name": "latest_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "checkpoint_document_sequence": { + "name": "checkpoint_document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "checkpoint_id": { + "name": "checkpoint_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "checkpoint_key_version": { + "name": "checkpoint_key_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext": { + "name": "checkpoint_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkpoint_ciphertext_hash": { + "name": "checkpoint_ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "materialized_revision": { + "name": "materialized_revision", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_documents_object_unique": { + "name": "sync_documents_object_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_documents_workspace_id_workspaces_id_fk": { + "name": "sync_documents_workspace_id_workspaces_id_fk", + "tableFrom": "sync_documents", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_documents_workspace_id_document_id_pk": { + "name": "sync_documents_workspace_id_document_id_pk", + "columns": [ + "workspace_id", + "document_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_events": { + "name": "sync_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "command_id": { + "name": "command_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_id": { + "name": "object_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "document_sequence": { + "name": "document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_events_workspace_sequence_unique": { + "name": "sync_events_workspace_sequence_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_events_workspace_event_unique": { + "name": "sync_events_workspace_event_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_events_document_idx": { + "name": "sync_events_document_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_events_created_idx": { + "name": "sync_events_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_events_workspace_id_workspaces_id_fk": { + "name": "sync_events_workspace_id_workspaces_id_fk", + "tableFrom": "sync_events", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_events_source_device_id_devices_id_fk": { + "name": "sync_events_source_device_id_devices_id_fk", + "tableFrom": "sync_events", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_resource_bindings": { + "name": "sync_resource_bindings", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_object_id": { + "name": "owner_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_revision": { + "name": "owner_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resource_object_id": { + "name": "resource_object_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_revision": { + "name": "resource_revision", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_resource_bindings_resource_idx": { + "name": "sync_resource_bindings_resource_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_resource_bindings_owner_idx": { + "name": "sync_resource_bindings_owner_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_object_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_resource_bindings_workspace_id_workspaces_id_fk": { + "name": "sync_resource_bindings_workspace_id_workspaces_id_fk", + "tableFrom": "sync_resource_bindings", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_resource_bindings_owner_version_fk": { + "name": "sync_resource_bindings_owner_version_fk", + "tableFrom": "sync_resource_bindings", + "tableTo": "object_versions", + "columnsFrom": [ + "workspace_id", + "owner_object_id", + "owner_revision" + ], + "columnsTo": [ + "workspace_id", + "object_id", + "revision" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_resource_bindings_resource_version_fk": { + "name": "sync_resource_bindings_resource_version_fk", + "tableFrom": "sync_resource_bindings", + "tableTo": "object_versions", + "columnsFrom": [ + "workspace_id", + "resource_object_id", + "resource_revision" + ], + "columnsTo": [ + "workspace_id", + "object_id", + "revision" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_resource_bindings_workspace_id_owner_object_id_owner_revision_resource_object_id_pk": { + "name": "sync_resource_bindings_workspace_id_owner_object_id_owner_revision_resource_object_id_pk", + "columns": [ + "workspace_id", + "owner_object_id", + "owner_revision", + "resource_object_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sync_updates": { + "name": "sync_updates", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_sequence": { + "name": "document_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "update_id": { + "name": "update_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_sequence": { + "name": "event_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_device_id": { + "name": "source_device_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "ciphertext": { + "name": "ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ciphertext_hash": { + "name": "ciphertext_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sync_updates_id_unique": { + "name": "sync_updates_id_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "update_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sync_updates_event_idx": { + "name": "sync_updates_event_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_sequence", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sync_updates_workspace_id_workspaces_id_fk": { + "name": "sync_updates_workspace_id_workspaces_id_fk", + "tableFrom": "sync_updates", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sync_updates_source_device_id_devices_id_fk": { + "name": "sync_updates_source_device_id_devices_id_fk", + "tableFrom": "sync_updates", + "tableTo": "devices", + "columnsFrom": [ + "source_device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sync_updates_workspace_id_document_id_document_sequence_pk": { + "name": "sync_updates_workspace_id_document_id_document_sequence_pk", + "columns": [ + "workspace_id", + "document_id", + "document_sequence" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_events": { + "name": "usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delta": { + "name": "delta", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resulting_value": { + "name": "resulting_value", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_period": { + "name": "billing_period", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "usage_events_workspace_idempotency_unique": { + "name": "usage_events_workspace_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_events\".\"workspace_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_events_account_idempotency_unique": { + "name": "usage_events_account_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_events\".\"workspace_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_events_account_occurred_idx": { + "name": "usage_events_account_occurred_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_events_account_id_accounts_id_fk": { + "name": "usage_events_account_id_accounts_id_fk", + "tableFrom": "usage_events", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_reservations": { + "name": "usage_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_upload_ref": { + "name": "provider_upload_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "usage_reservation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "usage_reservations_workspace_source_unique": { + "name": "usage_reservations_workspace_source_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_reservations\".\"workspace_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_reservations_account_source_unique": { + "name": "usage_reservations_account_source_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"usage_reservations\".\"workspace_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_reservations_expiry_idx": { + "name": "usage_reservations_expiry_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_reservations_account_id_accounts_id_fk": { + "name": "usage_reservations_account_id_accounts_id_fk", + "tableFrom": "usage_reservations", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.web_sessions": { + "name": "web_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "csrf_token_hash": { + "name": "csrf_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "issued_instance_auth_epoch": { + "name": "issued_instance_auth_epoch", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_ip": { + "name": "last_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "web_sessions_token_hash_unique": { + "name": "web_sessions_token_hash_unique", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "web_sessions_account_idx": { + "name": "web_sessions_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "web_sessions_expiry_idx": { + "name": "web_sessions_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "web_sessions_account_id_accounts_id_fk": { + "name": "web_sessions_account_id_accounts_id_fk", + "tableFrom": "web_sessions", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_key_envelopes": { + "name": "workspace_key_envelopes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "envelope_type": { + "name": "envelope_type", + "type": "key_envelope_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipient_id": { + "name": "recipient_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wrapped_key": { + "name": "wrapped_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kdf_salt": { + "name": "kdf_salt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kdf_params": { + "name": "kdf_params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "key_envelope_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "replacement_idempotency_key": { + "name": "replacement_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replacement_request_hash": { + "name": "replacement_request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_key_envelopes_key_idx": { + "name": "workspace_key_envelopes_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_key_envelopes_active_recipient_unique": { + "name": "workspace_key_envelopes_active_recipient_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "envelope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"recipient_id\", '')", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_key_envelopes\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_key_envelopes_replacement_idempotency_unique": { + "name": "workspace_key_envelopes_replacement_idempotency_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "replacement_idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspace_key_envelopes\".\"replacement_idempotency_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_key_envelopes_key_fk": { + "name": "workspace_key_envelopes_key_fk", + "tableFrom": "workspace_key_envelopes", + "tableTo": "workspace_keys", + "columnsFrom": [ + "workspace_id", + "key_version" + ], + "columnsTo": [ + "workspace_id", + "key_version" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_keys": { + "name": "workspace_keys", + "schema": "", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_version": { + "name": "key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_keys_workspace_id_workspaces_id_fk": { + "name": "workspace_keys_workspace_id_workspaces_id_fk", + "tableFrom": "workspace_keys", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "workspace_keys_workspace_id_key_version_pk": { + "name": "workspace_keys_workspace_id_key_version_pk", + "columns": [ + "workspace_id", + "key_version" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name_ciphertext": { + "name": "name_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "creation_idempotency_key": { + "name": "creation_idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "creation_request_hash": { + "name": "creation_request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "latest_sequence": { + "name": "latest_sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspaces_account_idx": { + "name": "workspaces_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_active_default_unique": { + "name": "workspaces_active_default_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspaces\".\"is_default\" = true and \"workspaces\".\"deleted_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_account_creation_idempotency_unique": { + "name": "workspaces_account_creation_idempotency_unique", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "creation_idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"workspaces\".\"creation_idempotency_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_account_id_accounts_id_fk": { + "name": "workspaces_account_id_accounts_id_fk", + "tableFrom": "workspaces", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_action_token_purpose": { + "name": "account_action_token_purpose", + "schema": "public", + "values": [ + "verify_email", + "reset_password", + "change_email" + ] + }, + "public.account_identity_kind": { + "name": "account_identity_kind", + "schema": "public", + "values": [ + "username", + "email" + ] + }, + "public.account_identity_state": { + "name": "account_identity_state", + "schema": "public", + "values": [ + "pending_verification", + "active", + "legacy_migration" + ] + }, + "public.account_login_claim_kind": { + "name": "account_login_claim_kind", + "schema": "public", + "values": [ + "legacy_username", + "username", + "email" + ] + }, + "public.account_subscription_status": { + "name": "account_subscription_status", + "schema": "public", + "values": [ + "incomplete", + "trialing", + "active", + "past_due", + "grace", + "paused", + "ended", + "review" + ] + }, + "public.background_job_status": { + "name": "background_job_status", + "schema": "public", + "values": [ + "pending", + "running", + "succeeded", + "dead_letter", + "cancelled" + ] + }, + "public.backup_run_status": { + "name": "backup_run_status", + "schema": "public", + "values": [ + "queued", + "preparing", + "draining", + "dumping", + "copying", + "verifying", + "ready", + "failed", + "deleting" + ] + }, + "public.billing_checkout_status": { + "name": "billing_checkout_status", + "schema": "public", + "values": [ + "pending_provider", + "open", + "completed", + "linked", + "expired", + "failed", + "reconciling" + ] + }, + "public.billing_plan_interval": { + "name": "billing_plan_interval", + "schema": "public", + "values": [ + "month", + "year" + ] + }, + "public.billing_provider_environment": { + "name": "billing_provider_environment", + "schema": "public", + "values": [ + "test", + "live" + ] + }, + "public.billing_webhook_event_status": { + "name": "billing_webhook_event_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed", + "ignored", + "failed", + "dead_letter" + ] + }, + "public.blob_state": { + "name": "blob_state", + "schema": "public", + "values": [ + "uploading", + "ready", + "deleting" + ] + }, + "public.bootstrap_credential_source": { + "name": "bootstrap_credential_source", + "schema": "public", + "values": [ + "cli", + "legacy_environment" + ] + }, + "public.change_type": { + "name": "change_type", + "schema": "public", + "values": [ + "upsert", + "delete" + ] + }, + "public.data_request_status": { + "name": "data_request_status", + "schema": "public", + "values": [ + "submitted", + "identity_check", + "queued", + "processing", + "awaiting_user", + "completed", + "rejected", + "canceled", + "held", + "failed" + ] + }, + "public.data_request_type": { + "name": "data_request_type", + "schema": "public", + "values": [ + "access", + "export", + "correct", + "delete", + "restrict", + "object" + ] + }, + "public.deletion_case_status": { + "name": "deletion_case_status", + "schema": "public", + "values": [ + "requested", + "cooling_off", + "scheduled", + "held", + "purging", + "completed", + "canceled", + "failed" + ] + }, + "public.deletion_fence_state": { + "name": "deletion_fence_state", + "schema": "public", + "values": [ + "cooling_off", + "scheduled", + "purging", + "completed", + "canceled" + ] + }, + "public.deletion_step_state": { + "name": "deletion_step_state", + "schema": "public", + "values": [ + "pending", + "running", + "completed", + "failed", + "skipped" + ] + }, + "public.deployment_mode": { + "name": "deployment_mode", + "schema": "public", + "values": [ + "hosted", + "self-hosted" + ] + }, + "public.device_authorization_status": { + "name": "device_authorization_status", + "schema": "public", + "values": [ + "pending", + "approved", + "denied" + ] + }, + "public.entitlement_grant_source": { + "name": "entitlement_grant_source", + "schema": "public", + "values": [ + "promotion", + "support", + "migration", + "staff" + ] + }, + "public.invitation_actor_type": { + "name": "invitation_actor_type", + "schema": "public", + "values": [ + "account", + "staff", + "system" + ] + }, + "public.key_envelope_status": { + "name": "key_envelope_status", + "schema": "public", + "values": [ + "active", + "revoked" + ] + }, + "public.key_envelope_type": { + "name": "key_envelope_type", + "schema": "public", + "values": [ + "passphrase", + "recovery", + "device", + "managed" + ] + }, + "public.maintenance_mode": { + "name": "maintenance_mode", + "schema": "public", + "values": [ + "normal", + "read_only", + "write_drain", + "offline" + ] + }, + "public.object_kind": { + "name": "object_kind", + "schema": "public", + "values": [ + "note", + "folder", + "asset", + "canvas", + "record", + "tag", + "mark", + "conversation", + "memory", + "setting", + "yjs-checkpoint", + "yjs-update" + ] + }, + "public.outbox_message_status": { + "name": "outbox_message_status", + "schema": "public", + "values": [ + "pending", + "sending", + "sent", + "dead_letter", + "delivery_unknown" + ] + }, + "public.registration_policy": { + "name": "registration_policy", + "schema": "public", + "values": [ + "bootstrap", + "disabled", + "invitation", + "public" + ] + }, + "public.restore_drill_mode": { + "name": "restore_drill_mode", + "schema": "public", + "values": [ + "verify-only", + "isolated-restore", + "full-drill" + ] + }, + "public.restore_marker_mode": { + "name": "restore_marker_mode", + "schema": "public", + "values": [ + "preserve", + "clone" + ] + }, + "public.restore_sanitation_status": { + "name": "restore_sanitation_status", + "schema": "public", + "values": [ + "pending", + "running", + "complete", + "failed" + ] + }, + "public.risk_provider_event_status": { + "name": "risk_provider_event_status", + "schema": "public", + "values": [ + "pending", + "processing", + "processed", + "ignored", + "failed", + "dead_letter" + ] + }, + "public.risk_restriction_action": { + "name": "risk_restriction_action", + "schema": "public", + "values": [ + "challenge", + "lock", + "read_only", + "deny", + "review" + ] + }, + "public.risk_restriction_scope": { + "name": "risk_restriction_scope", + "schema": "public", + "values": [ + "registration", + "authentication", + "recovery", + "device", + "sync_write", + "blob", + "billing", + "all" + ] + }, + "public.risk_restriction_source": { + "name": "risk_restriction_source", + "schema": "public", + "values": [ + "automatic", + "staff", + "provider" + ] + }, + "public.risk_restriction_subject_type": { + "name": "risk_restriction_subject_type", + "schema": "public", + "values": [ + "account", + "identity", + "device", + "ip_prefix" + ] + }, + "public.self_hosted_lifecycle": { + "name": "self_hosted_lifecycle", + "schema": "public", + "values": [ + "uninitialized", + "ready" + ] + }, + "public.step_up_actor_type": { + "name": "step_up_actor_type", + "schema": "public", + "values": [ + "account", + "staff" + ] + }, + "public.support_case_category": { + "name": "support_case_category", + "schema": "public", + "values": [ + "account", + "sync", + "device", + "encryption", + "billing", + "privacy", + "abuse", + "other" + ] + }, + "public.support_case_severity": { + "name": "support_case_severity", + "schema": "public", + "values": [ + "normal", + "high", + "urgent" + ] + }, + "public.support_case_source": { + "name": "support_case_source", + "schema": "public", + "values": [ + "web", + "client", + "email", + "staff", + "external" + ] + }, + "public.support_case_status": { + "name": "support_case_status", + "schema": "public", + "values": [ + "open", + "waiting_for_support", + "waiting_for_user", + "resolved", + "closed", + "spam" + ] + }, + "public.support_message_author_type": { + "name": "support_message_author_type", + "schema": "public", + "values": [ + "account", + "staff", + "system" + ] + }, + "public.support_message_visibility": { + "name": "support_message_visibility", + "schema": "public", + "values": [ + "customer", + "internal" + ] + }, + "public.usage_reservation_status": { + "name": "usage_reservation_status", + "schema": "public", + "values": [ + "reserved", + "external_started", + "committed", + "released", + "expired", + "reconciling" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/server/drizzle/meta/_journal.json b/apps/server/drizzle/meta/_journal.json index 54c4e98..fd5ae42 100644 --- a/apps/server/drizzle/meta/_journal.json +++ b/apps/server/drizzle/meta/_journal.json @@ -15,6 +15,34 @@ "when": 1786505769366, "tag": "0001_clean_omega_red", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1786578713623, + "tag": "0002_amazing_proemial_gods", + "breakpoints": true + }, + { + "idx": 3, + "version": "7", + "when": 1786624005937, + "tag": "0003_amused_tag", + "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1787130000000, + "tag": "0004_workspace_collaboration", + "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1787133600000, + "tag": "0005_workspace_object_boundaries", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/apps/server/scripts/self-test.ts b/apps/server/scripts/self-test.ts index a68b1e5..bdded9f 100644 --- a/apps/server/scripts/self-test.ts +++ b/apps/server/scripts/self-test.ts @@ -2,7 +2,6 @@ import { createHash, randomUUID } from 'node:crypto' import WebSocket from 'ws' const baseUrl = normalizeOrigin(process.env.SELF_TEST_BASE_URL ?? 'http://127.0.0.1:3789') -const setupToken = process.env.SELF_TEST_SETUP_TOKEN const keepData = process.env.SELF_TEST_KEEP_DATA === 'true' const password = 'notegen-self-test-password' @@ -20,6 +19,7 @@ async function main(): Promise { const capabilities = await request('/v1/capabilities') assert(capabilities.service === 'note-gen-server', '目标地址不是 NoteGen Sync Server') assert(capabilities.protocol.minimum <= 1 && capabilities.protocol.maximum >= 1, '服务端不支持协议 v1') + assert(capabilities.registration.methods.includes('password'), '自动验收需要管理员临时启用公开注册;完成后请恢复为仅邀请或关闭') console.log(`✓ 服务可用:${capabilities.serverName} ${capabilities.serverVersion}`) console.log(`✓ 实例身份:${capabilities.instanceId}`) @@ -29,7 +29,6 @@ async function main(): Promise { const deviceA = await request('/v1/auth/register', { method: 'POST', expectedStatus: 201, - ...(setupToken === undefined ? {} : { headers: { 'x-setup-token': setupToken } }), body: { login, password, @@ -208,16 +207,7 @@ async function request( }) const text = await response.text() if (response.status !== (options.expectedStatus ?? 200)) { - let hint = '' - try { - const error = JSON.parse(text) as { code?: string } - if (error.code === 'registration_closed' && setupToken === undefined) { - hint = '\n请设置 SELF_TEST_SETUP_TOKEN;它应与服务端 SETUP_TOKEN 一致。' - } - } catch { - // Preserve the raw response below. - } - throw new Error(`${options.method ?? 'GET'} ${path} 返回 ${response.status}: ${text}${hint}`) + throw new Error(`${options.method ?? 'GET'} ${path} 返回 ${response.status}: ${text}`) } if (response.status === 204 || text.length === 0) return undefined as T return JSON.parse(text) as T @@ -272,6 +262,7 @@ interface Capabilities { serverName: string serverVersion: string protocol: { minimum: number, maximum: number } + registration: { methods: string[] } } interface Session { accessToken: string } diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 3b443a2..16fe348 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -16,11 +16,11 @@ import { createAccountContextRoutes } from './routes/account-context.js' import { createSetupRoutes } from './routes/setup.js' import { createInvitationRoutes } from './routes/invitations.js' import { createComplianceRoutes } from './routes/compliance.js' -import { createBillingRoutes } from './routes/billing.js' import { createCapabilitiesRoutes } from './routes/capabilities.js' import { createHealthRoutes } from './routes/health.js' import { createAuthRoutes } from './routes/auth.js' import { createWorkspaceRoutes } from './routes/workspaces.js' +import { createWorkspaceCollaborationRoutes } from './routes/workspace-collaboration.js' import { createWebDashboardRoutes } from './routes/web-dashboard.js' import { createSyncRoutes } from './routes/sync.js' import { createEventRoutes } from './routes/events.js' @@ -32,18 +32,11 @@ import { createWebEmailRoutes } from './routes/web-email.js' import { createDeviceAuthorizationRoutes } from './routes/device-authorizations.js' import { createDevicePairingRoutes } from './routes/device-pairings.js' import { createWebAdminRoutes } from './routes/web-admin.js' -import { createSupportRoutes } from './routes/support.js' import { createMailAdminRoutes } from './routes/mail-admin.js' -import { createStaffSupportRoutes } from './routes/staff-support.js' -import { createStaffLegalHoldRoutes } from './routes/staff-legal-holds.js' -import { createStaffBillingRoutes } from './routes/staff-billing.js' -import { createStaffRiskRoutes } from './routes/staff-risk.js' import { isAllowedDevelopmentWebOrigin } from './development-origin.js' import { createPostgresRateLimitStore } from './observability/postgres-rate-limit-store.js' import type { DatabaseContext } from './database/client.js' import { createInstallationRoutes } from './routes/installation.js' -import { createStaffAuthRoutes } from './routes/staff-auth.js' -import { createStaffOperationsRoutes } from './routes/staff-operations.js' export async function buildApp( config: AppConfig, @@ -57,7 +50,6 @@ export async function buildApp( 'req.headers.authorization', 'req.headers["x-setup-token"]', 'req.headers["x-step-up-token"]', - 'req.headers["x-staff-session-id"]', 'req.body.password', 'req.body.smtpPassword', 'req.body.currentPassword', @@ -102,7 +94,7 @@ export async function buildApp( credentials: config.corsOrigins.length > 0 || config.nodeEnv === 'development', methods: ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], allowedHeaders: [ - 'authorization', 'content-type', 'x-request-id', 'x-setup-token', 'x-step-up-token', 'x-staff-session-id', 'x-csrf-token', 'range', + 'authorization', 'content-type', 'x-request-id', 'x-setup-token', 'x-step-up-token', 'x-csrf-token', 'range', ], exposedHeaders: [ 'accept-ranges', 'content-length', 'content-range', 'retry-after', @@ -126,7 +118,8 @@ export async function buildApp( }), }) await app.register(websocket, { options: { maxPayload: 4 * 1024 * 1024 } }) - registerMetrics(app, config) + registerMetrics(app, config, 'sql' in dependencies.database + ? dependencies.database as DatabaseContext : undefined) registerErrorHandler(app) app.addHook('onRequest', async (request) => { const safetyFailure = dependencies.deployment?.getSafetyFailure() @@ -186,6 +179,7 @@ export async function buildApp( 403: ErrorResponse, 404: ErrorResponse, 409: ErrorResponse, + 410: ErrorResponse, 413: ErrorResponse, 415: ErrorResponse, 416: ErrorResponse, @@ -213,37 +207,6 @@ export async function buildApp( if (dependencies.compliance !== undefined && dependencies.webSessions !== undefined) { await app.register(createComplianceRoutes(config, dependencies.compliance, dependencies.webSessions, dependencies.deletion, dependencies.stepUps)) } - if (dependencies.support !== undefined && dependencies.webSessions !== undefined - && dependencies.capabilities?.resolvePublic()['support.cases'] === true) { - await app.register(createSupportRoutes(dependencies.support, dependencies.webSessions)) - } - if (config.deploymentMode === 'hosted' && config.hostedReleaseStage === 'internal-test' - && dependencies.support !== undefined && dependencies.staffSessions !== undefined) { - await app.register(createStaffSupportRoutes(dependencies.support, dependencies.staffSessions)) - } - if (config.deploymentMode === 'hosted' && config.hostedReleaseStage === 'internal-test' - && dependencies.staff !== undefined && dependencies.staffSessions !== undefined) { - await app.register(createStaffAuthRoutes(config, dependencies.staff, dependencies.staffSessions)) - await app.register(createStaffOperationsRoutes( - dependencies.staff, - dependencies.staffSessions, - dependencies.support, - )) - } - if (config.deploymentMode === 'hosted' && config.hostedReleaseStage === 'internal-test' - && dependencies.legalHolds !== undefined && dependencies.staffSessions !== undefined) { - await app.register(createStaffLegalHoldRoutes(dependencies.legalHolds, dependencies.staffSessions)) - } - if (config.deploymentMode === 'hosted' && config.hostedReleaseStage === 'internal-test' - && dependencies.entitlements !== undefined && dependencies.staffSessions !== undefined) { - await app.register(createStaffBillingRoutes(dependencies.entitlements, dependencies.staffSessions)) - } - if (config.deploymentMode === 'hosted' && config.hostedReleaseStage === 'internal-test' && dependencies.risk !== undefined && dependencies.staffSessions !== undefined) { - await app.register(createStaffRiskRoutes(dependencies.risk, dependencies.staffSessions)) - } - if (dependencies.entitlements !== undefined && dependencies.webSessions !== undefined) { - await app.register(createBillingRoutes(dependencies.entitlements, dependencies.webSessions)) - } if (dependencies.auth !== undefined && dependencies.tokens !== undefined) { if (dependencies.deployment === undefined) throw new Error('Auth routes require DeploymentService') await app.register(createAuthRoutes(config, dependencies.auth, dependencies.tokens, dependencies.deployment, dependencies.deletion, dependencies.risk)) @@ -251,6 +214,11 @@ export async function buildApp( if (dependencies.workspaces !== undefined && dependencies.tokens !== undefined && dependencies.auth !== undefined) { await app.register(createWorkspaceRoutes(dependencies.workspaces, dependencies.tokens, dependencies.auth)) } + if (dependencies.workspaceCollaboration !== undefined && dependencies.tokens !== undefined && dependencies.auth !== undefined) { + await app.register(createWorkspaceCollaborationRoutes( + dependencies.workspaceCollaboration, dependencies.tokens, dependencies.auth, + )) + } if (dependencies.auth !== undefined && dependencies.tokens !== undefined && dependencies.deployment !== undefined) { await app.register(createAccountContextRoutes(dependencies.auth, dependencies.tokens, dependencies.deployment, dependencies.entitlements, dependencies.usage, dependencies.compliance, dependencies.risk, dependencies.usageHardEnforcementActive, dependencies.maintenanceCoordinator)) } @@ -260,8 +228,7 @@ export async function buildApp( if (dependencies.tokens !== undefined && dependencies.workspaces !== undefined && dependencies.notifier !== undefined && dependencies.auth !== undefined) { await app.register(createEventRoutes( - dependencies.tokens, dependencies.auth, dependencies.workspaces, dependencies.notifier, dependencies.maintenanceCoordinator, - dependencies.syncEpoch, + dependencies.tokens, dependencies.auth, dependencies.workspaces, dependencies.notifier, dependencies.syncEpoch, )) } if (dependencies.blobs !== undefined && dependencies.tokens !== undefined && dependencies.auth !== undefined) { diff --git a/apps/server/src/auth/service.ts b/apps/server/src/auth/service.ts index 4a38791..0a942cf 100644 --- a/apps/server/src/auth/service.ts +++ b/apps/server/src/auth/service.ts @@ -1,7 +1,7 @@ import argon2 from 'argon2' import { and, eq, isNull, ne, sql } from 'drizzle-orm' import type { DatabaseContext } from '../database/client.js' -import { accountIdentities, accountLoginClaimConflicts, accountLoginClaims, accounts, deploymentSettings, devicePairings, devices, refreshTokens, workspaces } from '../database/schema.js' +import { accountIdentities, accountLoginClaimConflicts, accountLoginClaims, accounts, deploymentSettings, devicePairings, devices, refreshTokens, syncDeviceCursors, syncEvents, workspaces } from '../database/schema.js' import { ApiError } from '../errors.js' import { normalizeLoginKey } from '../identity/service.js' import type { TokenService } from './tokens.js' @@ -175,15 +175,11 @@ export class AuthService { if (existingDevice !== undefined && existingDevice.accountId !== accountId) { throw new ApiError({ code: 'device_conflict', message: 'Device belongs to another account', statusCode: 409 }) } - if (existingDevice?.encryptionPublicKey !== null && existingDevice?.encryptionPublicKey !== undefined - && input.encryptionPublicKey !== undefined - && input.encryptionPublicKey !== existingDevice.encryptionPublicKey) { - throw new ApiError({ - code: 'device_key_conflict', - message: 'Device ID is already bound to another encryption key', - statusCode: 409, - }) - } + // Re-authentication is authoritative proof that this account may reclaim + // its installation identity. This also lets an installation recover when + // its local private key was lost without creating another device record. + // #issueSession revokes every older refresh token for this device before + // issuing the replacement session. if (existingDevice === undefined || existingDevice.revokedAt !== null) { await this.usage?.admitDevice(tx, accountId, this.deviceLimitResolver === undefined ? null : await this.deviceLimitResolver(accountId)) @@ -314,7 +310,11 @@ export class AuthService { ...(refreshRequestId === undefined ? {} : { rotationRequestId: refreshRequestId, rotationResponseCiphertext: this.tokens.sealRefreshRecovery(JSON.stringify(session), `${stored.id}:${refreshRequestId}`), - rotationResponseExpiresAt: new Date(now.getTime() + 5 * 60_000), + // The request id is high entropy and proves this is the same refresh + // attempt. Keep its encrypted recovery response until the original + // token expires so a crash before local persistence cannot revoke the + // installation on its next launch. + rotationResponseExpiresAt: stored.expiresAt, }), }).where(eq(refreshTokens.id, stored.id)) await tx.insert(refreshTokens).values({ @@ -544,7 +544,7 @@ export class AuthService { } async listDevices(accountId: string, currentDeviceId?: string) { - const rows = await this.database.db.select({ + const [rows, cursors] = await Promise.all([this.database.db.select({ id: devices.id, name: devices.name, platform: devices.platform, @@ -552,8 +552,41 @@ export class AuthService { lastSeenAt: devices.lastSeenAt, createdAt: devices.createdAt, revokedAt: devices.revokedAt, - }).from(devices).where(eq(devices.accountId, accountId)).orderBy(devices.createdAt) - return rows.map((device) => ({ ...device, current: currentDeviceId !== undefined && device.id === currentDeviceId })) + }).from(devices).where(eq(devices.accountId, accountId)).orderBy(devices.createdAt), + this.database.db.select({ + deviceId: syncDeviceCursors.deviceId, + acknowledgedSequence: syncDeviceCursors.acknowledgedSequence, + latestSequence: workspaces.latestSequence, + acknowledgedAt: syncDeviceCursors.updatedAt, + pendingObjectCount: sql`( + select count(distinct coalesce(${syncEvents.objectId}::text, ${syncEvents.eventId}::text)) + from ${syncEvents} + where ${syncEvents.workspaceId} = ${syncDeviceCursors.workspaceId} + and ${syncEvents.sequence} > ${syncDeviceCursors.acknowledgedSequence} + )`, + }).from(syncDeviceCursors).innerJoin(workspaces, and( + eq(workspaces.id, syncDeviceCursors.workspaceId), + eq(workspaces.accountId, accountId), + isNull(workspaces.deletedAt), + )),]) + return rows.map((device) => { + const deviceCursors = cursors.filter(cursor => cursor.deviceId === device.id) + const pendingEventCount = deviceCursors.reduce( + (total, cursor) => total + BigInt(cursor.pendingObjectCount), 0n, + ) + const isBehind = deviceCursors.some(cursor => cursor.latestSequence > cursor.acknowledgedSequence) + const acknowledgedAt = deviceCursors.reduce((latest, cursor) => ( + latest === null || cursor.acknowledgedAt > latest ? cursor.acknowledgedAt : latest + ), null) + return { + ...device, + current: currentDeviceId !== undefined && device.id === currentDeviceId, + syncStatus: deviceCursors.length === 0 ? 'never-acknowledged' as const + : isBehind ? 'behind' as const : 'caught-up' as const, + pendingEventCount: pendingEventCount.toString(), + acknowledgedAt, + } + }) } async #issueSession(accountId: string, deviceId: string, expectedCredentialEpoch?: string): Promise { diff --git a/apps/server/src/blobs/service.ts b/apps/server/src/blobs/service.ts index f4288eb..3b45f14 100644 --- a/apps/server/src/blobs/service.ts +++ b/apps/server/src/blobs/service.ts @@ -27,7 +27,7 @@ export class BlobService { input: { blobId: string, expectedSize: string, ciphertextHash: string, expectedSyncEpoch?: string }, ) { this.#assertExpectedSyncEpoch(input.expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.create') const expectedSize = parseSize(input.expectedSize) if (expectedSize === 0n) { throw new ApiError({ code: 'blob_empty', message: 'Blob must contain at least one byte', statusCode: 400 }) @@ -149,7 +149,7 @@ export class BlobService { partNumber: number, data: Buffer, ) { - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.update') if (data.byteLength === 0 || data.byteLength > this.partBytes) { throw new ApiError({ code: 'blob_part_invalid', message: 'Blob part size is invalid', statusCode: 400 }) } @@ -192,7 +192,7 @@ export class BlobService { async complete(accountId: string, workspaceId: string, uploadId: string, expectedSyncEpoch?: string) { this.#assertExpectedSyncEpoch(expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.update') const existing = await this.#getUpload(workspaceId, uploadId) this.#assertUploadEpoch(existing.syncEpoch) if (existing.completedAt !== null) { @@ -287,7 +287,7 @@ export class BlobService { } async get(accountId: string, workspaceId: string, blobId: string) { - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.read') const [blob] = await this.database.db.select().from(blobs).where(and( eq(blobs.workspaceId, workspaceId), eq(blobs.blobId, blobId), eq(blobs.state, 'ready'), )).limit(1) @@ -296,7 +296,7 @@ export class BlobService { } #assertExpectedSyncEpoch(expected: string | undefined): void { - if (expected !== undefined && this.syncEpoch !== undefined && expected !== this.syncEpoch) { + if (this.syncEpoch !== undefined && expected !== this.syncEpoch) { throw new ApiError({ code: 'sync_epoch_changed', message: 'Server restore epoch changed; re-bootstrap before writing', statusCode: 409 }) } } @@ -310,7 +310,7 @@ export class BlobService { } async uploadStatus(accountId: string, workspaceId: string, uploadId: string) { - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.read') const upload = await this.#getUpload(workspaceId, uploadId) return { uploadId: upload.id, @@ -339,7 +339,7 @@ export class BlobService { } async abort(accountId: string, workspaceId: string, uploadId: string): Promise { - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.delete') const upload = await this.#getActiveUpload(workspaceId, uploadId) await this.storage.abortUpload(upload.storageKey, upload.providerUploadId) await this.database.db.transaction(async (tx) => { diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index 868ad1d..496d7f2 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -420,41 +420,16 @@ export function loadConfig(environment: NodeJS.ProcessEnv = process.env): AppCon /** Applies the persisted installation profile before mode-specific services are assembled. */ export function applyPersistedDeploymentProfile( config: AppConfig, - deploymentMode: 'self-hosted' | 'hosted', - hostedRegistrationPolicy: 'disabled' | 'public' = 'disabled', + deploymentMode: 'self-hosted', ): void { - const profile = deploymentMode === 'hosted' - ? { - deploymentMode, - hostedReleaseStage: 'internal-test' as const, - hostedRegistrationPolicy, - billingProvider: 'mock' as const, - billingProviderEnvironment: 'test' as const, - billingMerchantEntity: 'internal-test-only', - hostedMailProvider: 'log' as const, - usageEnforcement: 'observe' as const, - mailDriver: 'disabled' as const, - capabilitiesEnable: hostedRegistrationPolicy === 'public' - ? ['mail.delivery', 'identity.email', 'identity.emailVerification', 'identity.passwordReset', 'registration.public'] - : [], - capabilitiesDisable: [], - } - : { - deploymentMode, - hostedRegistrationPolicy: 'disabled' as const, - billingProvider: 'none' as const, - hostedMailProvider: 'none' as const, - usageEnforcement: 'disabled' as const, - } - Object.assign(config as unknown as Record, profile) - - if (deploymentMode === 'hosted') { - const ledgerPath = resolve(config.deletionLedgerPath) - if (ledgerPath === resolve(config.backupPath) - || (config.blobStorageDriver === 'filesystem' && ledgerPath === resolve(config.blobStoragePath))) { - throw new Error('Invalid server configuration: hosted deletion ledger must be separate from backup and blob storage paths') - } + const profile = { + deploymentMode, + hostedRegistrationPolicy: 'disabled' as const, + billingProvider: 'none' as const, + hostedMailProvider: 'none' as const, + usageEnforcement: 'disabled' as const, } + Object.assign(config as unknown as Record, profile) } function parseList(value: string | undefined): string[] { diff --git a/apps/server/src/database/migration-compatibility.ts b/apps/server/src/database/migration-compatibility.ts index c371b20..c545400 100644 --- a/apps/server/src/database/migration-compatibility.ts +++ b/apps/server/src/database/migration-compatibility.ts @@ -40,6 +40,11 @@ export async function adoptSquashedMigrationBaseline(database: DatabaseContext): if (migrations.missing.length > 0 || migrations.entries.length === 0) return const baseline = migrations.entries[0]! await database.sql.begin(async (transaction) => { + const [migrationTable] = await transaction>` + select to_regclass('drizzle.__drizzle_migrations')::text as table_name` + // A brand-new database has no migration ledger yet. Drizzle creates both + // the ledger and schema while applying the first migration below. + if (migrationTable?.table_name === null) return await transaction`lock table drizzle.__drizzle_migrations in exclusive mode` const applied = await transaction>` select hash, created_at::text as created_at diff --git a/apps/server/src/database/schema.ts b/apps/server/src/database/schema.ts index a0fdab2..077a582 100644 --- a/apps/server/src/database/schema.ts +++ b/apps/server/src/database/schema.ts @@ -15,6 +15,19 @@ export const serverMetadata = pgTable('server_metadata', { createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }) +/** Durable, multi-process rate-limit counters. Raw client identifiers are + * HMACed before reaching this table. */ +export const rateLimitBuckets = pgTable('rate_limit_buckets', { + scope: text('scope').notNull(), + rateKey: text('rate_key').notNull(), + windowStart: timestamp('window_start', { withTimezone: true }).notNull(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + hits: integer('hits').notNull().default(0), +}, (table) => [ + primaryKey({ columns: [table.scope, table.rateKey, table.windowStart] }), + index('rate_limit_buckets_expiry_idx').on(table.expiresAt), +]) + export const deploymentMode = pgEnum('deployment_mode', ['hosted', 'self-hosted']) export const registrationPolicy = pgEnum('registration_policy', ['bootstrap', 'disabled', 'invitation', 'public']) export const selfHostedLifecycle = pgEnum('self_hosted_lifecycle', ['uninitialized', 'ready']) @@ -119,9 +132,13 @@ export const maintenanceState = pgTable('maintenance_state', { }) export const objectKind = pgEnum('object_kind', [ - 'note', 'folder', 'asset', 'canvas', 'record', 'tag', 'mark', 'conversation', + 'note', 'folder', 'asset', 'canvas', 'tag', 'mark', 'conversation', 'message', 'memory', 'setting', 'yjs-checkpoint', 'yjs-update', ]) +export const workspaceType = pgEnum('workspace_type', ['account-data', 'library']) +export const workspaceMemberRole = pgEnum('workspace_member_role', ['viewer', 'editor', 'manager']) +export const workspaceInvitationKind = pgEnum('workspace_invitation_kind', ['account', 'link']) +export const workspaceInvitationStatus = pgEnum('workspace_invitation_status', ['pending', 'accepted', 'revoked', 'expired']) export const changeType = pgEnum('change_type', ['upsert', 'delete']) export const blobState = pgEnum('blob_state', ['uploading', 'ready', 'deleting']) export const keyEnvelopeType = pgEnum('key_envelope_type', ['passphrase', 'recovery', 'device', 'managed']) @@ -578,6 +595,7 @@ export const devicePairings = pgTable('device_pairings', { export const workspaces = pgTable('workspaces', { id: uuid('id').primaryKey().defaultRandom(), accountId: uuid('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }), + type: workspaceType('workspace_type').notNull().default('library'), nameCiphertext: text('name_ciphertext').notNull(), creationIdempotencyKey: text('creation_idempotency_key'), creationRequestHash: text('creation_request_hash'), @@ -593,6 +611,50 @@ export const workspaces = pgTable('workspaces', { uniqueIndex('workspaces_account_creation_idempotency_unique') .on(table.accountId, table.creationIdempotencyKey) .where(sql`${table.creationIdempotencyKey} is not null`), + uniqueIndex('workspaces_account_data_unique') + .on(table.accountId) + .where(sql`${table.type} = 'account-data' and ${table.deletedAt} is null`), +]) + +/** Owners are represented by workspaces.account_id and always have every + * capability. Only accepted, non-owner members are stored here. */ +export const workspaceMembers = pgTable('workspace_members', { + workspaceId: uuid('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + accountId: uuid('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }), + role: workspaceMemberRole('role').notNull(), + capabilities: text('capabilities').array().notNull(), + invitedByAccountId: uuid('invited_by_account_id').notNull().references(() => accounts.id, { onDelete: 'restrict' }), + joinedAt: timestamp('joined_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}, (table) => [ + primaryKey({ columns: [table.workspaceId, table.accountId] }), + index('workspace_members_account_idx').on(table.accountId, table.updatedAt), +]) + +export const workspaceInvitations = pgTable('workspace_invitations', { + id: uuid('id').primaryKey().defaultRandom(), + workspaceId: uuid('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + kind: workspaceInvitationKind('kind').notNull(), + inviteeAccountId: uuid('invitee_account_id').references(() => accounts.id, { onDelete: 'cascade' }), + tokenHash: text('token_hash'), + tokenHint: text('token_hint'), + role: workspaceMemberRole('role').notNull(), + capabilities: text('capabilities').array().notNull(), + status: workspaceInvitationStatus('status').notNull().default('pending'), + invitedByAccountId: uuid('invited_by_account_id').notNull().references(() => accounts.id, { onDelete: 'restrict' }), + acceptedByAccountId: uuid('accepted_by_account_id').references(() => accounts.id, { onDelete: 'set null' }), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + acceptedAt: timestamp('accepted_at', { withTimezone: true }), + revokedAt: timestamp('revoked_at', { withTimezone: true }), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}, (table) => [ + index('workspace_invitations_workspace_idx').on(table.workspaceId, table.status, table.createdAt), + index('workspace_invitations_invitee_idx').on(table.inviteeAccountId, table.status, table.createdAt), + uniqueIndex('workspace_invitations_token_unique').on(table.tokenHash).where(sql`${table.tokenHash} is not null`), + uniqueIndex('workspace_invitations_pending_account_unique') + .on(table.workspaceId, table.inviteeAccountId) + .where(sql`${table.kind} = 'account' and ${table.status} = 'pending'`), ]) export const workspaceKeys = pgTable('workspace_keys', { @@ -821,6 +883,9 @@ export const syncCommands = pgTable('sync_commands', { commandId: uuid('command_id').notNull(), sourceDeviceId: uuid('source_device_id').notNull().references(() => devices.id), requestHash: text('request_hash').notNull(), + /** Epoch in which the command result was committed. Null denotes a legacy + * pre-fencing result and must not deduplicate a post-restore command. */ + syncEpoch: uuid('sync_epoch'), result: jsonb('result').$type>().notNull(), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ @@ -851,6 +916,19 @@ export const syncEvents = pgTable('sync_events', { index('sync_events_created_idx').on(table.createdAt), ]) +/** Highest durable event sequence explicitly acknowledged by each device. + * Delivery alone is not an acknowledgement: clients advance this only after + * the corresponding inbox rows have been committed locally. */ +export const syncDeviceCursors = pgTable('sync_device_cursors', { + workspaceId: uuid('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), + deviceId: uuid('device_id').notNull().references(() => devices.id, { onDelete: 'cascade' }), + acknowledgedSequence: bigint('acknowledged_sequence', { mode: 'bigint' }).notNull().default(sql`0`), + updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(), +}, (table) => [ + primaryKey({ columns: [table.workspaceId, table.deviceId] }), + index('sync_device_cursors_updated_idx').on(table.workspaceId, table.updatedAt), +]) + export const syncDocuments = pgTable('sync_documents', { workspaceId: uuid('workspace_id').notNull().references(() => workspaces.id, { onDelete: 'cascade' }), documentId: text('document_id').notNull(), diff --git a/apps/server/src/deployment/capabilities.ts b/apps/server/src/deployment/capabilities.ts index 63ce8fc..5f8795c 100644 --- a/apps/server/src/deployment/capabilities.ts +++ b/apps/server/src/deployment/capabilities.ts @@ -3,19 +3,17 @@ import type { DeploymentService } from './service.js' export const capabilityIds = [ 'identity.email', 'identity.emailVerification', 'identity.passwordReset', - 'registration.invitation', 'registration.public', 'risk.advanced', 'usage.enforcement', - 'billing.subscription', 'compliance.requests', 'support.cases', 'operations.webBootstrap', + 'registration.invitation', 'registration.public', 'operations.webBootstrap', 'mail.delivery', 'operations.smtpAdmin', 'operations.unifiedBackup', 'operations.upgradeAssistant', 'operations.preserveRestore', ] as const export type CapabilityId = typeof capabilityIds[number] -type DeploymentMode = 'hosted' | 'self-hosted' export interface CapabilityExplanation { id: CapabilityId available: boolean - deploymentMode: DeploymentMode + deploymentMode: 'self-hosted' requestedBy: 'enabled_override' | 'disabled_override' | 'default' | 'lifecycle' reasons: string[] dependencies: Array<{ id: CapabilityId, available: boolean }> @@ -23,33 +21,22 @@ export interface CapabilityExplanation { interface CapabilityDefinition { readonly id: CapabilityId - readonly supportedModes: readonly DeploymentMode[] - readonly defaultByMode: Readonly> readonly requires: readonly CapabilityId[] readonly overrideable: boolean - readonly availabilitySource: 'configuration' | 'lifecycle' | 'registration-policy' } -const both: readonly DeploymentMode[] = ['hosted', 'self-hosted'] -const disabled = { hosted: false, 'self-hosted': false } as const - const definitions: readonly CapabilityDefinition[] = [ - { id: 'identity.email', supportedModes: both, defaultByMode: disabled, requires: ['mail.delivery'], overrideable: true, availabilitySource: 'configuration' }, - { id: 'identity.emailVerification', supportedModes: both, defaultByMode: disabled, requires: ['identity.email'], overrideable: true, availabilitySource: 'configuration' }, - { id: 'identity.passwordReset', supportedModes: both, defaultByMode: disabled, requires: ['identity.email'], overrideable: true, availabilitySource: 'configuration' }, - { id: 'registration.invitation', supportedModes: both, defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'registration-policy' }, - { id: 'registration.public', supportedModes: both, defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'registration-policy' }, - { id: 'risk.advanced', supportedModes: ['hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'usage.enforcement', supportedModes: ['hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'billing.subscription', supportedModes: ['hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'compliance.requests', supportedModes: ['hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'support.cases', supportedModes: ['hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'operations.webBootstrap', supportedModes: ['self-hosted'], defaultByMode: disabled, requires: [], overrideable: false, availabilitySource: 'lifecycle' }, - { id: 'mail.delivery', supportedModes: both, defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'operations.smtpAdmin', supportedModes: ['self-hosted'], defaultByMode: disabled, requires: ['mail.delivery'], overrideable: true, availabilitySource: 'configuration' }, - { id: 'operations.unifiedBackup', supportedModes: ['self-hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'operations.upgradeAssistant', supportedModes: ['self-hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, - { id: 'operations.preserveRestore', supportedModes: ['self-hosted'], defaultByMode: disabled, requires: [], overrideable: true, availabilitySource: 'configuration' }, + { id: 'identity.email', requires: ['mail.delivery'], overrideable: true }, + { id: 'identity.emailVerification', requires: ['identity.email'], overrideable: true }, + { id: 'identity.passwordReset', requires: ['identity.email'], overrideable: true }, + { id: 'registration.invitation', requires: [], overrideable: true }, + { id: 'registration.public', requires: [], overrideable: true }, + { id: 'operations.webBootstrap', requires: [], overrideable: false }, + { id: 'mail.delivery', requires: [], overrideable: true }, + { id: 'operations.smtpAdmin', requires: ['mail.delivery'], overrideable: true }, + { id: 'operations.unifiedBackup', requires: [], overrideable: true }, + { id: 'operations.upgradeAssistant', requires: [], overrideable: true }, + { id: 'operations.preserveRestore', requires: [], overrideable: true }, ] const definitionById = new Map(definitions.map((definition) => [definition.id, definition])) @@ -60,35 +47,23 @@ export class CapabilityRegistry { } resolvePublic(): Record { - const mode = this.deployment.getState().deploymentMode const result = {} as Record for (const definition of definitions) { - const supported = definition.supportedModes.includes(mode) - const requested = this.requested(definition, mode) - const releaseStageAllows = this.releaseStageAllows(definition, mode) + const requested = this.requested(definition) const lifecycleAllows = definition.id !== 'operations.webBootstrap' || this.deployment.canBootstrapAdministrator() - // Backup and preserve restore are security contracts, not preview - // toggles. Until the unified writer, artifact-driven apply path and - // required client epoch fence all exist, no override may advertise them. const restoreSafetyAllows = definition.id !== 'operations.preserveRestore' && definition.id !== 'operations.unifiedBackup' - // Hosted's log sink never delivers externally. Self-hosted may expose - // the transport capability only when the reviewed SMTP adapter is - // explicitly configured; the worker is assembled from the same config. const mailDeliverySafetyAllows = definition.id !== 'mail.delivery' - || (mode === 'self-hosted' && this.config.mailDriver === 'smtp') - || (mode === 'hosted' && this.config.hostedReleaseStage === 'internal-test' && this.config.hostedMailProvider === 'log') - // The admin surface is self-hosted-only and requires the same reviewed - // SMTP adapter as delivery. Its route assembly independently checks the - // provider, while the explicit capability override controls discovery. + || this.config.mailDriver === 'smtp' const smtpAdminSafetyAllows = definition.id !== 'operations.smtpAdmin' - || (mode === 'self-hosted' && this.config.mailDriver === 'smtp') + || this.config.mailDriver === 'smtp' const policyAllows = definition.id === 'registration.public' ? this.deployment.getState().registrationPolicy === 'public' : definition.id === 'registration.invitation' ? this.deployment.getState().registrationPolicy === 'invitation' : true - result[definition.id] = supported && requested && releaseStageAllows && lifecycleAllows && restoreSafetyAllows && mailDeliverySafetyAllows && smtpAdminSafetyAllows && policyAllows + result[definition.id] = requested && lifecycleAllows && restoreSafetyAllows + && mailDeliverySafetyAllows && smtpAdminSafetyAllows && policyAllows } for (const definition of definitions) { if (!definition.requires.every((dependency) => result[dependency])) result[definition.id] = false @@ -96,66 +71,42 @@ export class CapabilityRegistry { return result } - /** Local-operator diagnostic view. It intentionally reports only policy - * decisions, never provider settings, credentials, or secret presence. */ explain(id: string): CapabilityExplanation { const definition = definitionById.get(id as CapabilityId) if (definition === undefined) throw new Error(`Unknown capability: ${id}`) - const mode = this.deployment.getState().deploymentMode const available = this.resolvePublic() const reasons: string[] = [] const requestedBy = this.requestSource(definition) - if (!definition.supportedModes.includes(mode)) reasons.push('unsupported_deployment_mode') - if (!this.requested(definition, mode)) reasons.push('not_requested') - if (!this.releaseStageAllows(definition, mode)) reasons.push('release_stage_gated') + if (!this.requested(definition)) reasons.push('not_requested') if (definition.id === 'operations.webBootstrap' && !this.deployment.canBootstrapAdministrator()) reasons.push('lifecycle_gated') if (definition.id === 'operations.unifiedBackup' || definition.id === 'operations.preserveRestore') reasons.push('restore_safety_gated') - if (definition.id === 'mail.delivery' && !(mode === 'self-hosted' && this.config.mailDriver === 'smtp') - && !(mode === 'hosted' && this.config.hostedReleaseStage === 'internal-test' && this.config.hostedMailProvider === 'log')) reasons.push('mail_delivery_adapter_unavailable') - if (definition.id === 'operations.smtpAdmin' && !(mode === 'self-hosted' && this.config.mailDriver === 'smtp')) reasons.push('smtp_admin_surface_unavailable') + if (definition.id === 'mail.delivery' && this.config.mailDriver !== 'smtp') reasons.push('mail_delivery_adapter_unavailable') + if (definition.id === 'operations.smtpAdmin' && this.config.mailDriver !== 'smtp') reasons.push('smtp_admin_surface_unavailable') if (definition.id === 'registration.public' && this.deployment.getState().registrationPolicy !== 'public') reasons.push('registration_policy_gated') if (definition.id === 'registration.invitation' && this.deployment.getState().registrationPolicy !== 'invitation') reasons.push('registration_policy_gated') const dependencies = definition.requires.map((dependency) => ({ id: dependency, available: available[dependency] })) if (dependencies.some((dependency) => !dependency.available)) reasons.push('dependency_unavailable') if (available[definition.id] && reasons.length === 0) reasons.push('available') - return { id: definition.id, available: available[definition.id], deploymentMode: mode, requestedBy, reasons, dependencies } + return { id: definition.id, available: available[definition.id], deploymentMode: 'self-hosted', requestedBy, reasons, dependencies } } - private requested(definition: CapabilityDefinition, mode: DeploymentMode): boolean { + private requested(definition: CapabilityDefinition): boolean { if (!definition.overrideable) return definition.id === 'operations.webBootstrap' if (this.config.capabilitiesEnable.includes(definition.id)) return true if (this.config.capabilitiesDisable.includes(definition.id)) return false - if (mode === 'self-hosted' && this.config.mailDriver === 'smtp' - && (definition.id === 'mail.delivery' || definition.id === 'operations.smtpAdmin')) return true - return definition.defaultByMode[mode] + return this.config.mailDriver === 'smtp' + && (definition.id === 'mail.delivery' || definition.id === 'operations.smtpAdmin') } private requestSource(definition: CapabilityDefinition): CapabilityExplanation['requestedBy'] { if (!definition.overrideable) return 'lifecycle' if (this.config.capabilitiesEnable.includes(definition.id)) return 'enabled_override' if (this.config.capabilitiesDisable.includes(definition.id)) return 'disabled_override' - if (this.config.deploymentMode === 'self-hosted' && this.config.mailDriver === 'smtp' + if (this.config.mailDriver === 'smtp' && (definition.id === 'mail.delivery' || definition.id === 'operations.smtpAdmin')) return 'enabled_override' return 'default' } - private releaseStageAllows(definition: CapabilityDefinition, mode: DeploymentMode): boolean { - if (mode !== 'hosted') return true - if (this.config.hostedReleaseStage === 'live') { - // The current support implementation is an internal-test customer case - // core only. It has no staff OIDC boundary or permission-enforced - // console, so a live config must not advertise it merely because an - // operator supplied an override. - return definition.id !== 'support.cases' - } - // Test configuration has no external delivery, payment, or actionable - // compliance surface. Its redacted LogMailProvider may be explicitly - // enabled for internal identity exercises only. Support is deliberately - // excluded from this list because its first customer case slice is - // internal-test only. - return !['billing.subscription', 'compliance.requests'].includes(definition.id) - } - private validateOverrides(): void { const enabled = new Set(this.config.capabilitiesEnable) const disabledIds = new Set(this.config.capabilitiesDisable) @@ -164,9 +115,6 @@ export class CapabilityRegistry { if (definition === undefined) throw new Error(`Unknown capability override: ${id}`) if (!definition.overrideable) throw new Error(`Capability cannot be overridden: ${id}`) if (enabled.has(id) && disabledIds.has(id)) throw new Error(`Capability is both enabled and disabled: ${id}`) - if (!definition.supportedModes.includes(this.config.deploymentMode)) { - throw new Error(`Capability is not supported for ${this.config.deploymentMode}: ${id}`) - } } } } diff --git a/apps/server/src/deployment/service.ts b/apps/server/src/deployment/service.ts index 687c466..e48cde3 100644 --- a/apps/server/src/deployment/service.ts +++ b/apps/server/src/deployment/service.ts @@ -32,7 +32,7 @@ export interface RuntimeConfiguration { } export interface DeploymentState { - deploymentMode: 'hosted' | 'self-hosted' + deploymentMode: 'self-hosted' registrationPolicy: RegistrationPolicy selfHostedLifecycle: 'uninitialized' | 'ready' | null adminRepairRequired: boolean @@ -40,7 +40,7 @@ export interface DeploymentState { runtimeConfiguration: RuntimeConfiguration } -/** Loads the immutable deployment choice made by the Web installation guide. */ +/** Loads the independent-instance configuration created by the Web installer. */ export class DeploymentService { private state: DeploymentState | undefined private configurationListening = false @@ -162,8 +162,11 @@ export class DeploymentService { } private toState(row: typeof deploymentSettings.$inferSelect): DeploymentState { + if (row.deploymentMode !== 'self-hosted') { + throw new Error('The legacy operations deployment mode is no longer supported') + } return { - deploymentMode: row.deploymentMode, + deploymentMode: 'self-hosted', registrationPolicy: row.registrationPolicy, selfHostedLifecycle: row.selfHostedLifecycle, adminRepairRequired: row.adminRepairRequired, diff --git a/apps/server/src/durable-sync/service.ts b/apps/server/src/durable-sync/service.ts index 327954f..902c290 100644 --- a/apps/server/src/durable-sync/service.ts +++ b/apps/server/src/durable-sync/service.ts @@ -4,14 +4,91 @@ import type { DatabaseContext } from '../database/client.js' import { blobs, objectVersions, objects, syncBootstrapObjects, syncBootstrapSessions, syncCheckpoints, syncCommands, syncConflicts, syncDocuments, syncEvents, - syncResourceBindings, syncUpdates, workspaceKeys, workspaces, + syncDeviceCursors, syncResourceBindings, syncUpdates, workspaceKeys, workspaces, } from '../database/schema.js' import { ApiError } from '../errors.js' import { assertAccountWriteAllowedInTransaction } from '../compliance/deletion-fence.js' -import type { ChangeNotifier } from '../sync/types.js' +import type { ChangeNotifier, SyncObjectKind } from '../sync/types.js' import type { WorkspaceService } from '../workspaces/service.js' import type { CiphertextEnvelope, SyncCommand, SyncCommandResult } from './types.js' import type { UsageService } from '../usage/service.js' +import type { WorkspaceCapability } from '../workspaces/capabilities.js' + +const cacheableRejectionCodes = new Set([ + 'request_invalid', 'object_too_large', 'ciphertext_hash_mismatch', + 'object_kind_mismatch', 'update_id_reused', 'conflict_id_reused', + 'document_identity_mismatch', 'asset_cannot_bind_resources', + 'name_blind_index_key_mismatch', 'workspace_object_kind_invalid', +]) +// Library events materialize to the filesystem and require atomic writes and +// fsync. A long stream of intermediate snapshots is therefore much more +// expensive on mobile than structured account-data events. Switch libraries +// to a fixed bootstrap snapshot earlier so reconnecting devices catch up to +// the current tree instead of replaying thousands of obsolete file versions. +const maxLibraryIncrementalLag = 250n +const maxAccountDataIncrementalLag = 10_000n + +const accountDataObjectKinds = new Set([ + 'asset', 'canvas', 'tag', 'mark', 'conversation', 'message', 'memory', 'setting', + 'yjs-checkpoint', 'yjs-update', +]) +const libraryObjectKinds = new Set([ + 'asset', 'folder', 'note', 'yjs-checkpoint', 'yjs-update', +]) + +function assertCommandWorkspaceType( + workspaceType: 'account-data' | 'library', + command: SyncCommand, +): void { + const kind = command.type === 'upsert-object' + || command.type === 'initialize-document' + || command.type === 'append-update' + || command.type === 'commit-checkpoint' + || command.type === 'create-conflict' + ? command.kind + : command.type === 'resolve-conflict' + ? command.objectResolution?.kind ?? command.resolution?.kind + : undefined + if (kind === undefined) return + const allowed = workspaceType === 'account-data' ? accountDataObjectKinds : libraryObjectKinds + if (allowed.has(kind)) return + throw new ApiError({ + code: 'workspace_object_kind_invalid', + message: `Object kind ${kind} is not valid in a ${workspaceType} workspace`, + statusCode: 409, + details: { workspaceType, kind }, + }) +} + +function assertCommandCapability(capabilities: readonly WorkspaceCapability[], command: SyncCommand): void { + let required: WorkspaceCapability + switch (command.type) { + case 'upsert-object': + required = command.baseRevision === null ? 'content.create' : 'content.update' + break + case 'delete-object': + case 'delete-subtree': + required = 'content.delete' + break + case 'resolve-conflict': + required = 'history.restore' + break + case 'initialize-document': + case 'append-update': + case 'commit-checkpoint': + case 'create-conflict': + required = 'content.update' + break + } + if (!capabilities.includes(required)) { + throw new ApiError({ + code: 'workspace_capability_denied', + message: `Workspace capability ${required} is required`, + statusCode: 403, + details: { capability: required }, + }) + } +} export class DurableSyncService { constructor( @@ -24,23 +101,112 @@ export class DurableSyncService { private readonly syncEpoch?: string, ) {} + async session( + accountId: string, + deviceId: string, + workspaceId: string, + cursorValue: string, + protocolVersion: number, + expectedSyncEpoch?: string, + ) { + if (this.syncEpoch === undefined) throw new Error('Protocol v1 requires a sync epoch') + if (expectedSyncEpoch !== undefined) this.#assertSyncEpoch(expectedSyncEpoch) + const access = await this.workspaces.assertCapability(accountId, workspaceId, 'content.read') + const cursor = counter(cursorValue, 'cursor') + const [[workspace], [oldestEvent], [deviceCursor], keyVersions] = await Promise.all([ + this.database.db.select({ latestSequence: workspaces.latestSequence }) + .from(workspaces).where(eq(workspaces.id, workspaceId)).limit(1), + this.database.db.select({ sequence: syncEvents.sequence }).from(syncEvents) + .where(eq(syncEvents.workspaceId, workspaceId)).orderBy(asc(syncEvents.sequence)).limit(1), + this.database.db.select({ acknowledgedSequence: syncDeviceCursors.acknowledgedSequence }) + .from(syncDeviceCursors).where(and( + eq(syncDeviceCursors.workspaceId, workspaceId), + eq(syncDeviceCursors.deviceId, deviceId), + )).limit(1), + this.database.db.select({ keyVersion: workspaceKeys.keyVersion, createdAt: workspaceKeys.createdAt }) + .from(workspaceKeys).where(eq(workspaceKeys.workspaceId, workspaceId)) + .orderBy(workspaceKeys.keyVersion), + ]) + if (workspace === undefined) { + throw new ApiError({ code: 'workspace_not_found', message: 'Workspace not found', statusCode: 404 }) + } + const cursorState = cursor > workspace.latestSequence + ? 'ahead' as const + : oldestEvent !== undefined && cursor + 1n < oldestEvent.sequence + ? 'expired' as const + : 'valid' as const + const uninitializedDevice = deviceCursor === undefined && workspace.latestSequence > 0n + const maxIncrementalLag = access.type === 'library' + ? maxLibraryIncrementalLag + : maxAccountDataIncrementalLag + const lagTooLarge = workspace.latestSequence > cursor + && workspace.latestSequence - cursor > maxIncrementalLag + const bootstrapReason = cursorState === 'ahead' + ? 'cursor_ahead' as const + : cursorState === 'expired' + ? 'cursor_expired' as const + : uninitializedDevice + ? 'device_uninitialized' as const + : lagTooLarge ? 'lag_too_large' as const : null + return { + protocol: { + requestedVersion: protocolVersion, + selectedVersion: 1, + compatible: protocolVersion === 1, + }, + workspace: { + id: workspaceId, + type: access.type, + role: access.role, + owner: access.owner, + capabilities: access.capabilities, + }, + cursor: { + supplied: cursorValue, + state: cursorState, + acknowledged: deviceCursor?.acknowledgedSequence.toString() ?? '0', + oldestAvailableSequence: oldestEvent?.sequence.toString() ?? null, + }, + latestSequence: workspace.latestSequence.toString(), + bootstrap: { + required: bootstrapReason !== null, + reason: bootstrapReason, + }, + limits: { + maxCommandsPerBatch: 100, + maxEventsPerPage: 1_000, + maxBootstrapObjectsPerPage: 1_000, + maxDocumentUpdatesPerPage: 1_000, + maxObjectBytes: typeof this.maxObjectBytes === 'function' ? this.maxObjectBytes() : this.maxObjectBytes, + }, + keyVersions: keyVersions.map(key => ({ keyVersion: key.keyVersion, createdAt: key.createdAt })), + syncEpoch: this.syncEpoch, + } + } + async commands(accountId: string, deviceId: string, workspaceId: string, commands: SyncCommand[], expectedSyncEpoch?: string) { this.#assertSyncEpoch(expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + const access = await this.workspaces.access(accountId, workspaceId) const results: SyncCommandResult[] = [] for (const command of commands) { try { - results.push(await this.#command(accountId, deviceId, workspaceId, command)) + assertCommandCapability(access.capabilities, command) + assertCommandWorkspaceType(access.type, command) + results.push(await this.#command(access.ownerAccountId, deviceId, workspaceId, command)) } catch (error) { if (!(error instanceof ApiError)) throw error - results.push({ + const rejected: SyncCommandResult = { commandId: command.commandId, status: 'rejected', duplicate: false, code: error.code, retryable: error.retryable, ...(error.details === undefined ? {} : { details: error.details }), - }) + } + if (!error.retryable && cacheableRejectionCodes.has(error.code)) { + await this.#recordRejectedCommand(access.ownerAccountId, deviceId, workspaceId, command, rejected).catch(() => undefined) + } + results.push(rejected) } } - return { results } + return { results, ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }) } } async recordCommandIngress(accountId: string, workspaceId: string, bytes: bigint, requestId: string): Promise { @@ -49,20 +215,59 @@ export class DurableSyncService { async events(accountId: string, workspaceId: string, after: string, limit: number, expectedSyncEpoch?: string) { this.#assertSyncEpoch(expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.read') const cursor = counter(after, 'after') + const [workspace] = await this.database.db.select({ latestSequence: workspaces.latestSequence }) + .from(workspaces).where(eq(workspaces.id, workspaceId)).limit(1) + const latestSequence = workspace?.latestSequence ?? 0n + if (cursor > latestSequence) throw new ApiError({ + code: 'cursor_invalid', message: 'Sync cursor is ahead of the workspace', statusCode: 409, + details: { latestSequence: latestSequence.toString() }, + }) const rows = await this.database.db.select().from(syncEvents).where(and( eq(syncEvents.workspaceId, workspaceId), gt(syncEvents.sequence, cursor), )).orderBy(asc(syncEvents.sequence)).limit(limit + 1) + if (cursor < latestSequence && (rows[0] === undefined || rows[0].sequence !== cursor + 1n)) { + throw new ApiError({ + code: 'cursor_expired', message: 'Sync cursor is older than the retained event window; bootstrap is required', + statusCode: 409, retryable: false, + details: { oldestAvailableSequence: rows[0]?.sequence.toString() ?? null, latestSequence: latestSequence.toString() }, + }) + } const hasMore = rows.length > limit const page = rows.slice(0, limit).map(serializeEvent) - const [workspace] = await this.database.db.select({ latestSequence: workspaces.latestSequence }) - .from(workspaces).where(eq(workspaces.id, workspaceId)).limit(1) return { events: page, nextCursor: page.at(-1)?.sequence ?? after, - latestSequence: (workspace?.latestSequence ?? cursor).toString(), + latestSequence: latestSequence.toString(), hasMore, + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), + } + } + + async acknowledge(accountId: string, deviceId: string, workspaceId: string, throughValue: string, expectedSyncEpoch?: string) { + this.#assertSyncEpoch(expectedSyncEpoch) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.read') + const through = counter(throughValue, 'through') + const [workspace] = await this.database.db.select({ latestSequence: workspaces.latestSequence }) + .from(workspaces).where(eq(workspaces.id, workspaceId)).limit(1) + if (workspace === undefined) throw new ApiError({ code: 'workspace_not_found', message: 'Workspace not found', statusCode: 404 }) + if (through > workspace.latestSequence) throw new ApiError({ + code: 'cursor_invalid', message: 'Acknowledgement is ahead of the workspace', statusCode: 409, + details: { latestSequence: workspace.latestSequence.toString() }, + }) + const [cursor] = await this.database.db.insert(syncDeviceCursors).values({ + workspaceId, deviceId, acknowledgedSequence: through, + }).onConflictDoUpdate({ + target: [syncDeviceCursors.workspaceId, syncDeviceCursors.deviceId], + set: { + acknowledgedSequence: sql`greatest(${syncDeviceCursors.acknowledgedSequence}, ${through})`, + updatedAt: new Date(), + }, + }).returning({ acknowledgedSequence: syncDeviceCursors.acknowledgedSequence }) + return { + acknowledgedSequence: (cursor?.acknowledgedSequence ?? through).toString(), + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), } } @@ -74,7 +279,7 @@ export class DurableSyncService { expectedSyncEpoch?: string, ) { this.#assertSyncEpoch(expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'history.view') const revision = counter(revisionValue, 'revision') const [version] = await this.database.db.select().from(objectVersions).where(and( eq(objectVersions.workspaceId, workspaceId), @@ -118,7 +323,10 @@ export class DurableSyncService { currentRevision: currentRevisions.get(row.objectId) ?? null, }) const resources = resourceRows.map(row => withCurrentRevision(row.version)) - return { object: withCurrentRevision(version), resources } + return { + object: withCurrentRevision(version), resources, + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), + } } async objectVersions( @@ -130,7 +338,7 @@ export class DurableSyncService { expectedSyncEpoch?: string, ) { this.#assertSyncEpoch(expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'history.view') const before = beforeValue === null ? null : counter(beforeValue, 'before') const rows = await this.database.db.select().from(objectVersions).where(and( eq(objectVersions.workspaceId, workspaceId), @@ -143,6 +351,7 @@ export class DurableSyncService { versions: page.map(serializeObjectVersion), nextBefore: hasMore ? page.at(-1)?.revision.toString() ?? null : null, hasMore, + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), } } @@ -155,15 +364,17 @@ export class DurableSyncService { expectedSyncEpoch?: string, ) { this.#assertSyncEpoch(expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.read') const session = await this.database.db.transaction(async (tx) => { await assertAccountWriteAllowedInTransaction(tx, accountId) await tx.delete(syncBootstrapSessions).where(sql`${syncBootstrapSessions.expiresAt} <= now()`) if (bootstrapId !== null) { - const [existing] = await tx.select().from(syncBootstrapSessions).where(and( + const [existing] = await tx.update(syncBootstrapSessions).set({ + expiresAt: new Date(Date.now() + 30 * 60_000), + }).where(and( eq(syncBootstrapSessions.id, bootstrapId), eq(syncBootstrapSessions.workspaceId, workspaceId), gt(syncBootstrapSessions.expiresAt, new Date()), - )).limit(1) + )).returning() if (existing === undefined) { throw new ApiError({ code: 'bootstrap_expired', message: 'Bootstrap snapshot expired; restart bootstrap', statusCode: 409, retryable: true }) } @@ -172,6 +383,16 @@ export class DurableSyncService { const [lockedWorkspace] = await tx.select({ latestSequence: workspaces.latestSequence }).from(workspaces) .where(eq(workspaces.id, workspaceId)).limit(1).for('update') if (lockedWorkspace === undefined) throw new ApiError({ code: 'workspace_not_found', message: 'Workspace not found', statusCode: 404 }) + const activeSessions = await tx.select({ id: syncBootstrapSessions.id }).from(syncBootstrapSessions).where(and( + eq(syncBootstrapSessions.workspaceId, workspaceId), gt(syncBootstrapSessions.expiresAt, new Date()), + )).limit(8) + if (activeSessions.length >= 8) throw new ApiError({ + code: 'bootstrap_capacity_exceeded', + message: 'Too many bootstrap snapshots are active for this workspace', + statusCode: 429, + retryable: true, + details: { retryAfterMilliseconds: 30_000 }, + }) const [created] = await tx.insert(syncBootstrapSessions).values({ workspaceId, snapshotSequence: lockedWorkspace.latestSequence, expiresAt: new Date(Date.now() + 30 * 60_000), @@ -240,28 +461,58 @@ export class DurableSyncService { conflicts: unresolved.map(serializeConflict), nextObjectId: hasMore ? page.at(-1)?.manifest.objectId ?? null : null, hasMore, + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), } } async documentUpdates(accountId: string, workspaceId: string, documentId: string, after: string, limit: number, expectedSyncEpoch?: string) { this.#assertSyncEpoch(expectedSyncEpoch) - await this.workspaces.assertOwned(accountId, workspaceId) + await this.workspaces.assertCapability(accountId, workspaceId, 'content.read') const cursor = counter(after, 'after') - const rows = await this.database.db.select().from(syncUpdates).where(and( - eq(syncUpdates.workspaceId, workspaceId), eq(syncUpdates.documentId, documentId), - gt(syncUpdates.documentSequence, cursor), - )).orderBy(asc(syncUpdates.documentSequence)).limit(limit + 1) + const [rows, documents] = await Promise.all([ + this.database.db.select().from(syncUpdates).where(and( + eq(syncUpdates.workspaceId, workspaceId), eq(syncUpdates.documentId, documentId), + gt(syncUpdates.documentSequence, cursor), + )).orderBy(asc(syncUpdates.documentSequence)).limit(limit + 1), + this.database.db.select({ + objectId: syncDocuments.objectId, + checkpointDocumentSequence: syncDocuments.checkpointDocumentSequence, + checkpointId: syncDocuments.checkpointId, + checkpointKeyVersion: syncDocuments.checkpointKeyVersion, + checkpointCiphertext: syncDocuments.checkpointCiphertext, + checkpointCiphertextHash: syncDocuments.checkpointCiphertextHash, + }).from(syncDocuments).where(and( + eq(syncDocuments.workspaceId, workspaceId), + eq(syncDocuments.documentId, documentId), + )).limit(1), + ]) const hasMore = rows.length > limit const page = rows.slice(0, limit) + const document = documents[0] + const checkpoint = document?.checkpointId + && document.checkpointKeyVersion !== null + && document.checkpointCiphertext !== null + && document.checkpointCiphertextHash !== null + ? { + objectId: document.objectId, + documentSequence: document.checkpointDocumentSequence.toString(), + checkpointId: document.checkpointId, + keyVersion: document.checkpointKeyVersion, + ciphertext: document.checkpointCiphertext, + ciphertextHash: document.checkpointCiphertextHash, + } + : null return { updates: page.map(row => ({ ...row, documentSequence: row.documentSequence.toString(), eventSequence: row.eventSequence.toString() })), + checkpoint, nextDocumentSequence: page.at(-1)?.documentSequence.toString() ?? after, hasMore, + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), } } #assertSyncEpoch(expectedSyncEpoch: string | undefined): void { - if (expectedSyncEpoch !== undefined && this.syncEpoch !== undefined && expectedSyncEpoch !== this.syncEpoch) { + if (this.syncEpoch !== undefined && expectedSyncEpoch !== this.syncEpoch) { throw new ApiError({ code: 'sync_epoch_changed', message: 'Server restore epoch changed; re-bootstrap before continuing', statusCode: 409 }) } } @@ -280,15 +531,45 @@ export class DurableSyncService { eq(syncCommands.workspaceId, workspaceId), eq(syncCommands.commandId, command.commandId), )).limit(1) if (previous !== undefined) { + if (this.syncEpoch !== undefined && previous.syncEpoch !== this.syncEpoch) { + throw new ApiError({ + code: 'command_id_reused', + message: 'Command ID belongs to another server restore epoch', + statusCode: 409, + details: { reason: 'sync_epoch_changed' }, + }) + } if (previous.requestHash !== requestHash) throw new ApiError({ code: 'command_id_reused', message: 'Command ID was reused', statusCode: 409 }) return { ...(previous.result as SyncCommandResult), duplicate: true } } + if (storageLimit !== null) await this.usage?.reconcileCurrentInTransaction(tx, accountId) const applied = await this.#apply(tx, accountId, deviceId, workspaceId, command, storageLimit) await tx.insert(syncCommands).values({ workspaceId, commandId: command.commandId, sourceDeviceId: deviceId, requestHash, + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), result: applied as unknown as Record, }) + const releasesStorage = command.type === 'delete-object' || command.type === 'delete-subtree' + || (command.type === 'resolve-conflict' && command.deleteObject === true) + if (storageLimit !== null && this.usage !== undefined && !releasesStorage) { + const exact = await this.usage.reconcileCurrentInTransaction(tx, accountId) + if (exact !== undefined + && exact.activeObjectBytes + exact.activeCrdtBytes + exact.activeBlobBytes + + exact.reservedBlobBytes + exact.retainedBytes > storageLimit) { + throw new ApiError({ + code: 'quota_exceeded', + message: 'Storage quota is exceeded', + statusCode: 409, + details: { + metric: 'storage_bytes', limit: storageLimit.toString(), + used: (exact.activeObjectBytes + exact.activeCrdtBytes + exact.activeBlobBytes + + exact.reservedBlobBytes + exact.retainedBytes).toString(), + reserved: exact.reservedBlobBytes.toString(), + }, + }) + } + } return applied }) if ((result.status === 'applied' || result.status === 'conflict') && result.sequence !== undefined) { @@ -297,6 +578,24 @@ export class DurableSyncService { return result } + async #recordRejectedCommand( + accountId: string, + deviceId: string, + workspaceId: string, + command: SyncCommand, + result: SyncCommandResult, + ): Promise { + const requestHash = createHash('sha256').update(JSON.stringify(command)).digest('base64url') + await this.database.db.transaction(async (tx) => { + await assertAccountWriteAllowedInTransaction(tx, accountId) + await tx.insert(syncCommands).values({ + workspaceId, commandId: command.commandId, sourceDeviceId: deviceId, requestHash, + ...(this.syncEpoch === undefined ? {} : { syncEpoch: this.syncEpoch }), + result: result as unknown as Record, + }).onConflictDoNothing() + }) + } + async #apply(tx: SyncTransaction, accountId: string, deviceId: string, workspaceId: string, command: SyncCommand, storageLimit: bigint | null): Promise { if (command.type === 'upsert-object' || command.type === 'delete-object') { const [current] = await tx.select().from(objects).where(and(eq(objects.workspaceId, workspaceId), eq(objects.objectId, command.objectId))).limit(1) @@ -309,6 +608,14 @@ export class DurableSyncService { commandId: command.commandId, status: 'conflict', duplicate: false, code: 'revision_conflict', ...(actual === null ? {} : { revision: actual.toString() }), } + if (command.type === 'delete-object' && current?.deletedAt !== null && current?.deletedAt !== undefined) { + return { + commandId: command.commandId, + status: 'applied', + duplicate: true, + revision: current.currentRevision.toString(), + } + } const parentObjectId = command.parentObjectId === undefined ? current?.parentObjectId ?? null : command.parentObjectId const nameCiphertext = command.nameCiphertext === undefined @@ -350,6 +657,7 @@ export class DurableSyncService { } } if (command.type === 'delete-object') { + if (command.kind === 'folder') await this.#assertFolderEmpty(tx, workspaceId, command.objectId) if (command.kind === 'asset') { const bindings = await tx.select({ ownerObjectId: syncResourceBindings.ownerObjectId, @@ -536,21 +844,26 @@ export class DurableSyncService { this.#validateCiphertext(command.conflictCiphertext, command.conflictCiphertextHash) await this.#assertKeyAndBlobs(tx, workspaceId, command.conflictKeyVersion, []) const currentObjects = await tx.select().from(objects).where(eq(objects.workspaceId, workspaceId)) - const root = currentObjects.find(object => object.objectId === command.rootObjectId) + const objectById = new Map(currentObjects.map(object => [object.objectId, object])) + const childrenByParent = new Map() + for (const object of currentObjects) { + if (object.parentObjectId === null) continue + const children = childrenByParent.get(object.parentObjectId) ?? [] + children.push(object) + childrenByParent.set(object.parentObjectId, children) + } + const root = objectById.get(command.rootObjectId) if (root === undefined || root.kind !== 'folder') { throw new ApiError({ code: 'subtree_root_invalid', message: 'Subtree root must be an active folder', statusCode: 409 }) } if (root.deletedAt !== null) { const descendants = new Set([root.objectId]) - let changed = true - while (changed) { - changed = false - for (const object of currentObjects) { - if (object.parentObjectId !== null && descendants.has(object.parentObjectId) - && !descendants.has(object.objectId)) { - descendants.add(object.objectId) - changed = true - } + const pending = [root.objectId] + while (pending.length > 0) { + for (const child of childrenByParent.get(pending.pop()!) ?? []) { + if (descendants.has(child.objectId)) continue + descendants.add(child.objectId) + pending.push(child.objectId) } } const activeDescendant = currentObjects.some(object => ( @@ -566,15 +879,12 @@ export class DurableSyncService { return { commandId: command.commandId, status: 'applied', duplicate: true } } const descendants = new Set([root.objectId]) - let changed = true - while (changed) { - changed = false - for (const object of currentObjects) { - if (object.deletedAt === null && object.parentObjectId !== null - && descendants.has(object.parentObjectId) && !descendants.has(object.objectId)) { - descendants.add(object.objectId) - changed = true - } + const pending = [root.objectId] + while (pending.length > 0) { + for (const child of childrenByParent.get(pending.pop()!) ?? []) { + if (child.deletedAt !== null || descendants.has(child.objectId)) continue + descendants.add(child.objectId) + pending.push(child.objectId) } } const requestedIds = new Set(command.objects.map(object => object.objectId)) @@ -586,7 +896,10 @@ export class DurableSyncService { }) } const requested = new Map(command.objects.map(object => [object.objectId, object])) - const documentRows = await tx.select().from(syncDocuments).where(eq(syncDocuments.workspaceId, workspaceId)) + const documentRows = await tx.select().from(syncDocuments).where(and( + eq(syncDocuments.workspaceId, workspaceId), inArray(syncDocuments.objectId, [...descendants]), + )) + const documentByObjectId = new Map(documentRows.map(document => [document.objectId, document])) let editConflict = false for (const current of currentObjects.filter(object => descendants.has(object.objectId))) { const item = requested.get(current.objectId) @@ -596,7 +909,7 @@ export class DurableSyncService { } this.#validateEnvelope(item) await this.#assertKeyAndBlobs(tx, workspaceId, item.keyVersion, item.blobRefs) - const document = documentRows.find(row => row.objectId === current.objectId) + const document = documentByObjectId.get(current.objectId) if ((document?.latestDocumentSequence ?? 0n) > counter(item.expectedDocumentSequence, 'expectedDocumentSequence')) { editConflict = true } @@ -626,7 +939,7 @@ export class DurableSyncService { while (parentId !== null && descendants.has(parentId) && !seen.has(parentId)) { seen.add(parentId) value += 1 - parentId = currentObjects.find(candidate => candidate.objectId === parentId)?.parentObjectId ?? null + parentId = objectById.get(parentId)?.parentObjectId ?? null } return value } @@ -663,7 +976,7 @@ export class DurableSyncService { return { commandId: command.commandId, status: 'applied', duplicate: false, sequence: finalSequence.toString() } } - if (command.type === 'append-update') { + if (command.type === 'initialize-document' || command.type === 'append-update') { await this.#assertKeyAndBlobs(tx, workspaceId, command.keyVersion, []) const [currentObject] = await tx.select({ deletedAt: objects.deletedAt, kind: objects.kind }).from(objects).where(and( eq(objects.workspaceId, workspaceId), eq(objects.objectId, command.objectId), @@ -686,6 +999,16 @@ export class DurableSyncService { if (document !== undefined && (document.objectId !== command.objectId || document.kind !== command.kind)) { throw new ApiError({ code: 'document_identity_mismatch', message: 'Document ID is already bound to another object', statusCode: 409 }) } + if (command.type === 'initialize-document' + && document !== undefined + && (document.latestDocumentSequence > 0n || document.checkpointDocumentSequence > 0n)) { + throw new ApiError({ + code: 'document_already_initialized', + message: 'Document already has an authoritative collaborative baseline', + statusCode: 409, + details: { latestDocumentSequence: document.latestDocumentSequence.toString() }, + }) + } const documentSequence = (document?.latestDocumentSequence ?? 0n) + 1n const sequence = await nextSequence(tx, workspaceId) await tx.insert(syncDocuments).values({ @@ -822,10 +1145,11 @@ export class DurableSyncService { throw new ApiError({ code: 'request_invalid', message: 'Delete conflict resolution cannot also restore content', statusCode: 400 }) } if (command.requiresCommandId !== undefined) { - const [required] = await tx.select({ result: syncCommands.result }).from(syncCommands).where(and( + const [required] = await tx.select({ result: syncCommands.result, syncEpoch: syncCommands.syncEpoch }).from(syncCommands).where(and( eq(syncCommands.workspaceId, workspaceId), eq(syncCommands.commandId, command.requiresCommandId), )).limit(1) - if (required === undefined || required.result.status !== 'applied') { + if (required === undefined || required.result.status !== 'applied' + || (this.syncEpoch !== undefined && required.syncEpoch !== this.syncEpoch)) { throw new ApiError({ code: 'required_command_not_applied', message: 'Required command was not applied', statusCode: 409, @@ -1011,6 +1335,7 @@ export class DurableSyncService { && object.currentRevision !== conflict.expectedRevision)) { return { commandId: command.commandId, status: 'conflict', duplicate: false, code: 'conflict_changed' } } + if (object.kind === 'folder') await this.#assertFolderEmpty(tx, workspaceId, object.objectId) const revision = object.currentRevision + 1n const deleteSequence = await nextSequence(tx, workspaceId) await this.usage?.applyCurrentObject(tx, { @@ -1113,6 +1438,19 @@ export class DurableSyncService { } } + async #assertFolderEmpty(tx: SyncTransaction, workspaceId: string, folderObjectId: string): Promise { + const [child] = await tx.select({ objectId: objects.objectId }).from(objects).where(and( + eq(objects.workspaceId, workspaceId), eq(objects.parentObjectId, folderObjectId), isNull(objects.deletedAt), + )).limit(1) + if (child !== undefined) throw new ApiError({ + code: 'folder_not_empty', + message: 'A folder with active children must be deleted with the atomic subtree command', + statusCode: 409, + retryable: false, + details: { childObjectId: child.objectId }, + }) + } + async #pruneCoveredUpdates(tx: SyncTransaction, workspaceId: string, documentId: string, covers: bigint): Promise { const [activeBootstrap] = await tx.select({ id: syncBootstrapSessions.id }).from(syncBootstrapSessions).where(and( eq(syncBootstrapSessions.workspaceId, workspaceId), gt(syncBootstrapSessions.expiresAt, new Date()), diff --git a/apps/server/src/durable-sync/types.ts b/apps/server/src/durable-sync/types.ts index ed4dc59..289f6e6 100644 --- a/apps/server/src/durable-sync/types.ts +++ b/apps/server/src/durable-sync/types.ts @@ -54,6 +54,14 @@ export type SyncCommand = blobRefs: string[] } & CiphertextEnvelope> } + | ({ + type: 'initialize-document' + commandId: string + updateId: string + documentId: string + objectId: string + kind: SyncObjectKind + } & CiphertextEnvelope) | ({ type: 'append-update' commandId: string diff --git a/apps/server/src/identity/email-service.ts b/apps/server/src/identity/email-service.ts index 963ec8f..58265be 100644 --- a/apps/server/src/identity/email-service.ts +++ b/apps/server/src/identity/email-service.ts @@ -239,7 +239,7 @@ export class EmailIdentityService { to: email, template, locale: this.config.mailDefaultLocale, - variables: { actionUrl: `${this.config.publicBaseUrl.replace(/\/$/, '')}/account/${purpose === 'verify_email' ? 'verify-email' : 'reset-password'}?token=${encodeURIComponent(issued.token)}` }, + variables: { actionUrl: `${this.config.webPublicBaseUrl.replace(/\/$/, '')}/account/${purpose === 'verify_email' ? 'verify-email' : 'reset-password'}/?token=${encodeURIComponent(issued.token)}` }, }, issued.expiresAt) await this.mailOutbox.enqueueInTransaction(tx, { template, diff --git a/apps/server/src/installation/service.ts b/apps/server/src/installation/service.ts index 38f5da0..abad370 100644 --- a/apps/server/src/installation/service.ts +++ b/apps/server/src/installation/service.ts @@ -1,5 +1,5 @@ import argon2 from 'argon2' -import { eq, sql } from 'drizzle-orm' +import { and, eq, isNull, sql } from 'drizzle-orm' import type { AppConfig } from '../config.js' import type { DatabaseContext } from '../database/client.js' import { @@ -8,27 +8,22 @@ import { accounts, adminAuditLogs, deploymentSettings, + maintenanceState, staffPrincipals, - staffRoleAssignments, } from '../database/schema.js' import { ApiError } from '../errors.js' import { normalizeLoginKey } from '../identity/service.js' -import { normalizeLocalStaffLogin } from '../staff/service.js' export type InstallationDeploymentMode = 'self-hosted' | 'hosted' -export type HostedRegistrationPolicy = 'disabled' | 'public' export interface InstallationStatus { installationRequired: boolean activationPending: boolean - deploymentMode: InstallationDeploymentMode | null serverName: string } export interface CompleteInstallationInput { - deploymentMode: InstallationDeploymentMode serverName: string - hostedRegistrationPolicy?: HostedRegistrationPolicy administrator: { login: string, password: string } } @@ -42,7 +37,6 @@ export class InstallationService { async status(): Promise { const [settings] = await this.database.db.select({ - deploymentMode: deploymentSettings.deploymentMode, runtimeConfiguration: deploymentSettings.runtimeConfiguration, }).from(deploymentSettings).where(eq(deploymentSettings.id, true)).limit(1) const serverName = typeof settings?.runtimeConfiguration.serverName === 'string' @@ -51,14 +45,13 @@ export class InstallationService { return { installationRequired: settings === undefined, activationPending: settings !== undefined && this.installationOnly, - deploymentMode: settings?.deploymentMode ?? null, serverName, } } async persistedSettings(): Promise<{ deploymentMode: InstallationDeploymentMode - registrationPolicy: HostedRegistrationPolicy | 'bootstrap' | 'invitation' + registrationPolicy: 'bootstrap' | 'disabled' | 'invitation' | 'public' } | undefined> { const [settings] = await this.database.db.select({ deploymentMode: deploymentSettings.deploymentMode, @@ -68,8 +61,35 @@ export class InstallationService { return settings } + /** Converts the removed hosted/internal-test marker into the single + * independent-instance model. This preserves accounts, registration policy, + * runtime configuration, workspaces and all synchronized data. */ + async migrateLegacyOperationsInstallation(): Promise { + return await this.database.db.transaction(async (tx) => { + await tx.execute(sql`select pg_advisory_xact_lock(hashtext('notegen-installation-mode-migration'))`) + const [settings] = await tx.select().from(deploymentSettings) + .where(eq(deploymentSettings.id, true)).limit(1).for('update') + if (settings === undefined || settings.deploymentMode === 'self-hosted') return false + const [activeAdmin] = await tx.select({ id: accounts.id }).from(accounts).where(and( + eq(accounts.isAdmin, true), isNull(accounts.suspendedAt), isNull(accounts.disabledAt), + )).limit(1) + await tx.update(deploymentSettings).set({ + deploymentMode: 'self-hosted', + selfHostedLifecycle: 'ready', + adminRepairRequired: activeAdmin === undefined, + initializedAt: settings.initializedAt ?? new Date(), + initializedByAccountId: settings.initializedByAccountId ?? activeAdmin?.id ?? null, + configurationRevision: sql`${deploymentSettings.configurationRevision} + 1`, + updatedAt: new Date(), + }).where(and( + eq(deploymentSettings.id, true), + eq(deploymentSettings.deploymentMode, settings.deploymentMode), + )) + return true + }) + } + async complete(input: CompleteInstallationInput): Promise<{ - deploymentMode: InstallationDeploymentMode serverName: string activationPending: true }> { @@ -90,8 +110,8 @@ export class InstallationService { throw new ApiError({ code: 'installation_already_completed', message: 'Installation has already been completed', statusCode: 409 }) } const [existingAccount] = await tx.select({ id: accounts.id }).from(accounts).limit(1) - const [existingStaffPrincipal] = await tx.select({ id: staffPrincipals.id }).from(staffPrincipals).limit(1) - if (existingAccount !== undefined || existingStaffPrincipal !== undefined) { + const [existingLegacyStaff] = await tx.select({ id: staffPrincipals.id }).from(staffPrincipals).limit(1) + if (existingAccount !== undefined || existingLegacyStaff !== undefined) { throw new ApiError({ code: 'installation_existing_data', message: 'Installation cannot run while account data already exists', @@ -100,32 +120,6 @@ export class InstallationService { } const runtimeConfiguration = initialRuntimeConfiguration(this.config, serverName) - if (input.deploymentMode === 'hosted') { - const administrator = input.administrator - const login = normalizeLocalStaffLogin(administrator.login) - if (login.length < 1 || login.length > 200) { - throw new ApiError({ code: 'installation_administrator_invalid', message: 'Administrator login is invalid', statusCode: 400 }) - } - const [principal] = await tx.insert(staffPrincipals).values({ - externalIssuer: 'https://local.notegen.invalid', - externalSubject: `local:${login}`, - displayName: administrator.login.trim(), - localLogin: login, - localPasswordHash: passwordHash, - }).returning({ id: staffPrincipals.id }) - if (principal === undefined) throw new Error('Installation Staff principal insert returned no row') - await tx.insert(staffRoleAssignments).values({ staffId: principal.id, roleKey: 'platform-admin' }) - await tx.insert(deploymentSettings).values({ - id: true, - deploymentMode: 'hosted', - registrationPolicy: input.hostedRegistrationPolicy ?? 'disabled', - selfHostedLifecycle: null, - initializedAt: new Date(), - runtimeConfiguration, - }) - return - } - const administrator = input.administrator const login = administrator.login.trim() const [created] = await tx.insert(accounts).values({ @@ -159,6 +153,7 @@ export class InstallationService { initializedByAccountId: created.id, runtimeConfiguration, }) + await tx.insert(maintenanceState).values({ id: true }) await tx.insert(adminAuditLogs).values({ actorAccountId: created.id, action: 'instance.web-installation-complete', @@ -168,7 +163,7 @@ export class InstallationService { }) }) - return { deploymentMode: input.deploymentMode, serverName, activationPending: true } + return { serverName, activationPending: true } } } diff --git a/apps/server/src/invitations/service.ts b/apps/server/src/invitations/service.ts index 58f309a..7f16caa 100644 --- a/apps/server/src/invitations/service.ts +++ b/apps/server/src/invitations/service.ts @@ -56,7 +56,7 @@ export class InvitationService { to: boundEmail, template: 'invitation', locale: this.config.mailDefaultLocale, - variables: { actionUrl: `${this.config.publicBaseUrl.replace(/\/$/, '')}/#/accept-invite/${token}` }, + variables: { actionUrl: `${this.config.webPublicBaseUrl.replace(/\/$/, '')}/account/accept-invite/?token=${encodeURIComponent(token)}` }, }, invitation.expiresAt) await this.mail.outbox.enqueueInTransaction(tx, { template: 'invitation', @@ -70,7 +70,7 @@ export class InvitationService { return [invitation] }) if (created === undefined) throw new Error('Invitation transaction returned no row') - return { id: created.id, token, url: `${this.config.webPublicBaseUrl}/#/accept-invite/${token}`, expiresAt: created.expiresAt, deliveryQueued: input.send === true } + return { id: created.id, token, url: `${this.config.webPublicBaseUrl.replace(/\/$/, '')}/account/accept-invite/?token=${encodeURIComponent(token)}`, expiresAt: created.expiresAt, deliveryQueued: input.send === true } } async inspect(token: string): Promise<{ canContinue: boolean, requiresEmail: boolean, serverName: string }> { @@ -183,7 +183,7 @@ export class InvitationService { idempotencyKey, to: old.boundEmailNormalized, template: 'invitation', locale: this.config.mailDefaultLocale, - variables: { actionUrl: `${this.config.publicBaseUrl.replace(/\/$/, '')}/#/accept-invite/${token}` }, + variables: { actionUrl: `${this.config.webPublicBaseUrl.replace(/\/$/, '')}/account/accept-invite/?token=${encodeURIComponent(token)}` }, }, replacement.expiresAt) await this.mail.outbox.enqueueInTransaction(tx, { template: 'invitation', diff --git a/apps/server/src/maintenance/service.ts b/apps/server/src/maintenance/service.ts index 6e25477..80980d5 100644 --- a/apps/server/src/maintenance/service.ts +++ b/apps/server/src/maintenance/service.ts @@ -4,7 +4,8 @@ import type { AppConfig } from '../config.js' import type { DatabaseContext } from '../database/client.js' import { blobs, blobUploads, bootstrapSessions, deviceAuthorizations, devicePairings, - stepUpGrants, supportDiagnosticGrants, syncBootstrapSessions, webSessions, workspaces, + stepUpGrants, supportDiagnosticGrants, syncBootstrapSessions, syncDeviceCursors, + webSessions, workspaces, } from '../database/schema.js' import type { BlobStorage } from '../storage/blob-storage.js' import { BLOB_COMPLETION_LEASE_MS } from '../blobs/constants.js' @@ -28,6 +29,11 @@ export interface MaintenanceResult { changes: number versions: number operations: number + syncEvents: number + syncCommands: number + syncCheckpoints: number + syncConflicts: number + syncDeviceCursors: number tombstones: number blobs: number deletionCases: number @@ -173,7 +179,8 @@ export class MaintenanceService { and not exists (select 1 from workspaces w where w.account_id = a.id) and not exists (select 1 from account_deletion_cases c where c.account_id = a.id) returning a.id` - const [changesResult, versionsResult, operationsResult, tombstonesResult, blobsResult] = + const [changesResult, versionsResult, operationsResult, syncEventsResult, syncCommandsResult, + syncCheckpointsResult, syncConflictsResult, tombstonesResult, blobsResult] = await this.database.sql.begin(async (sql) => { const removedChanges = await sql` delete from changes where id in ( @@ -219,6 +226,42 @@ export class MaintenanceService { select workspace_id, operation_id from operations where created_at < ${changeCutoff.toISOString()}::timestamptz limit 10000 ) returning operation_id` + const removedSyncEvents = await sql` + delete from sync_events where id in ( + select id from sync_events + where created_at < ${changeCutoff.toISOString()}::timestamptz + order by id limit 10000 + ) returning id` + const removedSyncCommands = await sql` + delete from sync_commands where (workspace_id, command_id) in ( + select workspace_id, command_id from sync_commands + where created_at < ${changeCutoff.toISOString()}::timestamptz + order by created_at limit 10000 + ) returning command_id` + const removedSyncCheckpoints = await sql` + delete from sync_checkpoints checkpoint where (workspace_id, checkpoint_id) in ( + select candidate.workspace_id, candidate.checkpoint_id + from sync_checkpoints candidate + left join sync_documents document + on document.workspace_id = candidate.workspace_id + and document.checkpoint_id = candidate.checkpoint_id + where candidate.created_at < ${versionCutoff.toISOString()}::timestamptz + and document.checkpoint_id is null + and not exists ( + select 1 from sync_bootstrap_objects snapshot + join sync_bootstrap_sessions session on session.id = snapshot.session_id + where snapshot.workspace_id = candidate.workspace_id + and snapshot.checkpoint_id = candidate.checkpoint_id + and session.expires_at > now() + ) + order by candidate.created_at limit 5000 + ) returning checkpoint_id` + const removedSyncConflicts = await sql` + delete from sync_conflicts where (workspace_id, conflict_id) in ( + select workspace_id, conflict_id from sync_conflicts + where status = 'resolved' and resolved_at < ${changeCutoff.toISOString()}::timestamptz + order by resolved_at limit 5000 + ) returning conflict_id` const removedTombstones = await sql` delete from objects where (workspace_id, object_id) in ( select workspace_id, object_id from objects @@ -238,9 +281,15 @@ export class MaintenanceService { where v.workspace_id = b.workspace_id and v.blob_refs ? b.blob_id ) limit 1000` - return [removedChanges, removedVersions, removedOperations, removedTombstones, candidates] as const + return [removedChanges, removedVersions, removedOperations, removedSyncEvents, + removedSyncCommands, removedSyncCheckpoints, removedSyncConflicts, + removedTombstones, candidates] as const }) + const removedSyncDeviceCursors = await this.database.db.delete(syncDeviceCursors) + .where(lt(syncDeviceCursors.updatedAt, changeCutoff)) + .returning({ deviceId: syncDeviceCursors.deviceId }) + let removedBlobs = 0 for (const blob of blobsResult) { await this.storage.delete(blob.storage_key) @@ -265,6 +314,11 @@ export class MaintenanceService { changes: changesResult.length, versions: versionsResult.length, operations: operationsResult.length, + syncEvents: syncEventsResult.length, + syncCommands: syncCommandsResult.length, + syncCheckpoints: syncCheckpointsResult.length, + syncConflicts: syncConflictsResult.length, + syncDeviceCursors: removedSyncDeviceCursors.length, tombstones: tombstonesResult.length, blobs: removedBlobs, deletionCases: advancedDeletionCases + purgingDeletionCases + finalizedDeletionAccounts, @@ -667,6 +721,11 @@ function emptyMaintenanceResult(): MaintenanceResult { changes: 0, versions: 0, operations: 0, + syncEvents: 0, + syncCommands: 0, + syncCheckpoints: 0, + syncConflicts: 0, + syncDeviceCursors: 0, tombstones: 0, blobs: 0, deletionCases: 0, diff --git a/apps/server/src/observability/metrics.ts b/apps/server/src/observability/metrics.ts index 6b9e87f..b95e288 100644 --- a/apps/server/src/observability/metrics.ts +++ b/apps/server/src/observability/metrics.ts @@ -1,10 +1,11 @@ import { timingSafeEqual } from 'node:crypto' import type { FastifyInstance } from 'fastify' -import { collectDefaultMetrics, Counter, Histogram, Registry } from 'prom-client' +import { collectDefaultMetrics, Counter, Gauge, Histogram, Registry } from 'prom-client' import type { AppConfig } from '../config.js' +import type { DatabaseContext } from '../database/client.js' import { ApiError } from '../errors.js' -export function registerMetrics(app: FastifyInstance, config: AppConfig): void { +export function registerMetrics(app: FastifyInstance, config: AppConfig, database?: DatabaseContext): void { const registry = new Registry() collectDefaultMetrics({ register: registry, prefix: 'notegen_sync_' }) const requestDuration = new Histogram({ @@ -32,6 +33,22 @@ export function registerMetrics(app: FastifyInstance, config: AppConfig): void { registers: [registry], buckets: [1_024, 64 * 1_024, 1024 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024], }) + const durableRows = new Gauge({ + name: 'notegen_sync_durable_table_rows', + help: 'Estimated live rows in durable synchronization tables', + labelNames: ['table'] as const, + registers: [registry], + }) + const activeBootstrapSnapshots = new Gauge({ + name: 'notegen_sync_active_bootstrap_snapshots', + help: 'Number of non-expired durable bootstrap snapshots', + registers: [registry], + }) + const oldestEventAge = new Gauge({ + name: 'notegen_sync_oldest_event_age_seconds', + help: 'Age of the oldest retained durable sync event', + registers: [registry], + }) app.addHook('onRequest', async (request) => { request.metricsStartedAt = process.hrtime.bigint() @@ -71,6 +88,24 @@ export function registerMetrics(app: FastifyInstance, config: AppConfig): void { return reply.status(401).send() } } + if (database !== undefined) { + try { + const [snapshot] = await database.sql>`select + (select count(*)::int from sync_bootstrap_sessions where expires_at > now()) as active_bootstraps, + coalesce((select extract(epoch from now() - min(created_at))::float8 from sync_events), 0) as oldest_event_age_seconds` + activeBootstrapSnapshots.set(snapshot?.active_bootstraps ?? 0) + oldestEventAge.set(snapshot?.oldest_event_age_seconds ?? 0) + const estimates = await database.sql>` + select relname, n_live_tup::float8 as rows from pg_stat_user_tables + where relname = any(array['sync_events','sync_commands','sync_checkpoints','sync_conflicts','sync_updates'])` + for (const estimate of estimates) durableRows.set({ table: estimate.relname }, estimate.rows) + } catch (error) { + request.log.warn({ err: error }, 'Failed to refresh durable sync metrics') + } + } return reply.type(registry.contentType).send(await registry.metrics()) }) } diff --git a/apps/server/src/routes/api-schemas.ts b/apps/server/src/routes/api-schemas.ts index 2bf195f..853eb01 100644 --- a/apps/server/src/routes/api-schemas.ts +++ b/apps/server/src/routes/api-schemas.ts @@ -15,10 +15,10 @@ export const SyncObjectKindSchema = Type.Union([ Type.Literal('folder'), Type.Literal('asset'), Type.Literal('canvas'), - Type.Literal('record'), Type.Literal('tag'), Type.Literal('mark'), Type.Literal('conversation'), + Type.Literal('message'), Type.Literal('memory'), Type.Literal('setting'), Type.Literal('yjs-checkpoint'), diff --git a/apps/server/src/routes/blobs.ts b/apps/server/src/routes/blobs.ts index 95952aa..5973bed 100644 --- a/apps/server/src/routes/blobs.ts +++ b/apps/server/src/routes/blobs.ts @@ -53,7 +53,7 @@ export function createBlobRoutes( blobId: Type.String({ pattern: '^[A-Za-z0-9_-]{43}$' }), expectedSize: Type.String({ pattern: '^\\d{1,19}$' }), ciphertextHash: Type.String({ pattern: '^[A-Za-z0-9_-]{43}$' }), - expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })), + expectedSyncEpoch: Type.String({ format: 'uuid' }), }), response: { 200: UploadSessionResponse, 201: UploadSessionResponse }, }, @@ -117,13 +117,7 @@ export function createBlobRoutes( app.post('/v1/workspaces/:workspaceId/blobs/uploads/:uploadId/complete', { schema: { params: UploadParams, - // Older clients completed an upload without a request body when the workspace did - // not expose a sync epoch. Fastify normalizes that missing body to null, so retain - // compatibility while newer clients consistently send an empty JSON object. - body: Type.Union([ - Type.Object({ expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })) }), - Type.Null(), - ]), + body: Type.Object({ expectedSyncEpoch: Type.String({ format: 'uuid' }) }), response: { 200: CompletedBlobResponse }, }, }, async (request) => { @@ -132,7 +126,7 @@ export function createBlobRoutes( claims.accountId, request.params.workspaceId, request.params.uploadId, - request.body?.expectedSyncEpoch, + request.body.expectedSyncEpoch, ) }) diff --git a/apps/server/src/routes/capabilities.ts b/apps/server/src/routes/capabilities.ts index 7db4789..f56d213 100644 --- a/apps/server/src/routes/capabilities.ts +++ b/apps/server/src/routes/capabilities.ts @@ -61,7 +61,7 @@ const CapabilitiesResponse = Type.Object({ emailVerificationRequired: Type.Boolean(), }), instanceCapabilities: Type.Record(Type.String(), Type.Boolean()), - deploymentMode: Type.Union([Type.Literal('self-hosted'), Type.Literal('hosted')]), + deploymentMode: Type.Literal('self-hosted'), web: Type.Object({ accountUrl: Type.String({ format: 'uri' }), deviceAuthorizationUrl: Type.String({ format: 'uri' }), @@ -85,9 +85,7 @@ export function createCapabilitiesRoutes( : registrationPolicy === 'invitation' ? ['invitation', 'browser'] : registrationPolicy === 'public' - ? config.deploymentMode === 'hosted' - ? instanceCapabilities['identity.emailVerification'] === true ? ['email-password'] : [] - : ['password'] + ? ['password'] : [] return { service: 'note-gen-server' as const, @@ -136,11 +134,11 @@ export function createCapabilitiesRoutes( tombstoneRetentionDays: config.tombstoneRetentionDays, }, registrationMode: dependencies.deployment?.legacyRegistrationMode() ?? config.registrationMode, - deploymentMode: dependencies.deployment?.getState().deploymentMode ?? config.deploymentMode, + deploymentMode: 'self-hosted' as const, capabilitySchema: 2 as const, instanceCapabilityRevision: dependencies.deployment?.getState().configurationRevision ?? '0', registrationPolicyRevision: dependencies.deployment?.getState().configurationRevision ?? '0', - requiredSyncFeatures: [], + requiredSyncFeatures: ['syncEpochFencing', 'durableCursorAcknowledgement'], registration: { policy: registrationPolicy, methods: registrationMethods, diff --git a/apps/server/src/routes/events.ts b/apps/server/src/routes/events.ts index 94ad118..522dbc2 100644 --- a/apps/server/src/routes/events.ts +++ b/apps/server/src/routes/events.ts @@ -4,14 +4,12 @@ import type { TokenService } from '../auth/tokens.js' import type { AuthService } from '../auth/service.js' import type { ChangeNotifier } from '../sync/types.js' import type { WorkspaceService } from '../workspaces/service.js' -import type { MaintenanceCoordinator } from '../maintenance/coordinator.js' -import { ApiError } from '../errors.js' interface AuthenticateMessage { type: 'authenticate' accessToken: string workspaceIds: string[] - expectedSyncEpoch?: string + expectedSyncEpoch: string } interface PresenceUpdateMessage { @@ -21,24 +19,16 @@ interface PresenceUpdateMessage { anchor: number head: number label: string + coordinateSpace: 'markdown' | 'prosemirror' | 'canvas' + canvas: { + nodes: Array<{ id: string; x: number; y: number }> + } | null } interface PresenceClearMessage { type: 'presence.clear' } -interface DocumentUpdateMessage { - type: 'document.update' - workspaceId: string - documentId: string - objectId: string - kind: string - updateId: string - keyVersion: number - ciphertext: string - ciphertextHash: string -} - interface DocumentSubscriptionMessage { type: 'document.subscribe' | 'document.unsubscribe' workspaceId: string @@ -46,7 +36,8 @@ interface DocumentSubscriptionMessage { } type RealtimeMessage = PresenceUpdateMessage | PresenceClearMessage - | DocumentUpdateMessage | DocumentSubscriptionMessage + | DocumentSubscriptionMessage +type PresenceMessage = PresenceUpdateMessage | PresenceClearMessage interface PresenceState extends PresenceUpdateMessage { deviceId: string @@ -58,14 +49,23 @@ interface PresenceConnection { workspaceIds: Set documentIds: Set presence: PresenceState | null + accountId: string + credentialEpoch: string + instanceAuthEpoch?: string + issuedAt?: number + rateTokens: number + rateUpdatedAt: number } +const REALTIME_BURST_BYTES = 4 * 1024 * 1024 +const REALTIME_BYTES_PER_SECOND = 1024 * 1024 +const MAX_SOCKET_BUFFERED_BYTES = 8 * 1024 * 1024 + export function createEventRoutes( tokens: TokenService, auth: AuthService, workspaces: WorkspaceService, notifier: ChangeNotifier, - maintenance?: MaintenanceCoordinator, syncEpoch?: string, ): FastifyPluginAsyncTypebox { const presenceRooms = new Map>() @@ -114,26 +114,23 @@ export function createEventRoutes( if (authenticated) { if (!presenceConnection) return try { + consumeRealtimeBudget(presenceConnection, Buffer.byteLength(raw.toString())) const message = parseRealtimeMessage(raw) - if (message.type === 'document.update') { - await assertRealtimeMutationAllowed(maintenance, currentSocket) - relayDocumentUpdate(message, presenceConnection, presenceRooms) - } else if (message.type === 'document.subscribe' + if (message.type === 'document.subscribe' || message.type === 'document.unsubscribe') { handleDocumentSubscription(message, presenceConnection, presenceRooms) } else { handlePresenceMessage(message, presenceConnection, presenceRooms) } - } catch (error) { - if (error instanceof ApiError && error.code === 'server_maintenance') return + } catch { currentSocket.close(1008, 'Invalid realtime message') } return } try { const message = parseAuthenticateMessage(raw) - if (message.expectedSyncEpoch !== undefined && syncEpoch !== undefined && message.expectedSyncEpoch !== syncEpoch) { - currentSocket.send(JSON.stringify({ type: 'sync.epoch-changed', code: 'sync_epoch_changed', retryable: false })) + if (syncEpoch !== undefined && message.expectedSyncEpoch !== syncEpoch) { + sendWithBackpressure(currentSocket, JSON.stringify({ type: 'sync.epoch-changed', code: 'sync_epoch_changed', retryable: false })) currentSocket.close(1008, 'Sync epoch changed') return } @@ -141,15 +138,27 @@ export function createEventRoutes( await auth.assertDeviceActive(claims.accountId, claims.deviceId, claims.credentialEpoch, claims.instanceAuthEpoch, claims.issuedAt) const uniqueWorkspaceIds = [...new Set(message.workspaceIds)] if (uniqueWorkspaceIds.length > 100) throw new Error('Too many workspace subscriptions') - await Promise.all(uniqueWorkspaceIds.map((id) => workspaces.assertOwned(claims.accountId, id))) + await Promise.all(uniqueWorkspaceIds.map((id) => ( + workspaces.assertCapability(claims.accountId, id, 'content.read') + ))) unsubscribe.push(notifier.subscribeAccount(claims.accountId, () => { if (currentSocket.readyState === currentSocket.OPEN) { - currentSocket.send(JSON.stringify({ type: 'account.workspaces-changed' })) + sendWithBackpressure(currentSocket, JSON.stringify({ type: 'account.workspaces-changed' })) } })) for (const workspaceId of uniqueWorkspaceIds) { unsubscribe.push(notifier.subscribeWorkspace(workspaceId, (notice) => { - if (currentSocket.readyState === currentSocket.OPEN) currentSocket.send(JSON.stringify(notice)) + if (currentSocket.readyState === currentSocket.OPEN) sendWithBackpressure(currentSocket, JSON.stringify(notice)) + if (notice.type === 'workspace.members-changed') { + void workspaces.assertCapability(claims.accountId, workspaceId, 'content.read').catch(() => { + if (currentSocket.readyState === currentSocket.OPEN) { + sendWithBackpressure(currentSocket, JSON.stringify({ + type: 'workspace.access-revoked', workspaceId, + })) + currentSocket.close(1008, 'Workspace access revoked') + } + }) + } })) } authenticated = true @@ -159,6 +168,12 @@ export function createEventRoutes( workspaceIds: new Set(uniqueWorkspaceIds), documentIds: new Set(), presence: null, + accountId: claims.accountId, + credentialEpoch: claims.credentialEpoch, + instanceAuthEpoch: claims.instanceAuthEpoch, + issuedAt: claims.issuedAt, + rateTokens: REALTIME_BURST_BYTES, + rateUpdatedAt: Date.now(), } for (const workspaceId of uniqueWorkspaceIds) { const room = presenceRooms.get(workspaceId) ?? new Set() @@ -169,7 +184,7 @@ export function createEventRoutes( sessionTimeout = setTimeout(() => { currentSocket.close(1008, 'Access token expired') }, Math.max(1, claims.expiresAt * 1_000 - Date.now())) - currentSocket.send(JSON.stringify({ + sendWithBackpressure(currentSocket, JSON.stringify({ type: 'authenticated', workspaceIds: uniqueWorkspaceIds, accessTokenExpiresAt: new Date(claims.expiresAt * 1_000).toISOString(), @@ -182,21 +197,6 @@ export function createEventRoutes( } } -async function assertRealtimeMutationAllowed(maintenance: MaintenanceCoordinator | undefined, socket: WebSocket): Promise { - if (maintenance === undefined) return - try { - await maintenance.requireMutationAllowed('/v1/sync/events') - } catch (error) { - if (error instanceof ApiError && error.code === 'server_maintenance') { - if (socket.readyState === socket.OPEN) socket.send(JSON.stringify({ - type: 'server.maintenance', code: error.code, retryable: error.retryable, details: error.details, - })) - socket.close(1013, 'Server maintenance') - } - throw error - } -} - function parseAuthenticateMessage(raw: RawData): AuthenticateMessage { const value: unknown = JSON.parse(raw.toString()) if (typeof value !== 'object' || value === null) throw new Error('Message must be an object') @@ -204,16 +204,15 @@ function parseAuthenticateMessage(raw: RawData): AuthenticateMessage { if (candidate.type !== 'authenticate' || typeof candidate.accessToken !== 'string' || !Array.isArray(candidate.workspaceIds) || !candidate.workspaceIds.every((id) => typeof id === 'string') - || (candidate.expectedSyncEpoch !== undefined - && (typeof candidate.expectedSyncEpoch !== 'string' - || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(candidate.expectedSyncEpoch)))) { + || typeof candidate.expectedSyncEpoch !== 'string' + || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(candidate.expectedSyncEpoch)) { throw new Error('Authentication message is invalid') } return { type: 'authenticate', accessToken: candidate.accessToken, workspaceIds: candidate.workspaceIds, - ...(candidate.expectedSyncEpoch === undefined ? {} : { expectedSyncEpoch: candidate.expectedSyncEpoch as string }), + expectedSyncEpoch: candidate.expectedSyncEpoch, } } @@ -232,31 +231,6 @@ function parseRealtimeMessage(raw: RawData): RealtimeMessage { documentId: candidate.documentId.slice(0, 200), } } - if (candidate.type === 'document.update') { - if (typeof candidate.workspaceId !== 'string' - || typeof candidate.documentId !== 'string' - || typeof candidate.objectId !== 'string' - || typeof candidate.kind !== 'string' - || typeof candidate.updateId !== 'string' - || typeof candidate.keyVersion !== 'number' || !Number.isSafeInteger(candidate.keyVersion) - || candidate.keyVersion < 1 - || typeof candidate.ciphertext !== 'string' - || typeof candidate.ciphertextHash !== 'string' - || candidate.ciphertext.length > 4 * 1024 * 1024) { - throw new Error('Document update message is invalid') - } - return { - type: 'document.update', - workspaceId: candidate.workspaceId, - documentId: candidate.documentId.slice(0, 200), - objectId: candidate.objectId.slice(0, 200), - kind: candidate.kind.slice(0, 40), - updateId: candidate.updateId.slice(0, 200), - keyVersion: candidate.keyVersion, - ciphertext: candidate.ciphertext, - ciphertextHash: candidate.ciphertextHash.slice(0, 200), - } - } if (candidate.type !== 'presence.update' || typeof candidate.workspaceId !== 'string' || typeof candidate.documentId !== 'string' @@ -273,6 +247,35 @@ function parseRealtimeMessage(raw: RawData): RealtimeMessage { anchor: candidate.anchor, head: candidate.head, label: candidate.label.trim().slice(0, 40) || '其他设备', + coordinateSpace: parsePresenceCoordinateSpace(candidate.coordinateSpace, candidate.canvas), + canvas: parseCanvasPresence(candidate.canvas), + } +} + +function parsePresenceCoordinateSpace( + value: unknown, + canvas: unknown, +): PresenceUpdateMessage['coordinateSpace'] { + if (value === 'markdown' || value === 'prosemirror' || value === 'canvas') return value + return canvas === undefined || canvas === null ? 'prosemirror' : 'canvas' +} + +function parseCanvasPresence(value: unknown): PresenceUpdateMessage['canvas'] { + if (value === undefined || value === null) return null + if (typeof value !== 'object' || value === null) throw new Error('Canvas presence is invalid') + const nodes = (value as Record).nodes + if (!Array.isArray(nodes) || nodes.length > 100) throw new Error('Canvas presence is invalid') + return { + nodes: nodes.map((node) => { + if (typeof node !== 'object' || node === null) throw new Error('Canvas presence node is invalid') + const candidate = node as Record + if (typeof candidate.id !== 'string' || candidate.id.length === 0 || candidate.id.length > 200 + || typeof candidate.x !== 'number' || !Number.isFinite(candidate.x) + || typeof candidate.y !== 'number' || !Number.isFinite(candidate.y)) { + throw new Error('Canvas presence node is invalid') + } + return { id: candidate.id, x: candidate.x, y: candidate.y } + }), } } @@ -298,31 +301,28 @@ function handleDocumentSubscription( for (const peer of rooms.get(message.workspaceId) ?? []) { if (peer !== connection && peer.documentIds.has(key) && peer.socket.readyState === peer.socket.OPEN) { - peer.socket.send(payload) + sendWithBackpressure(peer.socket, payload) } } } -function relayDocumentUpdate( - message: DocumentUpdateMessage, - connection: PresenceConnection, - rooms: Map>, -): void { - if (!connection.workspaceIds.has(message.workspaceId)) throw new Error('Workspace is not subscribed') - const room = rooms.get(message.workspaceId) - if (!room) return - const payload = JSON.stringify({ - ...message, - type: 'document.updated.realtime', - deviceId: connection.deviceId, - }) - for (const peer of room) { - if (peer !== connection - && peer.documentIds.has(`${message.workspaceId}\0${message.documentId}`) - && peer.socket.readyState === peer.socket.OPEN) { - peer.socket.send(payload) - } +function consumeRealtimeBudget(connection: PresenceConnection, bytes: number): void { + const now = Date.now() + const replenished = Math.min( + REALTIME_BURST_BYTES, + connection.rateTokens + ((now - connection.rateUpdatedAt) / 1_000) * REALTIME_BYTES_PER_SECOND, + ) + connection.rateUpdatedAt = now + connection.rateTokens = replenished - bytes + if (connection.rateTokens < 0) throw new Error('Realtime rate limit exceeded') +} + +function sendWithBackpressure(socket: WebSocket, payload: string): void { + if (socket.bufferedAmount > MAX_SOCKET_BUFFERED_BYTES) { + socket.close(1013, 'Realtime consumer is too slow') + return } + socket.send(payload) } function handlePresenceMessage( @@ -375,13 +375,13 @@ function clearPresence( deviceId: connection.deviceId, }) for (const peer of room) { - if (peer !== connection && peer.socket.readyState === peer.socket.OPEN) peer.socket.send(payload) + if (peer !== connection && peer.socket.readyState === peer.socket.OPEN) sendWithBackpressure(peer.socket, payload) } } function sendPresence(presence: PresenceState, socket: WebSocket): void { if (socket.readyState !== socket.OPEN) return - socket.send(JSON.stringify({ + sendWithBackpressure(socket, JSON.stringify({ type: 'presence.updated', workspaceId: presence.workspaceId, documentId: presence.documentId, @@ -389,5 +389,7 @@ function sendPresence(presence: PresenceState, socket: WebSocket): void { label: presence.label, anchor: presence.anchor, head: presence.head, + coordinateSpace: presence.coordinateSpace, + canvas: presence.canvas, })) } diff --git a/apps/server/src/routes/installation.ts b/apps/server/src/routes/installation.ts index 7828d7e..ab57286 100644 --- a/apps/server/src/routes/installation.ts +++ b/apps/server/src/routes/installation.ts @@ -13,7 +13,6 @@ export function createInstallationRoutes( const Status = Type.Object({ installationRequired: Type.Boolean(), activationPending: Type.Boolean(), - deploymentMode: Type.Union([Type.Literal('self-hosted'), Type.Literal('hosted'), Type.Null()]), serverName: Type.String(), }) @@ -23,9 +22,7 @@ export function createInstallationRoutes( config: { rateLimit: { max: 5, timeWindow: '1 minute' } }, schema: { body: Type.Object({ - deploymentMode: Type.Union([Type.Literal('self-hosted'), Type.Literal('hosted')]), serverName: Type.String({ minLength: 1, maxLength: 100, pattern: '.*\\S.*' }), - hostedRegistrationPolicy: Type.Optional(Type.Union([Type.Literal('disabled'), Type.Literal('public')])), administrator: Type.Object({ login: Type.String({ minLength: 1, maxLength: 200, pattern: '.*\\S.*' }), password: Type.String({ minLength: 8, maxLength: 1024 }), @@ -33,7 +30,6 @@ export function createInstallationRoutes( }), response: { 201: Type.Object({ - deploymentMode: Type.Union([Type.Literal('self-hosted'), Type.Literal('hosted')]), serverName: Type.String(), activationPending: Type.Literal(true), }), diff --git a/apps/server/src/routes/sync.ts b/apps/server/src/routes/sync.ts index 40853fa..e408db3 100644 --- a/apps/server/src/routes/sync.ts +++ b/apps/server/src/routes/sync.ts @@ -5,6 +5,7 @@ import { requireAuth } from '../auth/http-auth.js' import type { TokenService } from '../auth/tokens.js' import type { DurableSyncService } from '../durable-sync/service.js' import { syncObjectKinds } from '../sync/types.js' +import { workspaceCapabilities } from '../workspaces/capabilities.js' const Counter = Type.String({ pattern: '^\\d{1,19}$' }) const Hash = Type.String({ pattern: '^[A-Za-z0-9_-]{43}$' }) @@ -15,17 +16,88 @@ const ObjectVersionParams = Type.Object({ revision: Counter, }) const Kind = Type.Union(syncObjectKinds.map(kind => Type.Literal(kind))) +const Capability = Type.Union(workspaceCapabilities.map(capability => Type.Literal(capability))) const Envelope = { keyVersion: Type.Integer({ minimum: 1 }), ciphertext: Type.String({ pattern: '^[A-Za-z0-9_-]+$' }), ciphertextHash: Hash, } +const OptionalNameCiphertext = Type.Optional(Type.Union([ + Type.String({ maxLength: 16 * 1024 }), Type.Null(), +])) const CommandId = { commandId: Type.String({ format: 'uuid' }) } +const NullableUuid = Type.Union([Type.String({ format: 'uuid' }), Type.Null()]) +const NullableCounter = Type.Union([Counter, Type.Null()]) +const NullableString = Type.Union([Type.String(), Type.Null()]) +const SyncEpochField = { syncEpoch: Type.String({ format: 'uuid' }) } +const CommandResult = Type.Object({ + commandId: Type.String({ format: 'uuid' }), + status: Type.Union([Type.Literal('applied'), Type.Literal('conflict'), Type.Literal('rejected')]), + duplicate: Type.Boolean(), + sequence: Type.Optional(Counter), + revision: Type.Optional(Counter), + documentSequence: Type.Optional(Counter), + conflictId: Type.Optional(Type.String({ format: 'uuid' })), + code: Type.Optional(Type.String()), + retryable: Type.Optional(Type.Boolean()), + details: Type.Optional(Type.Record(Type.String(), Type.Unknown())), +}) +const SyncEvent = Type.Object({ + eventId: Type.String({ format: 'uuid' }), + commandId: Type.String({ format: 'uuid' }), + sourceDeviceId: Type.String({ format: 'uuid' }), + type: Type.String(), + objectId: NullableUuid, + documentId: NullableString, + sequence: Counter, + documentSequence: NullableCounter, + keyVersion: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]), + ciphertext: NullableString, + ciphertextHash: Type.Union([Hash, Type.Null()]), + metadata: Type.Record(Type.String(), Type.Unknown()), + createdAt: Type.String({ format: 'date-time' }), +}) +const ObjectVersion = Type.Object({ + workspaceId: Type.String({ format: 'uuid' }), + objectId: Type.String({ format: 'uuid' }), + revision: Counter, + sequence: Counter, + kind: Kind, + parentObjectId: NullableUuid, + nameCiphertext: NullableString, + nameBlindIndex: NullableString, + ciphertext: Type.String(), + ciphertextHash: Hash, + keyVersion: Type.Integer({ minimum: 1 }), + blobRefs: Type.Array(Hash), + sourceDeviceId: Type.String({ format: 'uuid' }), + deleted: Type.Boolean(), + createdAt: Type.String({ format: 'date-time' }), +}) +const Conflict = Type.Object({ + workspaceId: Type.String({ format: 'uuid' }), + conflictId: Type.String({ format: 'uuid' }), + objectId: Type.String({ format: 'uuid' }), + kind: Kind, + type: Type.String(), + status: Type.String(), + expectedRevision: NullableCounter, + expectedDocumentSequence: NullableCounter, + keyVersion: Type.Integer({ minimum: 1 }), + ciphertext: Type.String(), + ciphertextHash: Hash, + createdSequence: Counter, + resolvedSequence: NullableCounter, + resolvedByDeviceId: NullableUuid, + resolvedAt: Type.Union([Type.String({ format: 'date-time' }), Type.Null()]), + createdAt: Type.String({ format: 'date-time' }), + updatedAt: Type.String({ format: 'date-time' }), +}) const Command = Type.Union([ Type.Object({ ...CommandId, type: Type.Literal('upsert-object'), objectId: Type.String({ format: 'uuid' }), kind: Kind, parentObjectId: Type.Optional(Type.Union([Type.String({ format: 'uuid' }), Type.Null()])), - nameCiphertext: Type.Optional(Type.Union([Type.String(), Type.Null()])), + nameCiphertext: OptionalNameCiphertext, nameBlindIndex: Type.Optional(Type.Union([Hash, Type.Null()])), nameBlindIndexKeyVersion: Type.Optional(Type.Integer({ minimum: 1 })), nameConflictId: Type.Optional(Type.String({ format: 'uuid' })), @@ -38,7 +110,7 @@ const Command = Type.Union([ Type.Object({ ...CommandId, type: Type.Literal('delete-object'), objectId: Type.String({ format: 'uuid' }), kind: Kind, parentObjectId: Type.Optional(Type.Union([Type.String({ format: 'uuid' }), Type.Null()])), - nameCiphertext: Type.Optional(Type.Union([Type.String(), Type.Null()])), + nameCiphertext: OptionalNameCiphertext, baseRevision: Counter, expectedDocumentSequence: Counter, blobRefs: Type.Array(Hash, { maxItems: 1_000 }), conflictId: Type.String({ format: 'uuid' }), conflictCiphertext: Type.String({ pattern: '^[A-Za-z0-9_-]+$' }), conflictCiphertextHash: Hash, ...Envelope, @@ -47,11 +119,16 @@ const Command = Type.Union([ ...CommandId, type: Type.Literal('delete-subtree'), rootObjectId: Type.String({ format: 'uuid' }), conflictId: Type.String({ format: 'uuid' }), conflictKeyVersion: Type.Integer({ minimum: 1 }), conflictCiphertext: Type.String({ pattern: '^[A-Za-z0-9_-]+$' }), conflictCiphertextHash: Hash, - mutationIds: Type.Optional(Type.Array(Type.String({ format: 'uuid' }), { maxItems: 10_000 })), + mutationIds: Type.Optional(Type.Array(Type.String({ format: 'uuid' }), { maxItems: 2_000 })), objects: Type.Array(Type.Object({ objectId: Type.String({ format: 'uuid' }), kind: Kind, baseRevision: Counter, expectedDocumentSequence: Counter, blobRefs: Type.Array(Hash, { maxItems: 1_000 }), ...Envelope, - }), { minItems: 1, maxItems: 10_000 }), + }), { minItems: 1, maxItems: 2_000 }), + }), + Type.Object({ + ...CommandId, type: Type.Literal('initialize-document'), updateId: Type.String({ format: 'uuid' }), + documentId: Type.String({ minLength: 1, maxLength: 512 }), objectId: Type.String({ format: 'uuid' }), + kind: Kind, ...Envelope, }), Type.Object({ ...CommandId, type: Type.Literal('append-update'), updateId: Type.String({ format: 'uuid' }), @@ -77,7 +154,7 @@ const Command = Type.Union([ objectResolution: Type.Optional(Type.Object({ objectId: Type.String({ format: 'uuid' }), kind: Kind, parentObjectId: Type.Optional(Type.Union([Type.String({ format: 'uuid' }), Type.Null()])), - nameCiphertext: Type.Optional(Type.Union([Type.String(), Type.Null()])), + nameCiphertext: OptionalNameCiphertext, nameBlindIndex: Type.Optional(Type.Union([Hash, Type.Null()])), nameBlindIndexKeyVersion: Type.Optional(Type.Integer({ minimum: 1 })), blobRefs: Type.Optional(Type.Array(Hash, { maxItems: 1_000 })), ...Envelope, @@ -96,14 +173,86 @@ export function createSyncRoutes( auth: AuthService, ): FastifyPluginAsyncTypebox { return async function syncRoutes(app) { + app.get('/v1/workspaces/:workspaceId/sync/session', { + schema: { + params: WorkspaceParams, + querystring: Type.Object({ + protocolVersion: Type.Integer({ minimum: 1 }), + cursor: Type.Optional(Counter), + expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })), + }), + response: { + 200: Type.Object({ + protocol: Type.Object({ + requestedVersion: Type.Integer(), + selectedVersion: Type.Literal(1), + compatible: Type.Boolean(), + }), + workspace: Type.Object({ + id: Type.String({ format: 'uuid' }), + type: Type.Union([Type.Literal('account-data'), Type.Literal('library')]), + role: Type.Union([ + Type.Literal('owner'), Type.Literal('viewer'), Type.Literal('editor'), Type.Literal('manager'), + ]), + owner: Type.Boolean(), + capabilities: Type.Array(Capability), + }), + cursor: Type.Object({ + supplied: Counter, + state: Type.Union([Type.Literal('valid'), Type.Literal('ahead'), Type.Literal('expired')]), + acknowledged: Counter, + oldestAvailableSequence: Type.Union([Counter, Type.Null()]), + }), + latestSequence: Counter, + bootstrap: Type.Object({ + required: Type.Boolean(), + reason: Type.Union([ + Type.Literal('cursor_ahead'), Type.Literal('cursor_expired'), + Type.Literal('device_uninitialized'), Type.Literal('lag_too_large'), Type.Null(), + ]), + }), + limits: Type.Object({ + maxCommandsPerBatch: Type.Integer(), + maxEventsPerPage: Type.Integer(), + maxBootstrapObjectsPerPage: Type.Integer(), + maxDocumentUpdatesPerPage: Type.Integer(), + maxObjectBytes: Type.Integer(), + }), + keyVersions: Type.Array(Type.Object({ + keyVersion: Type.Integer({ minimum: 1 }), + createdAt: Type.String({ format: 'date-time' }), + })), + syncEpoch: Type.String({ format: 'uuid' }), + websocketUrl: Type.String({ format: 'uri' }), + }), + }, + }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + const session = await sync.session( + claims.accountId, + claims.deviceId, + request.params.workspaceId, + request.query.cursor ?? '0', + request.query.protocolVersion, + request.query.expectedSyncEpoch, + ) + const websocketScheme = request.protocol === 'https' ? 'wss' : 'ws' + return { + ...session, + websocketUrl: `${websocketScheme}://${request.host}/v1/sync/events`, + } + }) + app.post('/v1/workspaces/:workspaceId/sync/commands', { config: { rateLimit: { max: 600, timeWindow: '1 minute' } }, schema: { params: WorkspaceParams, body: Type.Object({ commands: Type.Array(Command, { minItems: 1, maxItems: 100 }), - expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })), + expectedSyncEpoch: Type.String({ format: 'uuid' }), }), + response: { 200: Type.Object({ results: Type.Array(CommandResult), ...SyncEpochField }) }, }, }, async request => { const claims = await requireAuth(request, tokens, auth) @@ -118,10 +267,31 @@ export function createSyncRoutes( return result }) + app.post('/v1/workspaces/:workspaceId/sync/ack', { + schema: { + params: WorkspaceParams, + body: Type.Object({ + through: Counter, + expectedSyncEpoch: Type.String({ format: 'uuid' }), + }), + response: { 200: Type.Object({ acknowledgedSequence: Counter, ...SyncEpochField }) }, + }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return sync.acknowledge( + claims.accountId, claims.deviceId, request.params.workspaceId, + request.body.through, request.body.expectedSyncEpoch, + ) + }) + app.get('/v1/workspaces/:workspaceId/sync/events', { schema: { params: WorkspaceParams, - querystring: Type.Object({ after: Type.Optional(Counter), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })), expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })) }), + querystring: Type.Object({ after: Type.Optional(Counter), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })), expectedSyncEpoch: Type.String({ format: 'uuid' }) }), + response: { 200: Type.Object({ + events: Type.Array(SyncEvent), nextCursor: Counter, latestSequence: Counter, + hasMore: Type.Boolean(), ...SyncEpochField, + }) }, }, }, async request => { const claims = await requireAuth(request, tokens, auth) @@ -129,7 +299,15 @@ export function createSyncRoutes( }) app.get('/v1/workspaces/:workspaceId/sync/objects/:objectId/versions/:revision', { - schema: { params: ObjectVersionParams, querystring: Type.Object({ expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })) }) }, + schema: { + params: ObjectVersionParams, + querystring: Type.Object({ expectedSyncEpoch: Type.String({ format: 'uuid' }) }), + response: { 200: Type.Object({ + object: Type.Intersect([ObjectVersion, Type.Object({ currentRevision: NullableCounter })]), + resources: Type.Array(Type.Intersect([ObjectVersion, Type.Object({ currentRevision: NullableCounter })])), + ...SyncEpochField, + }) }, + }, }, async request => { const claims = await requireAuth(request, tokens, auth) return sync.objectVersion( @@ -149,8 +327,12 @@ export function createSyncRoutes( querystring: Type.Object({ before: Type.Optional(Counter), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })), - expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })), + expectedSyncEpoch: Type.String({ format: 'uuid' }), }), + response: { 200: Type.Object({ + versions: Type.Array(ObjectVersion), nextBefore: NullableCounter, + hasMore: Type.Boolean(), ...SyncEpochField, + }) }, }, }, async request => { const claims = await requireAuth(request, tokens, auth) @@ -167,8 +349,26 @@ export function createSyncRoutes( bootstrapId: Type.Optional(Type.String({ format: 'uuid' })), afterObjectId: Type.Optional(Type.String({ format: 'uuid' })), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })), - expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })), + expectedSyncEpoch: Type.String({ format: 'uuid' }), }), + response: { 200: Type.Object({ + bootstrapId: Type.String({ format: 'uuid' }), + snapshotSequence: Counter, + objects: Type.Array(Type.Object({ + objectId: Type.String({ format: 'uuid' }), kind: Kind, parentObjectId: NullableUuid, + nameCiphertext: NullableString, nameBlindIndexPresent: Type.Boolean(), currentRevision: Counter, + ciphertext: Type.String(), ciphertextHash: Hash, keyVersion: Type.Integer({ minimum: 1 }), + blobRefs: Type.Array(Hash), deletedAt: Type.Union([Type.String({ format: 'date-time' }), Type.Null()]), + document: Type.Union([Type.Object({ + documentId: Type.String(), latestDocumentSequence: Counter, checkpointDocumentSequence: Counter, + checkpointId: NullableUuid, checkpointKeyVersion: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()]), + checkpointCiphertext: NullableString, checkpointCiphertextHash: Type.Union([Hash, Type.Null()]), + materializedRevision: NullableCounter, + }), Type.Null()]), + })), + conflicts: Type.Array(Conflict), nextObjectId: NullableUuid, + hasMore: Type.Boolean(), ...SyncEpochField, + }) }, }, }, async request => { const claims = await requireAuth(request, tokens, auth) @@ -181,7 +381,21 @@ export function createSyncRoutes( app.get('/v1/workspaces/:workspaceId/documents/:documentId/updates', { schema: { params: Type.Object({ workspaceId: Type.String({ format: 'uuid' }), documentId: Type.String({ minLength: 1, maxLength: 512 }) }), - querystring: Type.Object({ after: Type.Optional(Counter), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })), expectedSyncEpoch: Type.Optional(Type.String({ format: 'uuid' })) }), + querystring: Type.Object({ after: Type.Optional(Counter), limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 1_000 })), expectedSyncEpoch: Type.String({ format: 'uuid' }) }), + response: { 200: Type.Object({ + updates: Type.Array(Type.Object({ + workspaceId: Type.String({ format: 'uuid' }), documentId: Type.String(), documentSequence: Counter, + updateId: Type.String({ format: 'uuid' }), eventSequence: Counter, + sourceDeviceId: Type.String({ format: 'uuid' }), keyVersion: Type.Integer({ minimum: 1 }), + ciphertext: Type.String(), ciphertextHash: Hash, createdAt: Type.String({ format: 'date-time' }), + })), + checkpoint: Type.Union([Type.Object({ + objectId: Type.String({ format: 'uuid' }), documentSequence: Counter, + checkpointId: Type.String({ format: 'uuid' }), keyVersion: Type.Integer({ minimum: 1 }), + ciphertext: Type.String(), ciphertextHash: Hash, + }), Type.Null()]), + nextDocumentSequence: Counter, hasMore: Type.Boolean(), ...SyncEpochField, + }) }, }, }, async request => { const claims = await requireAuth(request, tokens, auth) diff --git a/apps/server/src/routes/web-admin.ts b/apps/server/src/routes/web-admin.ts index e12027b..398ab63 100644 --- a/apps/server/src/routes/web-admin.ts +++ b/apps/server/src/routes/web-admin.ts @@ -162,7 +162,7 @@ export function createWebAdminRoutes( app.get('/v1/web/admin/capabilities', { schema: { response: { 200: Type.Object({ - deploymentMode: Type.Union([Type.Literal('self-hosted'), Type.Literal('hosted')]), + deploymentMode: Type.Literal('self-hosted'), generatedAt: Timestamp, capabilities: Type.Array(Type.Object({ id: Type.String(), lifecycle: Type.Union([Type.Literal('stable'), Type.Literal('experimental')]), @@ -176,13 +176,12 @@ export function createWebAdminRoutes( const session = await requireWebSession(request.cookies[WEB_SESSION_COOKIE], webSessions) await admin.assertAdmin(session.accountId) const experimental = new Set([ - 'identity.email', 'identity.emailVerification', 'identity.passwordReset', 'risk.advanced', - 'usage.enforcement', 'billing.subscription', 'compliance.requests', 'support.cases', + 'identity.email', 'identity.emailVerification', 'identity.passwordReset', 'operations.unifiedBackup', 'operations.upgradeAssistant', 'operations.preserveRestore', ]) const rows = capabilityIds.map((id) => { const explanation = capabilities?.explain(id) ?? { - id, available: false, deploymentMode: _config.deploymentMode, requestedBy: 'default' as const, + id, available: false, deploymentMode: 'self-hosted' as const, requestedBy: 'default' as const, reasons: ['capability_registry_unavailable'], dependencies: [], } const status = explanation.available @@ -200,7 +199,7 @@ export function createWebAdminRoutes( } }) return { - deploymentMode: capabilities?.explain(capabilityIds[0]).deploymentMode ?? _config.deploymentMode, + deploymentMode: 'self-hosted' as const, generatedAt: new Date(), capabilities: rows, } diff --git a/apps/server/src/routes/web-auth.ts b/apps/server/src/routes/web-auth.ts index 7342243..3a27249 100644 --- a/apps/server/src/routes/web-auth.ts +++ b/apps/server/src/routes/web-auth.ts @@ -10,7 +10,7 @@ import type { WebAccountSession, WebSessionService } from '../auth/web-session-s import type { AdminService } from '../admin/service.js' import type { DeploymentService } from '../deployment/service.js' import type { RiskService } from '../risk/service.js' -import { NullableTimestamp, Timestamp } from './api-schemas.js' +import { CounterString, NullableTimestamp, Timestamp } from './api-schemas.js' export const WEB_SESSION_COOKIE = 'notegen_session' export const WEB_CSRF_COOKIE = 'notegen_csrf' @@ -134,6 +134,8 @@ export function createWebAuthRoutes( id: Type.String({ format: 'uuid' }), name: Type.String(), platform: Type.String(), encryptionPublicKey: Type.Union([Type.String(), Type.Null()]), lastSeenAt: Timestamp, createdAt: Timestamp, revokedAt: NullableTimestamp, current: Type.Boolean(), + syncStatus: Type.Union([Type.Literal('caught-up'), Type.Literal('behind'), Type.Literal('never-acknowledged')]), + pendingEventCount: CounterString, acknowledgedAt: NullableTimestamp, })) } } }, async (request) => { const session = await requireWebSession(request.cookies[WEB_SESSION_COOKIE], webSessions) return auth.listDevices(session.accountId) @@ -196,6 +198,28 @@ export function createWebAuthRoutes( return { revoked: await webSessions.destroyOthers(session.accountId, session.sessionId) } }) + app.delete('/v1/web/account', { + config: { rateLimit: { max: 3, timeWindow: '1 hour' } }, + schema: { + body: Type.Object({ + password: Type.String({ minLength: 8, maxLength: 1_024 }), + confirmation: Type.Literal('DELETE'), + }), + response: { 202: Type.Object({ purgeAfter: Timestamp }) }, + }, + }, async (request, reply) => { + const session = await requireWebSession(request.cookies[WEB_SESSION_COOKIE], webSessions) + requireCsrf(request.headers['x-csrf-token'], request.cookies[WEB_CSRF_COOKIE], session, webSessions) + const purgeAfter = await auth.requestAccountDeletion( + session.accountId, request.body.password, config.tombstoneRetentionDays, + ) + await webSessions.destroyForAccount(session.accountId) + clearSessionCookies(config, reply) + await admin?.recordAudit(session.accountId, 'account.deletion-request', 'account', session.accountId) + .catch((error: unknown) => request.log.warn({ err: error }, 'Failed to record account deletion request')) + return reply.status(202).send({ purgeAfter }) + }) + app.delete('/v1/web/devices/:deviceId', { schema: { params: Type.Object({ deviceId: Type.String({ format: 'uuid' }) }), diff --git a/apps/server/src/routes/web-dashboard.ts b/apps/server/src/routes/web-dashboard.ts index 7501964..1d81a64 100644 --- a/apps/server/src/routes/web-dashboard.ts +++ b/apps/server/src/routes/web-dashboard.ts @@ -8,18 +8,6 @@ import { requireCsrf, requireWebSession, WEB_CSRF_COOKIE, WEB_SESSION_COOKIE, } from './web-auth.js' -const Activity = Type.Object({ - sequence: CounterString, - kind: Type.String(), - changeType: Type.Union([Type.Literal('upsert'), Type.Literal('delete')]), - createdAt: Timestamp, - device: Type.Object({ - id: Type.String({ format: 'uuid' }), - name: Type.String(), - platform: Type.String(), - }), -}) - export function createWebDashboardRoutes( workspaces: WorkspaceService, webSessions: WebSessionService, @@ -41,13 +29,33 @@ export function createWebDashboardRoutes( encryptionMode: Type.Union([ Type.Literal('managed'), Type.Literal('e2ee'), Type.Literal('mixed'), Type.Null(), ]), + storageUsage: Type.Union([Type.Object({ + activeObjectBytes: CounterString, + activeCrdtBytes: CounterString, + activeBlobBytes: CounterString, + reservedBlobBytes: CounterString, + retainedBytes: CounterString, + }), Type.Null()]), kinds: Type.Array(Type.Object({ kind: Type.String(), activeCount: Type.Integer(), deletedCount: Type.Integer(), updatedAt: Timestamp, })), - recentActivity: Type.Array(Activity), + activityTimeline: Type.Array(Type.Object({ + startedAt: Timestamp, + updates: Type.Integer(), + deletes: Type.Integer(), + kinds: Type.Array(Type.Object({ + kind: Type.String(), + updates: Type.Integer(), + deletes: Type.Integer(), + })), + })), + activityKinds: Type.Array(Type.Object({ + kind: Type.String(), + count: Type.Integer(), + })), }), }, }, @@ -62,7 +70,9 @@ export function createWebDashboardRoutes( 200: Type.Array(Type.Object({ id: Type.String({ format: 'uuid' }), nameCiphertext: Type.String(), + type: Type.Union([Type.Literal('account-data'), Type.Literal('library')]), isDefault: Type.Boolean(), + isNoteGenDefault: Type.Boolean(), latestSequence: CounterString, latestKeyVersion: Type.Integer(), encryptionMode: Type.Union([Type.Literal('managed'), Type.Literal('e2ee')]), @@ -96,5 +106,24 @@ export function createWebDashboardRoutes( .catch((error: unknown) => request.log.warn({ err: error }, 'Failed to record workspace deletion audit')) return reply.status(204).send(null) }) + + app.post('/v1/web/workspaces/:workspaceId/restore', { + schema: { + params: Type.Object({ workspaceId: Type.String({ format: 'uuid' }) }), + response: { 204: Type.Null() }, + }, + }, async (request, reply) => { + const session = await requireWebSession(request.cookies[WEB_SESSION_COOKIE], webSessions) + requireCsrf( + request.headers['x-csrf-token'], + request.cookies[WEB_CSRF_COOKIE], + session, + webSessions, + ) + await workspaces.restore(session.accountId, request.params.workspaceId) + await admin?.recordAudit(session.accountId, 'workspace.restore', 'workspace', request.params.workspaceId) + .catch((error: unknown) => request.log.warn({ err: error }, 'Failed to record workspace restoration audit')) + return reply.status(204).send(null) + }) } } diff --git a/apps/server/src/routes/workspace-collaboration.ts b/apps/server/src/routes/workspace-collaboration.ts new file mode 100644 index 0000000..7fd3c22 --- /dev/null +++ b/apps/server/src/routes/workspace-collaboration.ts @@ -0,0 +1,164 @@ +import type { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox' +import { Type } from '@sinclair/typebox' +import type { AuthService } from '../auth/service.js' +import { requireAuth } from '../auth/http-auth.js' +import type { TokenService } from '../auth/tokens.js' +import type { WorkspaceCollaborationService } from '../workspaces/collaboration-service.js' +import { workspaceCapabilities } from '../workspaces/capabilities.js' + +const WorkspaceParams = Type.Object({ workspaceId: Type.String({ format: 'uuid' }) }) +const MemberParams = Type.Object({ + workspaceId: Type.String({ format: 'uuid' }), + accountId: Type.String({ format: 'uuid' }), +}) +const InvitationParams = Type.Object({ + workspaceId: Type.String({ format: 'uuid' }), + invitationId: Type.String({ format: 'uuid' }), +}) +const Role = Type.Union([Type.Literal('viewer'), Type.Literal('editor'), Type.Literal('manager')]) +const Capability = Type.Union(workspaceCapabilities.map(capability => Type.Literal(capability))) +const Membership = Type.Object({ + role: Role, + capabilities: Type.Optional(Type.Array(Capability, { uniqueItems: true, maxItems: workspaceCapabilities.length })), +}) +const NullableTimestamp = Type.Union([Type.String({ format: 'date-time' }), Type.Null()]) +const MemberResponse = Type.Object({ + accountId: Type.String({ format: 'uuid' }), + login: Type.String(), + role: Type.Union([Type.Literal('owner'), Role]), + capabilities: Type.Array(Capability), + joinedAt: NullableTimestamp, + updatedAt: NullableTimestamp, +}) +const InvitationResponse = Type.Object({ + id: Type.String({ format: 'uuid' }), + workspaceId: Type.String({ format: 'uuid' }), + kind: Type.Union([Type.Literal('account'), Type.Literal('link')]), + inviteeAccountId: Type.Union([Type.String({ format: 'uuid' }), Type.Null()]), + tokenHint: Type.Union([Type.String(), Type.Null()]), + role: Role, + capabilities: Type.Array(Capability), + status: Type.Union([ + Type.Literal('pending'), Type.Literal('accepted'), Type.Literal('revoked'), Type.Literal('expired'), + ]), + invitedByAccountId: Type.String({ format: 'uuid' }), + acceptedByAccountId: Type.Union([Type.String({ format: 'uuid' }), Type.Null()]), + expiresAt: Type.String({ format: 'date-time' }), + acceptedAt: NullableTimestamp, + revokedAt: NullableTimestamp, + createdAt: Type.String({ format: 'date-time' }), + updatedAt: Type.String({ format: 'date-time' }), +}) + +export function createWorkspaceCollaborationRoutes( + collaboration: WorkspaceCollaborationService, + tokens: TokenService, + auth: AuthService, +): FastifyPluginAsyncTypebox { + return async function workspaceCollaborationRoutes(app) { + app.get('/v1/workspaces/:workspaceId/members', { + schema: { params: WorkspaceParams, response: { 200: Type.Array(MemberResponse) } }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return collaboration.listMembers(claims.accountId, request.params.workspaceId) + }) + + app.get('/v1/workspaces/:workspaceId/invitations', { + schema: { params: WorkspaceParams, response: { 200: Type.Array(InvitationResponse) } }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return collaboration.listInvitations(claims.accountId, request.params.workspaceId) + }) + + app.get('/v1/workspace-invitations', { + schema: { + response: { 200: Type.Array(Type.Intersect([InvitationResponse, Type.Object({ + workspaceNameCiphertext: Type.String(), + inviterLogin: Type.String(), + })])) }, + }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return collaboration.listPendingForAccount(claims.accountId) + }) + + app.post('/v1/workspaces/:workspaceId/invitations/account', { + schema: { + params: WorkspaceParams, + body: Type.Intersect([Membership, Type.Object({ + login: Type.String({ minLength: 1, maxLength: 320 }), + expiresInSeconds: Type.Optional(Type.Integer({ minimum: 300, maximum: 30 * 24 * 60 * 60 })), + })]), + response: { 201: Type.Intersect([InvitationResponse, Type.Object({ inviteeLogin: Type.String() })]) }, + }, + }, async (request, reply) => { + const claims = await requireAuth(request, tokens, auth) + const result = await collaboration.inviteAccount( + claims.accountId, request.params.workspaceId, request.body.login, + request.body, request.body.expiresInSeconds, + ) + return reply.status(201).send(result) + }) + + app.post('/v1/workspaces/:workspaceId/invitations/link', { + schema: { + params: WorkspaceParams, + body: Type.Intersect([Membership, Type.Object({ + expiresInSeconds: Type.Optional(Type.Integer({ minimum: 300, maximum: 30 * 24 * 60 * 60 })), + })]), + response: { 201: Type.Intersect([InvitationResponse, Type.Object({ token: Type.String() })]) }, + }, + }, async (request, reply) => { + const claims = await requireAuth(request, tokens, auth) + const result = await collaboration.createLink( + claims.accountId, request.params.workspaceId, request.body, request.body.expiresInSeconds, + ) + return reply.status(201).send(result) + }) + + app.post('/v1/workspace-invitations/:invitationId/accept', { + schema: { + params: Type.Object({ invitationId: Type.String({ format: 'uuid' }) }), + response: { 200: Type.Object({ workspaceId: Type.String({ format: 'uuid' }) }) }, + }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return collaboration.acceptAccountInvitation(claims.accountId, request.params.invitationId) + }) + + app.post('/v1/workspace-invitations/accept-link', { + schema: { + body: Type.Object({ token: Type.String({ minLength: 32, maxLength: 128 }) }), + response: { 200: Type.Object({ workspaceId: Type.String({ format: 'uuid' }) }) }, + }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return collaboration.acceptLink(claims.accountId, request.body.token) + }) + + app.patch('/v1/workspaces/:workspaceId/members/:accountId', { + schema: { params: MemberParams, body: Membership, response: { 200: MemberResponse } }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return collaboration.updateMember( + claims.accountId, request.params.workspaceId, request.params.accountId, request.body, + ) + }) + + app.delete('/v1/workspaces/:workspaceId/members/:accountId', { + schema: { params: MemberParams, response: { 204: Type.Null() } }, + }, async (request, reply) => { + const claims = await requireAuth(request, tokens, auth) + await collaboration.removeMember(claims.accountId, request.params.workspaceId, request.params.accountId) + return reply.status(204).send(null) + }) + + app.delete('/v1/workspaces/:workspaceId/invitations/:invitationId', { + schema: { params: InvitationParams, response: { 204: Type.Null() } }, + }, async (request, reply) => { + const claims = await requireAuth(request, tokens, auth) + await collaboration.revokeInvitation(claims.accountId, request.params.workspaceId, request.params.invitationId) + return reply.status(204).send(null) + }) + } +} diff --git a/apps/server/src/routes/workspaces.ts b/apps/server/src/routes/workspaces.ts index dd53225..f607fdc 100644 --- a/apps/server/src/routes/workspaces.ts +++ b/apps/server/src/routes/workspaces.ts @@ -54,9 +54,10 @@ export function createWorkspaceRoutes( app.post('/v1/workspaces', { schema: { headers: IdempotencyKeyHeaders, - body: Type.Intersect([KeyBody, Type.Object({ + body: Type.Object({ nameCiphertext: Type.String({ minLength: 1, maxLength: 8_192 }), - })]), + managedKey: Type.String({ minLength: 40, maxLength: 128 }), + }), response: { 200: Type.Object({ id: Type.String({ format: 'uuid' }), @@ -78,7 +79,7 @@ export function createWorkspaceRoutes( const claims = await requireAuth(request, tokens, auth) const idempotencyKey = typeof request.headers['idempotency-key'] === 'string' ? request.headers['idempotency-key'] : undefined - const result = await workspaces.create(claims.accountId, { + const result = await workspaces.createManagedLibrary(claims.accountId, { ...request.body, ...(idempotencyKey === undefined ? {} : { idempotencyKey }), }) @@ -107,6 +108,13 @@ export function createWorkspaceRoutes( response: { 200: Type.Array(Type.Object({ id: Type.String({ format: 'uuid' }), + ownerAccountId: Type.String({ format: 'uuid' }), + type: Type.Union([Type.Literal('account-data'), Type.Literal('library')]), + owner: Type.Boolean(), + role: Type.Union([ + Type.Literal('owner'), Type.Literal('viewer'), Type.Literal('editor'), Type.Literal('manager'), + ]), + capabilities: Type.Array(Type.String()), nameCiphertext: Type.String(), latestSequence: CounterString, latestKeyVersion: Type.Integer(), @@ -243,6 +251,21 @@ export function createWorkspaceRoutes( return reply.status(204).send(null) }) + app.patch('/v1/workspaces/:workspaceId', { + schema: { + params: WorkspaceParams, + body: Type.Object({ nameCiphertext: Type.String({ minLength: 1, maxLength: 8_192 }) }), + response: { 200: Type.Object({ + id: Type.String({ format: 'uuid' }), + nameCiphertext: Type.String(), + updatedAt: Timestamp, + }) }, + }, + }, async request => { + const claims = await requireAuth(request, tokens, auth) + return workspaces.rename(claims.accountId, request.params.workspaceId, request.body.nameCiphertext) + }) + app.post('/v1/workspaces/:workspaceId/restore', { schema: { params: WorkspaceParams, response: { 204: Type.Null() } }, }, async (request, reply) => { diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 59bc531..8898564 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -28,29 +28,19 @@ import { BootstrapService } from './bootstrap/service.js' import { InvitationService } from './invitations/service.js' import { WebStepUpService } from './step-up/service.js' import { IdentityService } from './identity/service.js' -import { EmailIdentityService } from './identity/email-service.js' import { UsageService } from './usage/service.js' -import { EntitlementService } from './billing/service.js' -import { createBillingProvider } from './billing/provider.js' import { createMailProvider } from './mail/provider.js' import { MailOutboxService } from './mail/outbox-service.js' import { MailSecretPayloadService } from './mail/secret-payload-service.js' import { MailAdminService } from './mail/admin-service.js' import { MailOutboxWorker } from './mail/outbox-worker.js' -import { ComplianceService } from './compliance/service.js' -import { DeletionService } from './compliance/deletion-service.js' -import { LegalHoldService } from './compliance/legal-hold-service.js' import { RestoreFenceService } from './restore-fence/service.js' import { RiskService } from './risk/service.js' import { MaintenanceCoordinator } from './maintenance/coordinator.js' -import { SupportService } from './support/service.js' import { BackupInventoryService } from './backup/inventory-service.js' -import { StaffService } from './staff/service.js' -import { StaffSessionService } from './staff/session-service.js' import { AccountServiceAudit } from './audit/service.js' -import { FilesystemDeletionLedgerStore } from './compliance/deletion-ledger-store.js' -import { DeletionLedgerReplayService } from './compliance/deletion-ledger-replay-service.js' import { InstallationService } from './installation/service.js' +import { WorkspaceCollaborationService } from './workspaces/collaboration-service.js' async function main(): Promise { const config = loadConfig() @@ -77,7 +67,7 @@ async function main(): Promise { database = createDatabase(config) await assertMigrationCompatibility(database) const installationProbe = new InstallationService(database, config, true) - const persistedInstallation = await installationProbe.persistedSettings() + let persistedInstallation = await installationProbe.persistedSettings() if (persistedInstallation === undefined) { let finishInstallation!: () => void const installationCompleted = new Promise((resolve) => { finishInstallation = resolve }) @@ -104,19 +94,19 @@ async function main(): Promise { await main() return } - applyPersistedDeploymentProfile( - config, - persistedInstallation.deploymentMode, - persistedInstallation.registrationPolicy === 'public' ? 'public' : 'disabled', - ) + if (persistedInstallation.deploymentMode !== 'self-hosted') { + await installationProbe.migrateLegacyOperationsInstallation() + persistedInstallation = await installationProbe.persistedSettings() + if (persistedInstallation?.deploymentMode !== 'self-hosted') { + throw new Error('Legacy operations mode migration did not complete') + } + } + applyPersistedDeploymentProfile(config, 'self-hosted') const installation = new InstallationService(database, config, false) const deployment = new DeploymentService(database, config) await deployment.initialize() - // A readiness-only failure still leaves a listening process available to - // a misconfigured proxy or direct client. Deployment mode determines the - // business isolation boundary, so a persisted/configured mode mismatch - // (or the legacy hosted public-registration hazard) must prevent service - // assembly and socket binding altogether. + // Keep deployment safety failures ahead of service assembly and socket binding so a + // misconfigured instance cannot become reachable through a proxy. const deploymentSafetyFailure = deployment.getSafetyFailure() if (deploymentSafetyFailure !== undefined) { throw new Error(`Deployment safety gate is closed: ${deploymentSafetyFailure}`) @@ -128,53 +118,23 @@ async function main(): Promise { await restoreFence.reconcile() const maintenanceCoordinator = new MaintenanceCoordinator(database) await maintenanceCoordinator.getSnapshot() - // Restore credential-review restrictions are a self-hosted recovery - // boundary too; the durable restriction evaluator has no hosted-provider - // dependency and must therefore exist in both deployment modes. const accountAudit = new AccountServiceAudit(database) - const staff = config.deploymentMode === 'hosted' ? new StaffService(database) : undefined - const risk = new RiskService(database, config.authSecret, config, staff, accountAudit) - const bootstrap = config.deploymentMode === 'self-hosted' - ? new BootstrapService(database, config, deployment) - : undefined - await bootstrap?.initialize() + const risk = new RiskService(database, config.authSecret, config, undefined, accountAudit) + const bootstrap = new BootstrapService(database, config, deployment) + await bootstrap.initialize() const identities = new IdentityService(database) await identities.backfillLegacyIdentities() const mailOutbox = new MailOutboxService(database) const mailSecrets = new MailSecretPayloadService(database, config.authSecret) const mailProvider = createMailProvider(config) - const mailAdmin = config.deploymentMode === 'self-hosted' && mailProvider !== undefined + const mailAdmin = mailProvider !== undefined ? new MailAdminService(database, config, mailOutbox, mailSecrets, mailProvider) : undefined const invitations = new InvitationService(database, config, deployment, { outbox: mailOutbox, secrets: mailSecrets, deliveryAvailable: () => capabilities.resolvePublic()['mail.delivery'], }) - const emailIdentities = config.deploymentMode === 'hosted' - ? new EmailIdentityService(database, config, mailOutbox, mailSecrets, { - emailVerification: capabilities.resolvePublic()['identity.emailVerification'], - passwordReset: capabilities.resolvePublic()['identity.passwordReset'], - }, risk) : undefined const usage = new UsageService(database) - const entitlements = config.deploymentMode === 'hosted' - ? new EntitlementService(database, config, staff, accountAudit) : undefined - const usageHardEnforcementActive = config.usageEnforcement === 'hard' - && capabilities.resolvePublic()['usage.enforcement'] && entitlements !== undefined - const hardUsageLimitResolver = usageHardEnforcementActive && entitlements !== undefined - ? async (accountId: string) => entitlementLimit(await entitlements.getEffective(accountId), 'storage_bytes') - : undefined - const hardDeviceLimitResolver = usageHardEnforcementActive && entitlements !== undefined - ? async (accountId: string) => entitlementLimit(await entitlements.getEffective(accountId), 'devices') - : undefined - const hardWorkspaceLimitResolver = usageHardEnforcementActive && entitlements !== undefined - ? async (accountId: string) => entitlementLimit(await entitlements.getEffective(accountId), 'workspaces') - : undefined - const billingProvider = createBillingProvider(config) - const compliance = config.deploymentMode === 'hosted' ? new ComplianceService(database, config) : undefined - const legalHolds = config.deploymentMode === 'hosted' && staff !== undefined ? new LegalHoldService(database, config, staff, accountAudit) : undefined - const deletion = config.deploymentMode === 'hosted' ? new DeletionService(database, config, legalHolds, usage, risk) : undefined - const support = config.deploymentMode === 'hosted' ? new SupportService(database, config, staff, accountAudit) : undefined - const staffSessions = staff === undefined ? undefined : new StaffSessionService(database, staff, accountAudit) const instanceId = await getOrCreateInstanceId(database) const syncEpoch = await getOrCreateSyncEpoch(database) const blobStorage: BlobStorage = config.blobStorageDriver === 's3' @@ -188,38 +148,27 @@ async function main(): Promise { const tokens = new TokenService(config.authSecret, config.publicBaseUrl) const totp = new TotpService(config.authSecret) - const auth = new AuthService(database, tokens, totp, risk, - config.deploymentMode === 'hosted' ? usage : undefined, hardDeviceLimitResolver) + const auth = new AuthService(database, tokens, totp, risk) const webSessions = new WebSessionService(database, risk) const stepUps = new WebStepUpService(database, config.authSecret) const deviceAuthorizations = new DeviceAuthorizationService(database, auth) const devicePairings = new DevicePairingService(database, auth) - // Instance administration is a self-hosted authority. Hosted operations - // use the separate staff realm; assembling this service there would leave - // the customer-admin route surface present even when no customer can - // legitimately receive isAdmin. - const admin = config.deploymentMode === 'self-hosted' - ? new AdminService(database, blobStorage, usage, hardWorkspaceLimitResolver, config, deployment) - : undefined - await admin?.recoverInterruptedJobs() + const admin = new AdminService(database, blobStorage, usage, undefined, config, deployment) + await admin.recoverInterruptedJobs() notifier = new PostgresChangeNotifier(database.sql) await notifier.initialize() - const workspaces = new WorkspaceService(database, notifier, - config.deploymentMode === 'hosted' ? usage : undefined, hardWorkspaceLimitResolver) + const workspaces = new WorkspaceService(database, notifier) + const workspaceCollaboration = new WorkspaceCollaborationService(database, workspaces, notifier) const syncProtocol = new DurableSyncService(database, workspaces, notifier, () => config.maxObjectBytes, - config.deploymentMode === 'hosted' ? usage : undefined, hardUsageLimitResolver, syncEpoch) + undefined, undefined, syncEpoch) const blobService = new BlobService( database, workspaces, blobStorage, () => config.maxBlobBytes, config.blobPartBytes, - config.deploymentMode === 'hosted' ? usage : undefined, - hardUsageLimitResolver, syncEpoch, + undefined, undefined, syncEpoch, ) - const deletionLedger = config.deploymentMode === 'hosted' ? new FilesystemDeletionLedgerStore(config.deletionLedgerPath) : undefined - await deletionLedger?.initialize() - await (deletionLedger === undefined ? undefined : new DeletionLedgerReplayService(database, config, deletionLedger).reconcile()) const maintenance = new MaintenanceService( database, blobStorage, config, maintenanceCoordinator, - config.deploymentMode === 'hosted' ? usage : undefined, - deletionLedger, + undefined, + undefined, ) app = await buildApp(config, { @@ -231,6 +180,7 @@ async function main(): Promise { tokens, auth, workspaces, + workspaceCollaboration, syncProtocol, notifier, blobs: blobService, @@ -246,30 +196,16 @@ async function main(): Promise { bootstrap, invitations, identities, - emailIdentities, usage, - usageHardEnforcementActive, - entitlements, - billingProvider, mailProvider, mailOutbox, mailSecrets, mailAdmin, - compliance, - deletion, - legalHolds, risk, - support, - staff, - staffSessions, maintenanceCoordinator, accountAudit, installation, }) - // Hosted internal-test drains to the redacted LogMailProvider; a - // self-hosted instance drains only when its explicitly configured SMTP - // provider assembled successfully. In both cases the durable worker, not - // an HTTP request, owns network delivery and retry semantics. if (mailProvider !== undefined) { stopMailOutbox = new MailOutboxWorker(database, mailOutbox, mailProvider, mailSecrets, { error: (bindings, message) => app?.log.error(bindings, message), @@ -291,8 +227,3 @@ async function main(): Promise { } await main() - -function entitlementLimit(entitlements: { limits: Record }, metric: string): bigint | null { - const value = entitlements.limits[metric] - return value === undefined || value === null ? null : BigInt(value) -} diff --git a/apps/server/src/services.ts b/apps/server/src/services.ts index 39eaeea..1e608ec 100644 --- a/apps/server/src/services.ts +++ b/apps/server/src/services.ts @@ -36,6 +36,7 @@ import type { StaffSessionService } from './staff/session-service.js' import type { WebStepUpService } from './step-up/service.js' import type { AccountServiceAudit } from './audit/service.js' import type { InstallationService } from './installation/service.js' +import type { WorkspaceCollaborationService } from './workspaces/collaboration-service.js' export interface ServiceDependencies { readonly version: string @@ -47,6 +48,7 @@ export interface ServiceDependencies { readonly auth?: AuthService readonly tokens?: TokenService readonly workspaces?: WorkspaceService + readonly workspaceCollaboration?: WorkspaceCollaborationService readonly notifier?: ChangeNotifier readonly blobs?: BlobService readonly webSessions?: WebSessionService diff --git a/apps/server/src/sync/notifier.ts b/apps/server/src/sync/notifier.ts index 5a0f5b9..66b7655 100644 --- a/apps/server/src/sync/notifier.ts +++ b/apps/server/src/sync/notifier.ts @@ -91,6 +91,12 @@ function parseNotice(payload: string): SyncNotice | null { if (candidate.type === 'workspace.state-changed' && typeof candidate.deleted === 'boolean') { return { type: 'workspace.state-changed', workspaceId: candidate.workspaceId, deleted: candidate.deleted } } + if (candidate.type === 'workspace.members-changed') { + return { type: 'workspace.members-changed', workspaceId: candidate.workspaceId } + } + if (candidate.type === 'workspace.metadata-changed') { + return { type: 'workspace.metadata-changed', workspaceId: candidate.workspaceId } + } return null } catch { return null diff --git a/apps/server/src/sync/types.ts b/apps/server/src/sync/types.ts index 97fd1f6..75b0ad9 100644 --- a/apps/server/src/sync/types.ts +++ b/apps/server/src/sync/types.ts @@ -1,5 +1,5 @@ export const syncObjectKinds = [ - 'note', 'folder', 'asset', 'canvas', 'record', 'tag', 'mark', 'conversation', + 'note', 'folder', 'asset', 'canvas', 'tag', 'mark', 'conversation', 'message', 'memory', 'setting', 'yjs-checkpoint', 'yjs-update', ] as const @@ -40,8 +40,19 @@ export interface AccountWorkspacesChangedNotice { accountId: string } +export interface WorkspaceMembersChangedNotice { + type: 'workspace.members-changed' + workspaceId: string +} + +export interface WorkspaceMetadataChangedNotice { + type: 'workspace.metadata-changed' + workspaceId: string +} + export type SyncNotice = WorkspaceChangeNotice | WorkspaceKeysChangedNotice - | WorkspaceStateChangedNotice | AccountWorkspacesChangedNotice + | WorkspaceStateChangedNotice | AccountWorkspacesChangedNotice | WorkspaceMembersChangedNotice + | WorkspaceMetadataChangedNotice export interface ChangeNotifier { publish(notice: SyncNotice): Promise diff --git a/apps/server/src/usage/service.ts b/apps/server/src/usage/service.ts index 49c158e..460a96e 100644 --- a/apps/server/src/usage/service.ts +++ b/apps/server/src/usage/service.ts @@ -123,6 +123,7 @@ export class UsageService { active_object_bytes: string active_crdt_bytes: string active_blob_bytes: string + retained_bytes: string active_objects: string active_devices: string active_workspaces: string @@ -139,6 +140,34 @@ export class UsageService { where w.account_id = ${accountId} and w.deleted_at is null), 0))::text as active_crdt_bytes, coalesce((select sum(b.size)::bigint from blobs b join workspaces w on w.id = b.workspace_id where w.account_id = ${accountId} and w.deleted_at is null and b.state = 'ready'), 0)::text as active_blob_bytes, + (coalesce((select sum(length(v.ciphertext))::bigint + from object_versions v join workspaces w on w.id = v.workspace_id + where w.account_id = ${accountId}), 0) + + coalesce((select sum(length(e.ciphertext))::bigint + from sync_events e join workspaces w on w.id = e.workspace_id + where w.account_id = ${accountId} and e.ciphertext is not null), 0) + + coalesce((select sum(length(c.ciphertext))::bigint + from sync_checkpoints c join workspaces w on w.id = c.workspace_id + where w.account_id = ${accountId}), 0) + + coalesce((select sum(length(c.ciphertext))::bigint + from sync_conflicts c join workspaces w on w.id = c.workspace_id + where w.account_id = ${accountId}), 0) + + coalesce((select sum(pg_column_size(c.result) + length(c.request_hash))::bigint + from sync_commands c join workspaces w on w.id = c.workspace_id + where w.account_id = ${accountId}), 0) + + coalesce((select sum(length(o.ciphertext))::bigint + from objects o join workspaces w on w.id = o.workspace_id + where w.account_id = ${accountId} and (w.deleted_at is not null or o.deleted_at is not null)), 0) + + coalesce((select sum(length(d.checkpoint_ciphertext))::bigint + from sync_documents d join workspaces w on w.id = d.workspace_id + where w.account_id = ${accountId} and w.deleted_at is not null + and d.checkpoint_ciphertext is not null), 0) + + coalesce((select sum(length(u.ciphertext))::bigint + from sync_updates u join workspaces w on w.id = u.workspace_id + where w.account_id = ${accountId} and w.deleted_at is not null), 0) + + coalesce((select sum(b.size)::bigint + from blobs b join workspaces w on w.id = b.workspace_id + where w.account_id = ${accountId} and w.deleted_at is not null and b.state = 'ready'), 0))::text as retained_bytes, coalesce((select count(*) from objects o join workspaces w on w.id = o.workspace_id where w.account_id = ${accountId} and w.deleted_at is null and o.deleted_at is null), 0)::text as active_objects, coalesce((select count(*) from devices d where d.account_id = ${accountId} and d.revoked_at is null), 0)::text as active_devices, @@ -148,13 +177,14 @@ export class UsageService { const changed = existing.activeObjectBytes !== BigInt(actual.active_object_bytes) || existing.activeCrdtBytes !== BigInt(actual.active_crdt_bytes) || existing.activeBlobBytes !== BigInt(actual.active_blob_bytes) + || existing.retainedBytes !== BigInt(actual.retained_bytes) || existing.activeObjects !== BigInt(actual.active_objects) || existing.activeDevices !== BigInt(actual.active_devices) || existing.activeWorkspaces !== BigInt(actual.active_workspaces) const [row] = await tx.update(accountUsage).set({ ...(changed ? { activeObjectBytes: BigInt(actual.active_object_bytes), activeBlobBytes: BigInt(actual.active_blob_bytes), - activeCrdtBytes: BigInt(actual.active_crdt_bytes), + activeCrdtBytes: BigInt(actual.active_crdt_bytes), retainedBytes: BigInt(actual.retained_bytes), activeObjects: BigInt(actual.active_objects), activeDevices: BigInt(actual.active_devices), activeWorkspaces: BigInt(actual.active_workspaces), revision: sql`${accountUsage.revision} + 1`, } : {}), @@ -174,7 +204,14 @@ export class UsageService { }): Promise<{ reservationId: string, created: boolean }> { if (input.bytes <= 0n) throw new ApiError({ code: 'quota_invalid_request', message: 'Reserved bytes must be positive', statusCode: 400 }) return await this.database.db.transaction(async (tx) => { - await tx.insert(accountUsage).values({ accountId: input.accountId }).onConflictDoNothing() + if (input.storageLimit === null) { + await tx.insert(accountUsage).values({ accountId: input.accountId }).onConflictDoNothing() + } else { + // Hard-limit admission starts from the physical durable tables so + // retained history written since the last background reconciliation + // cannot open a temporary quota bypass window. + await this.reconcileCurrentInTransaction(tx, input.accountId) + } const [existing] = await tx.select({ id: usageReservations.id, requestHash: usageReservations.requestHash, quantity: usageReservations.quantity, status: usageReservations.status, @@ -272,13 +309,10 @@ export class UsageService { revision: sql`${accountUsage.revision} + 1`, updatedAt: new Date(), }).where(and( eq(accountUsage.accountId, input.accountId), - ...(input.storageLimit === null || byteDelta <= 0n ? [] : [sql`${accountUsage.activeObjectBytes} + ${accountUsage.activeCrdtBytes} + ${accountUsage.activeBlobBytes} + ${accountUsage.reservedBlobBytes} + ${byteDelta} <= ${input.storageLimit}`]), + ...(input.storageLimit === null || byteDelta <= 0n ? [] : [sql`${accountUsage.activeObjectBytes} + ${accountUsage.activeCrdtBytes} + ${accountUsage.activeBlobBytes} + ${accountUsage.reservedBlobBytes} + ${accountUsage.retainedBytes} + ${byteDelta} <= ${input.storageLimit}`]), )).returning({ accountId: accountUsage.accountId }) if (updated.length !== 1) { - throw new ApiError({ - code: 'quota_exceeded', message: 'Storage quota is exceeded', statusCode: 409, - details: { metric: 'storage_bytes', limit: input.storageLimit?.toString() }, - }) + throw await storageQuotaError(tx, input.accountId, input.storageLimit) } } @@ -295,11 +329,10 @@ export class UsageService { revision: sql`${accountUsage.revision} + 1`, updatedAt: new Date(), }).where(and( eq(accountUsage.accountId, accountId), - ...(storageLimit === null || byteDelta <= 0n ? [] : [sql`${accountUsage.activeObjectBytes} + ${accountUsage.activeCrdtBytes} + ${accountUsage.activeBlobBytes} + ${accountUsage.reservedBlobBytes} + ${byteDelta} <= ${storageLimit}`]), + ...(storageLimit === null || byteDelta <= 0n ? [] : [sql`${accountUsage.activeObjectBytes} + ${accountUsage.activeCrdtBytes} + ${accountUsage.activeBlobBytes} + ${accountUsage.reservedBlobBytes} + ${accountUsage.retainedBytes} + ${byteDelta} <= ${storageLimit}`]), )).returning({ accountId: accountUsage.accountId }) if (updated.length !== 1) { - throw new ApiError({ code: 'quota_exceeded', message: 'Storage quota is exceeded', statusCode: 409, - details: { metric: 'storage_bytes', limit: storageLimit?.toString() } }) + throw await storageQuotaError(tx, accountId, storageLimit) } } @@ -353,8 +386,8 @@ async function reserveBytes( const admitted = await tx.update(accountUsage).set({ reservedBlobBytes: sql`${accountUsage.reservedBlobBytes} + ${bytes}`, revision: sql`${accountUsage.revision} + 1`, updatedAt: new Date(), - }).where(and(eq(accountUsage.accountId, accountId), sql`${accountUsage.activeObjectBytes} + ${accountUsage.activeCrdtBytes} + ${accountUsage.activeBlobBytes} + ${accountUsage.reservedBlobBytes} + ${bytes} <= ${storageLimit}`)).returning({ accountId: accountUsage.accountId }) - if (admitted.length !== 1) throw new ApiError({ code: 'quota_exceeded', message: 'Storage quota is exceeded', statusCode: 409, details: { metric: 'storage_bytes', limit: storageLimit.toString() } }) + }).where(and(eq(accountUsage.accountId, accountId), sql`${accountUsage.activeObjectBytes} + ${accountUsage.activeCrdtBytes} + ${accountUsage.activeBlobBytes} + ${accountUsage.reservedBlobBytes} + ${accountUsage.retainedBytes} + ${bytes} <= ${storageLimit}`)).returning({ accountId: accountUsage.accountId }) + if (admitted.length !== 1) throw await storageQuotaError(tx, accountId, storageLimit) return } await tx.update(accountUsage).set({ @@ -363,6 +396,28 @@ async function reserveBytes( }).where(eq(accountUsage.accountId, accountId)) } +async function storageQuotaError(tx: any, accountId: string, limit: bigint | null): Promise { + const [usage] = await tx.select({ + activeObjectBytes: accountUsage.activeObjectBytes, + activeCrdtBytes: accountUsage.activeCrdtBytes, + activeBlobBytes: accountUsage.activeBlobBytes, + reservedBlobBytes: accountUsage.reservedBlobBytes, + retainedBytes: accountUsage.retainedBytes, + }).from(accountUsage).where(eq(accountUsage.accountId, accountId)).limit(1) + const used = usage === undefined ? 0n + : usage.activeObjectBytes + usage.activeCrdtBytes + usage.activeBlobBytes + + usage.reservedBlobBytes + usage.retainedBytes + return new ApiError({ + code: 'quota_exceeded', message: 'Storage quota is exceeded', statusCode: 409, + details: { + metric: 'storage_bytes', + limit: limit?.toString(), + used: used.toString(), + reserved: usage?.reservedBlobBytes.toString() ?? '0', + }, + }) +} + function usageEventRequestHash(value: Record): string { const canonical = Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right))) return createHash('sha256').update(JSON.stringify(canonical)).digest('base64url') diff --git a/apps/server/src/workspaces/capabilities.ts b/apps/server/src/workspaces/capabilities.ts new file mode 100644 index 0000000..24070b0 --- /dev/null +++ b/apps/server/src/workspaces/capabilities.ts @@ -0,0 +1,43 @@ +export const workspaceCapabilities = [ + 'content.read', + 'content.create', + 'content.update', + 'content.delete', + 'history.view', + 'history.restore', + 'member.invite', + 'member.update', + 'member.remove', + 'workspace.rename', + 'workspace.delete', +] as const + +export type WorkspaceCapability = typeof workspaceCapabilities[number] +export type WorkspaceMemberRole = 'viewer' | 'editor' | 'manager' + +export const workspaceRoleTemplates: Readonly> = { + viewer: ['content.read', 'history.view'], + editor: [ + 'content.read', 'content.create', 'content.update', 'content.delete', + 'history.view', 'history.restore', + ], + manager: [ + 'content.read', 'content.create', 'content.update', 'content.delete', + 'history.view', 'history.restore', 'member.invite', 'member.update', 'member.remove', + 'workspace.rename', + ], +} + +const capabilitySet = new Set(workspaceCapabilities) + +export function normalizeWorkspaceCapabilities(values: readonly string[]): WorkspaceCapability[] { + const unique = [...new Set(values)] + if (unique.some(value => !capabilitySet.has(value))) { + throw new Error('Unknown workspace capability') + } + return workspaceCapabilities.filter(capability => unique.includes(capability)) +} + +export function capabilitiesForRole(role: WorkspaceMemberRole): WorkspaceCapability[] { + return [...workspaceRoleTemplates[role]] +} diff --git a/apps/server/src/workspaces/collaboration-service.ts b/apps/server/src/workspaces/collaboration-service.ts new file mode 100644 index 0000000..0d215b9 --- /dev/null +++ b/apps/server/src/workspaces/collaboration-service.ts @@ -0,0 +1,337 @@ +import { createHash, randomBytes } from 'node:crypto' +import { and, eq, isNull, sql } from 'drizzle-orm' +import type { DatabaseContext } from '../database/client.js' +import { + accounts, workspaceInvitations, workspaceMembers, workspaces, +} from '../database/schema.js' +import { ApiError } from '../errors.js' +import type { ChangeNotifier } from '../sync/types.js' +import { + capabilitiesForRole, normalizeWorkspaceCapabilities, workspaceCapabilities, + type WorkspaceCapability, type WorkspaceMemberRole, +} from './capabilities.js' +import type { WorkspaceService } from './service.js' +import type { WorkspaceAccess } from './service.js' + +interface MembershipInput { + role: WorkspaceMemberRole + capabilities?: string[] +} + +export class WorkspaceCollaborationService { + constructor( + private readonly database: DatabaseContext, + private readonly workspacesService: WorkspaceService, + private readonly notifier: ChangeNotifier, + ) {} + + async listMembers(accountId: string, workspaceId: string) { + await this.workspacesService.assertCapability(accountId, workspaceId, 'content.read') + const [workspace] = await this.database.db.select({ + ownerAccountId: workspaces.accountId, + ownerLogin: accounts.login, + type: workspaces.type, + }).from(workspaces).innerJoin(accounts, eq(accounts.id, workspaces.accountId)) + .where(and(eq(workspaces.id, workspaceId), isNull(workspaces.deletedAt))).limit(1) + if (workspace === undefined) throw workspaceNotFound() + const members = await this.database.db.select({ + accountId: workspaceMembers.accountId, + login: accounts.login, + role: workspaceMembers.role, + capabilities: workspaceMembers.capabilities, + joinedAt: workspaceMembers.joinedAt, + updatedAt: workspaceMembers.updatedAt, + }).from(workspaceMembers).innerJoin(accounts, eq(accounts.id, workspaceMembers.accountId)) + .where(eq(workspaceMembers.workspaceId, workspaceId)) + .orderBy(workspaceMembers.joinedAt) + return [{ + accountId: workspace.ownerAccountId, + login: workspace.ownerLogin, + role: 'owner' as const, + capabilities: workspaceCapabilities, + joinedAt: null, + updatedAt: null, + }, ...members] + } + + async listInvitations(accountId: string, workspaceId: string) { + await this.#assertShareable(accountId, workspaceId, 'member.invite') + const rows = await this.database.db.select().from(workspaceInvitations).where(and( + eq(workspaceInvitations.workspaceId, workspaceId), + eq(workspaceInvitations.status, 'pending'), + )).orderBy(workspaceInvitations.createdAt) + return rows.map(serializeInvitation) + } + + async listPendingForAccount(accountId: string) { + const rows = await this.database.db.select({ + invitation: workspaceInvitations, + workspaceNameCiphertext: workspaces.nameCiphertext, + inviterLogin: accounts.login, + }).from(workspaceInvitations) + .innerJoin(workspaces, eq(workspaces.id, workspaceInvitations.workspaceId)) + .innerJoin(accounts, eq(accounts.id, workspaceInvitations.invitedByAccountId)) + .where(and( + eq(workspaceInvitations.kind, 'account'), + eq(workspaceInvitations.inviteeAccountId, accountId), + eq(workspaceInvitations.status, 'pending'), + isNull(workspaces.deletedAt), + )).orderBy(workspaceInvitations.createdAt) + return rows.map(row => ({ + ...serializeInvitation(row.invitation), + workspaceNameCiphertext: row.workspaceNameCiphertext, + inviterLogin: row.inviterLogin, + })) + } + + async inviteAccount( + actorAccountId: string, + workspaceId: string, + login: string, + input: MembershipInput, + expiresInSeconds = 7 * 24 * 60 * 60, + ) { + const actor = await this.#assertShareable(actorAccountId, workspaceId, 'member.invite') + const normalizedLogin = login.trim().toLocaleLowerCase('en-US') + const [invitee] = await this.database.db.select({ id: accounts.id, login: accounts.login }) + .from(accounts).where(and( + sql`lower(${accounts.login}) = ${normalizedLogin}`, + isNull(accounts.disabledAt), + isNull(accounts.suspendedAt), + )).limit(1) + if (invitee === undefined) { + throw new ApiError({ code: 'invitee_not_found', message: 'Invitee account was not found', statusCode: 404 }) + } + const [workspace] = await this.database.db.select({ ownerAccountId: workspaces.accountId }) + .from(workspaces).where(eq(workspaces.id, workspaceId)).limit(1) + if (workspace?.ownerAccountId === invitee.id) { + throw new ApiError({ code: 'workspace_owner_already_member', message: 'Workspace owner is already a member', statusCode: 409 }) + } + const capabilities = membershipCapabilities(input) + assertGrantable(actor, capabilities) + const [invitation] = await this.database.db.insert(workspaceInvitations).values({ + workspaceId, + kind: 'account', + inviteeAccountId: invitee.id, + role: input.role, + capabilities, + invitedByAccountId: actorAccountId, + expiresAt: new Date(Date.now() + expiresInSeconds * 1_000), + }).onConflictDoUpdate({ + target: [workspaceInvitations.workspaceId, workspaceInvitations.inviteeAccountId], + targetWhere: sql`${workspaceInvitations.kind} = 'account' and ${workspaceInvitations.status} = 'pending'`, + set: { + role: input.role, + capabilities, + invitedByAccountId: actorAccountId, + expiresAt: new Date(Date.now() + expiresInSeconds * 1_000), + updatedAt: new Date(), + }, + }).returning() + if (invitation === undefined) throw new Error('Workspace invitation insert returned no row') + await this.#publishMembershipChanged(workspaceId, invitee.id) + return { ...serializeInvitation(invitation), inviteeLogin: invitee.login } + } + + async createLink( + actorAccountId: string, + workspaceId: string, + input: MembershipInput, + expiresInSeconds = 7 * 24 * 60 * 60, + ) { + const actor = await this.#assertShareable(actorAccountId, workspaceId, 'member.invite') + const capabilities = membershipCapabilities(input) + assertGrantable(actor, capabilities) + const token = randomBytes(32).toString('base64url') + const [invitation] = await this.database.db.insert(workspaceInvitations).values({ + workspaceId, + kind: 'link', + tokenHash: tokenDigest(token), + tokenHint: token.slice(-6), + role: input.role, + capabilities, + invitedByAccountId: actorAccountId, + expiresAt: new Date(Date.now() + expiresInSeconds * 1_000), + }).returning() + if (invitation === undefined) throw new Error('Workspace invitation insert returned no row') + return { ...serializeInvitation(invitation), token } + } + + async acceptAccountInvitation(accountId: string, invitationId: string) { + return this.#accept(accountId, and( + eq(workspaceInvitations.id, invitationId), + eq(workspaceInvitations.kind, 'account'), + eq(workspaceInvitations.inviteeAccountId, accountId), + )) + } + + async acceptLink(accountId: string, token: string) { + return this.#accept(accountId, and( + eq(workspaceInvitations.tokenHash, tokenDigest(token)), + eq(workspaceInvitations.kind, 'link'), + )) + } + + async updateMember( + actorAccountId: string, + workspaceId: string, + memberAccountId: string, + input: MembershipInput, + ) { + const actor = await this.#assertShareable(actorAccountId, workspaceId, 'member.update') + const capabilities = membershipCapabilities(input) + assertGrantable(actor, capabilities) + const [member] = await this.database.db.update(workspaceMembers).set({ + role: input.role, + capabilities, + updatedAt: new Date(), + }).where(and( + eq(workspaceMembers.workspaceId, workspaceId), + eq(workspaceMembers.accountId, memberAccountId), + )).returning() + if (member === undefined) throw memberNotFound() + const [account] = await this.database.db.select({ login: accounts.login }) + .from(accounts).where(eq(accounts.id, memberAccountId)).limit(1) + if (account === undefined) throw memberNotFound() + await this.#publishMembershipChanged(workspaceId, memberAccountId) + return { + accountId: member.accountId, + login: account.login, + role: member.role, + capabilities: member.capabilities, + joinedAt: member.joinedAt, + updatedAt: member.updatedAt, + } + } + + async removeMember(actorAccountId: string, workspaceId: string, memberAccountId: string): Promise { + await this.#assertShareable(actorAccountId, workspaceId, 'member.remove') + const removed = await this.database.db.delete(workspaceMembers).where(and( + eq(workspaceMembers.workspaceId, workspaceId), + eq(workspaceMembers.accountId, memberAccountId), + )).returning({ accountId: workspaceMembers.accountId }) + if (removed.length === 0) throw memberNotFound() + await this.#publishMembershipChanged(workspaceId, memberAccountId) + } + + async revokeInvitation(actorAccountId: string, workspaceId: string, invitationId: string): Promise { + await this.#assertShareable(actorAccountId, workspaceId, 'member.invite') + const [revoked] = await this.database.db.update(workspaceInvitations).set({ + status: 'revoked', revokedAt: new Date(), updatedAt: new Date(), + }).where(and( + eq(workspaceInvitations.id, invitationId), + eq(workspaceInvitations.workspaceId, workspaceId), + eq(workspaceInvitations.status, 'pending'), + )).returning({ id: workspaceInvitations.id }) + if (revoked === undefined) { + throw new ApiError({ code: 'workspace_invitation_not_found', message: 'Pending invitation was not found', statusCode: 404 }) + } + await this.notifier.publish({ type: 'workspace.members-changed', workspaceId }).catch(() => undefined) + } + + async #accept(accountId: string, predicate: ReturnType) { + const accepted = await this.database.db.transaction(async tx => { + const [invitation] = await tx.select().from(workspaceInvitations) + .where(and(predicate, eq(workspaceInvitations.status, 'pending'))).limit(1).for('update') + if (invitation === undefined) { + throw new ApiError({ code: 'workspace_invitation_not_found', message: 'Pending invitation was not found', statusCode: 404 }) + } + if (invitation.expiresAt <= new Date()) { + await tx.update(workspaceInvitations).set({ status: 'expired', updatedAt: new Date() }) + .where(eq(workspaceInvitations.id, invitation.id)) + return { expired: true as const, workspaceId: invitation.workspaceId } + } + const [workspace] = await tx.select({ ownerAccountId: workspaces.accountId, type: workspaces.type }) + .from(workspaces).where(and(eq(workspaces.id, invitation.workspaceId), isNull(workspaces.deletedAt))).limit(1) + if (workspace === undefined) throw workspaceNotFound() + if (workspace.type !== 'library') throw accountDataCannotBeShared() + if (workspace.ownerAccountId === accountId) { + throw new ApiError({ code: 'workspace_owner_already_member', message: 'Workspace owner is already a member', statusCode: 409 }) + } + await tx.insert(workspaceMembers).values({ + workspaceId: invitation.workspaceId, + accountId, + role: invitation.role, + capabilities: invitation.capabilities, + invitedByAccountId: invitation.invitedByAccountId, + }).onConflictDoUpdate({ + target: [workspaceMembers.workspaceId, workspaceMembers.accountId], + set: { + role: invitation.role, + capabilities: invitation.capabilities, + invitedByAccountId: invitation.invitedByAccountId, + updatedAt: new Date(), + }, + }) + await tx.update(workspaceInvitations).set({ + status: 'accepted', acceptedByAccountId: accountId, + acceptedAt: new Date(), updatedAt: new Date(), + }).where(eq(workspaceInvitations.id, invitation.id)) + return { expired: false as const, workspaceId: invitation.workspaceId } + }) + if (accepted.expired) { + throw new ApiError({ code: 'workspace_invitation_expired', message: 'Invitation has expired', statusCode: 410 }) + } + await this.#publishMembershipChanged(accepted.workspaceId, accountId) + return { workspaceId: accepted.workspaceId } + } + + async #assertShareable( + accountId: string, + workspaceId: string, + capability: 'member.invite' | 'member.update' | 'member.remove', + ): Promise { + const access = await this.workspacesService.assertCapability(accountId, workspaceId, capability) + if (access.type !== 'library') throw accountDataCannotBeShared() + return access + } + + async #publishMembershipChanged(workspaceId: string, accountId: string): Promise { + await Promise.all([ + this.notifier.publish({ type: 'workspace.members-changed', workspaceId }).catch(() => undefined), + this.notifier.publish({ type: 'account.workspaces-changed', accountId }).catch(() => undefined), + ]) + } +} + +function membershipCapabilities(input: MembershipInput): WorkspaceCapability[] { + try { + return normalizeWorkspaceCapabilities(input.capabilities ?? capabilitiesForRole(input.role)) + } catch { + throw new ApiError({ code: 'workspace_capability_invalid', message: 'Workspace capability list is invalid', statusCode: 400 }) + } +} + +function tokenDigest(token: string): string { + return createHash('sha256').update(token).digest('base64url') +} + +function serializeInvitation(invitation: typeof workspaceInvitations.$inferSelect) { + const { tokenHash: _tokenHash, ...safe } = invitation + return safe +} + +function workspaceNotFound(): ApiError { + return new ApiError({ code: 'workspace_not_found', message: 'Workspace not found', statusCode: 404 }) +} + +function memberNotFound(): ApiError { + return new ApiError({ code: 'workspace_member_not_found', message: 'Workspace member was not found', statusCode: 404 }) +} + +function accountDataCannotBeShared(): ApiError { + return new ApiError({ + code: 'account_data_workspace_not_shareable', + message: 'Personal account-data workspaces cannot be shared', + statusCode: 409, + }) +} + +function assertGrantable(actor: WorkspaceAccess, capabilities: readonly WorkspaceCapability[]): void { + if (actor.owner || capabilities.every(capability => actor.capabilities.includes(capability))) return + throw new ApiError({ + code: 'workspace_capability_escalation_denied', + message: 'Members cannot grant capabilities they do not hold', + statusCode: 403, + }) +} diff --git a/apps/server/src/workspaces/service.ts b/apps/server/src/workspaces/service.ts index 62b6caf..2ea2376 100644 --- a/apps/server/src/workspaces/service.ts +++ b/apps/server/src/workspaces/service.ts @@ -1,11 +1,14 @@ import { createHash } from 'node:crypto' -import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm' +import { and, asc, desc, eq, inArray, isNull, or, sql } from 'drizzle-orm' import type { DatabaseContext } from '../database/client.js' -import { devices, objects, workspaceKeyEnvelopes, workspaceKeys, workspaces } from '../database/schema.js' +import { devices, objects, workspaceKeyEnvelopes, workspaceKeys, workspaceMembers, workspaces } from '../database/schema.js' import { ApiError } from '../errors.js' import { assertAccountWriteAllowedInTransaction } from '../compliance/deletion-fence.js' import type { ChangeNotifier } from '../sync/types.js' import type { UsageService } from '../usage/service.js' +import { workspaceCapabilities, type WorkspaceCapability } from './capabilities.js' + +const NOTEGEN_DEFAULT_LIBRARY_IDEMPOTENCY_KEY = 'notegen.default-library.v1' export interface CreateWorkspaceInput { nameCiphertext: string @@ -22,6 +25,14 @@ export interface KeyEnvelopeInput { kdfParams: Record | null } +export interface WorkspaceAccess { + ownerAccountId: string + owner: boolean + role: 'owner' | 'viewer' | 'editor' | 'manager' + capabilities: WorkspaceCapability[] + type: 'account-data' | 'library' +} + export class WorkspaceService { constructor( private readonly database: DatabaseContext, @@ -31,7 +42,8 @@ export class WorkspaceService { ) {} async create(accountId: string, input: CreateWorkspaceInput): Promise<{ id: string, createdAt: Date, latestSequence: string, nameCiphertext: string, created: boolean }> { - validateKeyEnvelopes(input.envelopes, true) + const managed = input.envelopes.length === 1 && input.envelopes[0]?.type === 'managed' + validateKeyEnvelopes(input.envelopes, !managed, managed) await this.#validateDeviceRecipients(accountId, input.envelopes) const workspaceLimit = await this.#workspaceLimit(accountId) const requestHash = input.idempotencyKey === undefined ? undefined : workspaceCreationHash(input) @@ -46,22 +58,47 @@ export class WorkspaceService { eq(workspaces.accountId, accountId), eq(workspaces.creationIdempotencyKey, input.idempotencyKey), )).limit(1) if (existing !== undefined) { - if (existing.creationRequestHash !== requestHash) { + // The default NoteGen library is bootstrapped independently by every + // device. Its encrypted name and generated managed key are expected + // to differ, so the account-scoped key elects the first successful + // creation instead of treating later devices as conflicting retries. + if (existing.creationRequestHash !== requestHash + && input.idempotencyKey !== NOTEGEN_DEFAULT_LIBRARY_IDEMPOTENCY_KEY) { throw new ApiError({ code: 'idempotency_conflict', message: 'Workspace creation key was reused with different input', statusCode: 409 }) } return { ...existing, latestSequence: existing.latestSequence.toString(), created: false } } + if (input.idempotencyKey === NOTEGEN_DEFAULT_LIBRARY_IDEMPOTENCY_KEY) { + const [legacyDefault] = await tx.select({ + id: workspaces.id, + createdAt: workspaces.createdAt, + latestSequence: workspaces.latestSequence, + nameCiphertext: workspaces.nameCiphertext, + }).from(workspaces).where(and( + eq(workspaces.accountId, accountId), + eq(workspaces.type, 'library'), + isNull(workspaces.deletedAt), + )).orderBy(asc(workspaces.createdAt)).limit(1).for('update') + if (legacyDefault !== undefined) { + await tx.update(workspaces).set({ + creationIdempotencyKey: input.idempotencyKey, + creationRequestHash: requestHash, + updatedAt: new Date(), + }).where(eq(workspaces.id, legacyDefault.id)) + return { + ...legacyDefault, + latestSequence: legacyDefault.latestSequence.toString(), + created: false, + } + } + } } - const [currentDefault] = await tx.select({ id: workspaces.id }).from(workspaces).where(and( - eq(workspaces.accountId, accountId), - eq(workspaces.isDefault, true), - isNull(workspaces.deletedAt), - )).limit(1) await this.usage?.admitWorkspace(tx, accountId, workspaceLimit) const [workspace] = await tx.insert(workspaces).values({ accountId, + type: 'library', nameCiphertext: input.nameCiphertext, - isDefault: currentDefault === undefined, + isDefault: false, ...(input.idempotencyKey === undefined ? {} : { creationIdempotencyKey: input.idempotencyKey, creationRequestHash: requestHash, @@ -78,6 +115,29 @@ export class WorkspaceService { return created } + async createManagedLibrary(accountId: string, input: { + nameCiphertext: string + managedKey: string + idempotencyKey?: string + }) { + if (!/^[A-Za-z0-9_-]{43}$/.test(input.managedKey)) { + throw new ApiError({ + code: 'managed_key_invalid', + message: 'Managed key must be a 256-bit Base64URL value', + statusCode: 400, + }) + } + return this.create(accountId, { + nameCiphertext: input.nameCiphertext, + keyVersion: 1, + envelopes: [{ + type: 'managed', recipientId: null, wrappedKey: input.managedKey, + kdfSalt: null, kdfParams: null, + }], + ...(input.idempotencyKey === undefined ? {} : { idempotencyKey: input.idempotencyKey }), + }) + } + async getCreationRequest(accountId: string, idempotencyKey: string): Promise<{ id: string, createdAt: Date } | undefined> { const [workspace] = await this.database.db.select({ id: workspaces.id, createdAt: workspaces.createdAt }) .from(workspaces).where(and( @@ -116,7 +176,7 @@ export class WorkspaceService { nameCiphertext: workspaces.nameCiphertext, }).from(workspaces).where(and( eq(workspaces.accountId, accountId), - eq(workspaces.isDefault, true), + eq(workspaces.type, 'account-data'), isNull(workspaces.deletedAt), )).limit(1) if (existing !== undefined) { @@ -141,6 +201,7 @@ export class WorkspaceService { const [workspace] = await tx.insert(workspaces).values({ accountId, + type: 'account-data', nameCiphertext: input.nameCiphertext, isDefault: true, }).returning({ @@ -170,13 +231,20 @@ export class WorkspaceService { async list(accountId: string, deviceId: string, includeDeleted: boolean) { const rows = await this.database.db.select({ id: workspaces.id, + ownerAccountId: workspaces.accountId, + type: workspaces.type, nameCiphertext: workspaces.nameCiphertext, latestSequence: workspaces.latestSequence, createdAt: workspaces.createdAt, updatedAt: workspaces.updatedAt, deletedAt: workspaces.deletedAt, - }).from(workspaces).where(and( - eq(workspaces.accountId, accountId), + memberRole: workspaceMembers.role, + memberCapabilities: workspaceMembers.capabilities, + }).from(workspaces).leftJoin(workspaceMembers, and( + eq(workspaceMembers.workspaceId, workspaces.id), + eq(workspaceMembers.accountId, accountId), + )).where(and( + or(eq(workspaces.accountId, accountId), eq(workspaceMembers.accountId, accountId)), ...(includeDeleted ? [] : [isNull(workspaces.deletedAt)]), )) .orderBy(desc(workspaces.updatedAt)) @@ -208,6 +276,11 @@ export class WorkspaceService { const managedWorkspaceIds = new Set(managedEnvelopes.map((item) => item.workspaceId)) return rows.map((row) => ({ ...row, + owner: row.ownerAccountId === accountId, + role: row.ownerAccountId === accountId ? 'owner' as const : row.memberRole, + capabilities: row.ownerAccountId === accountId + ? [...workspaceCapabilities] + : row.memberCapabilities ?? [], latestSequence: row.latestSequence.toString(), latestKeyVersion: latestKeyVersionByWorkspace.get(row.id) ?? 0, hasDeviceEnvelope: deviceWorkspaceIds.has(row.id), @@ -216,7 +289,7 @@ export class WorkspaceService { } async getAccountSyncOverview(accountId: string) { - const [summaries, kinds, recentActivity] = await Promise.all([ + const [summaries, kinds, activityTimeline, activityKinds, usage] = await Promise.all([ this.database.sql }>>` - select e.sequence::text as sequence, v.kind::text as kind, - case when e.event_type = 'object.deleted' then 'delete' else 'upsert' end as change_type, - e.created_at, d.id as device_id, - d.name as device_name, d.platform as device_platform + with buckets as ( + select generate_series( + date_trunc('minute', now()) - interval '59 minutes', + date_trunc('minute', now()), + interval '1 minute' + ) as bucket_start + ), activity_by_kind as ( + select date_trunc('minute', e.created_at) as bucket_start, + v.kind::text as kind, + count(distinct e.object_id) filter (where e.event_type = 'object.upserted')::int as updates, + count(distinct e.object_id) filter (where e.event_type = 'object.deleted')::int as deletes + from sync_events e + join workspaces w on w.id = e.workspace_id + join object_versions v on v.workspace_id = e.workspace_id + and v.object_id = e.object_id and v.sequence = e.sequence + where w.account_id = ${accountId} and w.deleted_at is null + and e.event_type in ('object.upserted', 'object.deleted') + and e.created_at >= date_trunc('minute', now()) - interval '59 minutes' + and e.created_at < date_trunc('minute', now()) + interval '1 minute' + group by date_trunc('minute', e.created_at), v.kind + ) + select buckets.bucket_start, + coalesce(sum(activity_by_kind.updates), 0)::int as updates, + coalesce(sum(activity_by_kind.deletes), 0)::int as deletes, + coalesce( + jsonb_agg( + jsonb_build_object( + 'kind', activity_by_kind.kind, + 'updates', activity_by_kind.updates, + 'deletes', activity_by_kind.deletes + ) order by activity_by_kind.kind + ) filter (where activity_by_kind.kind is not null), + '[]'::jsonb + ) as kinds + from buckets + left join activity_by_kind on activity_by_kind.bucket_start = buckets.bucket_start + group by buckets.bucket_start + order by buckets.bucket_start + `, + this.database.sql>` + select v.kind::text as kind, count(distinct e.object_id)::int as count from sync_events e join workspaces w on w.id = e.workspace_id join object_versions v on v.workspace_id = e.workspace_id and v.object_id = e.object_id and v.sequence = e.sequence - join devices d on d.id = e.source_device_id where w.account_id = ${accountId} and w.deleted_at is null and e.event_type in ('object.upserted', 'object.deleted') - order by e.created_at desc, e.sequence desc limit 30 + and e.created_at >= date_trunc('minute', now()) - interval '59 minutes' + group by v.kind + order by count desc, v.kind `, + this.usage?.getSnapshot(accountId) ?? null, ]) const summary = summaries[0] return { @@ -305,22 +418,28 @@ export class WorkspaceService { latestSequence: summary?.latest_sequence ?? '0', lastActivityAt: summary?.last_activity_at ?? null, encryptionMode: summary?.encryption_mode ?? null, + storageUsage: usage === null ? null : { + activeObjectBytes: usage.metrics.activeObjectBytes ?? '0', + activeCrdtBytes: usage.metrics.activeCrdtBytes ?? '0', + activeBlobBytes: usage.metrics.activeBlobBytes ?? '0', + reservedBlobBytes: usage.metrics.reservedBlobBytes ?? '0', + retainedBytes: usage.metrics.retainedBytes ?? '0', + }, kinds: kinds.map((item) => ({ kind: item.kind, activeCount: item.active_count, deletedCount: item.deleted_count, updatedAt: item.updated_at, })), - recentActivity: recentActivity.map((item) => ({ - sequence: item.sequence, + activityTimeline: activityTimeline.map((item) => ({ + startedAt: item.bucket_start, + updates: item.updates, + deletes: item.deletes, + kinds: item.kinds, + })), + activityKinds: activityKinds.map((item) => ({ kind: item.kind, - changeType: item.change_type, - createdAt: item.created_at, - device: { - id: item.device_id, - name: item.device_name, - platform: item.device_platform, - }, + count: item.count, })), } } @@ -329,7 +448,9 @@ export class WorkspaceService { const rows = await this.database.db.select({ id: workspaces.id, nameCiphertext: workspaces.nameCiphertext, + type: workspaces.type, isDefault: workspaces.isDefault, + creationIdempotencyKey: workspaces.creationIdempotencyKey, latestSequence: workspaces.latestSequence, createdAt: workspaces.createdAt, updatedAt: workspaces.updatedAt, @@ -365,8 +486,9 @@ export class WorkspaceService { const objectCountsByWorkspace = new Map(objectCounts.map((item) => ( [item.workspaceId, item] as const ))) - return rows.map((row) => ({ + return rows.map(({ creationIdempotencyKey, ...row }) => ({ ...row, + isNoteGenDefault: creationIdempotencyKey === NOTEGEN_DEFAULT_LIBRARY_IDEMPOTENCY_KEY, latestSequence: row.latestSequence.toString(), latestKeyVersion: latestKeyVersionByWorkspace.get(row.id) ?? 0, encryptionMode: managedWorkspaceIds.has(row.id) ? 'managed' as const : 'e2ee' as const, @@ -384,8 +506,63 @@ export class WorkspaceService { } } + async access(accountId: string, workspaceId: string): Promise { + const [workspace] = await this.database.db.select({ + ownerAccountId: workspaces.accountId, + type: workspaces.type, + memberRole: workspaceMembers.role, + memberCapabilities: workspaceMembers.capabilities, + }).from(workspaces).leftJoin(workspaceMembers, and( + eq(workspaceMembers.workspaceId, workspaces.id), + eq(workspaceMembers.accountId, accountId), + )).where(and( + eq(workspaces.id, workspaceId), + isNull(workspaces.deletedAt), + or(eq(workspaces.accountId, accountId), eq(workspaceMembers.accountId, accountId)), + )).limit(1) + if (workspace === undefined) { + throw new ApiError({ code: 'workspace_not_found', message: 'Workspace not found', statusCode: 404 }) + } + if (workspace.ownerAccountId === accountId) { + return { + ownerAccountId: workspace.ownerAccountId, + owner: true, + role: 'owner', + capabilities: [...workspaceCapabilities], + type: workspace.type, + } + } + if (workspace.memberRole === null) { + throw new ApiError({ code: 'workspace_not_found', message: 'Workspace not found', statusCode: 404 }) + } + return { + ownerAccountId: workspace.ownerAccountId, + owner: false, + role: workspace.memberRole, + capabilities: workspace.memberCapabilities as WorkspaceCapability[], + type: workspace.type, + } + } + + async assertCapability( + accountId: string, + workspaceId: string, + capability: WorkspaceCapability, + ): Promise { + const access = await this.access(accountId, workspaceId) + if (!access.capabilities.includes(capability)) { + throw new ApiError({ + code: 'workspace_capability_denied', + message: `Workspace capability ${capability} is required`, + statusCode: 403, + details: { capability }, + }) + } + return access + } + async listKeys(accountId: string, workspaceId: string) { - await this.assertOwned(accountId, workspaceId) + await this.assertCapability(accountId, workspaceId, 'content.read') const keys = await this.database.db.select({ keyVersion: workspaceKeys.keyVersion, createdAt: workspaceKeys.createdAt, @@ -610,18 +787,37 @@ export class WorkspaceService { workspaceId: string, options: { allowDefault?: boolean } = {}, ): Promise { - await this.assertOwned(accountId, workspaceId) + const access = await this.assertCapability(accountId, workspaceId, 'workspace.delete') const removed = await this.database.db.transaction(async (tx) => { await assertAccountWriteAllowedInTransaction(tx, accountId) - const [current] = await tx.select({ id: workspaces.id, deletedAt: workspaces.deletedAt }).from(workspaces) - .where(and(eq(workspaces.id, workspaceId), eq(workspaces.accountId, accountId), + const [current] = await tx.select({ + id: workspaces.id, deletedAt: workspaces.deletedAt, + type: workspaces.type, ownerAccountId: workspaces.accountId, + creationIdempotencyKey: workspaces.creationIdempotencyKey, + }).from(workspaces) + .where(and(eq(workspaces.id, workspaceId), ...(options.allowDefault === false ? [eq(workspaces.isDefault, false)] : []))) .limit(1).for('update') if (current === undefined) return undefined + if (current.type === 'account-data') { + throw new ApiError({ + code: 'account_data_workspace_delete_forbidden', + message: 'Personal account-data workspace cannot be deleted', + statusCode: 409, + }) + } + if (current.creationIdempotencyKey === NOTEGEN_DEFAULT_LIBRARY_IDEMPOTENCY_KEY + && options.allowDefault !== true) { + throw new ApiError({ + code: 'workspace_default_delete_forbidden', + message: 'The default NoteGen workspace cannot be deleted', + statusCode: 409, + }) + } if (current.deletedAt === null) { await tx.update(workspaces).set({ deletedAt: new Date(), updatedAt: new Date() }) .where(eq(workspaces.id, workspaceId)) - await this.usage?.releaseWorkspace(tx, accountId) + await this.usage?.releaseWorkspace(tx, access.ownerAccountId) } return current }) @@ -636,6 +832,24 @@ export class WorkspaceService { type: 'workspace.state-changed', workspaceId, deleted: true, }).catch(() => undefined) await this.#publishWorkspaceListChanged(accountId) + if (access.ownerAccountId !== accountId) await this.#publishWorkspaceListChanged(access.ownerAccountId) + } + + async rename(accountId: string, workspaceId: string, nameCiphertext: string) { + await this.assertCapability(accountId, workspaceId, 'workspace.rename') + const [updated] = await this.database.db.update(workspaces).set({ + nameCiphertext, + updatedAt: new Date(), + }).where(and(eq(workspaces.id, workspaceId), isNull(workspaces.deletedAt))).returning({ + id: workspaces.id, + nameCiphertext: workspaces.nameCiphertext, + updatedAt: workspaces.updatedAt, + }) + if (updated === undefined) { + throw new ApiError({ code: 'workspace_not_found', message: 'Workspace not found', statusCode: 404 }) + } + await this.notifier.publish({ type: 'workspace.metadata-changed', workspaceId }).catch(() => undefined) + return updated } async restore(accountId: string, workspaceId: string): Promise { diff --git a/apps/server/test/notegen-client-behavior.integration.test.ts b/apps/server/test/notegen-client-behavior.integration.test.ts index 923fdc6..67881ac 100644 --- a/apps/server/test/notegen-client-behavior.integration.test.ts +++ b/apps/server/test/notegen-client-behavior.integration.test.ts @@ -19,13 +19,16 @@ integration('NoteGen sync protocol durability', () => { const workspace = await api<{ id: string }>(baseUrl, '/v1/workspaces', { method: 'POST', expectedStatus: 201, headers: authorization, body: { - nameCiphertext: 'sync-test-workspace', keyVersion: 1, - envelopes: [{ type: 'passphrase', recipientId: null, wrappedKey: 'test-envelope', - kdfSalt: 'test-salt', kdfParams: { memorySize: 65536, iterations: 3, parallelism: 1 } }, - { type: 'recovery', recipientId: null, wrappedKey: 'test-recovery-envelope', - kdfSalt: null, kdfParams: null }], + nameCiphertext: 'sync-test-workspace', + managedKey: Buffer.alloc(32, 1).toString('base64url'), }, }) + const protocolSession = await api<{ syncEpoch: string }>( + baseUrl, + `/v1/workspaces/${workspace.id}/sync/session?protocolVersion=1&cursor=0`, + { headers: authorization }, + ) + syncEpochs.set(workspace.id, protocolSession.syncEpoch) const objectId = randomUUID() const objectEnvelope = encryptedTestEnvelope('object-v1') const command = { @@ -55,7 +58,7 @@ integration('NoteGen sync protocol durability', () => { const deleteResult = await syncCommands(baseUrl, workspace.id, authorization, [deletion]) expect(deleteResult[0]).toMatchObject({ status: 'conflict', code: 'delete_edit_conflict', conflictId }) const events = await api<{ events: Array<{ type: string }> }>( - baseUrl, `/v1/workspaces/${workspace.id}/sync/events?after=0`, { headers: authorization }, + baseUrl, `/v1/workspaces/${workspace.id}/sync/events?after=0&expectedSyncEpoch=${syncEpoch(workspace.id)}`, { headers: authorization }, ) expect(events.events.map(event => event.type)).toContain('conflict.created') @@ -69,7 +72,7 @@ integration('NoteGen sync protocol durability', () => { const firstBootstrap = await api<{ bootstrapId: string, snapshotSequence: string, objects: Array<{ objectId: string }>, nextObjectId: string | null, hasMore: boolean, - }>(baseUrl, `/v1/workspaces/${workspace.id}/sync/bootstrap?limit=1`, { headers: authorization }) + }>(baseUrl, `/v1/workspaces/${workspace.id}/sync/bootstrap?limit=1&expectedSyncEpoch=${syncEpoch(workspace.id)}`, { headers: authorization }) expect(firstBootstrap.hasMore).toBe(true) const laterObjectId = randomUUID() const laterEnvelope = encryptedTestEnvelope('created-after-bootstrap') @@ -84,7 +87,7 @@ integration('NoteGen sync protocol durability', () => { const page = await api<{ bootstrapId: string, snapshotSequence: string, objects: Array<{ objectId: string }>, nextObjectId: string | null, hasMore: boolean, - }>(baseUrl, `/v1/workspaces/${workspace.id}/sync/bootstrap?limit=1&bootstrapId=${firstBootstrap.bootstrapId}&afterObjectId=${afterObjectId}`, + }>(baseUrl, `/v1/workspaces/${workspace.id}/sync/bootstrap?limit=1&expectedSyncEpoch=${syncEpoch(workspace.id)}&bootstrapId=${firstBootstrap.bootstrapId}&afterObjectId=${afterObjectId}`, { headers: authorization }) expect(page.bootstrapId).toBe(firstBootstrap.bootstrapId) expect(page.snapshotSequence).toBe(firstBootstrap.snapshotSequence) @@ -135,11 +138,65 @@ integration('NoteGen sync protocol durability', () => { status: 'conflict', code: 'delete_edit_conflict', conflictId: subtreeConflictId, }) const afterSubtreeConflict = await api<{ objects: Array<{ objectId: string, deletedAt: string | null }> }>( - baseUrl, `/v1/workspaces/${workspace.id}/sync/bootstrap?limit=1000`, { headers: authorization }, + baseUrl, `/v1/workspaces/${workspace.id}/sync/bootstrap?limit=1000&expectedSyncEpoch=${syncEpoch(workspace.id)}`, { headers: authorization }, ) expect(afterSubtreeConflict.objects.find(item => item.objectId === subtreeRootId)?.deletedAt).toBeNull() expect(afterSubtreeConflict.objects.find(item => item.objectId === subtreeNoteId)?.deletedAt).toBeNull() }, 30_000) + + it('enforces member capabilities, invitation revocation, and removal', async () => { + if (baseUrl === undefined) throw new Error('INTEGRATION_BASE_URL is required') + const owner = await registerIntegrationAccount(baseUrl, 'owner') + const viewer = await registerIntegrationAccount(baseUrl, 'viewer') + const workspace = await api<{ id: string }>(baseUrl, '/v1/workspaces', { + method: 'POST', expectedStatus: 201, headers: owner.headers, + body: { + nameCiphertext: 'shared-library', + managedKey: Buffer.alloc(32, 2).toString('base64url'), + }, + }) + const invitation = await api<{ id: string }>( + baseUrl, `/v1/workspaces/${workspace.id}/invitations/account`, { + method: 'POST', expectedStatus: 201, headers: owner.headers, + body: { login: viewer.login, role: 'viewer' }, + }, + ) + await api(baseUrl, `/v1/workspace-invitations/${invitation.id}/accept`, { + method: 'POST', headers: viewer.headers, + }) + const session = await api<{ syncEpoch: string }>( + baseUrl, `/v1/workspaces/${workspace.id}/sync/session?protocolVersion=1&cursor=0`, + { headers: viewer.headers }, + ) + syncEpochs.set(workspace.id, session.syncEpoch) + const envelope = encryptedTestEnvelope('viewer-write') + const [rejected] = await syncCommands(baseUrl, workspace.id, viewer.headers, [{ + type: 'upsert-object', commandId: randomUUID(), objectId: randomUUID(), kind: 'note', + parentObjectId: null, nameCiphertext: envelope.ciphertext, baseRevision: null, + blobRefs: [], keyVersion: 1, ...envelope, + }]) + expect(rejected).toMatchObject({ status: 'rejected', code: 'workspace_capability_denied' }) + + const link = await api<{ id: string; token: string }>( + baseUrl, `/v1/workspaces/${workspace.id}/invitations/link`, { + method: 'POST', expectedStatus: 201, headers: owner.headers, + body: { role: 'editor' }, + }, + ) + await api(baseUrl, `/v1/workspaces/${workspace.id}/invitations/${link.id}`, { + method: 'DELETE', expectedStatus: 204, headers: owner.headers, + }) + await api(baseUrl, '/v1/workspace-invitations/accept-link', { + method: 'POST', expectedStatus: 404, headers: viewer.headers, body: { token: link.token }, + }) + + await api(baseUrl, `/v1/workspaces/${workspace.id}/members/${viewer.accountId}`, { + method: 'DELETE', expectedStatus: 204, headers: owner.headers, + }) + await api(baseUrl, `/v1/workspaces/${workspace.id}/sync/session?protocolVersion=1&cursor=0`, { + expectedStatus: 404, headers: viewer.headers, + }) + }, 30_000) }) function encryptedTestEnvelope(value: string) { @@ -152,13 +209,38 @@ function encryptedTestEnvelope(value: string) { async function syncCommands(origin: string, workspaceId: string, headers: Record, commands: unknown[]) { const response = await api<{ results: Array> }>( - origin, `/v1/workspaces/${workspaceId}/sync/commands`, { method: 'POST', headers, body: { commands } }, + origin, `/v1/workspaces/${workspaceId}/sync/commands`, { + method: 'POST', headers, body: { commands, expectedSyncEpoch: syncEpoch(workspaceId) }, + }, ) return response.results } +const syncEpochs = new Map() + +function syncEpoch(workspaceId: string) { + const value = syncEpochs.get(workspaceId) + if (!value) throw new Error(`Missing sync epoch for ${workspaceId}`) + return value +} + interface Session { accessToken: string } +async function registerIntegrationAccount(origin: string, label: string) { + const login = `${label}-${randomUUID()}@example.test` + const session = await api(origin, '/v1/auth/register', { + method: 'POST', expectedStatus: 201, + headers: { 'x-setup-token': process.env.INTEGRATION_SETUP_TOKEN ?? 'integration-setup-token' }, + body: { + login, password: 'integration-password', deviceId: randomUUID(), + deviceName: `${label} integration device`, platform: 'test', + }, + }) + const headers = { authorization: `Bearer ${session.accessToken}` } + const account = await api<{ id: string }>(origin, '/v1/account', { headers }) + return { login, accountId: account.id, headers } +} + async function api( origin: string, path: string, @@ -175,5 +257,6 @@ async function api( ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }), }) expect(response.status, await response.clone().text()).toBe(options.expectedStatus ?? 200) + if (response.status === 204) return undefined as T return await response.json() as T } diff --git a/apps/web/README.md b/apps/web/README.md index 9e15bf9..c3b108a 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -7,7 +7,7 @@ NoteGen Server 的账号管理与设备授权页面。该应用使用 Next.js - 注册和登录同步账号。 - 查看和撤销 NoteGen 设备。 - 确认一次性设备授权。 -- 显示自托管或官方托管服务器身份。 +- 显示当前独立实例身份。 它不包含 Markdown 编辑、AI 或多人协作功能。 diff --git a/apps/web/app/account/accept-invite/page.tsx b/apps/web/app/account/accept-invite/page.tsx new file mode 100644 index 0000000..1af0599 --- /dev/null +++ b/apps/web/app/account/accept-invite/page.tsx @@ -0,0 +1,5 @@ +import { InvitationAcceptance } from "@/components/invitation-acceptance" + +export default function AcceptInvitePage() { + return +} diff --git a/apps/web/app/account/reset-password/page.tsx b/apps/web/app/account/reset-password/page.tsx new file mode 100644 index 0000000..54a0387 --- /dev/null +++ b/apps/web/app/account/reset-password/page.tsx @@ -0,0 +1,5 @@ +import { AccountPortal } from "@/components/account-portal" + +export default function ResetPasswordPage() { + return +} diff --git a/apps/web/app/account/verify-email/page.tsx b/apps/web/app/account/verify-email/page.tsx new file mode 100644 index 0000000..89ef0da --- /dev/null +++ b/apps/web/app/account/verify-email/page.tsx @@ -0,0 +1,5 @@ +import { AccountPortal } from "@/components/account-portal" + +export default function VerifyEmailPage() { + return +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index bfdda76..795f38e 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -71,6 +71,16 @@ --chart-3: oklch(0.439 0 0); --chart-4: oklch(0.371 0 0); --chart-5: oklch(0.269 0 0); + --activity-note: oklch(0.62 0.18 255); + --activity-folder: oklch(0.72 0.16 75); + --activity-asset: oklch(0.68 0.14 205); + --activity-canvas: oklch(0.64 0.18 305); + --activity-record: oklch(0.7 0.17 45); + --activity-tag: oklch(0.66 0.16 145); + --activity-conversation: oklch(0.63 0.2 25); + --activity-memory: oklch(0.62 0.16 335); + --activity-setting: oklch(0.58 0.04 255); + --activity-collaboration: oklch(0.65 0.14 180); --radius: 0.625rem; --sidebar: oklch(0.985 0 0); --sidebar-foreground: oklch(0.145 0 0); @@ -106,6 +116,16 @@ --chart-3: oklch(0.439 0 0); --chart-4: oklch(0.371 0 0); --chart-5: oklch(0.269 0 0); + --activity-note: oklch(0.72 0.16 255); + --activity-folder: oklch(0.8 0.14 80); + --activity-asset: oklch(0.76 0.13 205); + --activity-canvas: oklch(0.74 0.16 305); + --activity-record: oklch(0.78 0.15 50); + --activity-tag: oklch(0.75 0.14 145); + --activity-conversation: oklch(0.72 0.18 25); + --activity-memory: oklch(0.72 0.15 335); + --activity-setting: oklch(0.72 0.04 255); + --activity-collaboration: oklch(0.75 0.12 180); --sidebar: oklch(0.205 0 0); --sidebar-foreground: oklch(0.985 0 0); --sidebar-primary: oklch(0.488 0.243 264.376); @@ -127,7 +147,6 @@ body { @apply min-h-svh bg-background text-foreground; - background-image: radial-gradient(circle at top, color-mix(in oklch, var(--primary) 8%, transparent), transparent 32rem); } button:not(:disabled), diff --git a/apps/web/app/icon.svg b/apps/web/app/icon.svg new file mode 100644 index 0000000..858bb5e --- /dev/null +++ b/apps/web/app/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 56ce8ac..58627c6 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -7,7 +7,7 @@ import { LocaleProvider } from "@/components/locale-provider" import { Toaster } from "@/components/ui/toast" export const metadata: Metadata = { - title: "NoteGen 同步管理后台", + title: "NoteGen Sync", description: "管理 NoteGen 同步内容、工作区与关联设备", } diff --git a/apps/web/app/operations/page.tsx b/apps/web/app/operations/page.tsx deleted file mode 100644 index 55301aa..0000000 --- a/apps/web/app/operations/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { OperationsPortal } from "@/components/operations-portal" - -export default function OperationsPage() { - return -} diff --git a/apps/web/app/service/page.tsx b/apps/web/app/service/page.tsx new file mode 100644 index 0000000..d40b494 --- /dev/null +++ b/apps/web/app/service/page.tsx @@ -0,0 +1,108 @@ +import type { Metadata } from "next" +import Link from "next/link" +import { + DatabaseBackupIcon, + HardDriveIcon, + MailIcon, + ServerIcon, + ShieldCheckIcon, + UserRoundCogIcon, +} from "lucide-react" + +import { NOTEGEN_SITE_URL } from "@/components/notegen-brand" +import { PublicSiteShell } from "@/components/site-chrome" +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Separator } from "@/components/ui/separator" + +export const metadata: Metadata = { + title: "服务说明 · NoteGen Sync", + description: "NoteGen 免费独立同步实例的服务范围、数据边界与使用须知", +} + +export default function ServicePage() { + return ( + +
+
+ 免费 · 独立实例 +

服务说明与数据边界

+

+ NoteGen Sync 不区分商业托管版与社区部署版。官方公共测试服务和自行部署实例使用同一套开放源代码、账号体系和管理能力。 +

+
+ + +
+
+ + + + 公共测试服务不是云备份承诺 + + 服务免费提供,不包含商业 SLA。请始终保留本地数据和必要备份;实例可能因维护、资源限制或滥用处理调整开放范围与保留周期。 + + + +
+ + + + +
+ + + + 实例会处理哪些数据 + 实际保留周期由实例配置和管理员维护策略决定。 + + +

服务会保存账号标识、密码哈希、设备与浏览器会话、同步协议元数据、加密后的同步对象与附件,以及保障安全和排查故障所需的有限日志。

+ +

默认托管密钥模式以跨设备零配置为目标,服务器在技术上具备恢复内容密钥的能力;启用高级端到端加密后,服务器只保存加密载荷和密钥封装,用户必须自行保管同步口令或恢复密钥。

+ +

管理员可以停用账号并执行实例提供的删除流程。由于保留期和备份轮换,数据从所有副本中消失可能晚于账号停止使用的时间。

+
+
+ + + + 使用前请确认 + 无论使用公共测试实例还是自行部署,都建议完成以下准备。 + + +
    +
  • 保留 NoteGen 本地数据,不把服务器当作唯一副本。
  • +
  • 为管理员账号设置独立强密码并启用 TOTP。
  • +
  • 自行部署时定期备份数据库和附件存储。
  • +
  • 公开注册前确认存储容量、限流与滥用处理方式。
  • +
+
+
+ + +
+
+ ) +} + +function BoundaryCard({ icon: Icon, title, description }: { + icon: typeof ServerIcon + title: string + description: string +}) { + return ( + + + + {title} + {description} + + + ) +} diff --git a/apps/web/components/account-portal.tsx b/apps/web/components/account-portal.tsx index 7743d20..42e624c 100644 --- a/apps/web/components/account-portal.tsx +++ b/apps/web/components/account-portal.tsx @@ -1,7 +1,8 @@ "use client" -import { useCallback, useEffect, useState, type FormEvent } from "react" +import { useCallback, useEffect, useRef, useState, type FormEvent } from "react" import { + ArrowRightIcon, CircleCheckIcon, HistoryIcon, LaptopIcon, @@ -13,8 +14,12 @@ import { UserRoundCogIcon, UsersIcon, } from "lucide-react" +import QRCode from "react-qr-code" +import { Bar, BarChart, CartesianGrid, XAxis } from "recharts" import { AdminShell, type AdminSection } from "@/components/admin-shell" +import { NOTEGEN_SITE_URL } from "@/components/notegen-brand" +import { PublicSiteShell } from "@/components/site-chrome" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, @@ -22,6 +27,7 @@ import { } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" import { Card, CardContent, @@ -29,7 +35,16 @@ import { CardHeader, CardTitle, } from "@/components/ui/card" +import { + ChartContainer, + ChartLegend, + ChartLegendContent, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart" import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" import { Input } from "@/components/ui/input" import { Item, @@ -46,8 +61,8 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import { Pagination, PaginationContent, PaginationItem, PaginationNext, PaginationPrevious } from "@/components/ui/pagination" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { toast } from "@/components/ui/toast" -import { ThemeToggle } from "@/components/theme-toggle" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { AdminInstanceSummary } from "@/components/admin-instance-summary" import { ExperimentalCenter } from "@/components/experimental-center" import { OperationsCenter } from "@/components/operations-center" @@ -80,8 +95,23 @@ import { formatRelativeTime as formatDate } from "@/lib/relative-time" import { cn } from "@/lib/utils" type AuthMode = "login" | "register" - -export function AccountPortal() { +type AuthStep = "credentials" | "totp" | "forgot" | "reset" | "verify" | "verification-pending" +type AdminDataView = "overview" | "accounts" | "data" | "audit" | "tools" + +const syncActivityKindDefinitions: ChartConfig = { + note: { label: "写作", color: "var(--activity-note)" }, + folder: { label: "目录", color: "var(--activity-folder)" }, + asset: { label: "附件", color: "var(--activity-asset)" }, + canvas: { label: "画布", color: "var(--activity-canvas)" }, + record: { label: "记录", color: "var(--activity-record)" }, + tag: { label: "标签", color: "var(--activity-tag)" }, + conversation: { label: "对话", color: "var(--activity-conversation)" }, + memory: { label: "记忆", color: "var(--activity-memory)" }, + setting: { label: "设置", color: "var(--activity-setting)" }, + collaboration: { label: "实时文档", color: "var(--activity-collaboration)" }, +} satisfies ChartConfig + +export function AccountPortal({ forceAuthenticationFlow = false }: { forceAuthenticationFlow?: boolean }) { const [, setRelativeTimeTick] = useState(0) const [account, setAccount] = useState(null) const [devices, setDevices] = useState([]) @@ -90,6 +120,7 @@ export function AccountPortal() { const [overview, setOverview] = useState(null) const [workspaces, setWorkspaces] = useState([]) const [deletingWorkspaceId, setDeletingWorkspaceId] = useState(null) + const [recentlyDeletedWorkspace, setRecentlyDeletedWorkspace] = useState(null) const [section, setSection] = useState(readSectionFromUrl) const [adminRefreshVersion, setAdminRefreshVersion] = useState(0) const [adminOverview, setAdminOverview] = useState(null) @@ -109,6 +140,7 @@ export function AccountPortal() { const [adminDeviceOffset, setAdminDeviceOffset] = useState(() => readUrlOffset("devicesOffset")) const [adminAuditOffset, setAdminAuditOffset] = useState(() => readUrlOffset("auditOffset")) const [adminAuditAction, setAdminAuditAction] = useState(() => readUrlParam("auditAction")) + const [adminView, setAdminView] = useState(readAdminViewFromUrl) const [adminLoading, setAdminLoading] = useState(false) const [adminError, setAdminError] = useState("") const [adminBusyAccountId, setAdminBusyAccountId] = useState(null) @@ -128,14 +160,12 @@ export function AccountPortal() { if (overviewResult.status === "fulfilled") setOverview(overviewResult.value) if (workspacesResult.status === "fulfilled") setWorkspaces(workspacesResult.value) - const failed = devicesResult.status === "rejected" - ? devicesResult - : overviewResult.status === "rejected" - ? overviewResult - : workspacesResult.status === "rejected" - ? workspacesResult - : null - if (failed) setError(errorMessage(failed.reason)) + const failures = [ + devicesResult.status === "rejected" ? { label: "设备", reason: devicesResult.reason } : null, + overviewResult.status === "rejected" ? { label: "同步概览", reason: overviewResult.reason } : null, + workspacesResult.status === "rejected" ? { label: "工作区", reason: workspacesResult.reason } : null, + ].filter((item): item is { label: string; reason: unknown } => item !== null) + if (failures.length) setError(`部分数据未更新(${failures.map((item) => item.label).join("、")})。${errorMessage(failures[0].reason)}`) }, []) const loadAdminData = useCallback(async () => { @@ -153,32 +183,38 @@ export function AccountPortal() { const auditQuery = new URLSearchParams(commonQuery) auditQuery.set("offset", String(adminAuditOffset)) if (adminAuditAction) auditQuery.set("action", adminAuditAction) - const [overviewResult, accountsResult, auditResult, workspacesResult, devicesResult, statusResult] = await Promise.allSettled([ - apiRequest("/v1/web/admin/overview"), - apiRequest(`/v1/web/admin/accounts?${accountQuery}`), - apiRequest(`/v1/web/admin/audit?${auditQuery}`), - apiRequest(`/v1/web/admin/workspaces?${workspaceQuery}`), - apiRequest(`/v1/web/admin/devices?${deviceQuery}`), - apiRequest("/v1/web/admin/status"), - ]) - if (overviewResult.status === "fulfilled") setAdminOverview(overviewResult.value) - if (accountsResult.status === "fulfilled") { setAdminAccounts(accountsResult.value.accounts); setAdminAccountTotal(accountsResult.value.total) } - if (auditResult.status === "fulfilled") { setAdminAudit(auditResult.value.entries); setAdminAuditTotal(auditResult.value.total) } - if (workspacesResult.status === "fulfilled") { setAdminWorkspaces(workspacesResult.value.workspaces); setAdminWorkspaceTotal(workspacesResult.value.total) } - if (devicesResult.status === "fulfilled") { setAdminDevices(devicesResult.value.devices); setAdminDeviceTotal(devicesResult.value.total) } - if (statusResult.status === "fulfilled") setAdminStatus(statusResult.value) - const failed = [overviewResult, accountsResult, auditResult, workspacesResult, devicesResult, statusResult] - .find((result) => result.status === "rejected") - if (failed?.status === "rejected") setAdminError(errorMessage(failed.reason)) + if (adminView === "overview") { + const [nextOverview, nextStatus] = await Promise.all([ + apiRequest("/v1/web/admin/overview"), + apiRequest("/v1/web/admin/status"), + ]) + setAdminOverview(nextOverview); setAdminStatus(nextStatus) + } else if (adminView === "accounts") { + const page = await apiRequest(`/v1/web/admin/accounts?${accountQuery}`) + setAdminAccounts(page.accounts); setAdminAccountTotal(page.total) + } else if (adminView === "audit") { + const page = await apiRequest(`/v1/web/admin/audit?${auditQuery}`) + setAdminAudit(page.entries); setAdminAuditTotal(page.total) + } else if (adminView === "data") { + const [workspacePage, devicePage] = await Promise.all([ + apiRequest(`/v1/web/admin/workspaces?${workspaceQuery}`), + apiRequest(`/v1/web/admin/devices?${deviceQuery}`), + ]) + setAdminWorkspaces(workspacePage.workspaces); setAdminWorkspaceTotal(workspacePage.total) + setAdminDevices(devicePage.devices); setAdminDeviceTotal(devicePage.total) + } + } catch (cause) { + setAdminError(errorMessage(cause)) } finally { setAdminLoading(false) } - }, [adminAccountOffset, adminAccountStatus, adminAuditAction, adminAuditOffset, adminDeviceOffset, adminQuery, adminWorkspaceOffset]) + }, [adminAccountOffset, adminAccountStatus, adminAuditAction, adminAuditOffset, adminDeviceOffset, adminQuery, adminView, adminWorkspaceOffset]) const loadAccount = useCallback(async () => { try { const session = await apiRequest<{ account: Account }>("/v1/web/session") setAccount(session.account) + setError("") setLoading(false) void loadDashboardData() redirectAfterAuth() @@ -231,6 +267,14 @@ export function AccountPortal() { return () => document.removeEventListener("visibilitychange", onVisibility) }, [account, loadDashboardData]) + useEffect(() => { + if (!account || !["overview", "devices", "workspaces"].includes(section)) return + const timer = window.setInterval(() => { + if (document.visibilityState === "visible") void loadDashboardData() + }, 15_000) + return () => window.clearInterval(timer) + }, [account, loadDashboardData, section]) + useEffect(() => { if (!account) return const timer = window.setInterval(() => { @@ -254,7 +298,7 @@ export function AccountPortal() { }, [account, section]) useEffect(() => { - const onPopState = () => setSection(readSectionFromUrl()) + const onPopState = () => { setSection(readSectionFromUrl()); setAdminView(readAdminViewFromUrl()) } window.addEventListener("popstate", onPopState) return () => window.removeEventListener("popstate", onPopState) }, []) @@ -269,8 +313,9 @@ export function AccountPortal() { setOptionalParam(url, "devicesOffset", adminDeviceOffset ? String(adminDeviceOffset) : "") setOptionalParam(url, "auditOffset", adminAuditOffset ? String(adminAuditOffset) : "") setOptionalParam(url, "auditAction", adminAuditAction) + setOptionalParam(url, "adminView", adminView === "overview" ? "" : adminView) window.history.replaceState(null, "", url) - }, [section, adminQuery, adminAccountStatus, adminAccountOffset, adminWorkspaceOffset, adminDeviceOffset, adminAuditOffset, adminAuditAction]) + }, [section, adminQuery, adminAccountStatus, adminAccountOffset, adminWorkspaceOffset, adminDeviceOffset, adminAuditOffset, adminAuditAction, adminView]) const navigateSection = useCallback((nextSection: AdminSection) => { setSection(nextSection) @@ -336,7 +381,23 @@ export function AccountPortal() { method: "DELETE", csrf: true, }) + setRecentlyDeletedWorkspace(workspaces.find((workspace) => workspace.id === targetWorkspaceId) ?? null) + await loadDashboardData() + } catch (cause) { + setError(errorMessage(cause)) + } finally { + setDeletingWorkspaceId(null) + } + } + + async function handleRestoreWorkspace(targetWorkspaceId: string) { + setDeletingWorkspaceId(targetWorkspaceId) + setError("") + try { + await apiRequest(`/v1/web/workspaces/${targetWorkspaceId}/restore`, { method: "POST", csrf: true }) + setRecentlyDeletedWorkspace(null) await loadDashboardData() + toast.add({ title: "工作区已恢复", type: "success" }) } catch (cause) { setError(errorMessage(cause)) } finally { @@ -380,7 +441,7 @@ export function AccountPortal() { if (loading) { return ( -
+
@@ -397,36 +458,56 @@ export function AccountPortal() { return } - if (!account) { + if (!account || forceAuthenticationFlow) { return ( -
- - {error ? : null} -
- - - NoteGen Sync - 让同步保持清晰、可控。 - - 在这里管理账号、设备和同步空间。敏感操作始终由服务器再次验证,而不只依赖页面状态。 - - - - - - - - - + +
+
+
+
+ + NoteGen Sync · 跨设备同步 + +

+ 连接设备,
保持同步。 +

+

+ 管理 NoteGen 账号、设备和同步空间。官方公共测试服务与自行部署使用相同的开放源代码和运行方式。 +

+
+ + + +
+

+ 还没有 NoteGen? + + 下载客户端 + + +

+
+
+ {error ? : null} + + + + 免费独立实例 + + 本服务不包含付费订阅或商业 SLA。请保留本地数据;实例维护、开放范围和数据保留策略由实例管理员负责。查看服务说明 + + +
+
+
) } @@ -456,15 +537,17 @@ export function AccountPortal() { }} > {error ? : null} - {section === "overview" ? <>{account.isAdmin ? : null} : null} + {section === "overview" ? <>{account.isAdmin ? : null} : null} {section === "instance" && account.isAdmin ? : null} {section === "experiments" && account.isAdmin ? : null} {section === "operations" && account.isAdmin ? : null} {section === "workspaces" ? ( ) : null} {section === "devices" ? ( @@ -472,6 +555,7 @@ export function AccountPortal() { devices={devices} busy={busy} onRevoke={(id) => void handleRevokeDevice(id)} + onConnect={() => navigateSection("connect")} /> ) : null} {section === "connect" ? : null} @@ -489,6 +573,7 @@ export function AccountPortal() { devices={adminDevices} deviceTotal={adminDeviceTotal} status={adminStatus} + adminView={adminView} query={adminQuery} accountStatus={adminAccountStatus} accountOffset={adminAccountOffset} @@ -500,6 +585,7 @@ export function AccountPortal() { error={adminError} busyAccountId={adminBusyAccountId} onRefresh={() => void loadAdminData()} + onAdminViewChange={setAdminView} onSetSuspended={handleSetAccountSuspended} onSetAdmin={handleSetAccountAdmin} onQueryChange={(value) => { @@ -518,40 +604,12 @@ export function AccountPortal() { ) } -function LoginHeader({ capabilities }: { capabilities: ServerCapabilities | null }) { - return ( -
-
- - - -
-

{capabilities?.serverName ?? "NoteGen 同步服务器"}

-

登录同步管理后台

-
-
-
- {capabilities ? ( - <> - - {capabilities.deploymentMode === "hosted" ? "官方托管" : "自托管"} - - - {capabilities.registrationMode === "open" ? "开放注册" : "关闭注册"} - - - ) : null} - -
-
- ) -} - function ErrorAlert({ message }: { message: string }) { + const connectionFailed = message.includes("无法连接同步服务器") return ( - 操作失败 + {connectionFailed ? "暂时无法连接" : "操作失败"} {message} ) @@ -560,31 +618,60 @@ function ErrorAlert({ message }: { message: string }) { function LoginBenefit({ icon: Icon, title, - description, }: { icon: typeof ShieldCheckIcon title: string - description: string }) { return ( -
- - - -
-

{title}

-

{description}

-
+
+ + {title}
) } -function OverviewSection({ overview }: { overview: SyncOverview | null }) { +function OverviewSection({ overview, devices, onNavigate }: { + overview: SyncOverview | null + devices: Device[] + onNavigate: (section: AdminSection) => void +}) { const contentStats = summarizeContentKinds(overview?.kinds ?? []) + const activityChart = summarizeSyncActivity( + overview?.activityTimeline ?? [], + overview?.activityKinds ?? [], + ) + const storage = overview?.storageUsage + const activeBytes = storage ? sumByteStrings( + storage.activeObjectBytes, storage.activeCrdtBytes, storage.activeBlobBytes, + ) : "0" + const enforcedBytes = storage ? sumByteStrings( + activeBytes, storage.reservedBlobBytes, storage.retainedBytes, + ) : "0" + const activeDevices = devices.filter((device) => !device.revokedAt) + const behindDevices = activeDevices.filter((device) => device.syncStatus === "behind") + const health = activeDevices.length === 0 + ? { label: "等待连接设备", description: "关联第一台 NoteGen 设备后,服务器会自动建立同步空间。", variant: "outline" as const } + : behindDevices.length > 0 + ? { label: `${behindDevices.length} 台设备尚未追平`, description: "打开对应设备上的 NoteGen,让它保持联网以完成同步。", variant: "destructive" as const } + : { label: "同步正常", description: `${activeDevices.length} 台设备已连接${overview?.lastActivityAt ? `,最近同步于 ${formatDate(overview.lastActivityAt)}` : ",正在等待首次同步"}。`, variant: "secondary" as const } return ( <> - + + +
+ {health.label} +
+ 你的同步状态 + {health.description} +
+
+ +
+
+ 同步概览 汇总当前账号的同步对象、物理存储、序列和加密状态;已移除附件会按保留策略延迟回收。 @@ -613,6 +700,23 @@ function OverviewSection({ overview }: { overview: SyncOverview | null }) { + {storage ? ( + + + 存储核算 + + 服务端按活跃数据、上传预留和历史保留的总和核算安全上限。删除内容后,历史保留空间会按保留策略延迟释放。 + + + + + + + + + + ) : null} + 内容数量 @@ -634,25 +738,62 @@ function OverviewSection({ overview }: { overview: SyncOverview | null }) { 最近同步活动 - 显示设备、内容类型和操作,不展示文件名或正文。 + 按分钟汇总最近 1 小时内发生变化的对象,不展示文件名或正文。 - {overview?.recentActivity.length ? ( - - {overview.recentActivity.slice(0, 10).map((activity) => ( - - - - {activity.changeType === "delete" ? "删除" : "更新"} {kindLabel(activity.kind)} - {activity.device.name} · {formatDate(activity.createdAt)} - - #{activity.sequence} - - ))} - - ) : ( -

还没有同步活动。

- )} + {activityChart.total ? ( +
+
+ + + + +
+ +
+
+
+
活动趋势
+
每根细柱表示一分钟内发生变化的唯一对象,不同颜色代表不同内容类型
+
+
+ + + + + } /> + } /> + {activityChart.series.map((item) => ( + + ))} + + +
+ {activityChart.kinds.map((item) => ( + + + ))} +
+
+
+ ) : onNavigate("connect")} />}
@@ -661,29 +802,41 @@ function OverviewSection({ overview }: { overview: SyncOverview | null }) { ) } +function sumByteStrings(...values: string[]): string { + return values.reduce((total, value) => total + BigInt(value || "0"), 0n).toString() +} + function WorkspaceManagement({ workspaces, + recentlyDeletedWorkspace, deletingWorkspaceId, onDelete, + onRestore, }: { workspaces: WebWorkspace[] + recentlyDeletedWorkspace: WebWorkspace | null deletingWorkspaceId: string | null onDelete: (id: string) => Promise + onRestore: (id: string) => Promise }) { const sorted = [...workspaces].sort((left, right) => ( Number(right.isDefault) - Number(left.isDefault) + || Number(right.isNoteGenDefault) - Number(left.isNoteGenDefault) || right.objectCount - left.objectCount || new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime() )) - const historicalCount = workspaces.filter((workspace) => !workspace.isDefault).length + const historicalCount = workspaces.filter((workspace) => ( + !workspace.isDefault && !workspace.isNoteGenDefault + )).length return ( 工作区列表 - 查看工作区状态与用量;默认工作区受到保护,历史工作区可在确认后软删除。 + 查看个人数据空间、默认写作工作区和历史工作区;只有真正的历史工作区可以软删除。 + {recentlyDeletedWorkspace ? 历史工作区已删除工作区 {recentlyDeletedWorkspace.id.slice(0, 8)} 已进入软删除状态,可以立即恢复。 : null} {historicalCount ? ( @@ -693,21 +846,30 @@ function WorkspaceManagement({ ) : null} {sorted.length ? ( - {sorted.map((workspace) => ( - - {workspace.isDefault ? : } + {sorted.map((workspace) => { + const protectedWorkspace = workspace.isDefault || workspace.isNoteGenDefault + const title = workspace.isDefault + ? "个人数据空间" + : workspace.isNoteGenDefault + ? `默认写作工作区 ${workspace.id.slice(0, 8)}` + : `历史工作区 ${workspace.id.slice(0, 8)}` + const kindLabel = workspace.isDefault + ? "个人数据" + : workspace.isNoteGenDefault ? "默认写作" : "历史" + return + {protectedWorkspace ? : } - {workspace.isDefault ? "当前默认工作区" : `历史工作区 ${workspace.id.slice(0, 8)}`} + {title} {workspace.objectCount} 项内容 · {workspace.deletedObjectCount} 项删除记录 · 创建于 {formatDate(workspace.createdAt)} - - {workspace.isDefault ? "默认" : "历史"} + + {kindLabel} {workspace.encryptionMode === "managed" ? "托管加密" : "E2EE"} - {!workspace.isDefault ? : null} - ))} + })} - ) : ( - - - 还没有同步工作区 - 连接 NoteGen 并完成首次同步后,工作区会显示在这里。 - - )} + ) : } ) @@ -735,54 +891,106 @@ function DeviceManagement({ devices, busy, onRevoke, + onConnect, }: { devices: Device[] busy: boolean onRevoke: (id: string) => void + onConnect: () => void }) { + const activeDevices = sortDevices(devices.filter((device) => !device.revokedAt)) + const revokedDevices = sortDevices(devices.filter((device) => Boolean(device.revokedAt))) + return ( 已关联设备 - 每台设备拥有独立会话,可以单独撤销;撤销不会删除已同步内容。 + 当前共 {activeDevices.length} 台有效设备。历史授权是已经失效的登录身份,不代表新增了物理设备;撤销不会删除已同步内容。 - - {devices.length ? ( + + {activeDevices.length ? ( - {sortDevices(devices).map((device) => ( - - - - {device.name} - - {device.platform} · 最近活动 {formatDate(device.lastSeenAt)} · 设备 ID:{device.id} - - {isLikelyDuplicateDevice(device, devices) ? 疑似重复设备 : null} - - - {device.revokedAt ? ( - 已撤销 - ) : ( - onRevoke(device.id)} - /> - )} - - + {activeDevices.map((device) => ( + ))} - ) : ( -

还没有关联 NoteGen 设备。

- )} + ) : } + + {revokedDevices.length ? ( + + + + 历史授权({revokedDevices.length}) + + + + {revokedDevices.map((device) => ( + + ))} + + + + ) : null}
) } +function DeviceManagementItem({ device, devices, busy, onRevoke }: { + device: Device + devices: Device[] + busy: boolean + onRevoke: (id: string) => void +}) { + return ( + + + + {device.name} + + {device.platform} · 最近活动 {formatDate(device.lastSeenAt)} · 关联于 {formatDate(device.createdAt)} + +
+ + {device.acknowledgedAt ? ( + 最后确认 {formatDate(device.acknowledgedAt)} + ) : null} +
+ {isLikelyDuplicateDevice(device, devices) ? 疑似重复设备 : null} +
+ + {device.revokedAt ? ( + 已撤销 + ) : ( + onRevoke(device.id)} + /> + )} + +
+ ) +} + +function DeviceSyncBadge({ device }: { device: Device }) { + if (device.syncStatus === "caught-up") return 已追平 + if (device.syncStatus === "behind") return {device.pendingEventCount} 个对象有变更 + return 从未确认同步 +} + +function ActionEmpty({ icon: Icon, title, description, action, onAction }: { + icon: typeof LaptopIcon + title: string + description: string + action?: string + onAction?: () => void +}) { + return {title}{description}{action && onAction ? : null} +} + function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) { const [currentPassword, setCurrentPassword] = useState("") const [newPassword, setNewPassword] = useState("") @@ -792,15 +1000,20 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) const [sessions, setSessions] = useState>([]) + const [sessionsLoading, setSessionsLoading] = useState(true) const [totpEnabled, setTotpEnabled] = useState(initiallyEnabled) const [totpPassword, setTotpPassword] = useState("") const [totpCode, setTotpCode] = useState("") const [totpSecret, setTotpSecret] = useState("") + const [totpUri, setTotpUri] = useState("") + const [deletionPassword, setDeletionPassword] = useState("") const mismatch = confirmPassword.length > 0 && newPassword !== confirmPassword const loadSessions = useCallback(async () => { + setSessionsLoading(true) try { setSessions(await apiRequest("/v1/web/sessions")) } catch (cause) { setError(errorMessage(cause)) } + finally { setSessionsLoading(false) } }, []) useEffect(() => { void loadSessions() }, [loadSessions]) @@ -822,6 +1035,7 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) method: "POST", csrf: true, body: JSON.stringify({ currentPassword: totpPassword }), }) setTotpSecret(setup.secret) + setTotpUri(setup.uri) } catch (cause) { setError(errorMessage(cause)) } finally { setBusy(false) } } @@ -830,7 +1044,8 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) setBusy(true); setError("") try { await apiRequest("/v1/web/auth/totp/enable", { method: "POST", csrf: true, body: JSON.stringify({ code: totpCode }) }) - setTotpEnabled(true); setTotpSecret(""); setTotpPassword(""); setTotpCode("") + setTotpEnabled(true); setTotpSecret(""); setTotpUri(""); setTotpPassword(""); setTotpCode("") + toast.add({ title: "双因素认证已启用", description: "下次登录时需要输入验证器验证码。", type: "success" }) } catch (cause) { setError(errorMessage(cause)) } finally { setBusy(false) } } @@ -842,6 +1057,7 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) method: "DELETE", csrf: true, body: JSON.stringify({ currentPassword: totpPassword, code: totpCode }), }) setTotpEnabled(false); setTotpPassword(""); setTotpCode("") + toast.add({ title: "双因素认证已关闭", type: "success" }) } catch (cause) { setError(errorMessage(cause)) } finally { setBusy(false) } } @@ -871,15 +1087,32 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) } } + async function requestAccountDeletion() { + setBusy(true) + setError("") + try { + await apiRequest("/v1/web/account", { + method: "DELETE", + csrf: true, + body: JSON.stringify({ password: deletionPassword, confirmation: "DELETE" }), + }) + window.location.assign("/") + } catch (cause) { + setError(errorMessage(cause)) + setBusy(false) + } + } + return ( -
+
+ {error ? : null} +
修改密码 修改后会撤销旧设备登录凭据并注销其他浏览器会话,当前浏览器会自动保持登录。 - {error ? : null}
@@ -909,7 +1142,7 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) 浏览器会话检查登录位置,并注销当前浏览器之外的全部会话。 - {sessions.map((session) => ( + {sessionsLoading ?
: sessions.length ? {sessions.map((session) => ( {session.current ? "当前浏览器" : "其他浏览器"} @@ -917,8 +1150,8 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) {session.current ? 当前 : null} - ))} -
@@ -928,17 +1161,44 @@ function PasswordManagement({ initiallyEnabled }: { initiallyEnabled: boolean }) {totpEnabled ? "已启用" : "未启用"} 当前密码 setTotpPassword(event.target.value)} /> - {totpSecret ? 添加到验证器密钥:{totpSecret} : null} + {totpSecret ?
扫描二维码使用验证器应用扫描二维码,然后输入生成的 6 位验证码。{totpSecret}
: null} 6 位验证码 setTotpCode(event.target.value.replace(/\D/g, "").slice(0, 6))} /> {totpEnabled ? ( ) : totpSecret ? ( - +
) : ( )}
+ + 停用并删除账号账号会立即停止登录并撤销设备凭据;数据进入实例配置的保留期,之后由维护任务清理。 + + 此操作会中断所有设备同步请先确认 NoteGen 本地数据完整。最后一个可用管理员不能删除自己的账号。 + + 当前密码 + setDeletionPassword(event.target.value)} /> + 输入密码后仍需在确认窗口中执行操作。 + + + }> + 停用并删除账号 + + + + 确认停用并删除账号? + 所有浏览器和设备会立即退出,服务器数据将在保留期结束后清理。请确认本地数据已经保存。 + + + 取消 + void requestAccountDeletion()}>确认删除 + + + + + +
) } @@ -955,6 +1215,7 @@ function SystemManagement({ devices, deviceTotal, status, + adminView, query, accountStatus, accountOffset, @@ -966,6 +1227,7 @@ function SystemManagement({ error, busyAccountId, onRefresh, + onAdminViewChange, onSetSuspended, onSetAdmin, onQueryChange, @@ -987,6 +1249,7 @@ function SystemManagement({ devices: AdminDevice[] deviceTotal: number status: AdminSystemStatus | null + adminView: AdminDataView query: string accountStatus: string accountOffset: number @@ -998,6 +1261,7 @@ function SystemManagement({ error: string busyAccountId: string | null onRefresh: () => void + onAdminViewChange: (view: AdminDataView) => void onSetSuspended: (accountId: string, suspended: boolean) => Promise onSetAdmin: (accountId: string, isAdmin: boolean) => Promise onQueryChange: (value: string) => void @@ -1015,6 +1279,8 @@ function SystemManagement({ const [webSessions, setWebSessions] = useState([]) const [backups, setBackups] = useState([]) const [storageReport, setStorageReport] = useState(null) + const [queryDraft, setQueryDraft] = useState(query) + useEffect(() => setQueryDraft(query), [query]) async function batchSuspend(suspended: boolean) { setManagementError("") @@ -1078,25 +1344,47 @@ function SystemManagement({ } catch (cause) { setManagementError(errorMessage(cause)) } } + async function exportData(scope: "accounts" | "workspaces" | "devices" | "audit") { + setManagementError("") + try { + await downloadAdminExport(scope) + } catch (cause) { + setManagementError(errorMessage(cause)) + } + } + return ( <> {error ? : null} {managementError ? : null} - + { if (value) onAdminViewChange(value as AdminDataView) }}> + 总览 + 账号 + 同步数据 + 审计 + 运维 + + {adminView !== "overview" && adminView !== "tools" ? 查询与导出 搜索账号、工作区 ID、设备或审计目标;导出文件不包含密码、令牌和解密密钥。 - - onQueryChange(event.target.value)} /> + + { event.preventDefault(); onQueryChange(queryDraft.trim()) }}> + setQueryDraft(event.target.value)} /> + + {query ? : null} + +
{(["accounts", "workspaces", "devices", "audit"] as const).map((scope) => ( - ))} +
-
- + : null} + {adminView === "overview" ?
服务器总览 @@ -1129,10 +1417,10 @@ function SystemManagement({ detail={`${overview?.auditCount ?? 0} 条后台审计记录`} /> - + : null}
- + {adminView === "accounts" ? 账号管理 @@ -1248,9 +1536,9 @@ function SystemManagement({ )} - + : null} - + {adminView === "audit" ? 操作审计 保留最近 100 条后台数据变更,便于确认谁在何时执行了什么操作。 @@ -1291,15 +1579,15 @@ function SystemManagement({ )} - + : null}
-
+ {adminView === "data" ?
-
+
: null} - + {adminView === "tools" ?
运维工具管理浏览器会话、旧备份记录和对象存储一致性。
@@ -1327,9 +1615,9 @@ function SystemManagement({
:

点击“检查”扫描数据库和对象存储。

}
-
+
: null} - + {adminView === "overview" ? 运行与存储状态实时查看数据库响应、进程资源和同步数据规模。 @@ -1337,7 +1625,7 @@ function SystemManagement({ - + : null} ) } @@ -1434,45 +1722,96 @@ async function downloadAdminExport(scope: "accounts" | "workspaces" | "devices" function AuthCard({ busy, registrationMethods, - deploymentMode, setBusy, onAuthenticated, onError, }: { busy: boolean registrationMethods: string[] - deploymentMode: "hosted" | "self-hosted" setBusy: (value: boolean) => void onAuthenticated: (account: Account) => Promise onError: (message: string) => void }) { const [mode, setMode] = useState("login") + const [step, setStep] = useState("credentials") const [login, setLogin] = useState("") const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") const [setupToken, setSetupToken] = useState("") + const [showRecoveryOptions, setShowRecoveryOptions] = useState(false) const [totpCode, setTotpCode] = useState("") const [notice, setNotice] = useState("") + const [cooldownSeconds, setCooldownSeconds] = useState(0) + const verificationAttempted = useRef(false) + const token = readAuthToken() const canRegister = registrationMethods.some((method) => ( method === "setup" || method === "password" || method === "email-password" )) const isAdministratorSetup = registrationMethods.includes("setup") + useEffect(() => setStep(readInitialAuthStep()), []) + useEffect(() => { + if (!token || readInitialAuthStep() !== "verify" || verificationAttempted.current) return + verificationAttempted.current = true + setStep("verify") + void verifyEmailToken() + // The action token is immutable for this route; the ref prevents Strict Mode retries. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]) + useEffect(() => { + if (cooldownSeconds <= 0) return + const timer = window.setTimeout(() => setCooldownSeconds((current) => Math.max(0, current - 1)), 1_000) + return () => window.clearTimeout(timer) + }, [cooldownSeconds]) + async function handleSubmit(event: FormEvent) { event.preventDefault() setBusy(true) onError("") setNotice("") try { - if (mode === "register" && deploymentMode === "hosted") { - await apiRequest("/v1/web/auth/register/email", { + if (step === "forgot") { + await apiRequest("/v1/web/auth/password-reset/request", { method: "POST", - body: JSON.stringify({ email: login, password }), + body: JSON.stringify({ email: login }), }) - setPassword("") - setNotice("验证邮件已进入内部测试投递流程。验证完成后即可登录。") + setNotice("如果该邮箱已注册,重置链接会发送到邮箱。请检查收件箱和垃圾邮件。") + setCooldownSeconds(60) + return + } + if (step === "verify") { + await verifyEmailToken() + return + } + if (step === "reset") { + if (password !== confirmPassword) { + onError("两次输入的新密码不一致。") + return + } + await apiRequest("/v1/web/auth/password-reset/complete", { + method: "POST", + body: JSON.stringify({ token, newPassword: password }), + }) + const result = await apiRequest<{ account: Account }>("/v1/web/session") + replaceAuthUrl() + await onAuthenticated(result.account) + window.location.assign("/") return } - if (mode === "register" && deploymentMode === "self-hosted" && isAdministratorSetup) { + if (step === "verification-pending") { + await apiRequest("/v1/web/auth/email/resend", { + method: "POST", + body: JSON.stringify({ email: login }), + }) + setNotice("验证邮件已重新发送,请检查收件箱和垃圾邮件。") + setCooldownSeconds(60) + return + } + if (mode === "register" && password !== confirmPassword) { + onError("两次输入的密码不一致。") + return + } + if (mode === "register" && isAdministratorSetup) { const result = await apiRequest<{ account: Account }>("/v1/setup/complete", { method: "POST", body: JSON.stringify({ login, password, token: setupToken.trim() }), @@ -1484,7 +1823,7 @@ function AuthCard({ } const body = mode === "register" ? { login, password, ...(setupToken.trim() ? { setupToken: setupToken.trim() } : {}) } - : { login, password, ...(totpCode.trim() ? { totpCode: totpCode.trim() } : {}) } + : { login, password, ...(step === "totp" ? { totpCode: totpCode.trim() } : {}) } const result = await apiRequest<{ account: Account }>(`/v1/web/auth/${mode}`, { method: "POST", body: JSON.stringify(body), @@ -1493,6 +1832,38 @@ function AuthCard({ setSetupToken("") setTotpCode("") await onAuthenticated(result.account) + } catch (cause) { + if (isApiRequestError(cause) && cause.code === "totp_required" && step !== "totp") { + setStep("totp") + setTotpCode("") + onError("") + return + } + if (isApiRequestError(cause) && cause.code === "email_verification_required") { + setStep("verification-pending") + onError("") + setNotice("登录前需要先验证邮箱。") + return + } + onError(errorMessage(cause)) + } finally { + setBusy(false) + } + } + + async function verifyEmailToken() { + setBusy(true) + onError("") + setNotice("") + try { + await apiRequest("/v1/web/auth/email/verify", { + method: "POST", + body: JSON.stringify({ token }), + }) + setStep("credentials") + setMode("login") + setNotice("邮箱验证成功,现在可以登录。") + replaceAuthUrl() } catch (cause) { onError(errorMessage(cause)) } finally { @@ -1501,31 +1872,35 @@ function AuthCard({ } return ( - + - 连接你的同步账号 + {authStepTitle(step)} - {deploymentMode === "hosted" && !canRegister - ? "当前官方托管测试实例暂未开放注册。" + {step === "totp" ? `输入 ${login} 的验证器验证码。` + : step === "forgot" ? "输入注册邮箱,我们会发送一个限时重置链接。" + : step === "reset" ? "设置新密码后会自动登录,并使旧登录凭据失效。" + : step === "verify" ? busy ? "正在自动验证邮箱,请稍候。" : "自动验证未完成,你可以重新验证或返回登录。" + : step === "verification-pending" ? `验证邮件已发送至 ${login || "你的邮箱"}。` : "同一账号可连接多个 NoteGen 设备。账号密码不会用于解密端到端加密工作区。"} - setMode(value as AuthMode)}> - + {step === "credentials" ? { setMode(value as AuthMode); setConfirmPassword(""); setNotice(""); onError("") }}> + {canRegister ? 登录 - {canRegister ? 注册 : null} - + 注册 + : null} -
+ - {deploymentMode === "hosted" ? "邮箱" : "账号"} + 账号 setLogin(event.target.value)} autoComplete="username" + maxLength={200} required /> @@ -1538,13 +1913,23 @@ function AuthCard({ onChange={(event) => setPassword(event.target.value)} autoComplete={mode === "login" ? "current-password" : "new-password"} minLength={8} + maxLength={1024} required /> 至少 8 个字符,仅用于账号登录;默认同步无需另设加密口令。 - {mode === "register" && deploymentMode === "self-hosted" && isAdministratorSetup ? ( + {mode === "register" ? 0 && password !== confirmPassword || undefined}> + 确认密码 + setConfirmPassword(event.target.value)} autoComplete="new-password" minLength={8} maxLength={1024} aria-invalid={confirmPassword.length > 0 && password !== confirmPassword || undefined} required /> + {confirmPassword.length > 0 && password !== confirmPassword ? 两次输入的密码不一致。 : null} + : null} + {mode === "register" && isAdministratorSetup && !showRecoveryOptions ? + + 仅灾备恢复时需要,正常创建管理员无需填写。 + : null} + {mode === "register" && isAdministratorSetup && showRecoveryOptions ? ( - 一次性恢复凭据 + 一次性恢复凭据(可选) setSetupToken(event.target.value)} autoComplete="off" /> - 灾备恢复场景请填写本机 CLI 签发的一次性初始化凭据。 - - ) : null} - {mode === "login" ? ( - - 双因素验证码(可选) - setTotpCode(event.target.value.replace(/\D/g, "").slice(0, 6))} /> - 账号启用双因素认证后填写验证器中的 6 位数字。 + 仅灾备恢复场景填写本机 CLI 签发的一次性初始化凭据。 ) : null} {notice ? ( @@ -1570,7 +1948,7 @@ function AuthCard({ ) : null} - @@ -1578,15 +1956,53 @@ function AuthCard({
-
+ : ( +
+ + {step === "totp" ? 6 位验证码 setTotpCode(event.target.value.replace(/\D/g, "").slice(0, 6))} required />请输入验证器当前显示的数字,验证码每 30 秒更新一次。 : null} + {step === "forgot" ? 邮箱 setLogin(event.target.value)} required /> : null} + {step === "reset" ? <>新密码 setPassword(event.target.value)} required />至少 8 个字符。 0 && password !== confirmPassword || undefined}>确认新密码 setConfirmPassword(event.target.value)} aria-invalid={confirmPassword.length > 0 && password !== confirmPassword || undefined} required />{confirmPassword.length > 0 && password !== confirmPassword ? 两次输入的新密码不一致。 : null} : null} + {step === "verify" && !token ? 验证链接不完整请重新打开邮件中的完整链接。 : null} + {notice ? 操作已提交{notice} : null} +
+
+
+ )}
) } +function authStepTitle(step: AuthStep): string { + return ({ credentials: "连接你的同步账号", totp: "双因素验证", forgot: "找回密码", reset: "设置新密码", verify: "验证邮箱", "verification-pending": "检查你的邮箱" })[step] +} + +function readInitialAuthStep(): AuthStep { + if (typeof window === "undefined") return "credentials" + if (window.location.pathname.startsWith("/account/verify-email")) return "verify" + if (window.location.pathname.startsWith("/account/reset-password")) return "reset" + return "credentials" +} + +function readAuthToken(): string { + return typeof window === "undefined" ? "" : new URLSearchParams(window.location.search).get("token") ?? "" +} + +function replaceAuthUrl() { + window.history.replaceState(null, "", "/") +} + function redirectAfterAuth() { const next = new URLSearchParams(window.location.search).get("next") - if (next?.startsWith("/") && !next.startsWith("//")) window.location.assign(next) + if (!next) return + try { + const target = new URL(next, window.location.origin) + if (target.origin === window.location.origin && target.pathname.startsWith("/")) { + window.location.assign(`${target.pathname}${target.search}${target.hash}`) + } + } catch { + // Invalid redirect targets are ignored and the account portal remains open. + } } const adminSections = new Set([ @@ -1616,6 +2032,11 @@ function readUrlOffset(name: string): number { return Number.isSafeInteger(value) && value >= 0 ? value : 0 } +function readAdminViewFromUrl(): AdminDataView { + const value = readUrlParam("adminView") + return value === "accounts" || value === "data" || value === "audit" || value === "tools" ? value : "overview" +} + function setOptionalParam(url: URL, name: string, value: string): void { if (value) url.searchParams.set(name, value) else url.searchParams.delete(name) @@ -1623,7 +2044,7 @@ function setOptionalParam(url: URL, name: string, value: string): void { function Metric({ label, value, detail }: { label: string; value: string; detail: string }) { return ( -
+

{label}

{value}

{detail}

@@ -1654,22 +2075,62 @@ function isLikelyDuplicateDevice(device: Device, devices: Device[]): boolean { return newest.id !== device.id } -function kindLabel(kind: string): string { - const labels: Record = { - note: "Markdown 笔记", - folder: "目录", - asset: "附件", - canvas: "画布", - record: "记录", - tag: "标签", - mark: "记录", - conversation: "对话", - memory: "记忆", - setting: "设置", - "yjs-checkpoint": "实时文档快照", - "yjs-update": "实时文档增量", +function summarizeSyncActivity( + timeline: SyncOverview["activityTimeline"], + kinds: SyncOverview["activityKinds"], +) { + const seriesKeys = [...new Set([ + ...kinds.map((item) => activityKindKey(item.kind)), + ...timeline.flatMap((bucket) => bucket.kinds.map((item) => activityKindKey(item.kind))), + ])] + const config = Object.fromEntries(seriesKeys.map((key) => [ + key, + syncActivityKindDefinitions[key] ?? { label: key, color: "var(--chart-5)" }, + ])) satisfies ChartConfig + const chartBuckets = timeline.map((bucket) => { + const values = Object.fromEntries(seriesKeys.map((key) => [key, 0])) + for (const item of bucket.kinds) { + const key = activityKindKey(item.kind) + values[key] = (values[key] ?? 0) + item.updates + item.deletes + } + return { + key: bucket.startedAt, + label: new Intl.DateTimeFormat("zh-CN", { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }).format(new Date(bucket.startedAt)), + ...values, + } + }) + const updates = timeline.reduce((total, bucket) => total + bucket.updates, 0) + const deletes = timeline.reduce((total, bucket) => total + bucket.deletes, 0) + const kindCounts = new Map() + for (const item of kinds) { + const key = activityKindKey(item.kind) + kindCounts.set(key, (kindCounts.get(key) ?? 0) + item.count) } - return labels[kind] ?? kind + return { + buckets: chartBuckets, + total: updates + deletes, + updates, + deletes, + config, + series: seriesKeys.map((key) => ({ key })), + kinds: seriesKeys.map((key) => ({ + key, + label: String(config[key]?.label ?? key), + color: config[key]?.color ?? "var(--chart-5)", + count: kindCounts.get(key) ?? 0, + })), + } +} + +function activityKindKey(kind: string): string { + if (kind === "mark" || kind === "record") return "record" + if (kind === "message" || kind === "conversation") return "conversation" + if (kind === "yjs-checkpoint" || kind === "yjs-update") return "collaboration" + return kind } function summarizeContentKinds(kinds: SyncOverview["kinds"]): Array<{ @@ -1678,9 +2139,9 @@ function summarizeContentKinds(kinds: SyncOverview["kinds"]): Array<{ deletedCount: number }> { const groups = [ - { label: "笔记", kinds: ["note"] }, + { label: "写作", kinds: ["note"] }, { label: "记录", kinds: ["mark", "record"] }, - { label: "绘图", kinds: ["canvas"] }, + { label: "画布", kinds: ["canvas"] }, { label: "对话", kinds: ["conversation"] }, { label: "记忆", kinds: ["memory"] }, { label: "配置", kinds: ["setting"] }, @@ -1706,6 +2167,7 @@ function auditActionLabel(action: string): string { "test-object.create": "创建测试内容", "object.delete": "删除同步内容", "workspace.delete": "删除历史工作区", + "workspace.restore": "恢复历史工作区", "workspace.admin-delete": "管理员删除工作区", "device.revoke": "撤销设备授权", "device.admin-revoke": "管理员撤销设备", diff --git a/apps/web/components/admin-shell.tsx b/apps/web/components/admin-shell.tsx index 43a60a9..b0480bc 100644 --- a/apps/web/components/admin-shell.tsx +++ b/apps/web/components/admin-shell.tsx @@ -2,20 +2,21 @@ import type { ReactNode } from "react" import { + BookOpenIcon, DatabaseIcon, + ExternalLinkIcon, FlaskConicalIcon, GaugeIcon, LayoutDashboardIcon, - LaptopIcon, + LinkIcon, KeyRoundIcon, + MonitorSmartphoneIcon, MailIcon, LogOutIcon, RefreshCwIcon, - ServerIcon, UsersIcon, } from "lucide-react" -import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { DropdownMenu, @@ -26,7 +27,6 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { Separator } from "@/components/ui/separator" import { Sidebar, SidebarContent, @@ -46,7 +46,15 @@ import { } from "@/components/ui/sidebar" import { Spinner } from "@/components/ui/spinner" import { ThemeToggle } from "@/components/theme-toggle" +import { + NOTEGEN_DOCS_URL, + NOTEGEN_SITE_URL, + GitHubMark, + NoteGenMark, + NoteGenWordmark, +} from "@/components/notegen-brand" import type { Account, ServerCapabilities } from "@/lib/api" +import { SiteFooter } from "@/components/site-chrome" export type AdminSection = "overview" | "workspaces" | "devices" | "connect" | "security" | "instance" | "operations" | "admin" | "experiments" @@ -89,8 +97,8 @@ export function AdminShell({ type NavigationItem = { section: AdminSection; label: string; icon: typeof LayoutDashboardIcon; count?: number } const personalNavigation: NavigationItem[] = [ { section: "overview" as const, label: "仪表盘", icon: LayoutDashboardIcon }, - { section: "connect" as const, label: "关联新设备", icon: LaptopIcon }, - { section: "devices" as const, label: "设备管理", icon: LaptopIcon, count: deviceCount }, + { section: "connect" as const, label: "关联新设备", icon: LinkIcon }, + { section: "devices" as const, label: "设备管理", icon: MonitorSmartphoneIcon, count: deviceCount }, { section: "workspaces" as const, label: "工作区管理", icon: DatabaseIcon, count: workspaceCount }, { section: "security" as const, label: "账户安全", icon: KeyRoundIcon }, ] @@ -130,14 +138,19 @@ export function AdminShell({ - - - - + } + > + - {capabilities?.serverName ?? "NoteGen"} - 同步管理后台 + + + {capabilities?.serverName ?? "同步服务"} + + @@ -175,7 +188,7 @@ export function AdminShell({ 账户安全 onSectionChange("connect")}> - + 关联新设备 @@ -194,26 +207,46 @@ export function AdminShell({ - -
+ +
-

{details.title}

+

{details.title}

{details.description}

-
- {capabilities ? ( - <> - - {capabilities.deploymentMode === "hosted" ? "官方托管" : "自托管"} - - - {capabilities.registrationMode === "open" ? "开放注册" : "关闭注册"} - - - ) : null} +
+ + +
- -
{children}
+
{children}
+
) diff --git a/apps/web/components/device-connection.tsx b/apps/web/components/device-connection.tsx index de76b38..865d5f1 100644 --- a/apps/web/components/device-connection.tsx +++ b/apps/web/components/device-connection.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useState } from "react" +import { useEffect, useRef, useState } from "react" import Link from "next/link" import { CheckCircle2Icon, CopyIcon, ExternalLinkIcon, LaptopIcon, QrCodeIcon, RefreshCwIcon, ShieldCheckIcon, Trash2Icon, XCircleIcon } from "lucide-react" import QRCode from "react-qr-code" @@ -12,6 +12,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field" import { Input } from "@/components/ui/input" import { Spinner } from "@/components/ui/spinner" +import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { ThemeToggle } from "@/components/theme-toggle" import { apiRequest, isApiRequestError, userFacingErrorMessage, type DeviceAuthorization } from "@/lib/api" @@ -43,10 +44,13 @@ export function DeviceConnection({ const [serverAddress, setServerAddress] = useState("") const [serverAddressError, setServerAddressError] = useState("") const [serverAddressCopied, setServerAddressCopied] = useState(false) + const [method, setMethod] = useState<"code" | "qr">("code") + const authorizationRequestId = useRef(0) + const activePairing = useRef<{ id: string, status: DevicePairingStatus | null } | null>(null) useEffect(() => { const initialCode = new URLSearchParams(window.location.search).get("code") ?? "" - setCode(initialCode) + setCode(formatDeviceCode(initialCode)) if (initialCode) { const url = new URL(window.location.href) url.pathname = "/connect/" @@ -63,13 +67,24 @@ export function DeviceConnection({ }) .catch((cause) => { if (isApiRequestError(cause) && cause.status === 401) { - redirectToLogin() + setSignedIn(false) return } setError(errorMessage(cause)) }) }, []) + useEffect(() => { + activePairing.current = pairing ? { id: pairing.id, status: pairingStatus } : null + }, [pairing, pairingStatus]) + + useEffect(() => () => { + const current = activePairing.current + if (current?.status === "pending") { + void apiRequest(`/v1/web/device-pairings/${current.id}`, { method: "DELETE", csrf: true }).catch(() => undefined) + } + }, []) + useEffect(() => { if (!pairing || pairingStatus !== "pending") return const updateRemaining = () => { @@ -83,7 +98,12 @@ export function DeviceConnection({ void apiRequest<{ status: DevicePairingStatus, expiresAt: string }>( `/v1/web/device-pairings/${pairing.id}` ).then((result) => setPairingStatus(result.status)).catch((cause) => { - if (!(isApiRequestError(cause) && cause.status === 404)) setPairingError(errorMessage(cause)) + if (isApiRequestError(cause) && cause.status === 404) { + setPairingStatus("expired") + setPairingError("") + return + } + setPairingError(errorMessage(cause)) }) }, 2_000) return () => { @@ -148,23 +168,53 @@ export function DeviceConnection({ } } + async function changeMethod(nextMethod: "code" | "qr") { + if (nextMethod === method) return + if (nextMethod === "qr") { + setMethod("qr") + if (!pairing || pairingStatus !== "pending") void createPairing() + return + } + if (pairing?.id && pairingStatus === "pending") { + setPairingBusy(true) + setPairingError("") + try { + await apiRequest(`/v1/web/device-pairings/${pairing.id}`, { method: "DELETE", csrf: true }) + } catch (cause) { + if (!(isApiRequestError(cause) && cause.status === 404)) { + setPairingError(errorMessage(cause)) + setPairingBusy(false) + return + } + } + setPairingBusy(false) + } + setPairing(null) + setPairingStatus(null) + setMethod("code") + } + async function loadAuthorization(value = code) { + const requestId = authorizationRequestId.current + 1 + authorizationRequestId.current = requestId setBusy(true) setError("") try { const result = await apiRequest( `/v1/web/device-authorizations/${encodeURIComponent(value)}` ) + if (requestId !== authorizationRequestId.current) return setAuthorization(result) if (result.status === "approved" || result.status === "denied") { setCompleted(result.status) removeAuthorizationCodeFromUrl() } } catch (cause) { + if (requestId !== authorizationRequestId.current) return setAuthorization(null) setError(errorMessage(cause)) } finally { - setBusy(false) + if (requestId === authorizationRequestId.current) setBusy(false) } } @@ -174,6 +224,7 @@ export function DeviceConnection({ try { await navigator.clipboard.writeText(serverAddress) setServerAddressCopied(true) + window.setTimeout(() => setServerAddressCopied(false), 2_000) } catch { setServerAddressError("复制失败,请手动选择并复制服务器地址。") } @@ -213,7 +264,9 @@ export function DeviceConnection({
)} -
+ {signedIn ? { if (value) void changeMethod(value as "code" | "qr") }}>输入验证码手机扫码 : null} +
+ {method === "code" ? ( 关联 NoteGen 设备 @@ -244,12 +297,7 @@ export function DeviceConnection({ {completed === "approved" ? "设备授权已确认,可以直接打开 NoteGen 完成关联。" : "这次授权不会签发任何设备会话。"} - {completed === "approved" ? ( - - ) : null} + {completed === "approved" ? 请返回刚才显示验证码的 NoteGen,客户端会自动继续连接。 : null} ) : ( @@ -291,9 +339,12 @@ export function DeviceConnection({ id="device-code" value={code} onChange={(event) => { - setCode(event.target.value.toUpperCase()) + const nextCode = formatDeviceCode(event.target.value) + authorizationRequestId.current += 1 + setCode(nextCode) setAuthorization(null) setCompleted(null) + if (signedIn && nextCode.length === 9) void loadAuthorization(nextCode) }} placeholder="ABCD-EFGH" autoCapitalize="characters" @@ -302,7 +353,7 @@ export function DeviceConnection({ 验证码由 NoteGen 客户端显示,5 分钟内有效且只能使用一次。 {signedIn && !authorization ? ( - @@ -349,7 +400,8 @@ export function DeviceConnection({ ) : null} - {signedIn ? ( + ) : null} + {signedIn && method === "qr" ? ( @@ -384,7 +436,7 @@ export function DeviceConnection({ 二维码已过期 - 请刷新二维码后再扫描。 + 二维码可能已过期或被撤销,请重新生成后再扫描。 ) : ( <> @@ -392,6 +444,7 @@ export function DeviceConnection({
剩余 {formatRemainingTime(remainingSeconds)} + {remainingSeconds === 60 ? "二维码还有一分钟过期" : remainingSeconds === 10 ? "二维码还有十秒过期" : ""}

二维码包含一次性配对凭据。扫码成功或倒计时结束后会自动失效。

@@ -429,6 +482,11 @@ export function DeviceConnection({ ) } +function formatDeviceCode(value: string): string { + const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "").slice(0, 8) + return normalized.length > 4 ? `${normalized.slice(0, 4)}-${normalized.slice(4)}` : normalized +} + const errorMessage = userFacingErrorMessage function formatRemainingTime(totalSeconds: number): string { @@ -437,13 +495,6 @@ function formatRemainingTime(totalSeconds: number): string { return `${minutes}:${seconds.toString().padStart(2, "0")}` } -function redirectToLogin(): void { - const returnPath = `${window.location.pathname}${window.location.search}` - const loginUrl = new URL("/", window.location.origin) - loginUrl.searchParams.set("next", returnPath) - window.location.replace(loginUrl.toString()) -} - function removeAuthorizationCodeFromUrl(): void { const url = new URL(window.location.href) url.searchParams.delete("code") diff --git a/apps/web/components/experimental-center.tsx b/apps/web/components/experimental-center.tsx index 2c875f7..86533de 100644 --- a/apps/web/components/experimental-center.tsx +++ b/apps/web/components/experimental-center.tsx @@ -28,7 +28,7 @@ export function ExperimentalCenter({ refreshVersion = 0 }: { refreshVersion?: nu return
实验入口不会绕过服务端安全门槛这里保留尚未稳定开放的功能及其启用条件。不可用项目只展示状态,不会调用对应写接口。 - 实验能力{data?.deploymentMode === "self-hosted" ? "当前为自托管实例;托管专属能力会明确标记为不可用。" : "当前为官方托管实例。"} + 实验能力当前为独立实例;实验能力只有满足服务端安全条件后才可启用。 {rows.map((item) => )}
@@ -43,11 +43,6 @@ function capabilityTitle(id: string): string { "identity.email": "邮箱身份", "identity.emailVerification": "邮箱验证", "identity.passwordReset": "密码找回", - "risk.advanced": "高级风控", - "usage.enforcement": "用量强制限制", - "billing.subscription": "订阅与权益", - "compliance.requests": "合规数据请求", - "support.cases": "客服工单", "operations.unifiedBackup": "统一备份", "operations.upgradeAssistant": "升级助手", "operations.preserveRestore": "原实例恢复", diff --git a/apps/web/components/installation-guide.tsx b/apps/web/components/installation-guide.tsx index 191b664..ddc207c 100644 --- a/apps/web/components/installation-guide.tsx +++ b/apps/web/components/installation-guide.tsx @@ -1,226 +1,125 @@ "use client" -import { useEffect, useState, type FormEvent, type ReactNode } from "react" -import { - ArrowLeftIcon, - ArrowRightIcon, - Building2Icon, - CheckIcon, - Globe2Icon, - ServerIcon, - Settings2Icon, - ShieldCheckIcon, - UserRoundCogIcon, -} from "lucide-react" +import { useEffect, useRef, useState, type FormEvent, type ReactNode } from "react" +import { ShieldCheckIcon, UserRoundCogIcon } from "lucide-react" -import { LanguageToggle } from "@/components/language-toggle" import { useLocale, type Locale } from "@/components/locale-provider" -import { ThemeToggle } from "@/components/theme-toggle" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card" +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field" import { Input } from "@/components/ui/input" -import { Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "@/components/ui/item" -import { Separator } from "@/components/ui/separator" import { Spinner } from "@/components/ui/spinner" -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { apiRequest, isApiRequestError } from "@/lib/api" export interface InstallationStatus { installationRequired: boolean activationPending: boolean - deploymentMode: "self-hosted" | "hosted" | null serverName: string } -type DeploymentMode = "self-hosted" | "hosted" -type RegistrationPolicy = "disabled" | "public" - const COPY = { "zh-CN": { - product: "NoteGen 同步服务器", - guide: "Web 安装向导", firstInstall: "首次安装", - steps: ["部署模式", "实例配置", "确认安装"], - stepDescriptions: ["确定服务运行方式", "填写实例初始信息", "核对并写入数据库"], - modeTitle: "选择部署模式", - modeDescription: "部署模式决定账号体系、管理权限和可用服务。", - modeLabel: "使用方式", - selfHosted: "自托管", - selfHostedDescription: "适合个人、家庭或社区,自主管理账号与数据。", - hosted: "运营模式", - hostedDescription: "适合平台运营,目前使用安全的内部测试适配器。", - immutableMode: "初始化完成后,当前数据库不能直接切换部署模式。", - configurationTitle: "配置实例信息", - selfHostedConfigurationDescription: "设置服务名称并创建首位管理员。", - hostedConfigurationDescription: "设置服务名称、创建首位运营管理员,并选择客户注册策略。", - serverName: "服务名称", - serverNameDescription: "显示在登录页和客户端连接信息中,之后可在后台修改。", + title: "创建管理员账号", + description: "这个账号将拥有实例管理权限。登录后可以继续配置注册、邮件、存储限制和数据保留策略。", administratorLogin: "管理员账号", administratorPassword: "管理员密码", - passwordDescription: "至少 8 个字符。安装完成后使用此账号登录 Web 管理后台。", - operationsAdministratorLogin: "运营管理员账号", - operationsAdministratorPassword: "运营管理员密码", - operationsPasswordDescription: "至少 8 个字符。此账号只用于运营后台,不是客户账号。", - registration: "新用户注册", - disabled: "关闭", - public: "公开", - registrationDescription: "运营人员使用独立的 Staff 权限域,不会成为客户账号管理员。", - internalTitle: "内部测试配置", - internalDescription: "计费使用 Mock,邮件写入脱敏日志,不会真实扣款或投递邮件。", - reviewTitle: "确认安装配置", - reviewDescription: "请检查下面的信息。提交后部署模式将固化到数据库。", - reviewMode: "部署模式", - reviewServer: "服务名称", - reviewAdministrator: "初始管理员", - reviewOperationsAdministrator: "首位运营管理员", - reviewRegistration: "注册策略", - beforePublicTitle: "请先安装,再开放公网服务", - beforePublicDescription: "空实例允许首位访问者完成初始化。完成本向导后,再配置公网反向代理或开放端口。", - back: "上一步", - continue: "继续", - install: "保存并完成初始化", + passwordDescription: "至少 8 个字符,请使用独立密码。", + confirmPassword: "确认密码", + passwordMismatch: "两次输入的密码不一致。", + securityTitle: "完成初始化后再开放公网", + securityDescription: "空实例允许首位访问者创建管理员。请先完成此步骤,再配置公开反向代理或开放端口。", + install: "创建管理员并启动", installing: "正在初始化", failed: "安装失败", - saved: "安装配置已保存", - activatingTitle: "正在启动", - activatingDescription: (mode: DeploymentMode) => `正在启用${mode === "hosted" ? "运营模式" : "自托管模式"}。`, + saved: "安装设置已保存", + activatingTitle: "正在启动实例", + activatingDescription: "实例正在加载配置,完成后会自动进入登录页。", }, en: { - product: "NoteGen Sync Server", - guide: "Web installation guide", - firstInstall: "First-time setup", - steps: ["Deployment", "Instance setup", "Review"], - stepDescriptions: ["Select how the service runs", "Enter the initial settings", "Review and save to the database"], - modeTitle: "Choose a deployment mode", - modeDescription: "The deployment mode determines the account model, administrative permissions, and available services.", - modeLabel: "Deployment type", - selfHosted: "Self-hosted", - selfHostedDescription: "For individuals, families, or communities that manage their own accounts and data.", - hosted: "Operations mode", - hostedDescription: "For a managed platform. Safe internal-test adapters are currently used.", - immutableMode: "The deployment mode cannot be changed in this database after installation.", - configurationTitle: "Configure the instance", - selfHostedConfigurationDescription: "Name the service and create the first administrator.", - hostedConfigurationDescription: "Name the service, create the first operations administrator, and choose the customer registration policy.", - serverName: "Service name", - serverNameDescription: "Shown on the sign-in page and in client connection details. It can be changed later.", + firstInstall: "First installation", + title: "Create an administrator account", + description: "This account will have full instance administration access. Registration, email, storage limits, and retention policies can be configured after signing in.", administratorLogin: "Administrator account", administratorPassword: "Administrator password", - passwordDescription: "Use at least 8 characters. This account signs in to the Web administration portal.", - operationsAdministratorLogin: "Operations administrator account", - operationsAdministratorPassword: "Operations administrator password", - operationsPasswordDescription: "Use at least 8 characters. This account is only for the operations portal and is not a customer account.", - registration: "New user registration", - disabled: "Disabled", - public: "Public", - registrationDescription: "Operators use a separate Staff permission realm and are not customer account administrators.", - internalTitle: "Internal-test configuration", - internalDescription: "Billing uses a mock provider and mail goes to a redacted log. No real charge or delivery occurs.", - reviewTitle: "Review installation", - reviewDescription: "Check the settings below. Submitting will make the deployment mode permanent for this database.", - reviewMode: "Deployment mode", - reviewServer: "Service name", - reviewAdministrator: "Initial administrator", - reviewOperationsAdministrator: "First operations administrator", - reviewRegistration: "Registration policy", - beforePublicTitle: "Install before exposing the service", - beforePublicDescription: "The first visitor to an empty instance can initialize it. Finish this guide before configuring a public reverse proxy or opening the port.", - back: "Back", - continue: "Continue", - install: "Save and finish installation", - installing: "Installing", + passwordDescription: "Use at least 8 characters and a password unique to this service.", + confirmPassword: "Confirm password", + passwordMismatch: "The passwords do not match.", + securityTitle: "Finish initialization before exposing the server", + securityDescription: "The first visitor to an empty instance can create its administrator. Complete this page before configuring a public reverse proxy or opening the port.", + install: "Create administrator and start", + installing: "Initializing", failed: "Installation failed", saved: "Installation settings saved", - activatingTitle: "Starting", - activatingDescription: (mode: DeploymentMode) => `${mode === "hosted" ? "Operations mode" : "Self-hosted mode"} is being enabled.`, + activatingTitle: "Starting the instance", + activatingDescription: "The instance is loading its configuration. You will be redirected to sign in when it is ready.", }, } as const -export function InstallationGuide({ - initialStatus, - onStatusChange, -}: { +export function InstallationGuide({ initialStatus, onStatusChange }: { initialStatus: InstallationStatus onStatusChange: (status: InstallationStatus) => void }) { const { locale } = useLocale() const c = COPY[locale] - const [step, setStep] = useState(0) - const [deploymentMode, setDeploymentMode] = useState("self-hosted") - const [registrationPolicy, setRegistrationPolicy] = useState("disabled") - const [serverName, setServerName] = useState(initialStatus.serverName) const [login, setLogin] = useState("") const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") const [busy, setBusy] = useState(false) const [error, setError] = useState("") + const [activationWaitSeconds, setActivationWaitSeconds] = useState(0) + const activationCheckBusy = useRef(false) useEffect(() => { if (!initialStatus.activationPending) return let active = true - const installedMode = initialStatus.deploymentMode ?? "self-hosted" const checkReady = async () => { + if (activationCheckBusy.current) return + activationCheckBusy.current = true try { const status = await apiRequest("/v1/installation/status") if (active && !status.activationPending) { - window.location.assign(installedMode === "hosted" ? "/operations/" : "/") + if (status.installationRequired) onStatusChange(status) + else window.location.assign("/") } } catch { // The API is briefly unavailable while it replaces the installation app. + } finally { + activationCheckBusy.current = false } } void checkReady() - const timer = window.setInterval(() => void checkReady(), 500) + const timer = window.setInterval(() => { + setActivationWaitSeconds((current) => current + 1) + void checkReady() + }, 1_000) return () => { active = false window.clearInterval(timer) } - }, [initialStatus.activationPending, initialStatus.deploymentMode]) + }, [initialStatus.activationPending, onStatusChange]) - const canContinue = step !== 1 || ( - serverName.trim().length > 0 - && login.trim().length > 0 - && password.length >= 8 - ) + const passwordMismatch = confirmPassword.length > 0 && password !== confirmPassword + const canSubmit = login.trim().length > 0 && password.length >= 8 && password === confirmPassword async function handleSubmit(event: FormEvent) { event.preventDefault() - if (step < 2) { - setStep((current) => Math.min(2, current + 1)) - return - } + if (!canSubmit) return setBusy(true) setError("") try { - const result = await apiRequest<{ - deploymentMode: DeploymentMode - serverName: string - activationPending: true - }>("/v1/installation/complete", { + const result = await apiRequest<{ serverName: string, activationPending: true }>("/v1/installation/complete", { method: "POST", body: JSON.stringify({ - deploymentMode, - serverName, - administrator: { login, password }, - ...(deploymentMode === "hosted" ? { hostedRegistrationPolicy: registrationPolicy } : {}), + serverName: initialStatus.serverName, + administrator: { login: login.trim(), password }, }), }) setPassword("") - onStatusChange({ - installationRequired: false, - activationPending: result.activationPending, - deploymentMode: result.deploymentMode, - serverName: result.serverName, - }) + setConfirmPassword("") + onStatusChange({ installationRequired: false, activationPending: result.activationPending, serverName: result.serverName }) } catch (cause) { setError(installationErrorMessage(cause, locale)) } finally { @@ -229,15 +128,26 @@ export function InstallationGuide({ } if (initialStatus.activationPending) { - const installedMode = initialStatus.deploymentMode ?? "self-hosted" return ( - + {c.saved} {c.activatingTitle} - {c.activatingDescription(installedMode)} + {c.activatingDescription} + {activationWaitSeconds >= 30 ? ( + + + + {locale === "zh-CN" ? "启动时间比预期更长" : "Startup is taking longer than expected"} + + {locale === "zh-CN" ? "配置已经保存。请确认服务进程正在运行;也可以重新检查当前状态。" : "Your settings are saved. Confirm that the service process is running, or check the current status again."} + + + + + ) : null} ) @@ -245,162 +155,50 @@ export function InstallationGuide({ return ( - + - {c.firstInstall} - {c.steps[step]} - {c.stepDescriptions[step]} +
+ +
+ {c.firstInstall} + {c.title} + {c.description}
-
- - -
- {step === 0 ? ( - - - - {c.modeLabel} - { - const value = values[0] - if (value === "self-hosted" || value === "hosted") setDeploymentMode(value) - }} - > - - - - {c.selfHosted} - {c.selfHostedDescription} - - - - - - {c.hosted} - {c.hostedDescription} - - - - {c.immutableMode} - - - - ) : null} - - {step === 1 ? ( - - - - {c.serverName} - setServerName(event.target.value)} maxLength={100} required /> - {c.serverNameDescription} - - - - {deploymentMode === "hosted" ? c.operationsAdministratorLogin : c.administratorLogin} - - setLogin(event.target.value)} autoComplete="username" maxLength={200} required /> - - - - {deploymentMode === "hosted" ? c.operationsAdministratorPassword : c.administratorPassword} - - setPassword(event.target.value)} autoComplete="new-password" minLength={8} maxLength={1024} required /> - - {deploymentMode === "hosted" ? c.operationsPasswordDescription : c.passwordDescription} - - - {deploymentMode === "hosted" ? ( - - {c.registration} - { - const value = values[0] - if (value === "disabled" || value === "public") setRegistrationPolicy(value) - }} - variant="outline" - spacing={0} - > - {c.disabled} - {c.public} - - {c.registrationDescription} - - ) : null} - {deploymentMode === "hosted" ? ( - - - {c.internalTitle} - {c.internalDescription} - - ) : null} - - - ) : null} - - {step === 2 ? ( - -
- - - - - {deploymentMode === "hosted" - ? - : null} - - - - {c.beforePublicTitle} - {c.beforePublicDescription} - - {error ? ( - - - {c.failed} - {error} - - ) : null} -
-
- ) : null} -
+ + + + {c.administratorLogin} + setLogin(event.target.value)} autoComplete="username" maxLength={200} autoFocus required /> + + + {c.administratorPassword} + setPassword(event.target.value)} autoComplete="new-password" minLength={8} maxLength={1024} required /> + {c.passwordDescription} + + + {c.confirmPassword} + setConfirmPassword(event.target.value)} autoComplete="new-password" minLength={8} maxLength={1024} aria-invalid={passwordMismatch || undefined} required /> + {passwordMismatch ? {c.passwordMismatch} : null} + + + + + {c.securityTitle} + {c.securityDescription} + + {error ? ( + + + {c.failed} + {error} + + ) : null} - - - @@ -409,88 +207,20 @@ export function InstallationGuide({ ) } -function StepSection({ - title, - description, - icon: Icon, - children, -}: { - title: string - description: string - icon: typeof ServerIcon - children: ReactNode -}) { - return ( -
-
- -
-

{title}

-

{description}

-
-
- {children} -
- ) -} - -function ReviewItem({ icon: Icon, title, description }: { icon: typeof ServerIcon, title: string, description: string }) { - return ( - - - - {title} - {description} - - - ) -} - function InstallationLayout({ children }: { children: ReactNode }) { - const { locale } = useLocale() - const c = COPY[locale] - return ( -
-
-
- -
-

{c.product}

-

{c.guide}

-
-
-
- - -
-
- {children} -
- ) + return
{children}
} function installationErrorMessage(cause: unknown, locale: Locale): string { if (locale === "zh-CN") { if (isApiRequestError(cause)) { - const messages: Record = { - installation_already_completed: "实例已经完成安装。", - installation_existing_data: "数据库中已经存在账号数据,不能作为全新实例初始化。", - installation_server_name_invalid: "服务名称不能为空,且不能超过 100 个字符。", - installation_administrator_required: "必须创建首位管理员账号。", - installation_administrator_invalid: "管理员账号格式不正确。", - } + const messages: Record = { installation_already_completed: "实例已经完成安装。", installation_existing_data: "数据库中已经存在账号数据,不能作为全新实例初始化。", installation_server_name_invalid: "服务名称配置无效。", installation_administrator_required: "必须创建首位管理员账号。", installation_administrator_invalid: "管理员账号格式不正确。" } return messages[cause.code] ?? `安装请求失败(HTTP ${cause.status})。` } return cause instanceof TypeError ? "无法连接同步服务器,请确认服务已启动。" : "安装未完成,请稍后重试。" } if (isApiRequestError(cause)) { - const messages: Record = { - installation_already_completed: "This instance has already been installed.", - installation_existing_data: "The database already contains account data and cannot be initialized as a new instance.", - installation_server_name_invalid: "Enter a service name no longer than 100 characters.", - installation_administrator_required: "An initial administrator account is required.", - installation_administrator_invalid: "The administrator account format is invalid.", - } + const messages: Record = { installation_already_completed: "This instance has already been installed.", installation_existing_data: "The database already contains account data and cannot be initialized as a new instance.", installation_server_name_invalid: "The configured service name is invalid.", installation_administrator_required: "An initial administrator account is required.", installation_administrator_invalid: "The administrator account format is invalid." } return messages[cause.code] ?? `The installation request failed (HTTP ${cause.status}).` } return cause instanceof TypeError ? "Could not connect to the sync server. Make sure it is running." : "Installation did not finish. Try again." diff --git a/apps/web/components/invitation-acceptance.tsx b/apps/web/components/invitation-acceptance.tsx new file mode 100644 index 0000000..d9fe924 --- /dev/null +++ b/apps/web/components/invitation-acceptance.tsx @@ -0,0 +1,81 @@ +"use client" + +import { useEffect, useState, type FormEvent } from "react" +import Link from "next/link" +import { CircleCheckIcon, TicketIcon } from "lucide-react" + +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" +import { Button, buttonVariants } from "@/components/ui/button" +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Spinner } from "@/components/ui/spinner" +import { apiRequest, userFacingErrorMessage, type Account } from "@/lib/api" +import { PublicSiteShell } from "@/components/site-chrome" + +interface InvitationInspection { + canContinue: boolean + requiresEmail: boolean + serverName: string +} + +export function InvitationAcceptance() { + const [inspection, setInspection] = useState(null) + const [login, setLogin] = useState("") + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [busy, setBusy] = useState(true) + const [fatalError, setFatalError] = useState("") + const [submitError, setSubmitError] = useState("") + const token = typeof window === "undefined" ? "" : new URLSearchParams(window.location.search).get("token") ?? "" + const mismatch = confirmPassword.length > 0 && password !== confirmPassword + + async function inspectInvitation() { + if (!token) { setBusy(false); setFatalError("邀请链接不完整,请重新打开管理员发送的完整链接。"); return } + setBusy(true) + setInspection(null) + setFatalError("") + try { + const result = await apiRequest("/v1/invitations/inspect", { + method: "POST", body: JSON.stringify({ token }), + }) + setInspection(result) + if (!result.canContinue) setFatalError("邀请链接无效、已经过期或已被使用。") + } catch (cause) { + setFatalError(userFacingErrorMessage(cause)) + } finally { + setBusy(false) + } + } + + useEffect(() => { + void inspectInvitation() + // The bearer token is immutable for the lifetime of this page. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]) + + async function accept(event: FormEvent) { + event.preventDefault() + if (!inspection?.canContinue || mismatch) return + setBusy(true); setSubmitError("") + try { + await apiRequest<{ account: Account }>("/v1/web/auth/register/invitation", { + method: "POST", + body: JSON.stringify({ token, login, password, ...(inspection.requiresEmail ? { email } : {}) }), + }) + window.location.assign("/") + } catch (cause) { + setSubmitError(userFacingErrorMessage(cause)) + } finally { setBusy(false) } + } + + return
+ +
接受 NoteGen 邀请{inspection?.serverName ? `创建账号并加入 ${inspection.serverName}。` : "正在检查邀请链接…"}
+ + {busy && !inspection ?
: fatalError ?
无法使用邀请{fatalError}
返回登录
: inspection?.canContinue ?
邀请有效填写下面的信息即可创建账号,完成后会自动登录。{submitError ? 暂时无法创建账号{submitError} 请修改后重试。 : null}账号 { setLogin(event.target.value); setSubmitError("") }} required />{inspection.requiresEmail ? 受邀邮箱 { setEmail(event.target.value); setSubmitError("") }} required />请输入收到邀请邮件的邮箱地址,用于确认你是受邀人。 : null}密码 { setPassword(event.target.value); setSubmitError("") }} required />至少 8 个字符。确认密码 { setConfirmPassword(event.target.value); setSubmitError("") }} aria-invalid={mismatch || undefined} required />{mismatch ? 两次输入的密码不一致。 : null}
: null} +
+
+
+} diff --git a/apps/web/components/locale-provider.tsx b/apps/web/components/locale-provider.tsx index 2a2926f..af11c82 100644 --- a/apps/web/components/locale-provider.tsx +++ b/apps/web/components/locale-provider.tsx @@ -18,7 +18,7 @@ export function LocaleProvider({ children }: { children: ReactNode }) { const stored = window.localStorage.getItem("notegen-locale") const nextLocale = stored === "en" || stored === "zh-CN" ? stored - : window.navigator.language.toLowerCase().startsWith("zh") ? "zh-CN" : "en" + : "zh-CN" setLocaleState(nextLocale) document.documentElement.lang = nextLocale }, []) diff --git a/apps/web/components/notegen-brand.tsx b/apps/web/components/notegen-brand.tsx new file mode 100644 index 0000000..87ba97c --- /dev/null +++ b/apps/web/components/notegen-brand.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils" + +export const NOTEGEN_SITE_URL = "https://notegen.top" +export const NOTEGEN_DOCS_URL = "https://notegen.top/cn/docs" +export const NOTEGEN_LOGO_URL = "https://s2.loli.net/2025/08/05/IceAMqnBJytp2wE.png" + +export function NoteGenMark({ className }: { className?: string }) { + return ( + + ) +} + +export function NoteGenWordmark({ className }: { className?: string }) { + return NOTEGEN. +} + +export function GitHubMark({ className }: { className?: string }) { + return ( + + ) +} diff --git a/apps/web/components/operations-center.tsx b/apps/web/components/operations-center.tsx index 8ed524c..242edc6 100644 --- a/apps/web/components/operations-center.tsx +++ b/apps/web/components/operations-center.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useState } from "react" -import { MailIcon, SendIcon, Settings2Icon, TicketIcon } from "lucide-react" +import { CopyIcon, MailIcon, SendIcon, Settings2Icon, TicketIcon } from "lucide-react" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Badge } from "@/components/ui/badge" @@ -14,7 +14,7 @@ import { Spinner } from "@/components/ui/spinner" import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { toast } from "@/components/ui/toast" import { - apiRequest, userFacingErrorMessage, + apiRequest, isApiRequestError, userFacingErrorMessage, type AdminInvitation, type AdminMailItem, type AdminMailStatus, type RuntimeConfiguration, type RuntimeConfigurationResponse, type ServerCapabilities, } from "@/lib/api" @@ -27,6 +27,9 @@ export function OperationsCenter({ refreshVersion = 0 }: { refreshVersion?: numb const [mail, setMail] = useState(null) const [queue, setQueue] = useState([]) const [password, setPassword] = useState("") + const [totpCode, setTotpCode] = useState("") + const [totpRequired, setTotpRequired] = useState(false) + const [latestInvitationUrl, setLatestInvitationUrl] = useState("") const [registration, setRegistration] = useState(null) const [nextPolicy, setNextPolicy] = useState("disabled") const [configuration, setConfiguration] = useState(null) @@ -73,15 +76,31 @@ export function OperationsCenter({ refreshVersion = 0 }: { refreshVersion?: numb if (failed?.status === "rejected") setError(userFacingErrorMessage(failed.reason)) }, []) useEffect(() => { void load() }, [load, refreshVersion]) + + async function requestStepUp(audience: "registration.invitation.create" | "registration.policy.update" | "runtime.configuration.update", requestHash: string) { + try { + const grant = await apiRequest<{ token: string }>("/v1/web/auth/step-up", { + method: "POST", csrf: true, + body: JSON.stringify({ audience, requestHash, password, ...(totpCode.length === 6 ? { totpCode } : {}) }), + }) + setTotpRequired(false) + setTotpCode("") + return grant + } catch (cause) { + if (isApiRequestError(cause) && cause.code === "totp_required") setTotpRequired(true) + throw cause + } + } + async function createInvitation() { const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() const body = { expiresAt, maxUses: 1, send: false } setBusy(true); setError("") try { const requestHash = await digest(body) - const grant = await apiRequest<{ token: string }>("/v1/web/auth/step-up", { method: "POST", csrf: true, body: JSON.stringify({ audience: "registration.invitation.create", requestHash, password }) }) + const grant = await requestStepUp("registration.invitation.create", requestHash) const invitation = await apiRequest<{ url: string }>("/v1/web/admin/invitations", { method: "POST", csrf: true, headers: { "x-step-up-token": grant.token }, body: JSON.stringify(body) }) - setPassword(""); toast.add({ title: "邀请已创建", description: invitation.url, type: "success" }); await load() + setPassword(""); setLatestInvitationUrl(invitation.url); toast.add({ title: "邀请已创建", description: "请复制并发送给受邀用户。", type: "success" }); await load() } catch (cause) { setError(userFacingErrorMessage(cause)) } finally { setBusy(false) } } @@ -90,7 +109,7 @@ export function OperationsCenter({ refreshVersion = 0 }: { refreshVersion?: numb setBusy(true); setError("") try { const requestHash = await digest(body) - const grant = await apiRequest<{ token: string }>("/v1/web/auth/step-up", { method: "POST", csrf: true, body: JSON.stringify({ audience: "registration.policy.update", requestHash, password }) }) + const grant = await requestStepUp("registration.policy.update", requestHash) await apiRequest("/v1/web/admin/registration-policy", { method: "PUT", csrf: true, headers: { "x-step-up-token": grant.token }, body: JSON.stringify(body) }) setPassword(""); toast.add({ title: "注册策略已保存", type: "success" }); await load() } catch (cause) { setError(userFacingErrorMessage(cause)) } @@ -98,6 +117,10 @@ export function OperationsCenter({ refreshVersion = 0 }: { refreshVersion?: numb } async function updateConfiguration() { if (!configuration) return + if (totpRequired && totpCode.length !== 6) { + setError("请输入验证器中的 6 位验证码。") + return + } setBusy(true); setError("") try { const body = { @@ -106,10 +129,7 @@ export function OperationsCenter({ refreshVersion = 0 }: { refreshVersion?: numb ...(clearSmtpPassword ? { smtpPassword: null } : smtpPassword.length > 0 ? { smtpPassword } : {}), } const requestHash = await digest(body) - const grant = await apiRequest<{ token: string }>("/v1/web/auth/step-up", { - method: "POST", csrf: true, - body: JSON.stringify({ audience: "runtime.configuration.update", requestHash, password }), - }) + const grant = await requestStepUp("runtime.configuration.update", requestHash) const response = await apiRequest("/v1/web/admin/configuration", { method: "PUT", csrf: true, headers: { "x-step-up-token": grant.token }, body: JSON.stringify(body), }) @@ -128,14 +148,17 @@ export function OperationsCenter({ refreshVersion = 0 }: { refreshVersion?: numb setConfiguration((current) => current === null ? null : { ...current, [key]: Number(value) }) } const configurationDirty = JSON.stringify(configuration) !== JSON.stringify(savedConfiguration) + const stepUpReady = password.length >= 8 && (!totpRequired || totpCode.length === 6) return
{error ? 实例运维操作失败{error} : null} + {totpRequired ? 还需要双因素验证码输入验证器当前显示的 6 位数字,然后再次执行刚才的保存或创建操作。6 位验证码 setTotpCode(event.target.value.replace(/\D/g, "").slice(0, 6))} /> : null} + {latestInvitationUrl ? 邀请链接已创建
: null} {configuration ? {configurationDirty ? "运行配置有未保存更改" : "运行配置已保存"}{lastSavedAt ? `最近保存 ${formatRelativeTime(lastSavedAt)}。` : `当前配置版本 ${configurationRevision}。`} : null} - 注册策略{registration?.deploymentMode === "hosted" ? "官方托管实例的策略由控制面管理,此处仅显示当前状态。" : "保存后立即写入实例数据库,不需要重启服务或修改环境变量。"}允许新账号加入 { if (value) setNextPolicy(value as RegistrationPolicy) }}>关闭仅邀请公开{nextPolicy === "public" ? "任何人都可以创建账号。" : nextPolicy === "invitation" ? "仅持有管理员邀请链接的用户可注册。" : "不接受新的注册请求。"}{registration?.deploymentMode !== "hosted" ? 当前密码 setPassword(event.target.value)} autoComplete="current-password" />修改实例策略需要重新验证当前密码。 : null} - 邀请注册创建的链接只显示一次;仅在“仅邀请”策略下可用。当前密码 setPassword(event.target.value)} autoComplete="current-password" />默认创建一条 7 天有效、仅使用一次且不发送邮件的邀请。{invitations.map((invitation) => {invitation.tokenHint}使用 {invitation.useCount}/{invitation.maxUses} · 过期 {formatRelativeTime(invitation.expiresAt)}{invitation.revokedAt ? "已撤销" : invitation.delivery?.status ?? "可用"})} + 注册策略保存后立即写入实例数据库,不需要重启服务或修改环境变量。允许新账号加入 { if (value) setNextPolicy(value as RegistrationPolicy) }}>关闭仅邀请公开{nextPolicy === "public" ? "任何人都可以创建账号。" : nextPolicy === "invitation" ? "仅持有管理员邀请链接的用户可注册。" : "不接受新的注册请求。"}当前密码 setPassword(event.target.value)} autoComplete="current-password" />修改实例策略需要重新验证当前密码。 + 邀请注册创建的链接只显示一次;仅在“仅邀请”策略下可用。当前密码 setPassword(event.target.value)} autoComplete="current-password" />默认创建一条 7 天有效、仅使用一次且不发送邮件的邀请。{invitations.map((invitation) => {invitation.tokenHint}使用 {invitation.useCount}/{invitation.maxUses} · 过期 {formatRelativeTime(invitation.expiresAt)}{invitation.revokedAt ? "已撤销" : invitation.delivery?.status ?? "可用"})} SMTP 状态仅展示脱敏队列元数据,不显示收件人、正文、密码或服务商原始响应。{registration?.instanceCapabilities["operations.smtpAdmin"] === true ? <>
{mail?.configured ? "已配置" : "未配置"}{mail?.health ?? "unknown"}{Object.entries(mail?.queue ?? {}).map(([status, count]) => {status} {count})}
{queue.map((item) => {item.template}尝试 {item.attempts}/{item.maxAttempts} · 创建于 {formatRelativeTime(item.createdAt)}{item.status})}{!queue.length ?

没有待展示的投递任务。

: null} : SMTP 管理尚未启用可以先保存安全的 SMTP 运行配置;功能状态和启用条件会显示在“实验功能”页面。}
- {configuration ? 运行配置这些设置保存在实例数据库中,保存后立即生效,无需修改 env 或重启。当前版本 {configurationRevision}。实例名称 setConfiguration({ ...configuration, serverName: event.target.value })} /> setNumber("maxObjectBytes", value)} /> setNumber("maxBlobBytes", value)} /> setNumber("changeRetentionDays", value)} /> setNumber("versionRetentionDays", value)} /> setNumber("tombstoneRetentionDays", value)} /> setNumber("pendingEmailVerificationDays", value)} /> setNumber("accountDeletionCoolingOffDays", value)} /> setNumber("accountDeletionRetentionDays", value)} />邮件默认语言 { if (value) setConfiguration({ ...configuration, mailDefaultLocale: value as "en" | "zh-CN" }) }}>中文English当前密码 setPassword(event.target.value)} autoComplete="current-password" />保存运行配置需要重新验证当前密码。
: null} - {configuration ? SMTP 配置SMTP 密码使用实例密钥加密保存且不会回显;修改后邮件工作线程会自动切换到新连接。邮件投递 { if (value) setConfiguration({ ...configuration, mailDriver: value as "disabled" | "smtp" }) }}>关闭SMTPSMTP 主机 setConfiguration({ ...configuration, smtpHost: event.target.value })} /> setNumber("smtpPort", value)} />发件地址 setConfiguration({ ...configuration, mailFromAddress: event.target.value })} />发件名称 setConfiguration({ ...configuration, mailFromName: event.target.value })} />回复地址 setConfiguration({ ...configuration, mailReplyTo: event.target.value })} />SMTP 用户名 setConfiguration({ ...configuration, smtpUsername: event.target.value })} />SMTP 密码 setSmtpPassword(event.target.value)} autoComplete="new-password" />{clearSmtpPassword ? "保存后将清除密码。" : "密码不会从服务器返回。"}TLS 模式 { if (value) setConfiguration({ ...configuration, smtpTlsMode: value as RuntimeConfiguration["smtpTlsMode"] }) }}>STARTTLSTLS可选 setNumber("smtpConnectTimeoutMs", value)} /> setNumber("smtpCommandTimeoutMs", value)} />凭据状态
{configuration.smtpPasswordConfigured ? "密码已配置" : "无密码"}
管理员当前密码 setPassword(event.target.value)} autoComplete="current-password" />
: null} + {configuration ? 运行配置这些设置保存在实例数据库中,保存后立即生效,无需修改 env 或重启。当前版本 {configurationRevision}。实例名称 setConfiguration({ ...configuration, serverName: event.target.value })} /> setNumber("maxObjectBytes", value)} /> setNumber("maxBlobBytes", value)} /> setNumber("changeRetentionDays", value)} /> setNumber("versionRetentionDays", value)} /> setNumber("tombstoneRetentionDays", value)} /> setNumber("pendingEmailVerificationDays", value)} /> setNumber("accountDeletionCoolingOffDays", value)} /> setNumber("accountDeletionRetentionDays", value)} />邮件默认语言 { if (value) setConfiguration({ ...configuration, mailDefaultLocale: value as "en" | "zh-CN" }) }}>中文English当前密码 setPassword(event.target.value)} autoComplete="current-password" />保存运行配置需要重新验证当前密码。
: null} + {configuration ? SMTP 配置SMTP 密码使用实例密钥加密保存且不会回显;修改后邮件工作线程会自动切换到新连接。邮件投递 { if (value) setConfiguration({ ...configuration, mailDriver: value as "disabled" | "smtp" }) }}>关闭SMTPSMTP 主机 setConfiguration({ ...configuration, smtpHost: event.target.value })} /> setNumber("smtpPort", value)} />发件地址 setConfiguration({ ...configuration, mailFromAddress: event.target.value })} />发件名称 setConfiguration({ ...configuration, mailFromName: event.target.value })} />回复地址 setConfiguration({ ...configuration, mailReplyTo: event.target.value })} />SMTP 用户名 setConfiguration({ ...configuration, smtpUsername: event.target.value })} />SMTP 密码 setSmtpPassword(event.target.value)} autoComplete="new-password" />{clearSmtpPassword ? "保存后将清除密码。" : "密码不会从服务器返回。"}TLS 模式 { if (value) setConfiguration({ ...configuration, smtpTlsMode: value as RuntimeConfiguration["smtpTlsMode"] }) }}>STARTTLSTLS可选 setNumber("smtpConnectTimeoutMs", value)} /> setNumber("smtpCommandTimeoutMs", value)} />凭据状态
{configuration.smtpPasswordConfigured ? "密码已配置" : "无密码"}
管理员当前密码 setPassword(event.target.value)} autoComplete="current-password" />
: null}
} diff --git a/apps/web/components/operations-portal.tsx b/apps/web/components/operations-portal.tsx deleted file mode 100644 index 07363e0..0000000 --- a/apps/web/components/operations-portal.tsx +++ /dev/null @@ -1,456 +0,0 @@ -"use client" - -import { useEffect, useMemo, useState, type FormEvent } from "react" -import { - ActivityIcon, - BadgeDollarSignIcon, - Building2Icon, - CalendarClockIcon, - CircleAlertIcon, - CircleCheckIcon, - DatabaseIcon, - HeadsetIcon, - KeyRoundIcon, - LogInIcon, - ShieldAlertIcon, - ShieldCheckIcon, - UserRoundCogIcon, - UsersIcon, -} from "lucide-react" - -import { LanguageToggle } from "@/components/language-toggle" -import { useLocale, type Locale } from "@/components/locale-provider" -import { - OperationsShell, - permittedOperationsSections, - type OperationsSection, -} from "@/components/operations-shell" -import { ThemeToggle } from "@/components/theme-toggle" -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { - Card, - CardAction, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card" -import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field" -import { Input } from "@/components/ui/input" -import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from "@/components/ui/empty" -import { - Item, - ItemActions, - ItemContent, - ItemDescription, - ItemGroup, - ItemMedia, - ItemTitle, -} from "@/components/ui/item" -import { Spinner } from "@/components/ui/spinner" -import { apiRequest, isApiRequestError } from "@/lib/api" - -export interface StaffProfile { - id: string - login: string - displayName: string - roles: string[] - permissions: string[] -} - -interface OperationsOverview { - accountCount: number - activeAccountCount: number - newAccountCount: number - activeSubscriptionCount: number - openSupportCaseCount: number - urgentSupportCaseCount: number - reviewRiskEventCount: number - pendingDataRequestCount: number - activeStaffSessionCount: number - generatedAt: string -} - -interface OperationsAccount { - id: string - login: string - identityState: string - status: string - workspaceCount: number - deviceCount: number - subscriptionStatus: string | null - createdAt: string -} - -interface SupportCase { - id: string - category: string - severity: string - status: string - subject: string - assignedStaffId: string | null - lastMessageAt: string | null - createdAt: string - updatedAt: string -} - -interface Subscription { - id: string - accountId: string | null - accountLogin: string | null - provider: string - status: string - isCurrent: boolean - currentPeriodEnd: string | null - createdAt: string -} - -interface RiskEvent { - id: string - eventType: string - accountId: string | null - accountLogin: string | null - outcome: string - reasonCodes: string[] - score: number | null - createdAt: string -} - -interface DataRequest { - id: string - accountId: string | null - accountLogin: string | null - type: string - status: string - requestChannel: string - dueAt: string | null - createdAt: string -} - -const LOGIN_COPY = { - "zh-CN": { - product: "NoteGen 运营后台", - realm: "独立运营账号", - title: "登录运营管理后台", - description: "使用安装时创建的运营管理员账号。客户账号无法登录这里。", - login: "运营管理员账号", - password: "密码", - passwordDescription: "该凭据只属于运营权限域,不与客户账号共享。", - submit: "进入运营后台", - submitting: "正在登录", - loading: "正在检查运营登录状态", - failed: "操作未完成", - }, - en: { - product: "NoteGen Operations", - realm: "Separate operations account", - title: "Sign in to operations", - description: "Use the operations administrator created during installation. Customer accounts cannot sign in here.", - login: "Operations administrator account", - password: "Password", - passwordDescription: "This credential belongs only to the operations realm and is not shared with customer accounts.", - submit: "Enter operations", - submitting: "Signing in", - loading: "Checking the operations session", - failed: "The operation did not complete", - }, -} as const - -export function OperationsPortal() { - const { locale } = useLocale() - const c = LOGIN_COPY[locale] - const [profile, setProfile] = useState(null) - const [login, setLogin] = useState("") - const [password, setPassword] = useState("") - const [section, setSection] = useState("overview") - const [overview, setOverview] = useState(null) - const [accounts, setAccounts] = useState([]) - const [supportCases, setSupportCases] = useState([]) - const [subscriptions, setSubscriptions] = useState([]) - const [riskEvents, setRiskEvents] = useState([]) - const [dataRequests, setDataRequests] = useState([]) - const [accountQuery, setAccountQuery] = useState("") - const [appliedAccountQuery, setAppliedAccountQuery] = useState("") - const [loading, setLoading] = useState(true) - const [busy, setBusy] = useState(false) - const [dataLoading, setDataLoading] = useState(false) - const [refreshVersion, setRefreshVersion] = useState(0) - const [error, setError] = useState("") - - useEffect(() => { - let active = true - apiRequest<{ activationPending: boolean }>("/v1/installation/status") - .then((status) => { - if (status.activationPending) { - window.location.replace("/") - return null - } - return apiRequest<{ staff: StaffProfile }>("/v1/staff/session") - }) - .then((result) => { - if (!active || result === null) return - setProfile(result.staff) - setSection(permittedOperationsSections(result.staff)[0] ?? "access") - }) - .catch((cause) => { - if (active && (!isApiRequestError(cause) || cause.status !== 401)) setError(staffErrorMessage(cause, locale)) - }) - .finally(() => { if (active) setLoading(false) }) - return () => { active = false } - }, [locale]) - - useEffect(() => { - if (profile === null || section === "access") return - let active = true - setDataLoading(true) - setError("") - const request = section === "overview" - ? apiRequest("/v1/staff/operations/overview").then(setOverview) - : section === "accounts" - ? apiRequest(`/v1/staff/operations/accounts?limit=100&query=${encodeURIComponent(appliedAccountQuery)}`).then(setAccounts) - : section === "support" - ? apiRequest("/v1/staff/operations/support/cases").then(setSupportCases) - : section === "billing" - ? apiRequest("/v1/staff/operations/billing/subscriptions?limit=100").then(setSubscriptions) - : section === "risk" - ? apiRequest("/v1/staff/operations/risk/events?limit=100").then(setRiskEvents) - : apiRequest("/v1/staff/operations/compliance/requests?limit=100").then(setDataRequests) - request.catch((cause) => { - if (!active) return - if (isApiRequestError(cause) && cause.status === 401) setProfile(null) - setError(staffErrorMessage(cause, locale)) - }).finally(() => { if (active) setDataLoading(false) }) - return () => { active = false } - }, [profile, section, appliedAccountQuery, refreshVersion, locale]) - - async function handleLogin(event: FormEvent) { - event.preventDefault() - setBusy(true) - setError("") - try { - const result = await apiRequest<{ staff: StaffProfile }>("/v1/staff/auth/login", { - method: "POST", - body: JSON.stringify({ login, password }), - }) - setPassword("") - setProfile(result.staff) - setSection(permittedOperationsSections(result.staff)[0] ?? "access") - } catch (cause) { - setError(staffErrorMessage(cause, locale)) - } finally { - setBusy(false) - } - } - - async function handleLogout() { - setBusy(true) - setError("") - try { - await apiRequest("/v1/staff/auth/logout", { method: "POST", csrf: true }) - setProfile(null) - setOverview(null) - } catch (cause) { - setError(staffErrorMessage(cause, locale)) - } finally { - setBusy(false) - } - } - - const counts = useMemo(() => ({ - accounts: overview?.accountCount, - support: overview?.openSupportCaseCount, - billing: overview?.activeSubscriptionCount, - risk: overview?.reviewRiskEventCount, - compliance: overview?.pendingDataRequestCount, - }), [overview]) - - if (loading) return {c.loading} - if (profile === null) return ( - - - - {c.realm} - {c.title} - {c.description} - -
- - - - {c.login} - setLogin(event.target.value)} autoComplete="username" maxLength={200} required /> - - - {c.password} - setPassword(event.target.value)} autoComplete="current-password" minLength={8} maxLength={1024} required /> - {c.passwordDescription} - - {error ? {c.failed}{error} : null} - - - - - -
-
-
- ) - - return ( - setRefreshVersion((value) => value + 1)} - onLogout={() => void handleLogout()} - > - {error ? {c.failed}{error} : null} - {dataLoading ? : null} - {!dataLoading && section === "overview" ? : null} - {!dataLoading && section === "accounts" ? setAppliedAccountQuery(accountQuery.trim())} /> : null} - {!dataLoading && section === "support" ? : null} - {!dataLoading && section === "billing" ? : null} - {!dataLoading && section === "risk" ? : null} - {!dataLoading && section === "compliance" ? : null} - {section === "access" ? : null} - - ) -} - -function LoginFrame({ children }: { children: React.ReactNode }) { - const { locale } = useLocale() - const c = LOGIN_COPY[locale] - return ( -
-
-
- -

{c.product}

{c.realm}

-
-
-
-
{children}
-
- ) -} - -function LoadingPanel() { - return 正在加载运营数据 -} - -function OverviewPanel({ overview, locale, onNavigate }: { overview: OperationsOverview | null, locale: Locale, onNavigate: (section: OperationsSection) => void }) { - if (overview === null) return - const metrics = [ - { label: locale === "zh-CN" ? "客户账号" : "Customer accounts", value: overview.accountCount, helper: `${overview.activeAccountCount} ${locale === "zh-CN" ? "个可用" : "active"}`, icon: UsersIcon, section: "accounts" as const }, - { label: locale === "zh-CN" ? "近 7 天新增" : "New in 7 days", value: overview.newAccountCount, helper: locale === "zh-CN" ? "新增客户" : "new customers", icon: ActivityIcon, section: "accounts" as const }, - { label: locale === "zh-CN" ? "有效订阅" : "Active subscriptions", value: overview.activeSubscriptionCount, helper: locale === "zh-CN" ? "含试用与宽限期" : "including trial and grace", icon: BadgeDollarSignIcon, section: "billing" as const }, - { label: locale === "zh-CN" ? "待处理工单" : "Open support cases", value: overview.openSupportCaseCount, helper: `${overview.urgentSupportCaseCount} ${locale === "zh-CN" ? "个紧急" : "urgent"}`, icon: HeadsetIcon, section: "support" as const }, - ] - return ( -
-
- {metrics.map((metric) => { - const Icon = metric.icon - return onNavigate(metric.section)}>{metric.label}{metric.value}{metric.helper} - })} -
-
- {locale === "zh-CN" ? "待办与风险" : "Queues and risk"}{locale === "zh-CN" ? "需要运营团队持续关注的跨域队列。" : "Cross-domain queues that need operations attention."} - onNavigate("risk")} /> - onNavigate("compliance")} /> - onNavigate("support")} /> - - {locale === "zh-CN" ? "运营状态" : "Operations status"}{formatDate(overview.generatedAt, locale)}
{locale === "zh-CN" ? "运营会话" : "Staff sessions"}{overview.activeStaffSessionCount}
{locale === "zh-CN" ? "控制台状态" : "Console status"}{locale === "zh-CN" ? "正常" : "Healthy"}
-
-
- ) -} - -function QueueItem({ icon: Icon, title, value, locale, onClick }: { icon: typeof UsersIcon, title: string, value: number, locale: Locale, onClick: () => void }) { - const description = value === 0 - ? (locale === "zh-CN" ? "当前队列为空" : "The queue is currently empty") - : (locale === "zh-CN" ? "点击进入队列查看详情" : "Open the queue to review details") - return }>{title}{description} 0 ? "secondary" : "outline"}>{value} -} - -function AccountsPanel({ accounts, query, locale, onQueryChange, onSearch }: { accounts: OperationsAccount[], query: string, locale: Locale, onQueryChange: (value: string) => void, onSearch: () => void }) { - return {locale === "zh-CN" ? "客户账号目录" : "Customer account directory"}{locale === "zh-CN" ? "按登录名检索,查看账号可用性、工作区、设备和当前订阅。" : "Search by login and inspect availability, workspaces, devices, and subscription."}
{ event.preventDefault(); onSearch() }}> onQueryChange(event.target.value)} placeholder={locale === "zh-CN" ? "输入账号或邮箱" : "Enter account or email"} maxLength={200} />
{accounts.map((account) => {account.login}{formatDate(account.createdAt, locale)} · {account.workspaceCount} {locale === "zh-CN" ? "个工作区" : "workspaces"} · {account.deviceCount} {locale === "zh-CN" ? "台设备" : "devices"}{account.subscriptionStatus ?? (locale === "zh-CN" ? "无订阅" : "No subscription")})}
-} - -function SupportPanel({ cases, locale }: { cases: SupportCase[], locale: Locale }) { - return {cases.map((item) => {item.subject}{item.category} · {formatDate(item.updatedAt, locale)} · {item.assignedStaffId ? (locale === "zh-CN" ? "已分配" : "Assigned") : (locale === "zh-CN" ? "未分配" : "Unassigned")}{item.severity})} -} - -function BillingPanel({ subscriptions, locale }: { subscriptions: Subscription[], locale: Locale }) { - return {subscriptions.map((item) => {item.accountLogin ?? (locale === "zh-CN" ? "已脱离账号" : "Detached account")}{item.provider} · {item.currentPeriodEnd ? `${locale === "zh-CN" ? "周期至" : "Period ends"} ${formatDate(item.currentPeriodEnd, locale)}` : formatDate(item.createdAt, locale)}{item.isCurrent ? {locale === "zh-CN" ? "当前" : "Current"} : null})} -} - -function RiskPanel({ events, locale }: { events: RiskEvent[], locale: Locale }) { - return {events.map((item) => {item.eventType}{item.accountLogin ?? (locale === "zh-CN" ? "匿名主体" : "Anonymous subject")} · {formatDate(item.createdAt, locale)}{item.reasonCodes.length ? ` · ${item.reasonCodes.join(", ")}` : ""}{item.score !== null ? {locale === "zh-CN" ? "分值" : "Score"} {item.score} : null})} -} - -function CompliancePanel({ requests, locale }: { requests: DataRequest[], locale: Locale }) { - return {requests.map((item) => {item.accountLogin ?? (locale === "zh-CN" ? "已匿名化主体" : "Anonymized subject")}{item.type} · {item.requestChannel} · {item.dueAt ? `${locale === "zh-CN" ? "截止" : "Due"} ${formatDate(item.dueAt, locale)}` : formatDate(item.createdAt, locale)})} -} - -function AccessPanel({ profile, locale }: { profile: StaffProfile, locale: Locale }) { - return
{locale === "zh-CN" ? "当前运营管理员" : "Current operations administrator"}{profile.login}

{profile.displayName}

{locale === "zh-CN" ? "运营身份与客户身份独立存储。" : "Operations and customer identities are stored separately."}

{profile.roles.map((role) => {role})}
{locale === "zh-CN" ? "已授权能力" : "Granted capabilities"}{profile.permissions.length}{profile.permissions.map((permission) => {permission})}{locale === "zh-CN" ? "内部测试认证" : "Internal-test authentication"}{locale === "zh-CN" ? "当前使用本地运营凭据。正式环境接入企业身份登录后,客户账号体系仍保持独立。" : "Local operations credentials are active. Customer identities remain separate when enterprise sign-in is connected."}
-} - -function RecordCard({ title, description, empty, locale, children }: { title: string, description: string, empty: boolean, locale: Locale, children: React.ReactNode }) { - return {title}{description}{children} -} - -function RecordList({ empty, locale, children }: { empty: boolean, locale: Locale, children: React.ReactNode }) { - if (empty) return {locale === "zh-CN" ? "当前没有记录" : "No records right now"}{locale === "zh-CN" ? "有新数据后会自动出现在这里。" : "New data will appear here when available."} - return {children} -} - -function NoticeCard({ title }: { title: string }) { - return {title} -} - -function StatusBadge({ value }: { value: string }) { - const positive = ["active", "trialing", "resolved", "completed", "allow", "ok"].includes(value) - return {value} -} - -function formatDate(value: string, locale: Locale): string { - const date = new Date(value) - if (!Number.isFinite(date.getTime())) return locale === "zh-CN" ? "未知时间" : "Unknown time" - return new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeStyle: "short" }).format(date) -} - -function staffErrorMessage(cause: unknown, locale: Locale): string { - if (isApiRequestError(cause)) { - const zh: Record = { - staff_credentials_invalid: "运营管理员账号或密码不正确。", - staff_session_invalid: "运营登录状态已过期,请重新登录。", - staff_permission_denied: "当前运营角色无权访问这个工作区。", - staff_local_auth_unavailable: "当前实例未启用运营模式的内部测试登录。", - csrf_invalid: "页面安全凭据已过期,请刷新后重试。", - origin_not_allowed: "当前页面地址未被服务器信任,请检查 Web 地址配置。", - rate_limited: "操作过于频繁,请稍后再试。", - } - const en: Record = { - staff_credentials_invalid: "The operations administrator account or password is incorrect.", - staff_session_invalid: "The operations session has expired. Sign in again.", - staff_permission_denied: "The current operations role cannot access this workspace.", - staff_local_auth_unavailable: "Internal-test operations sign-in is not enabled for this instance.", - csrf_invalid: "The page security credential has expired. Refresh and try again.", - origin_not_allowed: "This page origin is not trusted by the server. Check the Web URL configuration.", - rate_limited: "There have been too many requests. Try again later.", - } - const message = (locale === "zh-CN" ? zh : en)[cause.code] - if (message) return message - return locale === "zh-CN" ? `请求未完成(HTTP ${cause.status})。` : `The request did not complete (HTTP ${cause.status}).` - } - return locale === "zh-CN" ? "无法连接同步服务器,请确认服务已启动。" : "Could not connect to the sync server. Make sure it is running." -} diff --git a/apps/web/components/operations-shell.tsx b/apps/web/components/operations-shell.tsx deleted file mode 100644 index 6ed8894..0000000 --- a/apps/web/components/operations-shell.tsx +++ /dev/null @@ -1,249 +0,0 @@ -"use client" - -import type { ReactNode } from "react" -import { - BadgeDollarSignIcon, - Building2Icon, - ClipboardCheckIcon, - HeadsetIcon, - KeyRoundIcon, - LayoutDashboardIcon, - LogOutIcon, - RefreshCwIcon, - ShieldAlertIcon, - UsersIcon, -} from "lucide-react" - -import { LanguageToggle } from "@/components/language-toggle" -import { useLocale } from "@/components/locale-provider" -import { ThemeToggle } from "@/components/theme-toggle" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { Separator } from "@/components/ui/separator" -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarInset, - SidebarMenu, - SidebarMenuBadge, - SidebarMenuButton, - SidebarMenuItem, - SidebarProvider, - SidebarRail, - SidebarTrigger, -} from "@/components/ui/sidebar" -import { Spinner } from "@/components/ui/spinner" - -import type { StaffProfile } from "@/components/operations-portal" - -export type OperationsSection = - | "overview" - | "accounts" - | "support" - | "billing" - | "risk" - | "compliance" - | "access" - -const COPY = { - "zh-CN": { - product: "NoteGen 运营后台", - realm: "官方托管 · 内部运营", - group: "运营管理", - role: "运营管理员", - internal: "内部测试", - overview: ["运营总览", "关注增长、服务与风险队列。"], - accounts: ["客户账号", "检索客户并了解账号、设备与订阅状态。"], - support: ["支持工单", "处理等待运营团队响应的客户问题。"], - billing: ["订阅授权", "核对订阅生命周期与权益来源。"], - risk: ["风险中心", "观察近期风险决策与异常信号。"], - compliance: ["合规请求", "跟进数据访问、导出和删除请求。"], - access: ["权限中心", "查看当前运营身份、角色与授权能力。"], - refresh: "刷新当前数据", - logout: "退出运营后台", - }, - en: { - product: "NoteGen Operations", - realm: "Hosted · Internal operations", - group: "Operations", - role: "Operations administrator", - internal: "Internal test", - overview: ["Operations overview", "Monitor growth, service, and risk queues."], - accounts: ["Customer accounts", "Search customers and inspect account, device, and subscription state."], - support: ["Support cases", "Handle customer issues waiting for an operations response."], - billing: ["Subscriptions", "Review subscription lifecycle and entitlement sources."], - risk: ["Risk center", "Monitor recent risk decisions and unusual signals."], - compliance: ["Compliance requests", "Track data access, export, and deletion requests."], - access: ["Access center", "Review the current staff identity, roles, and permissions."], - refresh: "Refresh current data", - logout: "Sign out of operations", - }, -} as const - -const PERMISSION: Partial> = { - overview: "platform.provision", - accounts: "platform.provision", - support: "support.read", - billing: "billing.read", - risk: "risk.read", - compliance: "compliance.request.process", -} - -export function permittedOperationsSections(profile: StaffProfile): OperationsSection[] { - const order: OperationsSection[] = ["overview", "accounts", "support", "billing", "risk", "compliance", "access"] - return order.filter((section) => { - const permission = PERMISSION[section] - return permission === undefined || profile.permissions.includes(permission) - }) -} - -export function OperationsShell({ - profile, - section, - counts, - busy, - onSectionChange, - onRefresh, - onLogout, - children, -}: { - profile: StaffProfile - section: OperationsSection - counts: Partial> - busy: boolean - onSectionChange: (section: OperationsSection) => void - onRefresh: () => void - onLogout: () => void - children: ReactNode -}) { - const { locale } = useLocale() - const c = COPY[locale] - const sections = permittedOperationsSections(profile) - const details = c[section] - const icons = { - overview: LayoutDashboardIcon, - accounts: UsersIcon, - support: HeadsetIcon, - billing: BadgeDollarSignIcon, - risk: ShieldAlertIcon, - compliance: ClipboardCheckIcon, - access: KeyRoundIcon, - } - - return ( - - - - - - - - - - - {c.product} - {c.realm} - - - - - - - - {c.group} - - - {sections.map((item) => { - const Icon = icons[item] - return ( - - onSectionChange(item)} - > - - {c[item][0]} - - {counts[item] !== undefined ? {counts[item]} : null} - - ) - })} - - - - - - - - - }> - - {profile.displayName.slice(0, 1).toUpperCase()} - - - {profile.displayName} - {c.role} - - - - - {profile.login} - onSectionChange("access")}> - - {c.access[0]} - - - - - - {busy ? : } - {c.logout} - - - - - - - - - - - -
-
- -
-

{details[0]}

-

{details[1]}

-
-
-
- {c.internal} - - - -
-
- -
{children}
-
-
- ) -} diff --git a/apps/web/components/site-chrome.tsx b/apps/web/components/site-chrome.tsx new file mode 100644 index 0000000..8ed1ce0 --- /dev/null +++ b/apps/web/components/site-chrome.tsx @@ -0,0 +1,160 @@ +"use client" + +import type { ReactNode } from "react" +import { MenuIcon } from "lucide-react" + +import { LanguageToggle } from "@/components/language-toggle" +import { + NOTEGEN_DOCS_URL, + NOTEGEN_SITE_URL, + GitHubMark, + NoteGenMark, + NoteGenWordmark, +} from "@/components/notegen-brand" +import { ThemeToggle } from "@/components/theme-toggle" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet" + +const productLinks = [ + { label: "工作流", href: `${NOTEGEN_SITE_URL}/cn#workflow` }, + { label: "功能", href: `${NOTEGEN_SITE_URL}/cn#features` }, + { label: "网页剪藏", href: `${NOTEGEN_SITE_URL}/cn/web-clipper/download` }, + { label: "文档", href: NOTEGEN_DOCS_URL }, +] as const + +const moreLinks = [ + { label: "交流群", description: "加入 NoteGen 用户交流群", href: `${NOTEGEN_SITE_URL}/cn/community` }, + { label: "商务合作", description: "了解与 NoteGen 的合作方式", href: `${NOTEGEN_SITE_URL}/cn/business` }, + { label: "支持项目", description: "支持 NoteGen 开源项目", href: `${NOTEGEN_SITE_URL}/cn/donate` }, +] as const + +export function SiteHeader({ instanceName, showLanguage = false }: { + instanceName?: string | null + showLanguage?: boolean +}) { + return ( +
+
+ + + + SYNC + + + + +
+ {instanceName ? {instanceName} : null} +
+ {showLanguage ? : null} + + +
+ + }> + + + + + SYNC + NoteGen 产品与同步服务导航 + + +
+ {showLanguage ? : null} + + +
+
+
+
+
+
+ ) +} + +export function SiteFooter() { + return ( + + ) +} + +export function PublicSiteShell({ children, instanceName, showLanguage = false }: { + children: ReactNode + instanceName?: string | null + showLanguage?: boolean +}) { + return ( +
+ +
{children}
+ +
+ ) +} diff --git a/apps/web/components/ui/chart.tsx b/apps/web/components/ui/chart.tsx new file mode 100644 index 0000000..7c2dc84 --- /dev/null +++ b/apps/web/components/ui/chart.tsx @@ -0,0 +1,373 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" +import type { TooltipValueType } from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +const INITIAL_DIMENSION = { width: 320, height: 200 } as const +type TooltipNameType = number | string + +export type ChartConfig = Record< + string, + { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +> + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +function ChartContainer({ + id, + className, + children, + config, + initialDimension = INITIAL_DIMENSION, + ...props +}: React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + initialDimension?: { + width: number + height: number + } +}) { + const uniqueId = React.useId() + const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +} + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme ?? config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +