@tinycloud/sdk-services 3.1.0-beta.9 → 3.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -47,11 +47,13 @@ __export(src_exports, {
47
47
  KVListResultSchema: () => KVListResultSchema,
48
48
  KVResponseHeadersSchema: () => KVResponseHeadersSchema,
49
49
  KVService: () => KVService,
50
+ MAX_SECRET_SCOPE_LENGTH: () => MAX_SECRET_SCOPE_LENGTH,
50
51
  NETWORK_NAME_PATTERN: () => NETWORK_NAME_PATTERN,
51
52
  NetworkIdError: () => NetworkIdError,
52
53
  PrefixedKVService: () => PrefixedKVService,
53
54
  RetryPolicySchema: () => RetryPolicySchema,
54
55
  SECRET_NAME_RE: () => SECRET_NAME_RE,
56
+ SECRET_SCOPE_RE: () => SECRET_SCOPE_RE,
55
57
  SQLAction: () => SQLAction,
56
58
  SQLMigrations: () => SQLMigrations,
57
59
  SQLService: () => SQLService,
@@ -65,6 +67,7 @@ __export(src_exports, {
65
67
  ServiceResponseEventSchema: () => ServiceResponseEventSchema,
66
68
  ServiceRetryEventSchema: () => ServiceRetryEventSchema,
67
69
  ServiceSessionSchema: () => ServiceSessionSchema,
70
+ TC832_RESERVED_SCOPES: () => TC832_RESERVED_SCOPES,
68
71
  TelemetryEvents: () => TelemetryEvents,
69
72
  TelemetrySpanEventSchema: () => TelemetrySpanEventSchema,
70
73
  TinyCloudDebugLogger: () => TinyCloudDebugLogger,
@@ -72,6 +75,7 @@ __export(src_exports, {
72
75
  VaultHeaders: () => VaultHeaders,
73
76
  VaultPublicSpaceKVActions: () => VaultPublicSpaceKVActions,
74
77
  abortedError: () => abortedError,
78
+ appSecretScope: () => appSecretScope,
75
79
  authExpiredError: () => authExpiredError,
76
80
  authRequiredError: () => authRequiredError,
77
81
  authUnauthorizedError: () => authUnauthorizedError,
@@ -1863,6 +1867,24 @@ var MAX_KV_BATCH_READ_ITEMS = 100;
1863
1867
  function isJsonObject(value) {
1864
1868
  return typeof value === "object" && value !== null && !Array.isArray(value);
1865
1869
  }
1870
+ var pathByteEncoder = new TextEncoder();
1871
+ function comparePathsByBytes(a, b) {
1872
+ const left = pathByteEncoder.encode(a);
1873
+ const right = pathByteEncoder.encode(b);
1874
+ const length = Math.min(left.length, right.length);
1875
+ for (let index = 0; index < length; index++) {
1876
+ if (left[index] !== right[index]) {
1877
+ return left[index] - right[index];
1878
+ }
1879
+ }
1880
+ return left.length - right.length;
1881
+ }
1882
+ var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
1883
+ function revocationCodeOf(body) {
1884
+ const match = REVOKED_BODY.exec(body.trim());
1885
+ if (match === null) return void 0;
1886
+ return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
1887
+ }
1866
1888
  function encodeKvBatchPartName(path) {
1867
1889
  return encodeURIComponent(path).replace(
1868
1890
  /[!'()*]/g,
@@ -2090,15 +2112,25 @@ var KVService = class extends BaseService {
2090
2112
  )?.[1];
2091
2113
  return contentType?.includes("application/json") ? JSON.parse(text) : text;
2092
2114
  }
2115
+ /**
2116
+ * Validate a batch read response and return its items in the caller's
2117
+ * request order. The node flattens invocation abilities from a `BTreeMap`,
2118
+ * so results arrive sorted by UTF-8 byte order of the path, not in request
2119
+ * order. Items that carry their path in `key` are matched by path; if no
2120
+ * item carries a key, items are assumed to be in the node's byte-sorted
2121
+ * order and the request is aligned the same way. Any mix of keyed and
2122
+ * keyless items, a missing requested path, an unrequested path, or a
2123
+ * malformed item fails closed (returns undefined).
2124
+ */
2093
2125
  normalizeBatchReadResponse(data, paths, requireData) {
2094
2126
  if (!data || typeof data !== "object") return void 0;
2095
2127
  const response = data;
2096
- if (!Array.isArray(response.results) || response.results.length !== paths.length) {
2128
+ const items = response.results;
2129
+ if (!Array.isArray(items) || items.length !== paths.length) {
2097
2130
  return void 0;
2098
2131
  }
2099
- for (let index = 0; index < response.results.length; index++) {
2100
- const item = response.results[index];
2101
- if (!item || typeof item !== "object" || item.key !== paths[index] || typeof item.ok !== "boolean") {
2132
+ for (const item of items) {
2133
+ if (!item || typeof item !== "object" || typeof item.ok !== "boolean") {
2102
2134
  return void 0;
2103
2135
  }
2104
2136
  if (item.ok && (!item.headers || typeof item.headers !== "object" || Object.values(item.headers).some((value) => typeof value !== "string") || requireData && typeof item.dataBase64 !== "string")) {
@@ -2108,7 +2140,24 @@ var KVService = class extends BaseService {
2108
2140
  return void 0;
2109
2141
  }
2110
2142
  }
2111
- return response;
2143
+ if (items.every((item) => typeof item.key === "string")) {
2144
+ const byPath = new Map(items.map((item) => [item.key, item]));
2145
+ if (byPath.size !== items.length) return void 0;
2146
+ const aligned2 = [];
2147
+ for (const path of paths) {
2148
+ const item = byPath.get(path);
2149
+ if (!item) return void 0;
2150
+ aligned2.push(item);
2151
+ }
2152
+ return aligned2;
2153
+ }
2154
+ if (items.some((item) => item.key !== void 0)) return void 0;
2155
+ const requestOrder = paths.map((_, index) => index).sort((a, b) => comparePathsByBytes(paths[a], paths[b]));
2156
+ const aligned = new Array(items.length);
2157
+ for (let position = 0; position < requestOrder.length; position++) {
2158
+ aligned[requestOrder[position]] = items[position];
2159
+ }
2160
+ return aligned;
2112
2161
  }
2113
2162
  async batchRead(keys, action, options) {
2114
2163
  if (!this.requireAuth()) return err(authRequiredError("kv"));
@@ -2201,19 +2250,19 @@ var KVService = class extends BaseService {
2201
2250
  { meta: { status: response.status, statusText: response.statusText } }
2202
2251
  ));
2203
2252
  }
2204
- const payload = this.normalizeBatchReadResponse(
2253
+ const items = this.normalizeBatchReadResponse(
2205
2254
  await response.json(),
2206
2255
  paths,
2207
2256
  action === KVAction.GET
2208
2257
  );
2209
- if (!payload) {
2258
+ if (!items) {
2210
2259
  return err(serviceError(
2211
2260
  ErrorCodes.NETWORK_ERROR,
2212
2261
  "KV batch read response did not match the requested keys",
2213
2262
  "kv"
2214
2263
  ));
2215
2264
  }
2216
- const results = payload.results.map((item, index) => {
2265
+ const results = items.map((item, index) => {
2217
2266
  if (!item.ok) {
2218
2267
  return {
2219
2268
  key: keys[index],
@@ -2404,7 +2453,7 @@ var KVService = class extends BaseService {
2404
2453
  );
2405
2454
  }
2406
2455
  if (response.status === 404) {
2407
- return this.classifyNotFound(response, key);
2456
+ return await this.classifyNotFound(response, key);
2408
2457
  }
2409
2458
  const errorText = await response.text();
2410
2459
  if (response.status === 413) {
@@ -2847,8 +2896,8 @@ var KVService = class extends BaseService {
2847
2896
  }
2848
2897
  /**
2849
2898
  * Map a failed `kv/sync` response. 410 and the retention 403 carry
2850
- * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose text names
2851
- * `delegation-revoked` or `delegation-ancestor-revoked`. A body read that
2899
+ * `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
2900
+ * is the node's revocation error (see REVOKED_BODY). A body read that
2852
2901
  * fails because the request was cancelled or timed out rethrows, so the
2853
2902
  * caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
2854
2903
  */
@@ -2893,7 +2942,7 @@ var KVService = class extends BaseService {
2893
2942
  ));
2894
2943
  }
2895
2944
  if (response.status === 401) {
2896
- const revoked = /\bdelegation-ancestor-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED : /\bdelegation-revoked\b/.test(errorText) ? ErrorCodes.AUTH_DELEGATION_REVOKED : void 0;
2945
+ const revoked = revocationCodeOf(errorText);
2897
2946
  if (revoked !== void 0) {
2898
2947
  return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
2899
2948
  }
@@ -2973,7 +3022,7 @@ var KVService = class extends BaseService {
2973
3022
  );
2974
3023
  }
2975
3024
  if (response.status === 404) {
2976
- return this.classifyNotFound(response, key);
3025
+ return await this.classifyNotFound(response, key);
2977
3026
  }
2978
3027
  const errorText = await response.text();
2979
3028
  if (response.status === 412) {
@@ -3040,7 +3089,7 @@ var KVService = class extends BaseService {
3040
3089
  );
3041
3090
  }
3042
3091
  if (response.status === 404) {
3043
- return this.classifyNotFound(response, key);
3092
+ return await this.classifyNotFound(response, key);
3044
3093
  }
3045
3094
  const errorText = await response.text();
3046
3095
  return err(
@@ -3106,7 +3155,7 @@ var KVService = class extends BaseService {
3106
3155
  signal: request.signal
3107
3156
  });
3108
3157
  if (!response.ok) {
3109
- return this.createSignedReadUrlError(response, key, path);
3158
+ return await this.createSignedReadUrlError(response, key, path);
3110
3159
  }
3111
3160
  const signedUrl = this.normalizeSignedReadUrlResponse(
3112
3161
  await response.json()
@@ -4140,7 +4189,7 @@ var AsyncQueue = class {
4140
4189
  }
4141
4190
  const waiter = this.waiters.shift();
4142
4191
  if (waiter) {
4143
- waiter({ value, done: false });
4192
+ waiter.resolve({ value, done: false });
4144
4193
  return;
4145
4194
  }
4146
4195
  this.values.push(value);
@@ -4152,7 +4201,22 @@ var AsyncQueue = class {
4152
4201
  this.closed = true;
4153
4202
  while (this.waiters.length > 0) {
4154
4203
  const waiter = this.waiters.shift();
4155
- waiter?.({ value: void 0, done: true });
4204
+ waiter?.resolve({ value: void 0, done: true });
4205
+ }
4206
+ }
4207
+ /**
4208
+ * Fail the queue: pending and future `next()` calls reject with `error`,
4209
+ * so a `for await` consumer sees the iterator throw.
4210
+ */
4211
+ fail(error) {
4212
+ if (this.closed) {
4213
+ return;
4214
+ }
4215
+ this.closed = true;
4216
+ this.failure = error;
4217
+ while (this.waiters.length > 0) {
4218
+ const waiter = this.waiters.shift();
4219
+ waiter?.reject(error);
4156
4220
  }
4157
4221
  }
4158
4222
  next() {
@@ -4160,20 +4224,28 @@ var AsyncQueue = class {
4160
4224
  const value = this.values.shift();
4161
4225
  return Promise.resolve({ value, done: false });
4162
4226
  }
4227
+ if (this.failure !== void 0) {
4228
+ return Promise.reject(this.failure);
4229
+ }
4163
4230
  if (this.closed) {
4164
4231
  return Promise.resolve({ value: void 0, done: true });
4165
4232
  }
4166
- return new Promise((resolve) => {
4167
- this.waiters.push(resolve);
4233
+ return new Promise((resolve, reject) => {
4234
+ this.waiters.push({ resolve, reject });
4168
4235
  });
4169
4236
  }
4170
4237
  };
4238
+ var HOOK_STREAM_RETRY_BASE_DELAY_MS = 250;
4239
+ var HOOK_STREAM_RETRY_MAX_DELAY_MS = 3e4;
4240
+ var HOOK_STREAM_HEALTHY_MS = 5e3;
4171
4241
  var HooksService = class extends BaseService {
4172
4242
  constructor(config = {}) {
4173
4243
  super();
4174
4244
  this._subscribers = /* @__PURE__ */ new Set();
4175
- this._refreshChain = Promise.resolve();
4176
- this._activeSignature = "";
4245
+ /** True while the one supervisor loop runs. Set only by wake(), cleared only at loop exit. */
4246
+ this._supervising = false;
4247
+ /** Bumped on every session change; part of the plan key. */
4248
+ this._sessionGeneration = 0;
4177
4249
  this._config = config;
4178
4250
  }
4179
4251
  get config() {
@@ -4189,35 +4261,32 @@ var HooksService = class extends BaseService {
4189
4261
  if (subscriptions.length === 0) {
4190
4262
  throw new Error("At least one hook subscription is required");
4191
4263
  }
4192
- const normalized = subscriptions.map(normalizeSubscription);
4264
+ const requested = subscriptions.map(normalizeSubscription);
4193
4265
  const subscriber = {
4194
- requested: normalized,
4266
+ requested,
4267
+ signatures: requested.map(subscriptionSignature),
4195
4268
  ttlSeconds: options.ttlSeconds,
4196
4269
  queue: new AsyncQueue()
4197
4270
  };
4198
- this._subscribers.add(subscriber);
4199
- const abortHandler = () => {
4271
+ const leave = () => {
4200
4272
  this._subscribers.delete(subscriber);
4201
4273
  subscriber.queue.close();
4202
- void this.scheduleSharedStreamRefresh();
4274
+ this.wake();
4203
4275
  };
4204
- if (options.signal) {
4205
- if (options.signal.aborted) {
4206
- abortHandler();
4207
- } else {
4208
- options.signal.addEventListener("abort", abortHandler, { once: true });
4209
- }
4276
+ const { signal } = options;
4277
+ if (signal?.aborted) {
4278
+ return;
4210
4279
  }
4211
- void this.scheduleSharedStreamRefresh();
4280
+ signal?.addEventListener("abort", leave, { once: true });
4281
+ this._subscribers.add(subscriber);
4282
+ this.wake();
4212
4283
  try {
4213
4284
  for await (const event of subscriber.queue) {
4214
4285
  yield event;
4215
4286
  }
4216
4287
  } finally {
4217
- if (options.signal) {
4218
- options.signal.removeEventListener("abort", abortHandler);
4219
- }
4220
- abortHandler();
4288
+ signal?.removeEventListener("abort", leave);
4289
+ leave();
4221
4290
  }
4222
4291
  }
4223
4292
  async register(webhook) {
@@ -4358,146 +4427,292 @@ var HooksService = class extends BaseService {
4358
4427
  return err(wrapError("hooks", error));
4359
4428
  }
4360
4429
  }
4361
- async scheduleSharedStreamRefresh() {
4362
- this._refreshChain = this._refreshChain.then(() => this.refreshSharedStream()).catch(() => void 0);
4363
- await this._refreshChain;
4364
- }
4365
- async refreshSharedStream() {
4366
- if (!this.requireAuth() || this._subscribers.size === 0) {
4367
- this.abortSharedStream();
4368
- this._activeSignature = "";
4369
- return;
4370
- }
4371
- const state = this.collectSharedStreamState();
4372
- if (state.signature !== this._activeSignature) {
4373
- this._activeSignature = state.signature;
4374
- this.abortSharedStream();
4430
+ /**
4431
+ * Called after anything that may change what the supervisor should do:
4432
+ * subscribe, unsubscribe, session change, sign-out. Interrupts the running
4433
+ * step if the plan moved away from it, and starts the supervisor if none
4434
+ * runs. Touches only SDK-owned state, so it never throws into callers such
4435
+ * as `ServiceContext.setSession`.
4436
+ */
4437
+ wake() {
4438
+ const phase = this._phase;
4439
+ if (phase && phase.key !== this.plan()?.key) {
4440
+ phase.interrupt.abort();
4375
4441
  }
4376
- if (!this._sharedStreamTask) {
4377
- this._sharedStreamTask = this.runSharedStream(state).catch((error) => {
4378
- if (!isAbortError(error)) {
4379
- throw error;
4380
- }
4381
- }).finally(() => {
4382
- this._sharedStreamTask = void 0;
4383
- this._sharedStreamAbort = void 0;
4384
- if (this._subscribers.size > 0) {
4385
- void this.scheduleSharedStreamRefresh();
4386
- }
4387
- });
4442
+ if (this._supervising || this._subscribers.size === 0) {
4443
+ return;
4388
4444
  }
4445
+ this._supervising = true;
4446
+ this.supervise().catch(() => this.collapse());
4389
4447
  }
4390
- collectSharedStreamState() {
4391
- const merged = /* @__PURE__ */ new Map();
4392
- const ttlCandidates = [];
4393
- for (const subscriber of this._subscribers) {
4394
- if (typeof subscriber.ttlSeconds === "number") {
4395
- ttlCandidates.push(subscriber.ttlSeconds);
4448
+ /**
4449
+ * The only owner of the shared stream. Each iteration is one attempt
4450
+ * (mint → open → drain), then a classification and, unless the attempt was
4451
+ * superseded or released its subscribers, a backoff. Every await is a
4452
+ * `phase` that wake() can interrupt; an interrupted phase yields
4453
+ * `undefined`, so a superseded mint can never act. Outside `attempt` and
4454
+ * `pause`, which each own one catch, the loop touches only SDK state.
4455
+ */
4456
+ async supervise() {
4457
+ let streak = 0;
4458
+ for (; ; ) {
4459
+ const plan = this.plan();
4460
+ if (!plan) {
4461
+ this._supervising = false;
4462
+ return;
4463
+ }
4464
+ const health = { openedAt: 0, delivered: false };
4465
+ const outcome = await this.phase(
4466
+ plan.key,
4467
+ (signal) => this.attempt(plan, signal, health)
4468
+ );
4469
+ if (health.delivered || health.openedAt > 0 && performance.now() - health.openedAt >= HOOK_STREAM_HEALTHY_MS) {
4470
+ streak = 0;
4471
+ }
4472
+ if (!outcome) {
4473
+ continue;
4474
+ }
4475
+ if (this.plan()?.key !== plan.key) {
4476
+ continue;
4477
+ }
4478
+ if (outcome.kind === "stopped") {
4479
+ this.release();
4480
+ continue;
4481
+ }
4482
+ if (outcome.kind === "refused" && outcome.terminal) {
4483
+ this.release(outcome.error);
4484
+ this.report(outcome.error);
4485
+ continue;
4396
4486
  }
4397
- for (const subscription of subscriber.requested) {
4398
- merged.set(subscriptionSignature(subscription), subscription);
4487
+ if (outcome.kind !== "ended") {
4488
+ this.report(outcome.error);
4399
4489
  }
4490
+ streak += 1;
4491
+ await this.phase(
4492
+ plan.key,
4493
+ (signal) => this.pause(streak, signal)
4494
+ );
4400
4495
  }
4401
- const subscriptions = [...merged.values()].sort(
4402
- (left, right) => subscriptionSignature(left).localeCompare(subscriptionSignature(right))
4403
- );
4404
- const ttlSeconds = ttlCandidates.length > 0 ? Math.min(...ttlCandidates) : void 0;
4405
- const signature = JSON.stringify({
4406
- subscriptions: subscriptions.map(subscriptionSignature),
4407
- ttlSeconds
4408
- });
4409
- return {
4410
- subscriptions,
4411
- ttlSeconds,
4412
- signature
4413
- };
4414
4496
  }
4415
- async runSharedStream(state) {
4416
- const abortController = new AbortController();
4417
- this._sharedStreamAbort = abortController;
4497
+ /**
4498
+ * Run one step under an interrupt that wake() fires when the plan key moves
4499
+ * on. An interrupted step's result is discarded. The interrupt is also
4500
+ * fired when the step returns, which releases whatever it left open: the
4501
+ * request signal and its listeners, an unread response body, a timer.
4502
+ */
4503
+ async phase(key, step) {
4504
+ const interrupt = new AbortController();
4505
+ this._phase = { key, interrupt };
4506
+ try {
4507
+ if (this.plan()?.key !== key) {
4508
+ interrupt.abort();
4509
+ }
4510
+ const result = await step(interrupt.signal);
4511
+ return interrupt.signal.aborted ? void 0 : result;
4512
+ } finally {
4513
+ this._phase = void 0;
4514
+ interrupt.abort();
4515
+ }
4516
+ }
4517
+ /**
4518
+ * One mint → open → drain. The single catch site for everything the
4519
+ * attempt touches that the SDK does not own: context accessors, invoke,
4520
+ * fetch, response bodies, the SSE parser. Never rejects.
4521
+ */
4522
+ async attempt(plan, signal, health) {
4523
+ let phase = "mint";
4524
+ let request;
4525
+ const owns = () => !signal.aborted && !request?.signal.aborted && this.plan()?.key === plan.key;
4418
4526
  try {
4419
- const host = this._config.host ?? this.context.hosts[0];
4420
- const ticketResponse = await this.mintHookTicket(
4421
- state.subscriptions,
4422
- state.ttlSeconds,
4423
- abortController.signal
4527
+ const context = this.context;
4528
+ const contextSignal = context.abortSignal;
4529
+ this._lastContextSignal = contextSignal;
4530
+ if (!context.isAuthenticated || contextSignal?.aborted) {
4531
+ return { kind: "stopped" };
4532
+ }
4533
+ request = this.createRequestSignal(signal, 0);
4534
+ const host = this.host;
4535
+ const minted = await awaitOwned(
4536
+ request.signal,
4537
+ () => context.fetch(`${host}/hooks/tickets`, {
4538
+ method: "POST",
4539
+ headers: {
4540
+ ...serviceHeadersToRecord(this.createInvokeHeaders(plan.subscriptions)),
4541
+ "content-type": "application/json"
4542
+ },
4543
+ body: JSON.stringify({
4544
+ subscriptions: plan.subscriptions,
4545
+ ttlSeconds: plan.ttlSeconds
4546
+ }),
4547
+ signal: request.signal
4548
+ })
4424
4549
  );
4425
- const streamResponse = await this.openHookStream(
4426
- host,
4427
- ticketResponse.ticket,
4428
- abortController.signal
4550
+ if (!owns()) return { kind: "stopped" };
4551
+ if (!minted.ok) {
4552
+ const status = minted.status;
4553
+ const error = await awaitOwned(
4554
+ request.signal,
4555
+ () => responseError("hooks", "failed to mint hook ticket", minted)
4556
+ );
4557
+ if (!owns()) return { kind: "stopped" };
4558
+ return { kind: "refused", terminal: isTerminalMintRefusal(status), error };
4559
+ }
4560
+ const body = await awaitOwned(request.signal, () => minted.json());
4561
+ if (!owns()) return { kind: "stopped" };
4562
+ const ticket = body !== null && typeof body === "object" && "ticket" in body ? body.ticket : void 0;
4563
+ if (typeof ticket !== "string" || ticket.length === 0) {
4564
+ throw new Error("Hook ticket response did not include a ticket");
4565
+ }
4566
+ phase = "open";
4567
+ if (!owns()) return { kind: "stopped" };
4568
+ const stream = await awaitOwned(
4569
+ request.signal,
4570
+ () => context.fetch(`${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`, {
4571
+ method: "GET",
4572
+ headers: { accept: "text/event-stream" },
4573
+ signal: request.signal
4574
+ })
4429
4575
  );
4430
- for await (const message of parseSseStream(
4431
- streamResponse.body,
4432
- abortController.signal
4433
- )) {
4434
- if (!message.data) {
4435
- continue;
4436
- }
4437
- const event = parseHookEvent(message);
4438
- for (const subscriber of this._subscribers) {
4439
- if (matchesAnySubscription(event, subscriber.requested)) {
4440
- subscriber.queue.push(event);
4441
- }
4576
+ if (!owns()) return { kind: "stopped" };
4577
+ if (!stream.ok) {
4578
+ return { kind: "refused", terminal: false, error: hookStreamRefusal(stream.status) };
4579
+ }
4580
+ health.openedAt = performance.now();
4581
+ const iterator = parseSseStream(stream.body, request.signal)[Symbol.asyncIterator]();
4582
+ try {
4583
+ for (; ; ) {
4584
+ const next = await awaitOwned(request.signal, () => iterator.next());
4585
+ if (!owns()) return { kind: "stopped" };
4586
+ if (next.done) break;
4587
+ const message = next.value;
4588
+ if (!message.data) continue;
4589
+ const event = parseHookEvent(message);
4590
+ if (!owns()) return { kind: "stopped" };
4591
+ this.dispatch(event);
4592
+ health.delivered = true;
4442
4593
  }
4594
+ } finally {
4595
+ const closing = iterator.return?.();
4596
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
4443
4597
  }
4598
+ return owns() ? { kind: "ended" } : { kind: "stopped" };
4599
+ } catch (thrown) {
4600
+ return request?.signal.aborted || thrown === SUPERSEDED ? { kind: "stopped" } : { kind: "failed", error: hookStreamFailure(phase, thrown) };
4601
+ }
4602
+ }
4603
+ /**
4604
+ * Back off before the next attempt. `config.streamRetry` overrides the
4605
+ * delay and the clock; whatever they do, the pause ends by the delay or the
4606
+ * interrupt, then yields one timer turn so an instant clock cannot starve
4607
+ * due timers or the process.
4608
+ */
4609
+ async pause(attempt, signal) {
4610
+ try {
4611
+ this._lastContextSignal = this.context.abortSignal;
4612
+ } catch {
4613
+ }
4614
+ const contextSignal = this._lastContextSignal;
4615
+ const waits = [];
4616
+ const aborts = [];
4617
+ try {
4618
+ const retry = this._config.streamRetry;
4619
+ const delayMs = retry?.delay?.(attempt) ?? hookStreamRetryDelay(attempt);
4620
+ waits.push((retry?.wait ?? sleep)(delayMs, attempt, signal));
4621
+ } catch {
4622
+ waits.push(Promise.resolve());
4623
+ }
4624
+ aborts.push(abortPromise(signal));
4625
+ if (contextSignal) aborts.push(abortPromise(contextSignal));
4626
+ try {
4627
+ await Promise.race([...waits, ...aborts.map(({ promise }) => promise)]);
4628
+ } catch {
4444
4629
  } finally {
4445
- if (this._sharedStreamAbort === abortController) {
4446
- this._sharedStreamAbort = void 0;
4630
+ for (const abort of aborts) abort.dispose();
4631
+ }
4632
+ await new Promise((resolve) => setTimeout(resolve, 0));
4633
+ }
4634
+ /** The stream the current subscribers need, or undefined when none remain. */
4635
+ plan() {
4636
+ if (this._subscribers.size === 0) {
4637
+ return void 0;
4638
+ }
4639
+ const merged = /* @__PURE__ */ new Map();
4640
+ let ttlSeconds;
4641
+ for (const subscriber of this._subscribers) {
4642
+ subscriber.requested.forEach(
4643
+ (subscription, index) => merged.set(subscriber.signatures[index], subscription)
4644
+ );
4645
+ if (typeof subscriber.ttlSeconds === "number") {
4646
+ ttlSeconds = Math.min(subscriber.ttlSeconds, ttlSeconds ?? Infinity);
4447
4647
  }
4448
4648
  }
4649
+ const signatures = [...merged.keys()].sort();
4650
+ return {
4651
+ key: JSON.stringify([this._sessionGeneration, signatures, ttlSeconds ?? null]),
4652
+ subscriptions: signatures.map((signature) => merged.get(signature)),
4653
+ ttlSeconds
4654
+ };
4449
4655
  }
4450
- abortSharedStream() {
4451
- this._sharedStreamAbort?.abort();
4656
+ dispatch(event) {
4657
+ for (const subscriber of this._subscribers) {
4658
+ if (matchesAnySubscription(event, subscriber.requested)) {
4659
+ subscriber.queue.push(event);
4660
+ }
4661
+ }
4452
4662
  }
4453
- createHookHeaders(action, path) {
4454
- return this.context.invoke(this.session, "hooks", path, action);
4663
+ /**
4664
+ * Detach every subscriber. With `error`, their iterators throw it after
4665
+ * draining buffered events; without, they complete.
4666
+ */
4667
+ release(error) {
4668
+ const subscribers = [...this._subscribers];
4669
+ this._subscribers.clear();
4670
+ for (const { queue } of subscribers) {
4671
+ if (error) {
4672
+ queue.fail(error);
4673
+ } else {
4674
+ queue.close();
4675
+ }
4676
+ }
4455
4677
  }
4456
- async mintHookTicket(subscriptions, ttlSeconds, signal) {
4457
- const host = this._config.host ?? this.context.hosts[0];
4458
- const headers = this.createInvokeHeaders(subscriptions);
4459
- const ticketResponse = await this.context.fetch(`${host}/hooks/tickets`, {
4460
- method: "POST",
4461
- headers: {
4462
- ...serviceHeadersToRecord(headers),
4463
- "content-type": "application/json"
4464
- },
4465
- body: JSON.stringify({
4466
- subscriptions,
4467
- ttlSeconds
4468
- }),
4469
- signal
4470
- });
4471
- if (!ticketResponse.ok) {
4472
- throw await responseError(
4473
- "hooks",
4474
- "failed to mint hook ticket",
4475
- ticketResponse
4476
- );
4678
+ report(error) {
4679
+ if (error.meta && typeof error.meta === "object") {
4680
+ Object.freeze(error.meta);
4477
4681
  }
4478
- const ticketJson = await ticketResponse.json();
4479
- if (!ticketJson?.ticket) {
4480
- throw new Error("Hook ticket response did not include a ticket");
4682
+ Object.freeze(error);
4683
+ try {
4684
+ this.emitError(error, "stream");
4685
+ } catch {
4481
4686
  }
4482
- return ticketJson;
4483
4687
  }
4484
- async openHookStream(host, ticket, signal) {
4485
- const streamResponse = await this.context.fetch(
4486
- `${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`,
4487
- {
4488
- method: "GET",
4489
- headers: { accept: "text/event-stream" },
4490
- signal
4491
- }
4688
+ /**
4689
+ * Unreachable by construction. If the loop ever rejects anyway, fail its
4690
+ * subscribers visibly instead of stranding them or crashing the process.
4691
+ */
4692
+ collapse() {
4693
+ this._supervising = false;
4694
+ this._phase?.interrupt.abort();
4695
+ this._phase = void 0;
4696
+ const error = serviceError(
4697
+ ErrorCodes.NETWORK_ERROR,
4698
+ "hook stream supervisor stopped unexpectedly",
4699
+ "hooks"
4492
4700
  );
4493
- if (!streamResponse.ok) {
4494
- throw await responseError(
4495
- "hooks",
4496
- "failed to open hook stream",
4497
- streamResponse
4498
- );
4499
- }
4500
- return streamResponse;
4701
+ this.report(error);
4702
+ this.release(error);
4703
+ }
4704
+ onSessionChange(session) {
4705
+ super.onSessionChange(session);
4706
+ this._sessionGeneration += 1;
4707
+ this.wake();
4708
+ }
4709
+ onSignOut() {
4710
+ super.onSignOut();
4711
+ this.release();
4712
+ this.wake();
4713
+ }
4714
+ createHookHeaders(action, path) {
4715
+ return this.context.invoke(this.session, "hooks", path, action);
4501
4716
  }
4502
4717
  createInvokeHeaders(subscriptions) {
4503
4718
  const entries = subscriptions.map((subscription) => ({
@@ -4528,11 +4743,168 @@ function buildScopePath(service, pathPrefix) {
4528
4743
  const normalized = normalizePathPrefix(pathPrefix);
4529
4744
  return normalized ? `${service}/${normalized}` : service;
4530
4745
  }
4746
+ function hookStreamRetryDelay(attempt) {
4747
+ const cap = Math.min(
4748
+ HOOK_STREAM_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1),
4749
+ HOOK_STREAM_RETRY_MAX_DELAY_MS
4750
+ );
4751
+ return cap / 2 + Math.random() * (cap / 2);
4752
+ }
4753
+ function isTerminalMintRefusal(status) {
4754
+ return typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
4755
+ }
4756
+ var SUPERSEDED = /* @__PURE__ */ Symbol("hooks attempt superseded");
4757
+ async function awaitOwned(signal, operation) {
4758
+ if (signal.aborted) {
4759
+ if (typeof operation !== "function") {
4760
+ void operation.then(cancelLateResponse, () => void 0);
4761
+ }
4762
+ throw SUPERSEDED;
4763
+ }
4764
+ const promise = typeof operation === "function" ? operation() : operation;
4765
+ return new Promise((resolve, reject) => {
4766
+ let settled = false;
4767
+ const cleanup = () => signal.removeEventListener("abort", onAbort);
4768
+ const onAbort = () => {
4769
+ if (settled) return;
4770
+ settled = true;
4771
+ cleanup();
4772
+ reject(SUPERSEDED);
4773
+ };
4774
+ signal.addEventListener("abort", onAbort, { once: true });
4775
+ promise.then(
4776
+ (value) => {
4777
+ if (settled) {
4778
+ cancelLateResponse(value);
4779
+ return;
4780
+ }
4781
+ settled = true;
4782
+ cleanup();
4783
+ resolve(value);
4784
+ },
4785
+ (error) => {
4786
+ if (settled) return;
4787
+ settled = true;
4788
+ cleanup();
4789
+ reject(error);
4790
+ }
4791
+ );
4792
+ });
4793
+ }
4794
+ function cancelLateResponse(value) {
4795
+ if (!(value instanceof Response) || !value.body) return;
4796
+ try {
4797
+ const cancellation = value.body.cancel();
4798
+ void cancellation.then(() => void 0, () => void 0);
4799
+ } catch {
4800
+ }
4801
+ }
4802
+ function hookStreamRefusal(status) {
4803
+ const code = typeof status === "number" ? status : void 0;
4804
+ return serviceError(
4805
+ ErrorCodes.NETWORK_ERROR,
4806
+ `hook stream open refused: ${code ?? "unknown status"}`,
4807
+ "hooks",
4808
+ { meta: { phase: "open", status: code } }
4809
+ );
4810
+ }
4811
+ var STREAM_ERROR_NAMES = /* @__PURE__ */ new Set([
4812
+ "Error",
4813
+ "TypeError",
4814
+ "AbortError",
4815
+ "TimeoutError",
4816
+ "SyntaxError",
4817
+ "RangeError",
4818
+ "NetworkError"
4819
+ ]);
4820
+ function hookStreamFailure(phase, thrown) {
4821
+ const candidateName = readProperty(thrown, "name");
4822
+ const name = typeof candidateName === "string" && STREAM_ERROR_NAMES.has(candidateName) ? candidateName : "Error";
4823
+ const code = identifier(readProperty(thrown, "code")) ?? identifier(readProperty(readProperty(thrown, "cause"), "code"));
4824
+ return serviceError(
4825
+ ErrorCodes.NETWORK_ERROR,
4826
+ `hook stream ${phase} failed: ${name}${code ? ` (${code})` : ""}`,
4827
+ "hooks",
4828
+ { meta: { phase, errorName: name, ...code ? { errorCode: code } : {} } }
4829
+ );
4830
+ }
4831
+ var STREAM_ERROR_CODES = /* @__PURE__ */ new Set([
4832
+ "ECONNREFUSED",
4833
+ "ECONNRESET",
4834
+ "ECONNABORTED",
4835
+ "ETIMEDOUT",
4836
+ "EPIPE",
4837
+ "ENOTFOUND",
4838
+ "EAI_AGAIN",
4839
+ "EHOSTUNREACH",
4840
+ "ENETUNREACH",
4841
+ "ENETDOWN",
4842
+ "EADDRNOTAVAIL",
4843
+ "UND_ERR_SOCKET",
4844
+ "UND_ERR_CONNECT_TIMEOUT",
4845
+ "UND_ERR_HEADERS_TIMEOUT",
4846
+ "UND_ERR_BODY_TIMEOUT",
4847
+ "UND_ERR_CLOSED",
4848
+ "UND_ERR_ABORTED",
4849
+ "UND_ERR_RESPONSE_STATUS_CODE",
4850
+ "ERR_TLS_CERT_ALTNAME_INVALID",
4851
+ "CERT_HAS_EXPIRED",
4852
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
4853
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
4854
+ "SELF_SIGNED_CERT_IN_CHAIN",
4855
+ "ERR_SSL_WRONG_VERSION_NUMBER"
4856
+ ]);
4857
+ function identifier(value) {
4858
+ return typeof value === "string" && STREAM_ERROR_CODES.has(value) ? value : void 0;
4859
+ }
4860
+ function readProperty(value, key) {
4861
+ if (value === null || typeof value !== "object" && typeof value !== "function") {
4862
+ return void 0;
4863
+ }
4864
+ try {
4865
+ return Reflect.get(value, key);
4866
+ } catch {
4867
+ return void 0;
4868
+ }
4869
+ }
4870
+ function sleep(delayMs, _attempt, signal) {
4871
+ return new Promise((resolve) => {
4872
+ const finish = () => {
4873
+ clearTimeout(timer);
4874
+ signal.removeEventListener("abort", stop);
4875
+ resolve();
4876
+ };
4877
+ const timer = setTimeout(finish, delayMs);
4878
+ timer.unref?.();
4879
+ const stop = () => finish();
4880
+ if (signal.aborted) {
4881
+ finish();
4882
+ } else {
4883
+ signal.addEventListener("abort", stop, { once: true });
4884
+ }
4885
+ });
4886
+ }
4887
+ function abortPromise(signal) {
4888
+ let resolve;
4889
+ const promise = new Promise((done) => {
4890
+ resolve = done;
4891
+ });
4892
+ const onAbort = () => resolve();
4893
+ if (signal.aborted) {
4894
+ resolve();
4895
+ } else {
4896
+ signal.addEventListener("abort", onAbort, { once: true });
4897
+ }
4898
+ return {
4899
+ promise,
4900
+ dispose: () => signal.removeEventListener("abort", onAbort)
4901
+ };
4902
+ }
4531
4903
  function normalizeSubscription(subscription) {
4532
4904
  return {
4533
4905
  ...subscription,
4534
4906
  pathPrefix: normalizePathPrefix(subscription.pathPrefix),
4535
- abilities: subscription.abilities ?? []
4907
+ abilities: subscription.abilities ? [...subscription.abilities] : []
4536
4908
  };
4537
4909
  }
4538
4910
  function subscriptionSignature(subscription) {
@@ -4598,9 +4970,6 @@ async function responseError(service, message, response) {
4598
4970
  meta: { ...error.meta, status: response.status, statusText: response.statusText }
4599
4971
  };
4600
4972
  }
4601
- function isAbortError(error) {
4602
- return error instanceof DOMException && error.name === "AbortError";
4603
- }
4604
4973
  async function* parseSseStream(body, signal) {
4605
4974
  if (!body) {
4606
4975
  throw new Error("Hook stream response does not expose a readable body");
@@ -4629,11 +4998,24 @@ async function* parseSseStream(body, signal) {
4629
4998
  async function* readBodyChunks(body, signal) {
4630
4999
  const asyncIterable = body;
4631
5000
  if (typeof asyncIterable?.[Symbol.asyncIterator] === "function") {
4632
- for await (const chunk of asyncIterable) {
4633
- if (signal?.aborted) {
4634
- break;
5001
+ const iterator = asyncIterable[Symbol.asyncIterator]();
5002
+ const cancel = () => {
5003
+ try {
5004
+ const closing = iterator.return?.();
5005
+ if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
5006
+ } catch {
5007
+ }
5008
+ };
5009
+ signal?.addEventListener("abort", cancel, { once: true });
5010
+ try {
5011
+ while (!signal?.aborted) {
5012
+ const next = await iterator.next();
5013
+ if (next.done) break;
5014
+ yield next.value;
4635
5015
  }
4636
- yield chunk;
5016
+ } finally {
5017
+ signal?.removeEventListener("abort", cancel);
5018
+ cancel();
4637
5019
  }
4638
5020
  return;
4639
5021
  }
@@ -4642,21 +5024,23 @@ async function* readBodyChunks(body, signal) {
4642
5024
  throw new Error("Unsupported hook stream body type");
4643
5025
  }
4644
5026
  const reader = stream.getReader();
5027
+ const cancelReader = () => {
5028
+ try {
5029
+ const cancellation = reader.cancel?.();
5030
+ if (cancellation) void cancellation.then(() => void 0, () => void 0);
5031
+ } catch {
5032
+ }
5033
+ };
5034
+ signal?.addEventListener("abort", cancelReader, { once: true });
4645
5035
  try {
4646
5036
  while (!signal?.aborted) {
4647
5037
  const { done, value } = await reader.read();
4648
- if (done) {
4649
- break;
4650
- }
4651
- if (value) {
4652
- yield value;
4653
- }
5038
+ if (done) break;
5039
+ if (value) yield value;
4654
5040
  }
4655
5041
  } finally {
4656
- try {
4657
- await reader.cancel?.();
4658
- } catch {
4659
- }
5042
+ signal?.removeEventListener("abort", cancelReader);
5043
+ cancelReader();
4660
5044
  reader.releaseLock?.();
4661
5045
  }
4662
5046
  }
@@ -5062,6 +5446,12 @@ function defaultVaultMessage(input) {
5062
5446
  return input.message ?? `Public key not found for ${input.did}`;
5063
5447
  case "STORAGE_ERROR":
5064
5448
  return input.message ?? input.cause.message;
5449
+ case "ALREADY_EXISTS":
5450
+ return input.message ?? `A value already exists at ${input.key}`;
5451
+ case "AUTH_UNAUTHORIZED":
5452
+ return input.message;
5453
+ case "PERMISSION_DENIED":
5454
+ return input.message;
5065
5455
  case "STORAGE_QUOTA_EXCEEDED":
5066
5456
  case "STORAGE_LIMIT_REACHED":
5067
5457
  return input.message;
@@ -5075,13 +5465,33 @@ function vaultError(input) {
5075
5465
  };
5076
5466
  return { ok: false, error };
5077
5467
  }
5468
+ function typedKvCause(error) {
5469
+ return Object.assign(new Error(error.message), {
5470
+ name: "ServiceError",
5471
+ code: error.code,
5472
+ service: error.service,
5473
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
5474
+ ...error.cause !== void 0 ? { cause: error.cause } : {}
5475
+ });
5476
+ }
5078
5477
  function kvWriteError(context, error) {
5079
5478
  if (error.code === ErrorCodes.STORAGE_QUOTA_EXCEEDED || error.code === ErrorCodes.STORAGE_LIMIT_REACHED) {
5080
5479
  return vaultError({ code: error.code, message: error.message, meta: error.meta });
5081
5480
  }
5481
+ const message = `${context}: ${error.message}`;
5482
+ if (error.code === ErrorCodes.AUTH_UNAUTHORIZED) {
5483
+ const code = authorizationVerdictOf(error) === "unauthenticated" ? "AUTH_UNAUTHORIZED" : "PERMISSION_DENIED";
5484
+ return vaultError({
5485
+ code,
5486
+ message,
5487
+ ...error.meta !== void 0 ? { meta: error.meta } : {},
5488
+ cause: typedKvCause(error)
5489
+ });
5490
+ }
5082
5491
  return vaultError({
5083
5492
  code: "STORAGE_ERROR",
5084
- cause: new Error(`${context}: ${error.message}`)
5493
+ cause: new Error(message, { cause: typedKvCause(error) }),
5494
+ ...error.meta !== void 0 ? { meta: error.meta } : {}
5085
5495
  });
5086
5496
  }
5087
5497
  var DataVaultService = class extends BaseService {
@@ -5361,11 +5771,18 @@ var DataVaultService = class extends BaseService {
5361
5771
  cause: new Error(envelopeResult.error.message)
5362
5772
  });
5363
5773
  }
5364
- const valuePutResult = await this.tc.kv.put(
5365
- `vault/${key}`,
5366
- JSON.stringify(envelopeResult.data)
5367
- );
5774
+ const valuePath = `vault/${key}`;
5775
+ const valueBody = JSON.stringify(envelopeResult.data);
5776
+ const valuePutResult = options?.ifNoneMatch !== void 0 ? await this.tc.kv.put(valuePath, valueBody, { ifNoneMatch: options.ifNoneMatch }) : await this.tc.kv.put(valuePath, valueBody);
5368
5777
  if (!valuePutResult.ok) {
5778
+ if (options?.ifNoneMatch !== void 0 && valuePutResult.error.code === ErrorCodes.KV_PRECONDITION_FAILED) {
5779
+ return vaultError({
5780
+ code: "ALREADY_EXISTS",
5781
+ key,
5782
+ ...valuePutResult.error.meta !== void 0 ? { meta: valuePutResult.error.meta } : {},
5783
+ cause: typedKvCause(valuePutResult.error)
5784
+ });
5785
+ }
5369
5786
  return kvWriteError("Failed to store encrypted value", valuePutResult.error);
5370
5787
  }
5371
5788
  return { ok: true, data: void 0 };
@@ -5533,6 +5950,14 @@ var DataVaultService = class extends BaseService {
5533
5950
  if (this.usesNetworkEncryption) {
5534
5951
  return this.putNetworkEncrypted(key, value, options);
5535
5952
  }
5953
+ if (options?.ifNoneMatch !== void 0) {
5954
+ return vaultError({
5955
+ code: "STORAGE_ERROR",
5956
+ cause: new Error(
5957
+ "ifNoneMatch is only supported by network-encrypted vaults"
5958
+ )
5959
+ });
5960
+ }
5536
5961
  if (!this._isUnlocked || !this.masterKey) {
5537
5962
  return vaultError({
5538
5963
  code: "VAULT_LOCKED",
@@ -6344,6 +6769,35 @@ var SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
6344
6769
  var SECRET_PREFIX = "secrets/";
6345
6770
  var SCOPED_SECRET_PREFIX = "secrets/scoped/";
6346
6771
  var RESERVED_SECRET_SCOPES = /* @__PURE__ */ new Set(["default", "global"]);
6772
+ var SECRET_SCOPE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
6773
+ var MAX_SECRET_SCOPE_LENGTH = 64;
6774
+ var TC832_RESERVED_SCOPES = Object.freeze([
6775
+ "default",
6776
+ "global",
6777
+ "scoped"
6778
+ ]);
6779
+ function appSecretScope(appId) {
6780
+ if (appId.trim() === "") {
6781
+ throw new Error("App id must be non-empty to derive a secret scope.");
6782
+ }
6783
+ const scope = appId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
6784
+ if (scope === "") {
6785
+ throw new Error(
6786
+ `App id ${JSON.stringify(appId)} must contain at least one letter or number.`
6787
+ );
6788
+ }
6789
+ if (scope.length > MAX_SECRET_SCOPE_LENGTH) {
6790
+ throw new Error(
6791
+ `App id ${JSON.stringify(appId)} yields a secret scope longer than ${MAX_SECRET_SCOPE_LENGTH} characters.`
6792
+ );
6793
+ }
6794
+ if (TC832_RESERVED_SCOPES.includes(scope)) {
6795
+ throw new Error(
6796
+ `App id ${JSON.stringify(appId)} yields the reserved secret scope ${JSON.stringify(scope)}.`
6797
+ );
6798
+ }
6799
+ return scope;
6800
+ }
6347
6801
  function canonicalizeSecretScope(scope) {
6348
6802
  if (scope === void 0) {
6349
6803
  return void 0;
@@ -6456,11 +6910,12 @@ var SecretsService = class {
6456
6910
  const secretPath = resolveSecretPathResult(name, options);
6457
6911
  if ("ok" in secretPath) return secretPath;
6458
6912
  const now = (/* @__PURE__ */ new Date()).toISOString();
6459
- return this.vault.put(secretPath.vaultKey, {
6913
+ const payload = {
6460
6914
  value,
6461
6915
  createdAt: now,
6462
6916
  updatedAt: now
6463
- });
6917
+ };
6918
+ return options?.ifAbsent === true ? this.vault.put(secretPath.vaultKey, payload, { ifNoneMatch: "*" }) : this.vault.put(secretPath.vaultKey, payload);
6464
6919
  }
6465
6920
  async delete(name, options) {
6466
6921
  const secretPath = resolveSecretPathResult(name, options);
@@ -7300,10 +7755,10 @@ var EncryptionService = class extends BaseService {
7300
7755
  get crypto() {
7301
7756
  return this._config.crypto;
7302
7757
  }
7303
- async discoverNetwork(identifier, ownerDid) {
7758
+ async discoverNetwork(identifier2, ownerDid) {
7304
7759
  this.assertActive();
7305
7760
  const result = await discoverNetwork({
7306
- identifier,
7761
+ identifier: identifier2,
7307
7762
  ...ownerDid !== void 0 ? { ownerDid } : {},
7308
7763
  ...this._config.node !== void 0 ? { node: this._config.node } : {},
7309
7764
  ...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}
@@ -7516,11 +7971,13 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7516
7971
  KVListResultSchema,
7517
7972
  KVResponseHeadersSchema,
7518
7973
  KVService,
7974
+ MAX_SECRET_SCOPE_LENGTH,
7519
7975
  NETWORK_NAME_PATTERN,
7520
7976
  NetworkIdError,
7521
7977
  PrefixedKVService,
7522
7978
  RetryPolicySchema,
7523
7979
  SECRET_NAME_RE,
7980
+ SECRET_SCOPE_RE,
7524
7981
  SQLAction,
7525
7982
  SQLMigrations,
7526
7983
  SQLService,
@@ -7534,6 +7991,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7534
7991
  ServiceResponseEventSchema,
7535
7992
  ServiceRetryEventSchema,
7536
7993
  ServiceSessionSchema,
7994
+ TC832_RESERVED_SCOPES,
7537
7995
  TelemetryEvents,
7538
7996
  TelemetrySpanEventSchema,
7539
7997
  TinyCloudDebugLogger,
@@ -7541,6 +7999,7 @@ var import_decrypt_transport_response_error3 = require("@tinycloud/sdk-services/
7541
7999
  VaultHeaders,
7542
8000
  VaultPublicSpaceKVActions,
7543
8001
  abortedError,
8002
+ appSecretScope,
7544
8003
  authExpiredError,
7545
8004
  authRequiredError,
7546
8005
  authUnauthorizedError,