@gnolith/taproot 0.2.0 → 0.3.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/COMPATIBILITY.md +5 -3
  3. package/README.md +106 -33
  4. package/SECURITY.md +6 -3
  5. package/dist/authorization-maintenance.d.ts +55 -0
  6. package/dist/authorization-maintenance.d.ts.map +1 -0
  7. package/dist/authorization-maintenance.js +686 -0
  8. package/dist/authorization-maintenance.js.map +1 -0
  9. package/dist/authorization.d.ts +98 -0
  10. package/dist/authorization.d.ts.map +1 -0
  11. package/dist/authorization.js +1137 -0
  12. package/dist/authorization.js.map +1 -0
  13. package/dist/canonical.d.ts +2 -1
  14. package/dist/canonical.d.ts.map +1 -1
  15. package/dist/canonical.js +9 -1
  16. package/dist/canonical.js.map +1 -1
  17. package/dist/errors.d.ts +4 -0
  18. package/dist/errors.d.ts.map +1 -1
  19. package/dist/errors.js +4 -0
  20. package/dist/errors.js.map +1 -1
  21. package/dist/index.d.ts +125 -49
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +351 -44
  24. package/dist/index.js.map +1 -1
  25. package/dist/migrations.d.ts +9 -3
  26. package/dist/migrations.d.ts.map +1 -1
  27. package/dist/migrations.js +77 -5
  28. package/dist/migrations.js.map +1 -1
  29. package/dist/repository.d.ts +20 -9
  30. package/dist/repository.d.ts.map +1 -1
  31. package/dist/repository.js +593 -48
  32. package/dist/repository.js.map +1 -1
  33. package/dist/schema.d.ts +14 -5
  34. package/dist/schema.d.ts.map +1 -1
  35. package/dist/schema.js +461 -18
  36. package/dist/schema.js.map +1 -1
  37. package/dist/types.d.ts +59 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/docs/api.md +56 -18
  40. package/docs/architecture.md +19 -7
  41. package/docs/authorization.md +82 -0
  42. package/docs/completion-audit.md +15 -15
  43. package/docs/operations.md +28 -5
  44. package/docs/product-scope.md +5 -3
  45. package/docs/testing.md +18 -1
  46. package/docs/threat-model.md +30 -3
  47. package/examples/d1-diamond-interop/README.md +2 -1
  48. package/examples/d1-diamond-interop/demo.ts +150 -21
  49. package/migrations/0003_canonical_statement_text.sql +31 -0
  50. package/migrations/0004_canonical_authorization_policy.sql +213 -0
  51. package/package.json +4 -1
package/dist/schema.js CHANGED
@@ -3,10 +3,11 @@ import { DataFactory } from 'rdf-data-factory';
3
3
  import { parseEntityJson } from './canonical.js';
4
4
  import { buildEntityQuads } from './rdf.js';
5
5
  import { SchemaMismatchError } from './errors.js';
6
- export const TAPROOT_SCHEMA_VERSION = '2';
7
- export const TAPROOT_JSON_VERSION = '1';
6
+ export const TAPROOT_SCHEMA_VERSION = '3';
7
+ export const TAPROOT_JSON_VERSION = '2';
8
8
  export const TAPROOT_RDF_VERSION = '2';
