@secondlayer/sdk 10.1.0 → 10.2.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.
@@ -14,6 +14,12 @@ interface SecondLayerOptions {
14
14
  * Required for `streams.dumps.*`; without it the dumps client falls back to
15
15
  * its built-in default. */
16
16
  dumpsBaseUrl?: string;
17
+ /** Public base URL for the signed canonical archive tree. Defaults to
18
+ * `https://archive.secondlayer.tools`. Reaches `sl.archive`. */
19
+ archiveBaseUrl?: string;
20
+ /** Credits and fetch-gate API. Defaults to `https://api.secondlayer.tools`,
21
+ * not `baseUrl`. Reaches `sl.archive` quote/fetch/credits. */
22
+ archiveOpsUrl?: string;
17
23
  /** Deploy origin label sent as `x-sl-origin` (telemetry). Defaults to `cli`. */
18
24
  origin?: "cli" | "mcp" | "session";
19
25
  /** Check the ed25519 signature on every Streams read. Omit for lenient
@@ -207,7 +213,156 @@ declare class Subgraphs extends BaseClient {
207
213
  private createTableClient;
208
214
  }
209
215
  import { InferSubgraphClient as InferSubgraphClient2 } from "@secondlayer/subgraphs";
216
+ import { InstanceDiagnosis as InstanceDiagnosis2 } from "@secondlayer/shared/archive/instance-diagnosis";
210
217
  import { SubgraphSummary as SubgraphSummary2 } from "@secondlayer/shared/schemas";
218
+ import { InstanceDiagnosis, PublicStatus } from "@secondlayer/shared/archive/instance-diagnosis";
219
+ /** Structural `/public/status` payload. Extra fields are ignored by diagnosis. */
220
+ type InstanceStatus = PublicStatus;
221
+ type InstanceClient = {
222
+ status(): Promise<InstanceStatus>;
223
+ diagnose(): Promise<InstanceDiagnosis>;
224
+ };
225
+ type ArchiveVerifyInput = {
226
+ against: string;
227
+ target?: string;
228
+ fromBlock?: number;
229
+ toBlock?: number;
230
+ insecure?: boolean;
231
+ publicKeyPem?: string;
232
+ };
233
+ type ArchiveVerifyRangeStatus = "match" | "digest-mismatch" | "count-mismatch" | "missing";
234
+ type ArchiveVerifyResult = {
235
+ status: "clean" | "diverged" | "unanchored";
236
+ target: string;
237
+ against: string;
238
+ signature: {
239
+ verified: boolean;
240
+ reason?: string;
241
+ };
242
+ coverage?: {
243
+ from_block: number;
244
+ to_block: number;
245
+ };
246
+ ranges: Array<{
247
+ dataset: string;
248
+ from_block: number;
249
+ to_block: number;
250
+ status: ArchiveVerifyRangeStatus;
251
+ expected_digest: string | null;
252
+ actual_digest: string | null;
253
+ }>;
254
+ reason?: string;
255
+ };
256
+ import { RangeDigest } from "@secondlayer/shared/archive/range-digest";
257
+ type ArchiveDataset = "blocks" | "transactions" | "events";
258
+ type ArchiveFlow = "bootstrap" | "repair";
259
+ type ArchivePartition = {
260
+ dataset: string;
261
+ from_block: number;
262
+ to_block: number;
263
+ path: string;
264
+ row_count: number;
265
+ byte_size: number;
266
+ sha256: string;
267
+ };
268
+ type ArchiveManifest = {
269
+ network?: string;
270
+ coverage?: {
271
+ from_block: number;
272
+ to_block: number;
273
+ };
274
+ partition_size_blocks?: number;
275
+ range_digests?: RangeDigest[];
276
+ partitions?: ArchivePartition[];
277
+ signature?: string;
278
+ key_id?: string;
279
+ [key: string]: unknown;
280
+ };
281
+ type LoadedArchive = {
282
+ manifest: ArchiveManifest;
283
+ origin: string;
284
+ root: string;
285
+ isRemote: boolean;
286
+ signature: {
287
+ verified: boolean;
288
+ reason?: string;
289
+ };
290
+ };
291
+ type ArchiveQuote = {
292
+ partitions: number;
293
+ bundles: number;
294
+ usdMicros: number;
295
+ usd: string;
296
+ freeAllowanceAppliedMicros: number;
297
+ allowanceRemainingBundles: number;
298
+ balanceUsdMicros: number;
299
+ sufficient: boolean;
300
+ };
301
+ type ArchiveFetchItem = {
302
+ path: string;
303
+ url: string;
304
+ expiresAt: string;
305
+ chargedUsdMicros: number;
306
+ };
307
+ type ArchiveFetchResult = {
308
+ urls: ArchiveFetchItem[];
309
+ chargedTotalUsdMicros: number;
310
+ balanceAfterUsdMicros: number;
311
+ };
312
+ type ArchiveCreditsBalance = {
313
+ creditsUsdMicros: string;
314
+ refill: {
315
+ belowUsd: number | null;
316
+ packUsd: number | null;
317
+ lastAt: string | null;
318
+ };
319
+ };
320
+ type ArchiveLoadOptions = {
321
+ insecure?: boolean;
322
+ };
323
+ type ArchiveClient = {
324
+ latest(against?: string, opts?: ArchiveLoadOptions): Promise<LoadedArchive>;
325
+ load(against: string, opts?: ArchiveLoadOptions): Promise<LoadedArchive>;
326
+ status(): Promise<unknown>;
327
+ partitions(ref: LoadedArchive, filter?: {
328
+ dataset?: ArchiveDataset;
329
+ fromBlock?: number;
330
+ toBlock?: number;
331
+ }): ArchivePartition[];
332
+ quote(input: {
333
+ paths: string[];
334
+ flow: ArchiveFlow;
335
+ }): Promise<ArchiveQuote>;
336
+ fetch(input: {
337
+ paths: string[];
338
+ flow: ArchiveFlow;
339
+ }): Promise<ArchiveFetchResult>;
340
+ download(partition: ArchivePartition, opts?: {
341
+ url?: string;
342
+ }): Promise<Uint8Array>;
343
+ credits: {
344
+ balance(): Promise<ArchiveCreditsBalance>;
345
+ checkout(input: {
346
+ email: string;
347
+ pack: 10 | 25 | 50 | 100;
348
+ }): Promise<{
349
+ url: string;
350
+ }>;
351
+ refill(input: {
352
+ belowUsd: number;
353
+ packUsd: 10 | 25 | 50 | 100;
354
+ } | {
355
+ off: true;
356
+ }): Promise<{
357
+ belowUsd: number | null;
358
+ packUsd: number | null;
359
+ }>;
360
+ };
361
+ };
362
+ /** Hosted archive client plus instance `verify`. `status()` is still the public archive tree. */
363
+ type SecondLayerArchive = ArchiveClient & {
364
+ verify(input: ArchiveVerifyInput): Promise<ArchiveVerifyResult>;
365
+ };
211
366
  /**
212
367
  * Typed client for the contract-discovery API (`GET /v1/contracts`).
213
368
  *
@@ -2386,6 +2541,8 @@ interface ContextSnapshot {
2386
2541
  }>;
2387
2542
  /** In-flight reindex operations (bounded to subgraphs reporting `reindexing`). */
2388
2543
  activeOperations: ContextField<ActiveSubgraphOperation[]>;
2544
+ /** Decoder lag / empty-index from `GET /public/status`. */
2545
+ instance: ContextField<InstanceDiagnosis2>;
2389
2546
  }
