burnledger 0.2.2 → 0.3.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/cjs/index.browser.d.ts +2 -1
  4. package/dist/cjs/index.browser.d.ts.map +1 -1
  5. package/dist/cjs/index.browser.js +7 -1
  6. package/dist/cjs/index.browser.js.map +1 -1
  7. package/dist/cjs/index.d.ts +9 -1
  8. package/dist/cjs/index.d.ts.map +1 -1
  9. package/dist/cjs/index.js +17 -1
  10. package/dist/cjs/index.js.map +1 -1
  11. package/dist/cjs/models.d.ts +28 -1
  12. package/dist/cjs/models.d.ts.map +1 -1
  13. package/dist/cjs/models.js +8 -0
  14. package/dist/cjs/models.js.map +1 -1
  15. package/dist/cjs/verify.d.ts +55 -1
  16. package/dist/cjs/verify.d.ts.map +1 -1
  17. package/dist/cjs/verify.js +414 -104
  18. package/dist/cjs/verify.js.map +1 -1
  19. package/dist/cjs/web-verifier.d.ts +29 -0
  20. package/dist/cjs/web-verifier.d.ts.map +1 -0
  21. package/dist/cjs/web-verifier.js +70 -0
  22. package/dist/cjs/web-verifier.js.map +1 -0
  23. package/dist/esm/cli.d.ts.map +1 -1
  24. package/dist/esm/cli.js +12 -5
  25. package/dist/esm/cli.js.map +1 -1
  26. package/dist/esm/index.browser.d.ts +2 -1
  27. package/dist/esm/index.browser.d.ts.map +1 -1
  28. package/dist/esm/index.browser.js +3 -0
  29. package/dist/esm/index.browser.js.map +1 -1
  30. package/dist/esm/index.d.ts +9 -1
  31. package/dist/esm/index.d.ts.map +1 -1
  32. package/dist/esm/index.js +13 -1
  33. package/dist/esm/index.js.map +1 -1
  34. package/dist/esm/models.d.ts +28 -1
  35. package/dist/esm/models.d.ts.map +1 -1
  36. package/dist/esm/models.js +8 -0
  37. package/dist/esm/models.js.map +1 -1
  38. package/dist/esm/verify.d.ts +55 -1
  39. package/dist/esm/verify.d.ts.map +1 -1
  40. package/dist/esm/verify.js +411 -105
  41. package/dist/esm/verify.js.map +1 -1
  42. package/dist/esm/web-verifier.d.ts +29 -0
  43. package/dist/esm/web-verifier.d.ts.map +1 -0
  44. package/dist/esm/web-verifier.js +63 -0
  45. package/dist/esm/web-verifier.js.map +1 -0
  46. package/package.json +12 -11
  47. package/src/cli.ts +207 -0
  48. package/src/client.ts +555 -0
  49. package/src/crypto-browser.ts +49 -0
  50. package/src/crypto-node.ts +40 -0
  51. package/src/crypto.ts +10 -0
  52. package/src/errors.ts +154 -0
  53. package/src/http.ts +209 -0
  54. package/src/index.browser.ts +110 -0
  55. package/src/index.ts +134 -0
  56. package/src/keys.ts +18 -0
  57. package/src/models.ts +558 -0
  58. package/src/pagination.ts +64 -0
  59. package/src/verify.ts +956 -0
  60. package/src/web-verifier.ts +89 -0
  61. package/src/webhooks.ts +76 -0
