@secondlayer/sdk 6.13.0 → 6.15.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/README.md CHANGED
@@ -128,6 +128,31 @@ for await (const event of streams.events.stream({
128
128
  }
129
129
  ```
130
130
 
131
+ Real-time push (`events.subscribe`).
132
+
133
+ Use `client.events.subscribe` for callback-style live delivery — it pushes each
134
+ event to `onEvent` as it lands. It's fetch-based (so it carries the Bearer key)
135
+ and works in browsers and Node 18+. It auto-reconnects from the last delivered
136
+ cursor on a dropped connection, and returns an unsubscribe function.
137
+
138
+ ```typescript
139
+ const unsubscribe = streams.events.subscribe({
140
+ types: ["ft_transfer"], // notTypes / contractId / sender / recipient / assetIdentifier also filter
141
+ // fromCursor: lastCursor, // resume strictly after this cursor; omit to tail from the tip
142
+ onEvent: async (event) => {
143
+ console.log(event.cursor, event.tx_id);
144
+ },
145
+ onError: (err) => console.error("reconnecting…", err),
146
+ });
147
+
148
+ // later
149
+ unsubscribe(); // or pass `signal` and abort it
150
+ ```
151
+
152
+ Each pushed frame is `{ event, sig, key_id }`. When the client was created with
153
+ `verify` (or `{ publicKey }`), the per-frame ed25519 signature is checked before
154
+ `onEvent` runs; a bad/missing signature throws `StreamsSignatureError`.
155
+
131
156
  Bulk parquet dumps.
132
157
 
133
158
  Finalized history is published as public parquet files. Set `dumpsBaseUrl`
@@ -135,6 +160,12 @@ Finalized history is published as public parquet files. Set `dumpsBaseUrl`
135
160
  decode parquet; `download` hands you sha256-verified bytes to process with your
136
161
  own tooling.
137
162
 
163
+ The bulk **manifest is ed25519-signed**, and the SDK verifies that signature
164
+ before it trusts any per-file sha256 listed in it. The `verifyDumpsManifest`
165
+ option **defaults to `true`** — `dumps.list()` and `events.replay()` enforce it,
166
+ so you don't trust the file hashes unless the manifest itself verifies. Opt out
167
+ with `verifyDumpsManifest: false`.
168
+
138
169
  ```typescript
139
170
  const streams = createStreamsClient({
140
171
  apiKey: process.env.SL_API_KEY!,
@@ -317,6 +348,30 @@ const gaps = await sl.subgraphs.gaps("my-subgraph");
317
348
  const result = await sl.subgraphs.deploy({ name, sources, schema, handlerCode });
318
349
  ```
319
350
 
351
+ Stream rows live with the typed client — each table exposes `subscribe`
352
+ alongside `findMany`/`count`:
353
+
354
+ ```typescript
355
+ const subgraph = sl.subgraphs.typed(myDefinition); // { transfers, ... }
356
+
357
+ const unsubscribe = subgraph.transfers.subscribe(
358
+ (row) => console.log(row),
359
+ {
360
+ where: { amount: { gte: "1000000" } }, // optional row filter
361
+ since: 180000, // optional: replay from this block_height, then tail
362
+ onError: (err) => console.error(err),
363
+ },
364
+ );
365
+
366
+ // later
367
+ unsubscribe();
368
+ ```
369
+
370
+ `subscribe` is an SSE stream over the global `EventSource` (available in
371
+ browsers and Node ≥ 22; it throws if no `EventSource` is present). Frames are
372
+ unsigned rows. `since: <block_height>` replays matching rows from that height,
373
+ then tails the live edge; omit it to tail only.
374
+
320
375
  ## Subscriptions
321
376
 
322
377
  Signed HTTP webhooks. Subscriptions are polymorphic — pick one kind:
@@ -396,6 +451,38 @@ const { data: dead } = await sl.subscriptions.dead(id);
396
451
  await sl.subscriptions.requeueDead(id, outboxId);
397
452
  ```
398
453
 
454
+ ### Verifying deliveries
455
+
456
+ Every delivery — any kind, any `format` — also carries a universal authenticity
457
+ signature you can verify with one published key, no per-subscription secret. The
458
+ headers are `webhook-id`, `x-secondlayer-signature`, and
459
+ `x-secondlayer-signature-keyid`; the signed content is `` `${webhook-id}.${rawBody}` ``
460
+ (ed25519). Fetch the public key from `GET /public/streams/signing-key`.
461
+
462
+ ```typescript
463
+ import { verifySecondlayerSignature } from "@secondlayer/sdk";
464
+
465
+ app.post("/webhook", async (c) => {
466
+ const raw = await c.req.text(); // raw body — never re-stringify the parsed JSON
467
+ if (!verifySecondlayerSignature(raw, c.req.raw.headers, SECONDLAYER_PUBLIC_KEY)) {
468
+ return c.text("Invalid signature", 401);
469
+ }
470
+ // ... trusted ...
471
+ return c.body(null, 204);
472
+ });
473
+ ```
474
+
475
+ ```typescript
476
+ verifySecondlayerSignature(
477
+ rawBody: string,
478
+ headers: WebhookHeaderInput, // plain object, Fetch `Headers`, or a lookup fn
479
+ publicKeyPem: string,
480
+ ): boolean;
481
+ ```
482
+
483
+ Prefer the per-subscription HMAC (Standard Webhooks) secret instead? Use
484
+ `verifyWebhookSignature(rawBody, headers, secret)` — raw body first.
485
+
399
486
  ## Error Handling
400
487
 
401
488
  ```typescript
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReindexResponse, SubgraphDetail, SubgraphGapsResponse, SubgraphQueryParams, SubgraphSummary } from "@secondlayer/shared/schemas";
1
+ import { ReindexResponse, SubgraphAggregateParams, SubgraphAggregateResponse, SubgraphDetail, SubgraphGapsResponse, SubgraphQueryParams, SubgraphSummary } from "@secondlayer/shared/schemas";
2
2
  import { DeploySubgraphRequest, DeploySubgraphResponse } from "@secondlayer/shared/schemas/subgraphs";
3
3
  import { SubgraphAgentSchema, SubgraphSpecOptions } from "@secondlayer/shared/subgraphs/spec";
4
4
  import { InferSubgraphClient } from "@secondlayer/subgraphs";
@@ -110,6 +110,7 @@ declare class Subgraphs extends BaseClient {
110
110
  queryTableCount(name: string, table: string, params?: SubgraphQueryParams): Promise<{
111
111
  count: number
112
112
  }>;
113
+ queryTableAggregate(name: string, table: string, params?: SubgraphAggregateParams): Promise<SubgraphAggregateResponse>;
113
114
  /**
114
115
  * Returns a typed client for a subgraph defined with `defineSubgraph()`.
115
116
  * Row types are inferred from the subgraph's schema literal types.
@@ -1014,6 +1015,14 @@ type StreamsTip = {
1014
1015
  */
1015
1016
  finalized_height?: number
1016
1017
  lag_seconds: number
1018
+ /**
1019
+ * Oldest height still seekable on the live API for the caller's tier
1020
+ * (`tip - retention`). `null` = unlimited retention. Older reads must use the
1021
+ * cold dumps lane. Optional for back-compat.
1022
+ */
1023
+ oldest_seekable_height?: number | null
1024
+ /** Oldest seekable cursor (`<oldest_seekable_height>:0`); `null` = unlimited. */
1025
+ oldest_cursor?: string | null
1017
1026
  };
1018
1027
  type StreamsCanonicalBlock = {
1019
1028
  block_height: number
@@ -1274,7 +1283,7 @@ type StreamsUsage = {
1274
1283
  events_this_month: number
1275
1284
  }
1276
1285
  };
1277
- import { CreateSubscriptionRequest, CreateSubscriptionResponse, DeadRow, DeliveryRow, ReplayResult, RotateSecretResponse, SubscriptionDetail, SubscriptionSummary, UpdateSubscriptionRequest } from "@secondlayer/shared/schemas/subscriptions";
1286
+ import { CreateSubscriptionRequest, CreateSubscriptionResponse, DeadRow, DeliveryRow, ReplayResult, RotateSecretResponse, SubscriptionDetail, SubscriptionSummary, SubscriptionTestResult, UpdateSubscriptionRequest } from "@secondlayer/shared/schemas/subscriptions";
1278
1287
  import { ChainTrigger, ChainTriggerType, CreateSubscriptionRequest as CreateSubscriptionRequest2, CreateSubscriptionResponse as CreateSubscriptionResponse2, DeadRow as DeadRow2, DeliveryRow as DeliveryRow2, ReplayResult as ReplayResult2, RotateSecretResponse as RotateSecretResponse2, SubscriptionDetail as SubscriptionDetail2, SubscriptionFormat, SubscriptionKind, SubscriptionRuntime, SubscriptionStatus, SubscriptionSummary as SubscriptionSummary2, UpdateSubscriptionRequest as UpdateSubscriptionRequest2 } from "@secondlayer/shared/schemas/subscriptions";
1279
1288
  import { trigger } from "@secondlayer/shared/schemas/subscriptions";
1280
1289
  declare class Subscriptions extends BaseClient {
@@ -1290,6 +1299,9 @@ declare class Subscriptions extends BaseClient {
1290
1299
  ok: true
1291
1300
  }>;
1292
1301
  rotateSecret(id: string): Promise<RotateSecretResponse>;
1302
+ /** Send a one-off test webhook to the subscription's URL (built for its
1303
+ * format, SSRF-guarded). Logged as a delivery row, visible via recentDeliveries. */
1304
+ test(id: string): Promise<SubscriptionTestResult>;
1293
1305
  recentDeliveries(id: string): Promise<{
1294
1306
  data: DeliveryRow[]
1295
1307
  }>;
package/dist/index.js CHANGED
@@ -194,6 +194,26 @@ function buildSubgraphQueryString(params) {
194
194
  const str = qs.toString();
195
195
  return str ? `?${str}` : "";
196
196
  }
197
+ function buildAggregateQueryString(params) {
198
+ const qs = new URLSearchParams;
199
+ if (params.filters) {
200
+ for (const [key, value] of Object.entries(params.filters)) {
201
+ qs.set(key, String(value));
202
+ }
203
+ }
204
+ if (params.count)
205
+ qs.set("_count", "true");
206
+ if (params.countDistinct?.length)
207
+ qs.set("_countDistinct", params.countDistinct.join(","));
208
+ if (params.sum?.length)
209
+ qs.set("_sum", params.sum.join(","));
210
+ if (params.min?.length)
211
+ qs.set("_min", params.min.join(","));
212
+ if (params.max?.length)
213
+ qs.set("_max", params.max.join(","));
214
+ const str = qs.toString();
215
+ return str ? `?${str}` : "";
216
+ }
197
217
  function buildSpecQueryString(options) {
198
218
  const qs = new URLSearchParams;
199
219
  if (options?.serverUrl)
@@ -264,6 +284,9 @@ class Subgraphs extends BaseClient {
264
284
  async queryTableCount(name, table, params = {}) {
265
285
  return this.request("GET", `/api/subgraphs/${name}/${table}/count${buildSubgraphQueryString(params)}`);
266
286
  }
287
+ async queryTableAggregate(name, table, params = {}) {
288
+ return this.request("GET", `/api/subgraphs/${name}/${table}/aggregate${buildAggregateQueryString(params)}`);
289
+ }
267
290
  typed(def) {
268
291
  const result = {};
269
292
  for (const tableName of Object.keys(def.schema)) {
@@ -302,6 +325,18 @@ class Subgraphs extends BaseClient {
302
325
  });
303
326
  return result.count;
304
327
  },
328
+ async aggregate(spec) {
329
+ const filters = spec.where ? serializeWhere(spec.where) : undefined;
330
+ const result = await self.queryTableAggregate(subgraphName, tableName, {
331
+ filters,
332
+ count: spec.count,
333
+ countDistinct: spec.countDistinct,
334
+ sum: spec.sum,
335
+ min: spec.min,
336
+ max: spec.max
337
+ });
338
+ return result;
339
+ },
305
340
  subscribe(onRow, options = {}) {
306
341
  const filters = options.where ? serializeWhere(options.where) : {};
307
342
  const qs = new URLSearchParams;
@@ -1542,6 +1577,9 @@ class Subscriptions extends BaseClient {
1542
1577
  async rotateSecret(id) {
1543
1578
  return this.request("POST", `/api/subscriptions/${id}/rotate-secret`);
1544
1579
  }
1580
+ async test(id) {
1581
+ return this.request("POST", `/api/subscriptions/${id}/test`);
1582
+ }
1545
1583
  async recentDeliveries(id) {
1546
1584
  return this.request("GET", `/api/subscriptions/${id}/deliveries`);
1547
1585
  }
@@ -2090,5 +2128,5 @@ export {
2090
2128
  ApiError
2091
2129
  };
2092
2130
 
2093
- //# debugId=5E490BEDE2791EFC64756E2164756E21
2131
+ //# debugId=4980F60DADD3DFA064756E2164756E21
2094
2132
  //# sourceMappingURL=index.js.map