@factoidal/core 0.19.0 → 0.21.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 +65 -0
- package/README.md +59 -15
- package/authz/server.mjs +23 -2
- package/bin/engine.mjs +22 -1
- package/bin/factoidal.mjs +710 -38
- package/bin/pack-host.mjs +5 -0
- package/bin/serve.mjs +19 -3
- package/browser.d.ts +23 -0
- package/browser.js +26 -0
- package/index.d.ts +463 -24
- package/index.mjs +29 -0
- package/l4-assets/l4factoidal.js +1 -1
- package/l4-assets/l4factoidal.wasm +0 -0
- package/l4-assets/version.json +6 -6
- package/lib/api.mjs +480 -28
- package/lib/browser-engine.mjs +28 -1
- package/lib/cid-node.mjs +77 -0
- package/lib/cid.mjs +146 -0
- package/lib/cipher-host.mjs +126 -0
- package/lib/dataset.mjs +96 -12
- package/lib/engine-integrity.mjs +56 -0
- package/lib/lean-entry.mjs +30 -12
- package/lib/nodes.mjs +318 -17
- package/lib/sbm11-lws.mjs +4 -2
- package/lib/sbm11.mjs +29 -6
- package/lib/version.mjs +1 -1
- package/lws/README.md +9 -0
- package/lws/client.mjs +19 -2
- package/lws/persist.mjs +169 -0
- package/lws/server.mjs +82 -29
- package/package.json +2 -1
- package/solid/server/index.mjs +79 -13
- package/webdav/server.mjs +154 -25
- package/xmpp/client.mjs +332 -0
- package/xmpp/proxy.mjs +114 -0
package/lib/nodes.mjs
CHANGED
|
@@ -38,6 +38,22 @@
|
|
|
38
38
|
// {"$nquads": its N-Quads text}. With an older engine, records carry no
|
|
39
39
|
// settings and the evaluator's output wire is the registry's.
|
|
40
40
|
//
|
|
41
|
+
// Graphs (https://github.com/danbri/factoidal/issues/1130, stage 1b):
|
|
42
|
+
// graph({inputs, nodes}) is a pipeline whose nodes read named input ports,
|
|
43
|
+
// each port from a graph input or from the output of an earlier node, so a
|
|
44
|
+
// node can read two values (datasetMerge) and one value can feed two nodes.
|
|
45
|
+
// The engine decides the check (op graphCheckNodes, formal/lean4/Wasm/Ops/
|
|
46
|
+
// GraphNodes.lean, computed by graphIssues of formal/lean4/L4Factoidal/
|
|
47
|
+
// Dataflow/Graph.lean): unknown graph inputs, references to the node itself
|
|
48
|
+
// or to a later node, the number of sources per node, and the wires. graph()
|
|
49
|
+
// refuses a graph with an issue. The engine also gives each node's ports and
|
|
50
|
+
// output wire, the union of the named assumptions (the premises of
|
|
51
|
+
// Graph.guarantee_sound) and the stages with more than one port (node()
|
|
52
|
+
// reads a port stage such as datasetMerge from that op, as stageList has one
|
|
53
|
+
// input wire per stage). PORT_RUNNERS binds a port stage to the package
|
|
54
|
+
// function that runs it; tools/runners-gate.py checks it against the Lean
|
|
55
|
+
// list portStageNames. This file makes no RDF decision and no graph check.
|
|
56
|
+
//
|
|
41
57
|
// ES module, no Node built-ins: the browser loads this file as it is.
|
|
42
58
|
|
|
43
59
|
import { FactoidalError } from './errors.mjs';
|
|
@@ -49,7 +65,8 @@ import { refusalFor, makeRecord, summarize, unionAssumptions, unionLists } from
|
|
|
49
65
|
*/
|
|
50
66
|
export function buildNodes(deps) {
|
|
51
67
|
const { entry, entryResult, requireEntryFn, Dataset,
|
|
52
|
-
parse, closure, shaclValidate, query, serialize, canonicalize, explain, csvwToRdf
|
|
68
|
+
parse, closure, shaclValidate, query, serialize, canonicalize, canonicalHash, explain, csvwToRdf,
|
|
69
|
+
datasetMerge } = deps;
|
|
53
70
|
|
|
54
71
|
const reader = (format) => async (wire, params) => {
|
|
55
72
|
if (typeof wire.graph !== 'string') {
|
|
@@ -62,13 +79,18 @@ export function buildNodes(deps) {
|
|
|
62
79
|
graph: await closure(wire.graph, regime, params),
|
|
63
80
|
artefact: { regime },
|
|
64
81
|
});
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
// A writer's output wire is String: the text goes on the wire, so the next
|
|
83
|
+
// node (a reader, say) reads it. Before 0.20.0 the input Dataset stayed on
|
|
84
|
+
// the wire and only the artefact held the text.
|
|
85
|
+
const writer = (format) => async (wire, params) => {
|
|
86
|
+
const text = await serialize(wire.graph, { ...params, format });
|
|
87
|
+
return { graph: text, artefact: { format, text } };
|
|
88
|
+
};
|
|
68
89
|
|
|
69
90
|
// Stage name (stageList `name`) -> the package function that runs it.
|
|
70
|
-
// A runner answers `{ graph
|
|
71
|
-
//
|
|
91
|
+
// A runner answers `{ graph, artefact }`: `graph` is the value of the
|
|
92
|
+
// node's output wire (FactoidalNode.wireOut, the engine's), which run()
|
|
93
|
+
// checks (wireHolds); `artefact` goes in the evidence record.
|
|
72
94
|
const RUNNERS = {
|
|
73
95
|
ntriplesDocumentReader: reader('ntriples'),
|
|
74
96
|
nquadsDocumentReader: reader('nquads'),
|
|
@@ -83,23 +105,35 @@ export function buildNodes(deps) {
|
|
|
83
105
|
const { shapes, ...opts } = params;
|
|
84
106
|
if (shapes === undefined) throw new TypeError('node shaclValidator: params.shapes is required');
|
|
85
107
|
const r = await shaclValidate(wire.graph, shapes, opts);
|
|
86
|
-
|
|
108
|
+
const report = { conforms: r.conforms, report: r.report };
|
|
109
|
+
return { graph: report, artefact: report };
|
|
87
110
|
},
|
|
111
|
+
// SELECT gives a Table (the rows), ASK an Answer (a boolean), CONSTRUCT
|
|
112
|
+
// and DESCRIBE a Dataset: the engine picks the wire from the query form
|
|
113
|
+
// (op pipelineCheckNodes), and the value here follows it.
|
|
88
114
|
shippedEvaluator: async (wire, params) => {
|
|
89
115
|
const { query: sparql, ...opts } = params;
|
|
90
116
|
if (typeof sparql !== 'string') throw new TypeError('node shippedEvaluator: params.query must be a string');
|
|
91
117
|
const answer = await query(wire.graph, sparql, opts);
|
|
92
118
|
return answer instanceof Dataset ? { graph: answer, artefact: { form: 'graph' } }
|
|
93
|
-
: { artefact: { answer } };
|
|
119
|
+
: { graph: answer, artefact: { answer } };
|
|
94
120
|
},
|
|
95
121
|
ntriplesDocumentWriter: writer('ntriples'),
|
|
96
122
|
nquadsDocumentWriter: writer('nquads'),
|
|
97
123
|
turtleDocumentWriter: writer('turtle'),
|
|
98
124
|
trigDocumentWriter: writer('trig'),
|
|
99
125
|
rdfxmlDocumentWriter: writer('rdfxml'),
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
// String × String: the canonical N-Quads text and its SHA-256 hex
|
|
127
|
+
// (RdfcContract, Dataflow/PhaseE.lean). canonicalHash is the SHA-256 of
|
|
128
|
+
// the text canonicalize answers (op datasetCanonicalHash).
|
|
129
|
+
// The input is a Dataset or text (a string or {text, format, baseIRI}:
|
|
130
|
+
// characters, not bytes; the settings `format` and `baseIRI` read it).
|
|
131
|
+
rdfcStage: async (wire, params) => {
|
|
132
|
+
const isDataset = wire.graph instanceof Dataset;
|
|
133
|
+
const canonical = await canonicalize(wire.graph, params);
|
|
134
|
+
const sha256 = await canonicalHash(wire.graph, isDataset && Object.keys(params).length === 0 ? undefined : params);
|
|
135
|
+
return { graph: [canonical, sha256], artefact: { canonical, sha256 } };
|
|
136
|
+
},
|
|
103
137
|
'CSVW (tabular data to RDF)': async (wire, params) => {
|
|
104
138
|
if (typeof wire.graph !== 'string') {
|
|
105
139
|
throw new TypeError(`node CSVW: the stage takes CSV text on its wire (got ${typeof wire.graph})`);
|
|
@@ -113,10 +147,47 @@ export function buildNodes(deps) {
|
|
|
113
147
|
if (description === undefined) {
|
|
114
148
|
throw new TypeError('node rdfsExplainGraphStage: params.description is required');
|
|
115
149
|
}
|
|
116
|
-
|
|
150
|
+
const result = await explain(wire.graph, description, { ...opts, profile: 'rdfs' });
|
|
151
|
+
return { graph: result, artefact: result };
|
|
117
152
|
},
|
|
118
153
|
};
|
|
119
154
|
|
|
155
|
+
// Port stage name (the Lean list portStageNames, formal/lean4/Wasm/Ops/
|
|
156
|
+
// GraphNodes.lean) -> the package function that runs it. A runner of a
|
|
157
|
+
// stage with more than one port gets the values of its ports, in port
|
|
158
|
+
// order, as the array `wire.graph`.
|
|
159
|
+
const PORT_RUNNERS = {
|
|
160
|
+
// RDF 1.1 Semantics section 5.2 merge: the blank nodes of the two
|
|
161
|
+
// inputs are kept apart (op datasetMerge, theorem mergeDatasets_isMerge).
|
|
162
|
+
datasetMerge: async (wire) => {
|
|
163
|
+
const [left, right] = wire.graph;
|
|
164
|
+
return { graph: await datasetMerge(left, right), artefact: { ports: ['left', 'right'] } };
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const hasRunner = (n) => Object.prototype.hasOwnProperty.call(n.ports && n.ports.length > 1
|
|
169
|
+
? PORT_RUNNERS : RUNNERS, n.stage);
|
|
170
|
+
const runnerOf = (n) => (n.ports && n.ports.length > 1 ? PORT_RUNNERS : RUNNERS)[n.stage];
|
|
171
|
+
|
|
172
|
+
// Whether `value` represents the wire type `wire` (a wire name of the
|
|
173
|
+
// engine's registry, Dataflow/Wire.lean) in this package: a Dataset for the
|
|
174
|
+
// graph wires, text for String, and so on. A wire this table does not name
|
|
175
|
+
// needs a value that is not null or undefined.
|
|
176
|
+
function wireHolds(wire, value) {
|
|
177
|
+
switch (wire) {
|
|
178
|
+
case 'String': return typeof value === 'string';
|
|
179
|
+
case 'Dataset': case 'Graph': case 'Dataset × PrefixList': return value instanceof Dataset;
|
|
180
|
+
case 'Answer': return typeof value === 'boolean';
|
|
181
|
+
case 'Table': return Array.isArray(value);
|
|
182
|
+
case 'String × String':
|
|
183
|
+
return Array.isArray(value) && value.length === 2 && value.every((v) => typeof v === 'string');
|
|
184
|
+
default: return value !== undefined && value !== null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const describe = (v) => (v instanceof Dataset ? 'a Dataset' : Array.isArray(v) ? 'an array'
|
|
189
|
+
: v === null ? 'null' : typeof v);
|
|
190
|
+
|
|
120
191
|
let stageCache = null;
|
|
121
192
|
let engine = null; // the entry stages() loaded; node() calls stages(), so pipe() has it
|
|
122
193
|
|
|
@@ -167,7 +238,57 @@ export function buildNodes(deps) {
|
|
|
167
238
|
throw new TypeError(`node: setting ${path} must be a JSON value or a Dataset (got ${typeof v})`);
|
|
168
239
|
}
|
|
169
240
|
|
|
241
|
+
// The node's own copy of its settings, taken when node() checks them: plain
|
|
242
|
+
// objects and arrays copied and frozen at every depth, a Dataset cloned. A
|
|
243
|
+
// later change to the caller's objects does not reach the node, so the
|
|
244
|
+
// settings that run are the ones the engine checked and digested.
|
|
245
|
+
function snapshot(v) {
|
|
246
|
+
if (v instanceof Dataset) return typeof v.clone === 'function' ? v.clone() : v;
|
|
247
|
+
if (Array.isArray(v)) return Object.freeze(v.map(snapshot));
|
|
248
|
+
if (v !== null && typeof v === 'object') {
|
|
249
|
+
const out = {};
|
|
250
|
+
for (const [k, x] of Object.entries(v)) if (x !== undefined) out[k] = snapshot(x);
|
|
251
|
+
return Object.freeze(out);
|
|
252
|
+
}
|
|
253
|
+
return v;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// A node's settings must still be the ones it was checked with: a Dataset
|
|
257
|
+
// in them (the clone node() made) can still be changed through
|
|
258
|
+
// node.params. run() refuses such a node instead of running settings that
|
|
259
|
+
// its evidence record would misdescribe.
|
|
260
|
+
function assertSettingsUnchanged(n) {
|
|
261
|
+
const now = JSON.stringify(marshal(n.params, 'params'));
|
|
262
|
+
if (now !== JSON.stringify(n.settingsWire)) {
|
|
263
|
+
throw new FactoidalError(`run: the settings of node ${n.stage} changed after node() checked them ` +
|
|
264
|
+
'(make a new node with node() for new settings)', { code: 'op.failed' });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
170
268
|
const hasNodeCheck = () => !!engine && typeof engine.pipelineCheckNodes === 'function';
|
|
269
|
+
const hasGraphCheck = () => !!engine && typeof engine.graphCheckNodes === 'function';
|
|
270
|
+
|
|
271
|
+
// The engine's check of a graph request (op graphCheckNodes).
|
|
272
|
+
function graphCheck(inputs, items) {
|
|
273
|
+
return entryResult(engine.graphCheckNodes(JSON.stringify({
|
|
274
|
+
inputs,
|
|
275
|
+
nodes: items.map((it) => ({ stage: it.node.stage, settings: it.node.settingsWire, from: it.from })),
|
|
276
|
+
})), 'graph');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A node of a port stage (a stage with more than one input port), read
|
|
280
|
+
// from the engine's answer for a graph of that one node; null when the
|
|
281
|
+
// engine has no op graphCheckNodes or no port stage of this name. The
|
|
282
|
+
// answer's portCount issue (the probe gives no source) is not read.
|
|
283
|
+
function portNodeInfo(name, settingsWire) {
|
|
284
|
+
if (!hasGraphCheck()) return null;
|
|
285
|
+
const r = JSON.parse(engine.graphCheckNodes(JSON.stringify({
|
|
286
|
+
inputs: [], nodes: [{ stage: name, settings: settingsWire, from: [] }],
|
|
287
|
+
})));
|
|
288
|
+
if (!r.ok || !Array.isArray(r.nodes) || r.nodes.length !== 1) return null;
|
|
289
|
+
const s = r.nodes[0];
|
|
290
|
+
return s.ports.length > 1 ? s : null;
|
|
291
|
+
}
|
|
171
292
|
|
|
172
293
|
// The engine's check of nodes with settings (op pipelineCheckNodes).
|
|
173
294
|
function nodeCheck(items) {
|
|
@@ -189,11 +310,38 @@ export function buildNodes(deps) {
|
|
|
189
310
|
throw new TypeError('node: params must be an object');
|
|
190
311
|
}
|
|
191
312
|
const info = (await stages()).find((s) => s.name === name);
|
|
313
|
+
// The copy first, then its wire form: the engine checks and digests
|
|
314
|
+
// exactly the settings the node keeps.
|
|
315
|
+
const own = snapshot({ ...(params || {}) });
|
|
316
|
+
const settingsWire = snapshot(marshal(own, 'params'));
|
|
192
317
|
if (!info) {
|
|
193
|
-
|
|
318
|
+
const p = portNodeInfo(name, settingsWire);
|
|
319
|
+
if (!p) {
|
|
320
|
+
throw new FactoidalError(`node: no registered stage named '${name}' (see stages(); ` +
|
|
321
|
+
'an engine with the op graphCheckNodes also has the stages with more than one port)',
|
|
194
322
|
{ code: 'option.invalid-value' });
|
|
323
|
+
}
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
[NODE]: true,
|
|
326
|
+
stage: p.name,
|
|
327
|
+
kind: p.kind,
|
|
328
|
+
status: p.status,
|
|
329
|
+
registryStatus: p.registryStatus,
|
|
330
|
+
contract: p.contract,
|
|
331
|
+
assumptions: Object.freeze([...p.assumptions]),
|
|
332
|
+
ports: Object.freeze(p.ports.map((x) => Object.freeze({ ...x }))),
|
|
333
|
+
wireIn: null,
|
|
334
|
+
wireOut: p.wireOut,
|
|
335
|
+
runnable: p.status !== 'missing',
|
|
336
|
+
bound: Object.prototype.hasOwnProperty.call(PORT_RUNNERS, p.name),
|
|
337
|
+
params: own,
|
|
338
|
+
settingsWire,
|
|
339
|
+
settings: p.settings,
|
|
340
|
+
settingsDigest: p.settingsDigest,
|
|
341
|
+
outside: Object.freeze(p.outside.map((o) => Object.freeze({ ...o }))),
|
|
342
|
+
derived: Object.freeze({ ...p.derived }),
|
|
343
|
+
});
|
|
195
344
|
}
|
|
196
|
-
const settingsWire = marshal({ ...(params || {}) }, 'params');
|
|
197
345
|
// The engine's reading of the settings, when it has the op.
|
|
198
346
|
let s = null;
|
|
199
347
|
if (hasNodeCheck()) s = nodeCheck([{ stage: info.name, settingsWire }]).stages[0];
|
|
@@ -205,11 +353,12 @@ export function buildNodes(deps) {
|
|
|
205
353
|
registryStatus: info.status,
|
|
206
354
|
contract: info.contract,
|
|
207
355
|
assumptions: info.assumptions,
|
|
356
|
+
ports: Object.freeze([Object.freeze({ name: 'in', wire: info.wireIn })]),
|
|
208
357
|
wireIn: info.wireIn,
|
|
209
358
|
wireOut: s ? s.wireOut : info.wireOut,
|
|
210
359
|
runnable: info.runnable,
|
|
211
360
|
bound: info.bound,
|
|
212
|
-
params:
|
|
361
|
+
params: own,
|
|
213
362
|
settingsWire,
|
|
214
363
|
settings: s ? s.settings : null,
|
|
215
364
|
settingsDigest: s ? s.settingsDigest : null,
|
|
@@ -221,6 +370,10 @@ export function buildNodes(deps) {
|
|
|
221
370
|
function nodesOf(items, who) {
|
|
222
371
|
const out = [];
|
|
223
372
|
for (const n of items) {
|
|
373
|
+
if (n && n[NODE] && n.ports && n.ports.length > 1) {
|
|
374
|
+
throw new TypeError(`${who}: node ${n.stage} has ${n.ports.length} input ports ` +
|
|
375
|
+
`(${n.ports.map((x) => x.name).join(', ')}); a list pipeline gives each node one input; use graph()`);
|
|
376
|
+
}
|
|
224
377
|
if (n && n[NODE]) out.push(n);
|
|
225
378
|
else if (n && Array.isArray(n.nodes)) out.push(...nodesOf(n.nodes, who));
|
|
226
379
|
else throw new TypeError(`${who}: expected nodes from node() or pipelines from pipe()`);
|
|
@@ -296,10 +449,12 @@ export function buildNodes(deps) {
|
|
|
296
449
|
throw new TypeError('run: options must be an object');
|
|
297
450
|
}
|
|
298
451
|
const unchecked = !!(options && options.unchecked);
|
|
452
|
+
if (pipeline && pipeline[GRAPH]) return runGraph(pipeline, input, unchecked);
|
|
299
453
|
const nodes = nodesOf([pipeline], 'run');
|
|
300
454
|
for (const n of nodes) {
|
|
301
|
-
const why = refusalFor(n, unchecked,
|
|
455
|
+
const why = refusalFor(n, unchecked, hasRunner(n));
|
|
302
456
|
if (why) throw why;
|
|
457
|
+
assertSettingsUnchanged(n);
|
|
303
458
|
}
|
|
304
459
|
const c = check(nodes);
|
|
305
460
|
if (c && !c.wellTyped && !unchecked) {
|
|
@@ -314,6 +469,16 @@ export function buildNodes(deps) {
|
|
|
314
469
|
let owned = null; // a Dataset an earlier node of this run made
|
|
315
470
|
for (const n of nodes) {
|
|
316
471
|
const out = await RUNNERS[n.stage](wire, n.params);
|
|
472
|
+
// The value on the output wire must be one of the node's wireOut, the
|
|
473
|
+
// wire the engine's check typed: a well-typed pipeline then cannot fail
|
|
474
|
+
// between nodes for want of the value the next node reads. The output
|
|
475
|
+
// wires per setting are the engine's when it has op pipelineCheckNodes;
|
|
476
|
+
// with an older engine the registry wire is generic, so not checked.
|
|
477
|
+
const value = out.graph === undefined ? wire.graph : out.graph;
|
|
478
|
+
if (hasNodeCheck() && !wireHolds(n.wireOut, value)) {
|
|
479
|
+
throw new FactoidalError(`run: node ${n.stage} declares the output wire ${n.wireOut} ` +
|
|
480
|
+
`but its runner gave ${describe(value)}`, { code: 'op.failed' });
|
|
481
|
+
}
|
|
317
482
|
const record = makeRecord(n, out.artefact, unchecked);
|
|
318
483
|
let graph = wire.graph;
|
|
319
484
|
if (out.graph !== undefined && out.graph !== wire.graph) {
|
|
@@ -336,5 +501,141 @@ export function buildNodes(deps) {
|
|
|
336
501
|
};
|
|
337
502
|
}
|
|
338
503
|
|
|
339
|
-
|
|
504
|
+
const GRAPH = Symbol('factoidal.graph');
|
|
505
|
+
|
|
506
|
+
// The text of one issue of the engine's check (op graphCheckNodes).
|
|
507
|
+
function issueText(x) {
|
|
508
|
+
switch (x.kind) {
|
|
509
|
+
case 'unknownInput':
|
|
510
|
+
return `node ${x.node} port ${x.port} (${x.portName}) reads graph input ${x.input}, which does not exist`;
|
|
511
|
+
case 'forwardRef':
|
|
512
|
+
return `node ${x.node} port ${x.port} (${x.portName}) reads node ${x.ref}, which is not before it ` +
|
|
513
|
+
'(a cycle or a forward reference)';
|
|
514
|
+
case 'portCount':
|
|
515
|
+
return `node ${x.node} has ${x.ports} input ports and ${x.sources} sources`;
|
|
516
|
+
case 'wireMismatch':
|
|
517
|
+
return `node ${x.node} port ${x.port} (${x.portName}) reads ${x.portWire}, its source writes ${x.source}`;
|
|
518
|
+
default:
|
|
519
|
+
return JSON.stringify(x);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* A graph: nodes with named input ports (https://github.com/danbri/factoidal/issues/1130).
|
|
525
|
+
* `spec.inputs`: the wire of each graph input (a wire label, e.g. 'String';
|
|
526
|
+
* see stages() wireIn/wireOut). `spec.nodes`: in order, `{ node, from }`,
|
|
527
|
+
* where `node` comes from node() and `from` has one source per input port of
|
|
528
|
+
* the node: `{ input: k }` (graph input k) or `{ node: j }` (the output of
|
|
529
|
+
* node j, which must come before it). The engine's op graphCheckNodes checks
|
|
530
|
+
* the graph; a graph with an issue is refused (FactoidalError code
|
|
531
|
+
* op.failed, `err.issues` the engine's list). An engine without the op is
|
|
532
|
+
* refused with code op.unknown. The last node's output is the graph's output.
|
|
533
|
+
* @param {{inputs?: string[], nodes: Array<{node: object, from: object[]}>}} spec
|
|
534
|
+
* @returns {object}
|
|
535
|
+
*/
|
|
536
|
+
function graph(spec) {
|
|
537
|
+
if (spec === null || typeof spec !== 'object') throw new TypeError('graph: spec must be an object');
|
|
538
|
+
const inputs = spec.inputs === undefined ? [] : spec.inputs;
|
|
539
|
+
if (!Array.isArray(inputs) || !inputs.every((w) => typeof w === 'string')) {
|
|
540
|
+
throw new TypeError('graph: spec.inputs must be an array of wire labels (strings)');
|
|
541
|
+
}
|
|
542
|
+
if (!Array.isArray(spec.nodes) || spec.nodes.length === 0) {
|
|
543
|
+
throw new TypeError('graph: spec.nodes must be a non-empty array');
|
|
544
|
+
}
|
|
545
|
+
const items = spec.nodes.map((it, i) => {
|
|
546
|
+
if (!it || typeof it !== 'object' || !it.node || !it.node[NODE]) {
|
|
547
|
+
throw new TypeError(`graph: spec.nodes[${i}].node must be a node from node()`);
|
|
548
|
+
}
|
|
549
|
+
if (!Array.isArray(it.from)) throw new TypeError(`graph: spec.nodes[${i}].from must be an array`);
|
|
550
|
+
return Object.freeze({ node: it.node, from: snapshot(marshal(it.from, `nodes[${i}].from`)) });
|
|
551
|
+
});
|
|
552
|
+
requireEntryFn(engine || {}, 'graphCheckNodes', 'graph');
|
|
553
|
+
const r = graphCheck(inputs, items);
|
|
554
|
+
if (r.issues.length > 0) {
|
|
555
|
+
const err = new FactoidalError(`graph: the engine's check (op graphCheckNodes) found ` +
|
|
556
|
+
`${r.issues.length} issue${r.issues.length === 1 ? '' : 's'}: ${r.issues.map(issueText).join('; ')}`,
|
|
557
|
+
{ code: 'op.failed' });
|
|
558
|
+
err.issues = Object.freeze(r.issues.map((x) => Object.freeze({ ...x })));
|
|
559
|
+
throw err;
|
|
560
|
+
}
|
|
561
|
+
const check = Object.freeze({
|
|
562
|
+
issues: Object.freeze([]),
|
|
563
|
+
wireOut: r.wireOut,
|
|
564
|
+
assumptions: Object.freeze([...r.assumptions]),
|
|
565
|
+
contract: Object.freeze({ ...r.contract }),
|
|
566
|
+
nodes: Object.freeze(r.nodes.map((s) => Object.freeze({
|
|
567
|
+
...s,
|
|
568
|
+
theorems: Object.freeze([...s.theorems]),
|
|
569
|
+
assumptions: Object.freeze([...s.assumptions]),
|
|
570
|
+
ports: Object.freeze(s.ports.map((x) => Object.freeze({ ...x }))),
|
|
571
|
+
outside: Object.freeze(s.outside.map((o) => Object.freeze({ ...o }))),
|
|
572
|
+
}))),
|
|
573
|
+
});
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
[GRAPH]: true,
|
|
576
|
+
inputs: Object.freeze([...inputs]),
|
|
577
|
+
nodes: Object.freeze(items),
|
|
578
|
+
wireOut: r.wireOut,
|
|
579
|
+
assumptions: check.assumptions,
|
|
580
|
+
check,
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// run() of a graph: the nodes in order, each on the values of its sources.
|
|
585
|
+
async function runGraph(g, inputs, unchecked) {
|
|
586
|
+
if (!Array.isArray(inputs)) {
|
|
587
|
+
throw new TypeError('run: the input of a graph is an array, one value per graph input');
|
|
588
|
+
}
|
|
589
|
+
if (inputs.length !== g.inputs.length) {
|
|
590
|
+
throw new TypeError(`run: the graph has ${g.inputs.length} inputs, got ${inputs.length} values`);
|
|
591
|
+
}
|
|
592
|
+
for (const it of g.nodes) {
|
|
593
|
+
const why = refusalFor(it.node, unchecked, hasRunner(it.node));
|
|
594
|
+
if (why) throw why;
|
|
595
|
+
assertSettingsUnchanged(it.node);
|
|
596
|
+
}
|
|
597
|
+
// A graph input given as a wire value {graph, evidence} keeps its records.
|
|
598
|
+
const prior = [];
|
|
599
|
+
const inputValues = inputs.map((x, k) => {
|
|
600
|
+
const v = isWire(x) ? x.graph : x;
|
|
601
|
+
if (isWire(x)) prior.push(...x.evidence);
|
|
602
|
+
if (!wireHolds(g.inputs[k], v)) {
|
|
603
|
+
throw new FactoidalError(`run: graph input ${k} is declared ${g.inputs[k]}, got ${describe(v)}`,
|
|
604
|
+
{ code: 'op.failed' });
|
|
605
|
+
}
|
|
606
|
+
return v;
|
|
607
|
+
});
|
|
608
|
+
const values = [];
|
|
609
|
+
const records = [];
|
|
610
|
+
for (let i = 0; i < g.nodes.length; i++) {
|
|
611
|
+
const { node: n, from } = g.nodes[i];
|
|
612
|
+
// graph() refused every graph whose sources are not all before the node.
|
|
613
|
+
const vals = from.map((src) => (Object.prototype.hasOwnProperty.call(src, 'input')
|
|
614
|
+
? inputValues[src.input] : values[src.node]));
|
|
615
|
+
const wire = { graph: n.ports.length > 1 ? vals : vals[0], evidence: [] };
|
|
616
|
+
const out = await runnerOf(n)(wire, n.params);
|
|
617
|
+
const value = out.graph;
|
|
618
|
+
const wireOut = g.check.nodes[i].wireOut;
|
|
619
|
+
if (!wireHolds(wireOut, value)) {
|
|
620
|
+
throw new FactoidalError(`run: node ${i} (${n.stage}) declares the output wire ${wireOut} ` +
|
|
621
|
+
`but its runner gave ${describe(value)}`, { code: 'op.failed' });
|
|
622
|
+
}
|
|
623
|
+
values.push(value);
|
|
624
|
+
records.push(Object.freeze({ ...makeRecord(n, out.artefact, unchecked), node: i, from }));
|
|
625
|
+
}
|
|
626
|
+
const s = summarize(records);
|
|
627
|
+
return {
|
|
628
|
+
graph: values[values.length - 1],
|
|
629
|
+
values,
|
|
630
|
+
evidence: [...prior, ...records],
|
|
631
|
+
assumptions: prior.length === 0 ? [...g.check.assumptions]
|
|
632
|
+
: unionLists([unionAssumptions(prior), g.check.assumptions]),
|
|
633
|
+
proved: s.proved && summarize(prior).proved,
|
|
634
|
+
uncheckedSteps: [...summarize(prior).uncheckedSteps, ...s.uncheckedSteps],
|
|
635
|
+
wellTyped: true,
|
|
636
|
+
issues: [],
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return { stages, node, pipe, graph, run };
|
|
340
641
|
}
|
package/lib/sbm11-lws.mjs
CHANGED
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
// representation: it is kept in memory and every later read of that file is
|
|
18
18
|
// cut from it, with no further request. A changed entity tag between two
|
|
19
19
|
// reads of one file refuses the read (the bytes of two versions are never
|
|
20
|
-
// mixed).
|
|
21
|
-
//
|
|
20
|
+
// mixed). The engine checks every byte it decodes against the manifest's
|
|
21
|
+
// Merkle roots (openSbm11's default `verify: 'merkle'`), so it asks for whole
|
|
22
|
+
// 65,536-byte chunks and the `.merkle` sidecars; this file does no check.
|
|
23
|
+
// Limits: no eviction of the in-memory copies, no retry.
|
|
22
24
|
//
|
|
23
25
|
// EVIDENCE
|
|
24
26
|
// `evidence()` lists each resource read: { resource, etag, lastModified,
|
package/lib/sbm11.mjs
CHANGED
|
@@ -111,17 +111,33 @@ function envelope (raw) {
|
|
|
111
111
|
export function sbm11Api (loadEngine) {
|
|
112
112
|
/**
|
|
113
113
|
* Open an SBM11 generation (a generation directory, or a collection root
|
|
114
|
-
* whose CURRENT file names one). Returns { generation, terms,
|
|
115
|
-
* options), close() }. `query` answers { path, rows, format,
|
|
116
|
-
* `result` is the text `l4sbm11 query` writes (SPARQL JSON
|
|
117
|
-
* ASK, N-Triples for CONSTRUCT and DESCRIBE), `json` its
|
|
118
|
-
* SELECT and ASK.
|
|
114
|
+
* whose CURRENT file names one). Returns { generation, terms, verify,
|
|
115
|
+
* query(sparql, options), close() }. `query` answers { path, rows, format,
|
|
116
|
+
* result, json }: `result` is the text `l4sbm11 query` writes (SPARQL JSON
|
|
117
|
+
* for SELECT and ASK, N-Triples for CONSTRUCT and DESCRIBE), `json` its
|
|
118
|
+
* parse for SELECT and ASK.
|
|
119
|
+
*
|
|
120
|
+
* `options.verify`: 'merkle' (the default: the engine checks every byte
|
|
121
|
+
* it decodes against the manifest's Merkle roots, and refuses a changed
|
|
122
|
+
* file with its name) or 'none' (lengths only). The engine decides; this
|
|
123
|
+
* file passes the string.
|
|
124
|
+
*
|
|
125
|
+
* `options.manifestSha256`: the trust anchor, the SHA-256 of the bytes of
|
|
126
|
+
* `manifest.sbm11` as 64 hex digits (https://github.com/danbri/factoidal/issues/1105).
|
|
127
|
+
* The engine computes the digest and refuses the open when it differs;
|
|
128
|
+
* the error names manifest.sbm11 and both digests. `store.manifestSha256`
|
|
129
|
+
* is the digest the engine computed, so that a caller can pin it.
|
|
119
130
|
*/
|
|
120
131
|
async function openSbm11 (source, options = {}) {
|
|
121
132
|
installBridge()
|
|
122
133
|
if (options.reader) registerSbm11Source(source, options.reader)
|
|
123
134
|
const eng = await loadEngine()
|
|
124
|
-
const
|
|
135
|
+
const openArgs = [String(source)]
|
|
136
|
+
const o = {}
|
|
137
|
+
if (options.verify != null) o.verify = String(options.verify)
|
|
138
|
+
if (options.manifestSha256 != null) o.manifestSha256 = String(options.manifestSha256)
|
|
139
|
+
if (Object.keys(o).length > 0) openArgs.push(JSON.stringify(o))
|
|
140
|
+
const opened = envelope(eng.call('sbm11Open', openArgs))
|
|
125
141
|
const handle = opened.handle
|
|
126
142
|
let open = true
|
|
127
143
|
const reader = options.reader || readers.get(String(source))
|
|
@@ -135,6 +151,13 @@ export function sbm11Api (loadEngine) {
|
|
|
135
151
|
},
|
|
136
152
|
generation: opened.generation,
|
|
137
153
|
terms: Number(opened.terms),
|
|
154
|
+
/** how the engine checks reads: 'merkle' or 'none' (undefined from a
|
|
155
|
+
* wasm module built before the check) */
|
|
156
|
+
verify: opened.verify,
|
|
157
|
+
/** the SHA-256 of the bytes of manifest.sbm11 (64 lowercase hex
|
|
158
|
+
* digits), as the engine computed it at open; undefined from a wasm
|
|
159
|
+
* module built before the trust anchor (issue 1105) */
|
|
160
|
+
manifestSha256: opened.manifestSha256,
|
|
138
161
|
query (sparql, qopts = {}) {
|
|
139
162
|
if (!open) throw new Error('sbm11: the store is closed')
|
|
140
163
|
const o = {}
|
package/lib/version.mjs
CHANGED
package/lws/README.md
CHANGED
|
@@ -175,6 +175,15 @@ factoidal lws-serve /tmp/dev --port 3000 --insecure-no-auth
|
|
|
175
175
|
One of `--issuer`, `--auth-config FILE` or `--insecure-no-auth` is
|
|
176
176
|
required.
|
|
177
177
|
|
|
178
|
+
With `--insecure-no-auth` every request is served, so the storage answers
|
|
179
|
+
only its own host names (421 for any other `Host`: DNS rebinding), refuses a
|
|
180
|
+
write from another origin (403: a cross-site form or `fetch`), and sends no
|
|
181
|
+
CORS fields unless `--cors-origin ORIGIN` names a page. `--allow-host NAME`
|
|
182
|
+
and `--allow-origin ORIGIN` add names and origins (a reverse proxy, a
|
|
183
|
+
Tailscale name). The engine decides all of this
|
|
184
|
+
(`L4Factoidal/HTTP/LocalGuard.lean`,
|
|
185
|
+
https://github.com/danbri/factoidal/issues/1124).
|
|
186
|
+
|
|
178
187
|
`--port 0` takes an ephemeral port and the command prints the one it got;
|
|
179
188
|
`--base IRI` sets `baseIri`, which defaults to the bound origin. `DIR`
|
|
180
189
|
names the storage directory, but the first slice keeps the resource tree
|
package/lws/client.mjs
CHANGED
|
@@ -148,6 +148,12 @@ export async function lwsClientOpsAvailable (engine) {
|
|
|
148
148
|
* @param {string} [options.idToken] an OpenID Connect ID Token to exchange
|
|
149
149
|
* @param {object} [options.bearer] `{ token, realm? }`: an access token the
|
|
150
150
|
* caller already has; with a realm it is sent only under it
|
|
151
|
+
* @param {boolean} [options.legacyChallenge] follow the 401 challenge
|
|
152
|
+
* `Bearer as_uri="…", realm="…"` of the drafts before 2026-10-19 (default
|
|
153
|
+
* false: it skips two of the discovery checks of lws10-core)
|
|
154
|
+
* @param {boolean} [options.requireHttps] send no request to a URL whose
|
|
155
|
+
* scheme is not https, in any phase; such an operation fails with code
|
|
156
|
+
* `insecureTransport` (default false; the engine decides)
|
|
151
157
|
* @param {object} [options.session] a session from an earlier client
|
|
152
158
|
* @param {number} [options.maxSteps] the loop cap of one operation
|
|
153
159
|
* @returns {Promise<object>} the client
|
|
@@ -168,6 +174,11 @@ export async function createLwsClient (options = {}) {
|
|
|
168
174
|
// Read by the engine's session reader (`Machine.sessionOfJson`), unread here.
|
|
169
175
|
if (options.idToken !== undefined) session.idToken = options.idToken
|
|
170
176
|
if (options.bearer !== undefined) session.bearer = options.bearer
|
|
177
|
+
// Follow the `Bearer as_uri, realm` challenge of the drafts before
|
|
178
|
+
// 2026-10-19 (`Machine.on401`); the engine decides, off by default.
|
|
179
|
+
if (options.legacyChallenge === true) session.legacyChallenge = true
|
|
180
|
+
// Refuse every request to a URL that is not https (`Machine.enforce`).
|
|
181
|
+
if (options.requireHttps === true) session.requireHttps = true
|
|
171
182
|
|
|
172
183
|
function call (op, args) {
|
|
173
184
|
try {
|
|
@@ -248,7 +259,9 @@ export async function createLwsClient (options = {}) {
|
|
|
248
259
|
head: with_('head'),
|
|
249
260
|
/** `create(containerUrl, { body, mediaType, slug, container, types, links })` */
|
|
250
261
|
create: with_('create'),
|
|
251
|
-
/** `update(url, { body, mediaType, precondition, tag, retry412, links })
|
|
262
|
+
/** `update(url, { body, mediaType, precondition, tag, ifNoneMatchTags, retry412, links })`;
|
|
263
|
+
* `ifNoneMatchTags` (entity tags, or field values of them) sends
|
|
264
|
+
* `If-None-Match`, alone or with the `If-Match` of `precondition` */
|
|
252
265
|
update: with_('update'),
|
|
253
266
|
patch: with_('patch'),
|
|
254
267
|
/** `delete(url, { recursive, precondition, tag })` */
|
|
@@ -267,10 +280,14 @@ export async function createLwsClient (options = {}) {
|
|
|
267
280
|
* the service endpoint, `{ onePage: true }` for one page */
|
|
268
281
|
search: with_('search'),
|
|
269
282
|
queryFormats: with_('queryFormats'),
|
|
270
|
-
/** `subscribe(url, { topics, inbox, expires })`
|
|
283
|
+
/** `subscribe(url, { topics, inbox, expires })`, or with `{ endpoint }`
|
|
284
|
+
* when the caller has the notification service endpoint: the
|
|
285
|
+
* subscription is POSTed to it with no discovery */
|
|
271
286
|
subscribe: with_('subscribe'),
|
|
272
287
|
getSubscription: with_('getSubscription'),
|
|
273
288
|
unsubscribe: with_('unsubscribe'),
|
|
289
|
+
/** `requestAccess(url, { policies: [{ actions, assignee, targets, constraint }], inbox })`,
|
|
290
|
+
* or one policy as `{ actions, assignee, targets, constraint }` */
|
|
274
291
|
requestAccess: with_('requestAccess'),
|
|
275
292
|
grantAccess: with_('grantAccess'),
|
|
276
293
|
getAccess: with_('getAccess'),
|