@secondlayer/sdk 6.9.1 → 6.11.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.
@@ -171,6 +171,23 @@ type StreamsEventsStreamParams = {
171
171
  maxEmptyPolls?: number
172
172
  signal?: AbortSignal
173
173
  };
174
+ type StreamsEventsSubscribeParams = {
175
+ /** Resume strictly after this cursor; omit to live-tail from the tip. */
176
+ fromCursor?: string | null
177
+ types?: readonly StreamsEventType[]
178
+ notTypes?: readonly StreamsEventType[]
179
+ contractId?: StreamsFilterValue
180
+ sender?: StreamsFilterValue
181
+ recipient?: StreamsFilterValue
182
+ assetIdentifier?: string
183
+ /** Abort to unsubscribe (the returned function does the same). */
184
+ signal?: AbortSignal
185
+ /** Called for each pushed event, in order. */
186
+ onEvent: (event: StreamsEvent) => void | Promise<void>
187
+ /** Called on a connection error; the subscription auto-reconnects from the
188
+ * last delivered cursor unless the signal has aborted. */
189
+ onError?: (err: unknown) => void
190
+ };
174
191
  /**
175
192
  * The checkpoint the SDK computes for a batch. Persist `cursor` inside the same
176
193
  * transaction as your projection writes, then resume from it via `fromCursor`.
@@ -276,6 +293,11 @@ type StreamsDumpsManifest = {
276
293
  to_block: number
277
294
  }
278
295
  files: StreamsDumpFile[]
296
+ /** ed25519 signature over the manifest's canonical bytes. Absent on legacy
297
+ * unsigned manifests. Verified by `list()` when `verifyDumpsManifest` is on. */
298
+ signature?: string
299
+ /** Short id of the signing public key. */
300
+ key_id?: string
279
301
  };
