@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 +641 -182
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +147 -14
- package/dist/index.d.ts +147 -14
- package/dist/index.js +637 -182
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +60 -15
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +12 -2
- package/dist/kv/index.d.ts +12 -2
- package/dist/kv/index.js +60 -15
- package/dist/kv/index.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
2128
|
+
const items = response.results;
|
|
2129
|
+
if (!Array.isArray(items) || items.length !== paths.length) {
|
|
2097
2130
|
return void 0;
|
|
2098
2131
|
}
|
|
2099
|
-
for (
|
|
2100
|
-
|
|
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
|
-
|
|
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
|
|
2253
|
+
const items = this.normalizeBatchReadResponse(
|
|
2205
2254
|
await response.json(),
|
|
2206
2255
|
paths,
|
|
2207
2256
|
action === KVAction.GET
|
|
2208
2257
|
);
|
|
2209
|
-
if (!
|
|
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 =
|
|
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
|
|
2851
|
-
*
|
|
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 =
|
|
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
|
-
|
|
4176
|
-
this.
|
|
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
|
|
4264
|
+
const requested = subscriptions.map(normalizeSubscription);
|
|
4193
4265
|
const subscriber = {
|
|
4194
|
-
requested
|
|
4266
|
+
requested,
|
|
4267
|
+
signatures: requested.map(subscriptionSignature),
|
|
4195
4268
|
ttlSeconds: options.ttlSeconds,
|
|
4196
4269
|
queue: new AsyncQueue()
|
|
4197
4270
|
};
|
|
4198
|
-
|
|
4199
|
-
const abortHandler = () => {
|
|
4271
|
+
const leave = () => {
|
|
4200
4272
|
this._subscribers.delete(subscriber);
|
|
4201
4273
|
subscriber.queue.close();
|
|
4202
|
-
|
|
4274
|
+
this.wake();
|
|
4203
4275
|
};
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
} else {
|
|
4208
|
-
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
4209
|
-
}
|
|
4276
|
+
const { signal } = options;
|
|
4277
|
+
if (signal?.aborted) {
|
|
4278
|
+
return;
|
|
4210
4279
|
}
|
|
4211
|
-
|
|
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
|
-
|
|
4218
|
-
|
|
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
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
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 (
|
|
4377
|
-
|
|
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
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
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
|
-
|
|
4398
|
-
|
|
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
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
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
|
|
4420
|
-
const
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
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
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
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
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
if (
|
|
4440
|
-
|
|
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
|
-
|
|
4446
|
-
|
|
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
|
-
|
|
4451
|
-
this.
|
|
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
|
-
|
|
4454
|
-
|
|
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
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
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
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4682
|
+
Object.freeze(error);
|
|
4683
|
+
try {
|
|
4684
|
+
this.emitError(error, "stream");
|
|
4685
|
+
} catch {
|
|
4481
4686
|
}
|
|
4482
|
-
return ticketJson;
|
|
4483
4687
|
}
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
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
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
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
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4650
|
-
}
|
|
4651
|
-
if (value) {
|
|
4652
|
-
yield value;
|
|
4653
|
-
}
|
|
5038
|
+
if (done) break;
|
|
5039
|
+
if (value) yield value;
|
|
4654
5040
|
}
|
|
4655
5041
|
} finally {
|
|
4656
|
-
|
|
4657
|
-
|
|
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(
|
|
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
|
|
5365
|
-
|
|
5366
|
-
|
|
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
|
-
|
|
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(
|
|
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,
|