rapier-html 1.1.56 → 1.1.57
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/BUILD.json +5 -5
- package/README.md +21 -8
- package/{cli.mjs → dist/cli.mjs} +0 -0
- package/dist/ledger/LICENSE +45 -0
- package/dist/ledger/authorship.mjs +59 -0
- package/dist/ledger/carried.mjs +69 -0
- package/dist/ledger/data.mjs +33 -0
- package/dist/ledger/format.mjs +131 -0
- package/dist/ledger/hash.mjs +47 -0
- package/dist/ledger/journal-records.mjs +139 -0
- package/{page.mjs → dist/page.mjs} +8 -12
- package/dist/rapier.html +952 -0
- package/package.json +4 -7
- package/rapier.html +0 -952
- /package/{return-address.mjs → dist/return-address.mjs} +0 -0
package/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "rapier-html",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"path": "rapier.html",
|
|
5
|
-
"bytes":
|
|
6
|
-
"sha256": "
|
|
7
|
-
"builtAt": "2026-10-
|
|
3
|
+
"version": "1.1.57",
|
|
4
|
+
"path": "dist/rapier.html",
|
|
5
|
+
"bytes": 1788469,
|
|
6
|
+
"sha256": "5a89d07839f98151b413e820b17f9b037cf6a334d9f77309b0ef718855792418",
|
|
7
|
+
"builtAt": "2026-10-04T06:43:06.832Z",
|
|
8
8
|
"node": "v22.22.2",
|
|
9
9
|
"canonical": true,
|
|
10
10
|
"packing": "zopfli",
|
package/README.md
CHANGED
|
@@ -14,14 +14,14 @@ editor, offline, with the document inside.
|
|
|
14
14
|
Node 22 or newer.
|
|
15
15
|
|
|
16
16
|
```sh
|
|
17
|
-
npx -- rapier-html@1.1.
|
|
18
|
-
npx -- rapier-html@1.1.
|
|
19
|
-
npx -- rapier-html@1.1.
|
|
20
|
-
npx -- rapier-html@1.1.
|
|
21
|
-
npx -- rapier-html@1.1.
|
|
22
|
-
npx -- rapier-html@1.1.
|
|
23
|
-
npx -- rapier-html@1.1.
|
|
24
|
-
npm install rapier-html@1.1.
|
|
17
|
+
npx -- rapier-html@1.1.57 notes.md # the editor, notes.md inside it, written beside the file
|
|
18
|
+
npx -- rapier-html@1.1.57 notes.md --view draw # opens on Draw: sketch and paint, the document behind it
|
|
19
|
+
npx -- rapier-html@1.1.57 notes.md --view notes # opens the Notes library
|
|
20
|
+
npx -- rapier-html@1.1.57 notes.md --drawing sketch.svg # carries a drawing, opened on Draw over the document
|
|
21
|
+
npx -- rapier-html@1.1.57 proposal.md --base original.md # opens on the diff of a proposed change
|
|
22
|
+
npx -- rapier-html@1.1.57 notes.md --return "$RETURN_URL" --return-expires-at "$RETURN_EXPIRES_AT" # Send back
|
|
23
|
+
npx -- rapier-html@1.1.57 notes.md out.html # a named output
|
|
24
|
+
npm install rapier-html@1.1.57 # as a library: wrap, unwrap
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
The first `--` is for npm, so `--help` reaches the command. It never overwrites a file.
|
|
@@ -92,3 +92,16 @@ fact of the file and written back on Save.
|
|
|
92
92
|
AGPL-3.0-only (`LICENSE`): the page is the Rapier editor with the notices of the libraries it carries.
|
|
93
93
|
The Markdown standard without the editor is `rapier-markdown-kit` (MIT). Rapier is https://rapier.website.
|
|
94
94
|
`BUILD.json` records the page's size, SHA-256, build time and Node version.
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
## Optional edit history
|
|
98
|
+
|
|
99
|
+
`wrap(page, text, filename, {ledger, authorship})` accepts the public kit's
|
|
100
|
+
`rapier-ledger/1` and `rapier-authorship/1` objects. `unwrap(page)` returns the
|
|
101
|
+
checked parts beside its text. Neither is on by default. An authorship projection
|
|
102
|
+
contains only attribution of current characters; a full ledger contains deleted
|
|
103
|
+
text too. A wrong-text, malformed, duplicate or inconsistent part throws instead
|
|
104
|
+
of attaching false history. Saved Markdown written with the kit's
|
|
105
|
+
`ledger/carried.writeDocument` is read automatically when wrapping. Rewrapping a
|
|
106
|
+
page without parts drops its previous document's parts. Published npm files carry
|
|
107
|
+
their own staged MIT validator closure; no source checkout is needed.
|
package/{cli.mjs → dist/cli.mjs}
RENAMED
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jack Skipworth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
Files under this licence carry `SPDX-License-Identifier: MIT` in their first lines: the standard's
|
|
24
|
+
grammar, marks and reference style (`spec/md-layout.mjs`, `spec/md-marks.mjs`, `spec/flowchart.mjs`, `spec/markdown-style.css`), the layout model and line planner
|
|
25
|
+
(`layout/model.mjs`, `layout/line-plan.mjs`), the Will grammar (`agent/will.mjs`), the picture appendix reader and writer
|
|
26
|
+
(`spec/md-assets.mjs`), the two shared-page reference readers (`tools/read-shared-page.mjs`,
|
|
27
|
+
`tools/read-shared-page.py`), and the host helper `packages/rapier-embed/embed.mjs`. The vendored Pretext (`agent/vendor/pretext`) and micromark carry their
|
|
28
|
+
own MIT notices. The edit-ledger module (`kit/rapier-ledger.mjs`, `kit/ledger/*.mjs`) and
|
|
29
|
+
its editor compatibility import (`editor/journal-records.mjs`) are also covered. The
|
|
30
|
+
record validator/replayer and text-only conflict merger were moved, not copied,
|
|
31
|
+
into that module; the Notes index merger and the rest of the agent/editor remain
|
|
32
|
+
AGPL-3.0-only. The wrapper's carrier-staging helper
|
|
33
|
+
(`skills/rapier-html/package-files.mjs`) is MIT. Everything else in Rapier is AGPL-3.0-only (see `LICENSE`).
|
|
34
|
+
|
|
35
|
+
The picture appendix reader and writer used to be part of `images/assets.mjs`, a file that also
|
|
36
|
+
holds JPEG XL/raster header inspection and SVG sanitizing -- both of which reach the editor's own
|
|
37
|
+
AGPL-3.0-only decoders (`images/raster.mjs`, `draw/font.mjs`), so the whole file could not honestly
|
|
38
|
+
sit in this list. The appendix logic (`parseAssets`, `documentAssets`, `imageEnvironment`,
|
|
39
|
+
`mayRetireImageDefinitions`, `retireDeletedImageDefinitions`, `assetOmissions`, `isAssetBlock`,
|
|
40
|
+
`serializeAsset`, `appendAssetText`, `appendAsset`, and the small markdown-it plumbing they share)
|
|
41
|
+
depends on nothing but a markdown-it factory, so it lives in `spec/md-assets.mjs`; `images/assets.mjs`
|
|
42
|
+
re-imports and re-exports that surface unchanged (one owner, no copy) and keeps everything that
|
|
43
|
+
needs the AGPL decoders, so it is AGPL-3.0-only like the rest of the editor. The `rapier-markdown-kit`
|
|
44
|
+
package re-exports `spec/md-assets.mjs` directly, never `images/assets.mjs`, so its import graph
|
|
45
|
+
never touches AGPL code.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {readLedger, exportLedger, textRoot, rootAfter} from './format.mjs';
|
|
3
|
+
import {_rapierTransformSplices} from './journal-records.mjs';
|
|
4
|
+
import {sha256} from './hash.mjs';
|
|
5
|
+
import {plainData, documentText, nonnegative, exactKeys, fault} from './data.mjs';
|
|
6
|
+
function coalesce(runs) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const run of runs) {
|
|
9
|
+
if (run[0] === run[1]) continue;
|
|
10
|
+
const last = out.at(-1);
|
|
11
|
+
if (last && last[1] === run[0] && last.slice(2).every((value, i) => value === run[i + 2])) last[1] = run[1];
|
|
12
|
+
else out.push(run.slice());
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
export function authorship(input) {
|
|
17
|
+
const {ledger, text} = readLedger(input);
|
|
18
|
+
let runs = ledger.start.text ? [[0, ledger.start.text.length, 'system', 'unattributed', 0]] : [];
|
|
19
|
+
for (const record of ledger.records) for (const row of record.splices) {
|
|
20
|
+
const cut = row.pos + row.removed.length, delta = row.inserted.length - row.removed.length, before = [], after = [];
|
|
21
|
+
for (const run of runs) {
|
|
22
|
+
if (run[0] < row.pos) before.push([run[0], Math.min(run[1], row.pos), ...run.slice(2)]);
|
|
23
|
+
if (run[1] > cut) after.push([Math.max(run[0], cut) + delta, run[1] + delta, ...run.slice(2)]);
|
|
24
|
+
}
|
|
25
|
+
const tx = record.transaction;
|
|
26
|
+
runs = coalesce([...before, [row.pos, row.pos + row.inserted.length, tx.actor.kind, tx.actor.id, tx.createdAt], ...after]);
|
|
27
|
+
}
|
|
28
|
+
return {format: 'rapier-authorship/1', sha256: sha256(text), runs};
|
|
29
|
+
}
|
|
30
|
+
export function readAuthorship(input, text) {
|
|
31
|
+
documentText(text); const value = plainData(input);
|
|
32
|
+
if (!exactKeys(value, ['format','sha256','runs']) || value.format !== 'rapier-authorship/1' || value.sha256 !== sha256(text) || !Array.isArray(value.runs)) throw fault('authorship is not for this text');
|
|
33
|
+
let cursor = 0;
|
|
34
|
+
for (const run of value.runs) {
|
|
35
|
+
if (!Array.isArray(run) || run.length !== 5 || run[0] !== cursor || !nonnegative(run[1]) || run[1] <= cursor || run[1] > text.length ||
|
|
36
|
+
!['human','agent','system'].includes(run[2]) || typeof run[3] !== 'string' || !run[3] || run[3].length > 160 || !nonnegative(run[4]) ||
|
|
37
|
+
/[\uD800-\uDFFF]/u.test(text.slice(cursor, run[1]))) throw fault('invalid authorship run');
|
|
38
|
+
cursor = run[1];
|
|
39
|
+
}
|
|
40
|
+
if (cursor !== text.length) throw fault('authorship does not cover the text');
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
// Authorship-only imports become an incomplete insertion ledger. There is no mutable blame database.
|
|
44
|
+
export function ledgerFromAuthorship(input, text, documentAuthority) {
|
|
45
|
+
const value = readAuthorship(input, text), records = [];
|
|
46
|
+
let current = '', root = textRoot('');
|
|
47
|
+
for (const [start, end, kind, id, createdAt] of value.runs) {
|
|
48
|
+
const row = {pos: start, removed: '', inserted: text.slice(start, end)}, revision = records.length + 1;
|
|
49
|
+
const beforeHash = root; root = rootAfter(root, row);
|
|
50
|
+
current = _rapierTransformSplices(current, [row]);
|
|
51
|
+
records.push({beforeHash, afterHash: root, splices: [row], transaction: {
|
|
52
|
+
id: documentAuthority.slice(0, 48) + ':authorship:' + revision.toString(36), documentAuthority,
|
|
53
|
+
baseRevision: revision - 1, revision, actor: {kind, id}, transport: 'platform', operation: 'authorship.import',
|
|
54
|
+
requestId: null, sourceTransactionId: null, affectedBlockIds: [], parent: records.at(-1)?.transaction.id ?? null,
|
|
55
|
+
reverts: null, reapplies: null, createdAt,
|
|
56
|
+
}});
|
|
57
|
+
}
|
|
58
|
+
return exportLedger({text: current, records, documentAuthority, revision: records.length, root, complete: false});
|
|
59
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {readLedger} from './format.mjs';
|
|
3
|
+
import {authorship as projectAuthorship, readAuthorship} from './authorship.mjs';
|
|
4
|
+
import {canonicalJSON, fault} from './data.mjs';
|
|
5
|
+
export function encodeCarried(text) {
|
|
6
|
+
return String(text).replace(/[\\\r\0]|<[/!]/g, m => m === '\\' ? '\\\\' : m === '\r' ? '\\r' : m === '\0' ? '\\0' : m === '</' ? '\\/' : '\\!');
|
|
7
|
+
}
|
|
8
|
+
export function decodeCarried(text) {
|
|
9
|
+
return String(text).replace(/\\([\\/!r0])/g, (_, c) => c === '\\' ? '\\' : c === 'r' ? '\r' : c === '0' ? '\0' : c === '/' ? '</' : '<!');
|
|
10
|
+
}
|
|
11
|
+
export function validateParts(text, {ledger = null, authorship = null} = {}) {
|
|
12
|
+
// A file's leading BOM is metadata, as in the editor's source intake.
|
|
13
|
+
const canonical = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
14
|
+
const history = ledger === null ? null : readLedger(ledger, canonical).ledger;
|
|
15
|
+
const runs = authorship === null ? null : readAuthorship(authorship, canonical);
|
|
16
|
+
if (history && runs && canonicalJSON(projectAuthorship(history)) !== canonicalJSON(runs)) throw fault('authorship differs from ledger replay');
|
|
17
|
+
return {ledger: history, authorship: runs};
|
|
18
|
+
}
|
|
19
|
+
export function writeParts(text, options) {
|
|
20
|
+
const parts = validateParts(text, options);
|
|
21
|
+
return Object.entries(parts).filter(([, value]) => value !== null).map(([name, value]) =>
|
|
22
|
+
'<script type="application/json" id="rapier-' + name + '">' + encodeCarried(JSON.stringify(value)) + '</script>\n').join('');
|
|
23
|
+
}
|
|
24
|
+
// Pure adapter: the browser supplies inert script elements, Node supplies parsed script records.
|
|
25
|
+
export function readParts(text, elements) {
|
|
26
|
+
const parts = {ledger: null, authorship: null};
|
|
27
|
+
for (const element of elements) {
|
|
28
|
+
const name = element.id === 'rapier-ledger' ? 'ledger' : element.id === 'rapier-authorship' ? 'authorship' : null;
|
|
29
|
+
if (!name) continue;
|
|
30
|
+
if (parts[name] !== null || element.type !== 'application/json' || (element.tagName && element.tagName.toLowerCase() !== 'script')) throw fault('duplicate or executable carried part');
|
|
31
|
+
try { parts[name] = JSON.parse(decodeCarried(element.textContent)); }
|
|
32
|
+
catch (_) { throw fault('invalid carried JSON'); }
|
|
33
|
+
if (parts[name] === null) throw fault('empty carried part');
|
|
34
|
+
}
|
|
35
|
+
return validateParts(text, parts);
|
|
36
|
+
}
|
|
37
|
+
// The wrapper only removes whole script elements. Strings inside other scripts are not carriers.
|
|
38
|
+
export function takeParts(html, text) {
|
|
39
|
+
const elements = [];
|
|
40
|
+
const stripped = html.replace(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi, (whole, attrs, content) => {
|
|
41
|
+
const attributes = [...attrs.matchAll(/(?:^|\s)([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/g)];
|
|
42
|
+
const ids = attributes.filter(m => m[1].toLowerCase() === 'id').map(m => m[2] ?? m[3] ?? m[4]);
|
|
43
|
+
if (!ids.some(id => id === 'rapier-ledger' || id === 'rapier-authorship')) return whole;
|
|
44
|
+
const types = attributes.filter(m => m[1].toLowerCase() === 'type').map(m => m[2] ?? m[3] ?? m[4]);
|
|
45
|
+
if (ids.length !== 1 || types.length !== 1) throw fault('ambiguous carried part');
|
|
46
|
+
elements.push({id: ids[0], type: types[0].toLowerCase(), textContent: content}); return '';
|
|
47
|
+
});
|
|
48
|
+
if (elements.length && text === null) throw fault('carried history without a document');
|
|
49
|
+
return {html: stripped, ...readParts(text ?? '', elements)};
|
|
50
|
+
}
|
|
51
|
+
const OPEN = '\n\n<!-- rapier-carried/1\n', CLOSE = '\n-->\n';
|
|
52
|
+
// One optional HTML comment at EOF; every other Markdown reader ignores it. Source is never rewritten.
|
|
53
|
+
export function writeDocument(text, options) {
|
|
54
|
+
const parts = validateParts(text, options);
|
|
55
|
+
if (!parts.ledger && !parts.authorship) return text;
|
|
56
|
+
const json = JSON.stringify(parts).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
57
|
+
return text + OPEN + json + CLOSE;
|
|
58
|
+
}
|
|
59
|
+
export function readDocument(source) {
|
|
60
|
+
const at = source.lastIndexOf(OPEN);
|
|
61
|
+
if (at < 0) return {text: source, ledger: null, authorship: null};
|
|
62
|
+
if (!source.endsWith(CLOSE)) throw fault('unfinished document metadata');
|
|
63
|
+
let parts;
|
|
64
|
+
try { parts = JSON.parse(source.slice(at + OPEN.length, -CLOSE.length)); }
|
|
65
|
+
catch (_) { throw fault('invalid document metadata'); }
|
|
66
|
+
if (!parts || Object.keys(parts).length !== 2 || !Object.hasOwn(parts, 'ledger') || !Object.hasOwn(parts, 'authorship')) throw fault('invalid document parts');
|
|
67
|
+
const text = source.slice(0, at);
|
|
68
|
+
return {text, ...validateParts(text, parts)};
|
|
69
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Only plain JSON data enters the public format. Accessors and toJSON are never executed.
|
|
3
|
+
export function fault(message) { return Object.assign(new Error('Ledger: ' + message), {code: 'ledger_invalid'}); }
|
|
4
|
+
export function canonicalJSON(value, depth = 0, active = new Set()) {
|
|
5
|
+
if (value === null || typeof value === 'boolean') return JSON.stringify(value);
|
|
6
|
+
if (typeof value === 'string') { if (/[\uD800-\uDFFF]/u.test(value)) throw fault('unpaired surrogate'); return JSON.stringify(value); }
|
|
7
|
+
if (typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) return JSON.stringify(value);
|
|
8
|
+
if (!value || typeof value !== 'object' || depth > 64 || active.has(value)) throw fault('not plain JSON data');
|
|
9
|
+
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
10
|
+
if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null || Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) throw fault('not a plain object');
|
|
11
|
+
const descriptors = Object.getOwnPropertyDescriptors(value), keys = Reflect.ownKeys(descriptors);
|
|
12
|
+
for (const key of keys) {
|
|
13
|
+
if (array && key === 'length') continue;
|
|
14
|
+
const field = descriptors[key];
|
|
15
|
+
if (typeof key !== 'string' || ['__proto__', 'constructor', 'prototype'].includes(key) || !Object.hasOwn(field, 'value') || !field.enumerable) throw fault('non-data property');
|
|
16
|
+
}
|
|
17
|
+
if (array && (keys.length !== value.length + 1 || keys.some(k => k !== 'length' && (!/^(0|[1-9]\d*)$/.test(k) || +k >= value.length)))) throw fault('sparse array');
|
|
18
|
+
active.add(value);
|
|
19
|
+
const encode = item => canonicalJSON(item, depth + 1, active);
|
|
20
|
+
const out = array ? '[' + Array.from({length: value.length}, (_, i) => encode(descriptors[i].value)).join(',') + ']'
|
|
21
|
+
: '{' + keys.sort().map(key => JSON.stringify(key) + ':' + encode(descriptors[key].value)).join(',') + '}';
|
|
22
|
+
active.delete(value); return out;
|
|
23
|
+
}
|
|
24
|
+
export function plainData(value) { return JSON.parse(canonicalJSON(typeof value === 'string' ? JSON.parse(value) : value)); }
|
|
25
|
+
export function exactKeys(value, keys) {
|
|
26
|
+
return value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
27
|
+
}
|
|
28
|
+
export function documentText(text) {
|
|
29
|
+
if (typeof text !== 'string' || /[\uD800-\uDFFF]/u.test(text)) throw fault('document is not whole-character text');
|
|
30
|
+
if (new TextEncoder().encode(text).length > 25 * 1024 * 1024) throw fault('document exceeds the editor text limit');
|
|
31
|
+
return text;
|
|
32
|
+
}
|
|
33
|
+
export const nonnegative = value => Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
import {_rapierValidLedgerRecord, _rapierRecordSplices, _rapierTransformSplices} from './journal-records.mjs';
|
|
3
|
+
import {textRoot, rootAfter, sha256} from './hash.mjs';
|
|
4
|
+
import {canonicalJSON, plainData, documentText, exactKeys, nonnegative, fault} from './data.mjs';
|
|
5
|
+
export {textRoot, rootAfter};
|
|
6
|
+
const digest = value => sha256(canonicalJSON(value));
|
|
7
|
+
const rootShape = value => typeof value === 'string' && /^\d+:[a-z0-9]+:[a-z0-9]+$/.test(value);
|
|
8
|
+
export function readLedger(input, expectedText) {
|
|
9
|
+
const ledger = plainData(input);
|
|
10
|
+
if (!exactKeys(ledger, ['format','documentAuthority','start','records','head','complete','sha256']) || ledger.format !== 'rapier-ledger/1' ||
|
|
11
|
+
typeof ledger.documentAuthority !== 'string' || !ledger.documentAuthority || ledger.documentAuthority.length > 256 ||
|
|
12
|
+
typeof ledger.complete !== 'boolean' || !Array.isArray(ledger.records) ||
|
|
13
|
+
!exactKeys(ledger.start, ['text','revision','root','sha256']) || !exactKeys(ledger.head, ['revision','root','sha256'])) throw fault('invalid envelope');
|
|
14
|
+
const {sha256: claimed, ...payload} = ledger;
|
|
15
|
+
if (claimed !== digest(payload)) throw fault('envelope checksum differs');
|
|
16
|
+
let text = documentText(ledger.start.text), root = ledger.start.root, revision = ledger.start.revision;
|
|
17
|
+
if (!nonnegative(revision) || !rootShape(root) || (revision === 0 && root !== textRoot(text)) || ledger.start.sha256 !== sha256(text) ||
|
|
18
|
+
(ledger.complete && revision !== 0)) throw fault('invalid start');
|
|
19
|
+
const earlier = [];
|
|
20
|
+
for (const record of ledger.records) {
|
|
21
|
+
if (!Array.isArray(record.splices) || !record.splices.length || !_rapierValidLedgerRecord(record, earlier.at(-1) || null, earlier,
|
|
22
|
+
ledger.documentAuthority, 25 * 1024 * 1024, text => new TextEncoder().encode(text).length, !ledger.complete) ||
|
|
23
|
+
record.transaction.baseRevision < revision || (record.transaction.baseRevision !== revision && ledger.complete) ||
|
|
24
|
+
(record.transaction.baseRevision === revision && record.beforeHash !== root)) throw fault('record chain differs');
|
|
25
|
+
// A pruned cancelling pair leaves a stated revision gap, just as in the editor's restore reader.
|
|
26
|
+
if (record.transaction.baseRevision !== revision) root = record.beforeHash;
|
|
27
|
+
const next = _rapierTransformSplices(text, record.splices);
|
|
28
|
+
if (next === null) throw fault('splice does not replay');
|
|
29
|
+
text = documentText(next);
|
|
30
|
+
for (const row of record.splices) root = rootAfter(root, row);
|
|
31
|
+
if (root !== record.afterHash) throw fault('record root differs');
|
|
32
|
+
revision = record.transaction.revision; earlier.push(record);
|
|
33
|
+
}
|
|
34
|
+
if (ledger.head.revision !== revision || ledger.head.root !== root || ledger.head.sha256 !== sha256(text) ||
|
|
35
|
+
(expectedText !== undefined && text !== expectedText)) throw fault('history does not describe this document');
|
|
36
|
+
return {ledger, text, root, revision, complete: ledger.complete};
|
|
37
|
+
}
|
|
38
|
+
export function exportLedger({text, records, documentAuthority, revision, root, complete = false, earlier = records} = {}) {
|
|
39
|
+
documentText(text);
|
|
40
|
+
if (!Array.isArray(records) || !Array.isArray(earlier)) throw fault('records required');
|
|
41
|
+
// Inspect before touching properties: exporting must not call accessors on an alleged record.
|
|
42
|
+
records = plainData(records); earlier = plainData(earlier);
|
|
43
|
+
const expanded = records.map(record => {
|
|
44
|
+
const rows = _rapierRecordSplices(record, earlier);
|
|
45
|
+
if (!rows) throw fault('navigation target was not retained');
|
|
46
|
+
return {transaction: record.transaction, splices: rows.map(row => ({pos: row.pos, removed: row.removed, inserted: row.inserted})),
|
|
47
|
+
beforeHash: record.beforeHash, afterHash: record.afterHash};
|
|
48
|
+
});
|
|
49
|
+
let startText = text;
|
|
50
|
+
for (let i = expanded.length - 1; i >= 0; i--) {
|
|
51
|
+
startText = _rapierTransformSplices(startText, expanded[i].splices, true);
|
|
52
|
+
if (startText === null) throw fault('history does not reverse from this document');
|
|
53
|
+
}
|
|
54
|
+
revision ??= expanded.at(-1)?.transaction.revision ?? 0;
|
|
55
|
+
root ??= expanded.at(-1)?.afterHash ?? textRoot(text);
|
|
56
|
+
documentAuthority ??= expanded[0]?.transaction.documentAuthority;
|
|
57
|
+
const startRevision = expanded[0]?.transaction.baseRevision ?? revision;
|
|
58
|
+
const payload = {format: 'rapier-ledger/1', documentAuthority,
|
|
59
|
+
start: {text: startText, revision: startRevision, root: expanded[0]?.beforeHash ?? root, sha256: sha256(startText)},
|
|
60
|
+
records: expanded, head: {revision, root, sha256: sha256(text)}, complete: complete === true && startRevision === 0 && expanded.every((row, i) => !i || row.transaction.baseRevision === expanded[i - 1].transaction.revision)};
|
|
61
|
+
const ledger = {...payload, sha256: digest(payload)};
|
|
62
|
+
readLedger(ledger, text); return ledger;
|
|
63
|
+
}
|
|
64
|
+
// Derived navigation, never a second history. A target older than start is intentionally unavailable.
|
|
65
|
+
export function historyEnvelope(input, segmentIdentity = []) {
|
|
66
|
+
const {ledger, text} = readLedger(input), ids = new Set(ledger.records.map(row => row.transaction.id));
|
|
67
|
+
let branch = [], cursor = 0;
|
|
68
|
+
for (const {transaction: tx} of ledger.records) {
|
|
69
|
+
const target = tx.reverts || tx.reapplies;
|
|
70
|
+
if (target) {
|
|
71
|
+
if (!ids.has(target)) continue;
|
|
72
|
+
if (tx.reverts && branch[cursor - 1] === target) cursor--;
|
|
73
|
+
else if (tx.reapplies && branch[cursor] === target) cursor++;
|
|
74
|
+
else throw fault('invalid navigation order');
|
|
75
|
+
} else { branch = branch.slice(0, cursor); branch.push(tx.id); cursor = branch.length; }
|
|
76
|
+
}
|
|
77
|
+
const byId = new Map(ledger.records.map(row => [row.transaction.id, row]));
|
|
78
|
+
let traversed = text;
|
|
79
|
+
for (let i = cursor - 1; i >= 0; i--) {
|
|
80
|
+
traversed = _rapierTransformSplices(traversed, byId.get(branch[i]).splices, true);
|
|
81
|
+
if (traversed === null) throw fault('undo branch does not reverse');
|
|
82
|
+
}
|
|
83
|
+
traversed = text;
|
|
84
|
+
for (let i = cursor; i < branch.length; i++) {
|
|
85
|
+
traversed = _rapierTransformSplices(traversed, byId.get(branch[i]).splices);
|
|
86
|
+
if (traversed === null) throw fault('redo branch does not replay');
|
|
87
|
+
}
|
|
88
|
+
if (ledger.complete) {
|
|
89
|
+
if (ledger.records.length && !branch.length) throw fault('complete history lost its branch');
|
|
90
|
+
let active = ledger.start.text;
|
|
91
|
+
if (!cursor && active !== text) throw fault('complete history start differs');
|
|
92
|
+
for (let i = 0; i < branch.length; i++) {
|
|
93
|
+
active = _rapierTransformSplices(active, byId.get(branch[i]).splices);
|
|
94
|
+
if (active === null || (i + 1 === cursor && active !== text)) throw fault('complete branch differs');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return {schemaVersion: 4, documentAuthority: ledger.documentAuthority, documentRevision: ledger.head.revision,
|
|
98
|
+
ledger: ledger.records, branch, cursor, earliestRevision: ledger.start.revision, earliestHash: ledger.start.root,
|
|
99
|
+
sourceRootId: ledger.head.root, segmentIdentity, historyComplete: ledger.complete,
|
|
100
|
+
trimReason: ledger.complete ? '' : 'carried_start', trimmedBytes: 0};
|
|
101
|
+
}
|
|
102
|
+
export function agentActorId(door, agent) {
|
|
103
|
+
if (typeof door !== 'string' || !door || door.length > 160 || /[\u0000-\u001f\u007f]/.test(door)) throw fault('invalid door');
|
|
104
|
+
if (agent == null) return door;
|
|
105
|
+
agent = plainData(agent);
|
|
106
|
+
if (!exactKeys(agent, ['name']) || typeof agent.name !== 'string' || !agent.name.trim() || agent.name.length > 96 ||
|
|
107
|
+
/[\u0000-\u001f\u007f]/.test(agent.name) || door.length + agent.name.length + 1 > 160) throw fault('invalid host-given name');
|
|
108
|
+
return door + '/' + agent.name;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// A writer changing representation records that change, rather than attaching an unrelated head.
|
|
112
|
+
export function replaceLedgerText(input, text, {actor = {kind: 'system', id: 'rapier'}, operation = 'document.replace', at} = {}) {
|
|
113
|
+
const current = readLedger(input); documentText(text);
|
|
114
|
+
if (current.text === text) return current.ledger;
|
|
115
|
+
let start = 0, a = current.text.length, b = text.length;
|
|
116
|
+
while (start < a && start < b && current.text[start] === text[start]) start++;
|
|
117
|
+
if (start && /[\uDC00-\uDFFF]/.test(current.text[start] || text[start] || '')) start--;
|
|
118
|
+
while (a > start && b > start && current.text[a - 1] === text[b - 1]) { a--; b--; }
|
|
119
|
+
if (/[\uDC00-\uDFFF]/.test(current.text[a] || text[b] || '')) { a++; b++; }
|
|
120
|
+
const splices = [{pos: start, removed: current.text.slice(start, a), inserted: text.slice(start, b)}];
|
|
121
|
+
const revision = current.revision + 1, root = rootAfter(current.root, splices[0]);
|
|
122
|
+
const record = {beforeHash: current.root, afterHash: root, splices, transaction: {
|
|
123
|
+
id: current.ledger.documentAuthority.slice(0, 48) + ':export:' + revision.toString(36),
|
|
124
|
+
documentAuthority: current.ledger.documentAuthority, baseRevision: current.revision, revision,
|
|
125
|
+
actor, transport: 'platform', operation, requestId: null, sourceTransactionId: null,
|
|
126
|
+
affectedBlockIds: [], parent: current.ledger.records.at(-1)?.transaction.id ?? null,
|
|
127
|
+
reverts: null, reapplies: null, createdAt: at ?? current.ledger.records.at(-1)?.transaction.createdAt ?? 0,
|
|
128
|
+
}};
|
|
129
|
+
return exportLedger({text, records: [...current.ledger.records, record], documentAuthority: current.ledger.documentAuthority,
|
|
130
|
+
revision, root, complete: current.complete});
|
|
131
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// The editor's path-dependent root, plus synchronous SHA-256 for file readers (including offline wrappers).
|
|
3
|
+
export function textRoot(text) {
|
|
4
|
+
let fnv = 0x811c9dc5, a = 1, b = 0;
|
|
5
|
+
for (let i = 0; i < text.length; i++) {
|
|
6
|
+
const c = text.charCodeAt(i);
|
|
7
|
+
fnv = Math.imul(fnv ^ c, 16777619); a = (a + c) % 65521; b = (b + a) % 65521;
|
|
8
|
+
}
|
|
9
|
+
return text.length + ':' + (fnv >>> 0).toString(36) + ':' + (((b << 16) | a) >>> 0).toString(36);
|
|
10
|
+
}
|
|
11
|
+
export function rootAfter(root, row) {
|
|
12
|
+
return textRoot(String(root || '') + ':' + row.pos + ':' + textRoot(row.removed) + ':' + textRoot(row.inserted));
|
|
13
|
+
}
|
|
14
|
+
const K = new Uint32Array([
|
|
15
|
+
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
|
|
16
|
+
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
|
|
17
|
+
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
|
|
18
|
+
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
|
|
19
|
+
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
|
|
20
|
+
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
|
|
21
|
+
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
|
|
22
|
+
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2,
|
|
23
|
+
]);
|
|
24
|
+
const rotate = (x, n) => (x >>> n) | (x << (32 - n));
|
|
25
|
+
export function sha256(text) {
|
|
26
|
+
const bytes = new TextEncoder().encode(text);
|
|
27
|
+
const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64);
|
|
28
|
+
padded.set(bytes); padded[bytes.length] = 0x80;
|
|
29
|
+
const data = new DataView(padded.buffer), bits = bytes.length * 8;
|
|
30
|
+
data.setUint32(padded.length - 8, Math.floor(bits / 0x100000000)); data.setUint32(padded.length - 4, bits >>> 0);
|
|
31
|
+
const h = new Uint32Array([0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]), w = new Uint32Array(64);
|
|
32
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
33
|
+
for (let i = 0; i < 16; i++) w[i] = data.getUint32(offset + i * 4);
|
|
34
|
+
for (let i = 16; i < 64; i++) {
|
|
35
|
+
const x = w[i - 15], y = w[i - 2];
|
|
36
|
+
w[i] = w[i - 16] + (rotate(x, 7) ^ rotate(x, 18) ^ (x >>> 3)) + w[i - 7] + (rotate(y, 17) ^ rotate(y, 19) ^ (y >>> 10));
|
|
37
|
+
}
|
|
38
|
+
let [a,b,c,d,e,f,g,k] = h;
|
|
39
|
+
for (let i = 0; i < 64; i++) {
|
|
40
|
+
const t1 = (k + (rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25)) + ((e & f) ^ (~e & g)) + K[i] + w[i]) >>> 0;
|
|
41
|
+
const t2 = ((rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22)) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0;
|
|
42
|
+
k = g; g = f; f = e; e = (d + t1) >>> 0; d = c; c = b; b = a; a = (t1 + t2) >>> 0;
|
|
43
|
+
}
|
|
44
|
+
for (const [i, value] of [a,b,c,d,e,f,g,k].entries()) h[i] += value;
|
|
45
|
+
}
|
|
46
|
+
return [...h].map(x => x.toString(16).padStart(8, '0')).join('');
|
|
47
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
const _RAPIER_TRANSACTION_ACTOR_LIMIT = 160;
|
|
4
|
+
const _RAPIER_TRANSACTION_OPERATION_LIMIT = 96;
|
|
5
|
+
const _RAPIER_TRANSACTION_REQUEST_LIMIT = 160;
|
|
6
|
+
|
|
7
|
+
/* Validate every row, not only the final string: a half-pair replacement can produce a whole
|
|
8
|
+
character while leaving unencodable fragments in the ledger; two invalid rows can also repair
|
|
9
|
+
each other. Refusal is atomic. Pre-existing damage elsewhere stays untouched and editable. */
|
|
10
|
+
function _rapierTransformSplices(text, splices, inverse = false) {
|
|
11
|
+
try {
|
|
12
|
+
if (!Array.isArray(splices)) return null;
|
|
13
|
+
let value = String(text == null ? '' : text);
|
|
14
|
+
// In Unicode mode a complete pair is one scalar, outside the surrogate range.
|
|
15
|
+
const whole = text => typeof text === 'string' && !/[\uD800-\uDFFF]/u.test(text);
|
|
16
|
+
const splitsPair = at => at > 0 && at < value.length &&
|
|
17
|
+
(value.charCodeAt(at - 1) & 0xFC00) === 0xD800 &&
|
|
18
|
+
(value.charCodeAt(at) & 0xFC00) === 0xDC00;
|
|
19
|
+
const rows = inverse ? splices.slice().reverse() : splices;
|
|
20
|
+
for (const row of rows) {
|
|
21
|
+
if (!row || !Number.isSafeInteger(row.pos) || row.pos < 0 || Object.is(row.pos, -0)) return null;
|
|
22
|
+
const pos = row.pos;
|
|
23
|
+
const removed = inverse ? row.inserted : row.removed;
|
|
24
|
+
const inserted = inverse ? row.removed : row.inserted;
|
|
25
|
+
if (!whole(removed) || !whole(inserted) || pos > value.length ||
|
|
26
|
+
pos + removed.length > value.length || splitsPair(pos) || splitsPair(pos + removed.length) ||
|
|
27
|
+
value.slice(pos, pos + removed.length) !== removed) return null;
|
|
28
|
+
value = value.slice(0, pos) + inserted + value.slice(pos + removed.length);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
} catch (_) { return null; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _rapierRecordSplices(entry, ledger) {
|
|
35
|
+
try {
|
|
36
|
+
if (entry && Array.isArray(entry.splices) && entry.splices.length) return entry.splices;
|
|
37
|
+
const transaction = entry && entry.transaction;
|
|
38
|
+
const targetId = String(transaction?.reverts || transaction?.reapplies || '');
|
|
39
|
+
const target = targetId && (ledger || []).find(record => record?.transaction?.id === targetId);
|
|
40
|
+
if (!target || !Array.isArray(target.splices) || !target.splices.length) return null;
|
|
41
|
+
return transaction.reverts ? target.splices.slice().reverse().map(row => ({
|
|
42
|
+
pos: row.pos, removed: row.inserted, inserted: row.removed,
|
|
43
|
+
})) : target.splices;
|
|
44
|
+
} catch (_) { return null; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A ledger record as the journal and the Notes return validate it below: plain data, nothing
|
|
48
|
+
// undefined anywhere in it. A live record could carry a row whose type or order was not yet known
|
|
49
|
+
// (a block just inserted, before its first render: `type: undefined`), and the validator refused
|
|
50
|
+
// the whole ledger for that one field -- silently at boot, where a refused ledger is cleared, and on
|
|
51
|
+
// the founder's phone as "Rapier could not be reopened: the document history could not be restored"
|
|
52
|
+
// on every return from Notes (24 September). The copy is made once per record; records are frozen,
|
|
53
|
+
// so it never goes stale.
|
|
54
|
+
const _rapierJournalRecord = (() => {
|
|
55
|
+
const copies = new WeakMap();
|
|
56
|
+
return record => {
|
|
57
|
+
let copy = copies.get(record);
|
|
58
|
+
if (!copy) { copy = JSON.parse(JSON.stringify(record)); copies.set(record, copy); }
|
|
59
|
+
return copy;
|
|
60
|
+
};
|
|
61
|
+
})();
|
|
62
|
+
|
|
63
|
+
function _rapierValidLedgerRecord(record, prior, earlier, documentAuthority, maxDocumentBytes, utf8Length, allowGap = false) {
|
|
64
|
+
// Recovery records are data, not executable objects. Inspect descriptors without invoking
|
|
65
|
+
// accessors, and use an ancestor set (not a global seen set) so shared acyclic data stays valid.
|
|
66
|
+
// Iterative traversal refuses cycles without exhausting the call stack. Proxies/counters that
|
|
67
|
+
// throw are refused by the same gate rather than escaping into document recovery.
|
|
68
|
+
try {
|
|
69
|
+
const active = new Set(), pending = [[record, false]];
|
|
70
|
+
while (pending.length) {
|
|
71
|
+
const [value, leaving] = pending.pop();
|
|
72
|
+
if (leaving) { active.delete(value); continue; }
|
|
73
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') continue;
|
|
74
|
+
if (typeof value === 'number') { if (!Number.isFinite(value)) return false; continue; }
|
|
75
|
+
if (!value || typeof value !== 'object' || active.has(value)) return false;
|
|
76
|
+
const array = Array.isArray(value), proto = Object.getPrototypeOf(value);
|
|
77
|
+
if (!array && proto !== null && (Object.getPrototypeOf(proto) !== null ||
|
|
78
|
+
Object.getOwnPropertyDescriptor(proto, 'constructor')?.value?.name !== 'Object')) return false;
|
|
79
|
+
const keys = Reflect.ownKeys(value);
|
|
80
|
+
if (array && keys.length !== value.length + 1) return false;
|
|
81
|
+
active.add(value); pending.push([value, true]);
|
|
82
|
+
for (const key of keys) {
|
|
83
|
+
if (array && key === 'length') continue;
|
|
84
|
+
if (typeof key !== 'string' || key === '__proto__' || key === 'constructor' || key === 'prototype' ||
|
|
85
|
+
(array && (!Number.isSafeInteger(Number(key)) || Number(key) < 0 ||
|
|
86
|
+
Number(key) >= value.length || String(Number(key)) !== key))) return false;
|
|
87
|
+
const field = Object.getOwnPropertyDescriptor(value, key);
|
|
88
|
+
if (!field || !Object.hasOwn(field, 'value')) return false;
|
|
89
|
+
pending.push([field.value, false]);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (!record || typeof record !== 'object' ||
|
|
93
|
+
typeof record.beforeHash !== 'string' || !record.beforeHash.length ||
|
|
94
|
+
typeof record.afterHash !== 'string' || !record.afterHash.length || record.beforeHash === record.afterHash) return false;
|
|
95
|
+
const own = Array.isArray(record.splices) && record.splices.length;
|
|
96
|
+
const navigation = !!(record.transaction?.reverts || record.transaction?.reapplies);
|
|
97
|
+
if ((!own && !navigation) || (own && record.splices.length > 64) ||
|
|
98
|
+
(record.transaction?.reverts && record.transaction?.reapplies)) return false;
|
|
99
|
+
const splices = _rapierRecordSplices(record, earlier);
|
|
100
|
+
if (!splices || splices.length > 64 || !splices.every(row =>
|
|
101
|
+
row && Number.isSafeInteger(row.pos) && row.pos >= 0 && !Object.is(row.pos, -0) &&
|
|
102
|
+
typeof row.removed === 'string' && typeof row.inserted === 'string' &&
|
|
103
|
+
!/[\uD800-\uDFFF]/u.test(row.removed) && !/[\uD800-\uDFFF]/u.test(row.inserted) &&
|
|
104
|
+
utf8Length(row.removed) <= maxDocumentBytes &&
|
|
105
|
+
utf8Length(row.inserted) <= maxDocumentBytes)) return false;
|
|
106
|
+
const transaction = record.transaction;
|
|
107
|
+
const nullableText = (value, limit, empty = true) => value === null ||
|
|
108
|
+
(typeof value === 'string' && value.length <= limit && (empty || value.length > 0));
|
|
109
|
+
if (!transaction || typeof transaction.id !== 'string' || !transaction.id.length ||
|
|
110
|
+
transaction.id.length > 256 || !Array.isArray(earlier) ||
|
|
111
|
+
earlier.some(row => row?.transaction?.id === transaction.id) ||
|
|
112
|
+
typeof transaction.documentAuthority !== 'string' ||
|
|
113
|
+
transaction.documentAuthority !== documentAuthority ||
|
|
114
|
+
(transaction.transport !== 'platform' && transaction.transport !== 'webmcp') ||
|
|
115
|
+
typeof transaction.operation !== 'string' || !transaction.operation.length ||
|
|
116
|
+
transaction.operation.length > _RAPIER_TRANSACTION_OPERATION_LIMIT ||
|
|
117
|
+
!nullableText(transaction.requestId, _RAPIER_TRANSACTION_REQUEST_LIMIT) ||
|
|
118
|
+
!nullableText(transaction.sourceTransactionId, _RAPIER_TRANSACTION_REQUEST_LIMIT) ||
|
|
119
|
+
!nullableText(transaction.parent, 256, false) ||
|
|
120
|
+
!nullableText(transaction.reverts, 256, false) ||
|
|
121
|
+
!nullableText(transaction.reapplies, 256, false) ||
|
|
122
|
+
!Number.isSafeInteger(transaction.createdAt) || transaction.createdAt < 0 ||
|
|
123
|
+
!Array.isArray(transaction.affectedBlockIds) || transaction.affectedBlockIds.length > 64 ||
|
|
124
|
+
!transaction.affectedBlockIds.every(id => Number.isSafeInteger(id) && id >= 0) ||
|
|
125
|
+
!Number.isSafeInteger(transaction.revision) || transaction.revision < 1 ||
|
|
126
|
+
!Number.isSafeInteger(transaction.baseRevision) ||
|
|
127
|
+
transaction.baseRevision + 1 !== transaction.revision ||
|
|
128
|
+
(prior && (transaction.revision <= prior.transaction.revision ||
|
|
129
|
+
(transaction.baseRevision === prior.transaction.revision
|
|
130
|
+
? (transaction.parent !== prior.transaction.id || record.beforeHash !== prior.afterHash)
|
|
131
|
+
: (!allowGap || transaction.baseRevision <= prior.transaction.revision))))) return false;
|
|
132
|
+
return !!transaction.actor && typeof transaction.actor.kind === 'string' &&
|
|
133
|
+
/^(?:human|agent|system)$/.test(transaction.actor.kind) &&
|
|
134
|
+
typeof transaction.actor.id === 'string' && transaction.actor.id.length > 0 &&
|
|
135
|
+
transaction.actor.id.length <= _RAPIER_TRANSACTION_ACTOR_LIMIT;
|
|
136
|
+
} catch (_) { return false; }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { _RAPIER_TRANSACTION_ACTOR_LIMIT, _RAPIER_TRANSACTION_OPERATION_LIMIT, _RAPIER_TRANSACTION_REQUEST_LIMIT, _rapierTransformSplices, _rapierRecordSplices, _rapierValidLedgerRecord, _rapierJournalRecord};
|