@ccmsg/cli 0.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/LICENSE +21 -0
- package/README.md +23 -0
- package/package.json +32 -0
- package/src/cli.ts +1074 -0
- package/src/daemon/control.ts +88 -0
- package/src/daemon/index.ts +6 -0
- package/src/daemon/link.ts +93 -0
- package/src/daemon/log.ts +116 -0
- package/src/daemon/registry.ts +285 -0
- package/src/daemon/snapshot.ts +115 -0
- package/src/daemon/supervise.ts +446 -0
- package/src/dispatch/caller.ts +47 -0
- package/src/dispatch/dispatch.ts +128 -0
- package/src/dispatch/handler.ts +55 -0
- package/src/dispatch/identity.ts +22 -0
- package/src/dispatch/index.ts +5 -0
- package/src/dispatch/result.ts +58 -0
- package/src/files/containment.ts +263 -0
- package/src/files/files.ts +421 -0
- package/src/files/index.ts +14 -0
- package/src/files/sandbox.ts +0 -0
- package/src/greeting/hook.ts +48 -0
- package/src/greeting/index.ts +2 -0
- package/src/greeting/meta.ts +66 -0
- package/src/instance/config.ts +424 -0
- package/src/instance/handlers.ts +28 -0
- package/src/instance/identity.ts +44 -0
- package/src/instance/index.ts +8 -0
- package/src/instance/instance.ts +911 -0
- package/src/instance/lock.ts +108 -0
- package/src/instance/log.ts +30 -0
- package/src/instance/paths.ts +200 -0
- package/src/instance/socket.ts +62 -0
- package/src/kv/index.ts +2 -0
- package/src/kv/merge.ts +66 -0
- package/src/kv/store.ts +195 -0
- package/src/launcher/index.ts +4 -0
- package/src/launcher/launcher.ts +190 -0
- package/src/launcher/roots.ts +32 -0
- package/src/launcher/spawn.ts +81 -0
- package/src/launcher/tree.ts +80 -0
- package/src/mesh/index.ts +5 -0
- package/src/mesh/keys.ts +158 -0
- package/src/mesh/mesh.ts +1169 -0
- package/src/mesh/probe.ts +100 -0
- package/src/mesh/relay.ts +147 -0
- package/src/mesh/wire.ts +96 -0
- package/src/messaging/delivery.ts +375 -0
- package/src/messaging/direct.ts +433 -0
- package/src/messaging/handlers.ts +14 -0
- package/src/messaging/inbox.ts +191 -0
- package/src/messaging/index.ts +5 -0
- package/src/messaging/notify.ts +117 -0
- package/src/plugin/claude.ts +148 -0
- package/src/plugin/index.ts +13 -0
- package/src/plugin/install.ts +416 -0
- package/src/service/index.ts +1 -0
- package/src/service/service.ts +359 -0
- package/src/sessions/classify.ts +66 -0
- package/src/sessions/dump.ts +105 -0
- package/src/sessions/fork.ts +127 -0
- package/src/sessions/handlers.ts +158 -0
- package/src/sessions/harness.ts +167 -0
- package/src/sessions/index.ts +26 -0
- package/src/sessions/last-live.ts +111 -0
- package/src/sessions/processes.ts +413 -0
- package/src/sessions/registry.ts +785 -0
- package/src/sessions/search.ts +278 -0
- package/src/sessions/status.ts +209 -0
- package/src/sessions/terminals.ts +72 -0
- package/src/sessions/workspace.ts +140 -0
- package/src/topics/handlers.ts +42 -0
- package/src/topics/index.ts +2 -0
- package/src/topics/topics.ts +290 -0
- package/src/transcript/files.ts +201 -0
- package/src/transcript/fold.ts +833 -0
- package/src/transcript/index.ts +16 -0
- package/src/transcript/read.ts +82 -0
- package/src/transcript/tail.ts +195 -0
- package/src/transcript/transcripts.ts +162 -0
- package/src/translate/helper.ts +87 -0
- package/src/translate/index.ts +2 -0
- package/src/translate/translate.ts +127 -0
- package/src/transport/conn.ts +129 -0
- package/src/transport/dial.ts +65 -0
- package/src/transport/driver.ts +102 -0
- package/src/transport/entry.ts +39 -0
- package/src/transport/framing.ts +131 -0
- package/src/transport/index.ts +8 -0
- package/src/transport/listener.ts +39 -0
- package/src/transport/uds.ts +88 -0
- package/src/transport/ws.ts +170 -0
- package/src/upstream/events.ts +125 -0
- package/src/upstream/gateway.ts +275 -0
- package/src/upstream/index.ts +8 -0
- package/src/upstream/json.ts +81 -0
- package/src/upstream/requests.ts +234 -0
- package/src/upstream/stats.ts +99 -0
- package/src/upstream/status.ts +281 -0
- package/src/upstream/usage.ts +208 -0
- package/src/upstream/webhook.ts +141 -0
- package/src/version.ts +8 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
LlmUsageAuth,
|
|
3
|
+
LlmUsageCredential,
|
|
4
|
+
LlmUsageLimit,
|
|
5
|
+
LlmUsageOverage,
|
|
6
|
+
LlmUsageReadArgs,
|
|
7
|
+
LlmUsageReadResult,
|
|
8
|
+
LlmUsageSnapshot,
|
|
9
|
+
LlmUsageWindow,
|
|
10
|
+
} from "@ccmsg/protocol";
|
|
11
|
+
import { OpError } from "../dispatch/index.ts";
|
|
12
|
+
import {
|
|
13
|
+
fetchJson,
|
|
14
|
+
objectOf,
|
|
15
|
+
optionalFlag,
|
|
16
|
+
optionalInstant,
|
|
17
|
+
optionalInteger,
|
|
18
|
+
optionalText,
|
|
19
|
+
} from "./json.ts";
|
|
20
|
+
|
|
21
|
+
/** A handful of credentials. Past this the address is pointing at something
|
|
22
|
+
* else entirely, and the whole document is held to be parsed. */
|
|
23
|
+
const MAX_BYTES = 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/** The gateway answers a cached read from a snapshot it keeps. */
|
|
26
|
+
const TIMEOUT_MS = 10_000;
|
|
27
|
+
|
|
28
|
+
/** A probe waits on every provider in turn rather than on the gateway's own
|
|
29
|
+
* cache, so it is legitimately slower — wide enough that one slow provider does
|
|
30
|
+
* not lose the whole answer. */
|
|
31
|
+
const PROBE_TIMEOUT_MS = 60_000;
|
|
32
|
+
|
|
33
|
+
export interface UsageDeps {
|
|
34
|
+
/** Where the gateway answers, already joined to its usage path. */
|
|
35
|
+
readonly url: string;
|
|
36
|
+
readonly fetch?: typeof fetch;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Ask the gateway what quota each credential has left.
|
|
40
|
+
*
|
|
41
|
+
* A failed read is the op's failure, unlike the status report's: there is no
|
|
42
|
+
* last-good reading held here to fall back on, and answering with an empty list
|
|
43
|
+
* of credentials would read as "this host has none". */
|
|
44
|
+
export async function readUsage(
|
|
45
|
+
deps: UsageDeps,
|
|
46
|
+
args: LlmUsageReadArgs,
|
|
47
|
+
): Promise<LlmUsageReadResult> {
|
|
48
|
+
const refresh = args.refresh === true;
|
|
49
|
+
const url = refresh ? withQuery(deps.url, "refresh", "true") : deps.url;
|
|
50
|
+
let document: unknown;
|
|
51
|
+
try {
|
|
52
|
+
document = await fetchJson(url, {
|
|
53
|
+
timeoutMs: refresh ? PROBE_TIMEOUT_MS : TIMEOUT_MS,
|
|
54
|
+
maxBytes: MAX_BYTES,
|
|
55
|
+
...(deps.fetch === undefined ? {} : { fetch: deps.fetch }),
|
|
56
|
+
});
|
|
57
|
+
} catch (cause) {
|
|
58
|
+
throw new OpError("internal_error", `the gateway's usage could not be read: ${String(cause)}`);
|
|
59
|
+
}
|
|
60
|
+
const usage = usageOf(document, deps.url);
|
|
61
|
+
if (usage === undefined) {
|
|
62
|
+
throw new OpError("internal_error", "the gateway's usage could not be understood");
|
|
63
|
+
}
|
|
64
|
+
return usage;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Read the gateway's document as this contract's answer (§3.5).
|
|
68
|
+
*
|
|
69
|
+
* The names that differ are the two the gateway spells its own way — `reset`
|
|
70
|
+
* and `window_seconds` — and both are already Unix ms and seconds, so the
|
|
71
|
+
* conversion is the name alone. Everything else is taken at the type this
|
|
72
|
+
* contract states and is otherwise absent: an open vocabulary travels as sent,
|
|
73
|
+
* because a verdict this daemon does not recognise has to reach the screen as
|
|
74
|
+
* itself. */
|
|
75
|
+
export function usageOf(value: unknown, base?: string): LlmUsageReadResult | undefined {
|
|
76
|
+
const raw = objectOf(value);
|
|
77
|
+
if (raw === undefined || !Array.isArray(raw["credentials"])) return undefined;
|
|
78
|
+
return {
|
|
79
|
+
...optionalInstant("generated_at", raw["generated_at"]),
|
|
80
|
+
credentials: raw["credentials"].flatMap((entry) => credentialOf(entry, base) ?? []),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A credential with no name cannot be told from its neighbours, so it is
|
|
85
|
+
* dropped; every other field degrades to absent and the row still renders. */
|
|
86
|
+
function credentialOf(value: unknown, base?: string): LlmUsageCredential | undefined {
|
|
87
|
+
const raw = objectOf(value);
|
|
88
|
+
const name = raw?.["name"];
|
|
89
|
+
if (raw === undefined || typeof name !== "string") return undefined;
|
|
90
|
+
const auth = authOf(raw["auth"], base);
|
|
91
|
+
const snapshot = snapshotOf(raw["snapshot"]);
|
|
92
|
+
const limits = Array.isArray(raw["limits"])
|
|
93
|
+
? raw["limits"].flatMap((limit) => limitOf(limit) ?? [])
|
|
94
|
+
: [];
|
|
95
|
+
return {
|
|
96
|
+
name,
|
|
97
|
+
...optionalText("type", raw["type"]),
|
|
98
|
+
support: typeof raw["support"] === "string" ? raw["support"] : "unknown",
|
|
99
|
+
...(auth === undefined ? {} : { auth }),
|
|
100
|
+
...(snapshot === undefined ? {} : { snapshot }),
|
|
101
|
+
...(limits.length === 0 ? {} : { limits }),
|
|
102
|
+
...optionalText("probe_error", raw["probe_error"]),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Only `status` is required: a reading with no word for what it is would have
|
|
107
|
+
* to be shown as a state this daemon invented. */
|
|
108
|
+
function authOf(value: unknown, base?: string): LlmUsageAuth | undefined {
|
|
109
|
+
const raw = objectOf(value);
|
|
110
|
+
const status = raw?.["status"];
|
|
111
|
+
if (raw === undefined || typeof status !== "string") return undefined;
|
|
112
|
+
const login = loginUrl(raw["login_path"], base);
|
|
113
|
+
return {
|
|
114
|
+
status,
|
|
115
|
+
...optionalText("reason", raw["reason"]),
|
|
116
|
+
...optionalInstant("observed_at", raw["observed_at"]),
|
|
117
|
+
...(login === undefined ? {} : { login_url: login }),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The gateway states a path rather than an address, because it does not know
|
|
122
|
+
* the one it is published under. It is resolved against the endpoint this
|
|
123
|
+
* instance fetched from, and only that origin is kept.
|
|
124
|
+
*
|
|
125
|
+
* Design rationale: the link is rendered in a person's browser, so a gateway —
|
|
126
|
+
* or something answering in its place — that named another host would be
|
|
127
|
+
* planting a "log in here" button pointing off it. Dropping such a link costs a
|
|
128
|
+
* button; following one could cost a credential. */
|
|
129
|
+
function loginUrl(path: unknown, base?: string): string | undefined {
|
|
130
|
+
if (typeof path !== "string" || base === undefined) return undefined;
|
|
131
|
+
try {
|
|
132
|
+
const origin = new URL(base);
|
|
133
|
+
const resolved = new URL(path, origin);
|
|
134
|
+
return resolved.origin === origin.origin ? resolved.toString() : undefined;
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function overageOf(value: unknown): LlmUsageOverage | undefined {
|
|
141
|
+
const raw = objectOf(value);
|
|
142
|
+
const status = raw?.["status"];
|
|
143
|
+
if (raw === undefined || typeof status !== "string") return undefined;
|
|
144
|
+
return { status, ...optionalText("disabled_reason", raw["disabled_reason"]) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The windows are the snapshot's own keys, beside the two fields that are not
|
|
148
|
+
* windows. Read by shape rather than against a list of names, so a provider
|
|
149
|
+
* that starts reporting a third window needs no change here — and an unrelated
|
|
150
|
+
* key is dropped instead of drawn as an empty bar. */
|
|
151
|
+
function snapshotOf(value: unknown): LlmUsageSnapshot | undefined {
|
|
152
|
+
const raw = objectOf(value);
|
|
153
|
+
if (raw === undefined) return undefined;
|
|
154
|
+
const windows: Record<string, LlmUsageWindow> = {};
|
|
155
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
156
|
+
if (name === "observed_at" || name === "overage") continue;
|
|
157
|
+
const window = windowOf(entry);
|
|
158
|
+
if (window !== undefined) windows[name] = window;
|
|
159
|
+
}
|
|
160
|
+
const overage = overageOf(raw["overage"]);
|
|
161
|
+
return {
|
|
162
|
+
...optionalInstant("observed_at", raw["observed_at"]),
|
|
163
|
+
...(overage === undefined ? {} : { overage }),
|
|
164
|
+
windows,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function windowOf(value: unknown): LlmUsageWindow | undefined {
|
|
169
|
+
const raw = objectOf(value);
|
|
170
|
+
const utilization = raw?.["utilization"];
|
|
171
|
+
const status = raw?.["status"];
|
|
172
|
+
if (raw === undefined || typeof utilization !== "number" || typeof status !== "string") {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
utilization,
|
|
177
|
+
status,
|
|
178
|
+
...optionalInstant("reset_at", raw["reset"]),
|
|
179
|
+
...optionalInteger("window_secs", raw["window_seconds"]),
|
|
180
|
+
...optionalFlag("expired", raw["expired"]),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Recognised by shape like a window: an entry counts as a limit when it names
|
|
185
|
+
* what it limits and states a figure for it. */
|
|
186
|
+
function limitOf(value: unknown): LlmUsageLimit | undefined {
|
|
187
|
+
const raw = objectOf(value);
|
|
188
|
+
const kind = raw?.["kind"];
|
|
189
|
+
const percent = raw?.["percent"];
|
|
190
|
+
if (raw === undefined || typeof kind !== "string" || typeof percent !== "number") {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
kind,
|
|
195
|
+
percent,
|
|
196
|
+
severity: typeof raw["severity"] === "string" ? raw["severity"] : "unknown",
|
|
197
|
+
...optionalInstant("resets_at", raw["resets_at"]),
|
|
198
|
+
...optionalText("model", raw["model"]),
|
|
199
|
+
...optionalFlag("is_active", raw["is_active"]),
|
|
200
|
+
...optionalInteger("window_secs", raw["window_seconds"]),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function withQuery(base: string, name: string, value: string): string {
|
|
205
|
+
const url = new URL(base);
|
|
206
|
+
url.searchParams.set(name, value);
|
|
207
|
+
return url.toString();
|
|
208
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Cap on one posted body. The gateway splits its own deliveries at a
|
|
4
|
+
* megabyte, so a body past this is not one of its batches. */
|
|
5
|
+
export const MAX_BODY_BYTES = 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
/** What a path segment naming a producer may be. Narrow on purpose: the
|
|
8
|
+
* segment picks a configured source, so it must not be able to express
|
|
9
|
+
* traversal or a case that matches two ways. */
|
|
10
|
+
export const SOURCE_NAME = /^[a-z0-9-]{1,64}$/;
|
|
11
|
+
|
|
12
|
+
/** The producer a path names, matched at the end of the path.
|
|
13
|
+
*
|
|
14
|
+
* A gateway posts to whatever URL its operator gave it, which may sit under a
|
|
15
|
+
* proxy's prefix, so the segment before `/webhook/` is not read (DR-0001 §2.7).
|
|
16
|
+
* Which instance is meant was settled by the address the proxy forwarded to;
|
|
17
|
+
* what still has to be right is the source name and the token it presents.
|
|
18
|
+
*
|
|
19
|
+
* The last `/webhook/` wins, so a source cannot be smuggled in through a
|
|
20
|
+
* prefix that contains the marker itself. */
|
|
21
|
+
export function sourceOfPath(pathname: string): string | undefined {
|
|
22
|
+
const marker = "/webhook/";
|
|
23
|
+
const at = pathname.lastIndexOf(marker);
|
|
24
|
+
return at === -1 ? undefined : pathname.slice(at + marker.length);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WebhookSource {
|
|
28
|
+
/** The path segment this producer posts to, after `/webhook/`. */
|
|
29
|
+
readonly name: string;
|
|
30
|
+
/** The secret it presents as `Authorization: Bearer`. The gateway reads the
|
|
31
|
+
* same value out of its own token file (DR-0012); this is the only thing
|
|
32
|
+
* standing between the route and anyone the entry check let through. */
|
|
33
|
+
readonly token: string;
|
|
34
|
+
/** Take one posted batch. The items are raw: reading them is the source's
|
|
35
|
+
* own business, and one it cannot read must not fail the delivery. */
|
|
36
|
+
handle(items: readonly unknown[]): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Serve `POST /webhook/<source>`, or answer nothing when the request is for
|
|
40
|
+
* something else — the caller then goes on to its other routes.
|
|
41
|
+
*
|
|
42
|
+
* Fire and forget by design. The gateway sends one way, does not retry and
|
|
43
|
+
* does not reorder, so an item this instance cannot use is worth a log line
|
|
44
|
+
* and nothing more: answering 4xx over one bad item would only teach a
|
|
45
|
+
* producer to repeat it. The body being unreadable as a whole is the one
|
|
46
|
+
* failure its sender can act on, so it is the one it hears about. */
|
|
47
|
+
export async function handleWebhook(
|
|
48
|
+
request: Request,
|
|
49
|
+
source: WebhookSource | undefined,
|
|
50
|
+
log?: (msg: string, fields?: Record<string, unknown>) => void,
|
|
51
|
+
): Promise<Response | undefined> {
|
|
52
|
+
const name = sourceOfPath(new URL(request.url).pathname);
|
|
53
|
+
if (name === undefined) return undefined;
|
|
54
|
+
if (request.method !== "POST") return new Response("Method Not Allowed", { status: 405 });
|
|
55
|
+
// An unconfigured source does not exist as far as a caller can tell, which
|
|
56
|
+
// is also what keeps the answer from naming what could be turned on.
|
|
57
|
+
if (source === undefined || !SOURCE_NAME.test(name) || name !== source.name) {
|
|
58
|
+
return new Response("Not Found", { status: 404 });
|
|
59
|
+
}
|
|
60
|
+
if (!authorized(request.headers.get("authorization"), source.token)) {
|
|
61
|
+
return new Response("Unauthorized", { status: 401 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const declared = Number(request.headers.get("content-length"));
|
|
65
|
+
if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) return tooLarge();
|
|
66
|
+
|
|
67
|
+
let text: string;
|
|
68
|
+
try {
|
|
69
|
+
text = await bounded(request);
|
|
70
|
+
} catch (cause) {
|
|
71
|
+
if (cause instanceof TooLarge) return tooLarge();
|
|
72
|
+
return new Response(`the body could not be read: ${String(cause)}`, { status: 400 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let parsed: unknown;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(text);
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
log?.("a webhook delivery was not JSON", { source: source.name, error: String(cause) });
|
|
80
|
+
return new Response("the body is not JSON", { status: 400 });
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
// The gateway always posts an array; one bare item is the same thing to
|
|
84
|
+
// whoever reads them.
|
|
85
|
+
source.handle(Array.isArray(parsed) ? parsed : [parsed]);
|
|
86
|
+
} catch (cause) {
|
|
87
|
+
// A reader that throws is this instance's own fault, not the sender's: it
|
|
88
|
+
// is logged and the delivery is still accepted.
|
|
89
|
+
log?.("a webhook reader failed", { source: source.name, error: String(cause) });
|
|
90
|
+
}
|
|
91
|
+
return new Response(null, { status: 204 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether the request presents exactly this source's token.
|
|
95
|
+
*
|
|
96
|
+
* Compared in constant time. The route is reachable only by whoever the entry
|
|
97
|
+
* check of §3.1 already let through, but a comparison that leaks its prefix
|
|
98
|
+
* through timing is the kind of thing that quietly stops being enough once an
|
|
99
|
+
* instance is bound past loopback. */
|
|
100
|
+
function authorized(header: string | null, token: string): boolean {
|
|
101
|
+
if (header === null) return false;
|
|
102
|
+
const prefix = "Bearer ";
|
|
103
|
+
if (!header.startsWith(prefix)) return false;
|
|
104
|
+
const presented = Buffer.from(header.slice(prefix.length));
|
|
105
|
+
const expected = Buffer.from(token);
|
|
106
|
+
// A length mismatch cannot go through `timingSafeEqual`, and the length is
|
|
107
|
+
// not the secret.
|
|
108
|
+
if (presented.length !== expected.length) return false;
|
|
109
|
+
return timingSafeEqual(presented, expected);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class TooLarge extends Error {}
|
|
113
|
+
|
|
114
|
+
function tooLarge(): Response {
|
|
115
|
+
return new Response(`the body is over ${MAX_BODY_BYTES} bytes`, { status: 413 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Read the body with a hard ceiling. `request.json()` would buffer the whole
|
|
119
|
+
* stream before any limit could apply, and the declared length is advisory —
|
|
120
|
+
* absent entirely under chunked encoding. This route shares an event loop with
|
|
121
|
+
* every connected client. */
|
|
122
|
+
async function bounded(request: Request): Promise<string> {
|
|
123
|
+
const body = request.body;
|
|
124
|
+
if (body === null) return "";
|
|
125
|
+
const reader = body.getReader();
|
|
126
|
+
const decoder = new TextDecoder();
|
|
127
|
+
let size = 0;
|
|
128
|
+
let text = "";
|
|
129
|
+
try {
|
|
130
|
+
for (;;) {
|
|
131
|
+
const { done, value } = await reader.read();
|
|
132
|
+
if (done === true) break;
|
|
133
|
+
size += value.byteLength;
|
|
134
|
+
if (size > MAX_BODY_BYTES) throw new TooLarge();
|
|
135
|
+
text += decoder.decode(value, { stream: true });
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
reader.releaseLock();
|
|
139
|
+
}
|
|
140
|
+
return text + decoder.decode();
|
|
141
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { version } from "../package.json";
|
|
2
|
+
|
|
3
|
+
/** This build of ccmsg, taken from the package it is published as so that the
|
|
4
|
+
* binary, the CLI and the plugin it installs never name three versions.
|
|
5
|
+
*
|
|
6
|
+
* It is also the daemon build `hello` and `instance_ping` report, which is what
|
|
7
|
+
* makes "what a client is told" and "what was released" the same number. */
|
|
8
|
+
export const VERSION: string = version;
|