280
302
  type StreamsDumps = {
281
303
  /** Fetch and parse the latest dumps manifest. */
@@ -315,6 +337,13 @@ type StreamsClient = {
315
337
  * `maxEmptyPolls` stops it.
316
338
  */
317
339
  stream(params?: StreamsEventsStreamParams): AsyncIterable<StreamsEvent>
340
+ /**
341
+ * Subscribe to the real-time SSE push surface. Calls `onEvent` for each new
342
+ * canonical event as the server pushes it (chain cadence, not poll-bounded),
343
+ * and verifies each frame's inline ed25519 signature when the client was
344
+ * created with `verify`. Returns an unsubscribe function.
345
+ */
346
+ subscribe(params: StreamsEventsSubscribeParams): () => void
318
347
  }
319
348
  blocks: {
320
349
  events(heightOrHash: number | string): Promise<StreamsEventsListEnvelope>
@@ -359,6 +388,14 @@ type CreateStreamsClientOptions = {
359
388
  verify?: boolean | {
360
389
  publicKey: string
361
390
  }
391
+ /**
392
+ * Verify the bulk dumps manifest's ed25519 signature in `client.dumps.list()`
393
+ * before trusting any file sha256 (default ON). Uses the same key source as
394
+ * `verify` (fetches `/public/streams/signing-key`, or a pinned PEM). Pass
395
+ * `false` to opt out. A missing or invalid signature throws
396
+ * `StreamsSignatureError`.
397
+ */
398
+ verifyDumpsManifest?: boolean
362
399
  };
363
400
  declare function createStreamsClient(options: CreateStreamsClientOptions): StreamsClient;
364
401
  declare class AuthError extends Error {
@@ -653,4 +690,4 @@ declare const Cursor: {
653
690
  }
654
691
  };
655
692
  type DecodedEventRow = DecodedFtTransfer | DecodedNftTransfer | DecodedStxTransfer | DecodedStxMint | DecodedStxBurn | DecodedStxLock | DecodedFtMint | DecodedFtBurn | DecodedNftMint | DecodedNftBurn | DecodedPrint;
656
- export { isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, createStreamsClient, ValidationError, StreamsUsage, StreamsTip, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorgContext, StreamsReorg, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsClient, StreamsCanonicalBlock, StreamsBatchContext, STREAMS_EVENT_TYPES, RateLimitError, NftTransferPayload, NftTransferEvent, FtTransferPayload, FtTransferEvent, FetchLike, DecodedStxTransferPayload, DecodedStxTransfer, DecodedStxMintPayload, DecodedStxMint, DecodedStxLockPayload, DecodedStxLock, DecodedStxBurnPayload, DecodedStxBurn, DecodedPrintValue, DecodedPrintPayload, DecodedPrint, DecodedNftTransferPayload, DecodedNftTransfer, DecodedNftMintPayload, DecodedNftMint, DecodedNftBurnPayload, DecodedNftBurn, DecodedFtTransferPayload, DecodedFtTransfer, DecodedFtMintPayload, DecodedFtMint, DecodedFtBurnPayload, DecodedFtBurn, DecodedEventRow, DecodedEventColumns, Cursor, AuthError };
693
+ export { isStxTransfer, isStxMint, isStxLock, isStxBurn, isPrint, isNftTransfer, isNftMint, isNftBurn, isFtTransfer, isFtMint, isFtBurn, decodeStxTransfer, decodeStxMint, decodeStxLock, decodeStxBurn, decodePrint, decodeNftTransfer, decodeNftMint, decodeNftBurn, decodeFtTransfer, decodeFtMint, decodeFtBurn, createStreamsClient, ValidationError, StreamsUsage, StreamsTip, StreamsSignatureError, StreamsServerError, StreamsReorgsListParams, StreamsReorgsListEnvelope, StreamsReorgContext, StreamsReorg, StreamsEventsSubscribeParams, StreamsEventsStreamParams, StreamsEventsListParams, StreamsEventsListEnvelope, StreamsEventsEnvelope, StreamsEventsConsumeResult, StreamsEventsConsumeParams, StreamsEventType, StreamsEventPayload, StreamsEvent, StreamsDumpsManifest, StreamsDumps, StreamsDumpFile, StreamsClient, StreamsCanonicalBlock, StreamsBatchContext, STREAMS_EVENT_TYPES, RateLimitError, NftTransferPayload, NftTransferEvent, FtTransferPayload, FtTransferEvent, FetchLike, DecodedStxTransferPayload, DecodedStxTransfer, DecodedStxMintPayload, DecodedStxMint, DecodedStxLockPayload, DecodedStxLock, DecodedStxBurnPayload, DecodedStxBurn, DecodedPrintValue, DecodedPrintPayload, DecodedPrint, DecodedNftTransferPayload, DecodedNftTransfer, DecodedNftMintPayload, DecodedNftMint, DecodedNftBurnPayload, DecodedNftBurn, DecodedFtTransferPayload, DecodedFtTransfer, DecodedFtMintPayload, DecodedFtMint, DecodedFtBurnPayload, DecodedFtBurn, DecodedEventRow, DecodedEventColumns, Cursor, AuthError };
@@ -1,5 +1,5 @@
1
1
  // src/streams/client.ts
2
- import { ed25519 } from "@secondlayer/shared";
2
+ import { ed25519 as ed255192 } from "@secondlayer/shared";
3
3
 
4
4
  // src/errors.ts
5
5
  class ApiError extends Error {
@@ -317,6 +317,7 @@ async function* streamStreamsEvents(opts) {
317
317
 
318
318
  // src/streams/dumps.ts
319
319
  import { createHash } from "node:crypto";
320
+ import { verifyStreamsBulkManifestSignature } from "@secondlayer/shared/streams-bulk-manifest";
320
321
  function createStreamsDumps(opts) {
321
322
  const baseUrl = opts.baseUrl?.replace(/\/+$/, "");
322
323
  function requireBaseUrl() {
@@ -334,7 +335,17 @@ function createStreamsDumps(opts) {
334
335
  if (!res.ok) {
335
336
  throw new StreamsServerError(`Could not fetch dumps manifest (${res.status}).`, res.status);
336
337
  }
337
- return await res.json();
338
+ const manifest = await res.json();
339
+ if (opts.verifyManifest) {
340
+ if (!opts.loadPublicKeyPem) {
341
+ throw new StreamsSignatureError("Manifest verification is on but no signing key source is configured.");
342
+ }
343
+ const publicKeyPem = await opts.loadPublicKeyPem();
344
+ if (!verifyStreamsBulkManifestSignature(manifest, publicKeyPem)) {
345
+ throw new StreamsSignatureError("Dumps manifest signature is missing or invalid.");
346
+ }
347
+ }
348
+ return manifest;
338
349
  }
339
350
  async function download(file) {
340
351
  const res = await opts.fetchImpl(fileUrl(file));
@@ -351,6 +362,135 @@ function createStreamsDumps(opts) {
351
362
  return { list, fileUrl, download };
352
363
  }
353
364
 
365
+ // src/streams/subscribe.ts
366
+ import { ed25519 } from "@secondlayer/shared";
367
+ function subscribeStreamsEvents(opts) {
368
+ const { params } = opts;
369
+ const controller = new AbortController;
370
+ const external = params.signal;
371
+ if (external) {
372
+ if (external.aborted)
373
+ controller.abort();
374
+ else
375
+ external.addEventListener("abort", () => controller.abort(), {
376
+ once: true
377
+ });
378
+ }
379
+ let cursor = params.fromCursor ?? null;
380
+ const reconnectDelayMs = opts.reconnectDelayMs ?? 1000;
381
+ const run = async () => {
382
+ while (!controller.signal.aborted) {
383
+ try {
384
+ const url = `${opts.baseUrl}/v1/streams/events/stream${buildQuery({
385
+ from_cursor: cursor ?? undefined,
386
+ types: params.types,
387
+ not_types: params.notTypes,
388
+ contract_id: params.contractId,
389
+ sender: params.sender,
390
+ recipient: params.recipient,
391
+ asset_identifier: params.assetIdentifier
392
+ })}`;
393
+ const res = await opts.fetchImpl(url, {
394
+ headers: {
395
+ Authorization: `Bearer ${opts.apiKey}`,
396
+ Accept: "text/event-stream"
397
+ },
398
+ signal: controller.signal
399
+ });
400
+ if (!res.ok) {
401
+ throw new StreamsServerError(`Streams SSE returned ${res.status}.`, res.status);
402
+ }
403
+ if (!res.body) {
404
+ throw new StreamsServerError("Streams SSE response has no body.", 0);
405
+ }
406
+ for await (const frame of parseSseFrames(res.body, controller.signal)) {
407
+ if (frame.event === "ping" || !frame.data)
408
+ continue;
409
+ let parsed;
410
+ try {
411
+ parsed = JSON.parse(frame.data);
412
+ } catch {
413
+ continue;
414
+ }
415
+ if (!parsed.event)
416
+ continue;
417
+ if (opts.verify) {
418
+ const key = await opts.loadKey();
419
+ if (!parsed.sig || !ed25519.verifyEd25519(JSON.stringify(parsed.event), parsed.sig, key.publicKey)) {
420
+ throw new StreamsSignatureError("Streams SSE frame signature is missing or invalid.");
421
+ }
422
+ }
423
+ cursor = parsed.event.cursor ?? cursor;
424
+ await params.onEvent(parsed.event);
425
+ }
426
+ } catch (err) {
427
+ if (controller.signal.aborted)
428
+ return;
429
+ params.onError?.(err);
430
+ await sleep(reconnectDelayMs, controller.signal);
431
+ }
432
+ }
433
+ };
434
+ run();
435
+ return () => controller.abort();
436
+ }
437
+ function sleep(ms, signal) {
438
+ return new Promise((resolve) => {
439
+ if (signal.aborted)
440
+ return resolve();
441
+ const onAbort = () => {
442
+ clearTimeout(timer);
443
+ resolve();
444
+ };
445
+ const timer = setTimeout(() => {
446
+ signal.removeEventListener("abort", onAbort);
447
+ resolve();
448
+ }, ms);
449
+ signal.addEventListener("abort", onAbort, { once: true });
450
+ });
451
+ }
452
+ async function* parseSseFrames(body, signal) {
453
+ const reader = body.getReader();
454
+ const decoder = new TextDecoder;
455
+ let buffer = "";
456
+ try {
457
+ while (!signal.aborted) {
458
+ const { value, done } = await reader.read();
459
+ if (done)
460
+ break;
461
+ buffer += decoder.decode(value, { stream: true });
462
+ let sep = buffer.indexOf(`
463
+
464
+ `);
465
+ while (sep !== -1) {
466
+ yield parseFrame(buffer.slice(0, sep));
467
+ buffer = buffer.slice(sep + 2);
468
+ sep = buffer.indexOf(`
469
+
470
+ `);
471
+ }
472
+ }
473
+ } finally {
474
+ try {
475
+ await reader.cancel();
476
+ } catch {}
477
+ }
478
+ }
479
+ function parseFrame(raw) {
480
+ let event;
481
+ const data = [];
482
+ for (const line of raw.split(`
483
+ `)) {
484
+ if (line.startsWith("data:")) {
485
+ data.push(line.slice(line.startsWith("data: ") ? 6 : 5));
486
+ } else if (line.startsWith("event:")) {
487
+ event = line.slice(line.startsWith("event: ") ? 7 : 6).trim();
488
+ }
489
+ }
490
+ return { event, data: data.length > 0 ? data.join(`
491
+ `) : undefined };
492
+ }
493
+
354
494
  // src/streams/client.ts
355
495
  function cursorTuple(cursor) {
356
496
  if (!cursor)
@@ -410,10 +550,6 @@ function createStreamsClient(options) {
410
550
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_STREAMS_BASE_URL);
411
551
  const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
412
552
  const verify = options.verify ?? false;
413
- const dumps = createStreamsDumps({
414
- baseUrl: options.dumpsBaseUrl,
415
- fetchImpl
416
- });
417
553
  let keyPromise = null;
418
554
  function loadKey() {
419
555
  if (keyPromise)
@@ -421,8 +557,9 @@ function createStreamsClient(options) {
421
557
  keyPromise = (async () => {
422
558
  if (typeof verify === "object") {
423
559
  return {
424
- keyId: ed25519.ed25519KeyId(verify.publicKey),
425
- publicKey: ed25519.loadEd25519PublicKey(verify.publicKey)
560
+ keyId: ed255192.ed25519KeyId(verify.publicKey),
561
+ publicKeyPem: verify.publicKey,
562
+ publicKey: ed255192.loadEd25519PublicKey(verify.publicKey)
426
563
  };
427
564
  }
428
565
  const res = await fetchImpl(`${baseUrl}/public/streams/signing-key`);
@@ -434,12 +571,19 @@ function createStreamsClient(options) {
434
571
  throw new StreamsSignatureError("Signing key response missing key.");
435
572
  }
436
573
  return {
437
- keyId: body.key_id ?? ed25519.ed25519KeyId(body.public_key_pem),
438
- publicKey: ed25519.loadEd25519PublicKey(body.public_key_pem)
574
+ keyId: body.key_id ?? ed255192.ed25519KeyId(body.public_key_pem),
575
+ publicKeyPem: body.public_key_pem,
576
+ publicKey: ed255192.loadEd25519PublicKey(body.public_key_pem)
439
577
  };
440
578
  })();
441
579
  return keyPromise;
442
580
  }
581
+ const dumps = createStreamsDumps({
582
+ baseUrl: options.dumpsBaseUrl,
583
+ fetchImpl,
584
+ verifyManifest: options.verifyDumpsManifest ?? true,
585
+ loadPublicKeyPem: async () => (await loadKey()).publicKeyPem
586
+ });
443
587
  async function request(path) {
444
588
  const response = await fetchImpl(`${baseUrl}${path}`, {
445
589
  headers: { Authorization: `Bearer ${options.apiKey}` }
@@ -464,7 +608,7 @@ function createStreamsClient(options) {
464
608
  throw new StreamsSignatureError(`Response signed with key '${responseKeyId}' not served by the signing-key endpoint.`);
465
609
  }
466
610
  }
467
- if (!ed25519.verifyEd25519(text, signature, key.publicKey)) {
611
+ if (!ed255192.verifyEd25519(text, signature, key.publicKey)) {
468
612
  throw new StreamsSignatureError;
469
613
  }
470
614
  }
@@ -549,6 +693,16 @@ function createStreamsClient(options) {
549
693
  fetchEvents
550
694
  });
551
695
  },
696
+ subscribe(params) {
697
+ return subscribeStreamsEvents({
698
+ baseUrl,
699
+ apiKey: options.apiKey,
700
+ fetchImpl,
701
+ verify: Boolean(verify),
702
+ loadKey,
703
+ params
704
+ });
705
+ },
552
706
  async replay(params) {
553
707
  const fromCursor = params.from === "genesis" ? null : params.from ?? null;
554
708
  const fromBlock = fromCursor ? cursorTuple(fromCursor)[0] : 0;
@@ -914,5 +1068,5 @@ export {
914
1068
  AuthError
915
1069
  };
916
1070
 
917
- //# debugId=7AB22CC73A3769A464756E2164756E21
1071
+ //# debugId=12EEA92B91F2B3F064756E2164756E21
918
1072
  //# sourceMappingURL=index.js.map