@secondlayer/sdk 10.1.1 → 10.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.
package/dist/index.js CHANGED
@@ -110,6 +110,39 @@ class ValidationError extends ApiError {
110
110
  this.name = "ValidationError";
111
111
  }
112
112
  }
113
+
114
+ class ArchiveSignatureError extends SecondLayerError {
115
+ constructor(message = "Archive manifest signature is missing or invalid.") {
116
+ super(message, { code: "ARCHIVE_SIGNATURE", retryable: false });
117
+ this.name = "ArchiveSignatureError";
118
+ }
119
+ }
120
+
121
+ class ArchiveAuthError extends SecondLayerError {
122
+ constructor(message = "Archive ops request was not authenticated.") {
123
+ super(message, { code: "ARCHIVE_AUTH", retryable: false });
124
+ this.name = "ArchiveAuthError";
125
+ }
126
+ }
127
+
128
+ class InsufficientArchiveCreditsError extends SecondLayerError {
129
+ shortfallUsdMicros;
130
+ constructor(message = "Insufficient archive credits.", shortfallUsdMicros) {
131
+ super(message, {
132
+ code: "INSUFFICIENT_ARCHIVE_CREDITS",
133
+ retryable: false
134
+ });
135
+ this.name = "InsufficientArchiveCreditsError";
136
+ this.shortfallUsdMicros = shortfallUsdMicros;
137
+ }
138
+ }
139
+
140
+ class ArchiveGateNotConfiguredError extends SecondLayerError {
141
+ constructor(message = "The archive gate is not configured on the server.") {
142
+ super(message, { code: "ARCHIVE_GATE_NOT_CONFIGURED", retryable: false });
143
+ this.name = "ArchiveGateNotConfiguredError";
144
+ }
145
+ }
113
146
  function parseRetryAfter(value) {
114
147
  if (!value)
115
148
  return;
@@ -139,33 +172,55 @@ function raceAbort(promise, signal) {
139
172
  });
140
173
  }
141
174
  var LOCAL_API_URL = "http://127.0.0.1:3800";
175
+ var INSTANCE_TOKEN_ENV = "INSTANCE_TOKEN";
176
+ var ACCOUNT_KEY_ENV = "SECONDLAYER_API_KEY";
177
+ var warnedUrlAlias = false;
142
178
  function resolveBaseUrl(explicit) {
143
179
  if (explicit && explicit.length > 0)
144
180
  return explicit.replace(/\/+$/, "");
145
181
  if (typeof process !== "undefined") {
146
- const fromEnv = process.env?.SL_API_URL || process.env?.SECONDLAYER_API_URL;
147
- if (fromEnv)
148
- return fromEnv.replace(/\/+$/, "");
182
+ const primary = process.env?.SECONDLAYER_API_URL || undefined;
183
+ if (primary)
184
+ return primary.replace(/\/+$/, "");
185
+ const legacy = process.env?.SL_API_URL || undefined;
186
+ if (legacy) {
187
+ if (!warnedUrlAlias) {
188
+ warnedUrlAlias = true;
189
+ console.warn("[secondlayer] SL_API_URL is a legacy alias of SECONDLAYER_API_URL; using the alias this once.");
190
+ }
191
+ return legacy.replace(/\/+$/, "");
192
+ }
149
193
  }
150
194
  return LOCAL_API_URL;
151
195
  }
