@optimystic/quereus-plugin-crypto 0.13.5 → 0.16.2

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/dist/plugin.js CHANGED
@@ -1,12 +1,18 @@
1
1
  import { FunctionFlags, TEXT_TYPE, BOOLEAN_TYPE, INTEGER_TYPE } from '@quereus/quereus';
2
+ import { toString, fromString } from 'uint8arrays';
2
3
  import { sha512, sha256 } from '@noble/hashes/sha2.js';
3
4
  import { blake3 } from '@noble/hashes/blake3.js';
4
- import { randomBytes as randomBytes$1, utf8ToBytes } from '@noble/hashes/utils.js';
5
+ import { randomBytes as randomBytes$1, concatBytes, utf8ToBytes } from '@noble/hashes/utils.js';
5
6
  import { secp256k1 } from '@noble/curves/secp256k1.js';
6
7
  import { p256 } from '@noble/curves/nist.js';
7
8
  import { ed25519 } from '@noble/curves/ed25519.js';
8
- import { hexToBytes, bytesToHex } from '@noble/curves/utils.js';
9
- import { fromString, toString } from 'uint8arrays';
9
+ import { bytesToHex, hexToBytes } from '@noble/curves/utils.js';
10
+ import { CID } from 'multiformats/cid';
11
+ import * as Digest from 'multiformats/hashes/digest';
12
+ import { base32 } from 'multiformats/bases/base32';
13
+ import { base58btc } from 'multiformats/bases/base58';
14
+ import { base64url } from 'multiformats/bases/base64';
15
+ import { base16 } from 'multiformats/bases/base16';
10
16
 
11
17
  // src/plugin.ts
12
18
  function toBytes(input, encoding = "base64url") {
@@ -48,29 +54,133 @@ function fromBytes(bytes, encoding = "base64url") {
48
54
  return toString(bytes, "base64url");
49
55
  }
50
56
  }
