@provenonce/beats-client 0.4.0 → 1.0.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 (3) hide show
  1. package/index.d.ts +118 -1
  2. package/index.mjs +196 -9
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -117,10 +117,78 @@ export interface VerifyApiResponse {
117
117
  [key: string]: unknown;
118
118
  }
119
119
 
120
+ export interface BeatObject {
121
+ index: number;
122
+ hash: string;
123
+ prev: string;
124
+ timestamp: number;
125
+ nonce?: string;
126
+ anchor_hash?: string;
127
+ }
128
+
129
+ export interface SpotCheck {
130
+ index: number;
131
+ hash: string;
132
+ prev: string;
133
+ nonce?: string;
134
+ }
135
+
136
+ export interface WorkProofRequest {
137
+ from_beat: number;
138
+ to_beat: number;
139
+ from_hash: string;
140
+ to_hash: string;
141
+ beats_computed: number;
142
+ difficulty: number;
143
+ anchor_index: number;
144
+ anchor_hash?: string;
145
+ spot_checks: SpotCheck[];
146
+ }
147
+
148
+ export interface WorkProofReceiptPayload {
149
+ type: 'work_proof';
150
+ beats_verified: number;
151
+ difficulty: number;
152
+ anchor_index: number;
153
+ anchor_hash: string | null;
154
+ utc: number;
155
+ }
156
+
157
+ export interface WorkProofResponse {
158
+ ok: boolean;
159
+ valid: boolean;
160
+ receipt?: WorkProofReceiptPayload;
161
+ signature?: string;
162
+ public_key?: string;
163
+ spot_checks_verified?: number;
164
+ reason?: string;
165
+ failed_indices?: number[];
166
+ _note?: string;
167
+ }
168
+
169
+ export interface KeyInfo {
170
+ public_key_base58: string;
171
+ public_key_hex: string;
172
+ signing_context: string;
173
+ purpose: string;
174
+ }
175
+
176
+ export interface KeyResponse {
177
+ /** Timestamp receipt key (backward compat) */
178
+ public_key_base58: string;
179
+ public_key_hex: string;
180
+ algorithm: string;
181
+ keys: {
182
+ timestamp: KeyInfo;
183
+ work_proof: KeyInfo;
184
+ };
185
+ _note?: string;
186
+ }
187
+
120
188
  export interface BeatsClient {
121
189
  getHealth(): Promise<HealthResponse>;
122
190
  getAnchor(opts?: AnchorOptions): Promise<AnchorResponse>;
123
- getKey(): Promise<{ public_key_base58: string; public_key_hex: string; algorithm: string; purpose?: string; _note?: string }>;
191
+ getKey(): Promise<KeyResponse>;
124
192
  verify(payload: unknown): Promise<VerifyApiResponse>;
125
193
  timestampHash(hash: string): Promise<TimestampResponse>;
126
194
 
@@ -143,8 +211,57 @@ export interface BeatsClient {
143
211
  /** Returns true if chain continuity is broken and resync() is required. */
144
212
  isBroken(): boolean;
145
213
 
214
+ /**
215
+ * Submit a work proof to the Beats service and receive a signed receipt.
216
+ * The receipt certifies N beats at difficulty D anchored to a global beat.
217
+ * Policy-free: the caller (Registry or any consumer) decides what N means.
218
+ */
219
+ submitWorkProof(proof: WorkProofRequest): Promise<WorkProofResponse>;
220
+
221
+ /**
222
+ * Verify a work-proof receipt signature offline.
223
+ * Uses the work_proof key from GET /api/v1/beat/key (distinct from timestamp key).
224
+ */
225
+ verifyWorkProofReceipt(
226
+ receiptResponse: WorkProofResponse,
227
+ opts?: { publicKey?: string },
228
+ ): Promise<boolean>;
229
+
146
230
  /** Internal: resolve public key from cache or auto-fetch. */
147
231
  _resolveKey(): Promise<string | null>;
148
232
  }
149
233
 
150
234
  export declare function createBeatsClient(options?: BeatsClientOptions): BeatsClient;
235
+
236
+ // ============ STANDALONE COMPUTE (Node.js only) ============
237
+
238
+ /**
239
+ * Compute a single beat — sequential SHA-256 hash chain.
240
+ * Node.js only (uses node:crypto). Not browser-compatible.
241
+ *
242
+ * @param prevHash Previous beat hash (64 hex)
243
+ * @param beatIndex Beat index (monotonically increasing)
244
+ * @param difficulty Hash iterations per beat (default 1000)
245
+ * @param nonce Optional entropy
246
+ * @param anchorHash Optional global anchor hash to weave in
247
+ */
248
+ export declare function computeBeat(
249
+ prevHash: string,
250
+ beatIndex: number,
251
+ difficulty?: number,
252
+ nonce?: string,
253
+ anchorHash?: string,
254
+ ): Promise<BeatObject>;
255
+
256
+ /**
257
+ * Compute the genesis beat for a local chain.
258
+ * Deterministic from caller-provided seed + domain prefix.
259
+ * Node.js only.
260
+ *
261
+ * @param seed Unique identifier (e.g. agent hash)
262
+ * @param domainPrefix Namespace prefix (default: 'beats:genesis:v1:')
263
+ */
264
+ export declare function createGenesisBeat(
265
+ seed: string,
266
+ domainPrefix?: string,
267
+ ): Promise<BeatObject>;
package/index.mjs CHANGED
@@ -77,12 +77,33 @@ async function verifyEd25519(payload, signatureBase64, publicKey) {
77
77
 
78
78
  /**
79
79
  * Recompute an anchor's hash from its fields (B-3).
80
- * Requires Node.js crypto not available in browsers.
81
- * Returns true if the recomputed hash matches anchor.hash.
82
- *
83
- * A-4: If solana_entropy is present, uses the v2 domain-prefixed nonce.
80
+ * V3: If solana_entropy is present, uses binary-canonical single SHA-256.
81
+ * V1 legacy: string-based hash with difficulty iteration (Node.js only).
84
82
  */
85
- const ANCHOR_DOMAIN_PREFIX = 'provenonce:anchor:v2';
83
+ const ANCHOR_DOMAIN_PREFIX = 'PROVENONCE_BEATS_V1';
84
+
85
+ function hexToUint8Array(hex) {
86
+ const arr = new Uint8Array(hex.length / 2);
87
+ for (let i = 0; i < arr.length; i++) {
88
+ arr[i] = parseInt(hex.substr(i * 2, 2), 16);
89
+ }
90
+ return arr;
91
+ }
92
+
93
+ function u64beBytes(n) {
94
+ const buf = new Uint8Array(8);
95
+ const hi = Math.floor(n / 0x100000000);
96
+ const lo = n >>> 0;
97
+ buf[0] = (hi >>> 24) & 0xff;
98
+ buf[1] = (hi >>> 16) & 0xff;
99
+ buf[2] = (hi >>> 8) & 0xff;
100
+ buf[3] = hi & 0xff;
101
+ buf[4] = (lo >>> 24) & 0xff;
102
+ buf[5] = (lo >>> 16) & 0xff;
103
+ buf[6] = (lo >>> 8) & 0xff;
104
+ buf[7] = lo & 0xff;
105
+ return buf;
106
+ }
86
107
 
87
108
  async function recomputeAnchorHash(anchor) {
88
109
  if (!anchor || !HEX64.test(anchor.hash) || !HEX64.test(anchor.prev_hash)) return false;
@@ -91,11 +112,41 @@ async function recomputeAnchorHash(anchor) {
91
112
  if (!Number.isInteger(anchor.utc) || anchor.utc < 0) return false;
92
113
  if (!Number.isInteger(anchor.epoch) || anchor.epoch < 0) return false;
93
114
 
115
+ if (anchor.solana_entropy) {
116
+ // V3: binary-canonical single SHA-256
117
+ const prefix = new TextEncoder().encode(ANCHOR_DOMAIN_PREFIX); // 19 bytes
118
+ const prev = hexToUint8Array(anchor.prev_hash); // 32 bytes
119
+ const idx = u64beBytes(anchor.beat_index); // 8 bytes
120
+ const entropy = decodeBase58(anchor.solana_entropy); // 32 bytes
121
+
122
+ const preimage = new Uint8Array(prefix.length + prev.length + idx.length + entropy.length);
123
+ preimage.set(prefix, 0);
124
+ preimage.set(prev, prefix.length);
125
+ preimage.set(idx, prefix.length + prev.length);
126
+ preimage.set(entropy, prefix.length + prev.length + idx.length);
127
+
128
+ // Web Crypto path
129
+ const subtle = globalThis.crypto?.subtle;
130
+ if (subtle) {
131
+ try {
132
+ const digest = await subtle.digest('SHA-256', preimage);
133
+ const hashArr = Array.from(new Uint8Array(digest));
134
+ const computed = hashArr.map(b => b.toString(16).padStart(2, '0')).join('');
135
+ return computed === anchor.hash;
136
+ } catch {
137
+ // Fall through to Node.js
138
+ }
139
+ }
140
+
141
+ // Node.js fallback
142
+ const { createHash } = await import('node:crypto');
143
+ const computed = createHash('sha256').update(preimage).digest('hex');
144
+ return computed === anchor.hash;
145
+ }
146
+
147
+ // V1 legacy: string-based hash with difficulty iteration
94
148
  const { createHash } = await import('node:crypto');
95
- // A-4: v2 nonce includes solana_entropy; legacy nonce for pre-A4 anchors
96
- const nonce = anchor.solana_entropy
97
- ? `${ANCHOR_DOMAIN_PREFIX}:${anchor.utc}:${anchor.epoch}:${anchor.solana_entropy}`
98
- : `anchor:${anchor.utc}:${anchor.epoch}`;
149
+ const nonce = `anchor:${anchor.utc}:${anchor.epoch}`;
99
150
  const seed = `${anchor.prev_hash}:${anchor.beat_index}:${nonce}`;
100
151
  let current = createHash('sha256').update(seed, 'utf8').digest('hex');
101
152
  for (let i = 0; i < anchor.difficulty; i++) {
@@ -479,5 +530,141 @@ export function createBeatsClient({
479
530
  return null;
480
531
  }
481
532
  },
533
+
534
+ // ---- Work Proof ----
535
+
536
+ /**
537
+ * Submit a work proof to the Beats service and receive a signed receipt.
538
+ *
539
+ * @param {object} proof
540
+ * @param {number} proof.from_beat Starting beat index
541
+ * @param {number} proof.to_beat Ending beat index
542
+ * @param {string} proof.from_hash Hash at from_beat (64 hex)
543
+ * @param {string} proof.to_hash Hash at to_beat (64 hex)
544
+ * @param {number} proof.beats_computed to_beat - from_beat
545
+ * @param {number} proof.difficulty Hash iterations per beat
546
+ * @param {number} proof.anchor_index Global anchor index woven in
547
+ * @param {string} [proof.anchor_hash] Anchor hash woven in (64 hex)
548
+ * @param {Array} proof.spot_checks Spot-checked beats for verification
549
+ * @param {number} .index Beat index
550
+ * @param {string} .hash Hash at this beat (64 hex)
551
+ * @param {string} .prev Previous hash (64 hex)
552
+ * @param {string} [.nonce] Optional nonce
553
+ *
554
+ * @returns {Promise<WorkProofResponse>}
555
+ */
556
+ submitWorkProof(proof) {
557
+ return request('/api/v1/beat/work-proof', {
558
+ method: 'POST',
559
+ headers: { 'content-type': 'application/json' },
560
+ body: JSON.stringify(proof),
561
+ });
562
+ },
563
+
564
+ /**
565
+ * Verify a work-proof receipt signature (offline).
566
+ * Uses the work_proof key from GET /api/v1/beat/key, not the timestamp key.
567
+ *
568
+ * @param {object} receiptResponse The full response from submitWorkProof()
569
+ * @param {object} [opts]
570
+ * @param {string} [opts.publicKey] Override: hex or base58 work-proof public key
571
+ */
572
+ async verifyWorkProofReceipt(receiptResponse, opts = {}) {
573
+ const payload = receiptResponse?.receipt;
574
+ const signature = receiptResponse?.signature;
575
+ if (!payload || !signature) return false;
576
+
577
+ // Resolve work-proof public key: param > cached from /key endpoint
578
+ let key = opts.publicKey;
579
+ if (!key) {
580
+ try {
581
+ const keyData = await this.getKey();
582
+ key = keyData?.keys?.work_proof?.public_key_hex ||
583
+ keyData?.keys?.work_proof?.public_key_base58;
584
+ } catch {
585
+ return false;
586
+ }
587
+ }
588
+ if (!key) return false;
589
+
590
+ try {
591
+ return await verifyEd25519(payload, signature, key);
592
+ } catch {
593
+ return false;
594
+ }
595
+ },
596
+ };
597
+ }
598
+
599
+ // ============ STANDALONE COMPUTE (Node.js only) ============
600
+
601
+ /**
602
+ * Compute a single beat — sequential SHA-256 hash chain.
603
+ *
604
+ * Each beat requires `difficulty` sequential hash iterations.
605
+ * Because SHA-256 output feeds the next input, this cannot be
606
+ * parallelized. This is the CPU-work primitive for local beat chains.
607
+ *
608
+ * Node.js only: uses node:crypto. Not available in browser environments.
609
+ *
610
+ * @param {string} prevHash Previous beat hash (64 hex)
611
+ * @param {number} beatIndex Beat index (monotonically increasing)
612
+ * @param {number} [difficulty=1000] Hash iterations per beat
613
+ * @param {string} [nonce] Optional entropy
614
+ * @param {string} [anchorHash] Global anchor hash to weave in
615
+ * @returns {{ index, hash, prev, timestamp, nonce?, anchor_hash? }}
616
+ */
617
+ export async function computeBeat(prevHash, beatIndex, difficulty = 1000, nonce, anchorHash) {
618
+ if (typeof prevHash !== 'string' || prevHash.length === 0) {
619
+ throw new Error('computeBeat: prevHash must be a non-empty string');
620
+ }
621
+ if (!Number.isInteger(beatIndex) || beatIndex < 0) {
622
+ throw new Error('computeBeat: beatIndex must be a non-negative integer');
623
+ }
624
+ const d = Math.max(1, Math.min(Number.isFinite(difficulty) ? Math.floor(difficulty) : 1000, 1_000_000));
625
+
626
+ const { createHash } = await import('node:crypto');
627
+
628
+ const seed = anchorHash
629
+ ? `${prevHash}:${beatIndex}:${nonce || ''}:${anchorHash}`
630
+ : `${prevHash}:${beatIndex}:${nonce || ''}`;
631
+
632
+ let current = createHash('sha256').update(seed, 'utf8').digest('hex');
633
+ for (let i = 0; i < d; i++) {
634
+ current = createHash('sha256').update(current, 'utf8').digest('hex');
635
+ }
636
+
637
+ return {
638
+ index: beatIndex,
639
+ hash: current,
640
+ prev: prevHash,
641
+ timestamp: Date.now(),
642
+ nonce,
643
+ anchor_hash: anchorHash,
644
+ };
645
+ }
646
+
647
+ /**
648
+ * Compute the genesis beat for a local chain.
649
+ * Deterministic from caller-provided seed + optional domain prefix.
650
+ *
651
+ * Node.js only.
652
+ *
653
+ * @param {string} seed Unique identifier for this chain (e.g. agent hash)
654
+ * @param {string} [domainPrefix='beats:genesis:v1:'] Namespace prefix
655
+ */
656
+ export async function createGenesisBeat(seed, domainPrefix = 'beats:genesis:v1:') {
657
+ if (!seed || typeof seed !== 'string') {
658
+ throw new Error('createGenesisBeat: seed must be a non-empty string');
659
+ }
660
+ const { createHash } = await import('node:crypto');
661
+ const genesisHash = createHash('sha256')
662
+ .update(`${domainPrefix}${seed}`, 'utf8')
663
+ .digest('hex');
664
+ return {
665
+ index: 0,
666
+ hash: genesisHash,
667
+ prev: '0'.repeat(64),
668
+ timestamp: Date.now(),
482
669
  };
483
670
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@provenonce/beats-client",
3
- "version": "0.4.0",
3
+ "version": "1.0.0",
4
4
  "description": "Minimal client for the public Provenonce Beats service",
5
5
  "type": "module",
6
6
  "main": "index.mjs",