2390
2547
  declare class SecondLayer extends BaseClient {
2391
2548
  readonly streams: StreamsClient;
@@ -2393,6 +2550,8 @@ declare class SecondLayer extends BaseClient {
2393
2550
  readonly contracts: Contracts;
2394
2551
  readonly subgraphs: Subgraphs;
2395
2552
  readonly subscriptions: Subscriptions;
2553
+ readonly archive: SecondLayerArchive;
2554
+ readonly instance: InstanceClient;
2396
2555
  constructor(options?: Partial<SecondLayerOptions>);
2397
2556
  /**
2398
2557
  * Up to 10 public reads in one round trip (`POST /v1/batch`). Each item
@@ -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;
@@ -814,6 +847,239 @@ class Subgraphs extends BaseClient {
814
847
  };
815
848
  }
816
849
  }
850
+ // src/archive/client.ts
851
+ import { createHash } from "node:crypto";
852
+ import {
853
+ checkSignature,
854
+ loadReference
855
+ } from "@secondlayer/shared/archive/reference";
856
+ var OFFICIAL_ARCHIVE_HOST = "archive.secondlayer.tools";
857
+ var DEFAULT_ARCHIVE_BASE_URL = "https://archive.secondlayer.tools";
858
+ var DEFAULT_ARCHIVE_OPS_URL = "https://api.secondlayer.tools";
859
+ var MAX_FETCH_BATCH = 64;
860
+ function quoteFromBody(body) {
861
+ return {
862
+ partitions: body.partitions,
863
+ bundles: body.bundles,
864
+ usdMicros: body.usd_micros,
865
+ usd: body.usd,
866
+ freeAllowanceAppliedMicros: body.free_allowance_applied_micros,
867
+ allowanceRemainingBundles: body.allowance_remaining_bundles,
868
+ balanceUsdMicros: body.balance_usd_micros,
869
+ sufficient: body.sufficient
870
+ };
871
+ }
872
+ function stripSlash(url) {
873
+ return url.replace(/\/+$/, "");
874
+ }
875
+ function createArchiveClient(options = {}) {
876
+ const apiKey = resolveApiKey(options.apiKey);
877
+ const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
878
+ const archiveBaseUrl = stripSlash(options.archiveBaseUrl ?? DEFAULT_ARCHIVE_BASE_URL);
879
+ const archiveOpsUrl = stripSlash(options.archiveOpsUrl ?? DEFAULT_ARCHIVE_OPS_URL);
880
+ const verifyManifest = options.verifyManifest ?? true;
881
+ const pinnedPem = options.publicKeyPem;
882
+ let keyPromise = null;
883
+ async function fetchSigningKey() {
884
+ if (pinnedPem)
885
+ return pinnedPem;
886
+ if (keyPromise)
887
+ return keyPromise;
888
+ const pending = (async () => {
889
+ const res = await fetchImpl(`${archiveOpsUrl}/public/streams/signing-key`);
890
+ if (!res.ok) {
891
+ throw new ArchiveSignatureError(`Could not fetch archive signing key (${res.status}).`);
892
+ }
893
+ const body = await res.json();
894
+ if (typeof body.public_key_pem !== "string") {
895
+ throw new ArchiveSignatureError("Signing key response missing key.");
896
+ }
897
+ return body.public_key_pem;
898
+ })();
899
+ keyPromise = pending;
900
+ pending.catch(() => {
901
+ if (keyPromise === pending)
902
+ keyPromise = null;
903
+ });
904
+ return pending;
905
+ }
906
+ async function throwOpsError(response) {
907
+ const envelope = parseErrorEnvelope(await response.text());
908
+ if (response.status === 401) {
909
+ throw new ArchiveAuthError(envelope.message ?? "Archive ops request was not authenticated.");
910
+ }
911
+ if (response.status === 402) {
912
+ const raw = envelope.body && typeof envelope.body === "object" && "shortfall_usd_micros" in envelope.body ? Number(envelope.body.shortfall_usd_micros) : undefined;
913
+ throw new InsufficientArchiveCreditsError(envelope.message ?? "Insufficient archive credits.", Number.isFinite(raw) ? raw : undefined);
914
+ }
915
+ if (response.status === 503) {
916
+ throw new ArchiveGateNotConfiguredError(envelope.message ?? "The archive gate is not configured on the server.");
917
+ }
918
+ throw new ApiError(response.status, envelope.message ?? `Archive ops returned ${response.status}.`, envelope.body, envelope.code);
919
+ }
920
+ async function opsRequest(method, path, body, authed = true) {
921
+ const headers = {
922
+ "Content-Type": "application/json"
923
+ };
924
+ if (authed && apiKey)
925
+ headers.Authorization = `Bearer ${apiKey}`;
926
+ const response = await fetchImpl(`${archiveOpsUrl}${path}`, {
927
+ method,
928
+ headers,
929
+ body: body !== undefined ? JSON.stringify(body) : undefined
930
+ });
931
+ if (!response.ok)
932
+ await throwOpsError(response);
933
+ return await response.json();
934
+ }
935
+ async function load(against, opts = {}) {
936
+ if (!/^https?:\/\//i.test(against)) {
937
+ throw new ValidationError("archive.load() accepts only http(s) URLs; local paths stay on the CLI.", 400);
938
+ }
939
+ const insecure = opts.insecure === true;
940
+ const publicKeyPem = pinnedPem ?? (verifyManifest && !insecure ? await fetchSigningKey() : undefined);
941
+ const ref = await loadReference(against, {
942
+ publicKeyPem,
943
+ fetchImpl
944
+ });
945
+ const signature = checkSignature(ref.manifest, publicKeyPem, insecure);
946
+ if (verifyManifest && !insecure && !signature.verified) {
947
+ throw new ArchiveSignatureError(signature.reason ?? "Archive manifest signature is missing or invalid.");
948
+ }
949
+ return { ...ref, signature };
950
+ }
951
+ async function latest(against, opts = {}) {
952
+ return load(against ?? `${archiveBaseUrl}/latest.json`, opts);
953
+ }
954
+ async function status() {
955
+ const response = await fetchImpl(`${archiveBaseUrl}/status.json`);
956
+ if (!response.ok) {
957
+ throw new ApiError(response.status, `Could not fetch archive status (${response.status}).`);
958
+ }
959
+ return response.json();
960
+ }
961
+ function partitions(ref, filter) {
962
+ let list = ref.manifest.partitions ?? [];
963
+ if (!filter)
964
+ return list;
965
+ if (filter.dataset) {
966
+ list = list.filter((p) => p.dataset === filter.dataset);
967
+ }
968
+ const fromBlock = filter.fromBlock;
969
+ if (fromBlock !== undefined) {
970
+ list = list.filter((p) => p.to_block >= fromBlock);
971
+ }
972
+ const toBlock = filter.toBlock;
973
+ if (toBlock !== undefined) {
974
+ list = list.filter((p) => p.from_block <= toBlock);
975
+ }
976
+ return list;
977
+ }
978
+ async function quote(input) {
979
+ const body = await opsRequest("POST", "/api/archive/quote", { paths: input.paths, flow: input.flow });
980
+ return quoteFromBody(body);
981
+ }
982
+ async function fetchPartitions(input) {
983
+ const urls = [];
984
+ let chargedTotalUsdMicros = 0;
985
+ let balanceAfterUsdMicros = 0;
986
+ for (let i = 0;i < input.paths.length; i += MAX_FETCH_BATCH) {
987
+ const chunk = input.paths.slice(i, i + MAX_FETCH_BATCH);
988
+ const body = await opsRequest("POST", "/api/archive/fetch", { paths: chunk, flow: input.flow });
989
+ for (const item of body.urls) {
990
+ urls.push({
991
+ path: item.path,
992
+ url: item.url,
993
+ expiresAt: item.expires_at,
994
+ chargedUsdMicros: item.charged_usd_micros
995
+ });
996
+ }
997
+ chargedTotalUsdMicros += body.charged_total_usd_micros;
998
+ balanceAfterUsdMicros = body.balance_after_usd_micros;
999
+ }
1000
+ return { urls, chargedTotalUsdMicros, balanceAfterUsdMicros };
1001
+ }
1002
+ async function download(partition, opts) {
1003
+ let url = opts?.url;
1004
+ if (!url) {
1005
+ let official = false;
1006
+ try {
1007
+ official = new URL(archiveBaseUrl).hostname === OFFICIAL_ARCHIVE_HOST;
1008
+ } catch {
1009
+ official = false;
1010
+ }
1011
+ if (official) {
1012
+ throw new ValidationError("Official archive host requires a presigned URL from archive.fetch() before download.", 400);
1013
+ }
1014
+ url = `${archiveBaseUrl}/${partition.path.replace(/^\/+/, "")}`;
1015
+ }
1016
+ const response = await fetchImpl(url);
1017
+ if (!response.ok) {
1018
+ throw new ApiError(response.status, `Could not download ${partition.path} (${response.status}).`);
1019
+ }
1020
+ const bytes = new Uint8Array(await response.arrayBuffer());
1021
+ const digest = createHash("sha256").update(bytes).digest("hex");
1022
+ if (digest !== partition.sha256) {
1023
+ throw new ArchiveSignatureError(`${partition.path} sha256 mismatch (expected ${partition.sha256}, got ${digest}).`);
1024
+ }
1025
+ return bytes;
1026
+ }
1027
+ return {
1028
+ latest,
1029
+ load,
1030
+ status,
1031
+ partitions,
1032
+ quote,
1033
+ fetch: fetchPartitions,
1034
+ download,
1035
+ credits: {
1036
+ async balance() {
1037
+ const body = await opsRequest("GET", "/api/billing/status");
1038
+ return {
1039
+ creditsUsdMicros: body.creditsUsdMicros,
1040
+ refill: body.refill
1041
+ };
1042
+ },
1043
+ async checkout(input) {
1044
+ const body = await opsRequest("POST", "/api/public/credits/checkout", { email: input.email, amount: input.pack }, false);
1045
+ return { url: body.url };
1046
+ },
1047
+ async refill(input) {
1048
+ const payload = "off" in input && input.off ? { belowUsd: null } : {
1049
+ belowUsd: input.belowUsd,
1050
+ packUsd: input.packUsd
1051
+ };
1052
+ const body = await opsRequest("POST", "/api/billing/refill", payload);
1053
+ return { belowUsd: body.belowUsd, packUsd: body.packUsd };
1054
+ }
1055
+ }
1056
+ };
1057
+ }
1058
+
1059
+ // src/archive/instance.ts
1060
+ import {
1061
+ diagnoseInstanceStatus
1062
+ } from "@secondlayer/shared/archive/instance-diagnosis";
1063
+ function createArchiveVerify(request) {
1064
+ return (input) => request("POST", "/v1/archive/verify", {
1065
+ against: input.against,
1066
+ target: input.target ?? "raw",
1067
+ from_block: input.fromBlock,
1068
+ to_block: input.toBlock,
1069
+ insecure: input.insecure,
1070
+ public_key_pem: input.publicKeyPem
1071
+ });
1072
+ }
1073
+ function createInstanceClient(request) {
1074
+ async function status() {
1075
+ return request("GET", "/public/status");
1076
+ }
1077
+ async function diagnose() {
1078
+ return diagnoseInstanceStatus(await status());
1079
+ }
1080
+ return { status, diagnose };
1081
+ }
1082
+
817
1083
  // src/contracts/client.ts
818
1084
  class Contracts extends BaseClient {
819
1085
  constructor(options = {}) {
@@ -1786,7 +2052,7 @@ function decode(event) {
1786
2052
  }
1787
2053
 
1788
2054
  // src/streams/dumps.ts
1789
- import { createHash } from "node:crypto";
2055
+ import { createHash as createHash2 } from "node:crypto";
1790
2056
  import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
1791
2057
  function createStreamsDumps(opts) {
1792
2058
  const baseUrl = opts.baseUrl?.replace(/\/+$/, "");
@@ -1838,7 +2104,7 @@ function createStreamsDumps(opts) {
1838
2104
  if (!res.body) {
1839
2105
  throw new StreamsServerError(`Dump ${file.path} response has no body.`, 0);
1840
2106
  }
1841
- const hash = createHash("sha256");
2107
+ const hash = createHash2("sha256");
1842
2108
  const chunks = [];
1843
2109
  let total = 0;
1844
2110
  const reader = res.body.getReader();
@@ -2232,6 +2498,8 @@ class SecondLayer extends BaseClient {
2232
2498
  contracts;
2233
2499
  subgraphs;
2234
2500
  subscriptions;
2501
+ archive;
2502
+ instance;
2235
2503
  constructor(options = {}) {
2236
2504
  super(options);
2237
2505
  this.streams = createStreamsClient({
@@ -2247,17 +2515,36 @@ class SecondLayer extends BaseClient {
2247
2515
  this.contracts = new Contracts(options);
2248
2516
  this.subgraphs = new Subgraphs(options);
2249
2517
  this.subscriptions = new Subscriptions(options);
2518
+ const request = (method, path, body) => this.request(method, path, body);
2519
+ this.archive = {
2520
+ ...createArchiveClient({
2521
+ apiKey: this.apiKey,
2522
+ fetchImpl: options.fetchImpl,
2523
+ archiveBaseUrl: options.archiveBaseUrl,
2524
+ archiveOpsUrl: options.archiveOpsUrl
2525
+ }),
2526
+ verify: createArchiveVerify(request)
2527
+ };
2528
+ this.instance = createInstanceClient(request);
2250
2529
  }
2251
2530
  async batch(requests) {
2252
2531
  return this.request("POST", "/v1/batch", { requests });
2253
2532
  }
2254
2533
  async context() {
2255
- const [account, streamsTip, indexEnv, subgraphsRes, subscriptionsRes] = await Promise.all([
2534
+ const [
2535
+ account,
2536
+ streamsTip,
2537
+ indexEnv,
2538
+ subgraphsRes,
2539
+ subscriptionsRes,
2540
+ instance
2541
+ ] = await Promise.all([
2256
2542
  contextField(this.request("GET", "/api/accounts/me")),
2257
2543
  contextField(this.streams.tip()),
2258
2544
  contextField(this.index.canonical.list({ limit: 1 })),
2259
2545
  contextField(this.subgraphs.list()),
2260
- contextField(this.subscriptions.list())
2546
+ contextField(this.subscriptions.list()),
2547
+ contextField(this.instance.diagnose())
2261
2548
  ]);
2262
2549
  const subgraphs = {
2263
2550
  value: subgraphsRes.value?.data ?? null,
@@ -2309,7 +2596,8 @@ class SecondLayer extends BaseClient {
2309
2596
  },
2310
2597
  subgraphs,
2311
2598
  subscriptions,
2312
- activeOperations
2599
+ activeOperations,
2600
+ instance
2313
2601
  };
2314
2602
  }
2315
2603
  }
@@ -2329,5 +2617,5 @@ export {
2329
2617
  Subgraphs
2330
2618
  };
2331
2619
 
2332
- //# debugId=10070F0E5755447564756E2164756E21
2620
+ //# debugId=59EE80259E1C9AEC64756E2164756E21
2333
2621
  //# sourceMappingURL=index.js.map