@tinycloud/sdk-services 3.1.0-beta.7 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseService-DU-lE-Qx.d.ts → BaseService-B_UKhVht.d.ts} +1 -1
- package/dist/{BaseService-C8zRy0ww.d.cts → BaseService-DWbPbov7.d.cts} +1 -1
- package/dist/encryption/index.cjs +8 -0
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/encryption/index.js +8 -0
- package/dist/encryption/index.js.map +1 -1
- package/dist/index.cjs +760 -169
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +108 -17
- package/dist/index.d.ts +108 -17
- package/dist/index.js +760 -169
- package/dist/index.js.map +1 -1
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.cjs +256 -9
- package/dist/kv/index.cjs.map +1 -1
- package/dist/kv/index.d.cts +182 -3
- package/dist/kv/index.d.ts +182 -3
- package/dist/kv/index.js +256 -9
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +8 -0
- package/dist/sql/index.cjs.map +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/sql/index.js +8 -0
- package/dist/sql/index.js.map +1 -1
- package/dist/{types-qwMmB7qB.d.cts → types-CVwwxqXF.d.cts} +8 -0
- package/dist/{types-qwMmB7qB.d.ts → types-CVwwxqXF.d.ts} +8 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -5,6 +5,10 @@ var ErrorCodes = {
|
|
|
5
5
|
AUTH_EXPIRED: "AUTH_EXPIRED",
|
|
6
6
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
7
7
|
AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
|
|
8
|
+
/** HTTP 401: the delegation the invocation cites was revoked. */
|
|
9
|
+
AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
|
|
10
|
+
/** HTTP 401: a delegation the cited one rests on was revoked. */
|
|
11
|
+
AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
|
|
8
12
|
NETWORK_ERROR: "NETWORK_ERROR",
|
|
9
13
|
TIMEOUT: "TIMEOUT",
|
|
10
14
|
ABORTED: "ABORTED",
|
|
@@ -16,6 +20,10 @@ var ErrorCodes = {
|
|
|
16
20
|
KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
|
|
17
21
|
KV_CONFLICT: "KV_CONFLICT",
|
|
18
22
|
KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
|
|
23
|
+
/** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
|
|
24
|
+
KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
|
|
25
|
+
/** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
|
|
26
|
+
KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
|
|
19
27
|
// SQL-specific errors
|
|
20
28
|
SQL_ERROR: "SQL_ERROR",
|
|
21
29
|
SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
|
|
@@ -1617,6 +1625,30 @@ var PrefixedKVService = class _PrefixedKVService {
|
|
|
1617
1625
|
removePrefix
|
|
1618
1626
|
});
|
|
1619
1627
|
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Read the change feed for everything under this prefix. A view with an
|
|
1630
|
+
* empty prefix (`""` or `"/"`) is refused: the node does not serve a
|
|
1631
|
+
* whole-space feed.
|
|
1632
|
+
*/
|
|
1633
|
+
async changes(options) {
|
|
1634
|
+
if (this._prefix.replace(/\/+$/, "") === "") {
|
|
1635
|
+
return err(serviceError(
|
|
1636
|
+
ErrorCodes.INVALID_INPUT,
|
|
1637
|
+
"KV changes needs a non-empty prefix; this view has none",
|
|
1638
|
+
"kv"
|
|
1639
|
+
));
|
|
1640
|
+
}
|
|
1641
|
+
const syncPrefix = `${this._prefix}/`;
|
|
1642
|
+
const response = await this._kv.changes({ ...options, prefix: syncPrefix });
|
|
1643
|
+
if (!response.ok) return response;
|
|
1644
|
+
return ok({
|
|
1645
|
+
...response.data,
|
|
1646
|
+
changes: response.data.changes.map((change) => ({
|
|
1647
|
+
...change,
|
|
1648
|
+
key: change.key.startsWith(syncPrefix) ? change.key.slice(syncPrefix.length) : change.key
|
|
1649
|
+
}))
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1620
1652
|
/**
|
|
1621
1653
|
* Delete a key.
|
|
1622
1654
|
*/
|
|
@@ -1669,11 +1701,36 @@ var KVAction = {
|
|
|
1669
1701
|
PUT: "tinycloud.kv/put",
|
|
1670
1702
|
LIST: "tinycloud.kv/list",
|
|
1671
1703
|
DELETE: "tinycloud.kv/del",
|
|
1672
|
-
HEAD: "tinycloud.kv/metadata"
|
|
1704
|
+
HEAD: "tinycloud.kv/metadata",
|
|
1705
|
+
/** The change feed; never implied by `*` or `tinycloud.kv/*`. */
|
|
1706
|
+
SYNC: "tinycloud.kv/sync",
|
|
1707
|
+
/** Retention attestation; presented through `retentionGrant`, never invoked or implied. */
|
|
1708
|
+
RETAIN: "tinycloud.kv/retain"
|
|
1673
1709
|
};
|
|
1674
1710
|
|
|
1675
1711
|
// src/kv/KVService.ts
|
|
1676
1712
|
var MAX_KV_BATCH_READ_ITEMS = 100;
|
|
1713
|
+
function isJsonObject(value) {
|
|
1714
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1715
|
+
}
|
|
1716
|
+
var pathByteEncoder = new TextEncoder();
|
|
1717
|
+
function comparePathsByBytes(a, b) {
|
|
1718
|
+
const left = pathByteEncoder.encode(a);
|
|
1719
|
+
const right = pathByteEncoder.encode(b);
|
|
1720
|
+
const length = Math.min(left.length, right.length);
|
|
1721
|
+
for (let index = 0; index < length; index++) {
|
|
1722
|
+
if (left[index] !== right[index]) {
|
|
1723
|
+
return left[index] - right[index];
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
return left.length - right.length;
|
|
1727
|
+
}
|
|
1728
|
+
var REVOKED_BODY = /^(?:Invalid invocation: )?delegation-(?:(revoked): [A-Za-z0-9]+|(ancestor-revoked): ancestor=[A-Za-z0-9]+ invoked=[A-Za-z0-9]+)$/;
|
|
1729
|
+
function revocationCodeOf(body) {
|
|
1730
|
+
const match = REVOKED_BODY.exec(body.trim());
|
|
1731
|
+
if (match === null) return void 0;
|
|
1732
|
+
return match[1] !== void 0 ? ErrorCodes.AUTH_DELEGATION_REVOKED : ErrorCodes.AUTH_DELEGATION_ANCESTOR_REVOKED;
|
|
1733
|
+
}
|
|
1677
1734
|
function encodeKvBatchPartName(path) {
|
|
1678
1735
|
return encodeURIComponent(path).replace(
|
|
1679
1736
|
/[!'()*]/g,
|
|
@@ -1901,15 +1958,25 @@ var KVService = class extends BaseService {
|
|
|
1901
1958
|
)?.[1];
|
|
1902
1959
|
return contentType?.includes("application/json") ? JSON.parse(text) : text;
|
|
1903
1960
|
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Validate a batch read response and return its items in the caller's
|
|
1963
|
+
* request order. The node flattens invocation abilities from a `BTreeMap`,
|
|
1964
|
+
* so results arrive sorted by UTF-8 byte order of the path, not in request
|
|
1965
|
+
* order. Items that carry their path in `key` are matched by path; if no
|
|
1966
|
+
* item carries a key, items are assumed to be in the node's byte-sorted
|
|
1967
|
+
* order and the request is aligned the same way. Any mix of keyed and
|
|
1968
|
+
* keyless items, a missing requested path, an unrequested path, or a
|
|
1969
|
+
* malformed item fails closed (returns undefined).
|
|
1970
|
+
*/
|
|
1904
1971
|
normalizeBatchReadResponse(data, paths, requireData) {
|
|
1905
1972
|
if (!data || typeof data !== "object") return void 0;
|
|
1906
1973
|
const response = data;
|
|
1907
|
-
|
|
1974
|
+
const items = response.results;
|
|
1975
|
+
if (!Array.isArray(items) || items.length !== paths.length) {
|
|
1908
1976
|
return void 0;
|
|
1909
1977
|
}
|
|
1910
|
-
for (
|
|
1911
|
-
|
|
1912
|
-
if (!item || typeof item !== "object" || item.key !== paths[index] || typeof item.ok !== "boolean") {
|
|
1978
|
+
for (const item of items) {
|
|
1979
|
+
if (!item || typeof item !== "object" || typeof item.ok !== "boolean") {
|
|
1913
1980
|
return void 0;
|
|
1914
1981
|
}
|
|
1915
1982
|
if (item.ok && (!item.headers || typeof item.headers !== "object" || Object.values(item.headers).some((value) => typeof value !== "string") || requireData && typeof item.dataBase64 !== "string")) {
|
|
@@ -1919,7 +1986,24 @@ var KVService = class extends BaseService {
|
|
|
1919
1986
|
return void 0;
|
|
1920
1987
|
}
|
|
1921
1988
|
}
|
|
1922
|
-
|
|
1989
|
+
if (items.every((item) => typeof item.key === "string")) {
|
|
1990
|
+
const byPath = new Map(items.map((item) => [item.key, item]));
|
|
1991
|
+
if (byPath.size !== items.length) return void 0;
|
|
1992
|
+
const aligned2 = [];
|
|
1993
|
+
for (const path of paths) {
|
|
1994
|
+
const item = byPath.get(path);
|
|
1995
|
+
if (!item) return void 0;
|
|
1996
|
+
aligned2.push(item);
|
|
1997
|
+
}
|
|
1998
|
+
return aligned2;
|
|
1999
|
+
}
|
|
2000
|
+
if (items.some((item) => item.key !== void 0)) return void 0;
|
|
2001
|
+
const requestOrder = paths.map((_, index) => index).sort((a, b) => comparePathsByBytes(paths[a], paths[b]));
|
|
2002
|
+
const aligned = new Array(items.length);
|
|
2003
|
+
for (let position = 0; position < requestOrder.length; position++) {
|
|
2004
|
+
aligned[requestOrder[position]] = items[position];
|
|
2005
|
+
}
|
|
2006
|
+
return aligned;
|
|
1923
2007
|
}
|
|
1924
2008
|
async batchRead(keys, action, options) {
|
|
1925
2009
|
if (!this.requireAuth()) return err(authRequiredError("kv"));
|
|
@@ -2012,19 +2096,19 @@ var KVService = class extends BaseService {
|
|
|
2012
2096
|
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2013
2097
|
));
|
|
2014
2098
|
}
|
|
2015
|
-
const
|
|
2099
|
+
const items = this.normalizeBatchReadResponse(
|
|
2016
2100
|
await response.json(),
|
|
2017
2101
|
paths,
|
|
2018
2102
|
action === KVAction.GET
|
|
2019
2103
|
);
|
|
2020
|
-
if (!
|
|
2104
|
+
if (!items) {
|
|
2021
2105
|
return err(serviceError(
|
|
2022
2106
|
ErrorCodes.NETWORK_ERROR,
|
|
2023
2107
|
"KV batch read response did not match the requested keys",
|
|
2024
2108
|
"kv"
|
|
2025
2109
|
));
|
|
2026
2110
|
}
|
|
2027
|
-
const results =
|
|
2111
|
+
const results = items.map((item, index) => {
|
|
2028
2112
|
if (!item.ok) {
|
|
2029
2113
|
return {
|
|
2030
2114
|
key: keys[index],
|
|
@@ -2592,6 +2676,169 @@ var KVService = class extends BaseService {
|
|
|
2592
2676
|
}
|
|
2593
2677
|
});
|
|
2594
2678
|
}
|
|
2679
|
+
/**
|
|
2680
|
+
* Read one page of the `tinycloud.kv/sync` change feed for a prefix.
|
|
2681
|
+
*/
|
|
2682
|
+
async changes(options) {
|
|
2683
|
+
return this.withTelemetry("changes", options?.prefix, async () => {
|
|
2684
|
+
if (!this.requireAuth()) {
|
|
2685
|
+
return err(authRequiredError("kv"));
|
|
2686
|
+
}
|
|
2687
|
+
if (typeof options?.prefix !== "string" || options.prefix.length === 0) {
|
|
2688
|
+
return err(serviceError(
|
|
2689
|
+
ErrorCodes.INVALID_INPUT,
|
|
2690
|
+
"KV changes prefix must be a non-empty string",
|
|
2691
|
+
"kv"
|
|
2692
|
+
));
|
|
2693
|
+
}
|
|
2694
|
+
if (options.limit !== void 0 && (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 1e3)) {
|
|
2695
|
+
return err(serviceError(
|
|
2696
|
+
ErrorCodes.INVALID_INPUT,
|
|
2697
|
+
"KV changes limit must be an integer from 1 through 1000",
|
|
2698
|
+
"kv"
|
|
2699
|
+
));
|
|
2700
|
+
}
|
|
2701
|
+
const headers = {};
|
|
2702
|
+
if (options.limit !== void 0) headers["x-tinycloud-limit"] = String(options.limit);
|
|
2703
|
+
if (options.cursor !== void 0) headers["x-tinycloud-cursor"] = options.cursor;
|
|
2704
|
+
if (options.retentionGrant !== void 0) {
|
|
2705
|
+
headers["x-tinycloud-retention-grant"] = options.retentionGrant;
|
|
2706
|
+
}
|
|
2707
|
+
const request = this.createRequestSignal(options.signal, options.timeout);
|
|
2708
|
+
try {
|
|
2709
|
+
const response = await this.invokeOperation(
|
|
2710
|
+
options.prefix,
|
|
2711
|
+
KVAction.SYNC,
|
|
2712
|
+
void 0,
|
|
2713
|
+
request.signal,
|
|
2714
|
+
headers
|
|
2715
|
+
);
|
|
2716
|
+
if (!response.ok) {
|
|
2717
|
+
return await this.changesFailure(response, options.prefix, request.signal);
|
|
2718
|
+
}
|
|
2719
|
+
const text = await response.text();
|
|
2720
|
+
let body;
|
|
2721
|
+
try {
|
|
2722
|
+
body = JSON.parse(text);
|
|
2723
|
+
} catch {
|
|
2724
|
+
body = void 0;
|
|
2725
|
+
}
|
|
2726
|
+
const page = this.normalizeChangesResponse(body);
|
|
2727
|
+
if (!page) {
|
|
2728
|
+
return err(serviceError(
|
|
2729
|
+
ErrorCodes.NETWORK_ERROR,
|
|
2730
|
+
"KV changes response was not a tinycloud.kv/sync page",
|
|
2731
|
+
"kv",
|
|
2732
|
+
{ meta: { status: response.status, statusText: response.statusText } }
|
|
2733
|
+
));
|
|
2734
|
+
}
|
|
2735
|
+
return ok(page);
|
|
2736
|
+
} catch (error) {
|
|
2737
|
+
return err(wrapError("kv", error));
|
|
2738
|
+
} finally {
|
|
2739
|
+
request.dispose();
|
|
2740
|
+
}
|
|
2741
|
+
});
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* Map a failed `kv/sync` response. 410 and the retention 403 carry
|
|
2745
|
+
* `{"error":{"code","reason"}}`; a revoked grant is a 401 whose whole body
|
|
2746
|
+
* is the node's revocation error (see REVOKED_BODY). A body read that
|
|
2747
|
+
* fails because the request was cancelled or timed out rethrows, so the
|
|
2748
|
+
* caller sees `ABORTED`/`TIMEOUT` rather than a mapped status.
|
|
2749
|
+
*/
|
|
2750
|
+
async changesFailure(response, prefix, signal) {
|
|
2751
|
+
if (response.status === 404) {
|
|
2752
|
+
return this.classifyNotFound(response, prefix);
|
|
2753
|
+
}
|
|
2754
|
+
let errorText;
|
|
2755
|
+
try {
|
|
2756
|
+
errorText = await response.text();
|
|
2757
|
+
} catch (error) {
|
|
2758
|
+
if (signal.aborted) throw error;
|
|
2759
|
+
errorText = "";
|
|
2760
|
+
}
|
|
2761
|
+
const meta = { status: response.status, statusText: response.statusText };
|
|
2762
|
+
let reason;
|
|
2763
|
+
let bodyCode;
|
|
2764
|
+
try {
|
|
2765
|
+
const body = JSON.parse(errorText);
|
|
2766
|
+
const error = isJsonObject(body) ? body.error : void 0;
|
|
2767
|
+
if (isJsonObject(error)) {
|
|
2768
|
+
if (typeof error.code === "string") bodyCode = error.code;
|
|
2769
|
+
if (typeof error.reason === "string") reason = error.reason;
|
|
2770
|
+
}
|
|
2771
|
+
} catch {
|
|
2772
|
+
}
|
|
2773
|
+
const context = `Failed to read KV changes for ${JSON.stringify(prefix)}`;
|
|
2774
|
+
if (response.status === 410) {
|
|
2775
|
+
return err(serviceError(
|
|
2776
|
+
ErrorCodes.KV_SYNC_RESET_REQUIRED,
|
|
2777
|
+
`${context}: the node requires a reset (${reason ?? "reason not given"}); discard sync state and restart without a cursor`,
|
|
2778
|
+
"kv",
|
|
2779
|
+
{ meta: { ...meta, reason } }
|
|
2780
|
+
));
|
|
2781
|
+
}
|
|
2782
|
+
if (response.status === 403 && bodyCode === "RETENTION_GRANT_REFUSED") {
|
|
2783
|
+
return err(serviceError(
|
|
2784
|
+
ErrorCodes.KV_RETENTION_GRANT_REFUSED,
|
|
2785
|
+
`${context}: the retention grant was refused (${reason ?? "reason not given"})`,
|
|
2786
|
+
"kv",
|
|
2787
|
+
{ meta: { ...meta, reason } }
|
|
2788
|
+
));
|
|
2789
|
+
}
|
|
2790
|
+
if (response.status === 401) {
|
|
2791
|
+
const revoked = revocationCodeOf(errorText);
|
|
2792
|
+
if (revoked !== void 0) {
|
|
2793
|
+
return err(serviceError(revoked, `${context}: 401 - ${errorText}`, "kv", { meta }));
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
if (response.status === 401 || response.status === 403) {
|
|
2797
|
+
return this.authorizationFailure(context, response, errorText, [prefix], KVAction.SYNC);
|
|
2798
|
+
}
|
|
2799
|
+
return err(serviceError(
|
|
2800
|
+
response.status === 400 ? ErrorCodes.INVALID_INPUT : ErrorCodes.NETWORK_ERROR,
|
|
2801
|
+
`${context}: ${response.status} - ${errorText}`,
|
|
2802
|
+
"kv",
|
|
2803
|
+
{ meta }
|
|
2804
|
+
));
|
|
2805
|
+
}
|
|
2806
|
+
normalizeChangesResponse(data) {
|
|
2807
|
+
if (!isJsonObject(data)) return void 0;
|
|
2808
|
+
const { changes, more, cursor, source, authority } = data;
|
|
2809
|
+
if (!Array.isArray(changes) || typeof more !== "boolean" || typeof cursor !== "string" || !isJsonObject(source) || typeof source.nodeDid !== "string" || typeof source.space !== "string" || typeof source.prefix !== "string" || !isJsonObject(authority)) {
|
|
2810
|
+
return void 0;
|
|
2811
|
+
}
|
|
2812
|
+
const { notBefore, expiresAt, retainUntil } = authority;
|
|
2813
|
+
if (notBefore !== null && typeof notBefore !== "string" || expiresAt !== null && typeof expiresAt !== "string" || retainUntil !== null && typeof retainUntil !== "string") {
|
|
2814
|
+
return void 0;
|
|
2815
|
+
}
|
|
2816
|
+
const parsed = [];
|
|
2817
|
+
for (const change of changes) {
|
|
2818
|
+
if (!isJsonObject(change) || typeof change.key !== "string") return void 0;
|
|
2819
|
+
if (change.deleted === true) {
|
|
2820
|
+
parsed.push({ key: change.key, deleted: true });
|
|
2821
|
+
continue;
|
|
2822
|
+
}
|
|
2823
|
+
const { metadata } = change;
|
|
2824
|
+
if (change.deleted !== false || typeof change.etag !== "string" || !isJsonObject(metadata) || !Object.values(metadata).every((value) => typeof value === "string")) {
|
|
2825
|
+
return void 0;
|
|
2826
|
+
}
|
|
2827
|
+
parsed.push({
|
|
2828
|
+
key: change.key,
|
|
2829
|
+
deleted: false,
|
|
2830
|
+
etag: change.etag,
|
|
2831
|
+
metadata
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
return {
|
|
2835
|
+
changes: parsed,
|
|
2836
|
+
more,
|
|
2837
|
+
cursor,
|
|
2838
|
+
source: { nodeDid: source.nodeDid, space: source.space, prefix: source.prefix },
|
|
2839
|
+
authority: { notBefore, expiresAt, retainUntil }
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2595
2842
|
/**
|
|
2596
2843
|
* Delete a key.
|
|
2597
2844
|
*/
|
|
@@ -3788,7 +4035,7 @@ var AsyncQueue = class {
|
|
|
3788
4035
|
}
|
|
3789
4036
|
const waiter = this.waiters.shift();
|
|
3790
4037
|
if (waiter) {
|
|
3791
|
-
waiter({ value, done: false });
|
|
4038
|
+
waiter.resolve({ value, done: false });
|
|
3792
4039
|
return;
|
|
3793
4040
|
}
|
|
3794
4041
|
this.values.push(value);
|
|
@@ -3800,7 +4047,22 @@ var AsyncQueue = class {
|
|
|
3800
4047
|
this.closed = true;
|
|
3801
4048
|
while (this.waiters.length > 0) {
|
|
3802
4049
|
const waiter = this.waiters.shift();
|
|
3803
|
-
waiter?.({ value: void 0, done: true });
|
|
4050
|
+
waiter?.resolve({ value: void 0, done: true });
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
/**
|
|
4054
|
+
* Fail the queue: pending and future `next()` calls reject with `error`,
|
|
4055
|
+
* so a `for await` consumer sees the iterator throw.
|
|
4056
|
+
*/
|
|
4057
|
+
fail(error) {
|
|
4058
|
+
if (this.closed) {
|
|
4059
|
+
return;
|
|
4060
|
+
}
|
|
4061
|
+
this.closed = true;
|
|
4062
|
+
this.failure = error;
|
|
4063
|
+
while (this.waiters.length > 0) {
|
|
4064
|
+
const waiter = this.waiters.shift();
|
|
4065
|
+
waiter?.reject(error);
|
|
3804
4066
|
}
|
|
3805
4067
|
}
|
|
3806
4068
|
next() {
|
|
@@ -3808,20 +4070,28 @@ var AsyncQueue = class {
|
|
|
3808
4070
|
const value = this.values.shift();
|
|
3809
4071
|
return Promise.resolve({ value, done: false });
|
|
3810
4072
|
}
|
|
4073
|
+
if (this.failure !== void 0) {
|
|
4074
|
+
return Promise.reject(this.failure);
|
|
4075
|
+
}
|
|
3811
4076
|
if (this.closed) {
|
|
3812
4077
|
return Promise.resolve({ value: void 0, done: true });
|
|
3813
4078
|
}
|
|
3814
|
-
return new Promise((resolve) => {
|
|
3815
|
-
this.waiters.push(resolve);
|
|
4079
|
+
return new Promise((resolve, reject) => {
|
|
4080
|
+
this.waiters.push({ resolve, reject });
|
|
3816
4081
|
});
|
|
3817
4082
|
}
|
|
3818
4083
|
};
|
|
4084
|
+
var HOOK_STREAM_RETRY_BASE_DELAY_MS = 250;
|
|
4085
|
+
var HOOK_STREAM_RETRY_MAX_DELAY_MS = 3e4;
|
|
4086
|
+
var HOOK_STREAM_HEALTHY_MS = 5e3;
|
|
3819
4087
|
var HooksService = class extends BaseService {
|
|
3820
4088
|
constructor(config = {}) {
|
|
3821
4089
|
super();
|
|
3822
4090
|
this._subscribers = /* @__PURE__ */ new Set();
|
|
3823
|
-
|
|
3824
|
-
this.
|
|
4091
|
+
/** True while the one supervisor loop runs. Set only by wake(), cleared only at loop exit. */
|
|
4092
|
+
this._supervising = false;
|
|
4093
|
+
/** Bumped on every session change; part of the plan key. */
|
|
4094
|
+
this._sessionGeneration = 0;
|
|
3825
4095
|
this._config = config;
|
|
3826
4096
|
}
|
|
3827
4097
|
get config() {
|
|
@@ -3837,35 +4107,32 @@ var HooksService = class extends BaseService {
|
|
|
3837
4107
|
if (subscriptions.length === 0) {
|
|
3838
4108
|
throw new Error("At least one hook subscription is required");
|
|
3839
4109
|
}
|
|
3840
|
-
const
|
|
4110
|
+
const requested = subscriptions.map(normalizeSubscription);
|
|
3841
4111
|
const subscriber = {
|
|
3842
|
-
requested
|
|
4112
|
+
requested,
|
|
4113
|
+
signatures: requested.map(subscriptionSignature),
|
|
3843
4114
|
ttlSeconds: options.ttlSeconds,
|
|
3844
4115
|
queue: new AsyncQueue()
|
|
3845
4116
|
};
|
|
3846
|
-
|
|
3847
|
-
const abortHandler = () => {
|
|
4117
|
+
const leave = () => {
|
|
3848
4118
|
this._subscribers.delete(subscriber);
|
|
3849
4119
|
subscriber.queue.close();
|
|
3850
|
-
|
|
4120
|
+
this.wake();
|
|
3851
4121
|
};
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
} else {
|
|
3856
|
-
options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
3857
|
-
}
|
|
4122
|
+
const { signal } = options;
|
|
4123
|
+
if (signal?.aborted) {
|
|
4124
|
+
return;
|
|
3858
4125
|
}
|
|
3859
|
-
|
|
4126
|
+
signal?.addEventListener("abort", leave, { once: true });
|
|
4127
|
+
this._subscribers.add(subscriber);
|
|
4128
|
+
this.wake();
|
|
3860
4129
|
try {
|
|
3861
4130
|
for await (const event of subscriber.queue) {
|
|
3862
4131
|
yield event;
|
|
3863
4132
|
}
|
|
3864
4133
|
} finally {
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
}
|
|
3868
|
-
abortHandler();
|
|
4134
|
+
signal?.removeEventListener("abort", leave);
|
|
4135
|
+
leave();
|
|
3869
4136
|
}
|
|
3870
4137
|
}
|
|
3871
4138
|
async register(webhook) {
|
|
@@ -4006,146 +4273,292 @@ var HooksService = class extends BaseService {
|
|
|
4006
4273
|
return err(wrapError("hooks", error));
|
|
4007
4274
|
}
|
|
4008
4275
|
}
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
if (state.signature !== this._activeSignature) {
|
|
4021
|
-
this._activeSignature = state.signature;
|
|
4022
|
-
this.abortSharedStream();
|
|
4276
|
+
/**
|
|
4277
|
+
* Called after anything that may change what the supervisor should do:
|
|
4278
|
+
* subscribe, unsubscribe, session change, sign-out. Interrupts the running
|
|
4279
|
+
* step if the plan moved away from it, and starts the supervisor if none
|
|
4280
|
+
* runs. Touches only SDK-owned state, so it never throws into callers such
|
|
4281
|
+
* as `ServiceContext.setSession`.
|
|
4282
|
+
*/
|
|
4283
|
+
wake() {
|
|
4284
|
+
const phase = this._phase;
|
|
4285
|
+
if (phase && phase.key !== this.plan()?.key) {
|
|
4286
|
+
phase.interrupt.abort();
|
|
4023
4287
|
}
|
|
4024
|
-
if (
|
|
4025
|
-
|
|
4026
|
-
if (!isAbortError(error)) {
|
|
4027
|
-
throw error;
|
|
4028
|
-
}
|
|
4029
|
-
}).finally(() => {
|
|
4030
|
-
this._sharedStreamTask = void 0;
|
|
4031
|
-
this._sharedStreamAbort = void 0;
|
|
4032
|
-
if (this._subscribers.size > 0) {
|
|
4033
|
-
void this.scheduleSharedStreamRefresh();
|
|
4034
|
-
}
|
|
4035
|
-
});
|
|
4288
|
+
if (this._supervising || this._subscribers.size === 0) {
|
|
4289
|
+
return;
|
|
4036
4290
|
}
|
|
4291
|
+
this._supervising = true;
|
|
4292
|
+
this.supervise().catch(() => this.collapse());
|
|
4037
4293
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4294
|
+
/**
|
|
4295
|
+
* The only owner of the shared stream. Each iteration is one attempt
|
|
4296
|
+
* (mint → open → drain), then a classification and, unless the attempt was
|
|
4297
|
+
* superseded or released its subscribers, a backoff. Every await is a
|
|
4298
|
+
* `phase` that wake() can interrupt; an interrupted phase yields
|
|
4299
|
+
* `undefined`, so a superseded mint can never act. Outside `attempt` and
|
|
4300
|
+
* `pause`, which each own one catch, the loop touches only SDK state.
|
|
4301
|
+
*/
|
|
4302
|
+
async supervise() {
|
|
4303
|
+
let streak = 0;
|
|
4304
|
+
for (; ; ) {
|
|
4305
|
+
const plan = this.plan();
|
|
4306
|
+
if (!plan) {
|
|
4307
|
+
this._supervising = false;
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
const health = { openedAt: 0, delivered: false };
|
|
4311
|
+
const outcome = await this.phase(
|
|
4312
|
+
plan.key,
|
|
4313
|
+
(signal) => this.attempt(plan, signal, health)
|
|
4314
|
+
);
|
|
4315
|
+
if (health.delivered || health.openedAt > 0 && performance.now() - health.openedAt >= HOOK_STREAM_HEALTHY_MS) {
|
|
4316
|
+
streak = 0;
|
|
4317
|
+
}
|
|
4318
|
+
if (!outcome) {
|
|
4319
|
+
continue;
|
|
4044
4320
|
}
|
|
4045
|
-
|
|
4046
|
-
|
|
4321
|
+
if (this.plan()?.key !== plan.key) {
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
if (outcome.kind === "stopped") {
|
|
4325
|
+
this.release();
|
|
4326
|
+
continue;
|
|
4327
|
+
}
|
|
4328
|
+
if (outcome.kind === "refused" && outcome.terminal) {
|
|
4329
|
+
this.release(outcome.error);
|
|
4330
|
+
this.report(outcome.error);
|
|
4331
|
+
continue;
|
|
4047
4332
|
}
|
|
4333
|
+
if (outcome.kind !== "ended") {
|
|
4334
|
+
this.report(outcome.error);
|
|
4335
|
+
}
|
|
4336
|
+
streak += 1;
|
|
4337
|
+
await this.phase(
|
|
4338
|
+
plan.key,
|
|
4339
|
+
(signal) => this.pause(streak, signal)
|
|
4340
|
+
);
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
/**
|
|
4344
|
+
* Run one step under an interrupt that wake() fires when the plan key moves
|
|
4345
|
+
* on. An interrupted step's result is discarded. The interrupt is also
|
|
4346
|
+
* fired when the step returns, which releases whatever it left open: the
|
|
4347
|
+
* request signal and its listeners, an unread response body, a timer.
|
|
4348
|
+
*/
|
|
4349
|
+
async phase(key, step) {
|
|
4350
|
+
const interrupt = new AbortController();
|
|
4351
|
+
this._phase = { key, interrupt };
|
|
4352
|
+
try {
|
|
4353
|
+
if (this.plan()?.key !== key) {
|
|
4354
|
+
interrupt.abort();
|
|
4355
|
+
}
|
|
4356
|
+
const result = await step(interrupt.signal);
|
|
4357
|
+
return interrupt.signal.aborted ? void 0 : result;
|
|
4358
|
+
} finally {
|
|
4359
|
+
this._phase = void 0;
|
|
4360
|
+
interrupt.abort();
|
|
4048
4361
|
}
|
|
4049
|
-
const subscriptions = [...merged.values()].sort(
|
|
4050
|
-
(left, right) => subscriptionSignature(left).localeCompare(subscriptionSignature(right))
|
|
4051
|
-
);
|
|
4052
|
-
const ttlSeconds = ttlCandidates.length > 0 ? Math.min(...ttlCandidates) : void 0;
|
|
4053
|
-
const signature = JSON.stringify({
|
|
4054
|
-
subscriptions: subscriptions.map(subscriptionSignature),
|
|
4055
|
-
ttlSeconds
|
|
4056
|
-
});
|
|
4057
|
-
return {
|
|
4058
|
-
subscriptions,
|
|
4059
|
-
ttlSeconds,
|
|
4060
|
-
signature
|
|
4061
|
-
};
|
|
4062
4362
|
}
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4363
|
+
/**
|
|
4364
|
+
* One mint → open → drain. The single catch site for everything the
|
|
4365
|
+
* attempt touches that the SDK does not own: context accessors, invoke,
|
|
4366
|
+
* fetch, response bodies, the SSE parser. Never rejects.
|
|
4367
|
+
*/
|
|
4368
|
+
async attempt(plan, signal, health) {
|
|
4369
|
+
let phase = "mint";
|
|
4370
|
+
let request;
|
|
4371
|
+
const owns = () => !signal.aborted && !request?.signal.aborted && this.plan()?.key === plan.key;
|
|
4066
4372
|
try {
|
|
4067
|
-
const
|
|
4068
|
-
const
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4373
|
+
const context = this.context;
|
|
4374
|
+
const contextSignal = context.abortSignal;
|
|
4375
|
+
this._lastContextSignal = contextSignal;
|
|
4376
|
+
if (!context.isAuthenticated || contextSignal?.aborted) {
|
|
4377
|
+
return { kind: "stopped" };
|
|
4378
|
+
}
|
|
4379
|
+
request = this.createRequestSignal(signal, 0);
|
|
4380
|
+
const host = this.host;
|
|
4381
|
+
const minted = await awaitOwned(
|
|
4382
|
+
request.signal,
|
|
4383
|
+
() => context.fetch(`${host}/hooks/tickets`, {
|
|
4384
|
+
method: "POST",
|
|
4385
|
+
headers: {
|
|
4386
|
+
...serviceHeadersToRecord(this.createInvokeHeaders(plan.subscriptions)),
|
|
4387
|
+
"content-type": "application/json"
|
|
4388
|
+
},
|
|
4389
|
+
body: JSON.stringify({
|
|
4390
|
+
subscriptions: plan.subscriptions,
|
|
4391
|
+
ttlSeconds: plan.ttlSeconds
|
|
4392
|
+
}),
|
|
4393
|
+
signal: request.signal
|
|
4394
|
+
})
|
|
4072
4395
|
);
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4396
|
+
if (!owns()) return { kind: "stopped" };
|
|
4397
|
+
if (!minted.ok) {
|
|
4398
|
+
const status = minted.status;
|
|
4399
|
+
const error = await awaitOwned(
|
|
4400
|
+
request.signal,
|
|
4401
|
+
() => responseError("hooks", "failed to mint hook ticket", minted)
|
|
4402
|
+
);
|
|
4403
|
+
if (!owns()) return { kind: "stopped" };
|
|
4404
|
+
return { kind: "refused", terminal: isTerminalMintRefusal(status), error };
|
|
4405
|
+
}
|
|
4406
|
+
const body = await awaitOwned(request.signal, () => minted.json());
|
|
4407
|
+
if (!owns()) return { kind: "stopped" };
|
|
4408
|
+
const ticket = body !== null && typeof body === "object" && "ticket" in body ? body.ticket : void 0;
|
|
4409
|
+
if (typeof ticket !== "string" || ticket.length === 0) {
|
|
4410
|
+
throw new Error("Hook ticket response did not include a ticket");
|
|
4411
|
+
}
|
|
4412
|
+
phase = "open";
|
|
4413
|
+
if (!owns()) return { kind: "stopped" };
|
|
4414
|
+
const stream = await awaitOwned(
|
|
4415
|
+
request.signal,
|
|
4416
|
+
() => context.fetch(`${host}/hooks/events?ticket=${encodeURIComponent(ticket)}`, {
|
|
4417
|
+
method: "GET",
|
|
4418
|
+
headers: { accept: "text/event-stream" },
|
|
4419
|
+
signal: request.signal
|
|
4420
|
+
})
|
|
4077
4421
|
);
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
if (
|
|
4088
|
-
|
|
4089
|
-
|
|
4422
|
+
if (!owns()) return { kind: "stopped" };
|
|
4423
|
+
if (!stream.ok) {
|
|
4424
|
+
return { kind: "refused", terminal: false, error: hookStreamRefusal(stream.status) };
|
|
4425
|
+
}
|
|
4426
|
+
health.openedAt = performance.now();
|
|
4427
|
+
const iterator = parseSseStream(stream.body, request.signal)[Symbol.asyncIterator]();
|
|
4428
|
+
try {
|
|
4429
|
+
for (; ; ) {
|
|
4430
|
+
const next = await awaitOwned(request.signal, () => iterator.next());
|
|
4431
|
+
if (!owns()) return { kind: "stopped" };
|
|
4432
|
+
if (next.done) break;
|
|
4433
|
+
const message = next.value;
|
|
4434
|
+
if (!message.data) continue;
|
|
4435
|
+
const event = parseHookEvent(message);
|
|
4436
|
+
if (!owns()) return { kind: "stopped" };
|
|
4437
|
+
this.dispatch(event);
|
|
4438
|
+
health.delivered = true;
|
|
4090
4439
|
}
|
|
4440
|
+
} finally {
|
|
4441
|
+
const closing = iterator.return?.();
|
|
4442
|
+
if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
|
|
4091
4443
|
}
|
|
4444
|
+
return owns() ? { kind: "ended" } : { kind: "stopped" };
|
|
4445
|
+
} catch (thrown) {
|
|
4446
|
+
return request?.signal.aborted || thrown === SUPERSEDED ? { kind: "stopped" } : { kind: "failed", error: hookStreamFailure(phase, thrown) };
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
/**
|
|
4450
|
+
* Back off before the next attempt. `config.streamRetry` overrides the
|
|
4451
|
+
* delay and the clock; whatever they do, the pause ends by the delay or the
|
|
4452
|
+
* interrupt, then yields one timer turn so an instant clock cannot starve
|
|
4453
|
+
* due timers or the process.
|
|
4454
|
+
*/
|
|
4455
|
+
async pause(attempt, signal) {
|
|
4456
|
+
try {
|
|
4457
|
+
this._lastContextSignal = this.context.abortSignal;
|
|
4458
|
+
} catch {
|
|
4459
|
+
}
|
|
4460
|
+
const contextSignal = this._lastContextSignal;
|
|
4461
|
+
const waits = [];
|
|
4462
|
+
const aborts = [];
|
|
4463
|
+
try {
|
|
4464
|
+
const retry = this._config.streamRetry;
|
|
4465
|
+
const delayMs = retry?.delay?.(attempt) ?? hookStreamRetryDelay(attempt);
|
|
4466
|
+
waits.push((retry?.wait ?? sleep)(delayMs, attempt, signal));
|
|
4467
|
+
} catch {
|
|
4468
|
+
waits.push(Promise.resolve());
|
|
4469
|
+
}
|
|
4470
|
+
aborts.push(abortPromise(signal));
|
|
4471
|
+
if (contextSignal) aborts.push(abortPromise(contextSignal));
|
|
4472
|
+
try {
|
|
4473
|
+
await Promise.race([...waits, ...aborts.map(({ promise }) => promise)]);
|
|
4474
|
+
} catch {
|
|
4092
4475
|
} finally {
|
|
4093
|
-
|
|
4094
|
-
|
|
4476
|
+
for (const abort of aborts) abort.dispose();
|
|
4477
|
+
}
|
|
4478
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
4479
|
+
}
|
|
4480
|
+
/** The stream the current subscribers need, or undefined when none remain. */
|
|
4481
|
+
plan() {
|
|
4482
|
+
if (this._subscribers.size === 0) {
|
|
4483
|
+
return void 0;
|
|
4484
|
+
}
|
|
4485
|
+
const merged = /* @__PURE__ */ new Map();
|
|
4486
|
+
let ttlSeconds;
|
|
4487
|
+
for (const subscriber of this._subscribers) {
|
|
4488
|
+
subscriber.requested.forEach(
|
|
4489
|
+
(subscription, index) => merged.set(subscriber.signatures[index], subscription)
|
|
4490
|
+
);
|
|
4491
|
+
if (typeof subscriber.ttlSeconds === "number") {
|
|
4492
|
+
ttlSeconds = Math.min(subscriber.ttlSeconds, ttlSeconds ?? Infinity);
|
|
4095
4493
|
}
|
|
4096
4494
|
}
|
|
4495
|
+
const signatures = [...merged.keys()].sort();
|
|
4496
|
+
return {
|
|
4497
|
+
key: JSON.stringify([this._sessionGeneration, signatures, ttlSeconds ?? null]),
|
|
4498
|
+
subscriptions: signatures.map((signature) => merged.get(signature)),
|
|
4499
|
+
ttlSeconds
|
|
4500
|
+
};
|
|
4097
4501
|
}
|
|
4098
|
-
|
|
4099
|
-
this.
|
|
4502
|
+
dispatch(event) {
|
|
4503
|
+
for (const subscriber of this._subscribers) {
|
|
4504
|
+
if (matchesAnySubscription(event, subscriber.requested)) {
|
|
4505
|
+
subscriber.queue.push(event);
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4100
4508
|
}
|
|
4101
|
-
|
|
4102
|
-
|
|
4509
|
+
/**
|
|
4510
|
+
* Detach every subscriber. With `error`, their iterators throw it after
|
|
4511
|
+
* draining buffered events; without, they complete.
|
|
4512
|
+
*/
|
|
4513
|
+
release(error) {
|
|
4514
|
+
const subscribers = [...this._subscribers];
|
|
4515
|
+
this._subscribers.clear();
|
|
4516
|
+
for (const { queue } of subscribers) {
|
|
4517
|
+
if (error) {
|
|
4518
|
+
queue.fail(error);
|
|
4519
|
+
} else {
|
|
4520
|
+
queue.close();
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4103
4523
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
const ticketResponse = await this.context.fetch(`${host}/hooks/tickets`, {
|
|
4108
|
-
method: "POST",
|
|
4109
|
-
headers: {
|
|
4110
|
-
...serviceHeadersToRecord(headers),
|
|
4111
|
-
"content-type": "application/json"
|
|
4112
|
-
},
|
|
4113
|
-
body: JSON.stringify({
|
|
4114
|
-
subscriptions,
|
|
4115
|
-
ttlSeconds
|
|
4116
|
-
}),
|
|
4117
|
-
signal
|
|
4118
|
-
});
|
|
4119
|
-
if (!ticketResponse.ok) {
|
|
4120
|
-
throw await responseError(
|
|
4121
|
-
"hooks",
|
|
4122
|
-
"failed to mint hook ticket",
|
|
4123
|
-
ticketResponse
|
|
4124
|
-
);
|
|
4524
|
+
report(error) {
|
|
4525
|
+
if (error.meta && typeof error.meta === "object") {
|
|
4526
|
+
Object.freeze(error.meta);
|
|
4125
4527
|
}
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4528
|
+
Object.freeze(error);
|
|
4529
|
+
try {
|
|
4530
|
+
this.emitError(error, "stream");
|
|
4531
|
+
} catch {
|
|
4129
4532
|
}
|
|
4130
|
-
return ticketJson;
|
|
4131
4533
|
}
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4534
|
+
/**
|
|
4535
|
+
* Unreachable by construction. If the loop ever rejects anyway, fail its
|
|
4536
|
+
* subscribers visibly instead of stranding them or crashing the process.
|
|
4537
|
+
*/
|
|
4538
|
+
collapse() {
|
|
4539
|
+
this._supervising = false;
|
|
4540
|
+
this._phase?.interrupt.abort();
|
|
4541
|
+
this._phase = void 0;
|
|
4542
|
+
const error = serviceError(
|
|
4543
|
+
ErrorCodes.NETWORK_ERROR,
|
|
4544
|
+
"hook stream supervisor stopped unexpectedly",
|
|
4545
|
+
"hooks"
|
|
4140
4546
|
);
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4547
|
+
this.report(error);
|
|
4548
|
+
this.release(error);
|
|
4549
|
+
}
|
|
4550
|
+
onSessionChange(session) {
|
|
4551
|
+
super.onSessionChange(session);
|
|
4552
|
+
this._sessionGeneration += 1;
|
|
4553
|
+
this.wake();
|
|
4554
|
+
}
|
|
4555
|
+
onSignOut() {
|
|
4556
|
+
super.onSignOut();
|
|
4557
|
+
this.release();
|
|
4558
|
+
this.wake();
|
|
4559
|
+
}
|
|
4560
|
+
createHookHeaders(action, path) {
|
|
4561
|
+
return this.context.invoke(this.session, "hooks", path, action);
|
|
4149
4562
|
}
|
|
4150
4563
|
createInvokeHeaders(subscriptions) {
|
|
4151
4564
|
const entries = subscriptions.map((subscription) => ({
|
|
@@ -4176,11 +4589,168 @@ function buildScopePath(service, pathPrefix) {
|
|
|
4176
4589
|
const normalized = normalizePathPrefix(pathPrefix);
|
|
4177
4590
|
return normalized ? `${service}/${normalized}` : service;
|
|
4178
4591
|
}
|
|
4592
|
+
function hookStreamRetryDelay(attempt) {
|
|
4593
|
+
const cap = Math.min(
|
|
4594
|
+
HOOK_STREAM_RETRY_BASE_DELAY_MS * 2 ** Math.max(0, attempt - 1),
|
|
4595
|
+
HOOK_STREAM_RETRY_MAX_DELAY_MS
|
|
4596
|
+
);
|
|
4597
|
+
return cap / 2 + Math.random() * (cap / 2);
|
|
4598
|
+
}
|
|
4599
|
+
function isTerminalMintRefusal(status) {
|
|
4600
|
+
return typeof status === "number" && status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
4601
|
+
}
|
|
4602
|
+
var SUPERSEDED = /* @__PURE__ */ Symbol("hooks attempt superseded");
|
|
4603
|
+
async function awaitOwned(signal, operation) {
|
|
4604
|
+
if (signal.aborted) {
|
|
4605
|
+
if (typeof operation !== "function") {
|
|
4606
|
+
void operation.then(cancelLateResponse, () => void 0);
|
|
4607
|
+
}
|
|
4608
|
+
throw SUPERSEDED;
|
|
4609
|
+
}
|
|
4610
|
+
const promise = typeof operation === "function" ? operation() : operation;
|
|
4611
|
+
return new Promise((resolve, reject) => {
|
|
4612
|
+
let settled = false;
|
|
4613
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
4614
|
+
const onAbort = () => {
|
|
4615
|
+
if (settled) return;
|
|
4616
|
+
settled = true;
|
|
4617
|
+
cleanup();
|
|
4618
|
+
reject(SUPERSEDED);
|
|
4619
|
+
};
|
|
4620
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4621
|
+
promise.then(
|
|
4622
|
+
(value) => {
|
|
4623
|
+
if (settled) {
|
|
4624
|
+
cancelLateResponse(value);
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
settled = true;
|
|
4628
|
+
cleanup();
|
|
4629
|
+
resolve(value);
|
|
4630
|
+
},
|
|
4631
|
+
(error) => {
|
|
4632
|
+
if (settled) return;
|
|
4633
|
+
settled = true;
|
|
4634
|
+
cleanup();
|
|
4635
|
+
reject(error);
|
|
4636
|
+
}
|
|
4637
|
+
);
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
function cancelLateResponse(value) {
|
|
4641
|
+
if (!(value instanceof Response) || !value.body) return;
|
|
4642
|
+
try {
|
|
4643
|
+
const cancellation = value.body.cancel();
|
|
4644
|
+
void cancellation.then(() => void 0, () => void 0);
|
|
4645
|
+
} catch {
|
|
4646
|
+
}
|
|
4647
|
+
}
|
|
4648
|
+
function hookStreamRefusal(status) {
|
|
4649
|
+
const code = typeof status === "number" ? status : void 0;
|
|
4650
|
+
return serviceError(
|
|
4651
|
+
ErrorCodes.NETWORK_ERROR,
|
|
4652
|
+
`hook stream open refused: ${code ?? "unknown status"}`,
|
|
4653
|
+
"hooks",
|
|
4654
|
+
{ meta: { phase: "open", status: code } }
|
|
4655
|
+
);
|
|
4656
|
+
}
|
|
4657
|
+
var STREAM_ERROR_NAMES = /* @__PURE__ */ new Set([
|
|
4658
|
+
"Error",
|
|
4659
|
+
"TypeError",
|
|
4660
|
+
"AbortError",
|
|
4661
|
+
"TimeoutError",
|
|
4662
|
+
"SyntaxError",
|
|
4663
|
+
"RangeError",
|
|
4664
|
+
"NetworkError"
|
|
4665
|
+
]);
|
|
4666
|
+
function hookStreamFailure(phase, thrown) {
|
|
4667
|
+
const candidateName = readProperty(thrown, "name");
|
|
4668
|
+
const name = typeof candidateName === "string" && STREAM_ERROR_NAMES.has(candidateName) ? candidateName : "Error";
|
|
4669
|
+
const code = identifier(readProperty(thrown, "code")) ?? identifier(readProperty(readProperty(thrown, "cause"), "code"));
|
|
4670
|
+
return serviceError(
|
|
4671
|
+
ErrorCodes.NETWORK_ERROR,
|
|
4672
|
+
`hook stream ${phase} failed: ${name}${code ? ` (${code})` : ""}`,
|
|
4673
|
+
"hooks",
|
|
4674
|
+
{ meta: { phase, errorName: name, ...code ? { errorCode: code } : {} } }
|
|
4675
|
+
);
|
|
4676
|
+
}
|
|
4677
|
+
var STREAM_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
4678
|
+
"ECONNREFUSED",
|
|
4679
|
+
"ECONNRESET",
|
|
4680
|
+
"ECONNABORTED",
|
|
4681
|
+
"ETIMEDOUT",
|
|
4682
|
+
"EPIPE",
|
|
4683
|
+
"ENOTFOUND",
|
|
4684
|
+
"EAI_AGAIN",
|
|
4685
|
+
"EHOSTUNREACH",
|
|
4686
|
+
"ENETUNREACH",
|
|
4687
|
+
"ENETDOWN",
|
|
4688
|
+
"EADDRNOTAVAIL",
|
|
4689
|
+
"UND_ERR_SOCKET",
|
|
4690
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
4691
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
4692
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
4693
|
+
"UND_ERR_CLOSED",
|
|
4694
|
+
"UND_ERR_ABORTED",
|
|
4695
|
+
"UND_ERR_RESPONSE_STATUS_CODE",
|
|
4696
|
+
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
4697
|
+
"CERT_HAS_EXPIRED",
|
|
4698
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
4699
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
4700
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
4701
|
+
"ERR_SSL_WRONG_VERSION_NUMBER"
|
|
4702
|
+
]);
|
|
4703
|
+
function identifier(value) {
|
|
4704
|
+
return typeof value === "string" && STREAM_ERROR_CODES.has(value) ? value : void 0;
|
|
4705
|
+
}
|
|
4706
|
+
function readProperty(value, key) {
|
|
4707
|
+
if (value === null || typeof value !== "object" && typeof value !== "function") {
|
|
4708
|
+
return void 0;
|
|
4709
|
+
}
|
|
4710
|
+
try {
|
|
4711
|
+
return Reflect.get(value, key);
|
|
4712
|
+
} catch {
|
|
4713
|
+
return void 0;
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
function sleep(delayMs, _attempt, signal) {
|
|
4717
|
+
return new Promise((resolve) => {
|
|
4718
|
+
const finish = () => {
|
|
4719
|
+
clearTimeout(timer);
|
|
4720
|
+
signal.removeEventListener("abort", stop);
|
|
4721
|
+
resolve();
|
|
4722
|
+
};
|
|
4723
|
+
const timer = setTimeout(finish, delayMs);
|
|
4724
|
+
timer.unref?.();
|
|
4725
|
+
const stop = () => finish();
|
|
4726
|
+
if (signal.aborted) {
|
|
4727
|
+
finish();
|
|
4728
|
+
} else {
|
|
4729
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
4730
|
+
}
|
|
4731
|
+
});
|
|
4732
|
+
}
|
|
4733
|
+
function abortPromise(signal) {
|
|
4734
|
+
let resolve;
|
|
4735
|
+
const promise = new Promise((done) => {
|
|
4736
|
+
resolve = done;
|
|
4737
|
+
});
|
|
4738
|
+
const onAbort = () => resolve();
|
|
4739
|
+
if (signal.aborted) {
|
|
4740
|
+
resolve();
|
|
4741
|
+
} else {
|
|
4742
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4743
|
+
}
|
|
4744
|
+
return {
|
|
4745
|
+
promise,
|
|
4746
|
+
dispose: () => signal.removeEventListener("abort", onAbort)
|
|
4747
|
+
};
|
|
4748
|
+
}
|
|
4179
4749
|
function normalizeSubscription(subscription) {
|
|
4180
4750
|
return {
|
|
4181
4751
|
...subscription,
|
|
4182
4752
|
pathPrefix: normalizePathPrefix(subscription.pathPrefix),
|
|
4183
|
-
abilities: subscription.abilities
|
|
4753
|
+
abilities: subscription.abilities ? [...subscription.abilities] : []
|
|
4184
4754
|
};
|
|
4185
4755
|
}
|
|
4186
4756
|
function subscriptionSignature(subscription) {
|
|
@@ -4246,9 +4816,6 @@ async function responseError(service, message, response) {
|
|
|
4246
4816
|
meta: { ...error.meta, status: response.status, statusText: response.statusText }
|
|
4247
4817
|
};
|
|
4248
4818
|
}
|
|
4249
|
-
function isAbortError(error) {
|
|
4250
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
4251
|
-
}
|
|
4252
4819
|
async function* parseSseStream(body, signal) {
|
|
4253
4820
|
if (!body) {
|
|
4254
4821
|
throw new Error("Hook stream response does not expose a readable body");
|
|
@@ -4277,11 +4844,24 @@ async function* parseSseStream(body, signal) {
|
|
|
4277
4844
|
async function* readBodyChunks(body, signal) {
|
|
4278
4845
|
const asyncIterable = body;
|
|
4279
4846
|
if (typeof asyncIterable?.[Symbol.asyncIterator] === "function") {
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4847
|
+
const iterator = asyncIterable[Symbol.asyncIterator]();
|
|
4848
|
+
const cancel = () => {
|
|
4849
|
+
try {
|
|
4850
|
+
const closing = iterator.return?.();
|
|
4851
|
+
if (closing) void Promise.resolve(closing).then(() => void 0, () => void 0);
|
|
4852
|
+
} catch {
|
|
4283
4853
|
}
|
|
4284
|
-
|
|
4854
|
+
};
|
|
4855
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
4856
|
+
try {
|
|
4857
|
+
while (!signal?.aborted) {
|
|
4858
|
+
const next = await iterator.next();
|
|
4859
|
+
if (next.done) break;
|
|
4860
|
+
yield next.value;
|
|
4861
|
+
}
|
|
4862
|
+
} finally {
|
|
4863
|
+
signal?.removeEventListener("abort", cancel);
|
|
4864
|
+
cancel();
|
|
4285
4865
|
}
|
|
4286
4866
|
return;
|
|
4287
4867
|
}
|
|
@@ -4290,21 +4870,23 @@ async function* readBodyChunks(body, signal) {
|
|
|
4290
4870
|
throw new Error("Unsupported hook stream body type");
|
|
4291
4871
|
}
|
|
4292
4872
|
const reader = stream.getReader();
|
|
4873
|
+
const cancelReader = () => {
|
|
4874
|
+
try {
|
|
4875
|
+
const cancellation = reader.cancel?.();
|
|
4876
|
+
if (cancellation) void cancellation.then(() => void 0, () => void 0);
|
|
4877
|
+
} catch {
|
|
4878
|
+
}
|
|
4879
|
+
};
|
|
4880
|
+
signal?.addEventListener("abort", cancelReader, { once: true });
|
|
4293
4881
|
try {
|
|
4294
4882
|
while (!signal?.aborted) {
|
|
4295
4883
|
const { done, value } = await reader.read();
|
|
4296
|
-
if (done)
|
|
4297
|
-
|
|
4298
|
-
}
|
|
4299
|
-
if (value) {
|
|
4300
|
-
yield value;
|
|
4301
|
-
}
|
|
4884
|
+
if (done) break;
|
|
4885
|
+
if (value) yield value;
|
|
4302
4886
|
}
|
|
4303
4887
|
} finally {
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
} catch {
|
|
4307
|
-
}
|
|
4888
|
+
signal?.removeEventListener("abort", cancelReader);
|
|
4889
|
+
cancelReader();
|
|
4308
4890
|
reader.releaseLock?.();
|
|
4309
4891
|
}
|
|
4310
4892
|
}
|
|
@@ -6139,6 +6721,7 @@ var SecretsService = class {
|
|
|
6139
6721
|
}
|
|
6140
6722
|
async listAll() {
|
|
6141
6723
|
const entries = /* @__PURE__ */ new Map();
|
|
6724
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
6142
6725
|
let cursor;
|
|
6143
6726
|
do {
|
|
6144
6727
|
const result = await this.vault.listPage({
|
|
@@ -6161,6 +6744,14 @@ var SecretsService = class {
|
|
|
6161
6744
|
"Secret catalog listing was truncated without a continuation cursor."
|
|
6162
6745
|
);
|
|
6163
6746
|
}
|
|
6747
|
+
if (cursor !== void 0) {
|
|
6748
|
+
if (seenCursors.has(cursor)) {
|
|
6749
|
+
return invalidSecretInput(
|
|
6750
|
+
"Secret catalog listing repeated a continuation cursor; the node does not support paged KV lists."
|
|
6751
|
+
);
|
|
6752
|
+
}
|
|
6753
|
+
seenCursors.add(cursor);
|
|
6754
|
+
}
|
|
6164
6755
|
} while (cursor !== void 0);
|
|
6165
6756
|
return {
|
|
6166
6757
|
ok: true,
|
|
@@ -6939,10 +7530,10 @@ var EncryptionService = class extends BaseService {
|
|
|
6939
7530
|
get crypto() {
|
|
6940
7531
|
return this._config.crypto;
|
|
6941
7532
|
}
|
|
6942
|
-
async discoverNetwork(
|
|
7533
|
+
async discoverNetwork(identifier2, ownerDid) {
|
|
6943
7534
|
this.assertActive();
|
|
6944
7535
|
const result = await discoverNetwork({
|
|
6945
|
-
identifier,
|
|
7536
|
+
identifier: identifier2,
|
|
6946
7537
|
...ownerDid !== void 0 ? { ownerDid } : {},
|
|
6947
7538
|
...this._config.node !== void 0 ? { node: this._config.node } : {},
|
|
6948
7539
|
...this._config.wellKnown !== void 0 ? { wellKnown: this._config.wellKnown } : {}
|