@substrat-run/kernel 0.125.0 → 0.127.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.
@@ -0,0 +1,70 @@
1
+ import { scopeId, substratError } from '@substrat-run/contracts';
2
+ /**
3
+ * Re-point a dump's scope-level grants at the scope it lands in, the step every restore, fork
4
+ * and preview carry ends with (#1869). One definition for both adapters' `importDump`, so the
5
+ * DO and the pure host cannot move different rows. It was a `LIKE 'scope:%'`, which ignores
6
+ * case and so also moved an entity grant typed `Scope` or `SCOPE`.
7
+ *
8
+ * Scope-level grants are stored as `object = 'scope:<scopeId>'`, naming the scope the dump
9
+ * was captured FROM, so a copy landing anywhere else has to move them. Entity-narrowed
10
+ * grants are stored as `<entityType>:<entityId>` and travel with the dump unchanged.
11
+ *
12
+ * - **Exact**, when the source is named and either the dump holds a `scope:<source>` row or
13
+ * the source is `exact` (platform-exported): `scope:<source>` and nothing else moves. An
14
+ * entity grant whose type is spelled `scope` in any case stays put (the write verbs refuse
15
+ * that type since #1856, but rows written before it are still stored). When the source IS
16
+ * the destination (a carry onto a new version) nothing moves at all.
17
+ * - **Fallback**, for a dump the caller supplied, when no source is named (a caller that
18
+ * predates the field) or the dump holds no `scope:<source>` row (its stated provenance does
19
+ * not describe its rows, as when `substrat scope restore` stamps a local world with the
20
+ * target's id): every object whose first six characters are exactly `scope:`. That is the
21
+ * rule before #1869 minus LIKE's case folding, so `Scope:<id>` and `SCOPE:<id>` still stay
22
+ * put. An entity typed exactly `scope` cannot be told apart from a node grant here, and moves.
23
+ *
24
+ * A caller-supplied dump that holds its source's grants AND grants on a THIRD scope, shaped
25
+ * like a scope node (`scope:<ULID>`) and neither the source nor the destination, is refused:
26
+ * its stated provenance describes only part of it, and moving the rest would guess. The
27
+ * refusal throws inside the load's transaction, so the target keeps what it held. A platform
28
+ * copy (`exact`) is not refused: there a `scope:<X>` row authorized nothing in the source it
29
+ * was exported from, so it is left untouched and authorizes nothing here either. A
30
+ * `scope:<id>` whose id is not a scope id's shape is an entity grant, and is left alone.
31
+ *
32
+ * The probe and the update are separate statements rather than one with an `EXISTS` in its
33
+ * `WHERE`: the update rewrites the very rows the probe looks for, and the answer must not
34
+ * change part-way through.
35
+ *
36
+ * `COLLATE BINARY` is written out because the tuples table's DDL comes from the dump, and a
37
+ * column declared `COLLATE NOCASE` there would make a bare `=` fold case again.
38
+ */
39
+ export function repointScopeGrants(sql, destScopeId, source) {
40
+ // `exact` is a claim about a named source; with none named it would silently take the fallback.
41
+ if (source !== undefined && !source.scopeId) {
42
+ throw substratError('validation_failed', 'restore refused: `exact` needs the scope the dump came from');
43
+ }
44
+ const dest = `scope:${destScopeId}`;
45
+ const update = 'UPDATE OR REPLACE _substrat_tuples SET object = ? WHERE object <> ? COLLATE BINARY AND';
46
+ const from = source === undefined ? undefined : `scope:${source.scopeId}`;
47
+ if (from === undefined || (!source?.exact && sql.all(SOURCE_PROBE, from).length === 0)) {
48
+ sql.run(`${update} substr(object, 1, 6) = 'scope:' COLLATE BINARY`, dest, dest);
49
+ return;
50
+ }
51
+ const strays = source.exact
52
+ ? []
53
+ : sql
54
+ .all(STRAY_SCOPES, from, dest)
55
+ .map((r) => String(r.object))
56
+ .filter((o) => scopeId.safeParse(o.slice('scope:'.length)).success);
57
+ if (strays.length > 0) {
58
+ throw substratError('validation_failed', `restore refused: the dump holds grants on ${strays.length} scope(s) other than its source ` +
59
+ `(${source.scopeId}) and this scope (${destScopeId}), which a restore cannot re-point: ` +
60
+ `${strays.slice(0, 3).join(', ')}${strays.length > 3 ? ', …' : ''}`);
61
+ }
62
+ sql.run(`${update} object = ? COLLATE BINARY`, dest, dest, from);
63
+ }
64
+ const SOURCE_PROBE = 'SELECT 1 FROM _substrat_tuples WHERE object = ? COLLATE BINARY LIMIT 1';
65
+ // Live rows only: a revoked grant (K-21 tombstone) on a third scope authorizes nothing anywhere.
66
+ const STRAY_SCOPES = `SELECT DISTINCT object FROM _substrat_tuples
67
+ WHERE substr(object, 1, 6) = 'scope:' COLLATE BINARY AND object <> ? COLLATE BINARY AND object <> ? COLLATE BINARY
68
+ AND revoked_at IS NULL
69
+ ORDER BY object`;
70
+ //# sourceMappingURL=scope-repoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope-repoint.js","sourceRoot":"","sources":["../src/scope-repoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAejE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAc,EAAE,WAAmB,EAAE,MAAsB;IAC5F,gGAAgG;IAChG,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QAC5C,MAAM,aAAa,CAAC,mBAAmB,EAAE,6DAA6D,CAAC,CAAC;IAC1G,CAAC;IACD,MAAM,IAAI,GAAG,SAAS,WAAW,EAAE,CAAC;IACpC,MAAM,MAAM,GAAG,wFAAwF,CAAC;IACxG,MAAM,IAAI,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,OAAO,EAAE,CAAC;IAC1E,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,EAAE,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QACvF,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,iDAAiD,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QAChF,OAAO;IACT,CAAC;IACD,MAAM,MAAM,GAAG,MAAO,CAAC,KAAK;QAC1B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,GAAG;aACA,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,CAAC;aAC7B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;aAC5B,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;IAC1E,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,MAAM,aAAa,CACjB,mBAAmB,EACnB,6CAA6C,MAAM,CAAC,MAAM,kCAAkC;YAC1F,IAAI,MAAO,CAAC,OAAO,qBAAqB,WAAW,sCAAsC;YACzF,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CACtE,CAAC;IACJ,CAAC;IACD,GAAG,CAAC,GAAG,CAAC,GAAG,MAAM,4BAA4B,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,YAAY,GAAG,wEAAwE,CAAC;AAC9F,iGAAiG;AACjG,MAAM,YAAY,GAAG;;;kBAGH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@substrat-run/kernel",
3
- "version": "0.125.0",
3
+ "version": "0.127.0",
4
4
  "description": "Substrat kernel contracts and services — pure TypeScript, no platform imports (D-14)",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
@@ -22,11 +22,11 @@
22
22
  "dist"
23
23
  ],
24
24
  "dependencies": {
25
- "@substrat-run/contracts": "^0.125.0"
25
+ "@substrat-run/contracts": "^0.127.0"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/node": "^22.0.0",
29
- "hono": "^4.6.0",
28
+ "@types/node": "^22.20.4",
29
+ "hono": "^4.13.9",
30
30
  "typescript": "^7.0.0",
31
31
  "vitest": "^3.2.7"
32
32
  },