@tinycloud/cli 1.1.0-beta.18 → 1.1.0-beta.20

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
  );
@@ -10000,7 +10000,7 @@ var AsyncQueue = class {
10000
10000
  }
10001
10001
  const waiter = this.waiters.shift();
10002
10002
  if (waiter) {
10003
- waiter({ value, done: false });
10003
+ waiter.resolve({ value, done: false });
10004
10004
  return;
10005
10005
  }
10006
10006
  this.values.push(value);
@@ -10012,7 +10012,22 @@ var AsyncQueue = class {
10012
10012
  this.closed = true;
10013
10013
  while (this.waiters.length > 0) {
10014
10014
  const waiter = this.waiters.shift();
10015
- waiter?.({ value: void 0, done: true });
10015
+ waiter?.resolve({ value: void 0, done: true });
10016
+ }
10017
+ }
10018
+ /**
10019
+ * Fail the queue: pending and future `next()` calls reject with `error`,
10020
+ * so a `for await` consumer sees the iterator throw.
10021
+ */
10022
+ fail(error) {
10023
+ if (this.closed) {
10024
+ return;
10025
+ }
10026
+ this.closed = true;
10027
+ this.failure = error;
10028
+ while (this.waiters.length > 0) {
10029
+ const waiter = this.waiters.shift();
10030
+ waiter?.reject(error);
10016
10031
  }
10017
10032
  }