152
- var CREDENTIAL_ENV_VARS = ["INSTANCE_TOKEN", "SL_API_KEY"];
153
- var warnedConflicts = new Set;
154
196
  function resolveApiKey(apiKey) {
155
197
  if (apiKey !== undefined)
156
198
  return apiKey;
157
199
  if (typeof process === "undefined")
158
200
  return;
159
- const instanceToken = process.env?.INSTANCE_TOKEN || undefined;
160
- const legacyKey = process.env?.SL_API_KEY || undefined;
161
- if (instanceToken && legacyKey && instanceToken !== legacyKey) {
162
- const pair = `${instanceToken}\x00${legacyKey}`;
163
- if (!warnedConflicts.has(pair)) {
164
- warnedConflicts.add(pair);
165
- console.warn("[secondlayer] INSTANCE_TOKEN and SL_API_KEY are set to different values — using INSTANCE_TOKEN (SL_API_KEY is a legacy alias for it).");
201
+ const token = process.env?.INSTANCE_TOKEN || undefined;
202
+ return token;
203
+ }
204
+ var warnedArchiveAlias = false;
205
+ function resolveAccountKey(accountKey) {
206
+ if (accountKey !== undefined)
207
+ return accountKey;
208
+ if (typeof process === "undefined")
209
+ return;
210
+ const primary = process.env?.SECONDLAYER_API_KEY || undefined;
211
+ const slAlias = process.env?.SL_API_KEY || undefined;
212
+ const archiveAlias = process.env?.SL_ARCHIVE_API_KEY || undefined;
213
+ if (primary)
214
+ return primary;
215
+ const legacy = slAlias ?? archiveAlias;
216
+ if (legacy) {
217
+ if (!warnedArchiveAlias) {
218
+ warnedArchiveAlias = true;
219
+ console.warn("[secondlayer] SL_API_KEY / SL_ARCHIVE_API_KEY are legacy aliases of SECONDLAYER_API_KEY; using the alias this once.");
166
220
  }
221
+ return legacy;
167
222
  }
168
- return instanceToken ?? legacyKey;
223
+ return;
169
224
  }
170
225
  function seg(value) {
171
226
  return encodeURIComponent(String(value));
@@ -814,6 +869,244 @@ class Subgraphs extends BaseClient {
814
869
  };
815
870
  }
816
871
  }
872
+ // src/archive/client.ts
873
+ import { createHash } from "node:crypto";
874
+ import {
875
+ checkSignature,
876
+ loadReference
877
+ } from "@secondlayer/shared/archive/reference";
878
+ var OFFICIAL_ARCHIVE_HOST = "archive.secondlayer.tools";
879
+ var DEFAULT_ARCHIVE_BASE_URL = "https://archive.secondlayer.tools";
880
+ var DEFAULT_ARCHIVE_OPS_URL = "https://api.secondlayer.tools";
881
+ var MAX_FETCH_BATCH = 64;
882
+ var ARCHIVE_CREDENTIAL_RE = /^s[ks]-sl_/;
883
+ function quoteFromBody(body) {
884
+ return {
885
+ partitions: body.partitions,
886
+ bundles: body.bundles,
887
+ usdMicros: body.usd_micros,
888
+ usd: body.usd,
889
+ freeAllowanceAppliedMicros: body.free_allowance_applied_micros,
890
+ allowanceRemainingBundles: body.allowance_remaining_bundles,
891
+ balanceUsdMicros: body.balance_usd_micros,
892
+ sufficient: body.sufficient
893
+ };
894
+ }
895
+ function stripSlash(url) {
896
+ return url.replace(/\/+$/, "");
897
+ }
898
+ function createArchiveClient(options = {}) {
899
+ const accountKey = resolveAccountKey(options.accountKey ?? options.apiKey);
900
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
901
+ const archiveBaseUrl = stripSlash(options.archiveBaseUrl ?? DEFAULT_ARCHIVE_BASE_URL);
902
+ const archiveOpsUrl = stripSlash(options.archiveOpsUrl ?? DEFAULT_ARCHIVE_OPS_URL);
903
+ const verifyManifest = options.verifyManifest ?? true;
904
+ const pinnedPem = options.publicKeyPem;
905
+ let keyPromise = null;
906
+ async function fetchSigningKey() {
907
+ if (pinnedPem)
908
+ return pinnedPem;
909
+ if (keyPromise)
910
+ return keyPromise;
911
+ const pending = (async () => {
912
+ const res = await fetchImpl(`${archiveOpsUrl}/public/streams/signing-key`);
913
+ if (!res.ok) {
914
+ throw new ArchiveSignatureError(`Could not fetch archive signing key (${res.status}).`);
915
+ }
916
+ const body = await res.json();
917
+ if (typeof body.public_key_pem !== "string") {
918
+ throw new ArchiveSignatureError("Signing key response missing key.");
919
+ }
920
+ return body.public_key_pem;
921
+ })();
922
+ keyPromise = pending;
923
+ pending.catch(() => {
924
+ if (keyPromise === pending)
925
+ keyPromise = null;
926
+ });
927
+ return pending;
928
+ }
929
+ async function throwOpsError(response) {
930
+ const envelope = parseErrorEnvelope(await response.text());
931
+ if (response.status === 401) {
932
+ throw new ArchiveAuthError(envelope.message ?? "Archive ops request was not authenticated.");
933
+ }
934
+ if (response.status === 402) {
935
+ const raw = envelope.body && typeof envelope.body === "object" && "shortfall_usd_micros" in envelope.body ? Number(envelope.body.shortfall_usd_micros) : undefined;
936
+ throw new InsufficientArchiveCreditsError(envelope.message ?? "Insufficient archive credits.", Number.isFinite(raw) ? raw : undefined);
937
+ }
938
+ if (response.status === 503) {
939
+ throw new ArchiveGateNotConfiguredError(envelope.message ?? "The archive gate is not configured on the server.");
940
+ }
941
+ throw new ApiError(response.status, envelope.message ?? `Archive ops returned ${response.status}.`, envelope.body, envelope.code);
942
+ }
943
+ async function opsRequest(method, path, body, authed = true) {
944
+ const headers = {
945
+ "Content-Type": "application/json"
946
+ };
947
+ if (authed && accountKey) {
948
+ if (!ARCHIVE_CREDENTIAL_RE.test(accountKey)) {
949
+ throw new ArchiveAuthError("archive ops need SECONDLAYER_API_KEY (sk-sl_*), not INSTANCE_TOKEN");
950
+ }
951
+ headers.Authorization = `Bearer ${accountKey}`;
952
+ }
953
+ const response = await fetchImpl(`${archiveOpsUrl}${path}`, {
954
+ method,
955
+ headers,
956
+ body: body !== undefined ? JSON.stringify(body) : undefined
957
+ });
958
+ if (!response.ok)
959
+ await throwOpsError(response);
960
+ return await response.json();
961
+ }
962
+ async function load(against, opts = {}) {
963
+ if (!/^https?:\/\//i.test(against)) {
964
+ throw new ValidationError("archive.load() accepts only http(s) URLs; local paths stay on the CLI.", 400);
965
+ }
966
+ const insecure = opts.insecure === true;
967
+ const publicKeyPem = pinnedPem ?? (verifyManifest && !insecure ? await fetchSigningKey() : undefined);
968
+ const ref = await loadReference(against, {
969
+ publicKeyPem,
970
+ fetchImpl
971
+ });
972
+ const signature = checkSignature(ref.manifest, publicKeyPem, insecure);
973
+ if (verifyManifest && !insecure && !signature.verified) {
974
+ throw new ArchiveSignatureError(signature.reason ?? "Archive manifest signature is missing or invalid.");
975
+ }
976
+ return { ...ref, signature };
977
+ }
978
+ async function latest(against, opts = {}) {
979
+ return load(against ?? `${archiveBaseUrl}/latest.json`, opts);
980
+ }
981
+ async function status() {
982
+ const response = await fetchImpl(`${archiveBaseUrl}/status.json`);
983
+ if (!response.ok) {
984
+ throw new ApiError(response.status, `Could not fetch archive status (${response.status}).`);
985
+ }
986
+ return response.json();
987
+ }
988
+ function partitions(ref, filter) {
989
+ let list = ref.manifest.partitions ?? [];
990
+ if (!filter)
991
+ return list;
992
+ if (filter.dataset) {
993
+ list = list.filter((p) => p.dataset === filter.dataset);
994
+ }
995
+ const fromBlock = filter.fromBlock;
996
+ if (fromBlock !== undefined) {
997
+ list = list.filter((p) => p.to_block >= fromBlock);
998
+ }
999
+ const toBlock = filter.toBlock;
1000
+ if (toBlock !== undefined) {
1001
+ list = list.filter((p) => p.from_block <= toBlock);
1002
+ }
1003
+ return list;
1004
+ }
1005
+ async function quote(input) {
1006
+ const body = await opsRequest("POST", "/api/archive/quote", { paths: input.paths, flow: input.flow });
1007
+ return quoteFromBody(body);
1008
+ }
1009
+ async function fetchPartitions(input) {
1010
+ const urls = [];
1011
+ let chargedTotalUsdMicros = 0;
1012
+ let balanceAfterUsdMicros = 0;
1013
+ for (let i = 0;i < input.paths.length; i += MAX_FETCH_BATCH) {
1014
+ const chunk = input.paths.slice(i, i + MAX_FETCH_BATCH);
1015
+ const body = await opsRequest("POST", "/api/archive/fetch", { paths: chunk, flow: input.flow });
1016
+ for (const item of body.urls) {
1017
+ urls.push({
1018
+ path: item.path,
1019
+ url: item.url,
1020
+ expiresAt: item.expires_at,
1021
+ chargedUsdMicros: item.charged_usd_micros
1022
+ });
1023
+ }
1024
+ chargedTotalUsdMicros += body.charged_total_usd_micros;
1025
+ balanceAfterUsdMicros = body.balance_after_usd_micros;
1026
+ }
1027
+ return { urls, chargedTotalUsdMicros, balanceAfterUsdMicros };
1028
+ }
1029
+ async function download(partition, opts) {
1030
+ let url = opts?.url;
1031
+ if (!url) {
1032
+ let official = false;
1033
+ try {
1034
+ official = new URL(archiveBaseUrl).hostname === OFFICIAL_ARCHIVE_HOST;
1035
+ } catch {
1036
+ official = false;
1037
+ }
1038
+ if (official) {
1039
+ throw new ValidationError("Official archive host requires a presigned URL from archive.fetch() before download.", 400);
1040
+ }
1041
+ url = `${archiveBaseUrl}/${partition.path.replace(/^\/+/, "")}`;
1042
+ }
1043
+ const response = await fetchImpl(url);
1044
+ if (!response.ok) {
1045
+ throw new ApiError(response.status, `Could not download ${partition.path} (${response.status}).`);
1046
+ }
1047
+ const bytes = new Uint8Array(await response.arrayBuffer());
1048
+ const digest = createHash("sha256").update(bytes).digest("hex");
1049
+ if (digest !== partition.sha256) {
1050
+ throw new ArchiveSignatureError(`${partition.path} sha256 mismatch (expected ${partition.sha256}, got ${digest}).`);
1051
+ }
1052
+ return bytes;
1053
+ }
1054
+ return {
1055
+ latest,
1056
+ load,
1057
+ status,
1058
+ partitions,
1059
+ quote,
1060
+ fetch: fetchPartitions,
1061
+ download,
1062
+ credits: {
1063
+ async balance() {
1064
+ const body = await opsRequest("GET", "/api/billing/status");
1065
+ return {
1066
+ creditsUsdMicros: body.creditsUsdMicros,
1067
+ refill: body.refill
1068
+ };
1069
+ },
1070
+ async checkout(input) {
1071
+ const body = await opsRequest("POST", "/api/public/credits/checkout", { email: input.email, amount: input.pack }, false);
1072
+ return { url: body.url };
1073
+ },
1074
+ async refill(input) {
1075
+ const payload = "off" in input && input.off ? { belowUsd: null } : {
1076
+ belowUsd: input.belowUsd,
1077
+ packUsd: input.packUsd
1078
+ };
1079
+ const body = await opsRequest("POST", "/api/billing/refill", payload);
1080
+ return { belowUsd: body.belowUsd, packUsd: body.packUsd };
1081
+ }
1082
+ }
1083
+ };
1084
+ }
1085
+
1086
+ // src/archive/instance.ts
1087
+ import {
1088
+ diagnoseInstanceStatus
1089
+ } from "@secondlayer/shared/archive/instance-diagnosis";
1090
+ function createArchiveVerify(request) {
1091
+ return (input) => request("POST", "/v1/archive/verify", {
1092
+ against: input.against,
1093
+ target: input.target ?? "raw",
1094
+ from_block: input.fromBlock,
1095
+ to_block: input.toBlock,
1096
+ insecure: input.insecure,
1097
+ public_key_pem: input.publicKeyPem
1098
+ });
1099
+ }
1100
+ function createInstanceClient(request) {
1101
+ async function status() {
1102
+ return request("GET", "/public/status");
1103
+ }
1104
+ async function diagnose() {
1105
+ return diagnoseInstanceStatus(await status());
1106
+ }
1107
+ return { status, diagnose };
1108
+ }
1109
+
817
1110
  // src/contracts/client.ts
818
1111
  class Contracts extends BaseClient {
819
1112
  constructor(options = {}) {
@@ -1786,7 +2079,7 @@ function decode(event) {
1786
2079
  }
1787
2080
 
1788
2081
  // src/streams/dumps.ts
1789
- import { createHash } from "node:crypto";
2082
+ import { createHash as createHash2 } from "node:crypto";
1790
2083
  import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
1791
2084
  function createStreamsDumps(opts) {
1792
2085
  const baseUrl = opts.baseUrl?.replace(/\/+$/, "");
@@ -1838,7 +2131,7 @@ function createStreamsDumps(opts) {
1838
2131
  if (!res.body) {
1839
2132
  throw new StreamsServerError(`Dump ${file.path} response has no body.`, 0);
1840
2133
  }
1841
- const hash = createHash("sha256");
2134
+ const hash = createHash2("sha256");
1842
2135
  const chunks = [];
1843
2136
  let total = 0;
1844
2137
  const reader = res.body.getReader();
@@ -2232,6 +2525,8 @@ class SecondLayer extends BaseClient {
2232
2525
  contracts;
2233
2526
  subgraphs;
2234
2527
  subscriptions;
2528
+ archive;
2529
+ instance;
2235
2530
  constructor(options = {}) {
2236
2531
  super(options);
2237
2532
  this.streams = createStreamsClient({
@@ -2247,17 +2542,36 @@ class SecondLayer extends BaseClient {
2247
2542
  this.contracts = new Contracts(options);
2248
2543
  this.subgraphs = new Subgraphs(options);
2249
2544
  this.subscriptions = new Subscriptions(options);
2545
+ const request = (method, path, body) => this.request(method, path, body);
2546
+ this.archive = {
2547
+ ...createArchiveClient({
2548
+ accountKey: options.accountKey,
2549
+ fetchImpl: options.fetchImpl,
2550
+ archiveBaseUrl: options.archiveBaseUrl,
2551
+ archiveOpsUrl: options.archiveOpsUrl
2552
+ }),
2553
+ verify: createArchiveVerify(request)
2554
+ };
2555
+ this.instance = createInstanceClient(request);
2250
2556
  }
2251
2557
  async batch(requests) {
2252
2558
  return this.request("POST", "/v1/batch", { requests });
2253
2559
  }
2254
2560
  async context() {
2255
- const [account, streamsTip, indexEnv, subgraphsRes, subscriptionsRes] = await Promise.all([
2561
+ const [
2562
+ account,
2563
+ streamsTip,
2564
+ indexEnv,
2565
+ subgraphsRes,
2566
+ subscriptionsRes,
2567
+ instance
2568
+ ] = await Promise.all([
2256
2569
  contextField(this.request("GET", "/api/accounts/me")),
2257
2570
  contextField(this.streams.tip()),
2258
2571
  contextField(this.index.canonical.list({ limit: 1 })),
2259
2572
  contextField(this.subgraphs.list()),
2260
- contextField(this.subscriptions.list())
2573
+ contextField(this.subscriptions.list()),
2574
+ contextField(this.instance.diagnose())
2261
2575
  ]);
2262
2576
  const subgraphs = {
2263
2577
  value: subgraphsRes.value?.data ?? null,
@@ -2309,7 +2623,8 @@ class SecondLayer extends BaseClient {
2309
2623
  },
2310
2624
  subgraphs,
2311
2625
  subscriptions,
2312
- activeOperations
2626
+ activeOperations,
2627
+ instance
2313
2628
  };
2314
2629
  }
2315
2630
  }
@@ -2562,6 +2877,7 @@ export {
2562
2877
  shutdownSignal,
2563
2878
  resolveBaseUrl,
2564
2879
  resolveApiKey,
2880
+ resolveAccountKey,
2565
2881
  parseRetryAfter,
2566
2882
  isStxTransfer,
2567
2883
  isStxMint,
@@ -2591,6 +2907,7 @@ export {
2591
2907
  decodeChainWebhook,
2592
2908
  decode,
2593
2909
  createStreamsClient,
2910
+ createArchiveClient,
2594
2911
  contextField,
2595
2912
  consumerHealth,
2596
2913
  consumeIndexFeed,
@@ -2603,15 +2920,20 @@ export {
2603
2920
  SecondLayer,
2604
2921
  RateLimitError,
2605
2922
  LOCAL_API_URL,
2923
+ InsufficientArchiveCreditsError,
2606
2924
  Index,
2925
+ INSTANCE_TOKEN_ENV,
2607
2926
  INDEX_MAX_PAGE_SIZE,
2608
2927
  DEFAULT_REQUEST_TIMEOUT_MS,
2609
2928
  Cursor,
2610
2929
  Contracts,
2611
- CREDENTIAL_ENV_VARS,
2612
2930
  AuthError,
2613
- ApiError
2931
+ ArchiveSignatureError,
2932
+ ArchiveGateNotConfiguredError,
2933
+ ArchiveAuthError,
2934
+ ApiError,
2935
+ ACCOUNT_KEY_ENV
2614
2936
  };
2615
2937
 
2616
- //# debugId=CDF488058C75012864756E2164756E21
2938
+ //# debugId=4C4D7DCC9A7B5FE064756E2164756E21
2617
2939
  //# sourceMappingURL=index.js.map