@provablehq/veil-aleo-sdk 0.5.0 → 0.7.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/dist/index.js CHANGED
@@ -163,11 +163,161 @@ function mnemonicToHDKey(mnemonic, options = {}) {
163
163
  return BLS12377HDKey.fromMasterSeed(seed).derivePath(DERIVATION_PATHS[derivation]).deriveChild(index);
164
164
  }
165
165
 
166
+ // src/provableApi.ts
167
+ var DEFAULT_PROVABLE_API_URL = "https://api.provable.com";
168
+ var EXPIRY_SKEW_MS = 5 * 60 * 1e3;
169
+ function memoryCredentialStore(initial) {
170
+ let held = initial;
171
+ return {
172
+ load: () => held,
173
+ save: (credentials) => {
174
+ held = credentials;
175
+ }
176
+ };
177
+ }
178
+ async function registerProvableApi(params) {
179
+ const baseUrl = params.baseUrl ?? DEFAULT_PROVABLE_API_URL;
180
+ const response = await fetch(`${baseUrl}/consumers`, {
181
+ method: "POST",
182
+ headers: { "content-type": "application/json" },
183
+ body: JSON.stringify({ username: params.username })
184
+ });
185
+ if (!response.ok) {
186
+ const body2 = await response.text();
187
+ if (response.status === 409) {
188
+ throw new Error(
189
+ `Provable API username '${params.username}' is already registered. Credentials cannot be recovered from a username: supply the existing consumerId and apiKey, or register under a different name. (HTTP 409: ${body2})`
190
+ );
191
+ }
192
+ throw new Error(
193
+ `Provable API consumer registration failed (HTTP ${response.status}): ${body2}`
194
+ );
195
+ }
196
+ const body = await response.json();
197
+ if (!body.consumer?.id || !body.key) {
198
+ throw new Error("Provable API consumer registration response carried no consumer id and key.");
199
+ }
200
+ return { consumerId: body.consumer.id, apiKey: body.key };
201
+ }
202
+ async function mintJwt(credentials, baseUrl) {
203
+ const response = await fetch(`${baseUrl}/jwts/${encodeURIComponent(credentials.consumerId)}`, {
204
+ method: "POST",
205
+ headers: { "X-Provable-API-Key": credentials.apiKey }
206
+ });
207
+ if (!response.ok) {
208
+ throw new Error(
209
+ `Provable API JWT mint failed (HTTP ${response.status}): ${await response.text()}`
210
+ );
211
+ }
212
+ const header = response.headers.get("authorization");
213
+ if (!header) {
214
+ throw new Error("Provable API JWT mint response carried no authorization header.");
215
+ }
216
+ const body = await response.json();
217
+ if (typeof body.exp !== "number") {
218
+ throw new Error("Provable API JWT mint response carried no exp claim.");
219
+ }
220
+ return { jwt: header, expiration: body.exp * 1e3 };
221
+ }
222
+ function createProvableSession(options = {}) {
223
+ const baseUrl = options.baseUrl ?? DEFAULT_PROVABLE_API_URL;
224
+ const consumers = { proving: false, recordScanning: false };
225
+ let credentials = options.credentials;
226
+ let registered = false;
227
+ let credentialsInFlight;
228
+ let jwt;
229
+ let jwtInFlight;
230
+ async function resolveCredentials(usernameOverride) {
231
+ if (credentials) return credentials;
232
+ const stored = await options.store?.load();
233
+ if (stored) {
234
+ credentials = stored;
235
+ return credentials;
236
+ }
237
+ const username = usernameOverride ?? (typeof options.username === "function" ? options.username() : options.username);
238
+ if (!username) {
239
+ throw new Error(
240
+ "No Provable API credentials available \u2014 pass credentials, a store holding them, or a username to register with."
241
+ );
242
+ }
243
+ const issued = await registerProvableApi({ username, baseUrl });
244
+ credentials = issued;
245
+ registered = true;
246
+ try {
247
+ await options.store?.save(issued);
248
+ } catch (cause) {
249
+ throw new Error(
250
+ `Registered Provable API consumer ${issued.consumerId}, but persisting its credentials failed. They are live for this process \u2014 read them from getCredentials() and store them yourself \u2014 but the API key cannot be reissued, so a restart loses it.`,
251
+ { cause }
252
+ );
253
+ }
254
+ return credentials;
255
+ }
256
+ function getCredentials({ username } = {}) {
257
+ credentialsInFlight ??= resolveCredentials(username).finally(() => {
258
+ credentialsInFlight = void 0;
259
+ });
260
+ return credentialsInFlight;
261
+ }
262
+ function getJwt({ forceRefresh = false } = {}) {
263
+ const stale = !jwt || Date.now() >= jwt.expiration - EXPIRY_SKEW_MS;
264
+ if (!forceRefresh && !stale) return Promise.resolve(jwt);
265
+ jwtInFlight ??= (async () => {
266
+ const resolved = await getCredentials();
267
+ jwt = await mintJwt(resolved, baseUrl);
268
+ return jwt;
269
+ })().finally(() => {
270
+ jwtInFlight = void 0;
271
+ });
272
+ return jwtInFlight;
273
+ }
274
+ return {
275
+ registeredConsumer: () => registered,
276
+ getCredentials,
277
+ getJwt,
278
+ consumers,
279
+ attach: (consumer) => {
280
+ consumers[consumer] = true;
281
+ }
282
+ };
283
+ }
284
+ function getProvableSession(client) {
285
+ return client.proving?.session;
286
+ }
287
+ async function authenticateProvableApi(client, params = {}) {
288
+ const session = getProvableSession(client);
289
+ if (!session) {
290
+ throw new Error(
291
+ "No Provable API session on this client \u2014 pass consumerId and apiKey, or a credentialStore, when creating it."
292
+ );
293
+ }
294
+ const credentials = await session.getCredentials({ username: params.username });
295
+ const { expiration } = await session.getJwt({ forceRefresh: params.forceRefresh });
296
+ return {
297
+ credentials,
298
+ expiration,
299
+ registered: session.registeredConsumer(),
300
+ applied: { ...session.consumers }
301
+ };
302
+ }
303
+ function provableApiActions() {
304
+ return (client) => ({
305
+ authenticateProvableApi: (params) => authenticateProvableApi(client, params)
306
+ });
307
+ }
308
+
166
309
  // src/index.ts