51
- function digest(data, algorithm = "sha256", inputEncoding = "base64url", outputEncoding = "base64url") {
52
- const bytes = toBytes(data, inputEncoding);
53
- let hashBytes;
54
- switch (algorithm) {
55
- case "sha256":
56
- hashBytes = sha256(bytes);
57
- break;
58
- case "sha512":
59
- hashBytes = sha512(bytes);
60
- break;
61
- case "blake3":
62
- hashBytes = blake3(bytes);
63
- break;
57
+ var HASHERS = {
58
+ sha256,
59
+ sha512,
60
+ blake3
61
+ };
62
+ var OUTPUT_ENCODERS = {
63
+ base64url: (bytes) => toString(bytes, "base64url"),
64
+ base64: (bytes) => toString(bytes, "base64"),
65
+ hex: (bytes) => bytesToHex(bytes),
66
+ bytes: (bytes) => bytes
67
+ };
68
+ function resolveHasher(algorithm) {
69
+ const hasher = HASHERS[algorithm];
70
+ if (!hasher) {
71
+ throw new Error(`Unsupported hash algorithm: ${algorithm}`);
72
+ }
73
+ return hasher;
74
+ }
75
+ function resolveOutputEncoder(encoding) {
76
+ const encoder = OUTPUT_ENCODERS[encoding];
77
+ if (!encoder) {
78
+ throw new Error(`Unsupported output encoding: ${encoding}`);
79
+ }
80
+ return encoder;
81
+ }
82
+ var DIGEST_FORMAT_V1 = 1;
83
+ var TAG_NULL = 0;
84
+ var TAG_INT = 1;
85
+ var TAG_REAL = 2;
86
+ var TAG_TEXT = 3;
87
+ var TAG_BOOL = 4;
88
+ var TAG_BLOB = 5;
89
+ var TAG_JSON = 6;
90
+ function writeVarint(out, value) {
91
+ if (!Number.isInteger(value) || value < 0) {
92
+ throw new Error(`varint expects a non-negative integer, got ${value}`);
93
+ }
94
+ let v = value;
95
+ while (v >= 128) {
96
+ out.push(v & 127 | 128);
97
+ v = Math.floor(v / 128);
98
+ }
99
+ out.push(v);
100
+ }
101
+ function framed(tag, payload) {
102
+ const header = [tag];
103
+ writeVarint(header, payload.length);
104
+ return concatBytes(Uint8Array.from(header), payload);
105
+ }
106
+ function canonicalJson(value) {
107
+ if (value === null) return "null";
108
+ const t = typeof value;
109
+ if (t === "string") return JSON.stringify(value);
110
+ if (t === "boolean") return value ? "true" : "false";
111
+ if (t === "number") {
112
+ if (!Number.isFinite(value)) {
113
+ throw new Error("digest: cannot encode a non-finite number inside a JSON field");
114
+ }
115
+ return JSON.stringify(value);
116
+ }
117
+ if (t === "bigint") {
118
+ throw new Error("digest: bigint is not representable inside a JSON field");
119
+ }
120
+ if (Array.isArray(value)) {
121
+ return `[${value.map((el) => {
122
+ if (el === void 0) {
123
+ throw new Error("digest: undefined / sparse element inside a JSON field");
124
+ }
125
+ return canonicalJson(el);
126
+ }).join(",")}]`;
127
+ }
128
+ if (t === "object") {
129
+ const proto = Object.getPrototypeOf(value);
130
+ if (proto !== Object.prototype && proto !== null) {
131
+ throw new Error("digest: only plain objects are allowed inside a JSON field");
132
+ }
133
+ const obj = value;
134
+ const keys = Object.keys(obj).sort();
135
+ return `{${keys.map((k) => {
136
+ if (obj[k] === void 0) {
137
+ throw new Error(`digest: undefined value for JSON key '${k}'`);
138
+ }
139
+ return `${JSON.stringify(k)}:${canonicalJson(obj[k])}`;
140
+ }).join(",")}}`;
141
+ }
142
+ throw new Error(`digest: unsupported value of type '${t}' inside a JSON field`);
143
+ }
144
+ function encodeField(field) {
145
+ if (field === null || field === void 0) {
146
+ return Uint8Array.of(TAG_NULL);
147
+ }
148
+ switch (typeof field) {
149
+ case "boolean":
150
+ return Uint8Array.of(TAG_BOOL, field ? 1 : 0);
151
+ case "bigint":
152
+ return framed(TAG_INT, utf8ToBytes(field.toString()));
153
+ case "number":
154
+ if (!Number.isFinite(field)) {
155
+ throw new Error("digest: cannot encode a non-finite number");
156
+ }
157
+ return Number.isInteger(field) ? framed(TAG_INT, utf8ToBytes(BigInt(field).toString())) : framed(TAG_REAL, utf8ToBytes(field.toString()));
158
+ case "string":
159
+ return framed(TAG_TEXT, utf8ToBytes(field));
160
+ case "object":
161
+ if (field instanceof Uint8Array) {
162
+ return framed(TAG_BLOB, field);
163
+ }
164
+ return framed(TAG_JSON, utf8ToBytes(canonicalJson(field)));
64
165
  default:
65
- throw new Error(`Unsupported hash algorithm: ${algorithm}`);
166
+ throw new Error(`digest: unsupported field type '${typeof field}'`);
167
+ }
168
+ }
169
+ function encodeFields(fields) {
170
+ const chunks = [Uint8Array.of(DIGEST_FORMAT_V1)];
171
+ for (const field of fields) {
172
+ chunks.push(encodeField(field));
66
173
  }
67
- return fromBytes(hashBytes, outputEncoding);
174
+ return concatBytes(...chunks);
175
+ }
176
+ function digestFields(fields, hasher, encode) {
177
+ return encode(hasher(encodeFields(fields)));
68
178
  }
69
179
  function hashMod(data, bits, algorithm = "sha256", inputEncoding = "base64url") {
70
180
  if (bits <= 0 || bits > 53) {
71
181
  throw new Error("Bits must be between 1 and 53 (JavaScript safe integer limit)");
72
182
  }
73
- const hashBytes = toBytes(digest(data, algorithm, inputEncoding, "base64url"), "base64url");
183
+ const hashBytes = resolveHasher(algorithm)(toBytes(data, inputEncoding));
74
184
  const view = new DataView(hashBytes.buffer, hashBytes.byteOffset, Math.min(8, hashBytes.length));
75
185
  const fullHash = view.getBigUint64(0, false);
76
186
  const modulus = BigInt(2) ** BigInt(bits);
@@ -123,23 +233,316 @@ function randomBytes(bits = 256, encoding = "base64url") {
123
233
  const randomBytesArray = randomBytes$1(bytes);
124
234
  return fromBytes(randomBytesArray, encoding);
125
235
  }
236
+ var MULTICODEC_CODES = {
237
+ "raw": 85,
238
+ "dag-cbor": 113
239
+ };
240
+ var MULTIHASH_CODES = {
241
+ "sha2-256": 18,
242
+ "sha2-512": 19,
243
+ "blake3": 30
244
+ };
245
+ var MULTIHASH_TO_ALGORITHM = {
246
+ "sha2-256": "sha256",
247
+ "sha2-512": "sha512",
248
+ "blake3": "blake3"
249
+ };
250
+ var MULTIHASH_DIGEST_LENGTHS = {
251
+ "sha2-256": 32,
252
+ "sha2-512": 64,
253
+ "blake3": 32
254
+ };
255
+ var MULTICODEC_NAMES = new Map(
256
+ Object.entries(MULTICODEC_CODES).map(([name, code]) => [code, name])
257
+ );
258
+ var MULTIHASH_NAMES = new Map(
259
+ Object.entries(MULTIHASH_CODES).map(([name, code]) => [code, name])
260
+ );
261
+ var MULTIBASE_ENCODERS = {
262
+ "base32": base32,
263
+ "base58btc": base58btc,
264
+ "base64url": base64url,
265
+ "base16": base16
266
+ };
267
+ var MULTIBASE_DECODER = base32.decoder.or(base58btc.decoder).or(base64url.decoder).or(base16.decoder);
268
+ function resolveCodecCode(codec) {
269
+ const code = MULTICODEC_CODES[codec];
270
+ if (code === void 0) {
271
+ throw new Error(`cid: unsupported multicodec '${codec}' (expected one of ${Object.keys(MULTICODEC_CODES).join(", ")})`);
272
+ }
273
+ return code;
274
+ }
275
+ function resolveBaseEncoder(base) {
276
+ const encoder = MULTIBASE_ENCODERS[base];
277
+ if (!encoder) {
278
+ throw new Error(`cid: unsupported multibase '${base}' (expected one of ${Object.keys(MULTIBASE_ENCODERS).join(", ")})`);
279
+ }
280
+ return encoder;
281
+ }
282
+ function cidV1(digest, hash, codec = "raw", base = "base32") {
283
+ const hashCode = MULTIHASH_CODES[hash];
284
+ if (hashCode === void 0) {
285
+ throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(", ")})`);
286
+ }
287
+ const expectedLength = MULTIHASH_DIGEST_LENGTHS[hash];
288
+ if (digest.length !== expectedLength) {
289
+ throw new Error(`cid: digest length ${digest.length} does not match asserted hash '${hash}' (expected ${expectedLength} bytes)`);
290
+ }
291
+ const codecCode = resolveCodecCode(codec);
292
+ const encoder = resolveBaseEncoder(base);
293
+ const multihash = Digest.create(hashCode, digest);
294
+ return CID.createV1(codecCode, multihash).toString(encoder);
295
+ }
296
+ function cid(data, codec = "raw", hash = "sha2-256", base = "base32") {
297
+ const algorithm = MULTIHASH_TO_ALGORITHM[hash];
298
+ if (!algorithm) {
299
+ throw new Error(`cid: unsupported multihash code '${hash}' (expected one of ${Object.keys(MULTIHASH_CODES).join(", ")})`);
300
+ }
301
+ const digest = resolveHasher(algorithm)(data);
302
+ return cidV1(digest, hash, codec, base);
303
+ }
304
+ function cidDecode(value) {
305
+ const parsed = CID.parse(value, MULTIBASE_DECODER);
306
+ return {
307
+ version: parsed.version,
308
+ codec: MULTICODEC_NAMES.get(parsed.code) ?? parsed.code,
309
+ hashCode: MULTIHASH_NAMES.get(parsed.multihash.code) ?? parsed.multihash.code,
310
+ digest: parsed.multihash.digest
311
+ };
312
+ }
313
+ var SD_LEAF_DOMAIN_V1 = "optimystic/sd-leaf/v1";
314
+ var SD_SET_DOMAIN_V1 = "optimystic/sd-set/v1";
315
+ var HIDDEN_ENCODING = "base64url";
316
+ function compareBytes(a, b) {
317
+ const len = Math.min(a.length, b.length);
318
+ for (let i = 0; i < len; i++) {
319
+ const d = a[i] - b[i];
320
+ if (d !== 0) return d;
321
+ }
322
+ return a.length - b.length;
323
+ }
324
+ function bytesEqual(a, b) {
325
+ if (a.length !== b.length) return false;
326
+ for (let i = 0; i < a.length; i++) {
327
+ if (a[i] !== b[i]) return false;
328
+ }
329
+ return true;
330
+ }
331
+ function requireSaltBytes(leaf) {
332
+ const { salt } = leaf;
333
+ if (salt == null) {
334
+ throw new Error(`set commitment: leaf '${leaf.name}' is missing a salt (an unsalted leaf is brute-forceable)`);
335
+ }
336
+ const bytes = salt instanceof Uint8Array ? salt : fromString(salt, HIDDEN_ENCODING);
337
+ if (bytes.length === 0) {
338
+ throw new Error(`set commitment: leaf '${leaf.name}' has an empty salt (an unsalted leaf is brute-forceable)`);
339
+ }
340
+ return bytes;
341
+ }
342
+ function assertUniqueNames(leaves) {
343
+ const seen = /* @__PURE__ */ new Set();
344
+ for (const leaf of leaves) {
345
+ if (seen.has(leaf.name)) {
346
+ throw new Error(`set commitment: duplicate leaf name '${leaf.name}'`);
347
+ }
348
+ seen.add(leaf.name);
349
+ }
350
+ }
351
+ function leafDigest(leaf, hasher) {
352
+ const saltBytes = requireSaltBytes(leaf);
353
+ return hasher(encodeFields([SD_LEAF_DOMAIN_V1, leaf.name, leaf.value, saltBytes]));
354
+ }
355
+ function setCommit(leaves, hasher = resolveHasher("sha256"), encode = resolveOutputEncoder("base64url")) {
356
+ assertUniqueNames(leaves);
357
+ const leafDigests = leaves.map((leaf) => leafDigest(leaf, hasher));
358
+ leafDigests.sort(compareBytes);
359
+ return encode(hasher(encodeFields([SD_SET_DOMAIN_V1, ...leafDigests])));
360
+ }
361
+ function setVerify(root, disclosure, hasher = resolveHasher("sha256"), encode = resolveOutputEncoder("base64url")) {
362
+ try {
363
+ const { disclosed, hidden } = disclosure;
364
+ const digests = [];
365
+ for (const leaf of disclosed) {
366
+ digests.push(leafDigest(leaf, hasher));
367
+ }
368
+ for (const h of hidden) {
369
+ digests.push(fromString(h, HIDDEN_ENCODING));
370
+ }
371
+ digests.sort(compareBytes);
372
+ const recomputed = hasher(encodeFields([SD_SET_DOMAIN_V1, ...digests]));
373
+ if (root instanceof Uint8Array) {
374
+ return bytesEqual(recomputed, root);
375
+ }
376
+ const encoded = encode(recomputed);
377
+ return typeof encoded === "string" && encoded === root;
378
+ } catch {
379
+ return false;
380
+ }
381
+ }
126
382
 
127
383
  // src/plugin.ts
384
+ var DIGEST_ALGORITHMS = ["sha256", "sha512", "blake3"];
385
+ var DIGEST_TEXT_ENCODINGS = ["base64url", "base64", "hex"];
386
+ function configAlgorithm(config) {
387
+ const value = config.algorithm == null ? "sha256" : String(config.algorithm);
388
+ if (!DIGEST_ALGORITHMS.includes(value)) {
389
+ throw new Error(`crypto plugin: unsupported digest algorithm '${value}' (expected one of ${DIGEST_ALGORITHMS.join(", ")})`);
390
+ }
391
+ return value;
392
+ }
393
+ function configEncoding(config) {
394
+ const value = config.encoding == null ? "base64url" : String(config.encoding);
395
+ if (!DIGEST_TEXT_ENCODINGS.includes(value)) {
396
+ throw new Error(`crypto plugin: unsupported digest encoding '${value}' (expected one of ${DIGEST_TEXT_ENCODINGS.join(", ")})`);
397
+ }
398
+ return value;
399
+ }
400
+ function toContentBytes(value, fnName) {
401
+ if (value instanceof Uint8Array) {
402
+ return value;
403
+ }
404
+ if (typeof value === "string") {
405
+ return fromString(value, "base64url");
406
+ }
407
+ throw new Error(`${fnName}: expected a BLOB or base64url TEXT argument, got ${value == null ? "NULL" : typeof value}`);
408
+ }
409
+ function leafFromJson(entry, fnName) {
410
+ if (Array.isArray(entry)) {
411
+ if (entry.length < 3) {
412
+ throw new Error(`${fnName}: a leaf array must be [name, value, salt]`);
413
+ }
414
+ const [name, value, salt] = entry;
415
+ if (typeof name !== "string") {
416
+ throw new Error(`${fnName}: leaf name must be a string`);
417
+ }
418
+ return { name, value, salt };
419
+ }
420
+ if (entry !== null && typeof entry === "object") {
421
+ const o = entry;
422
+ if (typeof o.name !== "string") {
423
+ throw new Error(`${fnName}: leaf name must be a string`);
424
+ }
425
+ if (!("value" in o)) {
426
+ throw new Error(`${fnName}: leaf '${o.name}' is missing a value (pass value: null for a null-valued attribute)`);
427
+ }
428
+ return { name: o.name, value: o.value, salt: o.salt };
429
+ }
430
+ throw new Error(`${fnName}: each leaf must be a [name, value, salt] array or { name, value, salt } object`);
431
+ }
432
+ function parseLeaves(json, fnName) {
433
+ const parsed = JSON.parse(json);
434
+ if (!Array.isArray(parsed)) {
435
+ throw new Error(`${fnName}: expected a JSON array of leaves`);
436
+ }
437
+ return parsed.map((entry) => leafFromJson(entry, fnName));
438
+ }
128
439
  var DETERMINISTIC_FLAGS = FunctionFlags.UTF8 | FunctionFlags.DETERMINISTIC;
129
440
  var NON_DETERMINISTIC_FLAGS = FunctionFlags.UTF8;
130
- function register(_db, _config = {}) {
441
+ function register(_db, config = {}) {
442
+ const digestHasher = resolveHasher(configAlgorithm(config));
443
+ const digestEncoder = resolveOutputEncoder(configEncoding(config));
131
444
  const functions = [
132
445
  {
133
446
  schema: {
134
447
  name: "digest",
135
448
  numArgs: -1,
136
- // Variable arguments: data, algorithm?, inputEncoding?, outputEncoding?
449
+ // Variadic over data fields: digest(f1, f2, ..., fN)
137
450
  flags: DETERMINISTIC_FLAGS,
138
451
  // digest is deterministic
452
+ // Bit-identical across peers/platforms — these digests are signed and persisted.
453
+ replicable: true,
454
+ returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
455
+ implementation: (...fields) => digestFields(fields, digestHasher, digestEncoder)
456
+ }
457
+ },
458
+ {
459
+ schema: {
460
+ name: "cid",
461
+ numArgs: -1,
462
+ // cid(data, codec?, hash?, base?) — trailing args optional
463
+ flags: DETERMINISTIC_FLAGS,
464
+ // Self-describing content address; signed/persisted, so byte-identical across peers.
465
+ replicable: true,
139
466
  returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
140
467
  implementation: (...args) => {
141
- const [data, algorithm = "sha256", inputEncoding = "base64url", outputEncoding = "base64url"] = args;
142
- return digest(data, algorithm, inputEncoding, outputEncoding);
468
+ const [data, codec = "raw", hash = "sha2-256", base = "base32"] = args;
469
+ return cid(toContentBytes(data, "cid"), codec, hash, base);
470
+ }
471
+ }
472
+ },
473
+ {
474
+ schema: {
475
+ name: "cid_v1",
476
+ numArgs: -1,
477
+ // cid_v1(digest, hash, codec?, base?) — hash required, trailing args optional
478
+ flags: DETERMINISTIC_FLAGS,
479
+ replicable: true,
480
+ returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
481
+ implementation: (...args) => {
482
+ const [digest, hash, codec = "raw", base = "base32"] = args;
483
+ if (hash == null) {
484
+ throw new Error("cid_v1: 'hash' argument is required (the multihash code asserting which algorithm produced the digest)");
485
+ }
486
+ return cidV1(toContentBytes(digest, "cid_v1"), hash, codec, base);
487
+ }
488
+ }
489
+ },
490
+ {
491
+ schema: {
492
+ name: "cid_decode",
493
+ numArgs: 1,
494
+ // cid_decode(cid) -> JSON text { version, codec, hashCode, digest }
495
+ flags: DETERMINISTIC_FLAGS,
496
+ replicable: true,
497
+ returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
498
+ implementation: (value) => {
499
+ const parts = cidDecode(value);
500
+ return JSON.stringify({
501
+ version: parts.version,
502
+ codec: parts.codec,
503
+ hashCode: parts.hashCode,
504
+ digest: toString(parts.digest, "base64url")
505
+ });
506
+ }
507
+ }
508
+ },
509
+ {
510
+ schema: {
511
+ name: "set_commit",
512
+ numArgs: 1,
513
+ // set_commit(leaves_json) -> root over a JSON array of [name, value, salt] leaves
514
+ flags: DETERMINISTIC_FLAGS,
515
+ // The root is signed and persisted as a commitment, same bar as `digest`.
516
+ replicable: true,
517
+ returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
518
+ implementation: (leavesJson) => {
519
+ if (typeof leavesJson !== "string") {
520
+ throw new Error("set_commit: expected a JSON TEXT array of [name, value, salt] leaves");
521
+ }
522
+ const leaves = parseLeaves(leavesJson, "set_commit");
523
+ return setCommit(leaves, digestHasher, digestEncoder);
524
+ }
525
+ }
526
+ },
527
+ {
528
+ schema: {
529
+ name: "set_verify",
530
+ numArgs: 3,
531
+ // set_verify(root, disclosed_json, hidden_json) -> BOOLEAN
532
+ flags: DETERMINISTIC_FLAGS,
533
+ // pure, not persisted — matches `verify` (no `replicable`)
534
+ returnType: { typeClass: "scalar", logicalType: BOOLEAN_TYPE, nullable: false },
535
+ implementation: (root, disclosedJson, hiddenJson) => {
536
+ try {
537
+ if (typeof root !== "string" && !(root instanceof Uint8Array)) return false;
538
+ if (typeof disclosedJson !== "string" || typeof hiddenJson !== "string") return false;
539
+ const disclosed = parseLeaves(disclosedJson, "set_verify");
540
+ const hidden = JSON.parse(hiddenJson);
541
+ if (!Array.isArray(hidden) || !hidden.every((h) => typeof h === "string")) return false;
542
+ return setVerify(root, { disclosed, hidden }, digestHasher, digestEncoder);
543
+ } catch {
544
+ return false;
545
+ }
143
546
  }
144
547
  }
145
548
  },
@@ -150,6 +553,15 @@ function register(_db, _config = {}) {
150
553
  // Variable arguments: data, privateKey, curve?, inputEncoding?, keyEncoding?, outputEncoding?
151
554
  flags: DETERMINISTIC_FLAGS,
152
555
  // sign is deterministic (same key + data = same signature)
556
+ // Security: passing a private key as the second argument (literal or bound
557
+ // parameter) is SAFE with respect to replication. The Quereus engine rebuilds
558
+ // the replicated statement from evaluated column values, not from source SQL
559
+ // text, so the key argument is discarded before the record is written — peers
560
+ // re-execute `INSERT ... VALUES (<signature>)`, never `sign(..., key)`. What
561
+ // to avoid: storing a raw private key AS a column value, since any persisted
562
+ // column value is replicated. See docs/transactions.md § "Secrets and the
563
+ // replicated statement record" and the regression guard in
564
+ // quereus-plugin-optimystic/test/statement-secret-arg-redaction.spec.ts.
153
565
  returnType: { typeClass: "scalar", logicalType: TEXT_TYPE, nullable: false },
154
566
  implementation: (...args) => {
155
567
  const [data, privateKey, curve = "secp256k1", inputEncoding = "base64url", keyEncoding = "base64url", outputEncoding = "base64url"] = args;