10018
10033
  next() {
@@ -10020,20 +10035,26 @@ var AsyncQueue = class {
10020
10035
  const value = this.values.shift();
10021
10036
  return Promise.resolve({ value, done: false });
10022
10037
  }
10038
+ if (this.failure !== void 0) {
10039
+ return Promise.reject(this.failure);
10040
+ }
10023
10041
  if (this.closed) {
10024
10042
  return Promise.resolve({ value: void 0, done: true });
10025
10043
  }
10026
- return new Promise((resolve5) => {
10027
- this.waiters.push(resolve5);
10044
+ return new Promise((resolve5, reject) => {
10045
+ this.waiters.push({ resolve: resolve5, reject });
10028
10046
  });
10029
10047
  }
10030
10048
  };
10049
+ var HOOK_STREAM_RETRY_BASE_DELAY_MS = 250;
10050
+ var HOOK_STREAM_RETRY_MAX_DELAY_MS = 3e4;
10051
+ var HOOK_STREAM_HEALTHY_MS = 5e3;
10031
10052
  var HooksService = class extends BaseService {
10032
10053
  constructor(config = {}) {
10033
10054
  super();
10034
10055
  this._subscribers = /* @__PURE__ */ new Set();
10035
- this._refreshChain = Promise.resolve();
10036
- this._activeSignature = "";
10056
+ this._supervising = false;
10057
+ this._sessionGeneration = 0;
10037
10058
  this._config = config;
10038
10059
  }
10039
10060
  get config() {
@@ -10049,35 +10070,32 @@ var HooksService = class extends BaseService {
10049
10070
  if (subscriptions.length === 0) {
10050
10071
  throw new Error("At least one hook subscription is required");
10051
10072
  }
10052
- const normalized = subscriptions.map(normalizeSubscription);
10073
+ const requested = subscriptions.map(normalizeSubscription);
10053
10074
  const subscriber = {
10054
- requested: normalized,
10075
+ requested,
10076
+ signatures: requested.map(subscriptionSignature),
10055
10077
  ttlSeconds: options.ttlSeconds,
10056
10078
  queue: new AsyncQueue()
10057
10079
  };
10058
- this._subscribers.add(subscriber);
10059
- const abortHandler = () => {
10080
+ const leave = () => {
10060
10081
  this._subscribers.delete(subscriber);
10061
10082
  subscriber.queue.close();
10062
- void this.scheduleSharedStreamRefresh();
10083
+ this.wake();
10063
10084
  };
10064
- if (options.signal) {
10065
- if (options.signal.aborted) {
10066
- abortHandler();
10067
- } else {
10068
- options.signal.addEventListener("abort", abortHandler, { once: true });
10069
- }
10085
+ const { signal } = options;
10086
+ if (signal?.aborted) {
10087
+ return;
10070
10088
  }
10071
- void this.scheduleSharedStreamRefresh();
10089
+ signal?.addEventListener("abort", leave, { once: true });
10090
+ this._subscribers.add(subscriber);
10091
+ this.wake();
10072
10092
  try {
10073
10093
  for await (const event of subscriber.queue) {
10074
10094
  yield event;
10075
10095
  }
10076
10096
  } finally {
10077
- if (options.signal) {
10078
- options.signal.removeEventListener("abort", abortHandler);
10079
- }
10080
- abortHandler();
10097
+ signal?.removeEventListener("abort", leave);
10098
+ leave();
10081
10099
  }
10082
10100
  }
10083
10101
  async register(webhook) {
@@ -10218,146 +10236,292 @@ var HooksService = class extends BaseService {
10218
10236
  return err(wrapError2("hooks", error));
10219
10237
  }
10220
10238
  }
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();
10239
+ /**
10240
+ * Called after anything that may change what the supervisor should do:
10241
+ * subscribe, unsubscribe, session change, sign-out. Interrupts the running
10242
+ * step if the plan moved away from it, and starts the supervisor if none
10243
+ * runs. Touches only SDK-owned state, so it never throws into callers such
10244
+ * as `ServiceContext.setSession`.
10245
+ */
10246
+ wake() {
10247
+ const phase = this._phase;
10248
+ if (phase && phase.key !== this.plan()?.key) {
10249
+ phase.interrupt.abort();
10235
10250
  }
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
- });
10251
+ if (this._supervising || this._subscribers.size === 0) {
10252
+ return;
10248
10253
  }
10254
+ this._supervising = true;
10255
+ this.supervise().catch(() => this.collapse());
10249
10256
  }
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);
10257
+ /**
10258
+ * The only owner of the shared stream. Each iteration is one attempt
10259
+ * (mint → open → drain), then a classification and, unless the attempt was
10260
+ * superseded or released its subscribers, a backoff. Every await is a
10261
+ * `phase` that wake() can interrupt; an interrupted phase yields
10262
+ * `undefined`, so a superseded mint can never act. Outside `attempt` and
10263
+ * `pause`, which each own one catch, the loop touches only SDK state.
10264
+ */
10265
+ async supervise() {
10266
+ let streak = 0;
10267
+ for (; ; ) {
10268
+ const plan = this.plan();
10269
+ if (!plan) {
10270
+ this._supervising = false;
10271
+ return;
10272
+ }
10273
+ const health = { openedAt: 0, delivered: false };
10274
+ const outcome = await this.phase(
10275
+ plan.key,
10276
+ (signal) => this.attempt(plan, signal, health)
10277
+ );
10278
+ if (health.delivered || health.openedAt > 0 && performance.now() - health.openedAt >= HOOK_STREAM_HEALTHY_MS) {
10279
+ streak = 0;
10280
+ }
10281
+ if (!outcome) {
10282
+ continue;
10283
+ }
10284
+ if (this.plan()?.key !== plan.key) {
10285
+ continue;
10286
+ }
10287
+ if (outcome.kind === "stopped") {
10288
+ this.release();
10289
+ continue;
10256
10290
  }
10257
- for (const subscription of subscriber.requested) {
10258
- merged.set(subscriptionSignature(subscription), subscription);
10291
+ if (outcome.kind === "refused" && outcome.terminal) {
10292
+ this.release(outcome.error);
10293
+ this.report(outcome.error);
10294
+ continue;
10295
+ }
10296
+ if (outcome.kind !== "ended") {
10297
+ this.report(outcome.error);
10259
10298
  }
10299
+ streak += 1;
10300
+ await this.phase(
10301
+ plan.key,
10302
+ (signal) => this.pause(streak, signal)
10303
+ );
10304
+ }
10305
+ }
10306
+ /**
10307
+ * Run one step under an interrupt that wake() fires when the plan key moves
10308
+ * on. An interrupted step's result is discarded. The interrupt is also
10309
+ * fired when the step returns, which releases whatever it left open: the
10310
+ * request signal and its listeners, an unread response body, a timer.
10311
+ */
10312
+ async phase(key, step) {
10313
+ const interrupt = new AbortController();
10314
+ this._phase = { key, interrupt };
10315
+ try {
10316
+ if (this.plan()?.key !== key) {
10317
+ interrupt.abort();
10318
+ }
10319
+ const result = await step(interrupt.signal);
10320
+ return interrupt.signal.aborted ? void 0 : result;
10321
+ } finally {
10322
+ this._phase = void 0;
10323
+ interrupt.abort();
10260
10324
  }
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
10325
  }
10275
- async runSharedStream(state) {
10276
- const abortController = new AbortController();
10277
- this._sharedStreamAbort = abortController;
10326
+ /**
10327
+ * One mint → open → drain. The single catch site for everything the
10328
+ * attempt touches that the SDK does not own: context accessors, invoke,
10329
+ * fetch, response bodies, the SSE parser. Never rejects.
10330
+ */
10331
+ async attempt(plan, signal, health) {
10332
+ let phase = "mint";
10333
+ let request;
10334
+ const owns = () => !signal.aborted && !request?.signal.aborted && this.plan()?.key === plan.key;
10278
10335
  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
10336
+ const context = this.context;
10337
+ const contextSignal = context.abortSignal;
10338
+ this._lastContextSignal = contextSignal;
10339
+ if (!context.isAuthenticated || contextSignal?.aborted) {
10340
+ return { kind: "stopped" };
10341
+ }
10342
+ request = this.createRequestSignal(signal, 0);
10343
+ const host = this.host;
10344
+ const minted = await awaitOwned(
10345
+ request.signal,
10346
+ () => context.fetch(`${host}/hooks/tickets`, {
10347
+ method: "POST",
10348
+ headers: {
10349
+ ...serviceHeadersToRecord(this.createInvokeHeaders(plan.subscriptions)),
10350
+ "content-type": "application/json"
10351
+ },
10352
+ body: JSON.stringify({
10353
+ subscriptions: plan.subscriptions,
10354
+ ttlSeconds: plan.ttlSeconds
10355
+ }),
10356
+ signal: request.signal
10357
+ })
10284
10358
  );
10285
- const streamResponse = await this.openHookStream(
10286
- host,
10287
- ticketResponse.ticket,
10288
- abortController.signal
10359
+ if (!owns()) return { kind: "stopped" };
10360
+ if (!minted.ok) {
10361
+ const status = minted.status;
10362
+ const error = await awaitOwned(
10363
+ request.signal,
10364
+ () => responseError("hooks", "failed to mint hook ticket", minted)
10365
+ );
10366
+ if (!owns()) return { kind: "stopped" };
10367
+ return { kind: "refused", terminal: isTerminalMintRefusal(status), error };
10368
+ }
10369
+ const body = await awaitOwned(request.signal, () => minted.json());
10370
+ if (!owns()) return { kind: "stopped" };
10371
+ const ticket = body !== null && typeof body === "object" && "ticket" in body ? body.ticket : void 0;
10372
+ if (typeof ticket !== "string" || ticket.length === 0) {
10373
+ throw new Error("Hook ticket response did not include a ticket");
10374
+ }
10375
+ phase = "open";
10376
+ if (!owns()) return { kind: "stopped" };
10377
+ const stream = await awaitOwned(
10378
+ request.signal,
10379
+ () => context.fetch(`${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`, {
10380
+ method: "GET",
10381
+ headers: { accept: "text/event-stream" },
10382
+ signal: request.signal
10383
+ })
10289
10384
  );
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
- }
10385
+ if (!owns()) return { kind: "stopped" };
10386
+ if (!stream.ok) {
10387
+ return { kind: "refused", terminal: false, error: hookStreamRefusal(stream.status) };
10388
+ }
10389
+ health.openedAt = performance.now();
10390
+ const iterator = parseSseStream(stream.body, request.signal)[Symbol.asyncIterator]();
10391
+ try {
10392
+ for (; ; ) {
10393
+ const next = await awaitOwned(request.signal, () => iterator.next());
10394
+ if (!owns()) return { kind: "stopped" };
10395
+ if (next.done) break;
10396
+ const message = next.value;
10397
+ if (!message.data) continue;
10398
+ const event = parseHookEvent(message);
10399
+ if (!owns()) return { kind: "stopped" };
10400
+ this.dispatch(event);
10401
+ health.delivered = true;
10302
10402
  }
10403
+ } finally {
10404
+ const closing = iterator.return?.();
10405
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
10303
10406
  }
10407
+ return owns() ? { kind: "ended" } : { kind: "stopped" };
10408
+ } catch (thrown) {
10409
+ return request?.signal.aborted || thrown === SUPERSEDED ? { kind: "stopped" } : { kind: "failed", error: hookStreamFailure(phase, thrown) };
10410
+ }
10411
+ }
10412
+ /**
10413
+ * Back off before the next attempt. `config.streamRetry` overrides the
10414
+ * delay and the clock; whatever they do, the pause ends by the delay or the
10415
+ * interrupt, then yields one timer turn so an instant clock cannot starve
10416
+ * due timers or the process.
10417
+ */
10418
+ async pause(attempt, signal) {
10419
+ try {
10420
+ this._lastContextSignal = this.context.abortSignal;
10421
+ } catch {
10422
+ }
10423
+ const contextSignal = this._lastContextSignal;
10424
+ const waits = [];
10425
+ const aborts = [];
10426
+ try {
10427
+ const retry = this._config.streamRetry;
10428
+ const delayMs = retry?.delay?.(attempt) ?? hookStreamRetryDelay(attempt);
10429
+ waits.push((retry?.wait ?? sleep)(delayMs, attempt, signal));
10430
+ } catch {
10431
+ waits.push(Promise.resolve());
10432
+ }
10433
+ aborts.push(abortPromise(signal));
10434
+ if (contextSignal) aborts.push(abortPromise(contextSignal));
10435
+ try {
10436
+ await Promise.race([...waits, ...aborts.map(({ promise }) => promise)]);
10437
+ } catch {
10304
10438
  } finally {
10305
- if (this._sharedStreamAbort === abortController) {
10306
- this._sharedStreamAbort = void 0;
10439
+ for (const abort of aborts) abort.dispose();
10440
+ }
10441
+ await new Promise((resolve5) => setTimeout(resolve5, 0));
10442
+ }
10443
+ /** The stream the current subscribers need, or undefined when none remain. */
10444
+ plan() {
10445
+ if (this._subscribers.size === 0) {
10446
+ return void 0;
10447
+ }
10448
+ const merged = /* @__PURE__ */ new Map();
10449
+ let ttlSeconds;
10450
+ for (const subscriber of this._subscribers) {
10451
+ subscriber.requested.forEach(
10452
+ (subscription, index) => merged.set(subscriber.signatures[index], subscription)
10453
+ );
10454
+ if (typeof subscriber.ttlSeconds === "number") {
10455
+ ttlSeconds = Math.min(subscriber.ttlSeconds, ttlSeconds ?? Infinity);
10307
10456
  }
10308
10457
  }
10458
+ const signatures = [...merged.keys()].sort();
10459
+ return {
10460
+ key: JSON.stringify([this._sessionGeneration, signatures, ttlSeconds ?? null]),
10461
+ subscriptions: signatures.map((signature) => merged.get(signature)),
10462
+ ttlSeconds
10463
+ };
10309
10464
  }
10310
- abortSharedStream() {
10311
- this._sharedStreamAbort?.abort();
10465
+ dispatch(event) {
10466
+ for (const subscriber of this._subscribers) {
10467
+ if (matchesAnySubscription(event, subscriber.requested)) {
10468
+ subscriber.queue.push(event);
10469
+ }
10470
+ }
10312
10471
  }
10313
- createHookHeaders(action, path) {
10314
- return this.context.invoke(this.session, "hooks", path, action);
10472
+ /**
10473
+ * Detach every subscriber. With `error`, their iterators throw it after
10474
+ * draining buffered events; without, they complete.
10475
+ */
10476
+ release(error) {
10477
+ const subscribers = [...this._subscribers];
10478
+ this._subscribers.clear();
10479
+ for (const { queue } of subscribers) {
10480
+ if (error) {
10481
+ queue.fail(error);
10482
+ } else {
10483
+ queue.close();
10484
+ }
10485
+ }
10315
10486
  }
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
- );
10487
+ report(error) {
10488
+ if (error.meta && typeof error.meta === "object") {
10489
+ Object.freeze(error.meta);
10337
10490
  }
10338
- const ticketJson = await ticketResponse.json();
10339
- if (!ticketJson?.ticket) {
10340
- throw new Error("Hook ticket response did not include a ticket");
10491
+ Object.freeze(error);
10492
+ try {
10493
+ this.emitError(error, "stream");
10494
+ } catch {
10341
10495
  }
10342
- return ticketJson;
10343
10496
  }
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
- }
10497
+ /**
10498
+ * Unreachable by construction. If the loop ever rejects anyway, fail its
10499
+ * subscribers visibly instead of stranding them or crashing the process.
10500
+ */
10501
+ collapse() {
10502
+ this._supervising = false;
10503
+ this._phase?.interrupt.abort();
10504
+ this._phase = void 0;
10505
+ const error = serviceError(
10506
+ ErrorCodes.NETWORK_ERROR,
10507
+ "hook stream supervisor stopped unexpectedly",
10508
+ "hooks"
10352
10509
  );
10353
- if (!streamResponse.ok) {
10354
- throw await responseError(
10355
- "hooks",
10356
- "failed to open hook stream",
10357
- streamResponse
10358
- );
10359
- }
10360
- return streamResponse;
10510
+ this.report(error);
10511
+ this.release(error);
10512
+ }
10513
+ onSessionChange(session) {
10514
+ super.onSessionChange(session);
10515
+ this._sessionGeneration += 1;
10516
+ this.wake();
10517
+ }
10518
+ onSignOut() {
10519
+ super.onSignOut();
10520
+ this.release();
10521
+ this.wake();
10522
+ }
10523
+ createHookHeaders(action, path) {
10524
+ return this.context.invoke(this.session, "hooks", path, action);
10361
10525
  }
10362
10526
  createInvokeHeaders(subscriptions) {
10363
10527
  const entries = subscriptions.map((subscription) => ({
@@ -10388,11 +10552,168 @@ function buildScopePath(service, pathPrefix) {
10388
10552
  const normalized = normalizePathPrefix(pathPrefix);
10389
10553
  return normalized ? `${service}/${normalized}` : service;
10390
10554
  }
10555
+ function hookStreamRetryDelay(attempt) {
10556
+ const cap = Math.min(
10557
+ HOOK_STREAM_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1),
10558
+ HOOK_STREAM_RETRY_MAX_DELAY_MS
10559
+ );
10560
+ return cap / 2 + Math.random() * (cap / 2);
10561
+ }
10562
+ function isTerminalMintRefusal(status) {
10563
+ return typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
10564
+ }
10565
+ var SUPERSEDED = /* @__PURE__ */ Symbol("hooks attempt superseded");
10566
+ async function awaitOwned(signal, operation) {
10567
+ if (signal.aborted) {
10568
+ if (typeof operation !== "function") {
10569
+ void operation.then(cancelLateResponse, () => void 0);
10570
+ }
10571
+ throw SUPERSEDED;
10572
+ }
10573
+ const promise = typeof operation === "function" ? operation() : operation;
10574
+ return new Promise((resolve5, reject) => {
10575
+ let settled = false;
10576
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
10577
+ const onAbort = () => {
10578
+ if (settled) return;
10579
+ settled = true;
10580
+ cleanup();
10581
+ reject(SUPERSEDED);
10582
+ };
10583
+ signal.addEventListener("abort", onAbort, { once: true });
10584
+ promise.then(
10585
+ (value) => {
10586
+ if (settled) {
10587
+ cancelLateResponse(value);
10588
+ return;
10589
+ }
10590
+ settled = true;
10591
+ cleanup();
10592
+ resolve5(value);
10593
+ },
10594
+ (error) => {
10595
+ if (settled) return;
10596
+ settled = true;
10597
+ cleanup();
10598
+ reject(error);
10599
+ }
10600
+ );
10601
+ });
10602
+ }
10603
+ function cancelLateResponse(value) {
10604
+ if (!(value instanceof Response) || !value.body) return;
10605
+ try {
10606
+ const cancellation = value.body.cancel();
10607
+ void cancellation.then(() => void 0, () => void 0);
10608
+ } catch {
10609
+ }
10610
+ }
10611
+ function hookStreamRefusal(status) {
10612
+ const code3 = typeof status === "number" ? status : void 0;
10613
+ return serviceError(
10614
+ ErrorCodes.NETWORK_ERROR,
10615
+ `hook stream open refused: ${code3 ?? "unknown status"}`,
10616
+ "hooks",
10617
+ { meta: { phase: "open", status: code3 } }
10618
+ );
10619
+ }
10620
+ var STREAM_ERROR_NAMES = /* @__PURE__ */ new Set([
10621
+ "Error",
10622
+ "TypeError",
10623
+ "AbortError",
10624
+ "TimeoutError",
10625
+ "SyntaxError",
10626
+ "RangeError",
10627
+ "NetworkError"
10628
+ ]);
10629
+ function hookStreamFailure(phase, thrown) {
10630
+ const candidateName = readProperty(thrown, "name");
10631
+ const name = typeof candidateName === "string" && STREAM_ERROR_NAMES.has(candidateName) ? candidateName : "Error";
10632
+ const code3 = identifier(readProperty(thrown, "code")) ?? identifier(readProperty(readProperty(thrown, "cause"), "code"));
10633
+ return serviceError(
10634
+ ErrorCodes.NETWORK_ERROR,
10635
+ `hook stream ${phase} failed: ${name}${code3 ? ` (${code3})` : ""}`,
10636
+ "hooks",
10637
+ { meta: { phase, errorName: name, ...code3 ? { errorCode: code3 } : {} } }
10638
+ );
10639
+ }
10640
+ var STREAM_ERROR_CODES = /* @__PURE__ */ new Set([
10641
+ "ECONNREFUSED",
10642
+ "ECONNRESET",
10643
+ "ECONNABORTED",
10644
+ "ETIMEDOUT",
10645
+ "EPIPE",
10646
+ "ENOTFOUND",
10647
+ "EAI_AGAIN",
10648
+ "EHOSTUNREACH",
10649
+ "ENETUNREACH",
10650
+ "ENETDOWN",
10651
+ "EADDRNOTAVAIL",
10652
+ "UND_ERR_SOCKET",
10653
+ "UND_ERR_CONNECT_TIMEOUT",
10654
+ "UND_ERR_HEADERS_TIMEOUT",
10655
+ "UND_ERR_BODY_TIMEOUT",
10656
+ "UND_ERR_CLOSED",
10657
+ "UND_ERR_ABORTED",
10658
+ "UND_ERR_RESPONSE_STATUS_CODE",
10659
+ "ERR_TLS_CERT_ALTNAME_INVALID",
10660
+ "CERT_HAS_EXPIRED",
10661
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
10662
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
10663
+ "SELF_SIGNED_CERT_IN_CHAIN",
10664
+ "ERR_SSL_WRONG_VERSION_NUMBER"
10665
+ ]);
10666
+ function identifier(value) {
10667
+ return typeof value === "string" && STREAM_ERROR_CODES.has(value) ? value : void 0;
10668
+ }
10669
+ function readProperty(value, key) {
10670
+ if (value === null || typeof value !== "object" && typeof value !== "function") {
10671
+ return void 0;
10672
+ }
10673
+ try {
10674
+ return Reflect.get(value, key);
10675
+ } catch {
10676
+ return void 0;
10677
+ }
10678
+ }
10679
+ function sleep(delayMs, _attempt, signal) {
10680
+ return new Promise((resolve5) => {
10681
+ const finish = () => {
10682
+ clearTimeout(timer);
10683
+ signal.removeEventListener("abort", stop);
10684
+ resolve5();
10685
+ };
10686
+ const timer = setTimeout(finish, delayMs);
10687
+ timer.unref?.();
10688
+ const stop = () => finish();
10689
+ if (signal.aborted) {
10690
+ finish();
10691
+ } else {
10692
+ signal.addEventListener("abort", stop, { once: true });
10693
+ }
10694
+ });
10695
+ }
10696
+ function abortPromise(signal) {
10697
+ let resolve5;
10698
+ const promise = new Promise((done) => {
10699
+ resolve5 = done;
10700
+ });
10701
+ const onAbort = () => resolve5();
10702
+ if (signal.aborted) {
10703
+ resolve5();
10704
+ } else {
10705
+ signal.addEventListener("abort", onAbort, { once: true });
10706
+ }
10707
+ return {
10708
+ promise,
10709
+ dispose: () => signal.removeEventListener("abort", onAbort)
10710
+ };
10711
+ }
10391
10712
  function normalizeSubscription(subscription) {
10392
10713
  return {
10393
10714
  ...subscription,
10394
10715
  pathPrefix: normalizePathPrefix(subscription.pathPrefix),
10395
- abilities: subscription.abilities ?? []
10716
+ abilities: subscription.abilities ? [...subscription.abilities] : []
10396
10717
  };
10397
10718
  }
10398
10719
  function subscriptionSignature(subscription) {
@@ -10458,9 +10779,6 @@ async function responseError(service, message, response) {
10458
10779
  meta: { ...error.meta, status: response.status, statusText: response.statusText }
10459
10780
  };
10460
10781
  }
10461
- function isAbortError(error) {
10462
- return error instanceof DOMException && error.name === "AbortError";
10463
- }
10464
10782
  async function* parseSseStream(body, signal) {
10465
10783
  if (!body) {
10466
10784
  throw new Error("Hook stream response does not expose a readable body");
@@ -10489,11 +10807,24 @@ async function* parseSseStream(body, signal) {
10489
10807
  async function* readBodyChunks(body, signal) {
10490
10808
  const asyncIterable = body;
10491
10809
  if (typeof asyncIterable?.[Symbol.asyncIterator] === "function") {
10492
- for await (const chunk of asyncIterable) {
10493
- if (signal?.aborted) {
10494
- break;
10810
+ const iterator = asyncIterable[Symbol.asyncIterator]();
10811
+ const cancel = () => {
10812
+ try {
10813
+ const closing = iterator.return?.();
10814
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
10815
+ } catch {
10495
10816
  }
10496
- yield chunk;
10817
+ };
10818
+ signal?.addEventListener("abort", cancel, { once: true });
10819
+ try {
10820
+ while (!signal?.aborted) {
10821
+ const next = await iterator.next();
10822
+ if (next.done) break;
10823
+ yield next.value;
10824
+ }
10825
+ } finally {
10826
+ signal?.removeEventListener("abort", cancel);
10827
+ cancel();
10497
10828
  }
10498
10829
  return;
10499
10830
  }
@@ -10502,21 +10833,23 @@ async function* readBodyChunks(body, signal) {
10502
10833
  throw new Error("Unsupported hook stream body type");
10503
10834
  }
10504
10835
  const reader = stream.getReader();
10836
+ const cancelReader = () => {
10837
+ try {
10838
+ const cancellation = reader.cancel?.();
10839
+ if (cancellation) void cancellation.then(() => void 0, () => void 0);
10840
+ } catch {
10841
+ }
10842
+ };
10843
+ signal?.addEventListener("abort", cancelReader, { once: true });
10505
10844
  try {
10506
10845
  while (!signal?.aborted) {
10507
10846
  const { done, value } = await reader.read();
10508
- if (done) {
10509
- break;
10510
- }
10511
- if (value) {
10512
- yield value;
10513
- }
10847
+ if (done) break;
10848
+ if (value) yield value;
10514
10849
  }
10515
10850
  } finally {
10516
- try {
10517
- await reader.cancel?.();
10518
- } catch {
10519
- }
10851
+ signal?.removeEventListener("abort", cancelReader);
10852
+ cancelReader();
10520
10853
  reader.releaseLock?.();
10521
10854
  }
10522
10855
  }
@@ -12897,10 +13230,10 @@ var EncryptionService = class extends BaseService {
12897
13230
  get crypto() {
12898
13231
  return this._config.crypto;
12899
13232
  }
12900
- async discoverNetwork(identifier, ownerDid) {
13233
+ async discoverNetwork(identifier2, ownerDid) {
12901
13234
  this.assertActive();
12902
13235
  const result = await discoverNetwork({
12903
- identifier,
13236
+ identifier: identifier2,
12904
13237
  ...ownerDid !== void 0 ? { ownerDid } : {},
12905
13238
  ...this._config.node !== void 0 ? { node: this._config.node } : {},
12906
13239
  ...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}
@@ -13804,6 +14137,9 @@ function permissionRequestsPath(profile) {
13804
14137
  function grantHistoryPath(profile) {
13805
14138
  return join4(PROFILES_DIR, profile, "auth-grants.jsonl");
13806
14139
  }
14140
+ function localGrantArtifactsPath(profile) {
14141
+ return join4(PROFILES_DIR, profile, "auth-grant-artifacts.json");
14142
+ }
13807
14143
  function createPermissionRequestArtifact(params) {
13808
14144
  return buildPermissionRequestArtifact({
13809
14145
  profile: params.profileName,
@@ -13828,6 +14164,32 @@ function didWithoutFragment(did) {
13828
14164
  async function loadAdditionalDelegations(profile) {
13829
14165
  return readAdditionalDelegations(profile);
13830
14166
  }
14167
+ async function loadLocalGrantArtifacts(profile) {
14168
+ const path = localGrantArtifactsPath(profile);
14169
+ if (!await fileExists(path)) return [];
14170
+ let parsed;
14171
+ try {
14172
+ parsed = JSON.parse(await readFile2(path, "utf8"));
14173
+ } catch {
14174
+ return [];
14175
+ }
14176
+ if (!Array.isArray(parsed)) return [];
14177
+ return parsed.filter(
14178
+ (value) => value !== null && typeof value === "object" && typeof value.delegationCid === "string" && value.delegation !== null && typeof value.delegation === "object" && value.delegation.cid === value.delegationCid && typeof value.delegation.delegationHeader?.Authorization === "string"
14179
+ );
14180
+ }
14181
+ async function appendLocalGrantArtifact(profile, delegation) {
14182
+ await ProfileManager.ensureProfileDir(profile);
14183
+ await withProfileLock2(profile, async () => {
14184
+ await refuseWriteToDeletedProfile2(profile);
14185
+ const records = await loadLocalGrantArtifacts(profile);
14186
+ if (records.some((record) => record.delegationCid === delegation.cid)) return;
14187
+ await writeJsonAtomic(localGrantArtifactsPath(profile), [
14188
+ ...records,
14189
+ { delegationCid: delegation.cid, delegation }
14190
+ ]);
14191
+ });
14192
+ }
13831
14193
  async function appendAdditionalDelegations(profile, entries) {
13832
14194
  const { mergeDelegationsWithoutRequest } = await import("@tinycloud/operations/delegation-binding");
13833
14195
  await updateProfileStore(profile, "additional-delegations", (stored) => ({
@@ -15246,11 +15608,15 @@ function ownerSpaceId(space, ownerDid) {
15246
15608
  function permissionTuples(permissions, ownerDid) {
15247
15609
  return new Set(permissions.flatMap((permission) => actionTuples(permission, ownerDid)));
15248
15610
  }
15611
+ function isRawCidRevocationPermission(permission) {
15612
+ return permission.service === "tinycloud.delegation" && permission.space?.startsWith("urn:cid:") === true && permission.path === "" && permission.actions.includes("tinycloud.delegation/revoke");
15613
+ }
15249
15614
  function actionTuples(permission, ownerDid) {
15250
15615
  const service = permission.service.startsWith("tinycloud.") ? permission.service : `tinycloud.${permission.service}`;
15251
- const raw = isVerifiedRawEncryptionPermission(permission);
15252
- const space = raw ? ENCRYPTION_MANIFEST_SPACE2 : normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid));
15253
- const path = raw ? normalizePkhIdentifier(permission.path) : permission.path;
15616
+ const rawEncryption = isVerifiedRawEncryptionPermission(permission);
15617
+ const rawCidRevocation = isRawCidRevocationPermission(permission);
15618
+ const space = rawEncryption ? ENCRYPTION_MANIFEST_SPACE2 : rawCidRevocation ? permission.space : normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", ownerDid));
15619
+ const path = rawEncryption ? normalizePkhIdentifier(permission.path) : permission.path;
15254
15620
  return permission.actions.map((action) => JSON.stringify([service, space, path, action.includes("/") ? action : `${service}/${action}`]));
15255
15621
  }
15256
15622
  function isLegacyNestedDecrypt(permission, requested, ownerDid, spaceId) {
@@ -15321,7 +15687,7 @@ function declinedPermissions(requested, signed, ownerDid) {
15321
15687
  const missing = [...permissionTuples(requested, ownerDid)].filter((tuple) => !granted.has(tuple));
15322
15688
  const requestedScopes = /* @__PURE__ */ new Map();
15323
15689
  for (const entry of requested) {
15324
- const space = isVerifiedRawEncryptionPermission(entry) ? entry.space ?? ENCRYPTION_MANIFEST_SPACE2 : ownerSpaceId(entry.space ?? "", ownerDid);
15690
+ const space = isVerifiedRawEncryptionPermission(entry) ? entry.space ?? ENCRYPTION_MANIFEST_SPACE2 : isRawCidRevocationPermission(entry) ? entry.space : ownerSpaceId(entry.space ?? "", ownerDid);
15325
15691
  for (const tuple of actionTuples(entry, ownerDid)) {
15326
15692
  requestedScopes.set(tuple, { space, path: entry.path });
15327
15693
  }
@@ -15344,7 +15710,10 @@ function scopedLoginPermissions(permissions) {
15344
15710
  }
15345
15711
  function grantRequestPermissions(group, anchorSpace) {
15346
15712
  const request = group.map((p) => isRawEncryptionPermission(p) ? { ...p, space: ENCRYPTION_MANIFEST_SPACE2, path: canonicalNetworkUrn(p.path) } : p);
15347
- return withCapabilitiesRead(request, request.find((p) => !isRawEncryptionPermission(p))?.space ?? anchorSpace);
15713
+ const capabilitySpace = request.find(
15714
+ (permission) => !isRawEncryptionPermission(permission) && !isRawCidRevocationPermission(permission)
15715
+ )?.space ?? anchorSpace;
15716
+ return withCapabilitiesRead(request, capabilitySpace);
15348
15717
  }
15349
15718
  function withCapabilitiesRead(permissions, space) {
15350
15719
  const hasRead = permissions.some((p) => p.service === "tinycloud.capabilities" && p.path === "" && p.actions.includes(CAPABILITIES_READ) && normalizePkhIdentifier(p.space ?? "") === normalizePkhIdentifier(space));
@@ -15378,7 +15747,10 @@ function verifySignedSession(data, key, sessionDid, expected = {}) {
15378
15747
  if (/expir/i.test(error instanceof Error ? error.message : String(error))) {
15379
15748
  throw new CLIError("AUTH_EXPIRED", `The approved session has expired. ${notStored}`, ExitCode.AUTH_REQUIRED);
15380
15749
  }
15381
- throw new CLIError("OPENKEY_PROOF_INVALID", `OpenKey did not return a complete, verifiable session proof. ${notStored}`, ExitCode.AUTH_REQUIRED);
15750
+ throw Object.assign(
15751
+ new CLIError("OPENKEY_PROOF_INVALID", `OpenKey did not return a complete, verifiable session proof. ${notStored}`, ExitCode.AUTH_REQUIRED),
15752
+ { cause: error }
15753
+ );
15382
15754
  }
15383
15755
  const signedExpiry = Date.parse(expiresAt);
15384
15756
  if (signedExpiry <= Date.now()) {
@@ -15395,10 +15767,23 @@ function verifySignedSession(data, key, sessionDid, expected = {}) {
15395
15767
  }
15396
15768
  function verifyScopedLogin(data, key, sessionDid, requested, expected = {}) {
15397
15769
  const notStored = expected.purpose === "grant" ? "No grant was stored." : "No scoped session was saved.";
15398
- const signed = verifySignedSession(data, key, sessionDid, expected);
15770
+ let signed;
15771
+ try {
15772
+ signed = verifySignedSession(data, key, sessionDid, expected);
15773
+ } catch (error) {
15774
+ const cause = error instanceof Error ? error.cause : void 0;
15775
+ if (requested.some(isRawCidRevocationPermission) && /invalid ReCap resource URI/i.test(cause instanceof Error ? cause.message : String(cause ?? ""))) {
15776
+ throw new CLIError(
15777
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
15778
+ "This WASM build cannot verify raw urn:cid ReCap resources.",
15779
+ ExitCode.PERMISSION_DENIED
15780
+ );
15781
+ }
15782
+ throw error;
15783
+ }
15399
15784
  const spaceId = data.spaceId;
15400
15785
  for (const permission of requested) {
15401
- if (isRawEncryptionPermission(permission)) continue;
15786
+ if (isRawEncryptionPermission(permission) || isRawCidRevocationPermission(permission)) continue;
15402
15787
  if (normalizePkhIdentifier(ownerSpaceId(permission.space ?? "", signed.ownerDid)) !== normalizePkhIdentifier(spaceId)) {
15403
15788
  throw new CLIError("OPENKEY_SCOPE_MISMATCH", `The approved space differs from the requested space. ${notStored}`, ExitCode.PERMISSION_DENIED);
15404
15789
  }
@@ -16439,6 +16824,13 @@ function registerAuthCommand(program) {
16439
16824
  yes: options.yes === true
16440
16825
  });
16441
16826
  const grant = await grantAuthRequest(node, resolvedRequest);
16827
+ await appendLocalGrantArtifact(ctx.profile, grant.delegation);
16828
+ await appendGrantHistory(ctx.profile, {
16829
+ addedCaps: grant.permissions,
16830
+ source: "cli",
16831
+ delegationCid: grant.delegationCid,
16832
+ expiry: grant.expiry
16833
+ });
16442
16834
  outputJson(grant);
16443
16835
  } catch (error) {
16444
16836
  handleError(error);
@@ -16795,7 +17187,10 @@ async function activateAndStoreOpenKeyGrants(profileName2, host, node, grants, s
16795
17187
  });
16796
17188
  }
16797
17189
  async function ensureDelegationAuthority(params) {
16798
- if (!params.force && params.node.hasRuntimePermissions(params.requested)) return;
17190
+ if (!params.force && params.node.hasRuntimePermissions(params.requested)) return void 0;
17191
+ if (!params.consentAlreadyGiven && (params.persist === false || params.profile.authMethod !== "openkey")) {
17192
+ await requirePermissionConsent(params.requested, params.yes);
17193
+ }
16799
17194
  if (params.profile.authMethod === "openkey") {
16800
17195
  const key = await ProfileManager.getKey(params.ctx.profile);
16801
17196
  if (!key) {
@@ -16815,6 +17210,37 @@ async function ensureDelegationAuthority(params) {
16815
17210
  expiry: expiryCap
16816
17211
  };
16817
17212
  const anchorSpace = params.anchorSpace ?? params.profile.spaceId ?? params.profile.spaceName;
17213
+ if (params.persist === false) {
17214
+ const request = grantRequestPermissions(params.requested, anchorSpace);
17215
+ let delegationData;
17216
+ try {
17217
+ delegationData = await acquireOpenKey(params.profile.did, {
17218
+ jwk: key,
17219
+ host: params.ctx.host,
17220
+ permissions: request,
17221
+ reason: permissionGrantReason(params.reason, request),
17222
+ openkeyHost,
17223
+ expiry: expiryCap === void 0 ? void 0 : openKeyExpiryParam(expiryCap)
17224
+ });
17225
+ } catch (error) {
17226
+ if (params.requested.some(isRawCidRevocationPermission) && /invalid ReCap resource URI/i.test(error instanceof Error ? error.message : String(error))) {
17227
+ throw new CLIError(
17228
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
17229
+ "The OpenKey/WASM ReCap encoder does not support raw urn:cid revocation resources.",
17230
+ ExitCode.PERMISSION_DENIED
17231
+ );
17232
+ }
17233
+ throw error;
17234
+ }
17235
+ const delegation = portableFromOpenKeyDelegation(
17236
+ delegationData,
17237
+ request,
17238
+ params.ctx.host,
17239
+ proof
17240
+ );
17241
+ await params.node.useRuntimeDelegation(delegation);
17242
+ return delegation;
17243
+ }
16818
17244
  const grants = [];
16819
17245
  for (const group of groupPermissionsBySpace(params.requested)) {
16820
17246
  const request = grantRequestPermissions(group, anchorSpace);
@@ -16832,21 +17258,11 @@ async function ensureDelegationAuthority(params) {
16832
17258
  await activateAndStoreOpenKeyGrants(params.ctx.profile, params.ctx.host, params.node, grants, "cli");
16833
17259
  return;
16834
17260
  }
16835
- if (isInteractive()) {
16836
- if (!params.yes) {
16837
- await confirmPermissionRequest(params.requested);
16838
- }
16839
- } else if (!params.yes) {
16840
- throw new CLIError(
16841
- "CONFIRMATION_REQUIRED",
16842
- "Local-key auth grants in non-interactive mode require --yes.",
16843
- ExitCode.USAGE_ERROR
16844
- );
16845
- }
16846
17261
  const delegations = await params.node.grantRuntimePermissions(
16847
17262
  params.requested,
16848
17263
  params.expiryOption !== void 0 ? { expiry: params.expiryOption } : void 0
16849
17264
  );
17265
+ if (params.persist === false) return delegations[0];
16850
17266
  for (const delegation of delegations) {
16851
17267
  const covering = permissionsFromDelegation(delegation);
16852
17268
  await appendAdditionalDelegation(
@@ -16924,6 +17340,17 @@ async function confirmPermissionRequest(permissions) {
16924
17340
  throw new CLIError("REQUEST_CANCELLED", "Permission request cancelled.", ExitCode.ERROR);
16925
17341
  }
16926
17342
  }
17343
+ async function requirePermissionConsent(permissions, yes) {
17344
+ if (isInteractive()) {
17345
+ if (!yes) await confirmPermissionRequest(permissions);
17346
+ } else if (!yes) {
17347
+ throw new CLIError(
17348
+ "CONFIRMATION_REQUIRED",
17349
+ "Permission grants in non-interactive mode require --yes.",
17350
+ ExitCode.USAGE_ERROR
17351
+ );
17352
+ }
17353
+ }
16927
17354
  function isDangerousPermission(permission) {
16928
17355
  if (permission.path === "" || permission.path === "/") return true;
16929
17356
  return permission.actions.some(
@@ -16952,7 +17379,7 @@ function groupPermissionsBySpace(permissions) {
16952
17379
  const groups = /* @__PURE__ */ new Map();
16953
17380
  const rawEntries = [];
16954
17381
  for (const permission of permissions) {
16955
- if (isRawEncryptionPermission(permission)) {
17382
+ if (isRawEncryptionPermission(permission) || isRawCidRevocationPermission(permission)) {
16956
17383
  rawEntries.push(permission);
16957
17384
  continue;
16958
17385
  }
@@ -16983,11 +17410,13 @@ function portableFromOpenKeyDelegation(data, requested, host, proof) {
16983
17410
  }
16984
17411
  const returnedSpace = data.spaceId;
16985
17412
  const effective = session.permissions;
16986
- const spaced = effective.filter((permission) => !isRawEncryptionPermission(permission));
17413
+ const spaced = effective.filter(
17414
+ (permission) => !isRawEncryptionPermission(permission) && !isRawCidRevocationPermission(permission)
17415
+ );
16987
17416
  const primary = spaced.find((permission) => permission.service !== "tinycloud.capabilities") ?? spaced[0] ?? effective[0];
16988
17417
  const resources = effective.map((permission) => ({
16989
17418
  service: permission.service.slice("tinycloud.".length),
16990
- space: isVerifiedRawEncryptionPermission(permission) ? "encryption" : returnedSpace,
17419
+ space: isRawCidRevocationPermission(permission) ? permission.space : isVerifiedRawEncryptionPermission(permission) ? "encryption" : returnedSpace,
16991
17420
  path: permission.path,
16992
17421
  actions: [...permission.actions],
16993
17422
  ...permission.caveats === void 0 ? {} : { caveats: structuredClone(permission.caveats) }
@@ -17463,6 +17892,7 @@ init_profiles();
17463
17892
  init_formatter();
17464
17893
  init_errors();
17465
17894
  init_constants();
17895
+ import { parseSignedCompactUcanAttenuation } from "@tinycloud/sdk-core";
17466
17896
  function normalizeDid(input) {
17467
17897
  const normalized = input.trim();
17468
17898
  const fragmentIndex = normalized.indexOf("#");
@@ -17470,11 +17900,35 @@ function normalizeDid(input) {
17470
17900
  }
17471
17901
  function didMatches(actual, expected) {
17472
17902
  if (!actual) return false;
17473
- try {
17474
- return normalizeDid(actual) === normalizeDid(expected);
17475
- } catch {
17476
- return actual === expected;
17903
+ return normalizeDid(actual) === normalizeDid(expected);
17904
+ }
17905
+ function ownerDidFromSpace(spaceId) {
17906
+ const owner = /^tinycloud:((?:did:)?pkh:eip155:[1-9]\d*:0x[0-9a-fA-F]{40})(?::|\/|$)/.exec(spaceId ?? "")?.[1];
17907
+ if (!owner) return void 0;
17908
+ return owner.startsWith("did:") ? owner : `did:${owner}`;
17909
+ }
17910
+ async function resolveCidBoundTargetSpace(params) {
17911
+ if (!params.ownerDid) return void 0;
17912
+ const grants = await loadLocalGrantArtifacts(params.profileName);
17913
+ for (const grant of grants) {
17914
+ const delegation = grant.delegation;
17915
+ if (grant.delegationCid !== params.cid || delegation.cid !== params.cid) continue;
17916
+ try {
17917
+ const authorization = delegation.delegationHeader.Authorization.replace(/^Bearer /i, "");
17918
+ const signed = parseSignedCompactUcanAttenuation(authorization, params.cid);
17919
+ for (const resource of Object.keys(signed.payload.att)) {
17920
+ const space = /^(tinycloud:[^/]+)\/[^/]+\/.*$/.exec(resource)?.[1];
17921
+ if (!space) continue;
17922
+ const ownerMatch = /^tinycloud:((?:did:)?pkh:eip155:[1-9]\d*:0x[0-9a-fA-F]{40}):/.exec(space);
17923
+ const owner = ownerMatch?.[1];
17924
+ const ownerDid = owner?.startsWith("did:") ? owner : owner ? `did:${owner}` : void 0;
17925
+ if (ownerDid && normalizeDid(ownerDid) === normalizeDid(params.ownerDid)) return space;
17926
+ }
17927
+ } catch {
17928
+ continue;
17929
+ }
17477
17930
  }
17931
+ return void 0;
17478
17932
  }
17479
17933
  function registerDelegationCommand(program) {
17480
17934
  const delegation = program.command("delegation").description("Manage delegations");
@@ -17554,16 +18008,158 @@ function registerDelegationCommand(program) {
17554
18008
  handleError(error);
17555
18009
  }
17556
18010
  });
17557
- delegation.command("revoke <cid>").description("Revoke a delegation").action(async (cid, _options, cmd) => {
18011
+ delegation.command("revoke <cid>").description("Revoke a delegation").option("--yes", "Skip local-key TTY confirmation", false).action(async (cid, options, cmd) => {
17558
18012
  try {
17559
18013
  const globalOpts = cmd.optsWithGlobals();
17560
18014
  const ctx = await ProfileManager.resolveContext(globalOpts);
17561
18015
  const node = await ensureAuthenticated(ctx);
17562
- const result = await node.delegationManager.revoke(cid);
18016
+ const profile = await ProfileManager.getProfile(ctx.profile);
18017
+ const revokePermission = {
18018
+ service: "tinycloud.delegation",
18019
+ space: `urn:cid:${cid}`,
18020
+ path: "",
18021
+ actions: ["tinycloud.delegation/revoke"]
18022
+ };
18023
+ const rawAuthorityCovered = node.hasRuntimePermissions([revokePermission]);
18024
+ if (!rawAuthorityCovered) await requirePermissionConsent([revokePermission], options.yes);
18025
+ let targetFound = false;
18026
+ let targetSpaceSource;
18027
+ let queryDenied = false;
18028
+ let boundSpace;
18029
+ const accountSpaceId = node.accountSpaceId;
18030
+ if (!accountSpaceId) {
18031
+ throw new CLIError(
18032
+ "DELEGATION_QUERY_AUTHORITY_UNAVAILABLE",
18033
+ "Cannot determine the account space needed to query delegation records.",
18034
+ ExitCode.AUTH_REQUIRED
18035
+ );
18036
+ }
18037
+ let cursor;
18038
+ do {
18039
+ const query = await node.delegationManager.query({
18040
+ direction: "all",
18041
+ limit: 100,
18042
+ ...cursor === void 0 ? {} : { cursor }
18043
+ });
18044
+ if (!query.ok) {
18045
+ if (query.error.meta?.status === 403) {
18046
+ queryDenied = true;
18047
+ break;
18048
+ }
18049
+ throw cliErrorFromService(query.error);
18050
+ }
18051
+ targetFound = query.data.items.some((delegation2) => delegation2.cid === cid);
18052
+ cursor = targetFound ? void 0 : query.data.nextCursor;
18053
+ } while (cursor !== void 0);
18054
+ targetSpaceSource = "node";
18055
+ if (!targetFound) {
18056
+ const grantHistory = await readGrantHistory(ctx.profile);
18057
+ const recordedGrant = [...grantHistory].reverse().find((entry) => entry.delegationCid === cid);
18058
+ if (recordedGrant) {
18059
+ targetSpaceSource = "local-grant-history";
18060
+ } else if (queryDenied) {
18061
+ boundSpace = await resolveCidBoundTargetSpace({
18062
+ profileName: ctx.profile,
18063
+ ownerDid: profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId),
18064
+ cid
18065
+ });
18066
+ if (boundSpace) {
18067
+ targetSpaceSource = "local-signed-grant-artifact";
18068
+ } else {
18069
+ throw new CLIError(
18070
+ "TARGET_NOT_FOUND",
18071
+ `Delegation "${cid}" was not found by the node or local grant sources.`,
18072
+ ExitCode.NOT_FOUND
18073
+ );
18074
+ }
18075
+ } else {
18076
+ throw new CLIError(
18077
+ "TARGET_NOT_FOUND",
18078
+ `Delegation "${cid}" was not found by the node or local grant history.`,
18079
+ ExitCode.NOT_FOUND
18080
+ );
18081
+ }
18082
+ }
18083
+ let authorityScopeSource = "cid-resource";
18084
+ let authorityScopeReason = "The revoke authority is scoped to the exact delegation CID.";
18085
+ let fallbackSpaceId;
18086
+ let commandAuthorityCid;
18087
+ const authorizeFallbackSpace = async (spaceId) => {
18088
+ fallbackSpaceId = spaceId;
18089
+ authorityScopeSource = "local-signed-grant-artifact";
18090
+ authorityScopeReason = "Raw-CID revoke authority could not be issued; the fallback space came from an owner-signed local grant whose Authorization recomputes to this CID.";
18091
+ targetSpaceSource = "local-signed-grant-artifact";
18092
+ const acquired = await ensureDelegationAuthority({
18093
+ ctx,
18094
+ profile,
18095
+ node,
18096
+ requested: [{
18097
+ service: "tinycloud.delegation",
18098
+ space: spaceId === node.accountSpaceId ? "default" : spaceId,
18099
+ path: "",
18100
+ actions: ["tinycloud.delegation/revoke"]
18101
+ }],
18102
+ expiryOption: void 0,
18103
+ reason: `Revoke delegation ${cid} using its CID-bound owner-signed grant artifact`,
18104
+ yes: options.yes,
18105
+ consentAlreadyGiven: true,
18106
+ persist: false
18107
+ });
18108
+ commandAuthorityCid = acquired?.cid;
18109
+ };
18110
+ if (!rawAuthorityCovered) {
18111
+ try {
18112
+ const acquired = await ensureDelegationAuthority({
18113
+ ctx,
18114
+ profile,
18115
+ node,
18116
+ requested: [revokePermission],
18117
+ expiryOption: void 0,
18118
+ reason: `Revoke delegation ${cid}`,
18119
+ yes: options.yes,
18120
+ consentAlreadyGiven: true,
18121
+ persist: false
18122
+ });
18123
+ commandAuthorityCid = acquired?.cid;
18124
+ } catch (error) {
18125
+ if (error === null || typeof error !== "object" || !("code" in error) || error.code !== "RAW_RECAP_RESOURCE_UNSUPPORTED") {
18126
+ throw error;
18127
+ }
18128
+ const fallbackSpace = boundSpace ?? await resolveCidBoundTargetSpace({
18129
+ profileName: ctx.profile,
18130
+ ownerDid: profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId),
18131
+ cid
18132
+ });
18133
+ if (!fallbackSpace) {
18134
+ throw new CLIError(
18135
+ "RAW_RECAP_RESOURCE_UNSUPPORTED",
18136
+ `Cannot safely revoke "${cid}": raw CID ReCap resources are unavailable and no matching owner-signed local grant artifact binds its space to the CID.`,
18137
+ ExitCode.PERMISSION_DENIED
18138
+ );
18139
+ }
18140
+ await authorizeFallbackSpace(fallbackSpace);
18141
+ }
18142
+ }
18143
+ const result = fallbackSpaceId ? await node.delegationManager.revoke(cid, {
18144
+ targetSpaceId: fallbackSpaceId,
18145
+ ...commandAuthorityCid === void 0 ? {} : { authorityCid: commandAuthorityCid }
18146
+ }) : commandAuthorityCid === void 0 ? await node.revokeDelegation(cid) : await node.delegationManager.revoke(cid, { authorityCid: commandAuthorityCid });
17563
18147
  if (!result.ok) {
18148
+ if (result.error.meta?.status === 403 && result.error.message === "Failed to revoke delegation: 403 - Unauthorized Revoker") {
18149
+ const profileDid = profile.ownerDid ?? ownerDidFromSpace(node.accountSpaceId) ?? ownerDidFromSpace(profile.spaceId) ?? "unknown";
18150
+ throw new CLIError(
18151
+ "REVOKE_UNAUTHORIZED",
18152
+ "The node rejected this revocation: Unauthorized Revoker.",
18153
+ ExitCode.PERMISSION_DENIED,
18154
+ {
18155
+ status: 403,
18156
+ hint: `Use a different profile belonging to the delegation's grantor, its recipient, or the owner of a space it covers; the rejected profile was "${ctx.profile}" (DID ${profileDid}). Retry with \`tc --profile <profile-name> delegation revoke ${cid}\`.`
18157
+ }
18158
+ );
18159
+ }
17564
18160
  throw cliErrorFromService(result.error);
17565
18161
  }
17566
- outputJson({ cid, revoked: true });
18162
+ outputJson({ cid, revoked: true, targetSpaceSource, authorityScopeSource, authorityScopeReason });
17567
18163
  } catch (error) {
17568
18164
  handleError(error);
17569
18165
  }
@@ -29959,17 +30555,17 @@ function validRecipientDid(value) {
29959
30555
  if (value.length === 0 || value.length > 2048 || /[\u0000-\u0020\u007f]/.test(value)) return false;
29960
30556
  const parts = value.split(":");
29961
30557
  if (parts.length < 3 || parts[0] !== "did" || !/^[a-z0-9]+$/.test(parts[1] ?? "")) return false;
29962
- const identifier = parts.slice(2);
29963
- if (identifier.some((part) => part.length === 0)) return false;
30558
+ const identifier2 = parts.slice(2);
30559
+ if (identifier2.some((part) => part.length === 0)) return false;
29964
30560
  if (parts[1] === "web") {
29965
- const host = identifier[0] ?? "";
30561
+ const host = identifier2[0] ?? "";
29966
30562
  if (host.length > 253 || host.split(".").some((label) => !label || label.length > 63 || !/^[A-Za-z0-9-]+$/.test(label) || label.startsWith("-") || label.endsWith("-"))) return false;
29967
- return identifier.slice(1).every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
30563
+ return identifier2.slice(1).every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
29968
30564
  }
29969
- if (parts[1] === "pkh") return identifier.length >= 3 && identifier.every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
30565
+ if (parts[1] === "pkh") return identifier2.length >= 3 && identifier2.every((part) => /^[A-Za-z0-9._%-]+$/.test(part));
29970
30566
  if (parts[1] === "key") {
29971
30567
  try {
29972
- const bytes3 = base58btc2.decode(identifier.join(":"));
30568
+ const bytes3 = base58btc2.decode(identifier2.join(":"));
29973
30569
  return bytes3.length === 34 && bytes3[0] === 237 && bytes3[1] === 1;
29974
30570
  } catch {
29975
30571
  return false;