9
- export const taprootSchemaStatements = [
9
+ const PRE_AUTHORIZATION_SCHEMA_VERSION = '2';
10
+ const preAuthorizationTaprootSchemaStatements = [
10
11
  `CREATE TABLE IF NOT EXISTS taproot_entities (
11
12
  entity_id TEXT PRIMARY KEY,
12
13
  entity_type TEXT NOT NULL CHECK (entity_type IN ('item', 'property')),
@@ -113,15 +114,226 @@ export const taprootSchemaStatements = [
113
114
  ON CONFLICT(entity_type) DO NOTHING`,
114
115
  `INSERT INTO taproot_metadata(metadata_key, metadata_value)
115
116
  VALUES
116
- ('schema_version', '${TAPROOT_SCHEMA_VERSION}'),
117
+ ('schema_version', '${PRE_AUTHORIZATION_SCHEMA_VERSION}'),
117
118
  ('canonical_json_version', '${TAPROOT_JSON_VERSION}'),
118
119
  ('rdf_mapping_version', '${TAPROOT_RDF_VERSION}')
119
120
  ON CONFLICT(metadata_key) DO UPDATE SET metadata_value = excluded.metadata_value`,
120
- `INSERT INTO taproot_migrations(version, name) VALUES (1, 'initial'), (2, 'audit-and-operations')
121
+ `INSERT INTO taproot_migrations(version, name) VALUES (1, 'initial'), (2, 'audit-and-operations'), (3, 'canonical-statement-text')
121
122
  ON CONFLICT(version) DO NOTHING`,
122
123
  ];
124
+ /** Additive canonical authorization catalog introduced by migration 0004. */
125
+ export const taprootAuthorizationSchemaStatements = [
126
+ `CREATE TABLE IF NOT EXISTS taproot_installation_authorization (
127
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
128
+ installation_id TEXT NOT NULL UNIQUE,
129
+ authorization_revision INTEGER NOT NULL CHECK (authorization_revision >= 1),
130
+ search_generation INTEGER NOT NULL CHECK (search_generation >= 1),
131
+ last_advance_id TEXT NOT NULL,
132
+ created_at TEXT NOT NULL,
133
+ updated_at TEXT NOT NULL
134
+ ) STRICT`,
135
+ `CREATE TABLE IF NOT EXISTS taproot_entity_authorization (
136
+ entity_id TEXT PRIMARY KEY,
137
+ installation_id TEXT NOT NULL,
138
+ workspace_id TEXT,
139
+ owner_principal_id TEXT NOT NULL,
140
+ visibility_json TEXT NOT NULL CHECK (json_valid(visibility_json)),
141
+ effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
142
+ source_revision INTEGER NOT NULL,
143
+ authorization_revision INTEGER NOT NULL,
144
+ deleted_at TEXT,
145
+ event_id TEXT NOT NULL UNIQUE,
146
+ updated_at TEXT NOT NULL,
147
+ FOREIGN KEY (entity_id) REFERENCES taproot_entities(entity_id),
148
+ FOREIGN KEY (event_id) REFERENCES taproot_audit_events(event_id)
149
+ ) STRICT`,
150
+ `CREATE TABLE IF NOT EXISTS taproot_entity_authorization_revisions (
151
+ entity_id TEXT NOT NULL,
152
+ source_revision INTEGER NOT NULL,
153
+ installation_id TEXT NOT NULL,
154
+ workspace_id TEXT,
155
+ owner_principal_id TEXT NOT NULL,
156
+ visibility_json TEXT NOT NULL CHECK (json_valid(visibility_json)),
157
+ effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
158
+ authorization_revision INTEGER NOT NULL,
159
+ deleted_at TEXT,
160
+ event_id TEXT NOT NULL UNIQUE,
161
+ created_at TEXT NOT NULL,
162
+ PRIMARY KEY (entity_id, source_revision),
163
+ FOREIGN KEY (entity_id, source_revision)
164
+ REFERENCES taproot_entity_revisions(entity_id, revision),
165
+ FOREIGN KEY (event_id) REFERENCES taproot_audit_events(event_id)
166
+ ) STRICT`,
167
+ `CREATE TABLE IF NOT EXISTS taproot_statement_authorization (
168
+ entity_id TEXT NOT NULL,
169
+ statement_id TEXT NOT NULL,
170
+ source_revision INTEGER NOT NULL,
171
+ restrictions_json TEXT NOT NULL CHECK (json_valid(restrictions_json)),
172
+ effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
173
+ authorization_revision INTEGER NOT NULL,
174
+ PRIMARY KEY (entity_id, statement_id),
175
+ FOREIGN KEY (entity_id) REFERENCES taproot_entities(entity_id)
176
+ ) STRICT`,
177
+ `CREATE TABLE IF NOT EXISTS taproot_statement_authorization_revisions (
178
+ entity_id TEXT NOT NULL,
179
+ source_revision INTEGER NOT NULL,
180
+ statement_id TEXT NOT NULL,
181
+ restrictions_json TEXT NOT NULL CHECK (json_valid(restrictions_json)),
182
+ effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
183
+ authorization_revision INTEGER NOT NULL,
184
+ PRIMARY KEY (entity_id, source_revision, statement_id),
185
+ FOREIGN KEY (entity_id, source_revision)
186
+ REFERENCES taproot_entity_revisions(entity_id, revision)
187
+ ) STRICT`,
188
+ `CREATE TABLE IF NOT EXISTS taproot_authorization_projection_outbox (
189
+ event_id TEXT PRIMARY KEY,
190
+ entity_id TEXT NOT NULL,
191
+ source_revision INTEGER NOT NULL,
192
+ authorization_revision INTEGER NOT NULL,
193
+ search_generation INTEGER NOT NULL,
194
+ operation TEXT NOT NULL CHECK (operation IN ('upsert', 'delete', 'repair', 'backfill')),
195
+ state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'claimed', 'complete')),
196
+ created_at TEXT NOT NULL,
197
+ FOREIGN KEY (event_id) REFERENCES taproot_audit_events(event_id)
198
+ ) STRICT`,
199
+ `CREATE TABLE IF NOT EXISTS taproot_authorization_backfill_plans (
200
+ plan_id TEXT PRIMARY KEY,
201
+ installation_id TEXT NOT NULL,
202
+ base_authorization_revision INTEGER NOT NULL,
203
+ manifest_json TEXT NOT NULL CHECK (json_valid(manifest_json)),
204
+ manifest_hash TEXT NOT NULL,
205
+ entity_count INTEGER NOT NULL CHECK (entity_count > 0 AND entity_count <= 100),
206
+ revision_count INTEGER NOT NULL CHECK (revision_count > 0),
207
+ status TEXT NOT NULL CHECK (status IN ('planned', 'applying', 'complete')),
208
+ created_by TEXT NOT NULL,
209
+ created_at TEXT NOT NULL,
210
+ completed_at TEXT
211
+ ) STRICT`,
212
+ `CREATE TABLE IF NOT EXISTS taproot_authorization_admin_audit (
213
+ sequence INTEGER PRIMARY KEY AUTOINCREMENT,
214
+ audit_id TEXT NOT NULL UNIQUE,
215
+ event_type TEXT NOT NULL CHECK (event_type IN ('backfill-plan', 'backfill-apply')),
216
+ principal_id TEXT NOT NULL,
217
+ plan_id TEXT NOT NULL,
218
+ authorization_revision INTEGER NOT NULL,
219
+ details_json TEXT NOT NULL CHECK (json_valid(details_json)),
220
+ created_at TEXT NOT NULL,
221
+ FOREIGN KEY (plan_id) REFERENCES taproot_authorization_backfill_plans(plan_id)
222
+ ) STRICT`,
223
+ `CREATE TABLE IF NOT EXISTS taproot_installation_authorization_advances (
224
+ advance_id TEXT PRIMARY KEY,
225
+ installation_id TEXT NOT NULL,
226
+ from_revision INTEGER NOT NULL,
227
+ to_revision INTEGER NOT NULL,
228
+ search_generation INTEGER NOT NULL,
229
+ domain TEXT NOT NULL,
230
+ principal_id TEXT NOT NULL,
231
+ reason TEXT NOT NULL,
232
+ created_at TEXT NOT NULL,
233
+ CHECK (to_revision = from_revision + 1)
234
+ ) STRICT`,
235
+ `CREATE INDEX IF NOT EXISTS taproot_entity_authorization_candidate_idx
236
+ ON taproot_entity_authorization(installation_id, deleted_at, entity_id)`,
237
+ `CREATE INDEX IF NOT EXISTS taproot_entity_authorization_revision_idx
238
+ ON taproot_entity_authorization_revisions(entity_id, source_revision DESC)`,
239
+ `CREATE INDEX IF NOT EXISTS taproot_statement_authorization_candidate_idx
240
+ ON taproot_statement_authorization(entity_id, source_revision, statement_id)`,
241
+ `CREATE INDEX IF NOT EXISTS taproot_authorization_outbox_state_idx
242
+ ON taproot_authorization_projection_outbox(state, authorization_revision, event_id)`,
243
+ `CREATE TRIGGER IF NOT EXISTS taproot_revisions_no_replace
244
+ BEFORE INSERT ON taproot_entity_revisions
245
+ WHEN EXISTS (
246
+ SELECT 1 FROM taproot_entity_revisions
247
+ WHERE entity_id = NEW.entity_id AND revision = NEW.revision
248
+ )
249
+ BEGIN SELECT RAISE(ABORT, 'taproot revisions cannot be replaced'); END`,
250
+ `CREATE TRIGGER IF NOT EXISTS taproot_audit_no_replace
251
+ BEFORE INSERT ON taproot_audit_events
252
+ WHEN EXISTS (
253
+ SELECT 1 FROM taproot_audit_events WHERE event_id = NEW.event_id
254
+ )
255
+ BEGIN SELECT RAISE(ABORT, 'taproot audit events cannot be replaced'); END`,
256
+ `CREATE TRIGGER IF NOT EXISTS taproot_installation_identity_no_update
257
+ BEFORE UPDATE OF installation_id ON taproot_installation_authorization
258
+ BEGIN SELECT RAISE(ABORT, 'taproot installation identity is immutable'); END`,
259
+ `CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_no_delete
260
+ BEFORE DELETE ON taproot_installation_authorization
261
+ BEGIN SELECT RAISE(ABORT, 'taproot installation authorization is durable'); END`,
262
+ `CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_no_replace
263
+ BEFORE INSERT ON taproot_installation_authorization
264
+ WHEN EXISTS (SELECT 1 FROM taproot_installation_authorization WHERE singleton = NEW.singleton)
265
+ BEGIN SELECT RAISE(ABORT, 'taproot installation authorization cannot be replaced'); END`,
266
+ `CREATE TRIGGER IF NOT EXISTS taproot_entity_authorization_revisions_no_update
267
+ BEFORE UPDATE ON taproot_entity_authorization_revisions
268
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization revisions are immutable'); END`,
269
+ `CREATE TRIGGER IF NOT EXISTS taproot_entity_authorization_revisions_no_delete
270
+ BEFORE DELETE ON taproot_entity_authorization_revisions
271
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization revisions are immutable'); END`,
272
+ `CREATE TRIGGER IF NOT EXISTS taproot_entity_authorization_revisions_no_replace
273
+ BEFORE INSERT ON taproot_entity_authorization_revisions
274
+ WHEN EXISTS (
275
+ SELECT 1 FROM taproot_entity_authorization_revisions
276
+ WHERE (entity_id = NEW.entity_id AND source_revision = NEW.source_revision)
277
+ OR event_id = NEW.event_id
278
+ )
279
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization revisions cannot be replaced'); END`,
280
+ `CREATE TRIGGER IF NOT EXISTS taproot_statement_authorization_revisions_no_update
281
+ BEFORE UPDATE ON taproot_statement_authorization_revisions
282
+ BEGIN SELECT RAISE(ABORT, 'taproot statement authorization revisions are immutable'); END`,
283
+ `CREATE TRIGGER IF NOT EXISTS taproot_statement_authorization_revisions_no_delete
284
+ BEFORE DELETE ON taproot_statement_authorization_revisions
285
+ BEGIN SELECT RAISE(ABORT, 'taproot statement authorization revisions are immutable'); END`,
286
+ `CREATE TRIGGER IF NOT EXISTS taproot_statement_authorization_revisions_no_replace
287
+ BEFORE INSERT ON taproot_statement_authorization_revisions
288
+ WHEN EXISTS (
289
+ SELECT 1 FROM taproot_statement_authorization_revisions
290
+ WHERE entity_id = NEW.entity_id AND source_revision = NEW.source_revision
291
+ AND statement_id = NEW.statement_id
292
+ )
293
+ BEGIN SELECT RAISE(ABORT, 'taproot statement authorization revisions cannot be replaced'); END`,
294
+ `CREATE TRIGGER IF NOT EXISTS taproot_authorization_admin_audit_no_update
295
+ BEFORE UPDATE ON taproot_authorization_admin_audit
296
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization administration audit is immutable'); END`,
297
+ `CREATE TRIGGER IF NOT EXISTS taproot_authorization_admin_audit_no_delete
298
+ BEFORE DELETE ON taproot_authorization_admin_audit
299
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization administration audit is immutable'); END`,
300
+ `CREATE TRIGGER IF NOT EXISTS taproot_authorization_admin_audit_no_replace
301
+ BEFORE INSERT ON taproot_authorization_admin_audit
302
+ WHEN EXISTS (
303
+ SELECT 1 FROM taproot_authorization_admin_audit
304
+ WHERE sequence = NEW.sequence OR audit_id = NEW.audit_id
305
+ )
306
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization administration audit cannot be replaced'); END`,
307
+ `CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_advances_no_update
308
+ BEFORE UPDATE ON taproot_installation_authorization_advances
309
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization advances are immutable'); END`,
310
+ `CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_advances_no_delete
311
+ BEFORE DELETE ON taproot_installation_authorization_advances
312
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization advances are immutable'); END`,
313
+ `CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_advances_no_replace
314
+ BEFORE INSERT ON taproot_installation_authorization_advances
315
+ WHEN EXISTS (
316
+ SELECT 1 FROM taproot_installation_authorization_advances
317
+ WHERE advance_id = NEW.advance_id
318
+ )
319
+ BEGIN SELECT RAISE(ABORT, 'taproot authorization advances cannot be replaced'); END`,
320
+ `INSERT INTO taproot_metadata(metadata_key, metadata_value)
321
+ VALUES ('schema_version', '${TAPROOT_SCHEMA_VERSION}')
322
+ ON CONFLICT(metadata_key) DO UPDATE SET metadata_value = excluded.metadata_value`,
323
+ `INSERT INTO taproot_migrations(version, name)
324
+ VALUES (4, 'canonical-authorization-policy')
325
+ ON CONFLICT(version) DO NOTHING`,
326
+ ];
327
+ export const taprootSchemaStatements = [
328
+ ...preAuthorizationTaprootSchemaStatements,
329
+ ...taprootAuthorizationSchemaStatements,
330
+ ];
331
+ /** Exact package-created schema before authored statement text became required. */
332
+ export const preStatementTextTaprootSchemaStatements = preAuthorizationTaprootSchemaStatements.map((sql) => sql
333
+ .replace("('canonical_json_version', '2')", "('canonical_json_version', '1')")
334
+ .replace(", (3, 'canonical-statement-text')", ''));
123
335
  const schemaStatement = (prefix) => {
124
- const statement = taprootSchemaStatements.find((sql) => sql.trimStart().startsWith(prefix));
336
+ const statement = preAuthorizationTaprootSchemaStatements.find((sql) => sql.trimStart().startsWith(prefix));
125
337
  if (!statement)
126
338
  throw new Error(`Missing Taproot schema statement: ${prefix}`);
127
339
  return statement;
@@ -159,12 +371,12 @@ const currentRevisionStatement = schemaStatement('CREATE TABLE IF NOT EXISTS tap
159
371
  const transitionalRevisionStatement = currentRevisionStatement
160
372
  .replace('event_id TEXT NOT NULL', 'event_id TEXT')
161
373
  .replace('content_hash TEXT NOT NULL', 'content_hash TEXT');
162
- export const taprootUpgradeCatalogStatements = taprootSchemaStatements
374
+ export const taprootUpgradeCatalogStatements = preAuthorizationTaprootSchemaStatements
163
375
  .filter((sql) => /^\s*CREATE\s+(?:TABLE|INDEX)\s+/iu.test(sql) &&
164
376
  !/^\s*CREATE\s+TRIGGER\s+/iu.test(sql) &&
165
377
  !sql.includes('taproot_audit'))
166
378
  .map((sql) => sql === currentRevisionStatement ? transitionalRevisionStatement : sql);
167
- export const taprootPreFinalizeCatalogStatements = taprootSchemaStatements.filter((sql) => /^\s*CREATE\s+(?:TABLE|INDEX)\s+/iu.test(sql) &&
379
+ export const taprootPreFinalizeCatalogStatements = preAuthorizationTaprootSchemaStatements.filter((sql) => /^\s*CREATE\s+(?:TABLE|INDEX)\s+/iu.test(sql) &&
168
380
  !/^\s*CREATE\s+TRIGGER\s+/iu.test(sql));
169
381
  export const legacyTaprootStructureStatements = [
170
382
  `ALTER TABLE taproot_entity_revisions RENAME TO taproot_entity_revisions_v1`,
@@ -194,9 +406,12 @@ export const legacyRevisionFinalizeStatements = [
194
406
  schemaStatement('CREATE INDEX IF NOT EXISTS taproot_audit_entity_idx'),
195
407
  schemaStatement('CREATE INDEX IF NOT EXISTS taproot_audit_request_idx'),
196
408
  ];
197
- export const taprootFinalizeStatements = taprootSchemaStatements.filter((sql) => /^\s*CREATE\s+TRIGGER\s+/iu.test(sql) ||
198
- sql.includes(`('schema_version', '${TAPROOT_SCHEMA_VERSION}')`) ||
199
- sql.includes('INSERT INTO taproot_migrations'));
409
+ export const taprootFinalizeStatements = [
410
+ ...preAuthorizationTaprootSchemaStatements.filter((sql) => /^\s*CREATE\s+TRIGGER\s+/iu.test(sql) ||
411
+ sql.includes(`('schema_version', '${PRE_AUTHORIZATION_SCHEMA_VERSION}')`) ||
412
+ sql.includes('INSERT INTO taproot_migrations')),
413
+ ...taprootAuthorizationSchemaStatements,
414
+ ];
200
415
  export async function initializeTaproot(db, options = {}) {
201
416
  const { applyTaprootMigrations } = await import('./migrations.js');
202
417
  await applyTaprootMigrations(db, options);
@@ -347,9 +562,11 @@ export async function backfillTaprootAudit(db) {
347
562
  SELECT event_id, entity_id, revision, 'import',
348
563
  attribution_json, edit_summary, tags_json, content_hash, parent_hash,
349
564
  json_object('source', 'legacy-v1'), created_at
350
- FROM taproot_entity_revisions
351
- WHERE true
352
- ON CONFLICT(event_id) DO NOTHING`),
565
+ FROM taproot_entity_revisions revision
566
+ WHERE NOT EXISTS (
567
+ SELECT 1 FROM taproot_audit_events audit
568
+ WHERE audit.event_id = revision.event_id
569
+ )`),
353
570
  ]);
354
571
  }
355
572
  export async function verifyTaprootSemanticState(db, baseIri) {
@@ -415,17 +632,82 @@ export async function verifyTaprootSemanticState(db, baseIri) {
415
632
  throw new SchemaMismatchError(`Taproot RDF ownership backfill is incomplete for ${row.entity_id}`);
416
633
  }
417
634
  }
635
+ /** Uses JavaScript trim semantics, including Unicode whitespace, for parity with runtime validation. */
636
+ export const PERSISTED_STATEMENT_TEXT_PAGE_SIZE = 100;
637
+ export async function verifyPersistedStatementText(db) {
638
+ let currentEntityId = '';
639
+ while (true) {
640
+ const page = await db
641
+ .prepare(`/* taproot:statement-text-current-page */
642
+ SELECT entity_id, revision, entity_json
643
+ FROM taproot_entities
644
+ WHERE entity_id > ?
645
+ ORDER BY entity_id
646
+ LIMIT ?`)
647
+ .bind(currentEntityId, PERSISTED_STATEMENT_TEXT_PAGE_SIZE)
648
+ .all();
649
+ for (const row of page.results)
650
+ validatePersistedStatementText(row, 'current');
651
+ if (page.results.length < PERSISTED_STATEMENT_TEXT_PAGE_SIZE)
652
+ break;
653
+ currentEntityId = page.results.at(-1)
654
+ .entity_id;
655
+ }
656
+ let historicalEntityId = '';
657
+ let historicalRevision = -1;
658
+ while (true) {
659
+ const page = await db
660
+ .prepare(`/* taproot:statement-text-history-page */
661
+ SELECT entity_id, revision, entity_json
662
+ FROM taproot_entity_revisions
663
+ WHERE entity_id > ? OR (entity_id = ? AND revision > ?)
664
+ ORDER BY entity_id, revision
665
+ LIMIT ?`)
666
+ .bind(historicalEntityId, historicalEntityId, historicalRevision, PERSISTED_STATEMENT_TEXT_PAGE_SIZE)
667
+ .all();
668
+ for (const row of page.results)
669
+ validatePersistedStatementText(row, 'historical');
670
+ if (page.results.length < PERSISTED_STATEMENT_TEXT_PAGE_SIZE)
671
+ break;
672
+ const last = page.results.at(-1);
673
+ historicalEntityId = last.entity_id;
674
+ historicalRevision = last.revision;
675
+ }
676
+ }
677
+ function validatePersistedStatementText(row, source) {
678
+ let entity;
679
+ try {
680
+ entity = JSON.parse(row.entity_json);
681
+ }
682
+ catch (cause) {
683
+ throw new SchemaMismatchError(`Taproot ${source} entity ${row.entity_id}@${row.revision} is not valid JSON`, { cause });
684
+ }
685
+ if (!isRecord(entity) || !isRecord(entity.claims))
686
+ throw new SchemaMismatchError(`Taproot ${source} entity ${row.entity_id}@${row.revision} has invalid claims`);
687
+ for (const statements of Object.values(entity.claims)) {
688
+ if (!Array.isArray(statements))
689
+ throw new SchemaMismatchError(`Taproot ${source} entity ${row.entity_id}@${row.revision} has an invalid claim group`);
690
+ for (const statement of statements) {
691
+ if (!isRecord(statement) ||
692
+ typeof statement.text !== 'string' ||
693
+ statement.text.trim().length === 0)
694
+ throw new SchemaMismatchError(`Taproot ${source} statement text is missing or blank at ${row.entity_id}@${row.revision}`);
695
+ }
696
+ }
697
+ }
418
698
  export async function verifyTaprootPackageSeeds(db) {
419
699
  const migrationSeeds = await db
420
700
  .prepare(`SELECT COUNT(*) AS count FROM taproot_migrations
421
701
  WHERE (version = 1 AND name = 'initial')
422
- OR (version = 2 AND name = 'audit-and-operations')`)
702
+ OR (version = 2 AND name = 'audit-and-operations')
703
+ OR (version = 3 AND name = 'canonical-statement-text')
704
+ OR (version = 4 AND name = 'canonical-authorization-policy')`)
423
705
  .all();
424
706
  const migrationSeedTotal = await db
425
707
  .prepare(`SELECT COUNT(*) AS count FROM taproot_migrations`)
426
708
  .all();
427
- if (Number(migrationSeeds.results[0]?.count ?? 0) !== 2 ||
428
- Number(migrationSeedTotal.results[0]?.count ?? 0) !== 2)
709
+ if (Number(migrationSeeds.results[0]?.count ?? 0) !== 4 ||
710
+ Number(migrationSeedTotal.results[0]?.count ?? 0) !== 4)
429
711
  throw new SchemaMismatchError('Taproot package migration seeds are incomplete');
430
712
  }
431
713
  async function hash(value) {
@@ -445,6 +727,15 @@ export async function inspectTaprootSchema(db) {
445
727
  'taproot_audit_events',
446
728
  'taproot_migrations',
447
729
  'taproot_rdf_ownership',
730
+ 'taproot_installation_authorization',
731
+ 'taproot_entity_authorization',
732
+ 'taproot_entity_authorization_revisions',
733
+ 'taproot_statement_authorization',
734
+ 'taproot_statement_authorization_revisions',
735
+ 'taproot_authorization_projection_outbox',
736
+ 'taproot_authorization_backfill_plans',
737
+ 'taproot_authorization_admin_audit',
738
+ 'taproot_installation_authorization_advances',
448
739
  ];
449
740
  const tables = await db
450
741
  .prepare(`SELECT name, sql FROM sqlite_schema
@@ -487,6 +778,120 @@ export async function inspectTaprootSchema(db) {
487
778
  : { results: [] };
488
779
  const presentColumns = new Set(revisionColumns.results.map(({ name }) => name));
489
780
  const missingColumns = requiredRevisionColumns.filter((name) => !presentColumns.has(name));
781
+ const authorizationColumns = {
782
+ taproot_installation_authorization: [
783
+ 'singleton',
784
+ 'installation_id',
785
+ 'authorization_revision',
786
+ 'search_generation',
787
+ 'last_advance_id',
788
+ 'created_at',
789
+ 'updated_at',
790
+ ],
791
+ taproot_entity_authorization: [
792
+ 'entity_id',
793
+ 'installation_id',
794
+ 'workspace_id',
795
+ 'owner_principal_id',
796
+ 'visibility_json',
797
+ 'effective_visibility_json',
798
+ 'source_revision',
799
+ 'authorization_revision',
800
+ 'deleted_at',
801
+ 'event_id',
802
+ 'updated_at',
803
+ ],
804
+ taproot_entity_authorization_revisions: [
805
+ 'entity_id',
806
+ 'source_revision',
807
+ 'installation_id',
808
+ 'workspace_id',
809
+ 'owner_principal_id',
810
+ 'visibility_json',
811
+ 'effective_visibility_json',
812
+ 'authorization_revision',
813
+ 'deleted_at',
814
+ 'event_id',
815
+ 'created_at',
816
+ ],
817
+ taproot_statement_authorization: [
818
+ 'entity_id',
819
+ 'statement_id',
820
+ 'source_revision',
821
+ 'restrictions_json',
822
+ 'effective_visibility_json',
823
+ 'authorization_revision',
824
+ ],
825
+ taproot_statement_authorization_revisions: [
826
+ 'entity_id',
827
+ 'source_revision',
828
+ 'statement_id',
829
+ 'restrictions_json',
830
+ 'effective_visibility_json',
831
+ 'authorization_revision',
832
+ ],
833
+ taproot_authorization_projection_outbox: [
834
+ 'event_id',
835
+ 'entity_id',
836
+ 'source_revision',
837
+ 'authorization_revision',
838
+ 'search_generation',
839
+ 'operation',
840
+ 'state',
841
+ 'created_at',
842
+ ],
843
+ taproot_authorization_backfill_plans: [
844
+ 'plan_id',
845
+ 'installation_id',
846
+ 'base_authorization_revision',
847
+ 'manifest_json',
848
+ 'manifest_hash',
849
+ 'entity_count',
850
+ 'revision_count',
851
+ 'status',
852
+ 'created_by',
853
+ 'created_at',
854
+ 'completed_at',
855
+ ],
856
+ taproot_authorization_admin_audit: [
857
+ 'sequence',
858
+ 'audit_id',
859
+ 'event_type',
860
+ 'principal_id',
861
+ 'plan_id',
862
+ 'authorization_revision',
863
+ 'details_json',
864
+ 'created_at',
865
+ ],
866
+ taproot_installation_authorization_advances: [
867
+ 'advance_id',
868
+ 'installation_id',
869
+ 'from_revision',
870
+ 'to_revision',
871
+ 'search_generation',
872
+ 'domain',
873
+ 'principal_id',
874
+ 'reason',
875
+ 'created_at',
876
+ ],
877
+ };
878
+ for (const [table, expectedColumns] of Object.entries(authorizationColumns)) {
879
+ if (!names.has(table))
880
+ continue;
881
+ const columns = await db
882
+ .prepare(`PRAGMA table_info(${table})`)
883
+ .all();
884
+ const actualColumns = columns.results.map(({ name }) => name);
885
+ if (JSON.stringify(actualColumns) !== JSON.stringify(expectedColumns)) {
886
+ errors.push(`${table} columns are ${actualColumns.join(',')}, expected ${expectedColumns.join(',')}`);
887
+ }
888
+ const actualSql = tables.results.find(({ name }) => name === table)?.sql;
889
+ const expectedSql = taprootAuthorizationSchemaStatements.find((sql) => new RegExp(`^\\s*CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${table}\\b`, 'iu').test(sql));
890
+ if (actualSql == null ||
891
+ expectedSql === undefined ||
892
+ normalizeCatalogSql(actualSql) !== normalizeCatalogSql(expectedSql))
893
+ errors.push(`${table} definition does not match the package catalog`);
894
+ }
490
895
  const requiredIndexes = [
491
896
  'taproot_entities_type_idx',
492
897
  'taproot_entities_modified_idx',
@@ -495,6 +900,10 @@ export async function inspectTaprootSchema(db) {
495
900
  'taproot_audit_entity_idx',
496
901
  'taproot_audit_request_idx',
497
902
  'taproot_rdf_ownership_quad_idx',
903
+ 'taproot_entity_authorization_candidate_idx',
904
+ 'taproot_entity_authorization_revision_idx',
905
+ 'taproot_statement_authorization_candidate_idx',
906
+ 'taproot_authorization_outbox_state_idx',
498
907
  ];
499
908
  const indexRows = await db
500
909
  .prepare(`SELECT name FROM sqlite_schema WHERE type = 'index' AND name LIKE 'taproot_%'`)
@@ -506,12 +915,39 @@ export async function inspectTaprootSchema(db) {
506
915
  'taproot_revisions_no_delete',
507
916
  'taproot_audit_no_update',
508
917
  'taproot_audit_no_delete',
918
+ 'taproot_revisions_no_replace',
919
+ 'taproot_audit_no_replace',
920
+ 'taproot_installation_identity_no_update',
921
+ 'taproot_installation_authorization_no_delete',
922
+ 'taproot_installation_authorization_no_replace',
923
+ 'taproot_entity_authorization_revisions_no_update',
924
+ 'taproot_entity_authorization_revisions_no_delete',
925
+ 'taproot_entity_authorization_revisions_no_replace',
926
+ 'taproot_statement_authorization_revisions_no_update',
927
+ 'taproot_statement_authorization_revisions_no_delete',
928
+ 'taproot_statement_authorization_revisions_no_replace',
929
+ 'taproot_authorization_admin_audit_no_update',
930
+ 'taproot_authorization_admin_audit_no_delete',
931
+ 'taproot_authorization_admin_audit_no_replace',
932
+ 'taproot_installation_authorization_advances_no_update',
933
+ 'taproot_installation_authorization_advances_no_delete',
934
+ 'taproot_installation_authorization_advances_no_replace',
509
935
  ];
510
936
  const triggerRows = await db
511
- .prepare(`SELECT name FROM sqlite_schema WHERE type = 'trigger' AND name LIKE 'taproot_%'`)
937
+ .prepare(`SELECT name, sql FROM sqlite_schema
938
+ WHERE type = 'trigger' AND name LIKE 'taproot_%'`)
512
939
  .all();
513
940
  const presentTriggers = new Set(triggerRows.results.map(({ name }) => name));
514
941
  const missingTriggers = requiredTriggers.filter((name) => !presentTriggers.has(name));
942
+ for (const trigger of requiredTriggers) {
943
+ const actualSql = triggerRows.results.find(({ name }) => name === trigger)?.sql;
944
+ if (actualSql == null)
945
+ continue;
946
+ const expectedSql = taprootSchemaStatements.find((sql) => new RegExp(`^\\s*CREATE\\s+TRIGGER\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?${trigger}\\b`, 'iu').test(sql));
947
+ if (expectedSql === undefined ||
948
+ normalizeCatalogSql(actualSql) !== normalizeCatalogSql(expectedSql))
949
+ errors.push(`${trigger} definition does not match the package catalog`);
950
+ }
515
951
  errors.push(...missingColumns.map((name) => `taproot_entity_revisions.${name} is missing`));
516
952
  errors.push(...missingIndexes.map((name) => `${name} is missing`));
517
953
  errors.push(...missingTriggers.map((name) => `${name} is missing`));
@@ -539,12 +975,19 @@ export async function inspectTaprootSchema(db) {
539
975
  export async function isExactTaprootSchema(db) {
540
976
  return matchesExactCatalog(db, taprootSchemaStatements);
541
977
  }
978
+ /** Exact package catalog immediately before canonical authorization migration 0004. */
979
+ export async function isExactPreAuthorizationTaprootSchema(db) {
980
+ return matchesExactCatalog(db, preAuthorizationTaprootSchemaStatements);
981
+ }
542
982
  export async function isExactTaprootUpgradeSchema(db) {
543
983
  return matchesExactCatalog(db, taprootUpgradeCatalogStatements);
544
984
  }
545
985
  export async function isExactTaprootPreFinalizeSchema(db) {
546
986
  return matchesExactCatalog(db, taprootPreFinalizeCatalogStatements);
547
987
  }
988
+ function isRecord(value) {
989
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
990
+ }
548
991
  async function matchesExactCatalog(db, statements) {
549
992
  const expected = new Map();
550
993
  for (const sql of statements) {