@fulldivevr/workcord-cli 1.1.1 → 1.2.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/README.md +115 -2
- package/dist/auth.js +43 -0
- package/dist/cli.js +2 -0
- package/dist/client.js +6 -71
- package/dist/commands/hermes.js +644 -0
- package/dist/config.js +24 -0
- package/dist/generated/commands/custom-agents.js +14 -0
- package/dist/generated/commands/health.js +29 -0
- package/dist/generated/commands/live-kit-webhook.js +20 -0
- package/dist/generated/commands/room-call.js +33 -0
- package/dist/generated/commands/rooms.js +24 -0
- package/dist/generated/index.js +4 -0
- package/dist/hermes-client.js +87 -0
- package/dist/hermes-guards.js +70 -0
- package/dist/http.js +68 -0
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -12,6 +12,7 @@ export async function readConfig() {
|
|
|
12
12
|
return {
|
|
13
13
|
baseUrl: typeof config.baseUrl === "string" ? config.baseUrl : undefined,
|
|
14
14
|
token: typeof config.token === "string" ? config.token : undefined,
|
|
15
|
+
hermes: readHermes(config.hermes),
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
catch (err) {
|
|
@@ -20,6 +21,29 @@ export async function readConfig() {
|
|
|
20
21
|
throw new Error(`Could not read ${configPath}: ${err.message}`);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
24
|
+
function readHermes(value) {
|
|
25
|
+
if (!value || typeof value !== "object")
|
|
26
|
+
return undefined;
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const [ref, raw] of Object.entries(value)) {
|
|
29
|
+
if (!raw || typeof raw !== "object")
|
|
30
|
+
continue;
|
|
31
|
+
const entry = raw;
|
|
32
|
+
if (typeof entry.key !== "string" || typeof entry.hubBaseUrl !== "string")
|
|
33
|
+
continue;
|
|
34
|
+
out[ref] = {
|
|
35
|
+
key: entry.key,
|
|
36
|
+
tenantId: typeof entry.tenantId === "string" ? entry.tenantId : "",
|
|
37
|
+
hubBaseUrl: entry.hubBaseUrl,
|
|
38
|
+
revealedAt: typeof entry.revealedAt === "string" ? entry.revealedAt : "",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return Object.keys(out).length ? out : undefined;
|
|
42
|
+
}
|
|
43
|
+
export async function saveHermesEntry(agentRef, entry) {
|
|
44
|
+
const current = await readConfig();
|
|
45
|
+
await saveConfig({ hermes: { ...current.hermes, [agentRef]: entry } });
|
|
46
|
+
}
|
|
23
47
|
export async function saveConfig(update) {
|
|
24
48
|
const current = await readConfig();
|
|
25
49
|
const next = { ...current, ...update };
|
|
@@ -232,6 +232,20 @@ export function register(program) {
|
|
|
232
232
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
233
233
|
});
|
|
234
234
|
}
|
|
235
|
+
if (!hasSubcommand(group, "reveal-hermes-cli-key")) {
|
|
236
|
+
group
|
|
237
|
+
.command("reveal-hermes-cli-key")
|
|
238
|
+
.description("POST /api/v1/agents/{id}/hermes-cli-key/reveal")
|
|
239
|
+
.requiredOption("--id <value>", "id (path param)")
|
|
240
|
+
.option("--body <json>", "request body as JSON (no schema available)")
|
|
241
|
+
.action(async (opts) => {
|
|
242
|
+
const as = actingUserId(program);
|
|
243
|
+
const path = `/api/v1/agents/${encodeURIComponent(opts.id)}/hermes-cli-key/reveal`;
|
|
244
|
+
const body = opts.body ? JSON.parse(opts.body) : undefined;
|
|
245
|
+
const data = await call({ method: "POST", path, asUserId: as, body });
|
|
246
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
247
|
+
});
|
|
248
|
+
}
|
|
235
249
|
if (!hasSubcommand(group, "install-hermes-flavor-to-room")) {
|
|
236
250
|
group
|
|
237
251
|
.command("install-hermes-flavor-to-room")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { call } from "../../client.js";
|
|
2
|
+
import { actingUserId } from "../../auth.js";
|
|
3
|
+
import { getOrCreateGroup, hasSubcommand } from "../_helpers.js";
|
|
4
|
+
export function register(program) {
|
|
5
|
+
const group = getOrCreateGroup(program, "health", "Health operations");
|
|
6
|
+
if (!hasSubcommand(group, "health")) {
|
|
7
|
+
group
|
|
8
|
+
.command("health")
|
|
9
|
+
.description("GET /api/health")
|
|
10
|
+
.action(async (opts) => {
|
|
11
|
+
const as = actingUserId(program);
|
|
12
|
+
const path = `/api/health`;
|
|
13
|
+
const data = await call({ method: "GET", path, asUserId: as });
|
|
14
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
if (!hasSubcommand(group, "hello")) {
|
|
18
|
+
group
|
|
19
|
+
.command("hello")
|
|
20
|
+
.description("GET /api/hello")
|
|
21
|
+
.action(async (opts) => {
|
|
22
|
+
const as = actingUserId(program);
|
|
23
|
+
const path = `/api/hello`;
|
|
24
|
+
const data = await call({ method: "GET", path, asUserId: as });
|
|
25
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { call } from "../../client.js";
|
|
2
|
+
import { actingUserId } from "../../auth.js";
|
|
3
|
+
import { getOrCreateGroup, hasSubcommand } from "../_helpers.js";
|
|
4
|
+
export function register(program) {
|
|
5
|
+
const group = getOrCreateGroup(program, "live-kit-webhook", "LiveKitWebhook operations");
|
|
6
|
+
if (!hasSubcommand(group, "webhook")) {
|
|
7
|
+
group
|
|
8
|
+
.command("webhook")
|
|
9
|
+
.description("POST /api/v1/livekit/webhook")
|
|
10
|
+
.option("--body <json>", "request body as JSON (no schema available)")
|
|
11
|
+
.action(async (opts) => {
|
|
12
|
+
const as = actingUserId(program);
|
|
13
|
+
const path = `/api/v1/livekit/webhook`;
|
|
14
|
+
const body = opts.body ? JSON.parse(opts.body) : undefined;
|
|
15
|
+
const data = await call({ method: "POST", path, asUserId: as, body });
|
|
16
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=live-kit-webhook.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { call } from "../../client.js";
|
|
2
|
+
import { actingUserId } from "../../auth.js";
|
|
3
|
+
import { getOrCreateGroup, hasSubcommand } from "../_helpers.js";
|
|
4
|
+
export function register(program) {
|
|
5
|
+
const group = getOrCreateGroup(program, "room-call", "RoomCall operations");
|
|
6
|
+
if (!hasSubcommand(group, "token")) {
|
|
7
|
+
group
|
|
8
|
+
.command("token")
|
|
9
|
+
.description("POST /api/v1/rooms/{roomId}/call/token")
|
|
10
|
+
.requiredOption("--room-id <value>", "roomId (path param)")
|
|
11
|
+
.option("--body <json>", "request body as JSON (no schema available)")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
const as = actingUserId(program);
|
|
14
|
+
const path = `/api/v1/rooms/${encodeURIComponent(opts.roomId)}/call/token`;
|
|
15
|
+
const body = opts.body ? JSON.parse(opts.body) : undefined;
|
|
16
|
+
const data = await call({ method: "POST", path, asUserId: as, body });
|
|
17
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (!hasSubcommand(group, "state")) {
|
|
21
|
+
group
|
|
22
|
+
.command("state")
|
|
23
|
+
.description("GET /api/v1/rooms/{roomId}/call")
|
|
24
|
+
.requiredOption("--room-id <value>", "roomId (path param)")
|
|
25
|
+
.action(async (opts) => {
|
|
26
|
+
const as = actingUserId(program);
|
|
27
|
+
const path = `/api/v1/rooms/${encodeURIComponent(opts.roomId)}/call`;
|
|
28
|
+
const data = await call({ method: "GET", path, asUserId: as });
|
|
29
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=room-call.js.map
|
|
@@ -237,6 +237,30 @@ export function register(program) {
|
|
|
237
237
|
process.stdout.write(JSON.stringify(data) + "\n");
|
|
238
238
|
});
|
|
239
239
|
}
|
|
240
|
+
if (!hasSubcommand(group, "list-agent-install-targets")) {
|
|
241
|
+
group
|
|
242
|
+
.command("list-agent-install-targets")
|
|
243
|
+
.description("GET /api/v1/rooms/agent-install-targets")
|
|
244
|
+
.option("--cursor <value>", "string")
|
|
245
|
+
.option("--limit <value>", "string")
|
|
246
|
+
.action(async (opts) => {
|
|
247
|
+
const as = actingUserId(program);
|
|
248
|
+
const path = `/api/v1/rooms/agent-install-targets`;
|
|
249
|
+
const query = {};
|
|
250
|
+
{
|
|
251
|
+
const v = opts.cursor;
|
|
252
|
+
if (v !== undefined)
|
|
253
|
+
query["cursor"] = v;
|
|
254
|
+
}
|
|
255
|
+
{
|
|
256
|
+
const v = opts.limit;
|
|
257
|
+
if (v !== undefined)
|
|
258
|
+
query["limit"] = v;
|
|
259
|
+
}
|
|
260
|
+
const data = await call({ method: "GET", path, asUserId: as, query });
|
|
261
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
262
|
+
});
|
|
263
|
+
}
|
|
240
264
|
if (!hasSubcommand(group, "list-channel-templates")) {
|
|
241
265
|
group
|
|
242
266
|
.command("list-channel-templates")
|
package/dist/generated/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { register as register_invite_redirect } from "./commands/invite-redirect
|
|
|
25
25
|
import { register as register_jesuscord_admin } from "./commands/jesuscord-admin.js";
|
|
26
26
|
import { register as register_join } from "./commands/join.js";
|
|
27
27
|
import { register as register_kits } from "./commands/kits.js";
|
|
28
|
+
import { register as register_live_kit_webhook } from "./commands/live-kit-webhook.js";
|
|
28
29
|
import { register as register_meetings } from "./commands/meetings.js";
|
|
29
30
|
import { register as register_message_folders } from "./commands/message-folders.js";
|
|
30
31
|
import { register as register_messages } from "./commands/messages.js";
|
|
@@ -40,6 +41,7 @@ import { register as register_referrals } from "./commands/referrals.js";
|
|
|
40
41
|
import { register as register_reports } from "./commands/reports.js";
|
|
41
42
|
import { register as register_room_a2_a } from "./commands/room-a2-a.js";
|
|
42
43
|
import { register as register_room_agents } from "./commands/room-agents.js";
|
|
44
|
+
import { register as register_room_call } from "./commands/room-call.js";
|
|
43
45
|
import { register as register_room_invites } from "./commands/room-invites.js";
|
|
44
46
|
import { register as register_room_owned_agent } from "./commands/room-owned-agent.js";
|
|
45
47
|
import { register as register_room_page } from "./commands/room-page.js";
|
|
@@ -89,6 +91,7 @@ export function registerAll(program) {
|
|
|
89
91
|
register_jesuscord_admin(program);
|
|
90
92
|
register_join(program);
|
|
91
93
|
register_kits(program);
|
|
94
|
+
register_live_kit_webhook(program);
|
|
92
95
|
register_meetings(program);
|
|
93
96
|
register_message_folders(program);
|
|
94
97
|
register_messages(program);
|
|
@@ -104,6 +107,7 @@ export function registerAll(program) {
|
|
|
104
107
|
register_reports(program);
|
|
105
108
|
register_room_a2_a(program);
|
|
106
109
|
register_room_agents(program);
|
|
110
|
+
register_room_call(program);
|
|
107
111
|
register_room_invites(program);
|
|
108
112
|
register_room_owned_agent(program);
|
|
109
113
|
register_room_page(program);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { applyQuery, fetchWithRetry, httpError, normalizeBaseUrl, safeJson } from "./http.js";
|
|
2
|
+
function buildUrl(hubBaseUrl, path, query) {
|
|
3
|
+
return applyQuery(new URL(normalizeBaseUrl(hubBaseUrl) + path), query);
|
|
4
|
+
}
|
|
5
|
+
function authHeaders(key, headers) {
|
|
6
|
+
return { authorization: `Bearer ${key}`, ...headers };
|
|
7
|
+
}
|
|
8
|
+
async function failure(method, path, res) {
|
|
9
|
+
const text = await res.text();
|
|
10
|
+
throw httpError(`${method} ${path}`, res.status, text ? safeJson(text) : null);
|
|
11
|
+
}
|
|
12
|
+
export async function hermesCall({ hubBaseUrl, key, method = "GET", path, query, body, headers }) {
|
|
13
|
+
const url = buildUrl(hubBaseUrl, path, query);
|
|
14
|
+
const res = await fetchWithRetry(() => ({
|
|
15
|
+
url,
|
|
16
|
+
init: {
|
|
17
|
+
method,
|
|
18
|
+
headers: { "content-type": "application/json", ...authHeaders(key, headers) },
|
|
19
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
return failure(method, path, res);
|
|
24
|
+
const text = await res.text();
|
|
25
|
+
return text ? safeJson(text) : null;
|
|
26
|
+
}
|
|
27
|
+
export async function hermesUpload({ hubBaseUrl, key, method = "POST", path, query, headers, contentType, data }) {
|
|
28
|
+
const url = buildUrl(hubBaseUrl, path, query);
|
|
29
|
+
const res = await fetchWithRetry(() => ({
|
|
30
|
+
url,
|
|
31
|
+
init: { method, headers: { "content-type": contentType, ...authHeaders(key, headers) }, body: data },
|
|
32
|
+
}));
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
return failure(method, path, res);
|
|
35
|
+
const text = await res.text();
|
|
36
|
+
return text ? safeJson(text) : null;
|
|
37
|
+
}
|
|
38
|
+
export async function hermesBinary({ hubBaseUrl, key, method = "GET", path, query, body, headers, out = process.stdout }) {
|
|
39
|
+
const url = buildUrl(hubBaseUrl, path, query);
|
|
40
|
+
const res = await fetchWithRetry(() => ({
|
|
41
|
+
url,
|
|
42
|
+
init: {
|
|
43
|
+
method,
|
|
44
|
+
headers: authHeaders(key, headers),
|
|
45
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
46
|
+
},
|
|
47
|
+
}));
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
return failure(method, path, res);
|
|
50
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
51
|
+
out.write(buffer);
|
|
52
|
+
}
|
|
53
|
+
export async function hermesStream({ hubBaseUrl, key, method = "GET", path, query, body, headers, lastEventId, out = process.stdout }) {
|
|
54
|
+
const url = buildUrl(hubBaseUrl, path, query);
|
|
55
|
+
const res = await fetchWithRetry(() => ({
|
|
56
|
+
url,
|
|
57
|
+
init: {
|
|
58
|
+
method,
|
|
59
|
+
headers: {
|
|
60
|
+
accept: "text/event-stream",
|
|
61
|
+
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
|
62
|
+
...(lastEventId ? { "last-event-id": lastEventId } : {}),
|
|
63
|
+
...authHeaders(key, headers),
|
|
64
|
+
},
|
|
65
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
66
|
+
},
|
|
67
|
+
}));
|
|
68
|
+
if (!res.ok)
|
|
69
|
+
return failure(method, path, res);
|
|
70
|
+
if (!res.body)
|
|
71
|
+
return;
|
|
72
|
+
const decoder = new TextDecoder();
|
|
73
|
+
let pending = "";
|
|
74
|
+
for await (const chunk of res.body) {
|
|
75
|
+
pending += decoder.decode(chunk, { stream: true });
|
|
76
|
+
let index = pending.indexOf("\n");
|
|
77
|
+
while (index >= 0) {
|
|
78
|
+
out.write(pending.slice(0, index + 1));
|
|
79
|
+
pending = pending.slice(index + 1);
|
|
80
|
+
index = pending.indexOf("\n");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
pending += decoder.decode();
|
|
84
|
+
if (pending)
|
|
85
|
+
out.write(pending + "\n");
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=hermes-client.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export const MAX_ARCHIVE_BYTES = 32 * 1024 * 1024;
|
|
2
|
+
export function rolloutBody(flags) {
|
|
3
|
+
const tenants = flags.tenant ?? [];
|
|
4
|
+
if (flags.allMatching && tenants.length > 0) {
|
|
5
|
+
throw new Error("--all-matching and --tenant are mutually exclusive.");
|
|
6
|
+
}
|
|
7
|
+
if (tenants.length > 0) {
|
|
8
|
+
if (flags.canary) {
|
|
9
|
+
throw new Error("--canary selects the canary cohort of an automatic rollout and cannot be combined with --tenant.");
|
|
10
|
+
}
|
|
11
|
+
return { tenantIds: tenants };
|
|
12
|
+
}
|
|
13
|
+
if (flags.allMatching) {
|
|
14
|
+
if (flags.canary) {
|
|
15
|
+
throw new Error("--canary requires the automatic rollout and cannot be combined with --all-matching.");
|
|
16
|
+
}
|
|
17
|
+
if (!flags.force) {
|
|
18
|
+
throw new Error("--all-matching sends automatic:false, which re-pins EVERY tenant on this hub pinned to this flavor id and ignores each target tenant's own pinned update policy. Re-run with --force to accept that blast radius, drop --all-matching for the policy-respecting rollout, or pass --tenant <id> to target named tenants.");
|
|
19
|
+
}
|
|
20
|
+
return { allMatching: true, automatic: false };
|
|
21
|
+
}
|
|
22
|
+
return { allMatching: true, automatic: true, ...(flags.canary ? { canary: true } : {}) };
|
|
23
|
+
}
|
|
24
|
+
export function compareVersions(a, b) {
|
|
25
|
+
const parse = (v) => v.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
26
|
+
const left = parse(a);
|
|
27
|
+
const right = parse(b);
|
|
28
|
+
const length = Math.max(left.length, right.length);
|
|
29
|
+
for (let i = 0; i < length; i++) {
|
|
30
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
31
|
+
if (diff !== 0)
|
|
32
|
+
return diff < 0 ? -1 : 1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
export function flavorSetWarnings(currentVersion, targetVersion, versions) {
|
|
37
|
+
const warnings = [];
|
|
38
|
+
const target = versions.find((v) => v.version === targetVersion);
|
|
39
|
+
if (target?.deprecated) {
|
|
40
|
+
warnings.push(`flavor version ${targetVersion} is marked deprecated on the hub`);
|
|
41
|
+
}
|
|
42
|
+
if (currentVersion && compareVersions(targetVersion, currentVersion) < 0) {
|
|
43
|
+
warnings.push(`this is a downgrade: ${currentVersion} -> ${targetVersion}`);
|
|
44
|
+
}
|
|
45
|
+
return warnings;
|
|
46
|
+
}
|
|
47
|
+
export function assertArchiveSize(bytes, path) {
|
|
48
|
+
if (bytes > MAX_ARCHIVE_BYTES) {
|
|
49
|
+
throw new Error(`${path} is ${bytes} bytes; the hub rejects packages over ${MAX_ARCHIVE_BYTES} bytes (32 MiB). Trim the package and retry.`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function ownershipError(kind, namespaceId, tenantId, body) {
|
|
53
|
+
const who = tenantId || "this tenant";
|
|
54
|
+
const err = new Error(`Hermes Hub refused this as ${who}: it does not own the ${kind} namespace "${namespaceId}". A WorkCord agent tenant is provisioned as a consumer and owns no ${kind} namespace. Ownership is admin-assigned only (PUT /v1/${kind}s/${namespaceId}/owner) and there is no self-service bootstrap — ask a hub operator to assign "${namespaceId}" to ${who}.`);
|
|
55
|
+
err.status = 403;
|
|
56
|
+
err.body = body;
|
|
57
|
+
return err;
|
|
58
|
+
}
|
|
59
|
+
export async function guardOwnership(kind, namespaceId, tenantId, run) {
|
|
60
|
+
try {
|
|
61
|
+
return await run();
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (err.status === 403) {
|
|
65
|
+
throw ownershipError(kind, namespaceId, tenantId, err.body);
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=hermes-guards.js.map
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function normalizeBaseUrl(raw) {
|
|
2
|
+
const trimmed = raw.replace(/\/+$/, "");
|
|
3
|
+
let parsed;
|
|
4
|
+
try {
|
|
5
|
+
parsed = new URL(trimmed);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
throw new Error(`Invalid API base URL: ${raw}`);
|
|
9
|
+
}
|
|
10
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
11
|
+
throw new Error(`API base URL must be http or https: ${raw}`);
|
|
12
|
+
}
|
|
13
|
+
if (parsed.pathname !== "/" || parsed.search || parsed.hash) {
|
|
14
|
+
throw new Error(`API base URL must be an origin with no path, query or fragment: ${raw}`);
|
|
15
|
+
}
|
|
16
|
+
return parsed.origin;
|
|
17
|
+
}
|
|
18
|
+
const RETRY_MAX = Number(process.env.WORKCORD_RETRY_MAX ?? 3);
|
|
19
|
+
const RETRY_BACKOFF_MS = Number(process.env.WORKCORD_RETRY_BACKOFF_MS ?? 200);
|
|
20
|
+
const RETRY_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
21
|
+
export async function fetchWithRetry(build) {
|
|
22
|
+
let lastErr;
|
|
23
|
+
for (let attempt = 0; attempt <= RETRY_MAX; attempt++) {
|
|
24
|
+
try {
|
|
25
|
+
const { url, init } = build();
|
|
26
|
+
const res = await fetch(url, init);
|
|
27
|
+
if (!RETRY_STATUSES.has(res.status))
|
|
28
|
+
return res;
|
|
29
|
+
lastErr = res;
|
|
30
|
+
if (attempt >= RETRY_MAX)
|
|
31
|
+
return res;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
lastErr = err;
|
|
35
|
+
if (attempt >= RETRY_MAX)
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
38
|
+
const delay = RETRY_BACKOFF_MS * Math.pow(3, attempt);
|
|
39
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
40
|
+
}
|
|
41
|
+
if (lastErr instanceof Response)
|
|
42
|
+
return lastErr;
|
|
43
|
+
throw lastErr;
|
|
44
|
+
}
|
|
45
|
+
export function safeJson(s) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(s);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return s;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function httpError(label, status, body) {
|
|
54
|
+
const err = new Error(`${label} -> ${status}`);
|
|
55
|
+
err.status = status;
|
|
56
|
+
err.body = body;
|
|
57
|
+
return err;
|
|
58
|
+
}
|
|
59
|
+
export function applyQuery(url, query) {
|
|
60
|
+
if (!query)
|
|
61
|
+
return url;
|
|
62
|
+
for (const [k, v] of Object.entries(query)) {
|
|
63
|
+
if (v !== undefined && v !== null)
|
|
64
|
+
url.searchParams.set(k, String(v));
|
|
65
|
+
}
|
|
66
|
+
return url;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=http.js.map
|