@tinycloud/cli 1.1.0-beta.19 → 1.1.0-beta.21

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.
@@ -591,8 +591,8 @@ function canonicalizeAddress(address) {
591
591
  const trimmed = address.trim();
592
592
  return trimmed.startsWith("0x") ? `0x${trimmed.slice(2).toLowerCase()}` : trimmed.toLowerCase();
593
593
  }
594
- function normalizePkhIdentifier(identifier) {
595
- return identifier.replace(
594
+ function normalizePkhIdentifier(identifier2) {
595
+ return identifier2.replace(
596
596
  /(eip155:\d+:)(0x[0-9a-fA-F]{40})(?=[:#]|$)/,
597
597
  (_match, prefix, address) => prefix + address.toLowerCase()
598
598
  );
@@ -7740,6 +7740,18 @@ var MAX_KV_BATCH_READ_ITEMS = 100;
7740
7740
  function isJsonObject(value) {
7741
7741
  return typeof value === "object" && value !== null && !Array.isArray(value);
7742
7742
  }
7743
+ var pathByteEncoder = new TextEncoder();
7744
+ function comparePathsByBytes(a, b) {
7745
+ const left = pathByteEncoder.encode(a);
7746
+ const right = pathByteEncoder.encode(b);
7747
+ const length4 = Math.min(left.length, right.length);
7748
+ for (let index = 0; index < length4; index++) {
7749
+ if (left[index] !== right[index]) {
7750
+ return left[index] - right[index];
7751
+ }
7752
+ }
7753
+ return left.length - right.length;
7754
+ }
7743
7755
  var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
7744
7756
  function revocationCodeOf(body) {
7745
7757
  const match = REVOKED_BODY.exec(body.trim());
@@ -7973,15 +7985,25 @@ var KVService = class extends BaseService {
7973
7985
  )?.[1];
7974
7986
  return contentType?.includes("application/json") ? JSON.parse(text) : text;
7975
7987
  }
7988
+ /**
7989
+ * Validate a batch read response and return its items in the caller's
7990
+ * request order. The node flattens invocation abilities from a `BTreeMap`,
7991
+ * so results arrive sorted by UTF-8 byte order of the path, not in request
7992
+ * order. Items that carry their path in `key` are matched by path; if no
7993
+ * item carries a key, items are assumed to be in the node's byte-sorted
7994
+ * order and the request is aligned the same way. Any mix of keyed and
7995
+ * keyless items, a missing requested path, an unrequested path, or a
7996
+ * malformed item fails closed (returns undefined).
7997
+ */
7976
7998
  normalizeBatchReadResponse(data, paths, requireData) {
7977
7999
  if (!data || typeof data !== "object") return void 0;
7978
8000
  const response = data;
7979
- if (!Array.isArray(response.results) || response.results.length !== paths.length) {
8001
+ const items = response.results;
8002
+ if (!Array.isArray(items) || items.length !== paths.length) {
7980
8003
  return void 0;
7981
8004
  }
7982
- for (let index = 0; index < response.results.length; index++) {
7983
- const item = response.results[index];
7984
- if (!item || typeof item !== "object" || item.key !== paths[index] || typeof item.ok !== "boolean") {
8005
+ for (const item of items) {
8006
+ if (!item || typeof item !== "object" || typeof item.ok !== "boolean") {
7985
8007
  return void 0;
7986
8008
  }
7987
8009
  if (item.ok && (!item.headers || typeof item.headers !== "object" || Object.values(item.headers).some((value) => typeof value !== "string") || requireData && typeof item.dataBase64 !== "string")) {
@@ -7991,7 +8013,24 @@ var KVService = class extends BaseService {
7991
8013
  return void 0;
7992
8014
  }
7993
8015
  }
7994
- return response;
8016
+ if (items.every((item) => typeof item.key === "string")) {
8017
+ const byPath = new Map(items.map((item) => [item.key, item]));
8018
+ if (byPath.size !== items.length) return void 0;
8019
+ const aligned2 = [];
8020
+ for (const path of paths) {
8021
+ const item = byPath.get(path);
8022
+ if (!item) return void 0;
8023
+ aligned2.push(item);
8024
+ }
8025
+ return aligned2;
8026
+ }
8027
+ if (items.some((item) => item.key !== void 0)) return void 0;
8028
+ const requestOrder = paths.map((_, index) => index).sort((a, b) => comparePathsByBytes(paths[a], paths[b]));
8029
+ const aligned = new Array(items.length);
8030
+ for (let position = 0; position < requestOrder.length; position++) {
8031
+ aligned[requestOrder[position]] = items[position];
8032
+ }
8033
+ return aligned;
7995
8034
  }
7996
8035
  async batchRead(keys, action, options) {
7997
8036
  if (!this.requireAuth()) return err(authRequiredError("kv"));
@@ -8084,19 +8123,19 @@ var KVService = class extends BaseService {
8084
8123
  { meta: { status: response.status, statusText: response.statusText } }
8085
8124
  ));
8086
8125
  }
8087
- const payload = this.normalizeBatchReadResponse(
8126
+ const items = this.normalizeBatchReadResponse(
8088
8127
  await response.json(),
8089
8128
  paths,
8090
8129
  action === KVAction.GET
8091
8130
  );
8092
- if (!payload) {
8131
+ if (!items) {
8093
8132
  return err(serviceError(
8094
8133
  ErrorCodes.NETWORK_ERROR,
8095
8134
  "KV batch read response did not match the requested keys",
8096
8135
  "kv"
8097
8136
  ));
8098
8137
  }
8099
- const results = payload.results.map((item, index) => {
8138
+ const results = items.map((item, index) => {
8100
8139
  if (!item.ok) {
8101
8140
  return {
8102
8141
  key: keys[index],
@@ -10000,7 +10039,7 @@ var AsyncQueue = class {
10000
10039
  }
10001
10040
  const waiter = this.waiters.shift();
10002
10041
  if (waiter) {
10003
- waiter({ value, done: false });
10042
+ waiter.resolve({ value, done: false });
10004
10043
  return;
10005
10044
  }
10006
10045
  this.values.push(value);
@@ -10012,7 +10051,22 @@ var AsyncQueue = class {
10012
10051
  this.closed = true;
10013
10052
  while (this.waiters.length > 0) {
10014
10053
  const waiter = this.waiters.shift();
10015
- waiter?.({ value: void 0, done: true });
10054
+ waiter?.resolve({ value: void 0, done: true });
10055
+ }
10056
+ }
10057
+ /**
10058
+ * Fail the queue: pending and future `next()` calls reject with `error`,
10059
+ * so a `for await` consumer sees the iterator throw.
10060
+ */
10061
+ fail(error) {
10062
+ if (this.closed) {
10063
+ return;
10064
+ }
10065
+ this.closed = true;
10066
+ this.failure = error;
10067
+ while (this.waiters.length > 0) {
10068
+ const waiter = this.waiters.shift();
10069
+ waiter?.reject(error);
10016
10070
  }
10017
10071
  }
10018
10072
  next() {
@@ -10020,20 +10074,26 @@ var AsyncQueue = class {
10020
10074
  const value = this.values.shift();
10021
10075
  return Promise.resolve({ value, done: false });
10022
10076
  }
10077
+ if (this.failure !== void 0) {
10078
+ return Promise.reject(this.failure);
10079
+ }
10023
10080
  if (this.closed) {
10024
10081
  return Promise.resolve({ value: void 0, done: true });
10025
10082
  }
10026
- return new Promise((resolve5) => {
10027
- this.waiters.push(resolve5);
10083
+ return new Promise((resolve5, reject) => {
10084
+ this.waiters.push({ resolve: resolve5, reject });
10028
10085
  });
10029
10086
  }
10030
10087
  };
10088
+ var HOOK_STREAM_RETRY_BASE_DELAY_MS = 250;
10089
+ var HOOK_STREAM_RETRY_MAX_DELAY_MS = 3e4;
10090
+ var HOOK_STREAM_HEALTHY_MS = 5e3;
10031
10091
  var HooksService = class extends BaseService {
10032
10092
  constructor(config = {}) {
10033
10093
  super();
10034
10094
  this._subscribers = /* @__PURE__ */ new Set();
10035
- this._refreshChain = Promise.resolve();
10036
- this._activeSignature = "";
10095
+ this._supervising = false;
10096
+ this._sessionGeneration = 0;
10037
10097
  this._config = config;
10038
10098
  }
10039
10099
  get config() {
@@ -10049,35 +10109,32 @@ var HooksService = class extends BaseService {
10049
10109
  if (subscriptions.length === 0) {
10050
10110
  throw new Error("At least one hook subscription is required");
10051
10111
  }
10052
- const normalized = subscriptions.map(normalizeSubscription);
10112
+ const requested = subscriptions.map(normalizeSubscription);
10053
10113
  const subscriber = {
10054
- requested: normalized,
10114
+ requested,
10115
+ signatures: requested.map(subscriptionSignature),
10055
10116
  ttlSeconds: options.ttlSeconds,
10056
10117
  queue: new AsyncQueue()
10057
10118
  };
10058
- this._subscribers.add(subscriber);
10059
- const abortHandler = () => {
10119
+ const leave = () => {
10060
10120
  this._subscribers.delete(subscriber);
10061
10121
  subscriber.queue.close();
10062
- void this.scheduleSharedStreamRefresh();
10122
+ this.wake();
10063
10123
  };
10064
- if (options.signal) {
10065
- if (options.signal.aborted) {
10066
- abortHandler();
10067
- } else {
10068
- options.signal.addEventListener("abort", abortHandler, { once: true });
10069
- }
10124
+ const { signal } = options;
10125
+ if (signal?.aborted) {
10126
+ return;
10070
10127
  }
10071
- void this.scheduleSharedStreamRefresh();
10128
+ signal?.addEventListener("abort", leave, { once: true });
10129
+ this._subscribers.add(subscriber);
10130
+ this.wake();
10072
10131
  try {
10073
10132
  for await (const event of subscriber.queue) {
10074
10133
  yield event;
10075
10134
  }
10076
10135
  } finally {
10077
- if (options.signal) {
10078
- options.signal.removeEventListener("abort", abortHandler);
10079
- }
10080
- abortHandler();
10136
+ signal?.removeEventListener("abort", leave);
10137
+ leave();
10081
10138
  }
10082
10139
  }
10083
10140
  async register(webhook) {
@@ -10218,146 +10275,292 @@ var HooksService = class extends BaseService {
10218
10275
  return err(wrapError2("hooks", error));
10219
10276
  }
10220
10277
  }
10221
- async scheduleSharedStreamRefresh() {
10222
- this._refreshChain = this._refreshChain.then(() => this.refreshSharedStream()).catch(() => void 0);
10223
- await this._refreshChain;
10224
- }
10225
- async refreshSharedStream() {
10226
- if (!this.requireAuth() || this._subscribers.size === 0) {
10227
- this.abortSharedStream();
10228
- this._activeSignature = "";
10229
- return;
10230
- }
10231
- const state = this.collectSharedStreamState();
10232
- if (state.signature !== this._activeSignature) {
10233
- this._activeSignature = state.signature;
10234
- this.abortSharedStream();
10278
+ /**
10279
+ * Called after anything that may change what the supervisor should do:
10280
+ * subscribe, unsubscribe, session change, sign-out. Interrupts the running
10281
+ * step if the plan moved away from it, and starts the supervisor if none
10282
+ * runs. Touches only SDK-owned state, so it never throws into callers such
10283
+ * as `ServiceContext.setSession`.
10284
+ */
10285
+ wake() {
10286
+ const phase = this._phase;
10287
+ if (phase && phase.key !== this.plan()?.key) {
10288
+ phase.interrupt.abort();
10235
10289
  }
10236
- if (!this._sharedStreamTask) {
10237
- this._sharedStreamTask = this.runSharedStream(state).catch((error) => {
10238
- if (!isAbortError(error)) {
10239
- throw error;
10240
- }
10241
- }).finally(() => {
10242
- this._sharedStreamTask = void 0;
10243
- this._sharedStreamAbort = void 0;
10244
- if (this._subscribers.size > 0) {
10245
- void this.scheduleSharedStreamRefresh();
10246
- }
10247
- });
10290
+ if (this._supervising || this._subscribers.size === 0) {
10291
+ return;
10248
10292
  }
10293
+ this._supervising = true;
10294
+ this.supervise().catch(() => this.collapse());
10249
10295
  }
10250
- collectSharedStreamState() {
10251
- const merged = /* @__PURE__ */ new Map();
10252
- const ttlCandidates = [];
10253
- for (const subscriber of this._subscribers) {
10254
- if (typeof subscriber.ttlSeconds === "number") {
10255
- ttlCandidates.push(subscriber.ttlSeconds);
10296
+ /**
10297
+ * The only owner of the shared stream. Each iteration is one attempt
10298
+ * (mint → open → drain), then a classification and, unless the attempt was
10299
+ * superseded or released its subscribers, a backoff. Every await is a
10300
+ * `phase` that wake() can interrupt; an interrupted phase yields
10301
+ * `undefined`, so a superseded mint can never act. Outside `attempt` and
10302
+ * `pause`, which each own one catch, the loop touches only SDK state.
10303
+ */
10304
+ async supervise() {
10305
+ let streak = 0;
10306
+ for (; ; ) {
10307
+ const plan = this.plan();
10308
+ if (!plan) {
10309
+ this._supervising = false;
10310
+ return;
10311
+ }
10312
+ const health = { openedAt: 0, delivered: false };
10313
+ const outcome = await this.phase(
10314
+ plan.key,
10315
+ (signal) => this.attempt(plan, signal, health)
10316
+ );
10317
+ if (health.delivered || health.openedAt > 0 && performance.now() - health.openedAt >= HOOK_STREAM_HEALTHY_MS) {
10318
+ streak = 0;
10319
+ }
10320
+ if (!outcome) {
10321
+ continue;
10322
+ }
10323
+ if (this.plan()?.key !== plan.key) {
10324
+ continue;
10325
+ }
10326
+ if (outcome.kind === "stopped") {
10327
+ this.release();
10328
+ continue;
10256
10329
  }
10257
- for (const subscription of subscriber.requested) {
10258
- merged.set(subscriptionSignature(subscription), subscription);
10330
+ if (outcome.kind === "refused" && outcome.terminal) {
10331
+ this.release(outcome.error);
10332
+ this.report(outcome.error);
10333
+ continue;
10334
+ }
10335
+ if (outcome.kind !== "ended") {
10336
+ this.report(outcome.error);
10259
10337
  }
10338
+ streak += 1;
10339
+ await this.phase(
10340
+ plan.key,
10341
+ (signal) => this.pause(streak, signal)
10342
+ );
10260
10343
  }
10261
- const subscriptions = [...merged.values()].sort(
10262
- (left, right) => subscriptionSignature(left).localeCompare(subscriptionSignature(right))
10263
- );
10264
- const ttlSeconds = ttlCandidates.length > 0 ? Math.min(...ttlCandidates) : void 0;
10265
- const signature = JSON.stringify({
10266
- subscriptions: subscriptions.map(subscriptionSignature),
10267
- ttlSeconds
10268
- });
10269
- return {
10270
- subscriptions,
10271
- ttlSeconds,
10272
- signature
10273
- };
10274
10344
  }
10275
- async runSharedStream(state) {
10276
- const abortController = new AbortController();
10277
- this._sharedStreamAbort = abortController;
10345
+ /**
10346
+ * Run one step under an interrupt that wake() fires when the plan key moves
10347
+ * on. An interrupted step's result is discarded. The interrupt is also
10348
+ * fired when the step returns, which releases whatever it left open: the
10349
+ * request signal and its listeners, an unread response body, a timer.
10350
+ */
10351
+ async phase(key, step) {
10352
+ const interrupt = new AbortController();
10353
+ this._phase = { key, interrupt };
10278
10354
  try {
10279
- const host = this._config.host ?? this.context.hosts[0];
10280
- const ticketResponse = await this.mintHookTicket(
10281
- state.subscriptions,
10282
- state.ttlSeconds,
10283
- abortController.signal
10355
+ if (this.plan()?.key !== key) {
10356
+ interrupt.abort();
10357
+ }
10358
+ const result = await step(interrupt.signal);
10359
+ return interrupt.signal.aborted ? void 0 : result;
10360
+ } finally {
10361
+ this._phase = void 0;
10362
+ interrupt.abort();
10363
+ }
10364
+ }
10365
+ /**
10366
+ * One mint → open → drain. The single catch site for everything the
10367
+ * attempt touches that the SDK does not own: context accessors, invoke,
10368
+ * fetch, response bodies, the SSE parser. Never rejects.
10369
+ */
10370
+ async attempt(plan, signal, health) {
10371
+ let phase = "mint";
10372
+ let request;
10373
+ const owns = () => !signal.aborted && !request?.signal.aborted && this.plan()?.key === plan.key;
10374
+ try {
10375
+ const context = this.context;
10376
+ const contextSignal = context.abortSignal;
10377
+ this._lastContextSignal = contextSignal;
10378
+ if (!context.isAuthenticated || contextSignal?.aborted) {
10379
+ return { kind: "stopped" };
10380
+ }
10381
+ request = this.createRequestSignal(signal, 0);
10382
+ const host = this.host;
10383
+ const minted = await awaitOwned(
10384
+ request.signal,
10385
+ () => context.fetch(`${host}/hooks/tickets`, {
10386
+ method: "POST",
10387
+ headers: {
10388
+ ...serviceHeadersToRecord(this.createInvokeHeaders(plan.subscriptions)),
10389
+ "content-type": "application/json"
10390
+ },
10391
+ body: JSON.stringify({
10392
+ subscriptions: plan.subscriptions,
10393
+ ttlSeconds: plan.ttlSeconds
10394
+ }),
10395
+ signal: request.signal
10396
+ })
10284
10397
  );
10285
- const streamResponse = await this.openHookStream(
10286
- host,
10287
- ticketResponse.ticket,
10288
- abortController.signal
10398
+ if (!owns()) return { kind: "stopped" };
10399
+ if (!minted.ok) {
10400
+ const status = minted.status;
10401
+ const error = await awaitOwned(
10402
+ request.signal,
10403
+ () => responseError("hooks", "failed to mint hook ticket", minted)
10404
+ );
10405
+ if (!owns()) return { kind: "stopped" };
10406
+ return { kind: "refused", terminal: isTerminalMintRefusal(status), error };
10407
+ }
10408
+ const body = await awaitOwned(request.signal, () => minted.json());
10409
+ if (!owns()) return { kind: "stopped" };
10410
+ const ticket = body !== null && typeof body === "object" && "ticket" in body ? body.ticket : void 0;
10411
+ if (typeof ticket !== "string" || ticket.length === 0) {
10412
+ throw new Error("Hook ticket response did not include a ticket");
10413
+ }
10414
+ phase = "open";
10415
+ if (!owns()) return { kind: "stopped" };
10416
+ const stream = await awaitOwned(
10417
+ request.signal,
10418
+ () => context.fetch(`${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`, {
10419
+ method: "GET",
10420
+ headers: { accept: "text/event-stream" },
10421
+ signal: request.signal
10422
+ })
10289
10423
  );
10290
- for await (const message of parseSseStream(
10291
- streamResponse.body,
10292
- abortController.signal
10293
- )) {
10294
- if (!message.data) {
10295
- continue;
10296
- }
10297
- const event = parseHookEvent(message);
10298
- for (const subscriber of this._subscribers) {
10299
- if (matchesAnySubscription(event, subscriber.requested)) {
10300
- subscriber.queue.push(event);
10301
- }
10424
+ if (!owns()) return { kind: "stopped" };
10425
+ if (!stream.ok) {
10426
+ return { kind: "refused", terminal: false, error: hookStreamRefusal(stream.status) };
10427
+ }
10428
+ health.openedAt = performance.now();
10429
+ const iterator = parseSseStream(stream.body, request.signal)[Symbol.asyncIterator]();
10430
+ try {
10431
+ for (; ; ) {
10432
+ const next = await awaitOwned(request.signal, () => iterator.next());
10433
+ if (!owns()) return { kind: "stopped" };
10434
+ if (next.done) break;
10435
+ const message = next.value;
10436
+ if (!message.data) continue;
10437
+ const event = parseHookEvent(message);
10438
+ if (!owns()) return { kind: "stopped" };
10439
+ this.dispatch(event);
10440
+ health.delivered = true;
10302
10441
  }
10442
+ } finally {
10443
+ const closing = iterator.return?.();
10444
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
10303
10445
  }
10446
+ return owns() ? { kind: "ended" } : { kind: "stopped" };
10447
+ } catch (thrown) {
10448
+ return request?.signal.aborted || thrown === SUPERSEDED ? { kind: "stopped" } : { kind: "failed", error: hookStreamFailure(phase, thrown) };
10449
+ }
10450
+ }
10451
+ /**
10452
+ * Back off before the next attempt. `config.streamRetry` overrides the
10453
+ * delay and the clock; whatever they do, the pause ends by the delay or the
10454
+ * interrupt, then yields one timer turn so an instant clock cannot starve
10455
+ * due timers or the process.
10456
+ */
10457
+ async pause(attempt, signal) {
10458
+ try {
10459
+ this._lastContextSignal = this.context.abortSignal;
10460
+ } catch {
10461
+ }
10462
+ const contextSignal = this._lastContextSignal;
10463
+ const waits = [];
10464
+ const aborts = [];
10465
+ try {
10466
+ const retry = this._config.streamRetry;
10467
+ const delayMs = retry?.delay?.(attempt) ?? hookStreamRetryDelay(attempt);
10468
+ waits.push((retry?.wait ?? sleep)(delayMs, attempt, signal));
10469
+ } catch {
10470
+ waits.push(Promise.resolve());
10471
+ }
10472
+ aborts.push(abortPromise(signal));
10473
+ if (contextSignal) aborts.push(abortPromise(contextSignal));
10474
+ try {
10475
+ await Promise.race([...waits, ...aborts.map(({ promise }) => promise)]);
10476
+ } catch {
10304
10477
  } finally {
10305
- if (this._sharedStreamAbort === abortController) {
10306
- this._sharedStreamAbort = void 0;
10478
+ for (const abort of aborts) abort.dispose();
10479
+ }
10480
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
10481
+ }
10482
+ /** The stream the current subscribers need, or undefined when none remain. */
10483
+ plan() {
10484
+ if (this._subscribers.size === 0) {
10485
+ return void 0;
10486
+ }
10487
+ const merged = /* @__PURE__ */ new Map();
10488
+ let ttlSeconds;
10489
+ for (const subscriber of this._subscribers) {
10490
+ subscriber.requested.forEach(
10491
+ (subscription, index) => merged.set(subscriber.signatures[index], subscription)
10492
+ );
10493
+ if (typeof subscriber.ttlSeconds === "number") {
10494
+ ttlSeconds = Math.min(subscriber.ttlSeconds, ttlSeconds ?? Infinity);
10307
10495
  }
10308
10496
  }
10497
+ const signatures = [...merged.keys()].sort();
10498
+ return {
10499
+ key: JSON.stringify([this._sessionGeneration, signatures, ttlSeconds ?? null]),
10500
+ subscriptions: signatures.map((signature) => merged.get(signature)),
10501
+ ttlSeconds
10502
+ };
10309
10503
  }
10310
- abortSharedStream() {
10311
- this._sharedStreamAbort?.abort();
10504
+ dispatch(event) {
10505
+ for (const subscriber of this._subscribers) {
10506
+ if (matchesAnySubscription(event, subscriber.requested)) {
10507
+ subscriber.queue.push(event);
10508
+ }
10509
+ }
10312
10510
  }
10313
- createHookHeaders(action, path) {
10314
- return this.context.invoke(this.session, "hooks", path, action);
10511
+ /**
10512
+ * Detach every subscriber. With `error`, their iterators throw it after
10513
+ * draining buffered events; without, they complete.
10514
+ */
10515
+ release(error) {
10516
+ const subscribers = [...this._subscribers];
10517
+ this._subscribers.clear();
10518
+ for (const { queue } of subscribers) {
10519
+ if (error) {
10520
+ queue.fail(error);
10521
+ } else {
10522
+ queue.close();
10523
+ }
10524
+ }
10315
10525
  }
10316
- async mintHookTicket(subscriptions, ttlSeconds, signal) {
10317
- const host = this._config.host ?? this.context.hosts[0];
10318
- const headers = this.createInvokeHeaders(subscriptions);
10319
- const ticketResponse = await this.context.fetch(`${host}/hooks/tickets`, {
10320
- method: "POST",
10321
- headers: {
10322
- ...serviceHeadersToRecord(headers),
10323
- "content-type": "application/json"
10324
- },
10325
- body: JSON.stringify({
10326
- subscriptions,
10327
- ttlSeconds
10328
- }),
10329
- signal
10330
- });
10331
- if (!ticketResponse.ok) {
10332
- throw await responseError(
10333
- "hooks",
10334
- "failed to mint hook ticket",
10335
- ticketResponse
10336
- );
10526
+ report(error) {
10527
+ if (error.meta && typeof error.meta === "object") {
10528
+ Object.freeze(error.meta);
10337
10529
  }
10338
- const ticketJson = await ticketResponse.json();
10339
- if (!ticketJson?.ticket) {
10340
- throw new Error("Hook ticket response did not include a ticket");
10530
+ Object.freeze(error);
10531
+ try {
10532
+ this.emitError(error, "stream");
10533
+ } catch {
10341
10534
  }
10342
- return ticketJson;
10343
10535
  }
10344
- async openHookStream(host, ticket, signal) {
10345
- const streamResponse = await this.context.fetch(
10346
- `${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`,
10347
- {
10348
- method: "GET",
10349
- headers: { accept: "text/event-stream" },
10350
- signal
10351
- }
10536
+ /**
10537
+ * Unreachable by construction. If the loop ever rejects anyway, fail its
10538
+ * subscribers visibly instead of stranding them or crashing the process.
10539
+ */
10540
+ collapse() {
10541
+ this._supervising = false;
10542
+ this._phase?.interrupt.abort();
10543
+ this._phase = void 0;
10544
+ const error = serviceError(
10545
+ ErrorCodes.NETWORK_ERROR,
10546
+ "hook stream supervisor stopped unexpectedly",
10547
+ "hooks"
10352
10548
  );
10353
- if (!streamResponse.ok) {
10354
- throw await responseError(
10355
- "hooks",
10356
- "failed to open hook stream",
10357
- streamResponse
10358
- );
10359
- }
10360
- return streamResponse;
10549
+ this.report(error);
10550
+ this.release(error);
10551
+ }
10552
+ onSessionChange(session) {
10553
+ super.onSessionChange(session);
10554
+ this._sessionGeneration += 1;
10555
+ this.wake();
10556
+ }
10557
+ onSignOut() {
10558
+ super.onSignOut();
10559
+ this.release();
10560
+ this.wake();
10561
+ }
10562
+ createHookHeaders(action, path) {
10563
+ return this.context.invoke(this.session, "hooks", path, action);
10361
10564
  }
10362
10565
  createInvokeHeaders(subscriptions) {
10363
10566
  const entries = subscriptions.map((subscription) => ({
@@ -10388,11 +10591,168 @@ function buildScopePath(service, pathPrefix) {
10388
10591
  const normalized = normalizePathPrefix(pathPrefix);
10389
10592
  return normalized ? `${service}/${normalized}` : service;
10390
10593
  }
10594
+ function hookStreamRetryDelay(attempt) {
10595
+ const cap = Math.min(
10596
+ HOOK_STREAM_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1),
10597
+ HOOK_STREAM_RETRY_MAX_DELAY_MS
10598
+ );
10599
+ return cap / 2 + Math.random() * (cap / 2);
10600
+ }
10601
+ function isTerminalMintRefusal(status) {
10602
+ return typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
10603
+ }
10604
+ var SUPERSEDED = /* @__PURE__ */ Symbol("hooks attempt superseded");
10605
+ async function awaitOwned(signal, operation) {
10606
+ if (signal.aborted) {
10607
+ if (typeof operation !== "function") {
10608
+ void operation.then(cancelLateResponse, () => void 0);
10609
+ }
10610
+ throw SUPERSEDED;
10611
+ }
10612
+ const promise = typeof operation === "function" ? operation() : operation;
10613
+ return new Promise((resolve5, reject) => {
10614
+ let settled = false;
10615
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
10616
+ const onAbort = () => {
10617
+ if (settled) return;
10618
+ settled = true;
10619
+ cleanup();
10620
+ reject(SUPERSEDED);
10621
+ };
10622
+ signal.addEventListener("abort", onAbort, { once: true });
10623
+ promise.then(
10624
+ (value) => {
10625
+ if (settled) {
10626
+ cancelLateResponse(value);
10627
+ return;
10628
+ }
10629
+ settled = true;
10630
+ cleanup();
10631
+ resolve5(value);
10632
+ },
10633
+ (error) => {
10634
+ if (settled) return;
10635
+ settled = true;
10636
+ cleanup();
10637
+ reject(error);
10638
+ }
10639
+ );
10640
+ });
10641
+ }
10642
+ function cancelLateResponse(value) {
10643
+ if (!(value instanceof Response) || !value.body) return;
10644
+ try {
10645
+ const cancellation = value.body.cancel();
10646
+ void cancellation.then(() => void 0, () => void 0);
10647
+ } catch {
10648
+ }
10649
+ }
10650
+ function hookStreamRefusal(status) {
10651
+ const code3 = typeof status === "number" ? status : void 0;
10652
+ return serviceError(
10653
+ ErrorCodes.NETWORK_ERROR,
10654
+ `hook stream open refused: ${code3 ?? "unknown status"}`,
10655
+ "hooks",
10656
+ { meta: { phase: "open", status: code3 } }
10657
+ );
10658
+ }
10659
+ var STREAM_ERROR_NAMES = /* @__PURE__ */ new Set([
10660
+ "Error",
10661
+ "TypeError",
10662
+ "AbortError",
10663
+ "TimeoutError",
10664
+ "SyntaxError",
10665
+ "RangeError",
10666
+ "NetworkError"
10667
+ ]);
10668
+ function hookStreamFailure(phase, thrown) {
10669
+ const candidateName = readProperty(thrown, "name");
10670
+ const name = typeof candidateName === "string" && STREAM_ERROR_NAMES.has(candidateName) ? candidateName : "Error";
10671
+ const code3 = identifier(readProperty(thrown, "code")) ?? identifier(readProperty(readProperty(thrown, "cause"), "code"));
10672
+ return serviceError(
10673
+ ErrorCodes.NETWORK_ERROR,
10674
+ `hook stream ${phase} failed: ${name}${code3 ? ` (${code3})` : ""}`,
10675
+ "hooks",
10676
+ { meta: { phase, errorName: name, ...code3 ? { errorCode: code3 } : {} } }
10677
+ );
10678
+ }
10679
+ var STREAM_ERROR_CODES = /* @__PURE__ */ new Set([
10680
+ "ECONNREFUSED",
10681
+ "ECONNRESET",
10682
+ "ECONNABORTED",
10683
+ "ETIMEDOUT",
10684
+ "EPIPE",
10685
+ "ENOTFOUND",
10686
+ "EAI_AGAIN",
10687
+ "EHOSTUNREACH",
10688
+ "ENETUNREACH",
10689
+ "ENETDOWN",
10690
+ "EADDRNOTAVAIL",
10691
+ "UND_ERR_SOCKET",
10692
+ "UND_ERR_CONNECT_TIMEOUT",
10693
+ "UND_ERR_HEADERS_TIMEOUT",
10694
+ "UND_ERR_BODY_TIMEOUT",
10695
+ "UND_ERR_CLOSED",
10696
+ "UND_ERR_ABORTED",
10697
+ "UND_ERR_RESPONSE_STATUS_CODE",
10698
+ "ERR_TLS_CERT_ALTNAME_INVALID",
10699
+ "CERT_HAS_EXPIRED",
10700
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
10701
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
10702
+ "SELF_SIGNED_CERT_IN_CHAIN",
10703
+ "ERR_SSL_WRONG_VERSION_NUMBER"
10704
+ ]);
10705
+ function identifier(value) {
10706
+ return typeof value === "string" && STREAM_ERROR_CODES.has(value) ? value : void 0;
10707
+ }
10708
+ function readProperty(value, key) {
10709
+ if (value === null || typeof value !== "object" && typeof value !== "function") {
10710
+ return void 0;
10711
+ }
10712
+ try {
10713
+ return Reflect.get(value, key);
10714
+ } catch {
10715
+ return void 0;
10716
+ }
10717
+ }
10718
+ function sleep(delayMs, _attempt, signal) {
10719
+ return new Promise((resolve5) => {
10720
+ const finish = () => {
10721
+ clearTimeout(timer);
10722
+ signal.removeEventListener("abort", stop);
10723
+ resolve5();
10724
+ };
10725
+ const timer = setTimeout(finish, delayMs);
10726
+ timer.unref?.();
10727
+ const stop = () => finish();
10728
+ if (signal.aborted) {
10729
+ finish();
10730
+ } else {
10731
+ signal.addEventListener("abort", stop, { once: true });
10732
+ }
10733
+ });
10734
+ }
10735
+ function abortPromise(signal) {
10736
+ let resolve5;
10737
+ const promise = new Promise((done) => {
10738
+ resolve5 = done;
10739
+ });
10740
+ const onAbort = () => resolve5();
10741
+ if (signal.aborted) {
10742
+ resolve5();
10743
+ } else {
10744
+ signal.addEventListener("abort", onAbort, { once: true });
10745
+ }
10746
+ return {
10747
+ promise,
10748
+ dispose: () => signal.removeEventListener("abort", onAbort)
10749
+ };
10750
+ }
10391
10751
  function normalizeSubscription(subscription) {
10392
10752
  return {
10393
10753
  ...subscription,
10394
10754
  pathPrefix: normalizePathPrefix(subscription.pathPrefix),
10395
- abilities: subscription.abilities ?? []
10755
+ abilities: subscription.abilities ? [...subscription.abilities] : []
10396
10756
  };
10397
10757
  }
10398
10758
  function subscriptionSignature(subscription) {
@@ -10458,9 +10818,6 @@ async function responseError(service, message, response) {
10458
10818
  meta: { ...error.meta, status: response.status, statusText: response.statusText }
10459
10819
  };
10460
10820
  }
10461
- function isAbortError(error) {
10462
- return error instanceof DOMException && error.name === "AbortError";
10463
- }
10464
10821
  async function* parseSseStream(body, signal) {
10465
10822
  if (!body) {
10466
10823
  throw new Error("Hook stream response does not expose a readable body");
@@ -10489,11 +10846,24 @@ async function* parseSseStream(body, signal) {
10489
10846
  async function* readBodyChunks(body, signal) {
10490
10847
  const asyncIterable = body;
10491
10848
  if (typeof asyncIterable?.[Symbol.asyncIterator] === "function") {
10492
- for await (const chunk of asyncIterable) {
10493
- if (signal?.aborted) {
10494
- break;
10849
+ const iterator = asyncIterable[Symbol.asyncIterator]();
10850
+ const cancel = () => {
10851
+ try {
10852
+ const closing = iterator.return?.();
10853
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
10854
+ } catch {
10855
+ }
10856
+ };
10857
+ signal?.addEventListener("abort", cancel, { once: true });
10858
+ try {
10859
+ while (!signal?.aborted) {
10860
+ const next = await iterator.next();
10861
+ if (next.done) break;
10862
+ yield next.value;
10495
10863
  }
10496
- yield chunk;
10864
+ } finally {
10865
+ signal?.removeEventListener("abort", cancel);
10866
+ cancel();
10497
10867
  }
10498
10868
  return;
10499
10869
  }
@@ -10502,21 +10872,23 @@ async function* readBodyChunks(body, signal) {
10502
10872
  throw new Error("Unsupported hook stream body type");
10503
10873
  }
10504
10874
  const reader = stream.getReader();
10875
+ const cancelReader = () => {
10876
+ try {
10877
+ const cancellation = reader.cancel?.();
10878
+ if (cancellation) void cancellation.then(() => void 0, () => void 0);
10879
+ } catch {
10880
+ }
10881
+ };
10882
+ signal?.addEventListener("abort", cancelReader, { once: true });
10505
10883
  try {
10506
10884
  while (!signal?.aborted) {
10507
10885
  const { done, value } = await reader.read();
10508
- if (done) {
10509
- break;
10510
- }
10511
- if (value) {
10512
- yield value;
10513
- }
10886
+ if (done) break;
10887
+ if (value) yield value;
10514
10888
  }
10515
10889
  } finally {
10516
- try {
10517
- await reader.cancel?.();
10518
- } catch {
10519
- }
10890
+ signal?.removeEventListener("abort", cancelReader);
10891
+ cancelReader();
10520
10892
  reader.releaseLock?.();
10521
10893
  }
10522
10894
  }
@@ -12897,10 +13269,10 @@ var EncryptionService = class extends BaseService {
12897
13269
  get crypto() {
12898
13270
  return this._config.crypto;
12899
13271
  }
12900
- async discoverNetwork(identifier, ownerDid) {
13272
+ async discoverNetwork(identifier2, ownerDid) {
12901
13273
  this.assertActive();
12902
13274
  const result = await discoverNetwork({
12903
- identifier,
13275
+ identifier: identifier2,
12904
13276
  ...ownerDid !== void 0 ? { ownerDid } : {},
12905
13277
  ...this._config.node !== void 0 ? { node: this._config.node } : {},
12906
13278
  ...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}
@@ -30222,17 +30594,17 @@ function validRecipientDid(value) {
30222
30594
  if (value.length === 0 || value.length > 2048 || /[\u0000-\u0020\u007f]/.test(value)) return false;
30223
30595
  const parts = value.split(":");
30224
30596
  if (parts.length < 3 || parts[0] !== "did" || !/^[a-z0-9]+$/.test(parts[1] ?? "")) return false;
30225
- const identifier = parts.slice(2);
30226
- if (identifier.some((part) => part.length === 0)) return false;
30597
+ const identifier2 = parts.slice(2);
30598
+ if (identifier2.some((part) => part.length === 0)) return false;
30227
30599
  if (parts[1] === "web") {
30228
- const host = identifier[0] ?? "";
30600
+ const host = identifier2[0] ?? "";
30229
30601
  if (host.length > 253 || host.split(".").some((label) => !label || label.length > 63 || !/^[A-Za-z0-9-]+$/.test(label) || label.startsWith("-") || label.endsWith("-"))) return false;
30230
- return identifier.slice(1).every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
30602
+ return identifier2.slice(1).every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
30231
30603
  }
30232
- if (parts[1] === "pkh") return identifier.length >= 3 && identifier.every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
30604
+ if (parts[1] === "pkh") return identifier2.length >= 3 && identifier2.every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
30233
30605
  if (parts[1] === "key") {
30234
30606
  try {
30235
- const bytes3 = base58btc2.decode(identifier.join(":"));
30607
+ const bytes3 = base58btc2.decode(identifier2.join(":"));
30236
30608
  return bytes3.length === 34 && bytes3[0] === 237 && bytes3[1] === 1;
30237
30609
  } catch {
30238
30610
  return false;