310
+ var DEFAULT_PROVER_URL = "https://api.provable.com/prove";
311
+ var DEFAULT_SCANNER_URL = "https://api.provable.com/scanner";
167
312
  async function loadNetwork(name) {
168
313
  const sdk = await loadSdk(name);
314
+ silenceSdkLogs(sdk);
169
315
  return buildSdk(name, sdk);
170
316
  }
317
+ function silenceSdkLogs(sdk) {
318
+ const { setLogLevel } = sdk;
319
+ setLogLevel?.("silent");
320
+ }
171
321
  function buildSdk(initialNetwork, initialSdk) {
172
322
  let currentSdk = initialSdk;
173
323
  const network = initialNetwork;
@@ -210,9 +360,26 @@ function buildSdk(initialNetwork, initialSdk) {
210
360
  let networkUrl = options.networkUrl;
211
361
  let keyProvider = new currentSdk.AleoKeyProvider();
212
362
  keyProvider.useCache(true);
363
+ options.session?.attach("proving");
364
+ let configNetwork = network;
365
+ const resolveProverUrl = () => {
366
+ const configured = options.proverUrl ?? (options.mode === "delegated" ? DEFAULT_PROVER_URL : void 0);
367
+ if (!configured) return void 0;
368
+ const base = configured.replace(/\/+$/, "").replace(/\/(mainnet|testnet)$/, "");
369
+ return `${base}/${configNetwork}`;
370
+ };
213
371
  return {
214
372
  mode: options.mode,
215
- url: options.proverUrl,
373
+ // A getter, not a snapshot: the network segment moves with `switchChain`,
374
+ // and a fixed string here would report the network this config started on
375
+ // long after it left.
376
+ get url() {
377
+ return resolveProverUrl();
378
+ },
379
+ // Carried for `authenticateProvableApi` to find on a client. Core never
380
+ // reads binding-specific fields on a proving config — `url` and `apiKey`
381
+ // already travel the same way.
382
+ session: options.session,
216
383
  buildTransaction: async (txOptions) => {
217
384
  const programManager = new currentSdk.ProgramManager(
218
385
  networkUrl,
@@ -305,9 +472,10 @@ function buildSdk(initialNetwork, initialSdk) {
305
472
  const ct = RecordCiphertext.fromString(ciphertext);
306
473
  return ct.isOwner(accountViewKey) ? ct.decrypt(accountViewKey).toString() : null;
307
474
  } : void 0;
308
- const buildPollingClient = () => createPublicClient({ transport: http(networkUrl, { network }) });
475
+ const buildPollingClient = () => createPublicClient({ transport: http(networkUrl, { network: configNetwork }) });
309
476
  if (options.mode === "delegated") {
310
- if (!options.proverUrl) throw new ConfigurationError("Delegated execution requires proverUrl. Pass proverUrl to createProvingConfig or createAleoClient.");
477
+ const proverUrl = resolveProverUrl();
478
+ if (!proverUrl) throw new ConfigurationError("Delegated execution requires proverUrl. Pass proverUrl to createProvingConfig or createAleoClient.");
311
479
  let response;
312
480
  try {
313
481
  const provingRequest = await programManager.provingRequest({
@@ -321,13 +489,29 @@ function buildSdk(initialNetwork, initialSdk) {
321
489
  broadcast: true,
322
490
  useFeeMaster: options.useFeeMaster ?? true
323
491
  });
324
- const dpsClient = new AleoNetworkClient(options.proverUrl);
325
- response = await dpsClient.submitProvingRequest({
492
+ const dpsClient = new AleoNetworkClient(proverUrl);
493
+ const auth = async (forceRefresh) => options.session ? { jwtData: await options.session.getJwt({ forceRefresh }) } : { apiKey: options.apiKey, consumerId: options.consumerId };
494
+ let credentials = await auth(false);
495
+ let result = await dpsClient.submitProvingRequestSafe({
326
496
  provingRequest,
327
- url: options.proverUrl,
328
- apiKey: options.apiKey,
329
- consumerId: options.consumerId
497
+ url: proverUrl,
498
+ ...credentials
330
499
  });
500
+ if (!result.ok && (result.status === 401 || result.status === 403) && options.session) {
501
+ credentials = await auth(true);
502
+ result = await dpsClient.submitProvingRequestSafe({
503
+ provingRequest,
504
+ url: proverUrl,
505
+ ...credentials
506
+ });
507
+ }
508
+ if (!result.ok) {
509
+ throw new ProvingError({
510
+ message: `Delegated proving failed (HTTP ${result.status}): ${result.error?.message ?? "unknown error"}`,
511
+ statusCode: result.status
512
+ });
513
+ }
514
+ response = result.data;
331
515
  } catch (e) {
332
516
  if (e instanceof BaseError) throw e;
333
517
  throw classifyProvingError(e);
@@ -382,8 +566,10 @@ function buildSdk(initialNetwork, initialSdk) {
382
566
  );
383
567
  }
384
568
  currentSdk = await loadSdk(newNetwork);
569
+ silenceSdkLogs(currentSdk);
385
570
  keyProvider = new currentSdk.AleoKeyProvider();
386
571
  keyProvider.useCache(true);
572
+ configNetwork = newNetwork;
387
573
  }
388
574
  };
389
575
  }
@@ -428,12 +614,14 @@ function buildSdk(initialNetwork, initialSdk) {
428
614
  }
429
615
  };
430
616
  }
431
- async function scanOwned(scanner, program, statusFilter, canReMint) {
617
+ async function scanOwned(scanner, program, statusFilter, hasCredentials, session) {
432
618
  const ALWAYS_RETRY = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
433
619
  const AUTH_RETRY = /* @__PURE__ */ new Set([401, 403]);
620
+ const canReMint = hasCredentials || !!session;
434
621
  const retryable = (status) => ALWAYS_RETRY.has(status) || canReMint && AUTH_RETRY.has(status);
435
622
  const MAX_ATTEMPTS = 4;
436
623
  let last = "";
624
+ let lastStatus;
437
625
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
438
626
  try {
439
627
  const result = await scanner.owned({
@@ -442,29 +630,46 @@ function buildSdk(initialNetwork, initialSdk) {
442
630
  });
443
631
  if (result.ok) return (result.data ?? []).map((r) => toOwnedRecord(r));
444
632
  last = `HTTP ${result.status}: ${result.error?.message ?? "unknown error"}`;
633
+ lastStatus = result.status;
445
634
  if (!retryable(result.status)) break;
446
635
  } catch (err) {
447
636
  last = err instanceof Error ? err.message : String(err);
637
+ lastStatus = void 0;
448
638
  }
449
639
  if (attempt === MAX_ATTEMPTS - 1) break;
450
- scanner.setJwtData(void 0);
640
+ if (lastStatus !== void 0 && AUTH_RETRY.has(lastStatus)) {
641
+ if (session) scanner.setJwtData(await session.getJwt({ forceRefresh: true }));
642
+ else scanner.setJwtData(void 0);
643
+ }
451
644
  await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
452
645
  }
453
646
  throw new Error(`Record scan failed (${last})`);
454
647
  }
455
- function createRemoteScanner(options) {
648
+ function createRemoteScanner(options = {}) {
649
+ if (!options.session && options.apiKey && !options.consumerId) {
650
+ throw new ConfigurationError(
651
+ "Record scanning with an apiKey also needs consumerId \u2014 a JWT is minted from the pair. Pass both, pass a session, or omit both for an unauthenticated service."
652
+ );
653
+ }
456
654
  let scannerSdk = { RecordScanner, ViewKey };
457
655
  let scanner;
458
656
  let viewKey;
459
657
  let viewKeyString;
460
658
  let registration = makeRegisterOnce(options.startBlock ?? 0);
659
+ let session = options.session;
660
+ session?.attach("recordScanning");
661
+ const url = options.url ?? DEFAULT_SCANNER_URL;
461
662
  function buildScanner() {
462
663
  if (!viewKeyString) return;
463
664
  viewKey = scannerSdk.ViewKey.from_string(viewKeyString);
464
665
  scanner = new scannerSdk.RecordScanner({
465
- url: options.url,
466
- consumerId: options.consumerId,
467
- ...options.apiKey ? { apiKey: options.apiKey } : {},
666
+ url,
667
+ // A session supplies the token per scan, so the credentials stay out of
668
+ // the scanner and only one party mints.
669
+ ...session ? {} : {
670
+ ...options.consumerId ? { consumerId: options.consumerId } : {},
671
+ ...options.apiKey ? { apiKey: options.apiKey } : {}
672
+ },
468
673
  viewKeys: [viewKey],
469
674
  decryptEnabled: true,
470
675
  autoReRegister: true
@@ -476,6 +681,18 @@ function buildSdk(initialNetwork, initialSdk) {
476
681
  viewKeyString = account.viewKey;
477
682
  buildScanner();
478
683
  },
684
+ /**
685
+ * Shares a Provable API session with this scanner.
686
+ *
687
+ * Called by {@link createAleoClient} so one session covers proving and
688
+ * scanning. Takes effect on the next scan — the token is applied at the
689
+ * scan boundary, not at construction.
690
+ */
691
+ setSession: (next) => {
692
+ session = next;
693
+ next.attach("recordScanning");
694
+ buildScanner();
695
+ },
479
696
  requestRecords: async (params) => {
480
697
  if (!scanner) {
481
698
  throw new Error("No active account set on record scanner. Call setAccount() first.");
@@ -483,8 +700,9 @@ function buildSdk(initialNetwork, initialSdk) {
483
700
  const activeScanner = scanner;
484
701
  const activeViewKey = viewKey;
485
702
  const activeRegistration = registration;
703
+ if (session) activeScanner.setJwtData(await session.getJwt());
486
704
  await activeRegistration.ensure(activeScanner, activeViewKey);
487
- return scanOwned(activeScanner, params.program, params.statusFilter, !!options.apiKey);
705
+ return scanOwned(activeScanner, params.program, params.statusFilter, !!(options.apiKey && options.consumerId), session);
488
706
  },
489
707
  switchNetwork: async (newNetwork) => {
490
708
  if (newNetwork !== "mainnet" && newNetwork !== "testnet") {
@@ -498,11 +716,20 @@ function buildSdk(initialNetwork, initialSdk) {
498
716
  };
499
717
  }
500
718
  function createStandaloneScanner(options) {
719
+ if (!options.session && options.apiKey && !options.consumerId) {
720
+ throw new ConfigurationError(
721
+ "Record scanning with an apiKey also needs consumerId \u2014 a JWT is minted from the pair. Pass both, pass a session, or omit both for an unauthenticated service."
722
+ );
723
+ }
501
724
  const viewKey = ViewKey.from_string(options.viewKey);
725
+ const session = options.session;
502
726
  const scanner = new RecordScanner({
503
- url: options.url,
504
- consumerId: options.consumerId,
505
- ...options.apiKey ? { apiKey: options.apiKey } : {},
727
+ url: options.url ?? DEFAULT_SCANNER_URL,
728
+ // Credentials only when no session mints on this scanner's behalf.
729
+ ...session ? {} : {
730
+ ...options.consumerId ? { consumerId: options.consumerId } : {},
731
+ ...options.apiKey ? { apiKey: options.apiKey } : {}
732
+ },
506
733
  viewKeys: [viewKey],
507
734
  decryptEnabled: true,
508
735
  autoReRegister: true
@@ -510,25 +737,43 @@ function buildSdk(initialNetwork, initialSdk) {
510
737
  const registration = makeRegisterOnce(options.startBlock ?? 0);
511
738
  return {
512
739
  requestRecords: async (params) => {
740
+ if (session) scanner.setJwtData(await session.getJwt());
513
741
  await registration.ensure(scanner, viewKey);
514
- return scanOwned(scanner, params.program, params.statusFilter, !!options.apiKey);
742
+ return scanOwned(scanner, params.program, params.statusFilter, !!(options.apiKey && options.consumerId), session);
515
743
  }
516
744
  };
517
745
  }
518
746
  function createAleoClient(options) {
519
747
  const account = privateKeyToAccount(options.privateKey);
520
748
  const transport = http(options.networkUrl, { network });
749
+ const credentials = options.consumerId && options.apiKey ? { consumerId: options.consumerId, apiKey: options.apiKey } : void 0;
750
+ const configured = !!(credentials || options.credentialStore || options.session);
751
+ const session = options.session ?? createProvableSession({
752
+ credentials,
753
+ store: options.credentialStore ?? memoryCredentialStore(),
754
+ // Resolved lazily: only a registration needs it. The derived default
755
+ // carries a random suffix because a username is spent once — an account
756
+ // that lost its stored key must still be able to register, and the old
757
+ // key is unrecoverable. A caller-supplied name is used verbatim, so a
758
+ // collision fails rather than quietly registering something else.
759
+ username: options.username ?? (() => `veil-${account.address.slice(5, 13)}-${Math.random().toString(36).slice(2, 8)}`)
760
+ });
521
761
  const proving = createProvingConfig({
522
762
  mode: options.provingMode ?? "delegated",
523
763
  networkUrl: options.networkUrl,
524
764
  proverUrl: options.proverUrl,
765
+ session,
766
+ // Ignored by the proving config whenever a session is present, which is
767
+ // where the one-minter rule is enforced.
525
768
  apiKey: options.apiKey,
526
769
  consumerId: options.consumerId,
527
770
  account,
528
- ...options.useFeeMaster !== void 0 ? { useFeeMaster: options.useFeeMaster } : {}
771
+ ...options.useFeeMaster !== void 0 ? { useFeeMaster: options.useFeeMaster } : {},
772
+ ...options.confirmationTimeout !== void 0 ? { confirmationTimeout: options.confirmationTimeout } : {}
529
773
  });
530
774
  const publicClient = createPublicClient({ transport });
531
775
  if (options.records) {
776
+ if (configured) options.records.setSession?.(session);
532
777
  options.records.setAccount({ viewKey: account.viewKey });
533
778
  }
534
779
  const walletClient = createWalletClient({
@@ -536,7 +781,7 @@ function buildSdk(initialNetwork, initialSdk) {
536
781
  transport,
537
782
  proving,
538
783
  ...options.records ? { recordProvider: options.records } : {}
539
- });
784
+ }).extend(provableApiActions());
540
785
  return { publicClient, walletClient, account };
541
786
  }
542
787
  return {
@@ -554,7 +799,7 @@ function buildSdk(initialNetwork, initialSdk) {
554
799
  createAleoClient
555
800
  };
556
801
  }
557
- var DEVNODE_CONSENSUS_HEIGHTS = "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16";
802
+ var DEVNODE_CONSENSUS_HEIGHTS = "0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17";
558
803
  function privateKeyToAccount(privateKey) {
559
804
  const sdkAccount = new Account({ privateKey });
560
805
  const address = sdkAccount.address().to_string();
@@ -665,16 +910,23 @@ function createDevnodeClient(options) {
665
910
  }
666
911
  export {
667
912
  BLS12377HDKey,
913
+ DEFAULT_PROVER_URL,
914
+ DEFAULT_SCANNER_URL,
668
915
  DEVNODE_ADDR,
669
916
  DEVNODE_PRIVATE_KEY,
670
917
  LEGACY_PATH,
671
918
  STANDARD_PATH,
919
+ authenticateProvableApi,
672
920
  createDevnodeClient,
921
+ createProvableSession,
673
922
  generateAccount,
674
923
  generateMnemonic2 as generateMnemonic,
675
924
  loadNetwork,
925
+ memoryCredentialStore,
676
926
  mnemonicToHDKey,
677
927
  mnemonicToSeed,
928
+ provableApiActions,
929
+ registerProvableApi,
678
930
  validateMnemonic2 as validateMnemonic,
679
931
  validateWord
680
932
  };