package/src/client.ts ADDED
@@ -0,0 +1,555 @@
1
+ /** Async BurnLedger API client.
2
+ *
3
+ * Single class — Node.js is all-async, no sync/async split needed.
4
+ * Pagination uses AsyncIterable<T> via Paginator.
5
+ */
6
+
7
+ import { RateLimitError, TimeoutError } from "./errors.js";
8
+ import { Transport } from "./http.js";
9
+ import type {
10
+ ApiKeyListItem,
11
+ ApiKeyResponse,
12
+ Attestation,
13
+ BatchAttestationResponse,
14
+ CertificateResponse,
15
+ CertificateStats,
16
+ ConsistencyProof,
17
+ InclusionProof,
18
+ LogEntry,
19
+ Profile,
20
+ RevocationStatus,
21
+ SignedTreeHead,
22
+ System,
23
+ SystemHealth,
24
+ VerifyResult,
25
+ Webhook,
26
+ WebhookDelivery,
27
+ WebhookRotateResponse,
28
+ } from "./models.js";
29
+ import {
30
+ parseApiKeyListItem,
31
+ parseApiKeyResponse,
32
+ parseAttestation,
33
+ parseBatchAttestationResponse,
34
+ parseCertificateResponse,
35
+ parseCertificateStats,
36
+ parseConsistencyProof,
37
+ parseInclusionProof,
38
+ parseLogEntry,
39
+ parseProfile,
40
+ parseRevocationStatus,
41
+ parseSignedTreeHead,
42
+ parseSystem,
43
+ parseSystemHealth,
44
+ parseVerifyResult,
45
+ parseWebhook,
46
+ parseWebhookDelivery,
47
+ parseWebhookRotateResponse,
48
+ } from "./models.js";
49
+ import { Paginator } from "./pagination.js";
50
+
51
+ type Raw = Record<string, unknown>;
52
+
53
+ export interface BurnLedgerOptions {
54
+ apiKey: string;
55
+ baseUrl?: string;
56
+ timeout?: number;
57
+ maxRetries?: number;
58
+ }
59
+
60
+ export class BurnLedger {
61
+ private readonly transport: Transport;
62
+
63
+ constructor(opts: BurnLedgerOptions) {
64
+ this.transport = new Transport({
65
+ baseUrl: opts.baseUrl ?? "https://api.burnledger.io",
66
+ apiKey: opts.apiKey,
67
+ timeout: opts.timeout ?? 30,
68
+ maxRetries: opts.maxRetries ?? 2,
69
+ });
70
+ }
71
+
72
+ close(): void {
73
+ // Transport uses native fetch — no persistent connections to close.
74
+ // Method exists for API symmetry and future extensibility.
75
+ }
76
+
77
+ [Symbol.asyncDispose](): Promise<void> {
78
+ this.close();
79
+ return Promise.resolve();
80
+ }
81
+
82
+ // --- Systems ---
83
+
84
+ async registerSystem(opts: {
85
+ name: string;
86
+ connectorType: string;
87
+ connectionConfig?: Record<string, unknown>;
88
+ dsn?: string;
89
+ uri?: string;
90
+ subjectQuery: string;
91
+ hashScope?: string;
92
+ maxRecords?: number;
93
+ maxBytes?: number;
94
+ queryTimeout?: string;
95
+ }): Promise<System> {
96
+ const config = resolveConnectionConfig(
97
+ opts.connectionConfig,
98
+ opts.dsn,
99
+ opts.uri,
100
+ );
101
+ const body = {
102
+ name: opts.name,
103
+ connector_type: opts.connectorType,
104
+ connection_config: config,
105
+ subject_query: opts.subjectQuery,
106
+ hash_scope: opts.hashScope ?? "existence",
107
+ max_records: opts.maxRecords ?? 1_000_000,
108
+ max_bytes: opts.maxBytes ?? 10 * 1024 * 1024 * 1024,
109
+ query_timeout: opts.queryTimeout ?? "30s",
110
+ };
111
+ const data = await this.transport.request("POST", "/v1/systems", {
112
+ json: body,
113
+ });
114
+ return parseSystem(data as Raw);
115
+ }
116
+
117
+ async getSystem(systemId: string): Promise<System> {
118
+ const data = await this.transport.request(
119
+ "GET",
120
+ `/v1/systems/${systemId}`,
121
+ );
122
+ return parseSystem(data as Raw);
123
+ }
124
+
125
+ listSystems(opts?: { limit?: number }): Paginator<System> {
126
+ return new Paginator(this.transport, "/v1/systems", parseSystem, {
127
+ params: { limit: opts?.limit ?? 25 },
128
+ });
129
+ }
130
+
131
+ async deregisterSystem(systemId: string): Promise<void> {
132
+ await this.transport.request("DELETE", `/v1/systems/${systemId}`);
133
+ }
134
+
135
+ async healthCheck(systemId: string): Promise<System> {
136
+ const data = await this.transport.request(
137
+ "POST",
138
+ `/v1/systems/${systemId}/health-check`,
139
+ );
140
+ return parseSystem(data as Raw);
141
+ }
142
+
143
+ // --- Attestations ---
144
+
145
+ async attest(
146
+ subjectIdentifier: string,
147
+ opts?: {
148
+ systemIds?: string[];
149
+ proofMode?: string;
150
+ expiresIn?: string;
151
+ webhookUrl?: string;
152
+ },
153
+ ): Promise<Attestation> {
154
+ const body: Record<string, unknown> = {
155
+ subject_identifier: subjectIdentifier,
156
+ proof_mode: opts?.proofMode ?? "count",
157
+ expires_in: opts?.expiresIn ?? "72h",
158
+ };
159
+ if (opts?.systemIds !== undefined) body.system_ids = opts.systemIds;
160
+ if (opts?.webhookUrl !== undefined) body.webhook_url = opts.webhookUrl;
161
+
162
+ const data = await this.transport.request("POST", "/v1/attestations", {
163
+ json: body,
164
+ });
165
+ return parseAttestation(data as Raw);
166
+ }
167
+
168
+ async getAttestation(attestationId: string): Promise<Attestation> {
169
+ const data = await this.transport.request(
170
+ "GET",
171
+ `/v1/attestations/${attestationId}`,
172
+ );
173
+ return parseAttestation(data as Raw);
174
+ }
175
+
176
+ async waitFor(
177
+ attestationId: string,
178
+ opts?: { timeout?: number; pollInterval?: number },
179
+ ): Promise<Attestation> {
180
+ return poll({
181
+ fetch: () => this.getAttestation(attestationId),
182
+ done: (att) => att.status !== "PENDING_VERIFICATION",
183
+ operation: `waitFor(${attestationId})`,
184
+ timeout: opts?.timeout ?? 60,
185
+ pollInterval: opts?.pollInterval ?? 2,
186
+ });
187
+ }
188
+
189
+ async verify(
190
+ attestationId: string,
191
+ subjectIdentifier: string,
192
+ opts?: { timeout?: number; pollInterval?: number },
193
+ ): Promise<VerifyResult> {
194
+ const body = { subject_identifier: subjectIdentifier };
195
+ const data = await this.transport.request(
196
+ "POST",
197
+ `/v1/attestations/${attestationId}/verify`,
198
+ { json: body },
199
+ );
200
+ const result = parseVerifyResult(data as Raw);
201
+
202
+ const timeout = opts?.timeout ?? 0;
203
+ if (timeout <= 0 || result.status === "CERTIFIED") {
204
+ return result;
205
+ }
206
+
207
+ return poll({
208
+ fetch: async () => {
209
+ const d = await this.transport.request(
210
+ "POST",
211
+ `/v1/attestations/${attestationId}/verify`,
212
+ { json: body },
213
+ );
214
+ return parseVerifyResult(d as Raw);
215
+ },
216
+ done: (r) => r.status === "CERTIFIED",
217
+ operation: `verify(${attestationId})`,
218
+ timeout,
219
+ pollInterval: opts?.pollInterval ?? 2,
220
+ });
221
+ }
222
+
223
+ async batchAttest(opts: {
224
+ subjectIdentifiers: string[];
225
+ systemIds: string[];
226
+ proofMode?: string;
227
+ expiresIn?: string;
228
+ }): Promise<BatchAttestationResponse> {
229
+ const body = {
230
+ subject_identifiers: opts.subjectIdentifiers,
231
+ system_ids: opts.systemIds,
232
+ proof_mode: opts.proofMode ?? "count",
233
+ expires_in: opts.expiresIn ?? "72h",
234
+ };
235
+ const data = await this.transport.request("POST", "/v1/attestations/batch", {
236
+ json: body,
237
+ });
238
+ return parseBatchAttestationResponse(data as Raw);
239
+ }
240
+
241
+ // --- Certificates ---
242
+
243
+ async getCertificate(certificateId: string): Promise<CertificateResponse> {
244
+ const data = await this.transport.request(
245
+ "GET",
246
+ `/v1/certificates/${certificateId}`,
247
+ );
248
+ return parseCertificateResponse(data as Raw);
249
+ }
250
+
251
+ listCertificates(opts?: {
252
+ limit?: number;
253
+ }): Paginator<CertificateResponse> {
254
+ return new Paginator(
255
+ this.transport,
256
+ "/v1/certificates",
257
+ parseCertificateResponse,
258
+ { params: { limit: opts?.limit ?? 25 } },
259
+ );
260
+ }
261
+
262
+ async downloadPdf(certificateId: string): Promise<Uint8Array> {
263
+ return this.transport.requestBytes(
264
+ "GET",
265
+ `/v1/certificates/${certificateId}/pdf`,
266
+ );
267
+ }
268
+
269
+ async savePdf(certificateId: string, path: string): Promise<void> {
270
+ const pdf = await this.downloadPdf(certificateId);
271
+ const { writeFile } = await import("node:fs/promises");
272
+ await writeFile(path, pdf);
273
+ }
274
+
275
+ async getRevocationStatus(certificateId: string): Promise<RevocationStatus> {
276
+ const data = await this.transport.request(
277
+ "GET",
278
+ `/v1/certificates/${certificateId}/revocation-status`,
279
+ );
280
+ return parseRevocationStatus(data as Raw);
281
+ }
282
+
283
+ async revokeCertificate(
284
+ certificateId: string,
285
+ opts: { reason: string },
286
+ ): Promise<CertificateResponse> {
287
+ const data = await this.transport.request(
288
+ "POST",
289
+ `/v1/certificates/${certificateId}/revoke`,
290
+ { json: { reason: opts.reason } },
291
+ );
292
+ return parseCertificateResponse(data as Raw);
293
+ }
294
+
295
+ async getCertificateStats(): Promise<CertificateStats> {
296
+ const data = await this.transport.request("GET", "/v1/certificates/stats");
297
+ return parseCertificateStats(data as Raw);
298
+ }
299
+
300
+ async exportCertificates(opts?: {
301
+ format?: "csv" | "jsonl";
302
+ status?: "ACTIVE" | "REVOKED";
303
+ issuedAfter?: string;
304
+ issuedBefore?: string;
305
+ }): Promise<Uint8Array> {
306
+ const params: Record<string, string> = {};
307
+ if (opts?.format !== undefined) params.format = opts.format;
308
+ if (opts?.status !== undefined) params.status = opts.status;
309
+ if (opts?.issuedAfter !== undefined) params.issued_after = opts.issuedAfter;
310
+ if (opts?.issuedBefore !== undefined) params.issued_before = opts.issuedBefore;
311
+
312
+ const qs = new URLSearchParams(params).toString();
313
+ const path = qs ? `/v1/certificates/export?${qs}` : "/v1/certificates/export";
314
+ return this.transport.requestBytes("GET", path);
315
+ }
316
+
317
+ // --- Webhooks ---
318
+
319
+ async registerWebhook(opts: { url: string }): Promise<Webhook> {
320
+ const data = await this.transport.request("POST", "/v1/webhooks", {
321
+ json: { url: opts.url },
322
+ });
323
+ return parseWebhook(data as Raw);
324
+ }
325
+
326
+ listWebhooks(opts?: { limit?: number }): Paginator<Webhook> {
327
+ return new Paginator(this.transport, "/v1/webhooks", parseWebhook, {
328
+ params: { limit: opts?.limit ?? 25 },
329
+ });
330
+ }
331
+
332
+ async deleteWebhook(webhookId: string): Promise<void> {
333
+ await this.transport.request("DELETE", `/v1/webhooks/${webhookId}`);
334
+ }
335
+
336
+ async rotateWebhookSecret(webhookId: string): Promise<WebhookRotateResponse> {
337
+ const data = await this.transport.request(
338
+ "POST",
339
+ `/v1/webhooks/${webhookId}/rotate-secret`,
340
+ );
341
+ return parseWebhookRotateResponse(data as Raw);
342
+ }
343
+
344
+ async commitWebhookRotation(webhookId: string): Promise<void> {
345
+ await this.transport.request(
346
+ "POST",
347
+ `/v1/webhooks/${webhookId}/commit-rotation`,
348
+ );
349
+ }
350
+
351
+ listFailedDeliveries(opts?: { limit?: number }): Paginator<WebhookDelivery> {
352
+ return new Paginator(
353
+ this.transport,
354
+ "/v1/webhooks/deliveries/failed",
355
+ parseWebhookDelivery,
356
+ { params: { limit: opts?.limit ?? 25 } },
357
+ );
358
+ }
359
+
360
+ async retryDelivery(deliveryId: string): Promise<void> {
361
+ await this.transport.request(
362
+ "POST",
363
+ `/v1/webhooks/deliveries/${deliveryId}/retry`,
364
+ );
365
+ }
366
+
367
+ async resolveDelivery(deliveryId: string): Promise<void> {
368
+ await this.transport.request(
369
+ "DELETE",
370
+ `/v1/webhooks/deliveries/${deliveryId}`,
371
+ );
372
+ }
373
+
374
+ // --- API Keys ---
375
+
376
+ listApiKeys(opts?: { limit?: number }): Paginator<ApiKeyListItem> {
377
+ return new Paginator(
378
+ this.transport,
379
+ "/v1/api-keys",
380
+ parseApiKeyListItem,
381
+ { params: { limit: opts?.limit ?? 25 } },
382
+ );
383
+ }
384
+
385
+ async createApiKey(): Promise<ApiKeyResponse> {
386
+ const data = await this.transport.request("POST", "/v1/api-keys", {
387
+ json: {},
388
+ });
389
+ return parseApiKeyResponse(data as Raw);
390
+ }
391
+
392
+ async revokeApiKey(keyId: string): Promise<void> {
393
+ await this.transport.request("DELETE", `/v1/api-keys/${keyId}`);
394
+ }
395
+
396
+ // --- Profile ---
397
+
398
+ async getMe(): Promise<Profile> {
399
+ const data = await this.transport.request("GET", "/v1/me");
400
+ return parseProfile(data as Raw);
401
+ }
402
+
403
+ async updateMe(params: { name: string; email: string }): Promise<Profile> {
404
+ const data = await this.transport.request("PATCH", "/v1/me", {
405
+ json: { name: params.name, email: params.email },
406
+ });
407
+ return parseProfile(data as Raw);
408
+ }
409
+
410
+ async deleteMe(): Promise<void> {
411
+ await this.transport.request("DELETE", "/v1/me");
412
+ }
413
+
414
+ // --- System Health ---
415
+
416
+ async getSystemHealth(systemId: string): Promise<SystemHealth> {
417
+ const data = await this.transport.request(
418
+ "GET",
419
+ `/v1/systems/${systemId}/health`,
420
+ );
421
+ return parseSystemHealth(data as Raw);
422
+ }
423
+
424
+ // --- Transparency log (public, no auth) ---
425
+
426
+ async getLogHead(): Promise<SignedTreeHead> {
427
+ const data = await this.transport.request("GET", "/v1/log/head", {
428
+ authenticated: false,
429
+ });
430
+ return parseSignedTreeHead(data as Raw);
431
+ }
432
+
433
+ async getLogEntry(index: number): Promise<LogEntry> {
434
+ const data = await this.transport.request(
435
+ "GET",
436
+ `/v1/log/entry/${index}`,
437
+ { authenticated: false },
438
+ );
439
+ return parseLogEntry(data as Raw);
440
+ }
441
+
442
+ async getLogEntries(opts: {
443
+ start: number;
444
+ end: number;
445
+ }): Promise<LogEntry[]> {
446
+ const data = await this.transport.request("GET", "/v1/log/entries", {
447
+ params: { start: opts.start, end: opts.end },
448
+ authenticated: false,
449
+ });
450
+ return (data as Raw[]).map(parseLogEntry);
451
+ }
452
+
453
+ async getInclusionProof(opts: {
454
+ index: number;
455
+ treeSize: number;
456
+ }): Promise<InclusionProof> {
457
+ const data = await this.transport.request(
458
+ "GET",
459
+ "/v1/log/proof/inclusion",
460
+ {
461
+ params: { index: opts.index, tree_size: opts.treeSize },
462
+ authenticated: false,
463
+ },
464
+ );
465
+ return parseInclusionProof(data as Raw);
466
+ }
467
+
468
+ async getConsistencyProof(opts: {
469
+ oldSize: number;
470
+ newSize: number;
471
+ }): Promise<ConsistencyProof> {
472
+ const data = await this.transport.request(
473
+ "GET",
474
+ "/v1/log/proof/consistency",
475
+ {
476
+ params: { old_size: opts.oldSize, new_size: opts.newSize },
477
+ authenticated: false,
478
+ },
479
+ );
480
+ return parseConsistencyProof(data as Raw);
481
+ }
482
+ }
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // Connection config resolution
486
+ // ---------------------------------------------------------------------------
487
+
488
+ function resolveConnectionConfig(
489
+ connectionConfig: Record<string, unknown> | undefined,
490
+ dsn: string | undefined,
491
+ uri: string | undefined,
492
+ ): Record<string, unknown> {
493
+ const convenience = (dsn !== undefined ? 1 : 0) + (uri !== undefined ? 1 : 0);
494
+ if (convenience > 0 && connectionConfig !== undefined) {
495
+ throw new Error(
496
+ "Cannot provide both connectionConfig and a convenience parameter (dsn/uri). Use one or the other.",
497
+ );
498
+ }
499
+ if (convenience > 1) {
500
+ throw new Error("Cannot provide both dsn and uri.");
501
+ }
502
+ if (dsn !== undefined) return { dsn };
503
+ if (uri !== undefined) return { uri };
504
+ if (connectionConfig !== undefined) return connectionConfig;
505
+ return {};
506
+ }
507
+
508
+ // ---------------------------------------------------------------------------
509
+ // Polling helper
510
+ // ---------------------------------------------------------------------------
511
+
512
+ async function poll<T>(opts: {
513
+ fetch: () => Promise<T>;
514
+ done: (result: T) => boolean;
515
+ operation: string;
516
+ timeout: number;
517
+ pollInterval: number;
518
+ }): Promise<T> {
519
+ const deadline = Date.now() + opts.timeout * 1000;
520
+ let interval = opts.pollInterval * 1000;
521
+
522
+ while (true) {
523
+ const elapsed = (Date.now() - (deadline - opts.timeout * 1000)) / 1000;
524
+ if (Date.now() >= deadline) {
525
+ throw new TimeoutError(opts.operation, elapsed);
526
+ }
527
+
528
+ let result: T;
529
+ try {
530
+ result = await opts.fetch();
531
+ } catch (err) {
532
+ if (err instanceof RateLimitError && err.retryAfter !== undefined) {
533
+ await sleep(err.retryAfter * 1000);
534
+ continue;
535
+ }
536
+ throw err;
537
+ }
538
+
539
+ if (opts.done(result)) {
540
+ return result;
541
+ }
542
+
543
+ const remaining = deadline - Date.now();
544
+ const sleepTime = Math.min(interval, remaining);
545
+ if (sleepTime <= 0) {
546
+ throw new TimeoutError(opts.operation, elapsed);
547
+ }
548
+ await sleep(sleepTime);
549
+ interval = Math.min(interval * 1.5, 30_000);
550
+ }
551
+ }
552
+
553
+ function sleep(ms: number): Promise<void> {
554
+ return new Promise((resolve) => setTimeout(resolve, ms));
555
+ }
@@ -0,0 +1,49 @@
1
+ /** Browser CryptoOps implementation using WebCrypto (crypto.subtle).
2
+ *
3
+ * Ed25519 support: Chrome 113+, Firefox 128+, Safari 17+.
4
+ */
5
+
6
+ import type { CryptoOps } from "./crypto.js";
7
+
8
+ export const browserCrypto: CryptoOps = {
9
+ async sha256(data: Uint8Array): Promise<Uint8Array> {
10
+ const hash = await crypto.subtle.digest("SHA-256", toBuffer(data));
11
+ return new Uint8Array(hash);
12
+ },
13
+
14
+ async ed25519Verify(
15
+ publicKey: Uint8Array,
16
+ data: Uint8Array,
17
+ signature: Uint8Array,
18
+ ): Promise<boolean> {
19
+ try {
20
+ const spki = concatBytes(ED25519_DER_PREFIX, publicKey);
21
+ const key = await crypto.subtle.importKey(
22
+ "spki",
23
+ toBuffer(spki),
24
+ { name: "Ed25519" },
25
+ false,
26
+ ["verify"],
27
+ );
28
+ return crypto.subtle.verify("Ed25519", key, toBuffer(signature), toBuffer(data));
29
+ } catch {
30
+ return false;
31
+ }
32
+ },
33
+ };
34
+
35
+ const ED25519_DER_PREFIX = new Uint8Array([
36
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
37
+ ]);
38
+
39
+ /** Ensure the Uint8Array is backed by a plain ArrayBuffer (not SharedArrayBuffer). */
40
+ function toBuffer(data: Uint8Array): ArrayBuffer {
41
+ return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;
42
+ }
43
+
44
+ function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
45
+ const out = new Uint8Array(a.length + b.length);
46
+ out.set(a, 0);
47
+ out.set(b, a.length);
48
+ return out;
49
+ }
@@ -0,0 +1,40 @@
1
+ /** Node.js CryptoOps implementation wrapping node:crypto. */
2
+
3
+ import { createHash, verify } from "node:crypto";
4
+ import type { CryptoOps } from "./crypto.js";
5
+
6
+ const ED25519_DER_PREFIX = new Uint8Array([
7
+ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
8
+ ]);
9
+
10
+ export const nodeCrypto: CryptoOps = {
11
+ async sha256(data: Uint8Array): Promise<Uint8Array> {
12
+ const hash = createHash("sha256").update(data).digest();
13
+ return new Uint8Array(hash);
14
+ },
15
+
16
+ async ed25519Verify(
17
+ publicKey: Uint8Array,
18
+ data: Uint8Array,
19
+ signature: Uint8Array,
20
+ ): Promise<boolean> {
21
+ try {
22
+ const spki = concatBytes(ED25519_DER_PREFIX, publicKey);
23
+ return verify(
24
+ null,
25
+ data,
26
+ { key: Buffer.from(spki), format: "der", type: "spki" },
27
+ signature,
28
+ );
29
+ } catch {
30
+ return false;
31
+ }
32
+ },
33
+ };
34
+
35
+ function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
36
+ const out = new Uint8Array(a.length + b.length);
37
+ out.set(a, 0);
38
+ out.set(b, a.length);
39
+ return out;
40
+ }
package/src/crypto.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** Pluggable cryptographic operations for Node.js and browser environments. */
2
+
3
+ export interface CryptoOps {
4
+ sha256(data: Uint8Array): Promise<Uint8Array>;
5
+ ed25519Verify(
6
+ publicKey: Uint8Array,
7
+ data: Uint8Array,
8
+ signature: Uint8Array,
9
+ ): Promise<boolean>;
10
+ }