@gnolith/taproot 0.1.0-rc.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.
- package/CHANGELOG.md +63 -0
- package/COMPATIBILITY.md +15 -3
- package/LICENSE +21 -21
- package/README.md +122 -46
- package/SECURITY.md +10 -3
- package/SUPPORT.md +6 -1
- package/dist/authorization-maintenance.d.ts +55 -0
- package/dist/authorization-maintenance.d.ts.map +1 -0
- package/dist/authorization-maintenance.js +686 -0
- package/dist/authorization-maintenance.js.map +1 -0
- package/dist/authorization.d.ts +98 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +1137 -0
- package/dist/authorization.js.map +1 -0
- package/dist/canonical.d.ts +2 -1
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +9 -1
- package/dist/canonical.js.map +1 -1
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +127 -50
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -44
- package/dist/index.js.map +1 -1
- package/dist/migrations.d.ts +42 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +550 -0
- package/dist/migrations.js.map +1 -0
- package/dist/rdf.js +1 -1
- package/dist/rdf.js.map +1 -1
- package/dist/repository.d.ts +22 -11
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +596 -63
- package/dist/repository.js.map +1 -1
- package/dist/schema.d.ts +41 -7
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +722 -83
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/api.md +70 -18
- package/docs/architecture.md +21 -8
- package/docs/authorization.md +82 -0
- package/docs/completion-audit.md +22 -16
- package/docs/operations.md +48 -9
- package/docs/product-scope.md +11 -4
- package/docs/release-policy.md +4 -0
- package/docs/testing.md +31 -5
- package/docs/threat-model.md +38 -6
- package/examples/d1-diamond-interop/README.md +12 -0
- package/examples/d1-diamond-interop/demo.ts +281 -0
- package/migrations/0002_audit_operations.sql +4 -3
- package/migrations/0003_canonical_statement_text.sql +31 -0
- package/migrations/0004_canonical_authorization_policy.sql +213 -0
- package/package.json +12 -7
- package/examples/codex-site/.openai/hosting.json +0 -3
- package/examples/codex-site/README.md +0 -13
- package/examples/codex-site/demo.ts +0 -152
package/dist/schema.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { encodeTerm,
|
|
1
|
+
import { encodeTerm, prepareQuadPatch, } from '@gnolith/diamond';
|
|
2
2
|
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 = '
|
|
7
|
-
export const TAPROOT_JSON_VERSION = '
|
|
6
|
+
export const TAPROOT_SCHEMA_VERSION = '3';
|
|
7
|
+
export const TAPROOT_JSON_VERSION = '2';
|
|
8
8
|
export const TAPROOT_RDF_VERSION = '2';
|
|
9
|
-
|
|
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,41 +114,333 @@ 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', '${
|
|
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
|
];
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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')", ''));
|
|
335
|
+
const schemaStatement = (prefix) => {
|
|
336
|
+
const statement = preAuthorizationTaprootSchemaStatements.find((sql) => sql.trimStart().startsWith(prefix));
|
|
337
|
+
if (!statement)
|
|
338
|
+
throw new Error(`Missing Taproot schema statement: ${prefix}`);
|
|
339
|
+
return statement;
|
|
340
|
+
};
|
|
341
|
+
/** Exact package-created schema used before the v2 audit migration. */
|
|
342
|
+
export const legacyTaprootV1Statements = [
|
|
343
|
+
schemaStatement('CREATE TABLE IF NOT EXISTS taproot_entities'),
|
|
344
|
+
`CREATE TABLE IF NOT EXISTS taproot_entity_revisions (
|
|
345
|
+
entity_id TEXT NOT NULL,
|
|
346
|
+
revision INTEGER NOT NULL,
|
|
347
|
+
entity_json TEXT NOT NULL CHECK (json_valid(entity_json)),
|
|
348
|
+
actor TEXT,
|
|
349
|
+
edit_summary TEXT,
|
|
350
|
+
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
351
|
+
PRIMARY KEY (entity_id, revision),
|
|
352
|
+
FOREIGN KEY (entity_id) REFERENCES taproot_entities(entity_id)
|
|
353
|
+
) STRICT`,
|
|
354
|
+
schemaStatement('CREATE TABLE IF NOT EXISTS taproot_id_counters'),
|
|
355
|
+
schemaStatement('CREATE TABLE IF NOT EXISTS taproot_terms'),
|
|
356
|
+
schemaStatement('CREATE TABLE IF NOT EXISTS taproot_metadata'),
|
|
357
|
+
schemaStatement('CREATE TABLE IF NOT EXISTS taproot_assertions'),
|
|
358
|
+
schemaStatement('CREATE INDEX IF NOT EXISTS taproot_entities_type_idx'),
|
|
359
|
+
schemaStatement('CREATE INDEX IF NOT EXISTS taproot_entities_modified_idx'),
|
|
360
|
+
schemaStatement('CREATE INDEX IF NOT EXISTS taproot_revisions_entity_idx'),
|
|
361
|
+
schemaStatement('CREATE INDEX IF NOT EXISTS taproot_terms_lookup_idx'),
|
|
362
|
+
`INSERT INTO taproot_id_counters(entity_type, next_numeric_id)
|
|
363
|
+
VALUES ('item', 1), ('property', 1)`,
|
|
364
|
+
`INSERT INTO taproot_metadata(metadata_key, metadata_value)
|
|
365
|
+
VALUES
|
|
366
|
+
('schema_version', '1'),
|
|
367
|
+
('canonical_json_version', '1'),
|
|
368
|
+
('rdf_mapping_version', '1')`,
|
|
369
|
+
];
|
|
370
|
+
const currentRevisionStatement = schemaStatement('CREATE TABLE IF NOT EXISTS taproot_entity_revisions');
|
|
371
|
+
const transitionalRevisionStatement = currentRevisionStatement
|
|
372
|
+
.replace('event_id TEXT NOT NULL', 'event_id TEXT')
|
|
373
|
+
.replace('content_hash TEXT NOT NULL', 'content_hash TEXT');
|
|
374
|
+
export const taprootUpgradeCatalogStatements = preAuthorizationTaprootSchemaStatements
|
|
375
|
+
.filter((sql) => /^\s*CREATE\s+(?:TABLE|INDEX)\s+/iu.test(sql) &&
|
|
376
|
+
!/^\s*CREATE\s+TRIGGER\s+/iu.test(sql) &&
|
|
377
|
+
!sql.includes('taproot_audit'))
|
|
378
|
+
.map((sql) => sql === currentRevisionStatement ? transitionalRevisionStatement : sql);
|
|
379
|
+
export const taprootPreFinalizeCatalogStatements = preAuthorizationTaprootSchemaStatements.filter((sql) => /^\s*CREATE\s+(?:TABLE|INDEX)\s+/iu.test(sql) &&
|
|
380
|
+
!/^\s*CREATE\s+TRIGGER\s+/iu.test(sql));
|
|
381
|
+
export const legacyTaprootStructureStatements = [
|
|
382
|
+
`ALTER TABLE taproot_entity_revisions RENAME TO taproot_entity_revisions_v1`,
|
|
383
|
+
transitionalRevisionStatement,
|
|
384
|
+
`INSERT INTO taproot_entity_revisions(
|
|
385
|
+
entity_id, revision, entity_json, actor, attribution_json, edit_summary,
|
|
386
|
+
tags_json, event_id, content_hash, parent_hash, deleted_at, redirect_to, created_at
|
|
387
|
+
)
|
|
388
|
+
SELECT entity_id, revision, entity_json, actor, NULL, edit_summary,
|
|
389
|
+
'[]', NULL, NULL, NULL, NULL, NULL, created_at
|
|
390
|
+
FROM taproot_entity_revisions_v1`,
|
|
391
|
+
`DROP TABLE taproot_entity_revisions_v1`,
|
|
392
|
+
...taprootUpgradeCatalogStatements.filter((sql) => !sql.includes('taproot_entity_revisions (')),
|
|
393
|
+
];
|
|
394
|
+
export const legacyRevisionFinalizeStatements = [
|
|
395
|
+
`ALTER TABLE taproot_entity_revisions RENAME TO taproot_entity_revisions_backfill`,
|
|
396
|
+
currentRevisionStatement,
|
|
397
|
+
`INSERT INTO taproot_entity_revisions(
|
|
398
|
+
entity_id, revision, entity_json, actor, attribution_json, edit_summary,
|
|
399
|
+
tags_json, event_id, content_hash, parent_hash, deleted_at, redirect_to, created_at
|
|
400
|
+
) SELECT entity_id, revision, entity_json, actor, attribution_json, edit_summary,
|
|
401
|
+
tags_json, event_id, content_hash, parent_hash, deleted_at, redirect_to, created_at
|
|
402
|
+
FROM taproot_entity_revisions_backfill`,
|
|
403
|
+
`DROP TABLE taproot_entity_revisions_backfill`,
|
|
404
|
+
schemaStatement('CREATE TABLE IF NOT EXISTS taproot_audit_events'),
|
|
405
|
+
schemaStatement('CREATE INDEX IF NOT EXISTS taproot_revisions_entity_idx'),
|
|
406
|
+
schemaStatement('CREATE INDEX IF NOT EXISTS taproot_audit_entity_idx'),
|
|
407
|
+
schemaStatement('CREATE INDEX IF NOT EXISTS taproot_audit_request_idx'),
|
|
408
|
+
];
|
|
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
|
+
];
|
|
415
|
+
export async function initializeTaproot(db, options = {}) {
|
|
416
|
+
const { applyTaprootMigrations } = await import('./migrations.js');
|
|
417
|
+
await applyTaprootMigrations(db, options);
|
|
418
|
+
}
|
|
419
|
+
export async function isRecognizedLegacyV1(db, tables) {
|
|
420
|
+
const expectedTables = [
|
|
421
|
+
'taproot_assertions',
|
|
422
|
+
'taproot_entities',
|
|
423
|
+
'taproot_entity_revisions',
|
|
424
|
+
'taproot_id_counters',
|
|
425
|
+
'taproot_metadata',
|
|
426
|
+
'taproot_terms',
|
|
427
|
+
];
|
|
428
|
+
if (JSON.stringify(tables.map(({ name }) => name)) !==
|
|
429
|
+
JSON.stringify(expectedTables))
|
|
430
|
+
return false;
|
|
431
|
+
if (!(await matchesExactCatalog(db, legacyTaprootV1Statements)))
|
|
432
|
+
return false;
|
|
433
|
+
const metadata = await db
|
|
434
|
+
.prepare(`SELECT metadata_key, metadata_value FROM taproot_metadata
|
|
435
|
+
WHERE metadata_key IN ('schema_version', 'canonical_json_version', 'rdf_mapping_version')
|
|
436
|
+
ORDER BY metadata_key`)
|
|
437
|
+
.all();
|
|
438
|
+
return (JSON.stringify(metadata.results) ===
|
|
439
|
+
JSON.stringify([
|
|
440
|
+
{ metadata_key: 'canonical_json_version', metadata_value: '1' },
|
|
441
|
+
{ metadata_key: 'rdf_mapping_version', metadata_value: '1' },
|
|
442
|
+
{ metadata_key: 'schema_version', metadata_value: '1' },
|
|
443
|
+
]));
|
|
151
444
|
}
|
|
152
445
|
async function readMetadata(db, key) {
|
|
153
446
|
const table = await db
|
|
@@ -161,7 +454,7 @@ async function readMetadata(db, key) {
|
|
|
161
454
|
.all();
|
|
162
455
|
return result.results[0]?.metadata_value;
|
|
163
456
|
}
|
|
164
|
-
async function backfillRdfOwnership(db, previousVersion) {
|
|
457
|
+
export async function backfillRdfOwnership(db, previousVersion, previousBaseIri) {
|
|
165
458
|
const rows = await db
|
|
166
459
|
.prepare(`SELECT entity_id, entity_json, deleted_at, redirect_to FROM taproot_entities ORDER BY entity_id`)
|
|
167
460
|
.all();
|
|
@@ -169,20 +462,14 @@ async function backfillRdfOwnership(db, previousVersion) {
|
|
|
169
462
|
return;
|
|
170
463
|
const baseIri = await readMetadata(db, 'base_iri');
|
|
171
464
|
if (!baseIri)
|
|
172
|
-
|
|
465
|
+
throw new SchemaMismatchError('Taproot RDF ownership cannot be rebuilt without a database identity');
|
|
173
466
|
const factory = new DataFactory();
|
|
174
467
|
for (const row of rows.results) {
|
|
175
468
|
const entity = parseEntityJson(row.entity_json);
|
|
176
469
|
const current = lifecycleQuads(entity, row.deleted_at, row.redirect_to, baseIri, TAPROOT_RDF_VERSION, factory);
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
.all();
|
|
181
|
-
if (previousVersion === TAPROOT_RDF_VERSION &&
|
|
182
|
-
Number(existingOwnership.results[0]?.count ?? 0) > 0)
|
|
183
|
-
continue;
|
|
184
|
-
const old = previousVersion && previousVersion !== TAPROOT_RDF_VERSION
|
|
185
|
-
? lifecycleQuads(entity, row.deleted_at, row.redirect_to, baseIri, previousVersion, factory)
|
|
470
|
+
const old = previousVersion &&
|
|
471
|
+
(previousVersion !== TAPROOT_RDF_VERSION || previousBaseIri !== baseIri)
|
|
472
|
+
? lifecycleQuads(entity, row.deleted_at, row.redirect_to, previousBaseIri ?? baseIri, previousVersion, factory)
|
|
186
473
|
: [];
|
|
187
474
|
const patch = prepareQuadPatch(db, { delete: old, insert: current });
|
|
188
475
|
const ownershipRows = current.map((quad) => ({
|
|
@@ -207,7 +494,10 @@ async function backfillRdfOwnership(db, previousVersion) {
|
|
|
207
494
|
function lifecycleQuads(entity, deletedAt, redirectTo, baseIri, mappingVersion, factory) {
|
|
208
495
|
if (!deletedAt && !redirectTo)
|
|
209
496
|
return buildEntityQuads(entity, { baseIri, mappingVersion, factory });
|
|
210
|
-
|
|
497
|
+
let end = baseIri.length;
|
|
498
|
+
while (end > 0 && baseIri.charCodeAt(end - 1) === 47)
|
|
499
|
+
end -= 1;
|
|
500
|
+
const base = baseIri.slice(0, end);
|
|
211
501
|
const subject = factory.namedNode(`${base}/entity/${entity.id}`);
|
|
212
502
|
const quads = [
|
|
213
503
|
factory.quad(subject, factory.namedNode(`${base}/vocab/revision`), factory.literal(String(entity.lastrevid), factory.namedNode('http://www.w3.org/2001/XMLSchema#integer'))),
|
|
@@ -218,35 +508,7 @@ function lifecycleQuads(entity, deletedAt, redirectTo, baseIri, mappingVersion,
|
|
|
218
508
|
quads.push(factory.quad(subject, factory.namedNode('http://www.w3.org/2002/07/owl#sameAs'), factory.namedNode(`${base}/entity/${redirectTo}`)));
|
|
219
509
|
return quads;
|
|
220
510
|
}
|
|
221
|
-
async function
|
|
222
|
-
const tables = await db
|
|
223
|
-
.prepare(`SELECT name FROM sqlite_schema WHERE type = 'table' AND name = 'taproot_entity_revisions'`)
|
|
224
|
-
.all();
|
|
225
|
-
if (!tables.results.length)
|
|
226
|
-
return;
|
|
227
|
-
const columns = await db
|
|
228
|
-
.prepare(`PRAGMA table_info(taproot_entity_revisions)`)
|
|
229
|
-
.all();
|
|
230
|
-
const names = new Set(columns.results.map(({ name }) => name));
|
|
231
|
-
const additions = [
|
|
232
|
-
[
|
|
233
|
-
'attribution_json',
|
|
234
|
-
`TEXT CHECK (attribution_json IS NULL OR json_valid(attribution_json))`,
|
|
235
|
-
],
|
|
236
|
-
['tags_json', `TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(tags_json))`],
|
|
237
|
-
['event_id', `TEXT`],
|
|
238
|
-
['content_hash', `TEXT`],
|
|
239
|
-
['parent_hash', `TEXT`],
|
|
240
|
-
['deleted_at', `TEXT`],
|
|
241
|
-
['redirect_to', `TEXT`],
|
|
242
|
-
];
|
|
243
|
-
for (const [name, definition] of additions) {
|
|
244
|
-
if (!names.has(name)) {
|
|
245
|
-
await db
|
|
246
|
-
.prepare(`ALTER TABLE taproot_entity_revisions ADD COLUMN ${name} ${definition}`)
|
|
247
|
-
.run();
|
|
248
|
-
}
|
|
249
|
-
}
|
|
511
|
+
export async function backfillLegacyRevisions(db) {
|
|
250
512
|
const revisions = await db
|
|
251
513
|
.prepare(`SELECT r.entity_id, r.revision, r.entity_json, r.actor, r.event_id, r.content_hash,
|
|
252
514
|
CASE WHEN r.revision = e.revision THEN e.deleted_at ELSE r.deleted_at END AS deleted_at,
|
|
@@ -259,25 +521,195 @@ async function upgradeLegacySchema(db) {
|
|
|
259
521
|
for (const revision of revisions.results) {
|
|
260
522
|
if (revision.entity_id !== previousEntity)
|
|
261
523
|
parentHash = null;
|
|
262
|
-
const contentHash = revision.
|
|
263
|
-
|
|
524
|
+
const contentHash = await hash(`${revision.entity_json}\n${JSON.stringify({ deletedAt: revision.deleted_at, redirectTo: revision.redirect_to })}`);
|
|
525
|
+
const eventId = `legacy-${revision.entity_id}-${revision.revision}`;
|
|
526
|
+
if ((revision.event_id !== null && revision.event_id !== eventId) ||
|
|
527
|
+
(revision.content_hash !== null && revision.content_hash !== contentHash)) {
|
|
528
|
+
throw new SchemaMismatchError(`Legacy revision ${revision.entity_id}@${revision.revision} has conflicting durable identity`);
|
|
529
|
+
}
|
|
264
530
|
if (!revision.event_id || !revision.content_hash) {
|
|
265
531
|
const attribution = revision.actor
|
|
266
532
|
? JSON.stringify({ id: revision.actor, kind: 'human' })
|
|
267
533
|
: null;
|
|
268
|
-
await db
|
|
269
|
-
|
|
534
|
+
await db.batch([
|
|
535
|
+
db
|
|
536
|
+
.prepare(`UPDATE taproot_entity_revisions SET event_id = ?, content_hash = ?,
|
|
270
537
|
parent_hash = ?, attribution_json = COALESCE(attribution_json, ?),
|
|
271
538
|
tags_json = COALESCE(tags_json, '[]'), deleted_at = ?, redirect_to = ?
|
|
272
539
|
WHERE entity_id = ? AND revision = ?`)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
540
|
+
.bind(eventId, contentHash, parentHash, attribution, revision.deleted_at, revision.redirect_to, revision.entity_id, revision.revision),
|
|
541
|
+
]);
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
const storedParent = await db
|
|
545
|
+
.prepare(`SELECT parent_hash FROM taproot_entity_revisions
|
|
546
|
+
WHERE entity_id = ? AND revision = ?`)
|
|
547
|
+
.bind(revision.entity_id, revision.revision)
|
|
548
|
+
.all();
|
|
549
|
+
if (storedParent.results[0]?.parent_hash !== parentHash)
|
|
550
|
+
throw new SchemaMismatchError(`Legacy revision ${revision.entity_id}@${revision.revision} has a conflicting parent hash`);
|
|
276
551
|
}
|
|
277
552
|
previousEntity = revision.entity_id;
|
|
278
553
|
parentHash = contentHash;
|
|
279
554
|
}
|
|
280
555
|
}
|
|
556
|
+
export async function backfillTaprootAudit(db) {
|
|
557
|
+
await db.batch([
|
|
558
|
+
db.prepare(`INSERT INTO taproot_audit_events(
|
|
559
|
+
event_id, entity_id, revision, event_type, attribution_json,
|
|
560
|
+
edit_summary, tags_json, content_hash, parent_hash, details_json, created_at
|
|
561
|
+
)
|
|
562
|
+
SELECT event_id, entity_id, revision, 'import',
|
|
563
|
+
attribution_json, edit_summary, tags_json, content_hash, parent_hash,
|
|
564
|
+
json_object('source', 'legacy-v1'), created_at
|
|
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
|
+
)`),
|
|
570
|
+
]);
|
|
571
|
+
}
|
|
572
|
+
export async function verifyTaprootSemanticState(db, baseIri) {
|
|
573
|
+
const incomplete = await db
|
|
574
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
575
|
+
FROM taproot_entity_revisions r
|
|
576
|
+
LEFT JOIN taproot_audit_events a ON a.event_id = r.event_id
|
|
577
|
+
WHERE r.event_id IS NULL OR r.content_hash IS NULL OR a.event_id IS NULL
|
|
578
|
+
OR a.entity_id IS NOT r.entity_id OR a.revision IS NOT r.revision
|
|
579
|
+
OR a.content_hash IS NOT r.content_hash
|
|
580
|
+
OR a.parent_hash IS NOT r.parent_hash
|
|
581
|
+
OR a.attribution_json IS NOT r.attribution_json
|
|
582
|
+
OR a.edit_summary IS NOT r.edit_summary
|
|
583
|
+
OR a.tags_json IS NOT r.tags_json
|
|
584
|
+
OR a.created_at IS NOT r.created_at`)
|
|
585
|
+
.all();
|
|
586
|
+
if (Number(incomplete.results[0]?.count ?? 0) !== 0)
|
|
587
|
+
throw new SchemaMismatchError('Taproot revision identity or audit backfill is incomplete');
|
|
588
|
+
const dangling = await db
|
|
589
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
590
|
+
FROM taproot_rdf_ownership o
|
|
591
|
+
LEFT JOIN taproot_entities e ON e.entity_id = o.entity_id
|
|
592
|
+
WHERE e.entity_id IS NULL`)
|
|
593
|
+
.all();
|
|
594
|
+
if (Number(dangling.results[0]?.count ?? 0) !== 0)
|
|
595
|
+
throw new SchemaMismatchError('Taproot RDF ownership contains dangling rows');
|
|
596
|
+
const missingProjection = await db
|
|
597
|
+
.prepare(`SELECT COUNT(*) AS count
|
|
598
|
+
FROM taproot_rdf_ownership o
|
|
599
|
+
LEFT JOIN rdf_quads q
|
|
600
|
+
ON q.subject_key = o.subject_key
|
|
601
|
+
AND q.predicate_key = o.predicate_key
|
|
602
|
+
AND q.object_key = o.object_key
|
|
603
|
+
AND q.graph_key = o.graph_key
|
|
604
|
+
WHERE q.id IS NULL`)
|
|
605
|
+
.all();
|
|
606
|
+
if (Number(missingProjection.results[0]?.count ?? 0) !== 0)
|
|
607
|
+
throw new SchemaMismatchError('Taproot RDF ownership is missing its Diamond quad projection');
|
|
608
|
+
const rows = await db
|
|
609
|
+
.prepare(`SELECT entity_id, entity_json, deleted_at, redirect_to
|
|
610
|
+
FROM taproot_entities ORDER BY entity_id`)
|
|
611
|
+
.all();
|
|
612
|
+
const factory = new DataFactory();
|
|
613
|
+
for (const row of rows.results) {
|
|
614
|
+
const expected = lifecycleQuads(parseEntityJson(row.entity_json), row.deleted_at, row.redirect_to, baseIri, TAPROOT_RDF_VERSION, factory)
|
|
615
|
+
.map((quad) => [
|
|
616
|
+
encodeTerm(quad.subject).key,
|
|
617
|
+
encodeTerm(quad.predicate).key,
|
|
618
|
+
encodeTerm(quad.object).key,
|
|
619
|
+
encodeTerm(quad.graph).key,
|
|
620
|
+
].join('\u0000'))
|
|
621
|
+
.sort();
|
|
622
|
+
const actual = await db
|
|
623
|
+
.prepare(`SELECT subject_key, predicate_key, object_key, graph_key
|
|
624
|
+
FROM taproot_rdf_ownership WHERE entity_id = ?
|
|
625
|
+
ORDER BY subject_key, predicate_key, object_key, graph_key`)
|
|
626
|
+
.bind(row.entity_id)
|
|
627
|
+
.all();
|
|
628
|
+
const keys = actual.results
|
|
629
|
+
.map(({ subject_key, predicate_key, object_key, graph_key }) => [subject_key, predicate_key, object_key, graph_key].join('\u0000'))
|
|
630
|
+
.sort();
|
|
631
|
+
if (JSON.stringify(keys) !== JSON.stringify(expected))
|
|
632
|
+
throw new SchemaMismatchError(`Taproot RDF ownership backfill is incomplete for ${row.entity_id}`);
|
|
633
|
+
}
|
|
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
|
+
}
|
|
698
|
+
export async function verifyTaprootPackageSeeds(db) {
|
|
699
|
+
const migrationSeeds = await db
|
|
700
|
+
.prepare(`SELECT COUNT(*) AS count FROM taproot_migrations
|
|
701
|
+
WHERE (version = 1 AND name = 'initial')
|
|
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')`)
|
|
705
|
+
.all();
|
|
706
|
+
const migrationSeedTotal = await db
|
|
707
|
+
.prepare(`SELECT COUNT(*) AS count FROM taproot_migrations`)
|
|
708
|
+
.all();
|
|
709
|
+
if (Number(migrationSeeds.results[0]?.count ?? 0) !== 4 ||
|
|
710
|
+
Number(migrationSeedTotal.results[0]?.count ?? 0) !== 4)
|
|
711
|
+
throw new SchemaMismatchError('Taproot package migration seeds are incomplete');
|
|
712
|
+
}
|
|
281
713
|
async function hash(value) {
|
|
282
714
|
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value));
|
|
283
715
|
return [...new Uint8Array(digest)]
|
|
@@ -295,6 +727,15 @@ export async function inspectTaprootSchema(db) {
|
|
|
295
727
|
'taproot_audit_events',
|
|
296
728
|
'taproot_migrations',
|
|
297
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',
|
|
298
739
|
];
|
|
299
740
|
const tables = await db
|
|
300
741
|
.prepare(`SELECT name, sql FROM sqlite_schema
|
|
@@ -337,6 +778,120 @@ export async function inspectTaprootSchema(db) {
|
|
|
337
778
|
: { results: [] };
|
|
338
779
|
const presentColumns = new Set(revisionColumns.results.map(({ name }) => name));
|
|
339
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
|
+
}
|
|
340
895
|
const requiredIndexes = [
|
|
341
896
|
'taproot_entities_type_idx',
|
|
342
897
|
'taproot_entities_modified_idx',
|
|
@@ -345,6 +900,10 @@ export async function inspectTaprootSchema(db) {
|
|
|
345
900
|
'taproot_audit_entity_idx',
|
|
346
901
|
'taproot_audit_request_idx',
|
|
347
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',
|
|
348
907
|
];
|
|
349
908
|
const indexRows = await db
|
|
350
909
|
.prepare(`SELECT name FROM sqlite_schema WHERE type = 'index' AND name LIKE 'taproot_%'`)
|
|
@@ -356,12 +915,39 @@ export async function inspectTaprootSchema(db) {
|
|
|
356
915
|
'taproot_revisions_no_delete',
|
|
357
916
|
'taproot_audit_no_update',
|
|
358
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',
|
|
359
935
|
];
|
|
360
936
|
const triggerRows = await db
|
|
361
|
-
.prepare(`SELECT name FROM sqlite_schema
|
|
937
|
+
.prepare(`SELECT name, sql FROM sqlite_schema
|
|
938
|
+
WHERE type = 'trigger' AND name LIKE 'taproot_%'`)
|
|
362
939
|
.all();
|
|
363
940
|
const presentTriggers = new Set(triggerRows.results.map(({ name }) => name));
|
|
364
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
|
+
}
|
|
365
951
|
errors.push(...missingColumns.map((name) => `taproot_entity_revisions.${name} is missing`));
|
|
366
952
|
errors.push(...missingIndexes.map((name) => `${name} is missing`));
|
|
367
953
|
errors.push(...missingTriggers.map((name) => `${name} is missing`));
|
|
@@ -381,6 +967,59 @@ export async function inspectTaprootSchema(db) {
|
|
|
381
967
|
errors,
|
|
382
968
|
};
|
|
383
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Verify the exact package-owned catalog before adopting a current pre-ledger
|
|
972
|
+
* database. This is intentionally stricter than the operational inspection:
|
|
973
|
+
* names alone must never authorize stamping an arbitrary look-alike schema.
|
|
974
|
+
*/
|
|
975
|
+
export async function isExactTaprootSchema(db) {
|
|
976
|
+
return matchesExactCatalog(db, taprootSchemaStatements);
|
|
977
|
+
}
|
|
978
|
+
/** Exact package catalog immediately before canonical authorization migration 0004. */
|
|
979
|
+
export async function isExactPreAuthorizationTaprootSchema(db) {
|
|
980
|
+
return matchesExactCatalog(db, preAuthorizationTaprootSchemaStatements);
|
|
981
|
+
}
|
|
982
|
+
export async function isExactTaprootUpgradeSchema(db) {
|
|
983
|
+
return matchesExactCatalog(db, taprootUpgradeCatalogStatements);
|
|
984
|
+
}
|
|
985
|
+
export async function isExactTaprootPreFinalizeSchema(db) {
|
|
986
|
+
return matchesExactCatalog(db, taprootPreFinalizeCatalogStatements);
|
|
987
|
+
}
|
|
988
|
+
function isRecord(value) {
|
|
989
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
990
|
+
}
|
|
991
|
+
async function matchesExactCatalog(db, statements) {
|
|
992
|
+
const expected = new Map();
|
|
993
|
+
for (const sql of statements) {
|
|
994
|
+
const match = /^\s*CREATE\s+(TABLE|INDEX|TRIGGER)\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-z0-9_]+)/iu.exec(sql);
|
|
995
|
+
if (!match?.[1] || !match[2])
|
|
996
|
+
continue;
|
|
997
|
+
expected.set(`${match[1].toLowerCase()}:${match[2]}`, normalizeCatalogSql(sql));
|
|
998
|
+
}
|
|
999
|
+
const catalog = await db
|
|
1000
|
+
.prepare(`SELECT type, name, sql FROM sqlite_schema
|
|
1001
|
+
WHERE name LIKE 'taproot_%' AND type IN ('table', 'index', 'trigger')
|
|
1002
|
+
ORDER BY type, name`)
|
|
1003
|
+
.all();
|
|
1004
|
+
if (catalog.results.length !== expected.size)
|
|
1005
|
+
return false;
|
|
1006
|
+
for (const entry of catalog.results) {
|
|
1007
|
+
const expectedSql = expected.get(`${entry.type}:${entry.name}`);
|
|
1008
|
+
if (expectedSql === undefined ||
|
|
1009
|
+
entry.sql === null ||
|
|
1010
|
+
normalizeCatalogSql(entry.sql) !== expectedSql)
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
function normalizeCatalogSql(sql) {
|
|
1016
|
+
return sql
|
|
1017
|
+
.replace(/\bIF\s+NOT\s+EXISTS\s+/giu, '')
|
|
1018
|
+
.replace(/\s+/gu, ' ')
|
|
1019
|
+
.replace(/;\s*$/u, '')
|
|
1020
|
+
.trim()
|
|
1021
|
+
.toLowerCase();
|
|
1022
|
+
}
|
|
384
1023
|
export async function assertTaprootSchema(db) {
|
|
385
1024
|
const inspection = await inspectTaprootSchema(db);
|
|
386
1025
|
if (!inspection.valid) {
|