@provablehq/sdk 0.11.7 → 0.11.8

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.
@@ -0,0 +1,102 @@
1
+ import { TransportFunction } from "./utils/utils.js";
2
+ /**
3
+ * Interface for the JWT data.
4
+ *
5
+ * @property jwt {string} The JWT token string.
6
+ * @property expiration {number} The expiration time of the JWT token in UNIX timestamp format (milliseconds).
7
+ */
8
+ export interface JWTData {
9
+ jwt: string;
10
+ expiration: number;
11
+ }
12
+ /** Default header carrying a provisioned API key (e.g. edge.provable.com). */
13
+ export declare const DEFAULT_API_KEY_HEADER = "X-API-Key";
14
+ /**
15
+ * Authentication configuration for Provable API services.
16
+ *
17
+ * Two live modes exist, matching the two gateways:
18
+ * - `jwt` (api.provable.com): the apiKey + consumerId pair mints a short-lived JWT at
19
+ * `/jwts/{consumerId}` which is sent as an `Authorization` header and refreshed near expiry.
20
+ * - `api-key` (edge.provable.com): a provisioned key is sent verbatim on every request in a
21
+ * header (default `X-API-Key`). There is no registration, minting, or refresh in this mode;
22
+ * a 401 means the key is invalid or revoked and retrying cannot help.
23
+ * - `none`: requests carry no auth headers.
24
+ */
25
+ export type ApiAuthConfig = {
26
+ mode: "jwt";
27
+ apiKey?: string;
28
+ consumerId?: string;
29
+ jwtData?: JWTData;
30
+ } | {
31
+ mode: "api-key";
32
+ value: string;
33
+ header?: string;
34
+ } | {
35
+ mode: "none";
36
+ };
37
+ /**
38
+ * Legacy authentication options accepted by {@link AleoNetworkClient} and {@link RecordScanner}
39
+ * before explicit modes existed. Normalized by {@link normalizeAuthConfig}.
40
+ */
41
+ export interface LegacyAuthOptions {
42
+ auth?: ApiAuthConfig;
43
+ apiKey?: string | {
44
+ header: string;
45
+ value: string;
46
+ };
47
+ consumerId?: string;
48
+ jwtData?: JWTData;
49
+ }
50
+ /**
51
+ * Normalize legacy auth options into an explicit {@link ApiAuthConfig}.
52
+ *
53
+ * Rules, preserving the pre-mode behavior of both clients:
54
+ * - An explicit `auth` wins. Combining it with legacy fields throws — the caller's intent
55
+ * is ambiguous and guessing hides misconfiguration.
56
+ * - A string `apiKey` (with or without `consumerId`) or a bare `jwtData` selects `jwt` mode.
57
+ * - An `{ header, value }` apiKey selects `api-key` mode with that header. Combining it with
58
+ * a `consumerId` throws: keyed auth has no consumer and the pair would silently pick one.
59
+ * - Nothing configured selects `none`.
60
+ */
61
+ export declare function normalizeAuthConfig(options: LegacyAuthOptions): ApiAuthConfig;
62
+ /**
63
+ * ApiAuth resolves the auth headers each request must carry, owning the JWT mint/refresh
64
+ * lifecycle in `jwt` mode. Share one instance across clients that use the same credentials so
65
+ * only one minter exists and concurrent refreshes are deduplicated.
66
+ */
67
+ export declare class ApiAuth {
68
+ readonly config: ApiAuthConfig;
69
+ private readonly baseUrl;
70
+ private readonly transport;
71
+ private jwtData?;
72
+ private inFlight?;
73
+ private warned;
74
+ private readonly mintHeaders;
75
+ /**
76
+ * @param {ApiAuthConfig} config The auth mode and its material.
77
+ * @param {string} baseUrl API root origin; `/jwts/{consumerId}` lives here in `jwt` mode.
78
+ * @param {TransportFunction} transport Transport used for the JWT mint request.
79
+ * @param {Record<string, string>} [mintHeaders] Extra headers on the mint request (e.g. SDK telemetry).
80
+ */
81
+ constructor(config: ApiAuthConfig, baseUrl: string, transport?: TransportFunction, mintHeaders?: Record<string, string>);
82
+ /** The mode this instance authenticates with. */
83
+ get mode(): ApiAuthConfig["mode"];
84
+ /** Replace the stored JWT, e.g. one minted elsewhere. Only meaningful in `jwt` mode. */
85
+ setJwtData(jwtData: JWTData | undefined): void;
86
+ /** The stored JWT, when one exists. */
87
+ getJwtData(): JWTData | undefined;
88
+ /**
89
+ * The auth headers a request must carry, refreshing the JWT first when it is stale and a
90
+ * refresh is possible. Returns an empty object in `none` mode or when `jwt` mode lacks
91
+ * both a usable token and the material to mint one.
92
+ */
93
+ headers(): Promise<Record<string, string>>;
94
+ /**
95
+ * Refreshes the JWT by making a POST request to /jwts/{consumer_id}.
96
+ *
97
+ * @param {string} apiKey The API key to use for the refresh request.
98
+ * @param {string} consumerId The consumer ID for the JWT endpoint.
99
+ * @returns {Promise<JWTData>} The new JWT data.
100
+ */
101
+ private refreshJwt;
102
+ }
@@ -1436,21 +1436,6 @@ async function retryWithBackoff(fn, { maxAttempts = 5, baseDelay = 100, jitter,
1436
1436
  throw new Error("retryWithBackoff: unreachable");
1437
1437
  }
1438
1438
 
1439
- /** Type guard: value is a ProvingResponse. */
1440
- function isProvingResponse(value) {
1441
- return (typeof value === "object" &&
1442
- value !== null &&
1443
- "transaction" in value &&
1444
- "broadcast_result" in value &&
1445
- typeof value.broadcast_result === "object");
1446
- }
1447
- /** Type guard: value is a ProveApiErrorBody. */
1448
- function isProveApiErrorBody(value) {
1449
- return (typeof value === "object" &&
1450
- value !== null &&
1451
- "message" in value);
1452
- }
1453
-
1454
1439
  const KEY_STORE = testnet_js.Metadata.baseUrl();
1455
1440
  function convert(metadata) {
1456
1441
  // This looks up the method name in VerifyingKey
@@ -1551,6 +1536,165 @@ const RECORD_DOMAIN = "RecordScannerV0";
1551
1536
  const ZERO_ADDRESS = "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc";
1552
1537
  const FIVE_MINUTES = 5 * 60 * 1000; // 5 minutes in milliseconds
1553
1538
 
1539
+ /** Default header carrying a provisioned API key (e.g. edge.provable.com). */
1540
+ const DEFAULT_API_KEY_HEADER = "X-API-Key";
1541
+ /**
1542
+ * Normalize legacy auth options into an explicit {@link ApiAuthConfig}.
1543
+ *
1544
+ * Rules, preserving the pre-mode behavior of both clients:
1545
+ * - An explicit `auth` wins. Combining it with legacy fields throws — the caller's intent
1546
+ * is ambiguous and guessing hides misconfiguration.
1547
+ * - A string `apiKey` (with or without `consumerId`) or a bare `jwtData` selects `jwt` mode.
1548
+ * - An `{ header, value }` apiKey selects `api-key` mode with that header. Combining it with
1549
+ * a `consumerId` throws: keyed auth has no consumer and the pair would silently pick one.
1550
+ * - Nothing configured selects `none`.
1551
+ */
1552
+ function normalizeAuthConfig(options) {
1553
+ if (options.auth) {
1554
+ if (options.apiKey !== undefined || options.consumerId !== undefined || options.jwtData !== undefined) {
1555
+ throw new Error("Pass either `auth` or the legacy apiKey/consumerId/jwtData options, not both");
1556
+ }
1557
+ validateKeyed(options.auth);
1558
+ return options.auth;
1559
+ }
1560
+ if (typeof options.apiKey === "object" && options.apiKey !== null) {
1561
+ if (options.consumerId !== undefined) {
1562
+ throw new Error("A custom-header apiKey cannot be combined with consumerId — keyed auth has no consumer. Use a string apiKey for JWT auth.");
1563
+ }
1564
+ const config = { mode: "api-key", value: options.apiKey.value, header: options.apiKey.header };
1565
+ validateKeyed(config);
1566
+ return config;
1567
+ }
1568
+ if (options.apiKey !== undefined || options.consumerId !== undefined || options.jwtData !== undefined) {
1569
+ return { mode: "jwt", apiKey: options.apiKey, consumerId: options.consumerId, jwtData: options.jwtData };
1570
+ }
1571
+ return { mode: "none" };
1572
+ }
1573
+ /**
1574
+ * Rejects api-key configurations that would emit an empty or nameless header.
1575
+ *
1576
+ * @param config Any auth configuration; only the api-key mode has anything to check.
1577
+ * @throws When the key value is empty, or a custom header name is given but empty.
1578
+ */
1579
+ function validateKeyed(config) {
1580
+ if (config.mode !== "api-key")
1581
+ return;
1582
+ if (!config.value) {
1583
+ throw new Error("api-key auth mode requires a key value. Keys are provisioned, not registered — supply one.");
1584
+ }
1585
+ if (config.header !== undefined && !config.header) {
1586
+ throw new Error("api-key auth header must be a non-empty header name when given.");
1587
+ }
1588
+ }
1589
+ /**
1590
+ * ApiAuth resolves the auth headers each request must carry, owning the JWT mint/refresh
1591
+ * lifecycle in `jwt` mode. Share one instance across clients that use the same credentials so
1592
+ * only one minter exists and concurrent refreshes are deduplicated.
1593
+ */
1594
+ class ApiAuth {
1595
+ config;
1596
+ baseUrl;
1597
+ transport;
1598
+ jwtData;
1599
+ inFlight;
1600
+ warned = false;
1601
+ mintHeaders;
1602
+ /**
1603
+ * @param {ApiAuthConfig} config The auth mode and its material.
1604
+ * @param {string} baseUrl API root origin; `/jwts/{consumerId}` lives here in `jwt` mode.
1605
+ * @param {TransportFunction} transport Transport used for the JWT mint request.
1606
+ * @param {Record<string, string>} [mintHeaders] Extra headers on the mint request (e.g. SDK telemetry).
1607
+ */
1608
+ constructor(config, baseUrl, transport = defaultTransport, mintHeaders = {}) {
1609
+ this.config = config;
1610
+ this.baseUrl = baseUrl;
1611
+ this.transport = transport;
1612
+ this.mintHeaders = mintHeaders;
1613
+ if (config.mode === "jwt")
1614
+ this.jwtData = config.jwtData;
1615
+ }
1616
+ /** The mode this instance authenticates with. */
1617
+ get mode() {
1618
+ return this.config.mode;
1619
+ }
1620
+ /** Replace the stored JWT, e.g. one minted elsewhere. Only meaningful in `jwt` mode. */
1621
+ setJwtData(jwtData) {
1622
+ this.jwtData = jwtData;
1623
+ }
1624
+ /** The stored JWT, when one exists. */
1625
+ getJwtData() {
1626
+ return this.jwtData;
1627
+ }
1628
+ /**
1629
+ * The auth headers a request must carry, refreshing the JWT first when it is stale and a
1630
+ * refresh is possible. Returns an empty object in `none` mode or when `jwt` mode lacks
1631
+ * both a usable token and the material to mint one.
1632
+ */
1633
+ async headers() {
1634
+ if (this.config.mode === "none")
1635
+ return {};
1636
+ if (this.config.mode === "api-key") {
1637
+ return { [this.config.header ?? DEFAULT_API_KEY_HEADER]: this.config.value };
1638
+ }
1639
+ const stale = !this.jwtData || Date.now() >= this.jwtData.expiration - FIVE_MINUTES;
1640
+ if (stale) {
1641
+ const { apiKey, consumerId } = this.config;
1642
+ if (apiKey && consumerId) {
1643
+ this.inFlight ??= this.refreshJwt(apiKey, consumerId).finally(() => {
1644
+ this.inFlight = undefined;
1645
+ });
1646
+ this.jwtData = await this.inFlight;
1647
+ }
1648
+ else if (!this.jwtData && !this.warned) {
1649
+ this.warned = true;
1650
+ logger.warn("JWT or both apiKey and consumerId are required when using the Provable API");
1651
+ }
1652
+ // A stale JWT that cannot be refreshed is still sent: the server is the
1653
+ // authority on expiry and rejecting locally would break long-lived callers.
1654
+ }
1655
+ return this.jwtData?.jwt ? { Authorization: this.jwtData.jwt } : {};
1656
+ }
1657
+ /**
1658
+ * Refreshes the JWT by making a POST request to /jwts/{consumer_id}.
1659
+ *
1660
+ * @param {string} apiKey The API key to use for the refresh request.
1661
+ * @param {string} consumerId The consumer ID for the JWT endpoint.
1662
+ * @returns {Promise<JWTData>} The new JWT data.
1663
+ */
1664
+ async refreshJwt(apiKey, consumerId) {
1665
+ const response = await post(`${this.baseUrl}/jwts/${consumerId}`, {
1666
+ headers: {
1667
+ ...this.mintHeaders,
1668
+ "X-Provable-API-Key": apiKey,
1669
+ },
1670
+ }, this.transport);
1671
+ const authHeader = response.headers.get("authorization");
1672
+ if (!authHeader) {
1673
+ throw new Error("No authorization header in JWT refresh response");
1674
+ }
1675
+ const body = await response.json();
1676
+ return {
1677
+ jwt: authHeader,
1678
+ expiration: body.exp * 1000, // Convert to milliseconds
1679
+ };
1680
+ }
1681
+ }
1682
+
1683
+ /** Type guard: value is a ProvingResponse. */
1684
+ function isProvingResponse(value) {
1685
+ return (typeof value === "object" &&
1686
+ value !== null &&
1687
+ "transaction" in value &&
1688
+ "broadcast_result" in value &&
1689
+ typeof value.broadcast_result === "object");
1690
+ }
1691
+ /** Type guard: value is a ProveApiErrorBody. */
1692
+ function isProveApiErrorBody(value) {
1693
+ return (typeof value === "object" &&
1694
+ value !== null &&
1695
+ "message" in value);
1696
+ }
1697
+
1554
1698
  const HEADERS = new Set([
1555
1699
  "x-aleo-sdk-version",
1556
1700
  "x-aleo-environment",
@@ -1581,9 +1725,13 @@ class AleoNetworkClient {
1581
1725
  network;
1582
1726
  transport;
1583
1727
  hasCustomTransport;
1728
+ auth;
1584
1729
  apiKey;
1585
1730
  consumerId;
1586
1731
  jwtData;
1732
+ // The consumer the cached jwtData was minted for, so the cache is never
1733
+ // reused for a different identity on a shared client.
1734
+ jwtConsumerId;
1587
1735
  proverUri;
1588
1736
  recordScannerUri;
1589
1737
  constructor(host, options) {
@@ -1603,7 +1751,7 @@ class AleoNetworkClient {
1603
1751
  else {
1604
1752
  this.headers = {
1605
1753
  // This is replaced by the actual version by a Rollup plugin
1606
- "X-Aleo-SDK-Version": "0.11.7",
1754
+ "X-Aleo-SDK-Version": "0.11.8",
1607
1755
  "X-Aleo-environment": environment(),
1608
1756
  };
1609
1757
  }
@@ -1615,11 +1763,17 @@ class AleoNetworkClient {
1615
1763
  if (options.recordScannerUri) {
1616
1764
  this.recordScannerUri = options.recordScannerUri + "/testnet";
1617
1765
  }
1766
+ // If an explicit auth mode was specified, validate and set it, so a
1767
+ // bad key fails here rather than on the first proving request.
1768
+ if (options.auth) {
1769
+ normalizeAuthConfig({ auth: options.auth });
1770
+ this.auth = options.auth;
1771
+ }
1618
1772
  }
1619
1773
  else {
1620
1774
  this.headers = {
1621
1775
  // This is replaced by the actual version by a Rollup plugin
1622
- "X-Aleo-SDK-Version": "0.11.7",
1776
+ "X-Aleo-SDK-Version": "0.11.8",
1623
1777
  "X-Aleo-environment": environment(),
1624
1778
  };
1625
1779
  }
@@ -3053,33 +3207,6 @@ class AleoNetworkClient {
3053
3207
  throw new Error(`Error posting solution: No response received: ${error.message}`);
3054
3208
  }
3055
3209
  }
3056
- /**
3057
- * Refreshes the JWT by making a POST request to /jwts/{consumer_id}
3058
- *
3059
- * @param {string} apiKey - The API key for authentication.
3060
- * @param {string} consumerId - The consumer ID associated with the API key.
3061
- * @returns {Promise<JwtData>} The JWT token and expiration time
3062
- */
3063
- async refreshJwt(apiKey, consumerId) {
3064
- if (!apiKey || !consumerId) {
3065
- throw new Error('API key and consumer ID are required to refresh JWT');
3066
- }
3067
- const response = await post(`${this.baseUrl}/jwts/${consumerId}`, {
3068
- headers: {
3069
- ...this.method("refreshJwt"),
3070
- 'X-Provable-API-Key': apiKey,
3071
- }
3072
- }, this.transport);
3073
- const authHeader = response.headers.get('authorization');
3074
- if (!authHeader) {
3075
- throw new Error('No authorization header in JWT refresh response');
3076
- }
3077
- const body = await response.json();
3078
- return {
3079
- jwt: authHeader,
3080
- expiration: body.exp * 1000 // Convert to milliseconds
3081
- };
3082
- }
3083
3210
  /**
3084
3211
  * Parses a /prove/authorization or /prove/request response. Returns a result object (never throws for 200/400/500/503).
3085
3212
  */
@@ -3142,31 +3269,40 @@ class AleoNetworkClient {
3142
3269
  async submitProvingRequestSafe(options) {
3143
3270
  // Attempt to get the Prover URI first from the options, then from any configured globally, or third try the main configured host.
3144
3271
  const proverUri = (options.url ?? this.proverUri) ?? this.host;
3145
- // Try to get JWT data to access the Provable API.
3146
- const apiKey = options.apiKey ?? this.apiKey;
3147
- const consumerId = options.consumerId ?? this.consumerId;
3148
- let jwtData = options.jwtData ?? this.jwtData;
3149
- // Check to see if the JWT needs refreshing. Runs before parsing the
3150
- // proving request so JWT refresh errors propagate as they always have.
3151
- const isExpired = jwtData && Date.now() >= jwtData.expiration - FIVE_MINUTES;
3152
- if (!jwtData || isExpired) {
3153
- if (apiKey && consumerId) {
3154
- jwtData = await this.refreshJwt(apiKey, consumerId);
3155
- this.jwtData = jwtData;
3156
- options.jwtData = jwtData;
3157
- }
3158
- else {
3159
- logger.warn('JWT or both apiKey and consumerId are required when using the Provable API');
3160
- }
3272
+ // Resolve the auth mode: per-request, then client-wide, then the legacy fields.
3273
+ // Mixing an explicit per-request auth with per-request legacy fields is
3274
+ // ambiguous, so it throws — matching normalizeAuthConfig and RecordScanner.
3275
+ if (options.auth && (options.apiKey !== undefined || options.consumerId !== undefined || options.jwtData !== undefined)) {
3276
+ throw new Error("Pass either `auth` or the legacy apiKey/consumerId/jwtData options, not both");
3277
+ }
3278
+ const config = options.auth ?? this.auth ?? normalizeAuthConfig({
3279
+ apiKey: options.apiKey ?? this.apiKey,
3280
+ consumerId: options.consumerId ?? this.consumerId,
3281
+ jwtData: options.jwtData ?? this.jwtData,
3282
+ });
3283
+ const auth = new ApiAuth(config, this.baseUrl, this.transport, this.method("refreshJwt"));
3284
+ // Seed the cached token so an explicit jwt config reuses it until the
3285
+ // refresh window instead of minting on every request — but only for the
3286
+ // consumer that minted it, so per-request credentials on a shared
3287
+ // client never ride another identity's token.
3288
+ if (config.mode === "jwt" && !config.jwtData && this.jwtData && config.consumerId === this.jwtConsumerId) {
3289
+ auth.setJwtData(this.jwtData);
3290
+ }
3291
+ // Resolves the auth headers, which refreshes the JWT when it is stale. Runs before
3292
+ // parsing the proving request so JWT refresh errors propagate as they always have.
3293
+ const authHeaders = await auth.headers();
3294
+ const jwtData = auth.getJwtData();
3295
+ if (config.mode === "jwt" && jwtData) {
3296
+ this.jwtData = jwtData;
3297
+ this.jwtConsumerId = config.consumerId;
3298
+ options.jwtData = jwtData;
3161
3299
  }
3162
3300
  // Create the necessary headers to hit the provable api.
3163
3301
  const headers = {
3164
3302
  ...this.method("submitProvingRequest"),
3165
3303
  "Content-Type": "application/json",
3304
+ ...authHeaders,
3166
3305
  };
3167
- if (jwtData?.jwt) {
3168
- headers["Authorization"] = jwtData.jwt;
3169
- }
3170
3306
  // Send the proving request encrypted (libsodium-compatible sealed box).
3171
3307
  // Used by both `/prove/authorization` (Authorization variant) and
3172
3308
  // `/prove/request` (Request variant). The legacy plaintext `/prove`
@@ -4719,9 +4855,10 @@ class RecordScanner {
4719
4855
  cacheViewKeysOnRegister;
4720
4856
  url;
4721
4857
  baseUrl;
4722
- apiKey;
4858
+ auth;
4859
+ explicitAuth;
4860
+ legacyApiKey;
4723
4861
  consumerId;
4724
- jwtData;
4725
4862
  uuid;
4726
4863
  viewKeys;
4727
4864
  autoReRegister;
@@ -4758,23 +4895,71 @@ class RecordScanner {
4758
4895
  // Add the view key to the scanner's view keys.
4759
4896
  this.addViewKey(this.account.viewKey());
4760
4897
  }
4761
- // Configure authentication options.
4762
- this.apiKey = typeof options.apiKey === "string" ? {
4763
- header: "X-Provable-API-Key",
4764
- value: options.apiKey
4765
- } : options.apiKey;
4898
+ // Configure authentication options. Validated up front so an explicit
4899
+ // auth combined with the legacy fields throws instead of the legacy key
4900
+ // leaking onto requests the explicit mode should own.
4901
+ normalizeAuthConfig(options);
4902
+ this.explicitAuth = options.auth;
4903
+ this.legacyApiKey = options.apiKey;
4766
4904
  this.consumerId = options.consumerId;
4767
- this.jwtData = options.jwtData;
4905
+ this.auth = this.buildAuth(options.jwtData);
4768
4906
  this.autoReRegister = options.autoReRegister;
4769
4907
  this.decryptEnabled = options.decryptEnabled;
4770
4908
  }
4909
+ /**
4910
+ * Builds the ApiAuth for the current auth configuration. An explicit `auth` option wins;
4911
+ * the legacy apiKey/consumerId fields normalize onto a mode otherwise.
4912
+ *
4913
+ * @param {RecordScannerJWTData} [jwtData] JWT to carry into the rebuilt auth.
4914
+ * @returns {ApiAuth} The auth for the current configuration.
4915
+ */
4916
+ buildAuth(jwtData) {
4917
+ const config = this.explicitAuth ?? normalizeAuthConfig({
4918
+ apiKey: this.legacyApiKey,
4919
+ consumerId: this.consumerId,
4920
+ jwtData,
4921
+ });
4922
+ const auth = new ApiAuth(config, this.baseUrl, this.transport);
4923
+ if (jwtData)
4924
+ auth.setJwtData(jwtData);
4925
+ return auth;
4926
+ }
4927
+ /**
4928
+ * Legacy raw-key header echoed on every request alongside the mode's own headers, matching
4929
+ * the pre-mode wire behavior when the legacy apiKey option is used.
4930
+ *
4931
+ * @returns {{ header: string, value: string } | undefined} The raw key header, when a legacy apiKey is set.
4932
+ */
4933
+ rawKeyHeader() {
4934
+ if (this.legacyApiKey === undefined)
4935
+ return undefined;
4936
+ return typeof this.legacyApiKey === "string"
4937
+ ? { header: "X-Provable-API-Key", value: this.legacyApiKey }
4938
+ : this.legacyApiKey;
4939
+ }
4771
4940
  /**
4772
4941
  * Set the API key to use for the record scanner.
4773
4942
  *
4774
4943
  * @param {string | { header: string, value: string }} apiKey The API key to use for the record scanner.
4775
4944
  */
4776
4945
  setApiKey(apiKey) {
4777
- this.apiKey = typeof apiKey === "string" ? { header: "X-Provable-API-Key", value: apiKey } : apiKey;
4946
+ if (this.explicitAuth) {
4947
+ throw new Error("This scanner uses an explicit auth mode — replace it with setAuth instead of the legacy setApiKey");
4948
+ }
4949
+ this.legacyApiKey = apiKey;
4950
+ this.auth = this.buildAuth(this.auth.getJwtData());
4951
+ }
4952
+ /**
4953
+ * Set the explicit auth mode, replacing any legacy apiKey/consumerId configuration.
4954
+ *
4955
+ * @param {ApiAuthConfig} auth The auth mode and its material.
4956
+ */
4957
+ setAuth(auth) {
4958
+ normalizeAuthConfig({ auth });
4959
+ this.explicitAuth = auth;
4960
+ this.legacyApiKey = undefined;
4961
+ this.consumerId = undefined;
4962
+ this.auth = this.buildAuth();
4778
4963
  }
4779
4964
  /**
4780
4965
  * Set the consumer ID used for JWT refresh when using authenticated record scanner (e.g. Provable API).
@@ -4782,7 +4967,11 @@ class RecordScanner {
4782
4967
  * @param {string} consumerId The consumer ID to use for JWT refresh.
4783
4968
  */
4784
4969
  setConsumerId(consumerId) {
4970
+ if (this.explicitAuth) {
4971
+ throw new Error("This scanner uses an explicit auth mode — replace it with setAuth instead of the legacy setConsumerId");
4972
+ }
4785
4973
  this.consumerId = consumerId;
4974
+ this.auth = this.buildAuth(this.auth.getJwtData());
4786
4975
  }
4787
4976
  /**
4788
4977
  * Set JWT data for authentication. Optional; when not set, JWT can be refreshed from apiKey + consumerId if provided.
@@ -4790,7 +4979,7 @@ class RecordScanner {
4790
4979
  * @param {RecordScannerJWTData | undefined} jwtData The JWT data to use, or undefined to clear.
4791
4980
  */
4792
4981
  setJwtData(jwtData) {
4793
- this.jwtData = jwtData;
4982
+ this.auth.setJwtData(jwtData);
4794
4983
  }
4795
4984
  /**
4796
4985
  * Set whether /owned should automatically re-register on 422 (when a view key for the UUID is in viewKeys or account) and retry once.
@@ -4863,54 +5052,6 @@ class RecordScanner {
4863
5052
  this.removeViewKey(this.computeUUID(existingVk).toString());
4864
5053
  }
4865
5054
  }
4866
- /**
4867
- * Refreshes the JWT by making a POST request to /jwts/{consumer_id}. Used when authentication is required.
4868
- *
4869
- * @param {string} apiKey The API key to use for the refresh request.
4870
- * @param {string} consumerId The consumer ID for the JWT endpoint.
4871
- * @returns {Promise<RecordScannerJWTData>} The new JWT data.
4872
- */
4873
- async refreshJwt(apiKey, consumerId) {
4874
- const response = await post(`${this.baseUrl}/jwts/${consumerId}`, {
4875
- headers: {
4876
- "X-Provable-API-Key": apiKey,
4877
- },
4878
- }, this.transport);
4879
- const authHeader = response.headers.get("authorization");
4880
- if (!authHeader) {
4881
- throw new Error("No authorization header in JWT refresh response");
4882
- }
4883
- const body = await response.json();
4884
- return {
4885
- jwt: authHeader,
4886
- expiration: body.exp * 1000, // Convert to milliseconds
4887
- };
4888
- }
4889
- /**
4890
- * Returns auth headers (e.g. Authorization with JWT). Refreshes JWT if expired and apiKey + consumerId are set. Empty when auth is not configured.
4891
- *
4892
- * @returns {Promise<Record<string, string>>} Auth headers to add to requests, or empty object when not configured.
4893
- */
4894
- async getAuthHeaders() {
4895
- let jwtData = this.jwtData;
4896
- // Consider JWT expired a few minutes early to avoid race at boundary.
4897
- const isExpired = jwtData && Date.now() >= jwtData.expiration - FIVE_MINUTES;
4898
- if (!jwtData || isExpired) {
4899
- const apiKey = this.apiKey?.value;
4900
- if (apiKey && this.consumerId) {
4901
- jwtData = await this.refreshJwt(apiKey, this.consumerId);
4902
- this.jwtData = jwtData;
4903
- }
4904
- else if (jwtData?.jwt) {
4905
- // Use existing JWT even if expired when refresh is not possible.
4906
- return { Authorization: jwtData.jwt };
4907
- }
4908
- else {
4909
- return {};
4910
- }
4911
- }
4912
- return jwtData?.jwt ? { Authorization: jwtData.jwt } : {};
4913
- }
4914
5055
  /**
4915
5056
  * Set the UUID for the record scanner.
4916
5057
  *
@@ -5415,13 +5556,15 @@ class RecordScanner {
5415
5556
  */
5416
5557
  async request(req) {
5417
5558
  try {
5418
- // Attach JWT (if configured) and API key before sending.
5419
- const authHeaders = await this.getAuthHeaders();
5559
+ // Attach the mode's auth headers before sending.
5560
+ const authHeaders = await this.auth.headers();
5420
5561
  for (const [key, value] of Object.entries(authHeaders)) {
5421
5562
  req.headers.set(key, value);
5422
5563
  }
5423
- if (this.apiKey) {
5424
- req.headers.set(this.apiKey.header, this.apiKey.value);
5564
+ // Legacy apiKey callers also got their raw key echoed on every request.
5565
+ const rawKey = this.rawKeyHeader();
5566
+ if (rawKey) {
5567
+ req.headers.set(rawKey.header, rawKey.value);
5425
5568
  }
5426
5569
  const body = req.body ? await req.text() : undefined;
5427
5570
  const plainHeaders = {};
@@ -9618,9 +9761,11 @@ exports.Account = Account;
9618
9761
  exports.AleoKeyProvider = AleoKeyProvider;
9619
9762
  exports.AleoKeyProviderParams = AleoKeyProviderParams;
9620
9763
  exports.AleoNetworkClient = AleoNetworkClient;
9764
+ exports.ApiAuth = ApiAuth;
9621
9765
  exports.BlockHeightSearch = BlockHeightSearch;
9622
9766
  exports.CREDITS_PROGRAM_KEYS = CREDITS_PROGRAM_KEYS;
9623
9767
  exports.ChecksumMismatchError = KeyVerificationError;
9768
+ exports.DEFAULT_API_KEY_HEADER = DEFAULT_API_KEY_HEADER;
9624
9769
  exports.DecryptionNotEnabledError = DecryptionNotEnabledError;
9625
9770
  exports.IndexedDBKeyStore = IndexedDBKeyStore;
9626
9771
  exports.InvalidLocatorError = InvalidLocatorError;
@@ -9665,6 +9810,7 @@ exports.isProvingResponse = isProvingResponse;
9665
9810
  exports.isRecordViewKeyStrategy = isRecordViewKeyStrategy;
9666
9811
  exports.isViewKeyStrategy = isViewKeyStrategy;
9667
9812
  exports.logAndThrow = logAndThrow;
9813
+ exports.normalizeAuthConfig = normalizeAuthConfig;
9668
9814
  exports.programChecksum = programChecksum;
9669
9815
  exports.provingKeyLocator = provingKeyLocator;
9670
9816
  exports.serializeProvingRequest = serializeProvingRequest;