@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/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, manifestHex: hexOfBytes(opened.manifest), activated: true }
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
- function regionOf (store, keys) {
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 { blob, artifacts } = regionOf(store, keys)
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 = engine.callBlobIO('storeOpen',
381
- [store.manifestHex, JSON.stringify(artifacts)], blob).envelope
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 form of a single graph -- the graph-scoped
533
- * sibling of canonicalize(). It returns the canonical N-Quads TEXT, not
534
- * a digest: hash the string yourself (for example `vcSha256Hex`) when a
535
- * fixed-length value is needed. Every quad's graph component is dropped
536
- * before canonicalizing, so isomorphic graphs (including under
537
- * blank-node relabeling) give the same string regardless of what
538
- * graph name they were extracted from. Typically called with one
539
- * entry of graphs()'s output.
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
 
@@ -37,7 +37,7 @@
37
37
  // bytes change.
38
38
 
39
39
  // Stamped by formal/lean4/Wasm/build-wasm.sh step 9 -- do not hand-edit.
40
- const WASM_VERSION = "1b10444e4416";
40
+ const WASM_VERSION = "925f357453db";
41
41
 
42
42
  import createModule from './l4factoidal.mjs';
43
43
 
Binary file
@@ -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-0069da82efd7-web",
6
- "releaseRun": "https://github.com/danbri/factoidal-builds/actions/runs/40281606727",
7
- "gitSha": "0069da82efd75cc3bfe10785ef8853bd84931957",
8
- "builtAt": "2026-10-26T08:11:18Z",
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": "1b10444e44160b8689ebd5375c074509196930b1927ce4c9fbc829a73d9b58ac",
15
- "wasmBytes": 17034490,
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, manifestHex, fromNQuads, withExtensionRounds) {
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.storeManifestInspect(this._manifestHex), 'Store.inspect');
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 N-Quads of one graph: every quad's graph name is
888
- * dropped first (op datasetGraphAsDefault with the union of the graphs).
889
- * Text, not a digest.
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.toString('rdfc');
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, format?: string, baseIRI?: string}} [options]
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
- const manifestHex = hexOfBytes(gen.manifest);
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(e.storeQueryPlan(manifestHex, opts.sparql), 'openStore(plan)');
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(e.storeManifestInspect(manifestHex), 'openStore(inspect)'));
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(e.storeOpen(manifestHex, JSON.stringify(artifacts), blob), 'openStore');
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, manifestHex, (nq) => c.fromNQuads(nq, {}), withExtensionRounds);
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;
@@ -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 HEX = (() => {
103
- const t = new Array(256);
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
- const parts = new Array(bytes.length);
112
- for (let i = 0; i < bytes.length; i += 1) parts[i] = HEX[bytes[i]];
113
- return parts.join('');
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
@@ -2,4 +2,4 @@
2
2
  // needs import attributes, which not every runtime this package targets
3
3
  // reads). test/dts-drift.test.js checks that it equals package.json's
4
4
  // `version`.
5
- export const version = '0.16.0';
5
+ export const version = '0.18.0';
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', 'readLinkset',
47
- 'updateLinkset', 'move', 'list', 'typeIndex', 'search', 'subscribe', 'unsubscribe',
48
- 'requestAccess', 'grantAccess', 'getAccess', 'revokeAccess', 'verifyNotification'
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 key
142
- * to sign self-issued credentials with, instead of a subject token
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 = openLwsStorage(settings).then((value) => { opened = value; return value })
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
- answer = await (await storage()).step(record)
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
  })