@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.
- package/CHANGELOG.md +53 -0
- package/COMPATIBILITY.md +5 -3
- package/README.md +106 -33
- package/SECURITY.md +6 -3
- 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 +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +125 -49
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +351 -44
- package/dist/index.js.map +1 -1
- package/dist/migrations.d.ts +9 -3
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +77 -5
- package/dist/migrations.js.map +1 -1
- package/dist/repository.d.ts +20 -9
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +593 -48
- package/dist/repository.js.map +1 -1
- package/dist/schema.d.ts +14 -5
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +461 -18
- 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 +56 -18
- package/docs/architecture.md +19 -7
- package/docs/authorization.md +82 -0
- package/docs/completion-audit.md +15 -15
- package/docs/operations.md +28 -5
- package/docs/product-scope.md +5 -3
- package/docs/testing.md +18 -1
- package/docs/threat-model.md +30 -3
- package/examples/d1-diamond-interop/README.md +2 -1
- package/examples/d1-diamond-interop/demo.ts +150 -21
- package/migrations/0003_canonical_statement_text.sql +31 -0
- package/migrations/0004_canonical_authorization_policy.sql +213 -0
- package/package.json +4 -1
package/docs/testing.md
CHANGED
|
@@ -20,7 +20,9 @@ same-adapter enforcement, late-batch rollback, and concurrent writers.
|
|
|
20
20
|
`consumer:check` packs the
|
|
21
21
|
package, installs the tarball into a fresh temporary project with registry
|
|
22
22
|
Diamond and Miniflare dependencies, initializes a new D1 database, writes an
|
|
23
|
-
entity, and verifies the RDF through SPARQL using only
|
|
23
|
+
entity, and verifies the RDF through privileged package-test SPARQL using only
|
|
24
|
+
public package exports. This proves RDF interoperability, not authorization of
|
|
25
|
+
a user-facing SPARQL endpoint.
|
|
24
26
|
|
|
25
27
|
`npm run check` is the complete local package release-quality gate. Coverage
|
|
26
28
|
thresholds are enforced in `vitest.config.ts`; lowering them requires an
|
|
@@ -28,3 +30,18 @@ explained review. Miniflare/Workerd and packed-consumer success verifies the
|
|
|
28
30
|
package's supported D1 runtime contract, not assembly, provisioning, deployment,
|
|
29
31
|
or acceptance of a complete Gnolith Site. The Codex agent creating a Site owns
|
|
30
32
|
those checks.
|
|
33
|
+
|
|
34
|
+
## Miniflare dependency security
|
|
35
|
+
|
|
36
|
+
Miniflare is a development-only dependency used to qualify Taproot against its
|
|
37
|
+
supported Workerd D1 runtime. Taproot applies an exact root override of
|
|
38
|
+
`sharp@0.35.3` because Miniflare currently declares `sharp@0.34.5`, which is
|
|
39
|
+
affected by GHSA-f88m-g3jw-g9cj. This is a temporary upstream-compatibility
|
|
40
|
+
exception tracked in [Cloudflare workers-sdk PR #14493](https://github.com/cloudflare/workers-sdk/pull/14493).
|
|
41
|
+
Remove the override once a Miniflare release in Taproot's supported range
|
|
42
|
+
declares a non-vulnerable Sharp version and the full D1, interop,
|
|
43
|
+
packed-consumer, Node 22/24, and audit gates pass without it.
|
|
44
|
+
|
|
45
|
+
The override is qualified for Taproot's Miniflare D1 test use. It is not a claim
|
|
46
|
+
of general Cloudflare Images compatibility, nor a compatibility claim for
|
|
47
|
+
macOS, ARM, or musl environments.
|
package/docs/threat-model.md
CHANGED
|
@@ -8,8 +8,18 @@ association between an edit and its claimed attribution metadata.
|
|
|
8
8
|
|
|
9
9
|
## Trust boundaries
|
|
10
10
|
|
|
11
|
-
Taproot assumes its consumer supplies a D1 binding and absolute base IRI
|
|
12
|
-
|
|
11
|
+
Taproot assumes its consumer supplies a SQLite/D1 binding and absolute base IRI.
|
|
12
|
+
Hosts create `AuthorizationContext` values only from authenticated state;
|
|
13
|
+
Taproot loads persisted canonical policies and checks them before and after
|
|
14
|
+
hydration. The normal package export has no raw canonical repository. Every
|
|
15
|
+
public mutation requires a runtime-branded installation guard, a current
|
|
16
|
+
context with exact `knowledge:write`, current target visibility, and canonical
|
|
17
|
+
policy. Policy changes require exact `knowledge:policy`. Invalid guards or
|
|
18
|
+
contexts fail before database work or observers. Mutation receipts do not
|
|
19
|
+
disclose canonical bodies. Host assembly withholds the database binding,
|
|
20
|
+
host capability, guard, and keys from request, user, agent, and MCP code. The
|
|
21
|
+
host capability is bootstrap/maintenance authority, while ordinary mutations
|
|
22
|
+
remain per-entity policy authorized.
|
|
13
23
|
Attribution is a stored claim, not proof of identity. Diamond executes
|
|
14
24
|
the RDF patch prepared by Taproot and exposes read-only SPARQL querying; hosts
|
|
15
25
|
must not give untrusted callers an independent write path to Taproot or Diamond
|
|
@@ -19,6 +29,8 @@ tables.
|
|
|
19
29
|
|
|
20
30
|
- Lost updates are blocked by expected-revision guards inside the same D1 batch
|
|
21
31
|
as all canonical and projected writes.
|
|
32
|
+
- Authorization ABA races are blocked by a shared counter CAS plus a unique
|
|
33
|
+
durable advance ID asserted inside the same ordered batch.
|
|
22
34
|
- Partial writes are blocked by D1 batch atomicity; injected Workerd failures
|
|
23
35
|
verify rollback across Taproot and Diamond.
|
|
24
36
|
- History tampering is deterred and detected by immutable-table triggers,
|
|
@@ -28,13 +40,21 @@ tables.
|
|
|
28
40
|
outside the package boundary.
|
|
29
41
|
- RDF projection confusion is constrained by strict entity validation,
|
|
30
42
|
deterministic mapping, site-owned namespaces, and per-entity quad ownership.
|
|
43
|
+
- Application/search canonical reads fail closed on absent policy,
|
|
44
|
+
cross-installation identity, stale authorization revision, policy changes
|
|
45
|
+
during hydration, and malformed CNF visibility. Denials have a generic body.
|
|
46
|
+
- Search administration requires the exact `search:admin` capability; personas
|
|
47
|
+
and generic administrator labels do not imply it.
|
|
48
|
+
- Page cursors require a non-extractable host AES-GCM key, use fixed-size
|
|
49
|
+
plaintext padding, and bind caller, grants, operation, query/filter, auth
|
|
50
|
+
revision, and search generation.
|
|
31
51
|
- Dependency and release compromise is reduced through locked installs, pinned
|
|
32
52
|
GitHub Actions, dependency review, CodeQL, secret scanning, license checks,
|
|
33
53
|
OIDC npm publishing, provenance, SBOMs, checksums, and attestations.
|
|
34
54
|
|
|
35
55
|
## Out of scope
|
|
36
56
|
|
|
37
|
-
Taproot does not provide authentication,
|
|
57
|
+
Taproot does not provide authentication, principal/workspace persistence, agent/MCP isolation,
|
|
38
58
|
network rate limiting, D1 backup custody, UI sanitization, or arbitrary SPARQL
|
|
39
59
|
Update. A compromised host with direct D1 access can bypass library controls;
|
|
40
60
|
restore from a trusted backup and verify audit chains after such an incident.
|
|
@@ -43,3 +63,10 @@ Taproot's local Workerd tests validate its package controls under the supported
|
|
|
43
63
|
D1 contract. They do not qualify a deployed host. The Codex agent creating a
|
|
44
64
|
Site owns assembly, provisioning, route and security wiring, deployment, backup
|
|
45
65
|
policy, and production acceptance.
|
|
66
|
+
|
|
67
|
+
Diamond's database-level SPARQL handler sees Taproot's complete RDF projection
|
|
68
|
+
and is therefore privileged host maintenance/debug infrastructure, not a
|
|
69
|
+
normal user, agent, MCP, or search endpoint. Workshop must construct an
|
|
70
|
+
authorization-scoped dataset (with a final canonical recheck) before exposing
|
|
71
|
+
SPARQL results. Possession of the SQLite/D1 binding itself remains privileged:
|
|
72
|
+
a compromised host can query tables outside any package API.
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Pass a D1-compatible database to `runTaprootInteropDemo(db)`. The demo
|
|
4
4
|
initializes Taproot and Diamond in the same database, creates Properties and an
|
|
5
5
|
Item, applies revision-guarded edits, and queries Taproot's RDF projection
|
|
6
|
-
through Diamond SPARQL.
|
|
6
|
+
through Diamond SPARQL. The SPARQL call is a package interoperability probe
|
|
7
|
+
over a privileged database binding, not an authorization-safe endpoint.
|
|
7
8
|
|
|
8
9
|
The automated test runs this example locally with Miniflare's Workerd D1
|
|
9
10
|
implementation. It verifies the published package's D1 and Diamond contracts;
|
|
@@ -1,33 +1,66 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RevisionConflictError,
|
|
3
|
-
|
|
3
|
+
SEARCH_ADMIN_CAPABILITY,
|
|
4
|
+
KNOWLEDGE_WRITE_CAPABILITY,
|
|
5
|
+
KNOWLEDGE_POLICY_CAPABILITY,
|
|
6
|
+
addStatement,
|
|
7
|
+
bootstrapTaprootAuthorization,
|
|
8
|
+
createAuthorizationCursorCodec,
|
|
9
|
+
createAuthorizedTaproot,
|
|
10
|
+
createItem,
|
|
11
|
+
createProperty,
|
|
12
|
+
createTaprootHostWriteCapability,
|
|
13
|
+
createInstallationAuthorizationGuard,
|
|
4
14
|
exportEntityJson,
|
|
5
15
|
initializeTaproot,
|
|
16
|
+
setLabel,
|
|
17
|
+
setStatementRank,
|
|
18
|
+
type CanonicalAuthorizationPolicyInput,
|
|
6
19
|
type D1DatabaseLike,
|
|
7
20
|
type Statement,
|
|
21
|
+
type AuthorizationContext,
|
|
8
22
|
} from '@gnolith/taproot';
|
|
9
23
|
import { createSparqlHandler } from '@gnolith/diamond';
|
|
10
24
|
|
|
11
25
|
export async function runTaprootInteropDemo(db: D1DatabaseLike) {
|
|
12
26
|
await initializeTaproot(db, { baseIri: 'https://knowledge.example' });
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
27
|
+
const options = { baseIri: 'https://knowledge.example' };
|
|
28
|
+
const writeCapability = createTaprootHostWriteCapability(
|
|
29
|
+
db,
|
|
30
|
+
options,
|
|
31
|
+
await crypto.subtle.generateKey({ name: 'HMAC', hash: 'SHA-256' }, false, [
|
|
32
|
+
'sign',
|
|
33
|
+
]),
|
|
34
|
+
);
|
|
35
|
+
await bootstrapTaprootAuthorization(
|
|
36
|
+
db,
|
|
37
|
+
options,
|
|
38
|
+
writeCapability,
|
|
39
|
+
'demo-installation',
|
|
40
|
+
);
|
|
41
|
+
const authorizationGuard = await createInstallationAuthorizationGuard(
|
|
42
|
+
db,
|
|
43
|
+
options,
|
|
44
|
+
writeCapability,
|
|
45
|
+
);
|
|
16
46
|
|
|
17
|
-
await
|
|
47
|
+
await createProperty(db, options, authorizationGuard, writer(1), {
|
|
18
48
|
datatype: 'string',
|
|
19
49
|
labels: { en: { language: 'en', value: 'occupation' } },
|
|
50
|
+
authorization: policy(1),
|
|
20
51
|
});
|
|
21
|
-
await
|
|
52
|
+
await createProperty(db, options, authorizationGuard, writer(2), {
|
|
22
53
|
datatype: 'time',
|
|
23
54
|
labels: { en: { language: 'en', value: 'point in time' } },
|
|
55
|
+
authorization: policy(2),
|
|
24
56
|
});
|
|
25
|
-
await
|
|
57
|
+
await createProperty(db, options, authorizationGuard, writer(3), {
|
|
26
58
|
datatype: 'url',
|
|
27
59
|
labels: { en: { language: 'en', value: 'reference URL' } },
|
|
60
|
+
authorization: policy(3),
|
|
28
61
|
});
|
|
29
62
|
|
|
30
|
-
const created = await
|
|
63
|
+
const created = await createItem(db, options, authorizationGuard, writer(4), {
|
|
31
64
|
labels: { en: { language: 'en', value: 'Ada Lovelace' } },
|
|
32
65
|
descriptions: {
|
|
33
66
|
en: { language: 'en', value: 'English mathematician' },
|
|
@@ -38,10 +71,12 @@ export async function runTaprootInteropDemo(db: D1DatabaseLike) {
|
|
|
38
71
|
tool: 'gnolith-mcp',
|
|
39
72
|
},
|
|
40
73
|
requestId: 'demo-request',
|
|
74
|
+
authorization: policy(4),
|
|
41
75
|
});
|
|
42
76
|
const occupation: Statement = {
|
|
43
77
|
id: `${created.entityId}$occupation`,
|
|
44
78
|
type: 'statement',
|
|
79
|
+
text: 'Ada Lovelace worked as a computer programmer in 1843.',
|
|
45
80
|
rank: 'normal',
|
|
46
81
|
mainsnak: {
|
|
47
82
|
snaktype: 'value',
|
|
@@ -90,18 +125,36 @@ export async function runTaprootInteropDemo(db: D1DatabaseLike) {
|
|
|
90
125
|
},
|
|
91
126
|
],
|
|
92
127
|
};
|
|
93
|
-
const added = await
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
const added = await addStatement(
|
|
129
|
+
db,
|
|
130
|
+
options,
|
|
131
|
+
authorizationGuard,
|
|
132
|
+
writer(5, true),
|
|
133
|
+
created.entityId,
|
|
134
|
+
occupation,
|
|
135
|
+
{
|
|
136
|
+
expectedRevision: created.newRevision,
|
|
137
|
+
authorization: policy(5, { [occupation.id]: [] }),
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
const preferred = await setStatementRank(
|
|
141
|
+
db,
|
|
142
|
+
options,
|
|
143
|
+
authorizationGuard,
|
|
144
|
+
writer(6),
|
|
97
145
|
created.entityId,
|
|
98
146
|
occupation.id,
|
|
99
147
|
'preferred',
|
|
100
|
-
|
|
148
|
+
'Ada Lovelace worked as a computer programmer in 1843 (preferred).',
|
|
149
|
+
{
|
|
150
|
+
expectedRevision: added.newRevision,
|
|
151
|
+
authorization: policy(6, { [occupation.id]: [] }),
|
|
152
|
+
},
|
|
101
153
|
);
|
|
102
154
|
const unknownOccupation: Statement = {
|
|
103
155
|
id: `${created.entityId}$unknown-occupation`,
|
|
104
156
|
type: 'statement',
|
|
157
|
+
text: 'Ada Lovelace had another occupation whose value is unknown.',
|
|
105
158
|
rank: 'normal',
|
|
106
159
|
mainsnak: {
|
|
107
160
|
snaktype: 'somevalue',
|
|
@@ -112,10 +165,20 @@ export async function runTaprootInteropDemo(db: D1DatabaseLike) {
|
|
|
112
165
|
'qualifiers-order': [],
|
|
113
166
|
references: [],
|
|
114
167
|
};
|
|
115
|
-
|
|
168
|
+
await addStatement(
|
|
169
|
+
db,
|
|
170
|
+
options,
|
|
171
|
+
authorizationGuard,
|
|
172
|
+
writer(7, true),
|
|
116
173
|
created.entityId,
|
|
117
174
|
unknownOccupation,
|
|
118
|
-
{
|
|
175
|
+
{
|
|
176
|
+
expectedRevision: preferred.newRevision,
|
|
177
|
+
authorization: policy(7, {
|
|
178
|
+
[occupation.id]: [],
|
|
179
|
+
[unknownOccupation.id]: [],
|
|
180
|
+
}),
|
|
181
|
+
},
|
|
119
182
|
);
|
|
120
183
|
|
|
121
184
|
const sparql = createSparqlHandler({ db });
|
|
@@ -132,21 +195,87 @@ export async function runTaprootInteropDemo(db: D1DatabaseLike) {
|
|
|
132
195
|
|
|
133
196
|
let staleRevisionRejected = false;
|
|
134
197
|
try {
|
|
135
|
-
await
|
|
136
|
-
|
|
137
|
-
|
|
198
|
+
await setLabel(
|
|
199
|
+
db,
|
|
200
|
+
options,
|
|
201
|
+
authorizationGuard,
|
|
202
|
+
writer(8),
|
|
203
|
+
created.entityId,
|
|
204
|
+
'en',
|
|
205
|
+
'stale edit',
|
|
206
|
+
{
|
|
207
|
+
expectedRevision: created.newRevision,
|
|
208
|
+
authorization: policy(8, {
|
|
209
|
+
[occupation.id]: [],
|
|
210
|
+
[unknownOccupation.id]: [],
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
);
|
|
138
214
|
} catch (cause) {
|
|
139
215
|
staleRevisionRejected = cause instanceof RevisionConflictError;
|
|
140
216
|
}
|
|
141
217
|
|
|
142
218
|
const sparqlResults: unknown = await response.json();
|
|
143
|
-
const
|
|
144
|
-
|
|
219
|
+
const reader = createAuthorizedTaproot(
|
|
220
|
+
db,
|
|
221
|
+
options,
|
|
222
|
+
{
|
|
223
|
+
installationId: 'demo-installation',
|
|
224
|
+
principalId: 'demo-principal',
|
|
225
|
+
activeWorkspaceId: null,
|
|
226
|
+
workspaceIds: [],
|
|
227
|
+
capabilities: [SEARCH_ADMIN_CAPABILITY],
|
|
228
|
+
authorizationRevision: 8,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
cursorCodec: createAuthorizationCursorCodec(
|
|
232
|
+
await crypto.subtle.generateKey(
|
|
233
|
+
{ name: 'AES-GCM', length: 256 },
|
|
234
|
+
false,
|
|
235
|
+
['encrypt', 'decrypt'],
|
|
236
|
+
),
|
|
237
|
+
),
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
const audit = await reader.listAuditEvents({ entityId: created.entityId });
|
|
241
|
+
const integrity = await reader.inspectEntityIntegrity(created.entityId);
|
|
242
|
+
const canonical = await reader.getEntity(created.entityId);
|
|
145
243
|
return {
|
|
146
|
-
entityJson: exportEntityJson(
|
|
244
|
+
entityJson: exportEntityJson(canonical.entity),
|
|
147
245
|
sparqlResults,
|
|
148
246
|
staleRevisionRejected,
|
|
149
247
|
audit: audit.items,
|
|
150
248
|
integrity,
|
|
151
249
|
};
|
|
152
250
|
}
|
|
251
|
+
|
|
252
|
+
function policy(
|
|
253
|
+
expectedAuthorizationRevision: number,
|
|
254
|
+
statementRestrictions: CanonicalAuthorizationPolicyInput['statementRestrictions'] = {},
|
|
255
|
+
): CanonicalAuthorizationPolicyInput {
|
|
256
|
+
return {
|
|
257
|
+
installationId: 'demo-installation',
|
|
258
|
+
workspaceId: null,
|
|
259
|
+
ownerPrincipalId: 'demo-principal',
|
|
260
|
+
visibility: { version: 1, clauses: [] },
|
|
261
|
+
statementRestrictions,
|
|
262
|
+
expectedAuthorizationRevision,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function writer(
|
|
267
|
+
authorizationRevision: number,
|
|
268
|
+
policyAuthority = false,
|
|
269
|
+
): AuthorizationContext {
|
|
270
|
+
return {
|
|
271
|
+
installationId: 'demo-installation',
|
|
272
|
+
principalId: 'demo-principal',
|
|
273
|
+
activeWorkspaceId: null,
|
|
274
|
+
workspaceIds: [],
|
|
275
|
+
capabilities: [
|
|
276
|
+
KNOWLEDGE_WRITE_CAPABILITY,
|
|
277
|
+
...(policyAuthority ? [KNOWLEDGE_POLICY_CAPABILITY] : []),
|
|
278
|
+
],
|
|
279
|
+
authorizationRevision,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Documentation copy of canonical JSON migration 3.
|
|
2
|
+
-- Hosts must use applyTaprootMigrations() so the package migration ledger is
|
|
3
|
+
-- updated atomically. No fallback statement prose is generated.
|
|
4
|
+
INSERT INTO taproot_assertions(assertion_key)
|
|
5
|
+
WITH canonical_json(entity_json) AS (
|
|
6
|
+
SELECT entity_json FROM taproot_entities
|
|
7
|
+
UNION ALL
|
|
8
|
+
SELECT entity_json FROM taproot_entity_revisions
|
|
9
|
+
)
|
|
10
|
+
SELECT NULL
|
|
11
|
+
FROM canonical_json AS source,
|
|
12
|
+
json_each(source.entity_json, '$.claims') AS claim,
|
|
13
|
+
json_each(claim.value) AS statement
|
|
14
|
+
WHERE json_type(statement.value, '$.text') IS NOT 'text'
|
|
15
|
+
OR trim(json_extract(statement.value, '$.text')) = ''
|
|
16
|
+
LIMIT 1;
|
|
17
|
+
|
|
18
|
+
UPDATE taproot_metadata SET metadata_value = '2'
|
|
19
|
+
WHERE metadata_key = 'canonical_json_version'
|
|
20
|
+
AND metadata_value = '1';
|
|
21
|
+
|
|
22
|
+
INSERT INTO taproot_assertions(assertion_key)
|
|
23
|
+
SELECT NULL WHERE NOT EXISTS (
|
|
24
|
+
SELECT 1 FROM taproot_metadata
|
|
25
|
+
WHERE metadata_key = 'canonical_json_version'
|
|
26
|
+
AND metadata_value = '2'
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
INSERT INTO taproot_migrations(version, name)
|
|
30
|
+
VALUES (3, 'canonical-statement-text')
|
|
31
|
+
ON CONFLICT(version) DO NOTHING;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS taproot_installation_authorization (
|
|
2
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
3
|
+
installation_id TEXT NOT NULL UNIQUE,
|
|
4
|
+
authorization_revision INTEGER NOT NULL CHECK (authorization_revision >= 1),
|
|
5
|
+
search_generation INTEGER NOT NULL CHECK (search_generation >= 1),
|
|
6
|
+
last_advance_id TEXT NOT NULL,
|
|
7
|
+
created_at TEXT NOT NULL,
|
|
8
|
+
updated_at TEXT NOT NULL
|
|
9
|
+
) STRICT;
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS taproot_entity_authorization (
|
|
12
|
+
entity_id TEXT PRIMARY KEY,
|
|
13
|
+
installation_id TEXT NOT NULL,
|
|
14
|
+
workspace_id TEXT,
|
|
15
|
+
owner_principal_id TEXT NOT NULL,
|
|
16
|
+
visibility_json TEXT NOT NULL CHECK (json_valid(visibility_json)),
|
|
17
|
+
effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
|
|
18
|
+
source_revision INTEGER NOT NULL,
|
|
19
|
+
authorization_revision INTEGER NOT NULL,
|
|
20
|
+
deleted_at TEXT,
|
|
21
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
22
|
+
updated_at TEXT NOT NULL,
|
|
23
|
+
FOREIGN KEY (entity_id) REFERENCES taproot_entities(entity_id),
|
|
24
|
+
FOREIGN KEY (event_id) REFERENCES taproot_audit_events(event_id)
|
|
25
|
+
) STRICT;
|
|
26
|
+
|
|
27
|
+
CREATE TABLE IF NOT EXISTS taproot_entity_authorization_revisions (
|
|
28
|
+
entity_id TEXT NOT NULL,
|
|
29
|
+
source_revision INTEGER NOT NULL,
|
|
30
|
+
installation_id TEXT NOT NULL,
|
|
31
|
+
workspace_id TEXT,
|
|
32
|
+
owner_principal_id TEXT NOT NULL,
|
|
33
|
+
visibility_json TEXT NOT NULL CHECK (json_valid(visibility_json)),
|
|
34
|
+
effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
|
|
35
|
+
authorization_revision INTEGER NOT NULL,
|
|
36
|
+
deleted_at TEXT,
|
|
37
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
38
|
+
created_at TEXT NOT NULL,
|
|
39
|
+
PRIMARY KEY (entity_id, source_revision),
|
|
40
|
+
FOREIGN KEY (entity_id, source_revision)
|
|
41
|
+
REFERENCES taproot_entity_revisions(entity_id, revision),
|
|
42
|
+
FOREIGN KEY (event_id) REFERENCES taproot_audit_events(event_id)
|
|
43
|
+
) STRICT;
|
|
44
|
+
|
|
45
|
+
CREATE TABLE IF NOT EXISTS taproot_statement_authorization (
|
|
46
|
+
entity_id TEXT NOT NULL,
|
|
47
|
+
statement_id TEXT NOT NULL,
|
|
48
|
+
source_revision INTEGER NOT NULL,
|
|
49
|
+
restrictions_json TEXT NOT NULL CHECK (json_valid(restrictions_json)),
|
|
50
|
+
effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
|
|
51
|
+
authorization_revision INTEGER NOT NULL,
|
|
52
|
+
PRIMARY KEY (entity_id, statement_id),
|
|
53
|
+
FOREIGN KEY (entity_id) REFERENCES taproot_entities(entity_id)
|
|
54
|
+
) STRICT;
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS taproot_statement_authorization_revisions (
|
|
57
|
+
entity_id TEXT NOT NULL,
|
|
58
|
+
source_revision INTEGER NOT NULL,
|
|
59
|
+
statement_id TEXT NOT NULL,
|
|
60
|
+
restrictions_json TEXT NOT NULL CHECK (json_valid(restrictions_json)),
|
|
61
|
+
effective_visibility_json TEXT NOT NULL CHECK (json_valid(effective_visibility_json)),
|
|
62
|
+
authorization_revision INTEGER NOT NULL,
|
|
63
|
+
PRIMARY KEY (entity_id, source_revision, statement_id),
|
|
64
|
+
FOREIGN KEY (entity_id, source_revision)
|
|
65
|
+
REFERENCES taproot_entity_revisions(entity_id, revision)
|
|
66
|
+
) STRICT;
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS taproot_authorization_projection_outbox (
|
|
69
|
+
event_id TEXT PRIMARY KEY,
|
|
70
|
+
entity_id TEXT NOT NULL,
|
|
71
|
+
source_revision INTEGER NOT NULL,
|
|
72
|
+
authorization_revision INTEGER NOT NULL,
|
|
73
|
+
search_generation INTEGER NOT NULL,
|
|
74
|
+
operation TEXT NOT NULL CHECK (operation IN ('upsert', 'delete', 'repair', 'backfill')),
|
|
75
|
+
state TEXT NOT NULL DEFAULT 'pending' CHECK (state IN ('pending', 'claimed', 'complete')),
|
|
76
|
+
created_at TEXT NOT NULL,
|
|
77
|
+
FOREIGN KEY (event_id) REFERENCES taproot_audit_events(event_id)
|
|
78
|
+
) STRICT;
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS taproot_authorization_backfill_plans (
|
|
81
|
+
plan_id TEXT PRIMARY KEY,
|
|
82
|
+
installation_id TEXT NOT NULL,
|
|
83
|
+
base_authorization_revision INTEGER NOT NULL,
|
|
84
|
+
manifest_json TEXT NOT NULL CHECK (json_valid(manifest_json)),
|
|
85
|
+
manifest_hash TEXT NOT NULL,
|
|
86
|
+
entity_count INTEGER NOT NULL CHECK (entity_count > 0 AND entity_count <= 100),
|
|
87
|
+
revision_count INTEGER NOT NULL CHECK (revision_count > 0),
|
|
88
|
+
status TEXT NOT NULL CHECK (status IN ('planned', 'applying', 'complete')),
|
|
89
|
+
created_by TEXT NOT NULL,
|
|
90
|
+
created_at TEXT NOT NULL,
|
|
91
|
+
completed_at TEXT
|
|
92
|
+
) STRICT;
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS taproot_authorization_admin_audit (
|
|
95
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
audit_id TEXT NOT NULL UNIQUE,
|
|
97
|
+
event_type TEXT NOT NULL CHECK (event_type IN ('backfill-plan', 'backfill-apply')),
|
|
98
|
+
principal_id TEXT NOT NULL,
|
|
99
|
+
plan_id TEXT NOT NULL,
|
|
100
|
+
authorization_revision INTEGER NOT NULL,
|
|
101
|
+
details_json TEXT NOT NULL CHECK (json_valid(details_json)),
|
|
102
|
+
created_at TEXT NOT NULL,
|
|
103
|
+
FOREIGN KEY (plan_id) REFERENCES taproot_authorization_backfill_plans(plan_id)
|
|
104
|
+
) STRICT;
|
|
105
|
+
|
|
106
|
+
CREATE TABLE IF NOT EXISTS taproot_installation_authorization_advances (
|
|
107
|
+
advance_id TEXT PRIMARY KEY,
|
|
108
|
+
installation_id TEXT NOT NULL,
|
|
109
|
+
from_revision INTEGER NOT NULL,
|
|
110
|
+
to_revision INTEGER NOT NULL,
|
|
111
|
+
search_generation INTEGER NOT NULL,
|
|
112
|
+
domain TEXT NOT NULL,
|
|
113
|
+
principal_id TEXT NOT NULL,
|
|
114
|
+
reason TEXT NOT NULL,
|
|
115
|
+
created_at TEXT NOT NULL,
|
|
116
|
+
CHECK (to_revision = from_revision + 1)
|
|
117
|
+
) STRICT;
|
|
118
|
+
|
|
119
|
+
CREATE INDEX IF NOT EXISTS taproot_entity_authorization_candidate_idx
|
|
120
|
+
ON taproot_entity_authorization(installation_id, deleted_at, entity_id);
|
|
121
|
+
CREATE INDEX IF NOT EXISTS taproot_entity_authorization_revision_idx
|
|
122
|
+
ON taproot_entity_authorization_revisions(entity_id, source_revision DESC);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS taproot_statement_authorization_candidate_idx
|
|
124
|
+
ON taproot_statement_authorization(entity_id, source_revision, statement_id);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS taproot_authorization_outbox_state_idx
|
|
126
|
+
ON taproot_authorization_projection_outbox(state, authorization_revision, event_id);
|
|
127
|
+
|
|
128
|
+
CREATE TRIGGER IF NOT EXISTS taproot_revisions_no_replace
|
|
129
|
+
BEFORE INSERT ON taproot_entity_revisions
|
|
130
|
+
WHEN EXISTS (
|
|
131
|
+
SELECT 1 FROM taproot_entity_revisions
|
|
132
|
+
WHERE entity_id = NEW.entity_id AND revision = NEW.revision
|
|
133
|
+
)
|
|
134
|
+
BEGIN SELECT RAISE(ABORT, 'taproot revisions cannot be replaced'); END;
|
|
135
|
+
|
|
136
|
+
CREATE TRIGGER IF NOT EXISTS taproot_audit_no_replace
|
|
137
|
+
BEFORE INSERT ON taproot_audit_events
|
|
138
|
+
WHEN EXISTS (
|
|
139
|
+
SELECT 1 FROM taproot_audit_events WHERE event_id = NEW.event_id
|
|
140
|
+
)
|
|
141
|
+
BEGIN SELECT RAISE(ABORT, 'taproot audit events cannot be replaced'); END;
|
|
142
|
+
|
|
143
|
+
CREATE TRIGGER IF NOT EXISTS taproot_installation_identity_no_update
|
|
144
|
+
BEFORE UPDATE OF installation_id ON taproot_installation_authorization
|
|
145
|
+
BEGIN SELECT RAISE(ABORT, 'taproot installation identity is immutable'); END;
|
|
146
|
+
CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_no_delete
|
|
147
|
+
BEFORE DELETE ON taproot_installation_authorization
|
|
148
|
+
BEGIN SELECT RAISE(ABORT, 'taproot installation authorization is durable'); END;
|
|
149
|
+
CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_no_replace
|
|
150
|
+
BEFORE INSERT ON taproot_installation_authorization
|
|
151
|
+
WHEN EXISTS (SELECT 1 FROM taproot_installation_authorization WHERE singleton = NEW.singleton)
|
|
152
|
+
BEGIN SELECT RAISE(ABORT, 'taproot installation authorization cannot be replaced'); END;
|
|
153
|
+
CREATE TRIGGER IF NOT EXISTS taproot_entity_authorization_revisions_no_update
|
|
154
|
+
BEFORE UPDATE ON taproot_entity_authorization_revisions
|
|
155
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization revisions are immutable'); END;
|
|
156
|
+
CREATE TRIGGER IF NOT EXISTS taproot_entity_authorization_revisions_no_delete
|
|
157
|
+
BEFORE DELETE ON taproot_entity_authorization_revisions
|
|
158
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization revisions are immutable'); END;
|
|
159
|
+
CREATE TRIGGER IF NOT EXISTS taproot_entity_authorization_revisions_no_replace
|
|
160
|
+
BEFORE INSERT ON taproot_entity_authorization_revisions
|
|
161
|
+
WHEN EXISTS (
|
|
162
|
+
SELECT 1 FROM taproot_entity_authorization_revisions
|
|
163
|
+
WHERE (entity_id = NEW.entity_id AND source_revision = NEW.source_revision)
|
|
164
|
+
OR event_id = NEW.event_id
|
|
165
|
+
)
|
|
166
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization revisions cannot be replaced'); END;
|
|
167
|
+
CREATE TRIGGER IF NOT EXISTS taproot_statement_authorization_revisions_no_update
|
|
168
|
+
BEFORE UPDATE ON taproot_statement_authorization_revisions
|
|
169
|
+
BEGIN SELECT RAISE(ABORT, 'taproot statement authorization revisions are immutable'); END;
|
|
170
|
+
CREATE TRIGGER IF NOT EXISTS taproot_statement_authorization_revisions_no_delete
|
|
171
|
+
BEFORE DELETE ON taproot_statement_authorization_revisions
|
|
172
|
+
BEGIN SELECT RAISE(ABORT, 'taproot statement authorization revisions are immutable'); END;
|
|
173
|
+
CREATE TRIGGER IF NOT EXISTS taproot_statement_authorization_revisions_no_replace
|
|
174
|
+
BEFORE INSERT ON taproot_statement_authorization_revisions
|
|
175
|
+
WHEN EXISTS (
|
|
176
|
+
SELECT 1 FROM taproot_statement_authorization_revisions
|
|
177
|
+
WHERE entity_id = NEW.entity_id AND source_revision = NEW.source_revision
|
|
178
|
+
AND statement_id = NEW.statement_id
|
|
179
|
+
)
|
|
180
|
+
BEGIN SELECT RAISE(ABORT, 'taproot statement authorization revisions cannot be replaced'); END;
|
|
181
|
+
CREATE TRIGGER IF NOT EXISTS taproot_authorization_admin_audit_no_update
|
|
182
|
+
BEFORE UPDATE ON taproot_authorization_admin_audit
|
|
183
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization administration audit is immutable'); END;
|
|
184
|
+
CREATE TRIGGER IF NOT EXISTS taproot_authorization_admin_audit_no_delete
|
|
185
|
+
BEFORE DELETE ON taproot_authorization_admin_audit
|
|
186
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization administration audit is immutable'); END;
|
|
187
|
+
CREATE TRIGGER IF NOT EXISTS taproot_authorization_admin_audit_no_replace
|
|
188
|
+
BEFORE INSERT ON taproot_authorization_admin_audit
|
|
189
|
+
WHEN EXISTS (
|
|
190
|
+
SELECT 1 FROM taproot_authorization_admin_audit
|
|
191
|
+
WHERE sequence = NEW.sequence OR audit_id = NEW.audit_id
|
|
192
|
+
)
|
|
193
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization administration audit cannot be replaced'); END;
|
|
194
|
+
CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_advances_no_update
|
|
195
|
+
BEFORE UPDATE ON taproot_installation_authorization_advances
|
|
196
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization advances are immutable'); END;
|
|
197
|
+
CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_advances_no_delete
|
|
198
|
+
BEFORE DELETE ON taproot_installation_authorization_advances
|
|
199
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization advances are immutable'); END;
|
|
200
|
+
CREATE TRIGGER IF NOT EXISTS taproot_installation_authorization_advances_no_replace
|
|
201
|
+
BEFORE INSERT ON taproot_installation_authorization_advances
|
|
202
|
+
WHEN EXISTS (
|
|
203
|
+
SELECT 1 FROM taproot_installation_authorization_advances
|
|
204
|
+
WHERE advance_id = NEW.advance_id
|
|
205
|
+
)
|
|
206
|
+
BEGIN SELECT RAISE(ABORT, 'taproot authorization advances cannot be replaced'); END;
|
|
207
|
+
|
|
208
|
+
INSERT INTO taproot_metadata(metadata_key, metadata_value)
|
|
209
|
+
VALUES ('schema_version', '3')
|
|
210
|
+
ON CONFLICT(metadata_key) DO UPDATE SET metadata_value = excluded.metadata_value;
|
|
211
|
+
INSERT INTO taproot_migrations(version, name)
|
|
212
|
+
VALUES (4, 'canonical-authorization-policy')
|
|
213
|
+
ON CONFLICT(version) DO NOTHING;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gnolith/taproot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Portable SQLite and D1 Wikibase-compatible knowledge graph and revision layer for Gnolith.",
|
|
5
5
|
"author": "Gnolith contributors",
|
|
6
6
|
"keywords": [
|
|
@@ -75,6 +75,9 @@
|
|
|
75
75
|
"rdf-data-factory": "^2.0.2"
|
|
76
76
|
},
|
|
77
77
|
"packageManager": "npm@11.9.0",
|
|
78
|
+
"overrides": {
|
|
79
|
+
"sharp": "0.35.3"
|
|
80
|
+
},
|
|
78
81
|
"devDependencies": {
|
|
79
82
|
"@eslint/js": "^10.0.1",
|
|
80
83
|
"@types/node": "^24.0.0",
|