@factoidal/core 0.16.0 → 0.18.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 +23 -0
- package/README.md +3 -2
- package/authz/server.mjs +17 -3
- package/bin/engine.mjs +12 -10
- package/bin/factoidal.mjs +339 -24
- package/bin/pack-host.mjs +31 -1
- package/bin/store.mjs +67 -11
- package/index.d.ts +18 -8
- 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 +34 -15
- package/lib/dataset.mjs +2 -1
- package/lib/lean-entry.mjs +40 -1
- package/lib/store.mjs +10 -8
- package/lib/version.mjs +1 -1
- package/lws/client.mjs +50 -8
- package/lws/server.mjs +34 -3
- package/package.json +6 -1
- package/webdav/server.mjs +293 -0
package/bin/store.mjs
CHANGED
|
@@ -123,7 +123,7 @@ export const STACK_REMEDY = {
|
|
|
123
123
|
export function openStore (root, generationName = null) {
|
|
124
124
|
if (typeof generationName !== 'string') {
|
|
125
125
|
const opened = openCollection(root)
|
|
126
|
-
return { ...opened,
|
|
126
|
+
return withManifestHex({ ...opened, activated: true })
|
|
127
127
|
}
|
|
128
128
|
const generationDir = joinPath(root, generationName)
|
|
129
129
|
for (const manifestName of MANIFEST_NAMES) {
|
|
@@ -133,23 +133,67 @@ export function openStore (root, generationName = null) {
|
|
|
133
133
|
} catch (_error) {
|
|
134
134
|
continue
|
|
135
135
|
}
|
|
136
|
-
return {
|
|
136
|
+
return withManifestHex({
|
|
137
137
|
root,
|
|
138
138
|
generation: generationName,
|
|
139
139
|
generationDir,
|
|
140
140
|
manifestName,
|
|
141
141
|
manifest,
|
|
142
|
-
manifestHex: hexOfBytes(manifest),
|
|
143
142
|
activated: false
|
|
144
|
-
}
|
|
143
|
+
})
|
|
145
144
|
}
|
|
146
145
|
throw new StoreOperationError(
|
|
147
146
|
`${generationDir} has none of ${MANIFEST_NAMES.join(', ')}`)
|
|
148
147
|
}
|
|
149
148
|
|
|
149
|
+
/**
|
|
150
|
+
* `store.manifestHex`, the hexadecimal the older manifest ops take, computed
|
|
151
|
+
* on first read. The byte ops (`hasManifestBytesOps`) do not read it, so a
|
|
152
|
+
* store opened for them never builds the 1.5 million digits of a large
|
|
153
|
+
* manifest (https://github.com/danbri/factoidal/issues/1039).
|
|
154
|
+
*/
|
|
155
|
+
function withManifestHex (store) {
|
|
156
|
+
let hex = null
|
|
157
|
+
return Object.defineProperty(store, 'manifestHex', {
|
|
158
|
+
enumerable: true,
|
|
159
|
+
get () {
|
|
160
|
+
if (hex === null) hex = hexOfBytes(store.manifest)
|
|
161
|
+
return hex
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The byte forms of the manifest ops (formal/lean4/Wasm/Ops/StoreHandles.lean). */
|
|
167
|
+
const MANIFEST_BYTES_OPS = ['storeManifestInspectBytes', 'storeQueryPlanBytes', 'storeOpenBytes']
|
|
168
|
+
const manifestBytesSupport = new WeakMap()
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whether the engine serves the byte forms of the manifest ops. They take
|
|
172
|
+
* the manifest as the byte region, not as hexadecimal inside the JSON
|
|
173
|
+
* arguments, and the engine decodes it once for a plan and the open that
|
|
174
|
+
* follows. An engine built before them has only the hexadecimal ops, and
|
|
175
|
+
* this host then uses those, as before.
|
|
176
|
+
*/
|
|
177
|
+
function hasManifestBytesOps (engine) {
|
|
178
|
+
let known = manifestBytesSupport.get(engine)
|
|
179
|
+
if (known === undefined) {
|
|
180
|
+
known = false
|
|
181
|
+
if (typeof engine.callBlobIO === 'function') {
|
|
182
|
+
const reflection = engine.call('ops', [])
|
|
183
|
+
const blobIoOps = Array.isArray(reflection && reflection.blobIoOps) ? reflection.blobIoOps : []
|
|
184
|
+
known = MANIFEST_BYTES_OPS.every((op) => blobIoOps.includes(op))
|
|
185
|
+
}
|
|
186
|
+
manifestBytesSupport.set(engine, known)
|
|
187
|
+
}
|
|
188
|
+
return known
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
/** `storeManifestInspect` — decode one manifest. */
|
|
151
192
|
export function inspectManifest (engine, store) {
|
|
152
193
|
try {
|
|
194
|
+
if (hasManifestBytesOps(engine)) {
|
|
195
|
+
return engine.callBlobIO('storeManifestInspectBytes', [], store.manifest).envelope
|
|
196
|
+
}
|
|
153
197
|
return engine.call('storeManifestInspect', [store.manifestHex])
|
|
154
198
|
} catch (error) {
|
|
155
199
|
throw asStoreError(error)
|
|
@@ -159,6 +203,9 @@ export function inspectManifest (engine, store) {
|
|
|
159
203
|
/** `storeQueryPlan` — the artifact keys this query needs, and the mode. */
|
|
160
204
|
export function planQuery (engine, store, sparql) {
|
|
161
205
|
try {
|
|
206
|
+
if (hasManifestBytesOps(engine)) {
|
|
207
|
+
return engine.callBlobIO('storeQueryPlanBytes', [sparql], store.manifest).envelope
|
|
208
|
+
}
|
|
162
209
|
return engine.call('storeQueryPlan', [store.manifestHex, sparql])
|
|
163
210
|
} catch (error) {
|
|
164
211
|
throw asStoreError(error)
|
|
@@ -194,16 +241,20 @@ function capDecision (engine, store, sparql) {
|
|
|
194
241
|
}
|
|
195
242
|
}
|
|
196
243
|
|
|
197
|
-
/** Read the named artifacts and concatenate them into one region.
|
|
198
|
-
|
|
244
|
+
/** Read the named artifacts and concatenate them into one region. With a
|
|
245
|
+
* `prefix` (the manifest, for `storeOpenBytes`) the region starts with it,
|
|
246
|
+
* and the windows still count from the first artifact byte. */
|
|
247
|
+
function regionOf (store, keys, prefix = null) {
|
|
199
248
|
const chunks = keys.map((key) => readWhole(joinPath(store.generationDir, key)))
|
|
249
|
+
const start = prefix ? prefix.length : 0
|
|
200
250
|
let total = 0
|
|
201
251
|
for (const chunk of chunks) total += chunk.length
|
|
202
|
-
const blob = new Uint8Array(total)
|
|
252
|
+
const blob = new Uint8Array(start + total)
|
|
253
|
+
if (prefix) blob.set(prefix, 0)
|
|
203
254
|
const artifacts = []
|
|
204
255
|
let offset = 0
|
|
205
256
|
for (let index = 0; index < chunks.length; index += 1) {
|
|
206
|
-
blob.set(chunks[index], offset)
|
|
257
|
+
blob.set(chunks[index], start + offset)
|
|
207
258
|
artifacts.push({ key: keys[index], offset, len: chunks[index].length })
|
|
208
259
|
offset += chunks[index].length
|
|
209
260
|
}
|
|
@@ -374,11 +425,16 @@ export function openStoreHandle (engine, store, options = {}) {
|
|
|
374
425
|
} else {
|
|
375
426
|
keys = manifestKeys(engine, store)
|
|
376
427
|
}
|
|
377
|
-
const
|
|
428
|
+
const bytesOps = hasManifestBytesOps(engine)
|
|
429
|
+
// The byte op takes the manifest as the first bytes of the region.
|
|
430
|
+
const { blob, artifacts } = regionOf(store, keys, bytesOps ? store.manifest : null)
|
|
378
431
|
let envelope
|
|
379
432
|
try {
|
|
380
|
-
envelope =
|
|
381
|
-
|
|
433
|
+
envelope = bytesOps
|
|
434
|
+
? engine.callBlobIO('storeOpenBytes',
|
|
435
|
+
[String(store.manifest.length), JSON.stringify(artifacts)], blob).envelope
|
|
436
|
+
: engine.callBlobIO('storeOpen',
|
|
437
|
+
[store.manifestHex, JSON.stringify(artifacts)], blob).envelope
|
|
382
438
|
} catch (error) {
|
|
383
439
|
throw asStoreError(error)
|
|
384
440
|
}
|
package/index.d.ts
CHANGED
|
@@ -212,6 +212,9 @@ export interface WriteOptions {
|
|
|
212
212
|
useRdfType?: boolean;
|
|
213
213
|
/** 'jsonld' only. */
|
|
214
214
|
rdfDirection?: string;
|
|
215
|
+
/** 'jsonld' only: write RDF 1.2 triple terms as JSON-LD-star (`@triple`,
|
|
216
|
+
* `@reifies`, `@annotation`). Without it a triple term is refused. */
|
|
217
|
+
rdfstar?: boolean;
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
export type WriteFormat =
|
|
@@ -529,14 +532,15 @@ export function canonicalize(
|
|
|
529
532
|
export function graphs(dataset: Dataset): Array<[name: NamedNode | BlankNode, graph: Dataset]>;
|
|
530
533
|
|
|
531
534
|
/**
|
|
532
|
-
* RDFC-1.0 canonical
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
535
|
+
* The RDFC-1.0 canonical hash of a single graph: the SHA-256 of its
|
|
536
|
+
* canonical N-Quads document, as 64 lowercase hexadecimal digits (RDF
|
|
537
|
+
* Dataset Canonicalization 1.0, section 4.4 note 2). The engine computes
|
|
538
|
+
* the canonical form and the digest. Every quad's graph component is
|
|
539
|
+
* dropped first, so isomorphic graphs (including under blank-node
|
|
540
|
+
* relabeling) give the same hash whatever graph name they were extracted
|
|
541
|
+
* from. Typically called with one entry of graphs()'s output. For the
|
|
542
|
+
* canonical text, use canonicalize(). Rejects when the input holds an
|
|
543
|
+
* RDF 1.2 triple term or the section 4.4 call budget is exceeded.
|
|
540
544
|
*/
|
|
541
545
|
export function canonicalHash(datasetOrGraph: Dataset): Promise<string>;
|
|
542
546
|
|
|
@@ -1010,6 +1014,9 @@ export interface JsonLdOptions {
|
|
|
1010
1014
|
rdfDirection?: string;
|
|
1011
1015
|
expandContext?: string;
|
|
1012
1016
|
processingMode?: string;
|
|
1017
|
+
/** JSON-LD-star: read `@triple`, `@reifies`, `@annotation` as RDF 1.2
|
|
1018
|
+
* triple terms and reifiers. */
|
|
1019
|
+
rdfstar?: boolean;
|
|
1013
1020
|
}
|
|
1014
1021
|
|
|
1015
1022
|
/**
|
|
@@ -1026,6 +1033,9 @@ export function jsonldToRdf(
|
|
|
1026
1033
|
export interface JsonLdFromRdfOptions {
|
|
1027
1034
|
useNativeTypes?: boolean;
|
|
1028
1035
|
useRdfType?: boolean;
|
|
1036
|
+
/** JSON-LD-star: write RDF 1.2 triple terms (`@triple`, `@reifies`,
|
|
1037
|
+
* `@annotation`). Without it a triple term is refused. */
|
|
1038
|
+
rdfstar?: boolean;
|
|
1029
1039
|
format?: DataFormat;
|
|
1030
1040
|
}
|
|
1031
1041
|
|
package/l4-assets/l4factoidal.js
CHANGED
|
Binary file
|
package/l4-assets/version.json
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
"engine": "lean4",
|
|
3
3
|
"note": "The Lean 4 (L4Factoidal) engine build provenance and claims: the only engine of @factoidal/core since 0.9.0 (issue 713). This committed file is a placeholder. tools/fetch-builds.sh --set web (through tools/stamp-l4-version.mjs) writes the provenance members from the danbri/factoidal-builds release that built the wasm; formal/lean4/Wasm/build-wasm.sh step 9 writes them for a local build. Do not commit a stamped copy.",
|
|
4
4
|
"stamped": true,
|
|
5
|
-
"release": "danbri/factoidal-builds build-
|
|
6
|
-
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/
|
|
7
|
-
"gitSha": "
|
|
8
|
-
"builtAt": "2026-10-
|
|
5
|
+
"release": "danbri/factoidal-builds build-788ee43aec79-web",
|
|
6
|
+
"releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40457275423",
|
|
7
|
+
"gitSha": "788ee43aec79c4d29592c5e82cc7ab350f0d05dd",
|
|
8
|
+
"builtAt": "2026-10-27T06:36:59Z",
|
|
9
9
|
"leanToolchain": "leanprover/lean4:v4.33.1",
|
|
10
10
|
"emscripten": "6.0.8",
|
|
11
11
|
"abiVersion": "1",
|
|
12
12
|
"engineVersion": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
|
|
13
13
|
"version": "l4factoidal-wasm 0.2.0 (dispatch: RDF parse/serialize, SPARQL query+update, RDFS/OWL-RL closures, RDFC-1.0, CL/IKL)",
|
|
14
|
-
"wasmSha256": "
|
|
15
|
-
"wasmBytes":
|
|
14
|
+
"wasmSha256": "925f357453dbb71d087bef70fd428b054999756d7c967a806403d3d8ee875a57",
|
|
15
|
+
"wasmBytes": 17565361,
|
|
16
16
|
"claims": {
|
|
17
17
|
"source": "formal/lean4 (L4Factoidal): no sorry, no user axioms, no native_decide; W3C behaviour pinned by build-time #guard",
|
|
18
18
|
"suitesAtBuildSha": "see docs/test-results and formal/lean4/PORT_NOTES.md at gitSha"
|
package/lib/api.mjs
CHANGED
|
@@ -182,11 +182,10 @@ function utf16Diagnostics(text, diagnostics) {
|
|
|
182
182
|
// is never one of a partial generation.
|
|
183
183
|
// ---------------------------------------------------------------------
|
|
184
184
|
class Store {
|
|
185
|
-
constructor(e, env, gen,
|
|
185
|
+
constructor(e, env, gen, fromNQuads, withExtensionRounds) {
|
|
186
186
|
this._e = e;
|
|
187
187
|
this._env = env;
|
|
188
188
|
this._gen = gen;
|
|
189
|
-
this._manifestHex = manifestHex;
|
|
190
189
|
this._closed = false;
|
|
191
190
|
this._fromNQuads = fromNQuads;
|
|
192
191
|
this._withExtensionRounds = withExtensionRounds;
|
|
@@ -236,7 +235,9 @@ class Store {
|
|
|
236
235
|
|
|
237
236
|
/** The manifest, decoded by the engine (op storeManifestInspect). */
|
|
238
237
|
async inspect() {
|
|
239
|
-
const r = entryResult(this._e.
|
|
238
|
+
const r = entryResult(typeof this._e.storeManifestInspectBytes === 'function'
|
|
239
|
+
? this._e.storeManifestInspectBytes(this._gen.manifest)
|
|
240
|
+
: this._e.storeManifestInspect(hexOfBytes(this._gen.manifest)), 'Store.inspect');
|
|
240
241
|
const { ok: _ok, ...rest } = r;
|
|
241
242
|
return rest;
|
|
242
243
|
}
|
|
@@ -841,7 +842,7 @@ function buildApi(driver) {
|
|
|
841
842
|
* @param {Dataset|string|Array} data
|
|
842
843
|
* @param {{format?: string, version?: '1.1'|'1.2', prefixes?: object,
|
|
843
844
|
* literalShorthand?: boolean, useNativeTypes?: boolean,
|
|
844
|
-
* useRdfType?: boolean, rdfDirection?: string,
|
|
845
|
+
* useRdfType?: boolean, rdfDirection?: string, rdfstar?: boolean,
|
|
845
846
|
* inputFormat?: string, baseIRI?: string}} [options]
|
|
846
847
|
* format: nquads (default), ntriples, turtle, trig, rdfxml, jsonld,
|
|
847
848
|
* rdfc. inputFormat and baseIRI read text `data`.
|
|
@@ -884,9 +885,12 @@ function buildApi(driver) {
|
|
|
884
885
|
}
|
|
885
886
|
|
|
886
887
|
/**
|
|
887
|
-
* RDFC-1.0 canonical
|
|
888
|
-
*
|
|
889
|
-
*
|
|
888
|
+
* The RDFC-1.0 canonical hash of one graph: the SHA-256, as 64 lowercase
|
|
889
|
+
* hexadecimal digits, of the canonical N-Quads document (RDF Dataset
|
|
890
|
+
* Canonicalization 1.0, section 4.4 note 2). Every quad's graph name is
|
|
891
|
+
* dropped first (op datasetGraphAsDefault with the union of the graphs);
|
|
892
|
+
* then op datasetCanonicalHash computes the canonical form and the digest
|
|
893
|
+
* in Lean. canonicalize() answers the canonical text.
|
|
890
894
|
* @param {Dataset} datasetOrGraph
|
|
891
895
|
* @returns {Promise<string>}
|
|
892
896
|
*/
|
|
@@ -897,10 +901,11 @@ function buildApi(driver) {
|
|
|
897
901
|
datasetOrGraph.size; // throws the disposed error
|
|
898
902
|
const c = datasetOrGraph._ctx;
|
|
899
903
|
requireEntryFn(c.e, 'datasetGraphAsDefault', 'canonicalHash');
|
|
904
|
+
requireEntryFn(c.e, 'datasetCanonicalHash', 'canonicalHash');
|
|
900
905
|
const merged = c.wrap(entryResult(c.e.datasetGraphAsDefault(datasetOrGraph.handle,
|
|
901
906
|
JSON.stringify({ type: 'union' })), 'canonicalHash'), {});
|
|
902
907
|
try {
|
|
903
|
-
return merged.
|
|
908
|
+
return entryResult(c.e.datasetCanonicalHash(merged.handle), 'canonicalHash').hash;
|
|
904
909
|
} finally {
|
|
905
910
|
merged.dispose();
|
|
906
911
|
}
|
|
@@ -1206,7 +1211,9 @@ function buildApi(driver) {
|
|
|
1206
1211
|
* A remote `@context` is not fetched.
|
|
1207
1212
|
* @param {string} jsonldText
|
|
1208
1213
|
* @param {{base?: string, rdfDirection?: string, expandContext?: string,
|
|
1209
|
-
* processingMode?: string}} [options]
|
|
1214
|
+
* processingMode?: string, rdfstar?: boolean}} [options]
|
|
1215
|
+
* rdfstar: JSON-LD-star (`@triple`, `@reifies`, `@annotation` as RDF 1.2
|
|
1216
|
+
* triple terms and reifiers).
|
|
1210
1217
|
* @returns {Promise<Dataset>}
|
|
1211
1218
|
*/
|
|
1212
1219
|
async function jsonldToRdf(jsonldText, options) {
|
|
@@ -1219,6 +1226,7 @@ function buildApi(driver) {
|
|
|
1219
1226
|
...(opts.rdfDirection ? { rdfDirection: opts.rdfDirection } : {}),
|
|
1220
1227
|
...(opts.expandContext ? { expandContext: opts.expandContext } : {}),
|
|
1221
1228
|
...(opts.processingMode ? { processingMode: opts.processingMode } : {}),
|
|
1229
|
+
...(opts.rdfstar ? { rdfstar: true } : {}),
|
|
1222
1230
|
});
|
|
1223
1231
|
return c.wrap(entryResult(c.e.datasetFromJsonLd(jsonldText, optionsJson), 'jsonldToRdf'), {},
|
|
1224
1232
|
{ format: 'jsonld', formatInferred: false });
|
|
@@ -1246,7 +1254,8 @@ function buildApi(driver) {
|
|
|
1246
1254
|
* The text form is `serialize(data, {format: 'jsonld'})`.
|
|
1247
1255
|
* @param {Dataset|string|Array} data
|
|
1248
1256
|
* @param {{useNativeTypes?: boolean, useRdfType?: boolean,
|
|
1249
|
-
* rdfDirection?: string,
|
|
1257
|
+
* rdfDirection?: string, rdfstar?: boolean, format?: string,
|
|
1258
|
+
* baseIRI?: string}} [options]
|
|
1250
1259
|
* @returns {Promise<any>}
|
|
1251
1260
|
*/
|
|
1252
1261
|
async function jsonldFromRdf(data, options) {
|
|
@@ -1903,20 +1912,30 @@ function buildApi(driver) {
|
|
|
1903
1912
|
const e = await entry();
|
|
1904
1913
|
requireEntryFn(e, 'storeOpen', 'openStore (storeOpen)');
|
|
1905
1914
|
const gen = await openGeneration(reader, opts.generation);
|
|
1906
|
-
|
|
1915
|
+
// The byte ops when the engine has them (issue 1039): the manifest
|
|
1916
|
+
// crosses as bytes and is decoded once for the plan and the open. An
|
|
1917
|
+
// older engine gets the hexadecimal ops, as before.
|
|
1918
|
+
const bytesOps = typeof e.storeOpenBytes === 'function';
|
|
1919
|
+
const manifestHex = bytesOps ? null : hexOfBytes(gen.manifest);
|
|
1907
1920
|
let keys;
|
|
1908
1921
|
if (Array.isArray(opts.keys)) {
|
|
1909
1922
|
keys = opts.keys.map(String);
|
|
1910
1923
|
} else if (typeof opts.sparql === 'string') {
|
|
1911
|
-
const plan = entryResult(
|
|
1924
|
+
const plan = entryResult(bytesOps
|
|
1925
|
+
? e.storeQueryPlanBytes(gen.manifest, opts.sparql)
|
|
1926
|
+
: e.storeQueryPlan(manifestHex, opts.sparql), 'openStore(plan)');
|
|
1912
1927
|
keys = plan.keys.concat(plan.sidecarKeys || [], plan.blobKeys || []);
|
|
1913
1928
|
} else {
|
|
1914
|
-
keys = manifestKeys(entryResult(
|
|
1929
|
+
keys = manifestKeys(entryResult(bytesOps
|
|
1930
|
+
? e.storeManifestInspectBytes(gen.manifest)
|
|
1931
|
+
: e.storeManifestInspect(manifestHex), 'openStore(inspect)'));
|
|
1915
1932
|
}
|
|
1916
1933
|
const { blob, artifacts } = await readRegion(reader, gen.generation, keys);
|
|
1917
|
-
const env = entryResult(
|
|
1934
|
+
const env = entryResult(bytesOps
|
|
1935
|
+
? e.storeOpenBytes(gen.manifest, JSON.stringify(artifacts), blob)
|
|
1936
|
+
: e.storeOpen(manifestHex, JSON.stringify(artifacts), blob), 'openStore');
|
|
1918
1937
|
const c = await ctx();
|
|
1919
|
-
return new Store(e, env, gen,
|
|
1938
|
+
return new Store(e, env, gen, (nq) => c.fromNQuads(nq, {}), withExtensionRounds);
|
|
1920
1939
|
}
|
|
1921
1940
|
|
|
1922
1941
|
/**
|
package/lib/dataset.mjs
CHANGED
|
@@ -279,7 +279,7 @@ class Dataset {
|
|
|
279
279
|
* The dataset as a document.
|
|
280
280
|
* @param {{format?: string, version?: '1.1'|'1.2', prefixes?: object,
|
|
281
281
|
* literalShorthand?: boolean, useNativeTypes?: boolean,
|
|
282
|
-
* useRdfType?: boolean, rdfDirection?: string}} [options]
|
|
282
|
+
* useRdfType?: boolean, rdfDirection?: string, rdfstar?: boolean}} [options]
|
|
283
283
|
* format: nquads (default), ntriples, turtle, trig, rdfxml, jsonld,
|
|
284
284
|
* rdfc (RDFC-1.0 canonical N-Quads).
|
|
285
285
|
* @returns {Promise<string>}
|
|
@@ -315,6 +315,7 @@ class Dataset {
|
|
|
315
315
|
...(opts.useNativeTypes ? { useNativeTypes: true } : {}),
|
|
316
316
|
...(opts.useRdfType ? { useRdfType: true } : {}),
|
|
317
317
|
...(opts.rdfDirection ? { rdfDirection: String(opts.rdfDirection) } : {}),
|
|
318
|
+
...(opts.rdfstar ? { rdfstar: true } : {}),
|
|
318
319
|
...(suffix ? { version: String(opts.version) } : {}),
|
|
319
320
|
})), 'serialize');
|
|
320
321
|
return r.jsonld;
|
package/lib/lean-entry.mjs
CHANGED
|
@@ -125,6 +125,9 @@ function installExtensionBridge(eng, entry) {
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** The byte forms of the manifest ops (Wasm/Ops/StoreHandles.lean). */
|
|
129
|
+
const MANIFEST_BYTES_OPS = ['storeManifestInspectBytes', 'storeQueryPlanBytes', 'storeOpenBytes'];
|
|
130
|
+
|
|
128
131
|
/**
|
|
129
132
|
* Build the entry object lib/api.mjs drives, from a loaded engine handle.
|
|
130
133
|
*
|
|
@@ -199,9 +202,45 @@ export function entryFromEngine(eng) {
|
|
|
199
202
|
}
|
|
200
203
|
};
|
|
201
204
|
}
|
|
205
|
+
const blobIoOps = Array.isArray(reflection.blobIoOps) ? reflection.blobIoOps : [];
|
|
206
|
+
// The byte forms of the manifest ops (https://github.com/danbri/factoidal/issues/1039):
|
|
207
|
+
// the manifest crosses as the byte region, not as hexadecimal inside the
|
|
208
|
+
// JSON arguments, and the engine decodes it once for a plan and the open
|
|
209
|
+
// that follows. An engine without them has only the hexadecimal ops; the
|
|
210
|
+
// methods are then absent and lib/api.mjs uses the hexadecimal ops.
|
|
211
|
+
if (typeof eng.callBlobIO === 'function' &&
|
|
212
|
+
MANIFEST_BYTES_OPS.every((op) => blobIoOps.includes(op))) {
|
|
213
|
+
entry.storeManifestInspectBytes = (manifestBytes) => {
|
|
214
|
+
try {
|
|
215
|
+
return JSON.stringify(eng.callBlobIO('storeManifestInspectBytes', [], manifestBytes).envelope);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return failEnvelope(err);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
entry.storeQueryPlanBytes = (manifestBytes, sparql) => {
|
|
221
|
+
try {
|
|
222
|
+
return JSON.stringify(
|
|
223
|
+
eng.callBlobIO('storeQueryPlanBytes', [String(sparql)], manifestBytes).envelope);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
return failEnvelope(err);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
entry.storeOpenBytes = (manifestBytes, artifactsJson, bytes) => {
|
|
229
|
+
try {
|
|
230
|
+
const region = new Uint8Array(manifestBytes.length + bytes.length);
|
|
231
|
+
region.set(manifestBytes, 0);
|
|
232
|
+
region.set(bytes, manifestBytes.length);
|
|
233
|
+
return JSON.stringify(eng.callBlobIO('storeOpenBytes',
|
|
234
|
+
[String(manifestBytes.length), String(artifactsJson)], region).envelope);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return failEnvelope(err);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
} else {
|
|
240
|
+
for (const op of MANIFEST_BYTES_OPS) delete entry[op];
|
|
241
|
+
}
|
|
202
242
|
// datasetQuadsBin answers a byte region (formal/lean4/L4Factoidal/RDF/QuadsBin.lean):
|
|
203
243
|
// this method answers {envelope, bytes} and throws on a refusal.
|
|
204
|
-
const blobIoOps = Array.isArray(reflection.blobIoOps) ? reflection.blobIoOps : [];
|
|
205
244
|
if (typeof eng.callBlobIO === 'function' && blobIoOps.includes('datasetQuadsBin')) {
|
|
206
245
|
entry.datasetQuadsBin = (handle, optsJson) => {
|
|
207
246
|
try {
|
package/lib/store.mjs
CHANGED
|
@@ -99,16 +99,18 @@ export function manifestKeys(inspectEnvelope) {
|
|
|
99
99
|
return keys;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
for (let b = 0; b < 256; b += 1) t[b] = b.toString(16).padStart(2, '0');
|
|
105
|
-
return t;
|
|
106
|
-
})();
|
|
102
|
+
const HEX_DIGIT = Uint8Array.from('0123456789abcdef', (c) => c.charCodeAt(0));
|
|
103
|
+
const HEX_DECODER = new TextDecoder();
|
|
107
104
|
|
|
108
105
|
/** Lowercase hexadecimal: the transport the manifest argument of the store
|
|
109
106
|
* ops takes. Artifact bytes never take this path. */
|
|
110
107
|
export function hexOfBytes(bytes) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
// One byte buffer of ASCII digits, decoded once (issue 1039; see
|
|
109
|
+
// bin/engine.mjs hexOfBytes).
|
|
110
|
+
const out = new Uint8Array(bytes.length * 2);
|
|
111
|
+
for (let i = 0, at = 0; i < bytes.length; i += 1, at += 2) {
|
|
112
|
+
out[at] = HEX_DIGIT[bytes[i] >> 4];
|
|
113
|
+
out[at + 1] = HEX_DIGIT[bytes[i] & 15];
|
|
114
|
+
}
|
|
115
|
+
return HEX_DECODER.decode(out);
|
|
114
116
|
}
|
package/lib/version.mjs
CHANGED
package/lws/client.mjs
CHANGED
|
@@ -38,14 +38,15 @@ import { loadEngine } from '../bin/engine.mjs'
|
|
|
38
38
|
import { FactoidalError } from '../lib/errors.mjs'
|
|
39
39
|
|
|
40
40
|
/** The ops this host needs from the engine. */
|
|
41
|
-
export const LWS_CLIENT_OPS = ['lwsClientStart', 'lwsClientStep']
|
|
41
|
+
export const LWS_CLIENT_OPS = ['lwsClientStart', 'lwsClientStep', 'lwsClientNewKey']
|
|
42
42
|
|
|
43
43
|
/** The operation kinds of the ABI. Listed so a caller can be checked
|
|
44
44
|
* against the contract; this host attaches no meaning to any of them. */
|
|
45
45
|
export const LWS_CLIENT_KINDS = [
|
|
46
|
-
'discover', 'read', 'head', 'create', 'update', 'patch', 'delete',
|
|
47
|
-
'
|
|
48
|
-
'
|
|
46
|
+
'discover', 'describe', 'read', 'readPage', 'head', 'create', 'update', 'patch', 'delete',
|
|
47
|
+
'findLinkset', 'readLinkset', 'updateLinkset', 'putLinkset', 'move', 'list', 'typeIndex',
|
|
48
|
+
'search', 'queryFormats', 'subscribe', 'getSubscription', 'unsubscribe', 'requestAccess',
|
|
49
|
+
'grantAccess', 'getAccess', 'revokeAccess', 'verifyNotification'
|
|
49
50
|
]
|
|
50
51
|
|
|
51
52
|
/** The error an operation ends with. `reason` is the engine's code
|
|
@@ -58,6 +59,9 @@ export class LwsClientError extends FactoidalError {
|
|
|
58
59
|
this.name = 'LwsClientError'
|
|
59
60
|
this.reason = reason
|
|
60
61
|
this.detail = fail
|
|
62
|
+
// The engine's name for the failure in the lws-driver/1 adapter
|
|
63
|
+
// protocol ({ kind, message, ... }), as the engine wrote it.
|
|
64
|
+
this.report = fail && typeof fail.report === 'object' ? fail.report : undefined
|
|
61
65
|
this.unknownOp = extra.unknownOp === true
|
|
62
66
|
}
|
|
63
67
|
}
|
|
@@ -138,8 +142,12 @@ export async function lwsClientOpsAvailable (engine) {
|
|
|
138
142
|
* @param {string} [options.subjectToken] the authentication credential to
|
|
139
143
|
* exchange for access tokens (RFC 8693 subject_token)
|
|
140
144
|
* @param {string} [options.subjectTokenType] its token type URI
|
|
141
|
-
* @param {object} [options.selfIssued] `{ subject, keyId, jwk }`: a P-256
|
|
142
|
-
* to sign self-issued credentials with, instead of a subject
|
|
145
|
+
* @param {object} [options.selfIssued] `{ subject, keyId, jwk }`: a P-256 or
|
|
146
|
+
* Ed25519 key to sign self-issued credentials with, instead of a subject
|
|
147
|
+
* token (`createDidKey` makes one)
|
|
148
|
+
* @param {string} [options.idToken] an OpenID Connect ID Token to exchange
|
|
149
|
+
* @param {object} [options.bearer] `{ token, realm? }`: an access token the
|
|
150
|
+
* caller already has; with a realm it is sent only under it
|
|
143
151
|
* @param {object} [options.session] a session from an earlier client
|
|
144
152
|
* @param {number} [options.maxSteps] the loop cap of one operation
|
|
145
153
|
* @returns {Promise<object>} the client
|
|
@@ -157,6 +165,9 @@ export async function createLwsClient (options = {}) {
|
|
|
157
165
|
// `{ subject, keyId, jwk }`: a P-256 key the engine signs self-issued
|
|
158
166
|
// credentials with (lws10-authn-ssi-cid), passed through unread.
|
|
159
167
|
if (options.selfIssued !== undefined) session.selfIssued = options.selfIssued
|
|
168
|
+
// Read by the engine's session reader (`Machine.sessionOfJson`), unread here.
|
|
169
|
+
if (options.idToken !== undefined) session.idToken = options.idToken
|
|
170
|
+
if (options.bearer !== undefined) session.bearer = options.bearer
|
|
160
171
|
|
|
161
172
|
function call (op, args) {
|
|
162
173
|
try {
|
|
@@ -230,7 +241,10 @@ export async function createLwsClient (options = {}) {
|
|
|
230
241
|
/** The session (access tokens and subject token), for a later client. */
|
|
231
242
|
session: () => ({ ...session }),
|
|
232
243
|
discover: with_('discover'),
|
|
244
|
+
describe: with_('describe'),
|
|
245
|
+
/** `read(url, { accept, rangeStart, rangeEnd, ifNoneMatch, prefer })` */
|
|
233
246
|
read: with_('read'),
|
|
247
|
+
readPage: with_('readPage'),
|
|
234
248
|
head: with_('head'),
|
|
235
249
|
/** `create(containerUrl, { body, mediaType, slug, container, types, links })` */
|
|
236
250
|
create: with_('create'),
|
|
@@ -239,26 +253,54 @@ export async function createLwsClient (options = {}) {
|
|
|
239
253
|
patch: with_('patch'),
|
|
240
254
|
/** `delete(url, { recursive, precondition, tag })` */
|
|
241
255
|
delete: with_('delete'),
|
|
256
|
+
findLinkset: with_('findLinkset'),
|
|
242
257
|
readLinkset: with_('readLinkset'),
|
|
258
|
+
/** `updateLinkset(resourceUrl, { body })`, or with `{ linksetUrl, body }`
|
|
259
|
+
* when the caller has the linkset resource. */
|
|
243
260
|
updateLinkset: with_('updateLinkset'),
|
|
261
|
+
putLinkset: with_('putLinkset'),
|
|
244
262
|
/** `move(url, { destination })` */
|
|
245
263
|
move: with_('move'),
|
|
246
264
|
list: with_('list'),
|
|
247
265
|
typeIndex: with_('typeIndex'),
|
|
248
|
-
/** `search(anyUrlInTheStorage, { query })`
|
|
266
|
+
/** `search(anyUrlInTheStorage, { query })`, or `{ endpoint, query }` with
|
|
267
|
+
* the service endpoint, `{ onePage: true }` for one page */
|
|
249
268
|
search: with_('search'),
|
|
269
|
+
queryFormats: with_('queryFormats'),
|
|
270
|
+
/** `subscribe(url, { topics, inbox, expires })` */
|
|
250
271
|
subscribe: with_('subscribe'),
|
|
272
|
+
getSubscription: with_('getSubscription'),
|
|
251
273
|
unsubscribe: with_('unsubscribe'),
|
|
252
274
|
requestAccess: with_('requestAccess'),
|
|
253
275
|
grantAccess: with_('grantAccess'),
|
|
254
276
|
getAccess: with_('getAccess'),
|
|
255
277
|
revokeAccess: with_('revokeAccess'),
|
|
256
|
-
/** `verifyNotification(baseUrl, { requestTarget, method, headers, body })`:
|
|
278
|
+
/** `verifyNotification(baseUrl, { requestTarget, method, headers, body, trustedStorages })`:
|
|
257
279
|
* the engine's verdict `{ accept, status, reason? }`. */
|
|
258
280
|
verifyNotification: with_('verifyNotification')
|
|
259
281
|
}
|
|
260
282
|
}
|
|
261
283
|
|
|
284
|
+
/**
|
|
285
|
+
* A new `did:key` agent: the engine makes the key pair and the identifier from
|
|
286
|
+
* 64 random bytes of this host's random source. The host reads none of it.
|
|
287
|
+
*
|
|
288
|
+
* @param {object} [options]
|
|
289
|
+
* @param {object} [options.engine] an engine handle to reuse
|
|
290
|
+
* @param {string} [options.algorithm] the engine's name of the algorithm
|
|
291
|
+
* (default `ES256`, a P-256 key; `EdDSA` makes an Ed25519 key)
|
|
292
|
+
* @returns {Promise<{agent: string, kid: string, selfIssued: object}>}
|
|
293
|
+
* `selfIssued` is the `createLwsClient` option of that name
|
|
294
|
+
*/
|
|
295
|
+
export async function createDidKey (options = {}) {
|
|
296
|
+
const engine = options.engine !== undefined ? options.engine : await loadEngine()
|
|
297
|
+
const algorithm = typeof options.algorithm === 'string' ? options.algorithm : 'ES256'
|
|
298
|
+
const random = new Uint8Array(64)
|
|
299
|
+
globalThis.crypto.getRandomValues(random)
|
|
300
|
+
const answer = engine.call('lwsClientNewKey', [algorithm, base64OfBytes(random)])
|
|
301
|
+
return answer.key
|
|
302
|
+
}
|
|
303
|
+
|
|
262
304
|
/**
|
|
263
305
|
* An HTTP receiver for webhook notifications. For each request it asks the
|
|
264
306
|
* engine (`verifyNotification`) and answers with the status of the
|
package/lws/server.mjs
CHANGED
|
@@ -584,11 +584,33 @@ export async function createLwsServer (options = {}) {
|
|
|
584
584
|
let settings = { ...options }
|
|
585
585
|
let opening = null
|
|
586
586
|
let opened = null
|
|
587
|
+
let authz = null
|
|
587
588
|
|
|
588
|
-
/** The storage handle, opened once.
|
|
589
|
+
/** The storage handle, opened once. With `authz` (an object of
|
|
590
|
+
* `openAuthorizationServer` options), an authorization server that shares
|
|
591
|
+
* the storage's issuer is opened first, on `mountedOn: baseIri`; the
|
|
592
|
+
* storage then trusts that issuer with the JWK Set the engine published
|
|
593
|
+
* (L4Factoidal/Authz/Mount.lean). */
|
|
589
594
|
function storage () {
|
|
590
595
|
if (opening === null) {
|
|
591
|
-
opening =
|
|
596
|
+
opening = (async () => {
|
|
597
|
+
let storageSettings = settings
|
|
598
|
+
if (settings.authz !== undefined && settings.authz !== null) {
|
|
599
|
+
if (typeof settings.baseIri !== 'string') throw new LwsHostError('authz needs baseIri')
|
|
600
|
+
const { openAuthorizationServer } = await import('../authz/server.mjs')
|
|
601
|
+
authz = await openAuthorizationServer({
|
|
602
|
+
...settings.authz,
|
|
603
|
+
...(settings.engine !== undefined ? { engine: settings.engine } : {}),
|
|
604
|
+
...(settings.auth && typeof settings.auth.audience === 'string' ? { audience: settings.auth.audience } : {}),
|
|
605
|
+
mountedOn: settings.baseIri
|
|
606
|
+
})
|
|
607
|
+
const issuers = [{ issuer: authz.issuer, jwks: authz.jwks }]
|
|
608
|
+
const prior = settings.auth && Array.isArray(settings.auth.issuers) ? settings.auth.issuers : []
|
|
609
|
+
storageSettings = { ...settings, auth: { ...(settings.auth || {}), issuers: issuers.concat(prior) } }
|
|
610
|
+
}
|
|
611
|
+
opened = await openLwsStorage(storageSettings)
|
|
612
|
+
return opened
|
|
613
|
+
})()
|
|
592
614
|
}
|
|
593
615
|
return opening
|
|
594
616
|
}
|
|
@@ -610,7 +632,13 @@ export async function createLwsServer (options = {}) {
|
|
|
610
632
|
}
|
|
611
633
|
let answer
|
|
612
634
|
try {
|
|
613
|
-
|
|
635
|
+
const store = await storage()
|
|
636
|
+
// One port for both servers: the engine names the authorization
|
|
637
|
+
// server's paths (L4Factoidal.Authz.owns); every other request is the
|
|
638
|
+
// storage's. The token request body is form text.
|
|
639
|
+
answer = authz !== null && authz.owns(record.target)
|
|
640
|
+
? await authz.step({ ...record, body: new TextDecoder('utf-8').decode(record.body) })
|
|
641
|
+
: await store.step(record)
|
|
614
642
|
} catch (error) {
|
|
615
643
|
// A failure of this host or of the engine, not a protocol answer.
|
|
616
644
|
// The engine's own words are kept; nothing is invented.
|
|
@@ -627,6 +655,8 @@ export async function createLwsServer (options = {}) {
|
|
|
627
655
|
return {
|
|
628
656
|
server,
|
|
629
657
|
storage,
|
|
658
|
+
/** The authorization server sharing the storage's issuer, or null. */
|
|
659
|
+
authz () { return authz },
|
|
630
660
|
/** Set the storage root's absolute IRI, before the first request. */
|
|
631
661
|
setBaseIri (value) {
|
|
632
662
|
if (opening !== null) {
|
|
@@ -646,6 +676,7 @@ export async function createLwsServer (options = {}) {
|
|
|
646
676
|
return new Promise((resolve) => {
|
|
647
677
|
server.close(() => {
|
|
648
678
|
if (opened !== null) opened.close()
|
|
679
|
+
if (authz !== null) authz.close()
|
|
649
680
|
resolve()
|
|
650
681
|
})
|
|
651
682
|
})
|