@marinoscar/kvox-eval 0.0.0-bootstrap → 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/ask/index.d.ts +6 -0
- package/dist/ask/index.js +28 -0
- package/dist/ask/question-schema.d.ts +121 -0
- package/dist/ask/question-schema.js +167 -0
- package/dist/ask/report.d.ts +88 -0
- package/dist/ask/report.js +170 -0
- package/dist/ask/scorer.d.ts +104 -0
- package/dist/ask/scorer.js +164 -0
- package/dist/ask-run/ask-command.d.ts +62 -0
- package/dist/ask-run/ask-command.js +254 -0
- package/dist/ask-run/host/app.d.ts +65 -0
- package/dist/ask-run/host/app.js +146 -0
- package/dist/ask-run/host/db.d.ts +38 -0
- package/dist/ask-run/host/db.js +126 -0
- package/dist/ask-run/host/ports.d.ts +35 -0
- package/dist/ask-run/host/ports.js +113 -0
- package/dist/ask-run/index.d.ts +13 -0
- package/dist/ask-run/index.js +35 -0
- package/dist/ask-run/replay.d.ts +128 -0
- package/dist/ask-run/replay.js +212 -0
- package/dist/ask-run/run-ask-eval.d.ts +125 -0
- package/dist/ask-run/run-ask-eval.js +193 -0
- package/dist/ask-run/seed-golden-graph.d.ts +64 -0
- package/dist/ask-run/seed-golden-graph.js +495 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +12 -0
- package/dist/cli/kg-command.d.ts +66 -0
- package/dist/cli/kg-command.js +238 -0
- package/dist/cli/main.d.ts +18 -0
- package/dist/cli/main.js +69 -0
- package/dist/fixtures/domains.d.ts +16 -0
- package/dist/fixtures/domains.js +33 -0
- package/dist/fixtures/fixture-schema.d.ts +323 -0
- package/dist/fixtures/fixture-schema.js +199 -0
- package/dist/fixtures/index.d.ts +16 -0
- package/dist/fixtures/index.js +40 -0
- package/dist/fixtures/load.d.ts +73 -0
- package/dist/fixtures/load.js +137 -0
- package/dist/fixtures/questions.d.ts +15 -0
- package/dist/fixtures/questions.js +26 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +36 -0
- package/dist/kg/gold-runner.d.ts +13 -0
- package/dist/kg/gold-runner.js +57 -0
- package/dist/kg/index.d.ts +17 -0
- package/dist/kg/index.js +65 -0
- package/dist/kg/internals.d.ts +7 -0
- package/dist/kg/internals.js +17 -0
- package/dist/kg/prediction-schema.d.ts +245 -0
- package/dist/kg/prediction-schema.js +102 -0
- package/dist/kg/predictions.d.ts +11 -0
- package/dist/kg/predictions.js +49 -0
- package/dist/kg/report.d.ts +84 -0
- package/dist/kg/report.js +125 -0
- package/dist/kg/run-kg-eval.d.ts +77 -0
- package/dist/kg/run-kg-eval.js +137 -0
- package/dist/kg/runner.d.ts +72 -0
- package/dist/kg/runner.js +51 -0
- package/dist/kg/runners/extract-resolve-runner.d.ts +40 -0
- package/dist/kg/runners/extract-resolve-runner.js +199 -0
- package/dist/kg/runners/extract-runner.d.ts +65 -0
- package/dist/kg/runners/extract-runner.js +243 -0
- package/dist/kg/scorer.d.ts +226 -0
- package/dist/kg/scorer.js +463 -0
- package/dist/kg/targets.d.ts +17 -0
- package/dist/kg/targets.js +16 -0
- package/dist/kg/text.d.ts +45 -0
- package/dist/kg/text.js +100 -0
- package/dist/live/adapters.d.ts +16 -0
- package/dist/live/adapters.js +58 -0
- package/dist/live/env.d.ts +39 -0
- package/dist/live/env.js +56 -0
- package/dist/live/index.d.ts +6 -0
- package/dist/live/index.js +16 -0
- package/dist/live/live-ai.d.ts +104 -0
- package/dist/live/live-ai.js +157 -0
- package/dist/parity/constants.d.ts +79 -0
- package/dist/parity/constants.js +104 -0
- package/dist/parity/index.d.ts +20 -0
- package/dist/parity/index.js +38 -0
- package/dist/parity/internals.d.ts +8 -0
- package/dist/parity/internals.js +16 -0
- package/dist/parity/scenario.d.ts +45 -0
- package/dist/parity/scenario.js +245 -0
- package/dist/parity/script.d.ts +44 -0
- package/dist/parity/script.js +136 -0
- package/dist/parity/snapshot.d.ts +43 -0
- package/dist/parity/snapshot.js +71 -0
- package/dist/real-data/index.d.ts +2 -0
- package/dist/real-data/index.js +12 -0
- package/dist/real-data/real-data.d.ts +76 -0
- package/dist/real-data/real-data.js +156 -0
- package/fixtures/kg-golden/README.md +136 -0
- package/fixtures/kg-golden/ask-questions.json +458 -0
- package/fixtures/kg-golden/meetings/m01-northwind-pilot-kickoff.json +792 -0
- package/fixtures/kg-golden/meetings/m02-halden-migration-planning.json +814 -0
- package/fixtures/kg-golden/meetings/m03-larkspur-weekly-sync.json +601 -0
- package/fixtures/kg-golden/meetings/m04-corvid-intake-discovery.json +690 -0
- package/fixtures/kg-golden/meetings/m05-hosting-vendor-selection.json +631 -0
- package/fixtures/kg-golden/meetings/m06-northwind-floor-data-review.json +649 -0
- package/fixtures/kg-golden/meetings/m07-gullwing-intro-notes.json +428 -0
- package/fixtures/kg-golden/meetings/m08-halden-migration-checkpoint.json +672 -0
- package/fixtures/kg-golden/meetings/m09-northwind-platform-org-update.json +711 -0
- package/fixtures/kg-golden/meetings/m10-corvid-prototype-review.json +623 -0
- package/fixtures/kg-golden/meetings/m11-solvane-grid-dashboard-scoping.json +689 -0
- package/fixtures/kg-golden/meetings/m12-northwind-pilot-replan.json +681 -0
- package/fixtures/kg-golden/meetings/m13-halden-cutover-go-no-go.json +721 -0
- package/fixtures/kg-golden/meetings/m14-gullwing-discovery-readout.json +719 -0
- package/fixtures/kg-golden/meetings/m15-halden-handover-ravi-leaving.json +716 -0
- package/fixtures/kg-golden/meetings/m16-varden-claims-portal-intro.json +704 -0
- package/fixtures/kg-golden/meetings/m17-corvid-usability-debrief.json +572 -0
- package/fixtures/kg-golden/meetings/m18-hosting-decision-revisited.json +655 -0
- package/fixtures/kg-golden/meetings/m19-solvane-integration-kickoff.json +726 -0
- package/fixtures/kg-golden/meetings/m20-northwind-go-live-retro.json +673 -0
- package/fixtures/kg-golden/meetings/m21-quarterly-account-review.json +449 -0
- package/fixtures/kg-golden/meetings/m22-halden-intro-call-2020.json +558 -0
- package/fixtures/kg-golden/meetings/m23-gullwing-model-review.json +702 -0
- package/fixtures/kg-golden/meetings/m24-varden-discovery-midpoint.json +643 -0
- package/fixtures/kg-golden/meetings/m25-corvid-rollout-planning.json +642 -0
- package/fixtures/kg-golden/meetings/m26-larkspur-engineering-prep.json +695 -0
- package/fixtures/kg-golden/meetings/m27-solvane-phase-one-demo.json +649 -0
- package/fixtures/kg-golden/meetings/m28-northwind-building-5-scoping.json +715 -0
- package/fixtures/kg-golden/meetings/m29-halden-southern-cutover-review.json +752 -0
- package/fixtures/kg-golden/meetings/m30-varden-portal-build-kickoff.json +697 -0
- package/fixtures/kg-golden/meetings/m31-gullwing-rollout-renewal.json +681 -0
- package/fixtures/kg-golden/meetings/m32-brambleway-weekly-sync.json +472 -0
- package/fixtures/kg-golden/meetings/m33-nina-oliver-one-on-one.json +342 -0
- package/fixtures/kg-golden/meetings/m34-coffee-with-dana.json +249 -0
- package/fixtures/kg-golden/meetings/m35-brambleway-june-sync.json +553 -0
- package/fixtures/kg-golden/meetings/m36-copenhagen-conference-planning.json +296 -0
- package/host/generated/client.d.ts +1 -0
- package/host/generated/client.js +5 -0
- package/host/generated/default.d.ts +1 -0
- package/host/generated/default.js +5 -0
- package/host/generated/edge.d.ts +1 -0
- package/host/generated/edge.js +1658 -0
- package/host/generated/index-browser.js +1684 -0
- package/host/generated/index.d.ts +192335 -0
- package/host/generated/index.js +1658 -0
- package/host/generated/package.json +139 -0
- package/host/generated/query_compiler_fast_bg.js +2 -0
- package/host/generated/query_compiler_fast_bg.wasm +0 -0
- package/host/generated/query_compiler_fast_bg.wasm-base64.js +2 -0
- package/host/generated/runtime/client.d.ts +3433 -0
- package/host/generated/runtime/client.js +110 -0
- package/host/generated/runtime/index-browser.d.ts +90 -0
- package/host/generated/runtime/index-browser.js +6 -0
- package/host/generated/runtime/wasm-compiler-edge.js +100 -0
- package/host/generated/schema.prisma +6312 -0
- package/host/generated/wasm-edge-light-loader.mjs +5 -0
- package/host/generated/wasm-worker-loader.mjs +5 -0
- package/host/prisma/fragments/kvox.ask.prisma +126 -0
- package/host/prisma/fragments/kvox.graph.prisma +1073 -0
- package/host/prisma/fragments/kvox.notes.prisma +787 -0
- package/host/prisma/fragments/kvox.search.prisma +211 -0
- package/host/prisma/fragments/kvox.transcripts.prisma +790 -0
- package/host/prisma/kvox.lock +101 -0
- package/host/prisma/migrations/20240101000000_initial/migration.sql +297 -0
- package/host/prisma/migrations/20240101000001_add_personal_access_tokens/migration.sql +31 -0
- package/host/prisma/migrations/20240101000002_add_credentials/migration.sql +20 -0
- package/host/prisma/migrations/20240101000003_add_notification_deliveries/migration.sql +27 -0
- package/host/prisma/migrations/20240101000004_drop_stale_uuid_defaults/migration.sql +35 -0
- package/host/prisma/migrations/20240101000005_add_notifications/migration.sql +22 -0
- package/host/prisma/migrations/20240101000006_add_push_subscriptions/migration.sql +25 -0
- package/host/prisma/migrations/20240101000007_add_jobs/migration.sql +110 -0
- package/host/prisma/migrations/20240101000008_add_worker_nodes/migration.sql +90 -0
- package/host/prisma/migrations/20240101000009_add_database_backup_runs/migration.sql +104 -0
- package/host/prisma/migrations/20240101000010_add_notification_broadcasts/migration.sql +57 -0
- package/host/prisma/migrations/20240101000011_add_backup_run_job_link/migration.sql +87 -0
- package/host/prisma/migrations/20240101000012_add_job_node_secrets/migration.sql +61 -0
- package/host/prisma/migrations/20240101000013_add_backup_run_pg_dump_version/migration.sql +26 -0
- package/host/prisma/migrations/20240101000014_add_job_claim_token/migration.sql +92 -0
- package/host/prisma/migrations/20240101000015_add_ai_platform/migration.sql +108 -0
- package/host/prisma/migrations/20240101000016_revoke_viewer_ai_use/migration.sql +32 -0
- package/host/prisma/migrations/20240101000017_add_user_credentials/migration.sql +29 -0
- package/host/prisma/migrations/20240101000018_add_device_session_credential_link/migration.sql +52 -0
- package/host/prisma/migrations/20240101000019_add_worker_node_vitals/migration.sql +3 -0
- package/host/prisma/migrations/20240101000020_add_job_trace_context/migration.sql +6 -0
- package/host/prisma/migrations/20240101000021_add_retention_created_at_indexes/migration.sql +13 -0
- package/host/prisma/migrations/20240101000022_add_organizations/migration.sql +142 -0
- package/host/prisma/migrations/20240101000023_split_system_org_roles/migration.sql +131 -0
- package/host/prisma/migrations/20240101000024_org_scoped_rls/migration.sql +215 -0
- package/host/prisma/migrations/20240101000025_add_groups/migration.sql +165 -0
- package/host/prisma/migrations/20240101000026_add_grants/migration.sql +117 -0
- package/host/prisma/migrations/20240101000027_add_org_settings/migration.sql +49 -0
- package/host/prisma/migrations/20240101000028_add_org_credentials/migration.sql +54 -0
- package/host/prisma/migrations/20240101000029_add_jobs_org_id/migration.sql +18 -0
- package/host/prisma/migrations/20240101000030_add_jobs_org_id_status_index/migration.sql +6 -0
- package/host/prisma/migrations/20240101000031_add_broadcast_target_org/migration.sql +21 -0
- package/host/prisma/migrations/20240101000032_add_android_app/migration.sql +60 -0
- package/host/prisma/migrations/20240101000033_open_job_reason/migration.sql +16 -0
- package/host/prisma/migrations/20240101000034_ai_usage_feature/migration.sql +37 -0
- package/host/prisma/migrations/20240101000035_add_app_metadata/migration.sql +49 -0
- package/host/prisma/migrations/20240101000036_open_sharing_vocabularies/migration.sql +97 -0
- package/host/prisma/migrations/20260914130000_kvox_add_transcripts/migration.sql +322 -0
- package/host/prisma/migrations/20260914140000_kvox_add_notes/migration.sql +311 -0
- package/host/prisma/migrations/20260915120000_kvox_add_search_vectors/migration.sql +150 -0
- package/host/prisma/migrations/20260915130000_kvox_add_search_embeddings/migration.sql +362 -0
- package/host/prisma/migrations/20260915210000_kvox_add_note_title_source/migration.sql +30 -0
- package/host/prisma/migrations/20260923120000_kvox_add_note_generation_context_snapshot/migration.sql +24 -0
- package/host/prisma/migrations/20260923130000_kvox_add_user_hidden_note_templates/migration.sql +35 -0
- package/host/prisma/migrations/20260923140000_kvox_add_transcript_speaker_identities/migration.sql +6 -0
- package/host/prisma/migrations/20260923150000_kvox_transcript_name_checks/migration.sql +95 -0
- package/host/prisma/migrations/20260923160000_kvox_add_note_body_format/migration.sql +27 -0
- package/host/prisma/migrations/20260923170000_kvox_add_note_version_body_format/migration.sql +23 -0
- package/host/prisma/migrations/20260926012548_kvox_add_knowledge_graph/migration.sql +680 -0
- package/host/prisma/migrations/20260926030000_kvox_fix_kg_relations_valid_precision_chk/migration.sql +29 -0
- package/host/prisma/migrations/20260926030100_kvox_add_transcript_recorded_at/migration.sql +9 -0
- package/host/prisma/migrations/20260926030200_kvox_kg_evidence_invariant/migration.sql +60 -0
- package/host/prisma/migrations/20260926040000_kvox_add_kg_graph_layouts/migration.sql +30 -0
- package/host/prisma/migrations/20260926050000_kvox_add_ask_conversations/migration.sql +99 -0
- package/host/prisma/migrations/20260926231512_kvox_add_kg_exports/migration.sql +52 -0
- package/host/prisma/migrations/20261016120000_kvox_transcripts_org_id/migration.sql +49 -0
- package/host/prisma/platform.lock +319 -0
- package/host/prisma/schema/app.kvox.ask.prisma +110 -0
- package/host/prisma/schema/app.kvox.graph.prisma +1030 -0
- package/host/prisma/schema/app.kvox.notes.prisma +746 -0
- package/host/prisma/schema/app.kvox.search.prisma +208 -0
- package/host/prisma/schema/app.kvox.transcripts.prisma +726 -0
- package/host/prisma/schema/platform.ai.prisma +419 -0
- package/host/prisma/schema/platform.android-app.prisma +46 -0
- package/host/prisma/schema/platform.base.prisma +15 -0
- package/host/prisma/schema/platform.credentials.prisma +156 -0
- package/host/prisma/schema/platform.db-backup.prisma +359 -0
- package/host/prisma/schema/platform.identity.prisma +638 -0
- package/host/prisma/schema/platform.jobs.prisma +824 -0
- package/host/prisma/schema/platform.notifications.prisma +641 -0
- package/host/prisma/schema/platform.settings.prisma +53 -0
- package/host/prisma/schema/platform.sharing.prisma +208 -0
- package/host/prisma/schema/platform.storage.prisma +118 -0
- package/package.json +112 -1
- package/parity/requests.ask.json +2681 -0
- package/parity/requests.m01.json +1554 -0
- package/parity/requests.m02.json +1554 -0
- package/parity/requests.m03.json +1554 -0
- package/parity/requests.m04.json +1554 -0
- package/parity/requests.m05.json +1554 -0
- package/parity/requests.m06.json +1554 -0
- package/parity/requests.m07.json +1554 -0
- package/parity/requests.m08.json +1554 -0
- package/parity/requests.m09.json +1554 -0
- package/parity/requests.m10.json +1554 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// GENERATED by platform db compose — do not edit
|
|
2
|
+
// source: package:ai.prisma
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// AI Platform (issue #423, epic #419, umbrella #418)
|
|
6
|
+
// =============================================================================
|
|
7
|
+
//
|
|
8
|
+
// Four tables — the model catalog, per-user encrypted provider keys,
|
|
9
|
+
// background AI run results, and usage accounting — for the whole platform
|
|
10
|
+
// this epic builds. THIS ISSUE OWNS SCHEMA ONLY: no service or controller in
|
|
11
|
+
// this build reads or writes any of these tables yet (that is #427, #428,
|
|
12
|
+
// #431, #432). They land ahead of their consumers, on the same "declare the
|
|
13
|
+
// collision hotspot once, let every other AI story run in parallel" logic
|
|
14
|
+
// `system-settings.service.ts`'s operations-namespaces header already argues
|
|
15
|
+
// for `system_settings`: `schema.prisma`, `seed-data.ts`,
|
|
16
|
+
// `roles.constants.ts` and `settings.schema.ts` are each touched by every
|
|
17
|
+
// story in the epic, so this one story claims them in a single migration
|
|
18
|
+
// instead of every later story racing to edit the same four files.
|
|
19
|
+
//
|
|
20
|
+
// -----------------------------------------------------------------------------
|
|
21
|
+
// ⚠ NO COLUMN HERE CAN EVER HOLD A PLAINTEXT PROVIDER KEY
|
|
22
|
+
// -----------------------------------------------------------------------------
|
|
23
|
+
// `UserAiKey.secret` is ciphertext ONLY — the output of `encryptSecret(
|
|
24
|
+
// plaintext, 'ai_user_key')` (`@marinoscar/platform-api/core`), the same
|
|
25
|
+
// AES-GCM helper `Credential.secret` already uses, keyed under a purpose
|
|
26
|
+
// label specific to this table so a ciphertext lifted from one row cannot be
|
|
27
|
+
// decrypted as if it were the other's. `hint` is the non-secret display aid
|
|
28
|
+
// (`'••••' + last 4`, mirroring `Credential.hint`), never enough on its own
|
|
29
|
+
// to reconstruct the key. `AiRun.request` is explicitly a NORMALIZED request
|
|
30
|
+
// — model id, parameters, truncated/redacted prompt shape — never the
|
|
31
|
+
// caller's raw provider key, which the run's own executor already holds
|
|
32
|
+
// separately (from `UserAiKey`, decrypted for that one call and discarded).
|
|
33
|
+
//
|
|
34
|
+
// -----------------------------------------------------------------------------
|
|
35
|
+
// `UserAiKey` — Cascade, NOT SetNull, and the contrast with `Credential` is
|
|
36
|
+
// deliberate
|
|
37
|
+
// -----------------------------------------------------------------------------
|
|
38
|
+
// `Credential.updatedByUser` is `SetNull`: that table is shared
|
|
39
|
+
// infrastructure (an SMTP password, a storage secret) that outlives whoever
|
|
40
|
+
// last edited it, because the credential belongs to the DEPLOYMENT, not to
|
|
41
|
+
// the editor. `UserAiKey.user` is the opposite shape on purpose — a BYOK key
|
|
42
|
+
// belongs to the ONE ACCOUNT that typed it in, has no meaning to anyone else,
|
|
43
|
+
// and (like `PushSubscription.user`, which makes the identical argument for a
|
|
44
|
+
// live per-device credential) has no reader once that account is gone. There
|
|
45
|
+
// is no "the deployment's OpenAI key" here, only "this user's OpenAI key", so
|
|
46
|
+
// deleting the user is what should delete it.
|
|
47
|
+
//
|
|
48
|
+
// -----------------------------------------------------------------------------
|
|
49
|
+
// `AiRun.user` / `AiUsageEvent.user` — SetNull, matching `NotificationDelivery
|
|
50
|
+
// .user`
|
|
51
|
+
// -----------------------------------------------------------------------------
|
|
52
|
+
// Both are HISTORY, not live state: a run's output and a usage event's token
|
|
53
|
+
// counts remain meaningful — for billing reconciliation, for "was this
|
|
54
|
+
// account affected by the incident on provider X" — after the account that
|
|
55
|
+
// triggered them is gone, exactly the argument `NotificationDelivery.user`'s
|
|
56
|
+
// own block comment makes for a delivery attempt. Nulling the FK preserves
|
|
57
|
+
// that record; cascading it would erase usage accounting as a side effect of
|
|
58
|
+
// an unrelated account-deletion path elsewhere in the app. Both are also
|
|
59
|
+
// deliberately nullable up front (not merely nullable-after-cascade), the same
|
|
60
|
+
// "global job" shape `Job.subjectType`/`subjectId` use: a background run or a
|
|
61
|
+
// catalog-discovery usage event may have no requesting user at all.
|
|
62
|
+
//
|
|
63
|
+
// -----------------------------------------------------------------------------
|
|
64
|
+
// `capabilities` / `units` — JSONB, not typed columns, and Zod-validated in
|
|
65
|
+
// code
|
|
66
|
+
// -----------------------------------------------------------------------------
|
|
67
|
+
// `AiModel.capabilities` (what a model can do — text, vision, tool calling,
|
|
68
|
+
// structured output, ...) and `AiUsageEvent.units` (a per-operation unit
|
|
69
|
+
// count that varies by kind: `{ images: 2 }`, `{ audioSeconds: 31.4 }`) are
|
|
70
|
+
// each shaped differently per provider and per operation, and neither is
|
|
71
|
+
// filtered or indexed on by any query this issue's schema needs to support.
|
|
72
|
+
// Both follow `Job.payload`'s and `WorkerNode.capabilities`' own precedent:
|
|
73
|
+
// opaque JSONB here, a real Zod schema (`AiModelCapabilities`) at the
|
|
74
|
+
// consuming layer that arrives with #427/#428. A typed column per capability
|
|
75
|
+
// would be a migration every time a provider ships a new one.
|
|
76
|
+
//
|
|
77
|
+
// -----------------------------------------------------------------------------
|
|
78
|
+
// `provider` / `operation` / `status` / `keySource` — plain strings, not
|
|
79
|
+
// Prisma enums
|
|
80
|
+
// -----------------------------------------------------------------------------
|
|
81
|
+
// Same reasoning as `Job.type`, `NotificationDelivery.channel` and
|
|
82
|
+
// `WorkerNode.executor` throughout this file: these are small, code-owned,
|
|
83
|
+
// GROWING sets (`AI_PROVIDER_IDS` names `'openai'` today and Phase 3 appends
|
|
84
|
+
// to it; new operations and key sources are exactly the shape of change a
|
|
85
|
+
// provider integration adds), so a Postgres enum would reintroduce the
|
|
86
|
+
// migration this design otherwise avoids, and a row must be able to outlive
|
|
87
|
+
// whatever build produced it. `AiRun.status` and `AiUsageEvent.status` are
|
|
88
|
+
// each that model's OWN closed state machine in every other sense — but they
|
|
89
|
+
// are kept as strings rather than promoted to a Prisma enum for the same
|
|
90
|
+
// forward-compatibility reason the rest of this section gives, not because
|
|
91
|
+
// they are open-ended in practice.
|
|
92
|
+
//
|
|
93
|
+
// -----------------------------------------------------------------------------
|
|
94
|
+
// `AiModel.capabilitySource` — `'catalog' | 'admin_override' | 'unclassified'`
|
|
95
|
+
// -----------------------------------------------------------------------------
|
|
96
|
+
// Records WHERE `capabilities` came from, because the three sources disagree
|
|
97
|
+
// in trustworthiness and in how long they last: `'catalog'` is the
|
|
98
|
+
// provider's own advertised capability list (refreshed on discovery, and the
|
|
99
|
+
// first thing a later sync overwrites), `'admin_override'` is a deployment
|
|
100
|
+
// operator's manual correction (must survive a catalog refresh that would
|
|
101
|
+
// otherwise clobber it — that survival rule belongs to #427's sync logic, not
|
|
102
|
+
// to this column), and `'unclassified'` is the default for a model this
|
|
103
|
+
// deployment has seen but never classified at all. A model discovered before
|
|
104
|
+
// any classification runs is `'unclassified'`, never a guess.
|
|
105
|
+
model AiModel {
|
|
106
|
+
id String @id @default(uuid()) @db.Uuid
|
|
107
|
+
|
|
108
|
+
// The provider registration this model belongs to, e.g. `'openai'`. See
|
|
109
|
+
// `AI_PROVIDER_IDS` (`common/schemas/settings.schema.ts`) — a growing,
|
|
110
|
+
// code-owned list, and a plain string here for the reason given above.
|
|
111
|
+
provider String
|
|
112
|
+
|
|
113
|
+
// The provider's own raw model identifier (e.g. `'gpt-4o-mini'`), as
|
|
114
|
+
// opposed to `id` above, which is this table's own row identity.
|
|
115
|
+
modelId String @map("model_id")
|
|
116
|
+
|
|
117
|
+
// Operator/UI-facing label. Nullable: a freshly discovered model has none
|
|
118
|
+
// until an admin (or a later sync) names it.
|
|
119
|
+
displayName String? @map("display_name")
|
|
120
|
+
|
|
121
|
+
// AiModelCapabilities, Zod-validated at the consuming layer (#427) — see
|
|
122
|
+
// the block comment above for why this is JSONB rather than typed columns.
|
|
123
|
+
capabilities Json
|
|
124
|
+
|
|
125
|
+
// Where `capabilities` came from — see the block comment above.
|
|
126
|
+
// `'unclassified'` is the default: a model this deployment has observed but
|
|
127
|
+
// not yet classified, one way or the other.
|
|
128
|
+
capabilitySource String @default("unclassified") @map("capability_source")
|
|
129
|
+
|
|
130
|
+
// Whether this deployment offers the model to callers at all. Defaults to
|
|
131
|
+
// `false`: a freshly discovered model is inert until an administrator (or a
|
|
132
|
+
// later sync's own policy) turns it on, the same "nothing changes behaviour
|
|
133
|
+
// on discovery alone" posture `WorkerNode.status` defaulting to `offline`
|
|
134
|
+
// and `databaseBackup.enabled` defaulting to `false` both take elsewhere in
|
|
135
|
+
// this schema.
|
|
136
|
+
enabled Boolean @default(false)
|
|
137
|
+
|
|
138
|
+
contextWindow Int? @map("context_window")
|
|
139
|
+
maxOutputTokens Int? @map("max_output_tokens")
|
|
140
|
+
|
|
141
|
+
// When this row was first created, vs. `lastSeenAt` below, which moves on
|
|
142
|
+
// every discovery sync that still finds the model — the same "creation time
|
|
143
|
+
// vs. liveness" split `WorkerNode.registeredAt`/`.lastHeartbeatAt` make for
|
|
144
|
+
// an entirely different kind of inventory.
|
|
145
|
+
discoveredAt DateTime @default(now()) @map("discovered_at") @db.Timestamptz
|
|
146
|
+
lastSeenAt DateTime @default(now()) @map("last_seen_at") @db.Timestamptz
|
|
147
|
+
// Set by a discovery sync (#427) that no longer sees this model in the
|
|
148
|
+
// provider's own catalog. Nullable: most rows are never deprecated.
|
|
149
|
+
// Deliberately not deleted — a deprecated model may still be referenced by
|
|
150
|
+
// historical `AiRun`/`AiUsageEvent` rows via its `provider`/`modelId` pair,
|
|
151
|
+
// and losing the row would lose the display name and capability record for
|
|
152
|
+
// that history.
|
|
153
|
+
deprecatedAt DateTime? @map("deprecated_at") @db.Timestamptz
|
|
154
|
+
|
|
155
|
+
// Provenance of the last edit — see the block comment above for why this
|
|
156
|
+
// is `SetNull`, matching `Credential.updatedByUser` rather than
|
|
157
|
+
// `UserAiKey.user` below.
|
|
158
|
+
updatedByUserId String? @map("updated_by_user_id") @db.Uuid
|
|
159
|
+
updatedByUser User? @relation("AiModelUpdatedBy", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
|
160
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
161
|
+
|
|
162
|
+
// One row per (provider, model) pair, ever — a discovery sync upserts on
|
|
163
|
+
// this rather than inserting duplicates on every re-run.
|
|
164
|
+
@@unique([provider, modelId])
|
|
165
|
+
// "Which models may this deployment currently offer" — the catalog read
|
|
166
|
+
// every AI-calling path needs, filtered to the provider in play.
|
|
167
|
+
@@index([provider, enabled])
|
|
168
|
+
@@map("ai_models")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// BYOK ("bring your own key"): one encrypted provider key per (user,
|
|
172
|
+
// provider), never per deployment. See the block comment above this section
|
|
173
|
+
// for why `user` is `Cascade` here and NOT `SetNull` like `Credential
|
|
174
|
+
// .updatedByUser` or `AiModel.updatedByUser` above — this key has exactly one
|
|
175
|
+
// owner and no meaning to anyone else once that owner's account is gone.
|
|
176
|
+
model UserAiKey {
|
|
177
|
+
id String @id @default(uuid()) @db.Uuid
|
|
178
|
+
userId String @map("user_id") @db.Uuid
|
|
179
|
+
|
|
180
|
+
// Which provider this key authenticates against, e.g. `'openai'`. Plain
|
|
181
|
+
// string for the same forward-compatibility reason `AiModel.provider` is.
|
|
182
|
+
provider String
|
|
183
|
+
|
|
184
|
+
// Ciphertext ONLY — `encryptSecret(plaintext, 'ai_user_key')`. See the
|
|
185
|
+
// block comment above this section (and `Credential.secret`'s own comment)
|
|
186
|
+
// for the full argument; @db.Text for the identical reason
|
|
187
|
+
// `Credential.secret` is: an AES-GCM payload has no meaningful length
|
|
188
|
+
// bound, so it must never be truncated.
|
|
189
|
+
secret String @db.Text
|
|
190
|
+
|
|
191
|
+
// Non-secret display aid (`'••••' + last 4`), mirroring `Credential.hint`.
|
|
192
|
+
// Nullable: a key can exist before anyone has decided how to hint it.
|
|
193
|
+
hint String?
|
|
194
|
+
|
|
195
|
+
// The last time this key was proven to work against its provider (a live
|
|
196
|
+
// probe call, not merely "was saved"). Nullable: never verified yet.
|
|
197
|
+
verifiedAt DateTime? @map("verified_at") @db.Timestamptz
|
|
198
|
+
// The provider's own error code from the most recent failed verification or
|
|
199
|
+
// use, so a user who cannot see why a key stopped working has somewhere to
|
|
200
|
+
// look. Nullable: no failure recorded (yet, or ever).
|
|
201
|
+
lastErrorCode String? @map("last_error_code")
|
|
202
|
+
|
|
203
|
+
// Which of this provider's models this specific key was actually able to
|
|
204
|
+
// reach on its last verification, as opposed to `AiModel.enabled`, which is
|
|
205
|
+
// a DEPLOYMENT-WIDE policy decision independent of any one key. A key with
|
|
206
|
+
// a narrower API scope, or one hitting a provider-side allowlist, can
|
|
207
|
+
// legitimately reach fewer models than the deployment otherwise offers.
|
|
208
|
+
reachableModelIds String[] @default([]) @map("reachable_model_ids")
|
|
209
|
+
reachableCheckedAt DateTime? @map("reachable_checked_at") @db.Timestamptz
|
|
210
|
+
|
|
211
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
212
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
213
|
+
|
|
214
|
+
// Relations
|
|
215
|
+
//
|
|
216
|
+
// Cascade — see the block comment above this section for why this
|
|
217
|
+
// deliberately reverses `Credential.updatedByUser`'s `SetNull`.
|
|
218
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
219
|
+
|
|
220
|
+
// One key per (user, provider) — saving a new key for a provider replaces
|
|
221
|
+
// the old one rather than accumulating a history of them.
|
|
222
|
+
@@unique([userId, provider])
|
|
223
|
+
@@map("user_ai_keys")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// One row per AI call this platform ever ran through the background queue —
|
|
227
|
+
// the record `#432`'s job handler(s) read from and write progress/output to.
|
|
228
|
+
// This issue is schema only, matching every other table in this section: no
|
|
229
|
+
// enqueue path, no job handler and no controller exists yet.
|
|
230
|
+
// @extensible — back-relations only, added with "extend model AiRun { ... }" in a fragment
|
|
231
|
+
// (an app's own annotation of a run, for example a review verdict). The table is org-owned
|
|
232
|
+
// with row-level security forced, so a table of the app's that holds `org_id` links to a run
|
|
233
|
+
// with a composite foreign key `(ai_run_id, org_id)` -> `ai_runs (id, org_id)`; the
|
|
234
|
+
// `@@unique([id, orgId])` below exists for it.
|
|
235
|
+
model AiRun {
|
|
236
|
+
id String @id @default(uuid()) @db.Uuid
|
|
237
|
+
|
|
238
|
+
// Nullable, deliberately — see the block comment above this section for why
|
|
239
|
+
// this is `SetNull` rather than `Cascade`, and why a run may have no
|
|
240
|
+
// requesting user at all (a system-initiated run, mirroring `Job.subjectType`
|
|
241
|
+
// /`subjectId` both being null together for a global job).
|
|
242
|
+
userId String? @map("user_id") @db.Uuid
|
|
243
|
+
|
|
244
|
+
// The `jobs` row this run executes under, when it runs through the queue at
|
|
245
|
+
// all. Nullable, and deliberately NOT a foreign key — mirrors
|
|
246
|
+
// `DatabaseBackupRun`'s OWN relation to `Job` in spirit (a dedicated table
|
|
247
|
+
// outliving the generic queue's retention) but stays a plain column rather
|
|
248
|
+
// than a `@relation`+`@unique` pair: unlike a backup run, an `AiRun` is not
|
|
249
|
+
// one-to-one with its job in every future shape this platform may take
|
|
250
|
+
// (a retried run may reuse the id, or a future synchronous path may have no
|
|
251
|
+
// job at all), so this issue commits only to recording the id, not to the
|
|
252
|
+
// stronger one-to-one relation `DatabaseBackupRun.jobId` commits to.
|
|
253
|
+
jobId String? @map("job_id") @db.Uuid
|
|
254
|
+
|
|
255
|
+
// pending|running|succeeded|failed|cancelled — this run's own state
|
|
256
|
+
// machine, kept as a plain string rather than a Prisma enum for the
|
|
257
|
+
// forward-compatibility reason given in the block comment above this
|
|
258
|
+
// section.
|
|
259
|
+
status String @default("pending")
|
|
260
|
+
|
|
261
|
+
provider String
|
|
262
|
+
modelId String @map("model_id")
|
|
263
|
+
|
|
264
|
+
// The NORMALIZED request — model id, parameters, and the FULL prompt shape
|
|
265
|
+
// (instructions, input, tools, structured-output schema) `toStoredRunRequest`
|
|
266
|
+
// (`runtime/ai-run-request.ts`) built from it — and NEVER the caller's key
|
|
267
|
+
// material, which this run's executor holds separately and only for the
|
|
268
|
+
// duration of the call. Deliberately not redacted or truncated: the
|
|
269
|
+
// worker that claims this job re-issues the user's exact request, and a
|
|
270
|
+
// truncated prompt would make the replayed call diverge from the one the
|
|
271
|
+
// user actually asked for. See the ⚠ block comment above this section.
|
|
272
|
+
request Json
|
|
273
|
+
|
|
274
|
+
output Json?
|
|
275
|
+
errorCode String? @map("error_code")
|
|
276
|
+
errorMessage String? @map("error_message")
|
|
277
|
+
|
|
278
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
279
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
280
|
+
completedAt DateTime? @map("completed_at") @db.Timestamptz
|
|
281
|
+
|
|
282
|
+
// The organization the run belongs to (#725). NOT NULL, row-level security
|
|
283
|
+
// forced (policy `ai_runs_org_isolation`). `Restrict`, not `SetNull`: the
|
|
284
|
+
// column is NOT NULL, so history cannot survive an organization's deletion
|
|
285
|
+
// by nulling it; deleting an organization first purges its runs.
|
|
286
|
+
orgId String @map("org_id") @db.Uuid
|
|
287
|
+
|
|
288
|
+
// The registered AI feature the run was started for (`registerAiFeature`,
|
|
289
|
+
// PP-14.17, #935): a bounded id the feature registry validated, NEVER free
|
|
290
|
+
// text. Nullable (a run started without a feature, and every row written
|
|
291
|
+
// before this column existed), and a plain string rather than a reference:
|
|
292
|
+
// a feature is code, not data, and a row must outlive the feature that
|
|
293
|
+
// wrote it. Carried onto the run's usage rows, so per-feature usage is
|
|
294
|
+
// answerable from the database.
|
|
295
|
+
feature String?
|
|
296
|
+
|
|
297
|
+
// Relations
|
|
298
|
+
//
|
|
299
|
+
// SetNull — see the block comment above this section.
|
|
300
|
+
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
301
|
+
org Organization @relation(fields: [orgId], references: [id], onDelete: Restrict)
|
|
302
|
+
|
|
303
|
+
// "This user's run history, most recent first" — the one query this table
|
|
304
|
+
// is known to need today (a user's own run list).
|
|
305
|
+
@@index([userId, createdAt])
|
|
306
|
+
// The same query behind the policy filter `org_id = X`.
|
|
307
|
+
@@index([orgId, userId, createdAt])
|
|
308
|
+
// The retention sweep (`ai.runs.purge`, #681): "oldest first, older than
|
|
309
|
+
// the cutoff" across every user, which `[userId, createdAt]` cannot serve.
|
|
310
|
+
@@index([createdAt])
|
|
311
|
+
// The target of an app table's composite foreign key (id, org_id), like
|
|
312
|
+
// `Group` and `StorageObject`: a plain FK on `id` would let a row of one
|
|
313
|
+
// organization point at another's (an FK check bypasses row-level security).
|
|
314
|
+
@@unique([id, orgId])
|
|
315
|
+
@@map("ai_runs")
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// One row per AI operation attempted, successful or not — the usage/billing
|
|
319
|
+
// accounting ledger, deliberately separate from `AiRun` above. `AiRun` is
|
|
320
|
+
// "what did this background call do" (one row per queued call, with its own
|
|
321
|
+
// request/output); `AiUsageEvent` is "what did this cost", covering every
|
|
322
|
+
// operation kind (`responses|images|audio.transcribe|audio.speech|
|
|
323
|
+
// embeddings|catalog`) whether or not it went through the queue at all — a
|
|
324
|
+
// synchronous catalog-listing call has no `AiRun` but still consumes a
|
|
325
|
+
// provider call worth recording.
|
|
326
|
+
model AiUsageEvent {
|
|
327
|
+
id String @id @default(uuid()) @db.Uuid
|
|
328
|
+
|
|
329
|
+
// Nullable, `SetNull` — see the block comment above this section. A
|
|
330
|
+
// catalog-discovery sync run by the deployment itself, not by any one
|
|
331
|
+
// user, is the concrete case that leaves this null.
|
|
332
|
+
userId String? @map("user_id") @db.Uuid
|
|
333
|
+
|
|
334
|
+
provider String
|
|
335
|
+
modelId String @map("model_id")
|
|
336
|
+
|
|
337
|
+
// responses|images|audio.transcribe|audio.speech|embeddings|catalog — the
|
|
338
|
+
// kind of provider call this event records. Plain string for the
|
|
339
|
+
// forward-compatibility reason given above: a new operation kind (a future
|
|
340
|
+
// provider capability) must cost zero migrations here.
|
|
341
|
+
operation String
|
|
342
|
+
|
|
343
|
+
// user|org|admin_discovery — WHOSE key paid for this call. Distinct from
|
|
344
|
+
// `provider`/`modelId` (which model answered) and from `userId` (who asked):
|
|
345
|
+
// an admin-triggered catalog discovery sync can be `keySource:
|
|
346
|
+
// 'admin_discovery'` with no requesting `userId` at all, and a BYOK-only
|
|
347
|
+
// deployment's every real usage event is `keySource: 'user'`. `'org'` is
|
|
348
|
+
// reserved for a future org-wide fallback key, matching `AI_KEY_POLICIES`'
|
|
349
|
+
// `'byok_with_org_fallback'` in `common/schemas/settings.schema.ts`.
|
|
350
|
+
keySource String @map("key_source")
|
|
351
|
+
|
|
352
|
+
// Token accounting. All nullable: an `images`/`audio.*` operation has no
|
|
353
|
+
// token counts at all, and `reasoningTokens`/`cachedInputTokens` are
|
|
354
|
+
// provider-specific breakdowns that not every provider or model reports.
|
|
355
|
+
inputTokens Int? @map("input_tokens")
|
|
356
|
+
outputTokens Int? @map("output_tokens")
|
|
357
|
+
reasoningTokens Int? @map("reasoning_tokens")
|
|
358
|
+
cachedInputTokens Int? @map("cached_input_tokens")
|
|
359
|
+
|
|
360
|
+
// Non-token unit accounting for operations tokens don't measure — e.g.
|
|
361
|
+
// `{ images: 2 }` for an image generation call, `{ audioSeconds: 31.4 }` for
|
|
362
|
+
// transcription/speech. JSONB for the reason given in the block comment
|
|
363
|
+
// above this section: the shape varies by `operation` and no query here
|
|
364
|
+
// filters or indexes on it.
|
|
365
|
+
units Json?
|
|
366
|
+
|
|
367
|
+
latencyMs Int @map("latency_ms")
|
|
368
|
+
|
|
369
|
+
// succeeded|failed|cancelled — this event's own closed outcome, kept as a
|
|
370
|
+
// plain string for the identical forward-compatibility reason `AiRun
|
|
371
|
+
// .status` is.
|
|
372
|
+
status String
|
|
373
|
+
errorCode String? @map("error_code")
|
|
374
|
+
|
|
375
|
+
// The provider's own request/trace id, for reconciling a usage row against
|
|
376
|
+
// that provider's dashboard or support ticket. Nullable: not every provider
|
|
377
|
+
// call surfaces one.
|
|
378
|
+
providerRequestId String? @map("provider_request_id")
|
|
379
|
+
|
|
380
|
+
// The `jobs` row this usage event was incurred under, when it was. Plain
|
|
381
|
+
// column, no relation — same reasoning as `AiRun.jobId` above.
|
|
382
|
+
jobId String? @map("job_id") @db.Uuid
|
|
383
|
+
|
|
384
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
385
|
+
|
|
386
|
+
// The organization that incurred the call (#725). Nullable on purpose: a
|
|
387
|
+
// deployment-wide event (a catalogue sync) has none, and `SetNull` keeps the
|
|
388
|
+
// history of a deleted organization. Row-level security forced (policy
|
|
389
|
+
// `ai_usage_events_org_isolation`): a NULL row is visible to the system
|
|
390
|
+
// client only.
|
|
391
|
+
orgId String? @map("org_id") @db.Uuid
|
|
392
|
+
|
|
393
|
+
// The registered AI feature the call was made for (`registerAiFeature`,
|
|
394
|
+
// PP-14.17, #935), so usage can be filtered and grouped per feature from
|
|
395
|
+
// the database. Nullable: a call made without a feature, a catalogue sync,
|
|
396
|
+
// and every row written before this column existed have none. A plain
|
|
397
|
+
// string, never a reference, and never free text: only an id the feature
|
|
398
|
+
// registry validated is written.
|
|
399
|
+
feature String?
|
|
400
|
+
|
|
401
|
+
// Relations
|
|
402
|
+
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
403
|
+
org Organization? @relation(fields: [orgId], references: [id], onDelete: SetNull)
|
|
404
|
+
|
|
405
|
+
// "This user's usage history, most recent first" — mirrors `AiRun`'s own
|
|
406
|
+
// index for the identical reason.
|
|
407
|
+
@@index([userId, createdAt])
|
|
408
|
+
// The same query behind the policy filter `org_id = X`, and the per-org
|
|
409
|
+
// usage window.
|
|
410
|
+
@@index([orgId, userId, createdAt])
|
|
411
|
+
@@index([orgId, createdAt])
|
|
412
|
+
// "This organization's usage of one feature in a window" (PP-14.17, #935):
|
|
413
|
+
// the per-feature filter and group-by of the usage report.
|
|
414
|
+
@@index([orgId, feature, createdAt])
|
|
415
|
+
// "Usage across all users in a time window" — the deployment-wide
|
|
416
|
+
// accounting/billing query, independent of any one user.
|
|
417
|
+
@@index([createdAt])
|
|
418
|
+
@@map("ai_usage_events")
|
|
419
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// GENERATED by platform db compose — do not edit
|
|
2
|
+
// source: package:android-app.prisma
|
|
3
|
+
|
|
4
|
+
/// An Android APK this deployment hosts (#746, PP-9.4; merged from EvoPath and
|
|
5
|
+
/// MemoriaHub). Uploaded by an administrator (the admin Android app page, or
|
|
6
|
+
/// `appctl android publish`), streamed into object storage under the
|
|
7
|
+
/// `android-releases/` key prefix, downloaded by signed-in users through a
|
|
8
|
+
/// short-lived signed link (`/api/android-app/download/<token>`).
|
|
9
|
+
///
|
|
10
|
+
/// AT MOST ONE RELEASE IS CURRENT deployment-wide: the raw-SQL partial unique
|
|
11
|
+
/// index `android_app_releases_one_current_uniq_idx` (migration SQL only;
|
|
12
|
+
/// Prisma cannot express a partial unique index). Never declare it as
|
|
13
|
+
/// @@unique and never replace it with a findFirst pre-check: make-current
|
|
14
|
+
/// clears the old flag and sets the new one in one transaction, and the
|
|
15
|
+
/// database rejects a second current row (P2002, mapped to 409
|
|
16
|
+
/// RELEASE_CURRENT_CONFLICT).
|
|
17
|
+
///
|
|
18
|
+
/// `sizeBytes` is BigInt (MemoriaHub's choice; EvoPath used Int): the API
|
|
19
|
+
/// returns it as a decimal string. `storageProvider` and `bucket` record where
|
|
20
|
+
/// the bytes were written (the DatabaseBackupRun precedent), so a doctor check
|
|
21
|
+
/// can tell a release stranded by a provider switch. A deployment artifact:
|
|
22
|
+
/// kept by the user-data reset (uploader SetNull) and by the factory reset
|
|
23
|
+
/// (its prefix is registered `survivesFactoryReset: true`).
|
|
24
|
+
model AndroidAppRelease {
|
|
25
|
+
id String @id @default(uuid()) @db.Uuid
|
|
26
|
+
packageName String @map("package_name")
|
|
27
|
+
versionName String @map("version_name") @db.VarChar(50)
|
|
28
|
+
versionCode Int @map("version_code")
|
|
29
|
+
signingSha256 String @map("signing_sha256")
|
|
30
|
+
fileSha256 String @map("file_sha256") @db.Char(64)
|
|
31
|
+
sizeBytes BigInt @map("size_bytes")
|
|
32
|
+
storageKey String @map("storage_key")
|
|
33
|
+
storageProvider String? @map("storage_provider")
|
|
34
|
+
bucket String?
|
|
35
|
+
notes String? @db.VarChar(2000)
|
|
36
|
+
isCurrent Boolean @default(false) @map("is_current")
|
|
37
|
+
uploadedById String? @map("uploaded_by_id") @db.Uuid
|
|
38
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
39
|
+
|
|
40
|
+
uploadedBy User? @relation("AndroidAppReleaseUploader", fields: [uploadedById], references: [id], onDelete: SetNull)
|
|
41
|
+
|
|
42
|
+
@@unique([packageName, versionCode])
|
|
43
|
+
@@index([createdAt])
|
|
44
|
+
@@index([uploadedById])
|
|
45
|
+
@@map("android_app_releases")
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// GENERATED by platform db compose — do not edit
|
|
2
|
+
// source: package:base.prisma
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Prisma Schema - Web Application Foundation
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
generator client {
|
|
9
|
+
provider = "prisma-client-js"
|
|
10
|
+
output = "../../generated"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
datasource db {
|
|
14
|
+
provider = "postgresql"
|
|
15
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// GENERATED by platform db compose — do not edit
|
|
2
|
+
// source: package:credentials.prisma
|
|
3
|
+
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Encrypted Credential Store (issue #115, epic #108)
|
|
6
|
+
// =============================================================================
|
|
7
|
+
//
|
|
8
|
+
// Runtime-configured secrets (an SMTP password, an OAuth client secret, a
|
|
9
|
+
// second bucket's S3 key, ...) addressed by (purpose, name) rather than one
|
|
10
|
+
// column per consumer, so every future secret reuses this table instead of
|
|
11
|
+
// inventing its own column, its own encrypt call, and its own chance to leak.
|
|
12
|
+
//
|
|
13
|
+
// `secret` holds ONLY the opaque base64 ciphertext produced by
|
|
14
|
+
// `encryptSecret()` from `@marinoscar/platform-api/core`, encrypted under
|
|
15
|
+
// `purpose` as the sub-key derivation label — a ciphertext lifted from one
|
|
16
|
+
// row cannot be decrypted under a different purpose. This model stores
|
|
17
|
+
// ciphertext and non-secret metadata only; it never sees plaintext, and
|
|
18
|
+
// Prisma performs no encryption of its own.
|
|
19
|
+
//
|
|
20
|
+
// NEVER select `secret` into anything that reaches an API response or a log
|
|
21
|
+
// line. `CredentialsService` is the sole reader/writer and is what actually
|
|
22
|
+
// enforces that split (a plaintext-returning method for server-side
|
|
23
|
+
// consumers, a metadata-only method for presentation) — this comment is the
|
|
24
|
+
// signpost, not the guarantee.
|
|
25
|
+
// @extensible — back-relations only, added with "extend model Credential { ... }" in a
|
|
26
|
+
// fragment, so an app can hang its own NON-SECRET bookkeeping on a deployment credential
|
|
27
|
+
// (which feature uses it, when it was last rotated). The `secret` column is never the
|
|
28
|
+
// app's to read or relate to: an app reads plaintext only through `CredentialsService`,
|
|
29
|
+
// and no app table may copy ciphertext or plaintext of it.
|
|
30
|
+
model Credential {
|
|
31
|
+
id String @id @default(uuid()) @db.Uuid
|
|
32
|
+
purpose String
|
|
33
|
+
name String
|
|
34
|
+
// Opaque [iv][authTag][ciphertext] payload, base64-encoded by
|
|
35
|
+
// encryptSecret(). @db.Text (not the default `varchar`) because a base64
|
|
36
|
+
// AES-GCM payload has no meaningful bound: encrypted key material,
|
|
37
|
+
// passwords, or tokens of arbitrary length must never be truncated.
|
|
38
|
+
secret String @db.Text
|
|
39
|
+
// Non-secret display aid for the admin UI (e.g. last 4 chars, or a fixed
|
|
40
|
+
// mask like '••••'). Nullable: a credential can exist before anyone has
|
|
41
|
+
// decided how to hint it, and some secrets have no meaningful suffix to show.
|
|
42
|
+
hint String?
|
|
43
|
+
// Human-readable description for the admin UI. Nullable for the same reason.
|
|
44
|
+
label String?
|
|
45
|
+
updatedByUserId String? @map("updated_by_user_id") @db.Uuid
|
|
46
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
47
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
48
|
+
|
|
49
|
+
// Relations
|
|
50
|
+
//
|
|
51
|
+
// onDelete: SetNull — deliberately NOT Cascade. A credential is
|
|
52
|
+
// infrastructure shared by whatever consumer looks it up by (purpose,
|
|
53
|
+
// name); the admin who last set it is provenance, not an owner the
|
|
54
|
+
// credential's lifetime should depend on. Offboarding the user who last
|
|
55
|
+
// typed in the SMTP password must not silently delete a working SMTP
|
|
56
|
+
// configuration — that failure mode (email breaks because someone left
|
|
57
|
+
// the company) is exactly what this FK behaviour avoids. The credential
|
|
58
|
+
// outliving its last editor is correct; losing it is not.
|
|
59
|
+
updatedByUser User? @relation("CredentialUpdatedBy", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
|
60
|
+
|
|
61
|
+
@@unique([purpose, name])
|
|
62
|
+
@@map("credentials")
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// Per-User Encrypted Credentials (issue #387)
|
|
67
|
+
// =============================================================================
|
|
68
|
+
//
|
|
69
|
+
// The sibling of `Credential` above for secrets OWNED BY ONE USER — a user's
|
|
70
|
+
// own key for some external service (bring-your-own-key). Addressed by
|
|
71
|
+
// (userId, purpose, name); `credentials` is untouched and keeps its
|
|
72
|
+
// ownerless (purpose, name) address.
|
|
73
|
+
//
|
|
74
|
+
// `secret` is the same opaque base64 [iv][authTag][ciphertext] payload
|
|
75
|
+
// `encryptSecret()` produces, but encrypted under an OWNER-BOUND sub-key
|
|
76
|
+
// domain, `user:<userId>:<purpose>` (`userCredentialPurpose()` in
|
|
77
|
+
// `@marinoscar/platform-api/core`), not under `purpose` alone. A ciphertext
|
|
78
|
+
// copied from user A's row into user B's row therefore fails GCM
|
|
79
|
+
// authentication instead of handing B the use of A's key.
|
|
80
|
+
//
|
|
81
|
+
// All three unique columns are NOT NULL, so `@@unique` is a real Postgres
|
|
82
|
+
// constraint (NULLs are distinct in a unique index) and `findUnique`/`upsert`
|
|
83
|
+
// on `userId_purpose_name` work. Its leading column also serves every
|
|
84
|
+
// by-user lookup, so there is no separate `user_id` index.
|
|
85
|
+
//
|
|
86
|
+
// onDelete: Cascade — deliberately the OPPOSITE of `Credential.updatedByUser`
|
|
87
|
+
// (SetNull). There, the user is provenance for shared infrastructure; here,
|
|
88
|
+
// the user is the owner, and a secret nobody owns is a secret nobody can
|
|
89
|
+
// revoke. `UserCredentialsService` is the sole reader/writer; NEVER select
|
|
90
|
+
// `secret` into anything that reaches an API response or a log line.
|
|
91
|
+
model UserCredential {
|
|
92
|
+
id String @id @default(uuid()) @db.Uuid
|
|
93
|
+
userId String @map("user_id") @db.Uuid
|
|
94
|
+
purpose String
|
|
95
|
+
name String
|
|
96
|
+
// @db.Text for the same reason as `Credential.secret`: never truncate.
|
|
97
|
+
secret String @db.Text
|
|
98
|
+
// Non-secret display aid, derived from the plaintext on write.
|
|
99
|
+
hint String?
|
|
100
|
+
// Human-readable description. User-entered, non-secret.
|
|
101
|
+
label String?
|
|
102
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
103
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
104
|
+
|
|
105
|
+
user User @relation("UserCredentials", fields: [userId], references: [id], onDelete: Cascade)
|
|
106
|
+
|
|
107
|
+
@@unique([userId, purpose, name])
|
|
108
|
+
@@map("user_credentials")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// =============================================================================
|
|
112
|
+
// Organization-Owned Encrypted Credentials (issue #735, PP-8.8)
|
|
113
|
+
// =============================================================================
|
|
114
|
+
//
|
|
115
|
+
// The third tier: secrets OWNED BY ONE ORGANIZATION (an org's own AI provider
|
|
116
|
+
// key, an org's own storage or e-mail account), addressed by
|
|
117
|
+
// (orgId, purpose, name). It mirrors `UserCredential` exactly, with the owner
|
|
118
|
+
// an organization instead of a user.
|
|
119
|
+
//
|
|
120
|
+
// `secret` is the opaque [iv][authTag][ciphertext] payload `encryptSecret()`
|
|
121
|
+
// produces under the ORG-BOUND sub-key domain `org:<orgId>:<purpose>`
|
|
122
|
+
// (`orgCredentialPurpose()` in `@marinoscar/platform-api/core`): a ciphertext
|
|
123
|
+
// copied from one organization's row into another's fails GCM
|
|
124
|
+
// authentication.
|
|
125
|
+
//
|
|
126
|
+
// A plain composite unique suffices (no raw-SQL index): all three columns are
|
|
127
|
+
// NOT NULL. Its leading column serves every by-org lookup.
|
|
128
|
+
//
|
|
129
|
+
// TENANT DATA: `org_credentials` is an `org` table under FORCEd row-level
|
|
130
|
+
// security (`org_credentials_org_isolation`, as in 0025/0026), so every read
|
|
131
|
+
// and write runs in the owning organization's scope. Cascade with the
|
|
132
|
+
// organization; the last editor is provenance only (SetNull), as on
|
|
133
|
+
// `Credential`. `OrgCredentialsService` is the sole reader/writer; NEVER
|
|
134
|
+
// select `secret` into anything that reaches an API response or a log line.
|
|
135
|
+
// =============================================================================
|
|
136
|
+
model OrgCredential {
|
|
137
|
+
id String @id @default(uuid()) @db.Uuid
|
|
138
|
+
orgId String @map("org_id") @db.Uuid
|
|
139
|
+
purpose String
|
|
140
|
+
name String
|
|
141
|
+
// @db.Text for the same reason as `Credential.secret`: never truncate.
|
|
142
|
+
secret String @db.Text
|
|
143
|
+
// Non-secret display aid, derived from the plaintext on write.
|
|
144
|
+
hint String?
|
|
145
|
+
// Human-readable description. Admin-entered, non-secret.
|
|
146
|
+
label String?
|
|
147
|
+
updatedByUserId String? @map("updated_by_user_id") @db.Uuid
|
|
148
|
+
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz
|
|
149
|
+
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz
|
|
150
|
+
|
|
151
|
+
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
|
|
152
|
+
updatedByUser User? @relation("OrgCredentialUpdatedBy", fields: [updatedByUserId], references: [id], onDelete: SetNull)
|
|
153
|
+
|
|
154
|
+
@@unique([orgId, purpose, name])
|
|
155
|
+
@@map("org_credentials")
|
|
156
|
+
}
|