@wakeshift/sdk 0.0.0-stage → 0.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/CHANGELOG.md +25 -0
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +198 -2
- package/dist/errors.d.ts +20 -0
- package/dist/errors.js +28 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +195 -0
- package/dist/events.js +523 -0
- package/dist/events.js.map +1 -0
- package/dist/guest.d.ts +119 -0
- package/dist/guest.js +506 -0
- package/dist/guest.js.map +1 -0
- package/dist/harness.d.ts +6 -0
- package/dist/harness.js +78 -0
- package/dist/harness.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/key.d.ts +29 -0
- package/dist/key.js +107 -0
- package/dist/key.js.map +1 -0
- package/dist/limits.d.ts +19 -0
- package/dist/limits.js +105 -0
- package/dist/limits.js.map +1 -0
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +58 -0
- package/dist/retry.js.map +1 -0
- package/dist/sealed.d.ts +4 -0
- package/dist/sealed.js +38 -0
- package/dist/sealed.js.map +1 -0
- package/dist/spec.d.ts +28 -0
- package/dist/spec.js +43 -0
- package/dist/spec.js.map +1 -0
- package/dist/wakeshift.d.ts +415 -0
- package/dist/wakeshift.js +481 -0
- package/dist/wakeshift.js.map +1 -0
- package/package.json +48 -4
package/dist/harness.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// harnessAgent names the agent a guest's MCP client signs in as: its
|
|
2
|
+
// harness's ID ("copilot-cli" → copilot, "Claude Code" → claude-code), as
|
|
3
|
+
// topics name it, else its name as an ID. A port
|
|
4
|
+
// of the core's harnesses.AgentID: test/agent-ids.json is the Go
|
|
5
|
+
// function's output, and both sides' tests check it.
|
|
6
|
+
// The core's harness table (internal/harnesses): only what naming needs.
|
|
7
|
+
const known = [
|
|
8
|
+
{ id: "codex", clients: ["codex-mcp-client"] },
|
|
9
|
+
{ id: "claude-code", clients: ["claude-code"] },
|
|
10
|
+
{ id: "cursor", clients: ["Cursor", "cursor-vscode"] },
|
|
11
|
+
{ id: "cursor-cloud", clients: [] },
|
|
12
|
+
{ id: "grok-bot", clients: [] },
|
|
13
|
+
{ id: "copilot", clients: ["copilot-cli"] },
|
|
14
|
+
{ id: "opencode", clients: ["opencode"] },
|
|
15
|
+
{ id: "antigravity", clients: ["antigravity-client"] },
|
|
16
|
+
{ id: "openclaw", clients: ["openclaw-bundle-mcp"] },
|
|
17
|
+
{ id: "hermes", clients: ["mcp"], unsure: true },
|
|
18
|
+
{ id: "chatgpt", clients: ["openai-mcp"] },
|
|
19
|
+
{ id: "gemini", clients: ["gemini-cli-mcp-client"] },
|
|
20
|
+
{ id: "goose", clients: ["goose-cli", "goose-desktop"] },
|
|
21
|
+
{ id: "cline", clients: ["Cline"] },
|
|
22
|
+
{ id: "amp", clients: [] },
|
|
23
|
+
{ id: "devin", clients: [] },
|
|
24
|
+
{ id: "vscode", clients: ["Visual Studio Code", "Visual Studio Code - Insiders"] },
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* The agent ID an MCP client's name maps to: its harness's ID, else the
|
|
28
|
+
* name as an ID (2–40 lowercase letters, digits and dashes; "agent" if
|
|
29
|
+
* nothing's left).
|
|
30
|
+
*/
|
|
31
|
+
export function harnessAgent(client) {
|
|
32
|
+
const name = client.trim().toLowerCase();
|
|
33
|
+
if (name) {
|
|
34
|
+
for (const h of known) {
|
|
35
|
+
if (h.clients.some((c) => c.toLowerCase() === name)) {
|
|
36
|
+
if (!h.unsure)
|
|
37
|
+
return h.id;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const id = slug(client);
|
|
43
|
+
for (const h of known) {
|
|
44
|
+
if (h.unsure)
|
|
45
|
+
continue;
|
|
46
|
+
if (id === h.id || id.startsWith(h.id + "-"))
|
|
47
|
+
return h.id;
|
|
48
|
+
if (h.clients.some((c) => id === slug(c)))
|
|
49
|
+
return h.id;
|
|
50
|
+
}
|
|
51
|
+
return id;
|
|
52
|
+
}
|
|
53
|
+
// slug is a name as an agent ID: lowercase letters, digits and dashes, up
|
|
54
|
+
// to 40 characters, without anything in parentheses ("agent" if fewer than
|
|
55
|
+
// 2 are left).
|
|
56
|
+
function slug(name) {
|
|
57
|
+
let n = name.toLowerCase();
|
|
58
|
+
const i = n.indexOf("(");
|
|
59
|
+
if (i > 0)
|
|
60
|
+
n = n.slice(0, i);
|
|
61
|
+
let out = "";
|
|
62
|
+
let dash = false;
|
|
63
|
+
for (const r of n.trim()) {
|
|
64
|
+
if ((r >= "a" && r <= "z") || (r >= "0" && r <= "9")) {
|
|
65
|
+
out += r;
|
|
66
|
+
dash = false;
|
|
67
|
+
}
|
|
68
|
+
else if (!dash && out.length > 0) {
|
|
69
|
+
out += "-";
|
|
70
|
+
dash = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
let id = out.replace(/^-+|-+$/g, "");
|
|
74
|
+
if (id.length > 40)
|
|
75
|
+
id = id.slice(0, 40).replace(/^-+|-+$/g, "");
|
|
76
|
+
return id.length < 2 ? "agent" : id;
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=harness.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harness.js","sourceRoot":"","sources":["../src/harness.ts"],"names":[],"mappings":"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","sourcesContent":["// harnessAgent names the agent a guest's MCP client signs in as: its\n// harness's ID (\"copilot-cli\" → copilot, \"Claude Code\" → claude-code), as\n// topics name it, else its name as an ID. A port\n// of the core's harnesses.AgentID: test/agent-ids.json is the Go\n// function's output, and both sides' tests check it.\n\ninterface Known {\n id: string;\n clients: string[];\n /** Its client name is one others send too (Hermes Agent's \"mcp\"): it keeps its own ID. */\n unsure?: boolean;\n}\n\n// The core's harness table (internal/harnesses): only what naming needs.\nconst known: Known[] = [\n { id: \"codex\", clients: [\"codex-mcp-client\"] },\n { id: \"claude-code\", clients: [\"claude-code\"] },\n { id: \"cursor\", clients: [\"Cursor\", \"cursor-vscode\"] },\n { id: \"cursor-cloud\", clients: [] },\n { id: \"grok-bot\", clients: [] },\n { id: \"copilot\", clients: [\"copilot-cli\"] },\n { id: \"opencode\", clients: [\"opencode\"] },\n { id: \"antigravity\", clients: [\"antigravity-client\"] },\n { id: \"openclaw\", clients: [\"openclaw-bundle-mcp\"] },\n { id: \"hermes\", clients: [\"mcp\"], unsure: true },\n { id: \"chatgpt\", clients: [\"openai-mcp\"] },\n { id: \"gemini\", clients: [\"gemini-cli-mcp-client\"] },\n { id: \"goose\", clients: [\"goose-cli\", \"goose-desktop\"] },\n { id: \"cline\", clients: [\"Cline\"] },\n { id: \"amp\", clients: [] },\n { id: \"devin\", clients: [] },\n { id: \"vscode\", clients: [\"Visual Studio Code\", \"Visual Studio Code - Insiders\"] },\n];\n\n/**\n * The agent ID an MCP client's name maps to: its harness's ID, else the\n * name as an ID (2–40 lowercase letters, digits and dashes; \"agent\" if\n * nothing's left).\n */\nexport function harnessAgent(client: string): string {\n const name = client.trim().toLowerCase();\n if (name) {\n for (const h of known) {\n if (h.clients.some((c) => c.toLowerCase() === name)) {\n if (!h.unsure) return h.id;\n break;\n }\n }\n }\n const id = slug(client);\n for (const h of known) {\n if (h.unsure) continue;\n if (id === h.id || id.startsWith(h.id + \"-\")) return h.id;\n if (h.clients.some((c) => id === slug(c))) return h.id;\n }\n return id;\n}\n\n// slug is a name as an agent ID: lowercase letters, digits and dashes, up\n// to 40 characters, without anything in parentheses (\"agent\" if fewer than\n// 2 are left).\nfunction slug(name: string): string {\n let n = name.toLowerCase();\n const i = n.indexOf(\"(\");\n if (i > 0) n = n.slice(0, i);\n let out = \"\";\n let dash = false;\n for (const r of n.trim()) {\n if ((r >= \"a\" && r <= \"z\") || (r >= \"0\" && r <= \"9\")) {\n out += r;\n dash = false;\n } else if (!dash && out.length > 0) {\n out += \"-\";\n dash = true;\n }\n }\n let id = out.replace(/^-+|-+$/g, \"\");\n if (id.length > 40) id = id.slice(0, 40).replace(/^-+|-+$/g, \"\");\n return id.length < 2 ? \"agent\" : id;\n}\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Wakeshift, DEFAULT_CORE, SESSION_ARG, HARNESS_ARG, CONVERSATION_ARG, UNKNOWN_CLIENT_WARNING, type WakeshiftOptions, type AppInfo, type Call, type CallOptions, type ClientInfoSource, type Duration, type SubscribeOptions, type Subscription, type Addition, type Recognised, type TopicEvent, type Published, type SendStatus, type Via, type TopicStatus, type SubscriptionStatus, type PublishUrl, type ToolExtra, type Manifest, type AgentScope, type ScopeEvent, } from "./wakeshift.js";
|
|
2
|
+
export { GuestAuth, guestOf, type Guest, type GuestAuthOptions, type GuestAuthInfo } from "./guest.js";
|
|
3
|
+
export { harnessAgent } from "./harness.js";
|
|
4
|
+
export { WakeshiftError, isCode } from "./errors.js";
|
|
5
|
+
export { parseKey, signAssertion, type AppKey } from "./key.js";
|
|
6
|
+
export { WakeEvent, ArgsError, speaksEvents, EVENT_TOOL_TEXT, SUBSCRIBE_TOOL_DESCRIPTION, UNSUBSCRIBE_TOOL_DESCRIPTION, LIST_TOOL_DESCRIPTION, type EventOptions, type EmitOptions, type CallerSubscription, type EventTypeManifest, type AttachOptions, type Identity, type Subscriber, } from "./events.js";
|
|
7
|
+
export { EXTENSION_KEY, LEGACY_KEY, TEXT_META, REVISION as EVENTS_REVISION } from "./spec.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { Wakeshift, DEFAULT_CORE, SESSION_ARG, HARNESS_ARG, CONVERSATION_ARG, UNKNOWN_CLIENT_WARNING, } from "./wakeshift.js";
|
|
2
|
+
export { GuestAuth, guestOf } from "./guest.js";
|
|
3
|
+
export { harnessAgent } from "./harness.js";
|
|
4
|
+
export { WakeshiftError, isCode } from "./errors.js";
|
|
5
|
+
export { parseKey, signAssertion } from "./key.js";
|
|
6
|
+
export { WakeEvent, ArgsError, speaksEvents, EVENT_TOOL_TEXT, SUBSCRIBE_TOOL_DESCRIPTION, UNSUBSCRIBE_TOOL_DESCRIPTION, LIST_TOOL_DESCRIPTION, } from "./events.js";
|
|
7
|
+
export { EXTENSION_KEY, LEGACY_KEY, TEXT_META, REVISION as EVENTS_REVISION } from "./spec.js";
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,sBAAsB,GAsBvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,SAAS,EAAE,OAAO,EAAyD,MAAM,YAAY,CAAC;AACvG,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAe,MAAM,UAAU,CAAC;AAChE,OAAO,EACL,SAAS,EACT,SAAS,EACT,YAAY,EACZ,eAAe,EACf,0BAA0B,EAC1B,4BAA4B,EAC5B,qBAAqB,GAQtB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,WAAW,CAAC","sourcesContent":["export {\n Wakeshift,\n DEFAULT_CORE,\n SESSION_ARG,\n HARNESS_ARG,\n CONVERSATION_ARG,\n UNKNOWN_CLIENT_WARNING,\n type WakeshiftOptions,\n type AppInfo,\n type Call,\n type CallOptions,\n type ClientInfoSource,\n type Duration,\n type SubscribeOptions,\n type Subscription,\n type Addition,\n type Recognised,\n type TopicEvent,\n type Published,\n type SendStatus,\n type Via,\n type TopicStatus,\n type SubscriptionStatus,\n type PublishUrl,\n type ToolExtra,\n type Manifest,\n type AgentScope,\n type ScopeEvent,\n} from \"./wakeshift.js\";\nexport { GuestAuth, guestOf, type Guest, type GuestAuthOptions, type GuestAuthInfo } from \"./guest.js\";\nexport { harnessAgent } from \"./harness.js\";\nexport { WakeshiftError, isCode } from \"./errors.js\";\nexport { parseKey, signAssertion, type AppKey } from \"./key.js\";\nexport {\n WakeEvent,\n ArgsError,\n speaksEvents,\n EVENT_TOOL_TEXT,\n SUBSCRIBE_TOOL_DESCRIPTION,\n UNSUBSCRIBE_TOOL_DESCRIPTION,\n LIST_TOOL_DESCRIPTION,\n type EventOptions,\n type EmitOptions,\n type CallerSubscription,\n type EventTypeManifest,\n type AttachOptions,\n type Identity,\n type Subscriber,\n} from \"./events.js\";\nexport { EXTENSION_KEY, LEGACY_KEY, TEXT_META, REVISION as EVENTS_REVISION } from \"./spec.js\";\n"]}
|
package/dist/key.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
export interface AppKey {
|
|
3
|
+
/**
|
|
4
|
+
* An older key's mode; null for a current key.
|
|
5
|
+
* @deprecated Modes mean nothing now, and current keys have none.
|
|
6
|
+
*/
|
|
7
|
+
mode: "test" | "live" | null;
|
|
8
|
+
/** The app's ID, for an older key that names it; null for a current key (ask Wakeshift: `wake.app()`). */
|
|
9
|
+
appId: string | null;
|
|
10
|
+
privateKey: KeyObject;
|
|
11
|
+
/** The 32-byte seed: what the SDK's own signed values (guest sign-in) are keyed from. */
|
|
12
|
+
seed: Buffer;
|
|
13
|
+
/** The key's ID in assertions' headers: base64url of the first 16 bytes of the public key's SHA-256. */
|
|
14
|
+
kid: string;
|
|
15
|
+
}
|
|
16
|
+
/** Reads an app key from the console, either format. Throws on anything else. */
|
|
17
|
+
export declare function parseKey(key: string): AppKey;
|
|
18
|
+
/** A raw 32-byte Ed25519 public key's kid: base64url of the first 16 bytes of its SHA-256. */
|
|
19
|
+
export declare function keyIdOf(publicKey: Uint8Array): string;
|
|
20
|
+
/** The app's public key, base64url (what the console stores). */
|
|
21
|
+
export declare function publicKeyOf(k: AppKey): string;
|
|
22
|
+
/**
|
|
23
|
+
* Signs a fresh assertion of the app's identity for core (its base URL,
|
|
24
|
+
* without a trailing slash): a compact JWS, alg EdDSA, typ app+jwt, with
|
|
25
|
+
* the key's kid, living two minutes. iss and sub are the app's ID when
|
|
26
|
+
* it's known (an older key names it; pass appId once Wakeshift has said
|
|
27
|
+
* it), else the kid.
|
|
28
|
+
*/
|
|
29
|
+
export declare function signAssertion(k: AppKey, core: string, now?: number, appId?: string | null): string;
|
package/dist/key.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, randomBytes, sign } from "node:crypto";
|
|
2
|
+
const appIdRE = /^[a-z][a-z0-9-]{1,39}$/;
|
|
3
|
+
const seedRE = /^[A-Za-z0-9_-]{43}$/;
|
|
4
|
+
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
5
|
+
const LEN = 50;
|
|
6
|
+
const LIMIT = 1n << 288n;
|
|
7
|
+
// PKCS #8 for an Ed25519 key, before its 32-byte seed (RFC 8410).
|
|
8
|
+
const pkcs8Prefix = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
9
|
+
// Errors never include the key.
|
|
10
|
+
const errFormat = "wakeshift: an app key is wst_ followed by 50 letters and digits (or an older wst_test_… or wst_live_… key)";
|
|
11
|
+
const errMistyped = "wakeshift: the app key is cut short or mistyped";
|
|
12
|
+
// CRC32 (IEEE). zlib.crc32 needs Node 22.2; the package supports 22.0.
|
|
13
|
+
const crcTable = (() => {
|
|
14
|
+
const t = new Uint32Array(256);
|
|
15
|
+
for (let n = 0; n < 256; n++) {
|
|
16
|
+
let c = n;
|
|
17
|
+
for (let k = 0; k < 8; k++)
|
|
18
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
19
|
+
t[n] = c >>> 0;
|
|
20
|
+
}
|
|
21
|
+
return t;
|
|
22
|
+
})();
|
|
23
|
+
function crc32(b) {
|
|
24
|
+
let c = 0xffffffff;
|
|
25
|
+
for (const x of b)
|
|
26
|
+
c = crcTable[(c ^ x) & 0xff] ^ (c >>> 8);
|
|
27
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
28
|
+
}
|
|
29
|
+
/** Reads an app key from the console, either format. Throws on anything else. */
|
|
30
|
+
export function parseKey(key) {
|
|
31
|
+
const s = String(key ?? "").trim();
|
|
32
|
+
if (!s.startsWith("wst_"))
|
|
33
|
+
throw new Error("wakeshift: an app key starts with wst_");
|
|
34
|
+
const rest = s.slice(4);
|
|
35
|
+
const i = rest.indexOf("_");
|
|
36
|
+
if (i >= 0)
|
|
37
|
+
return parseOld(rest.slice(0, i), rest.slice(i + 1));
|
|
38
|
+
if (rest.length !== LEN)
|
|
39
|
+
throw new Error(errMistyped);
|
|
40
|
+
let n = 0n;
|
|
41
|
+
for (const ch of rest) {
|
|
42
|
+
const d = alphabet.indexOf(ch);
|
|
43
|
+
if (d < 0)
|
|
44
|
+
throw new Error(errMistyped);
|
|
45
|
+
n = n * 58n + BigInt(d);
|
|
46
|
+
}
|
|
47
|
+
if (n >= LIMIT)
|
|
48
|
+
throw new Error(errMistyped);
|
|
49
|
+
const b = Buffer.from(n.toString(16).padStart(72, "0"), "hex");
|
|
50
|
+
const seed = Buffer.from(b.subarray(0, 32));
|
|
51
|
+
if (crc32(seed) !== b.readUInt32BE(32))
|
|
52
|
+
throw new Error(errMistyped);
|
|
53
|
+
return fromSeed(null, null, seed);
|
|
54
|
+
}
|
|
55
|
+
// An older key: wst_<mode>_<app id>_<seed>. App IDs have no "_"; the seed
|
|
56
|
+
// (base64url) may.
|
|
57
|
+
function parseOld(mode, after) {
|
|
58
|
+
if (mode !== "test" && mode !== "live")
|
|
59
|
+
throw new Error(errFormat);
|
|
60
|
+
const j = after.indexOf("_");
|
|
61
|
+
const appId = j < 0 ? "" : after.slice(0, j);
|
|
62
|
+
if (!appIdRE.test(appId))
|
|
63
|
+
throw new Error("wakeshift: the app key has no app ID");
|
|
64
|
+
const seed = after.slice(j + 1);
|
|
65
|
+
const raw = seedRE.test(seed) ? Buffer.from(seed, "base64url") : Buffer.alloc(0);
|
|
66
|
+
if (raw.length !== 32)
|
|
67
|
+
throw new Error(errMistyped);
|
|
68
|
+
return fromSeed(mode, appId, raw);
|
|
69
|
+
}
|
|
70
|
+
function fromSeed(mode, appId, seed) {
|
|
71
|
+
const privateKey = createPrivateKey({ key: Buffer.concat([pkcs8Prefix, seed]), format: "der", type: "pkcs8" });
|
|
72
|
+
return { mode, appId, privateKey, seed, kid: keyIdOf(rawPublicKey(privateKey)) };
|
|
73
|
+
}
|
|
74
|
+
function rawPublicKey(k) {
|
|
75
|
+
return Buffer.from(createPublicKey(k).export({ format: "jwk" }).x, "base64url");
|
|
76
|
+
}
|
|
77
|
+
/** A raw 32-byte Ed25519 public key's kid: base64url of the first 16 bytes of its SHA-256. */
|
|
78
|
+
export function keyIdOf(publicKey) {
|
|
79
|
+
return createHash("sha256").update(publicKey).digest().subarray(0, 16).toString("base64url");
|
|
80
|
+
}
|
|
81
|
+
/** The app's public key, base64url (what the console stores). */
|
|
82
|
+
export function publicKeyOf(k) {
|
|
83
|
+
return rawPublicKey(k.privateKey).toString("base64url");
|
|
84
|
+
}
|
|
85
|
+
const b64 = (v) => Buffer.from(typeof v === "string" ? v : JSON.stringify(v)).toString("base64url");
|
|
86
|
+
/**
|
|
87
|
+
* Signs a fresh assertion of the app's identity for core (its base URL,
|
|
88
|
+
* without a trailing slash): a compact JWS, alg EdDSA, typ app+jwt, with
|
|
89
|
+
* the key's kid, living two minutes. iss and sub are the app's ID when
|
|
90
|
+
* it's known (an older key names it; pass appId once Wakeshift has said
|
|
91
|
+
* it), else the kid.
|
|
92
|
+
*/
|
|
93
|
+
export function signAssertion(k, core, now = Date.now(), appId) {
|
|
94
|
+
const iat = Math.floor(now / 1000);
|
|
95
|
+
const who = appId || k.appId || k.kid;
|
|
96
|
+
const claims = {
|
|
97
|
+
iss: who,
|
|
98
|
+
sub: who,
|
|
99
|
+
aud: core + "/api/apps",
|
|
100
|
+
iat,
|
|
101
|
+
exp: iat + 120,
|
|
102
|
+
jti: randomBytes(16).toString("base64url"),
|
|
103
|
+
};
|
|
104
|
+
const input = b64({ alg: "EdDSA", typ: "app+jwt", kid: k.kid }) + "." + b64(claims);
|
|
105
|
+
return input + "." + sign(null, Buffer.from(input), k.privateKey).toString("base64url");
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=key.js.map
|
package/dist/key.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key.js","sourceRoot":"","sources":["../src/key.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, createPrivateKey, createPublicKey, randomBytes, sign, type KeyObject } from \"node:crypto\";\n\n// An app key from the Wakeshift console is the app's Ed25519 private key.\n// Current keys are\n//\n// wst_<50 base58 characters>\n//\n// 36 bytes, the 32-byte seed then the CRC32 (IEEE, big-endian) of the\n// seed, read as one big-endian integer and written in base58 (Bitcoin's\n// alphabet), left-padded with \"1\". They name no app: Wakeshift finds the\n// app by the key's kid, which every assertion's header carries, and says\n// which it is (GET /api/apps/v1/app). Older keys, which keep working, are\n//\n// wst_<test|live>_<app id>_<seed, base64url>\n//\n// The two can't be confused: base58 has no \"_\". The key never goes over\n// the wire: it signs short-lived assertions, and Wakeshift keeps only the\n// public half.\n\nexport interface AppKey {\n /**\n * An older key's mode; null for a current key.\n * @deprecated Modes mean nothing now, and current keys have none.\n */\n mode: \"test\" | \"live\" | null;\n /** The app's ID, for an older key that names it; null for a current key (ask Wakeshift: `wake.app()`). */\n appId: string | null;\n privateKey: KeyObject;\n /** The 32-byte seed: what the SDK's own signed values (guest sign-in) are keyed from. */\n seed: Buffer;\n /** The key's ID in assertions' headers: base64url of the first 16 bytes of the public key's SHA-256. */\n kid: string;\n}\n\nconst appIdRE = /^[a-z][a-z0-9-]{1,39}$/;\nconst seedRE = /^[A-Za-z0-9_-]{43}$/;\nconst alphabet = \"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz\";\nconst LEN = 50;\nconst LIMIT = 1n << 288n;\n// PKCS #8 for an Ed25519 key, before its 32-byte seed (RFC 8410).\nconst pkcs8Prefix = Buffer.from(\"302e020100300506032b657004220420\", \"hex\");\n\n// Errors never include the key.\nconst errFormat = \"wakeshift: an app key is wst_ followed by 50 letters and digits (or an older wst_test_… or wst_live_… key)\";\nconst errMistyped = \"wakeshift: the app key is cut short or mistyped\";\n\n// CRC32 (IEEE). zlib.crc32 needs Node 22.2; the package supports 22.0.\nconst crcTable = (() => {\n const t = new Uint32Array(256);\n for (let n = 0; n < 256; n++) {\n let c = n;\n for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;\n t[n] = c >>> 0;\n }\n return t;\n})();\n\nfunction crc32(b: Uint8Array): number {\n let c = 0xffffffff;\n for (const x of b) c = crcTable[(c ^ x) & 0xff] ^ (c >>> 8);\n return (c ^ 0xffffffff) >>> 0;\n}\n\n/** Reads an app key from the console, either format. Throws on anything else. */\nexport function parseKey(key: string): AppKey {\n const s = String(key ?? \"\").trim();\n if (!s.startsWith(\"wst_\")) throw new Error(\"wakeshift: an app key starts with wst_\");\n const rest = s.slice(4);\n const i = rest.indexOf(\"_\");\n if (i >= 0) return parseOld(rest.slice(0, i), rest.slice(i + 1));\n if (rest.length !== LEN) throw new Error(errMistyped);\n let n = 0n;\n for (const ch of rest) {\n const d = alphabet.indexOf(ch);\n if (d < 0) throw new Error(errMistyped);\n n = n * 58n + BigInt(d);\n }\n if (n >= LIMIT) throw new Error(errMistyped);\n const b = Buffer.from(n.toString(16).padStart(72, \"0\"), \"hex\");\n const seed = Buffer.from(b.subarray(0, 32));\n if (crc32(seed) !== b.readUInt32BE(32)) throw new Error(errMistyped);\n return fromSeed(null, null, seed);\n}\n\n// An older key: wst_<mode>_<app id>_<seed>. App IDs have no \"_\"; the seed\n// (base64url) may.\nfunction parseOld(mode: string, after: string): AppKey {\n if (mode !== \"test\" && mode !== \"live\") throw new Error(errFormat);\n const j = after.indexOf(\"_\");\n const appId = j < 0 ? \"\" : after.slice(0, j);\n if (!appIdRE.test(appId)) throw new Error(\"wakeshift: the app key has no app ID\");\n const seed = after.slice(j + 1);\n const raw = seedRE.test(seed) ? Buffer.from(seed, \"base64url\") : Buffer.alloc(0);\n if (raw.length !== 32) throw new Error(errMistyped);\n return fromSeed(mode, appId, raw);\n}\n\nfunction fromSeed(mode: AppKey[\"mode\"], appId: string | null, seed: Buffer): AppKey {\n const privateKey = createPrivateKey({ key: Buffer.concat([pkcs8Prefix, seed]), format: \"der\", type: \"pkcs8\" });\n return { mode, appId, privateKey, seed, kid: keyIdOf(rawPublicKey(privateKey)) };\n}\n\nfunction rawPublicKey(k: KeyObject): Buffer {\n return Buffer.from(createPublicKey(k).export({ format: \"jwk\" }).x as string, \"base64url\");\n}\n\n/** A raw 32-byte Ed25519 public key's kid: base64url of the first 16 bytes of its SHA-256. */\nexport function keyIdOf(publicKey: Uint8Array): string {\n return createHash(\"sha256\").update(publicKey).digest().subarray(0, 16).toString(\"base64url\");\n}\n\n/** The app's public key, base64url (what the console stores). */\nexport function publicKeyOf(k: AppKey): string {\n return rawPublicKey(k.privateKey).toString(\"base64url\");\n}\n\nconst b64 = (v: unknown) => Buffer.from(typeof v === \"string\" ? v : JSON.stringify(v)).toString(\"base64url\");\n\n/**\n * Signs a fresh assertion of the app's identity for core (its base URL,\n * without a trailing slash): a compact JWS, alg EdDSA, typ app+jwt, with\n * the key's kid, living two minutes. iss and sub are the app's ID when\n * it's known (an older key names it; pass appId once Wakeshift has said\n * it), else the kid.\n */\nexport function signAssertion(k: AppKey, core: string, now = Date.now(), appId?: string | null): string {\n const iat = Math.floor(now / 1000);\n const who = appId || k.appId || k.kid;\n const claims = {\n iss: who,\n sub: who,\n aud: core + \"/api/apps\",\n iat,\n exp: iat + 120,\n jti: randomBytes(16).toString(\"base64url\"),\n };\n const input = b64({ alg: \"EdDSA\", typ: \"app+jwt\", kid: k.kid }) + \".\" + b64(claims);\n return input + \".\" + sign(null, Buffer.from(input), k.privateKey).toString(\"base64url\");\n}\n"]}
|
package/dist/limits.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
/** A rate limiter: at most max events per window (ms) for each key, in memory, pruning idle keys as it goes. A negative max turns it off. */
|
|
3
|
+
export declare class RateLimiter {
|
|
4
|
+
readonly max: number;
|
|
5
|
+
readonly window: number;
|
|
6
|
+
readonly keys: number;
|
|
7
|
+
private readonly seen;
|
|
8
|
+
private pruned;
|
|
9
|
+
constructor(max: number, window: number, keys?: number);
|
|
10
|
+
allow(key: string, now: number): boolean;
|
|
11
|
+
}
|
|
12
|
+
/** What an address counts as for the limits: an IPv6 address's /64 (one host usually has a whole /64), anything else as it is. */
|
|
13
|
+
export declare function limitKey(addr: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* The request's client address: with hops trusted proxies in front (each
|
|
16
|
+
* appending to X-Forwarded-For), the entry the outermost one added; entries
|
|
17
|
+
* before it are the client's own claims.
|
|
18
|
+
*/
|
|
19
|
+
export declare function clientAddr(req: IncomingMessage, hops: number): string;
|
package/dist/limits.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Per-address limits for unauthenticated endpoints (GuestAuth's register
|
|
2
|
+
// and token): who a request is from, behind trusted proxies, and how many
|
|
3
|
+
// requests each address may make. In memory, per process.
|
|
4
|
+
import { isIP } from "node:net";
|
|
5
|
+
// LIMITER_KEYS bounds a limiter's memory: past it, a new address is refused
|
|
6
|
+
// until old ones expire (the endpoints are unauthenticated, and IPv6 makes
|
|
7
|
+
// addresses cheap).
|
|
8
|
+
const LIMITER_KEYS = 100_000;
|
|
9
|
+
/** A rate limiter: at most max events per window (ms) for each key, in memory, pruning idle keys as it goes. A negative max turns it off. */
|
|
10
|
+
export class RateLimiter {
|
|
11
|
+
max;
|
|
12
|
+
window;
|
|
13
|
+
keys;
|
|
14
|
+
seen = new Map();
|
|
15
|
+
pruned = 0;
|
|
16
|
+
constructor(max, window, keys = LIMITER_KEYS) {
|
|
17
|
+
this.max = max;
|
|
18
|
+
this.window = window;
|
|
19
|
+
this.keys = keys;
|
|
20
|
+
}
|
|
21
|
+
allow(key, now) {
|
|
22
|
+
if (this.max < 0)
|
|
23
|
+
return true;
|
|
24
|
+
key = limitKey(key);
|
|
25
|
+
const cut = now - this.window;
|
|
26
|
+
const prune = () => {
|
|
27
|
+
for (const [k, ts] of this.seen)
|
|
28
|
+
if (ts.length === 0 || ts[ts.length - 1] <= cut)
|
|
29
|
+
this.seen.delete(k);
|
|
30
|
+
this.pruned = now;
|
|
31
|
+
};
|
|
32
|
+
if (now - this.pruned > this.window)
|
|
33
|
+
prune();
|
|
34
|
+
if (!this.seen.has(key) && this.seen.size >= this.keys) {
|
|
35
|
+
prune();
|
|
36
|
+
if (this.seen.size >= this.keys)
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const ts = (this.seen.get(key) ?? []).filter((t) => t > cut);
|
|
40
|
+
if (ts.length >= this.max) {
|
|
41
|
+
this.seen.set(key, ts);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
ts.push(now);
|
|
45
|
+
this.seen.set(key, ts);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** What an address counts as for the limits: an IPv6 address's /64 (one host usually has a whole /64), anything else as it is. */
|
|
50
|
+
export function limitKey(addr) {
|
|
51
|
+
const a = addr.replace(/%.*$/, "");
|
|
52
|
+
if (isIP(a) !== 6 || /^::ffff:\d+\.\d+\.\d+\.\d+$/i.test(a))
|
|
53
|
+
return addr;
|
|
54
|
+
const groups = expandIPv6(a);
|
|
55
|
+
return groups ? groups.slice(0, 4).join(":") + "::/64" : addr;
|
|
56
|
+
}
|
|
57
|
+
// expandIPv6 is an IPv6 address's eight groups (a dotted IPv4 tail as two).
|
|
58
|
+
function expandIPv6(a) {
|
|
59
|
+
let s = a.toLowerCase();
|
|
60
|
+
const v4 = /(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(s);
|
|
61
|
+
if (v4) {
|
|
62
|
+
const b = v4.slice(1).map(Number);
|
|
63
|
+
s = s.slice(0, v4.index) + ((b[0] << 8) | b[1]).toString(16) + ":" + ((b[2] << 8) | b[3]).toString(16);
|
|
64
|
+
}
|
|
65
|
+
const halves = s.split("::");
|
|
66
|
+
if (halves.length > 2)
|
|
67
|
+
return null;
|
|
68
|
+
const head = halves[0] ? halves[0].split(":") : [];
|
|
69
|
+
const tail = halves.length === 2 && halves[1] ? halves[1].split(":") : [];
|
|
70
|
+
const fill = halves.length === 2 ? 8 - head.length - tail.length : 0;
|
|
71
|
+
const all = [...head, ...Array(Math.max(fill, 0)).fill("0"), ...tail];
|
|
72
|
+
if (all.length !== 8)
|
|
73
|
+
return null;
|
|
74
|
+
return all.map((g) => parseInt(g, 16).toString(16));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The request's client address: with hops trusted proxies in front (each
|
|
78
|
+
* appending to X-Forwarded-For), the entry the outermost one added; entries
|
|
79
|
+
* before it are the client's own claims.
|
|
80
|
+
*/
|
|
81
|
+
export function clientAddr(req, hops) {
|
|
82
|
+
// The first X-Forwarded-For header only, as Go's Header.Get reads it.
|
|
83
|
+
const distinct = req.headersDistinct?.["x-forwarded-for"];
|
|
84
|
+
const raw = req.headers["x-forwarded-for"];
|
|
85
|
+
const f = distinct?.[0] ?? (Array.isArray(raw) ? raw[0] : raw);
|
|
86
|
+
if (f && hops > 0) {
|
|
87
|
+
// Only an entry the proxies added: with fewer entries than hops, or one
|
|
88
|
+
// that isn't an address, the header isn't the proxies' (a request that
|
|
89
|
+
// reached the app some other way), so it's ignored.
|
|
90
|
+
const parts = f.split(",");
|
|
91
|
+
if (parts.length >= hops) {
|
|
92
|
+
const ip = parts[parts.length - hops].trim();
|
|
93
|
+
if (isIP(ip))
|
|
94
|
+
return normalizeAddr(ip);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return normalizeAddr(req.socket?.remoteAddress ?? "");
|
|
98
|
+
}
|
|
99
|
+
// normalizeAddr writes an IPv4 address a dual-stack socket reports as
|
|
100
|
+
// ::ffff:a.b.c.d as a.b.c.d, as Go does.
|
|
101
|
+
function normalizeAddr(a) {
|
|
102
|
+
const m = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/i.exec(a);
|
|
103
|
+
return m ? m[1] : a;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=limits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"limits.js","sourceRoot":"","sources":["../src/limits.ts"],"names":[],"mappings":"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","sourcesContent":["// Per-address limits for unauthenticated endpoints (GuestAuth's register\n// and token): who a request is from, behind trusted proxies, and how many\n// requests each address may make. In memory, per process.\n\nimport type { IncomingMessage } from \"node:http\";\nimport { isIP } from \"node:net\";\n\n// LIMITER_KEYS bounds a limiter's memory: past it, a new address is refused\n// until old ones expire (the endpoints are unauthenticated, and IPv6 makes\n// addresses cheap).\nconst LIMITER_KEYS = 100_000;\n\n/** A rate limiter: at most max events per window (ms) for each key, in memory, pruning idle keys as it goes. A negative max turns it off. */\nexport class RateLimiter {\n private readonly seen = new Map<string, number[]>();\n private pruned = 0;\n\n constructor(\n readonly max: number,\n readonly window: number,\n readonly keys = LIMITER_KEYS,\n ) {}\n\n allow(key: string, now: number): boolean {\n if (this.max < 0) return true;\n key = limitKey(key);\n const cut = now - this.window;\n const prune = () => {\n for (const [k, ts] of this.seen) if (ts.length === 0 || ts[ts.length - 1] <= cut) this.seen.delete(k);\n this.pruned = now;\n };\n if (now - this.pruned > this.window) prune();\n if (!this.seen.has(key) && this.seen.size >= this.keys) {\n prune();\n if (this.seen.size >= this.keys) return false;\n }\n const ts = (this.seen.get(key) ?? []).filter((t) => t > cut);\n if (ts.length >= this.max) {\n this.seen.set(key, ts);\n return false;\n }\n ts.push(now);\n this.seen.set(key, ts);\n return true;\n }\n}\n\n/** What an address counts as for the limits: an IPv6 address's /64 (one host usually has a whole /64), anything else as it is. */\nexport function limitKey(addr: string): string {\n const a = addr.replace(/%.*$/, \"\");\n if (isIP(a) !== 6 || /^::ffff:\\d+\\.\\d+\\.\\d+\\.\\d+$/i.test(a)) return addr;\n const groups = expandIPv6(a);\n return groups ? groups.slice(0, 4).join(\":\") + \"::/64\" : addr;\n}\n\n// expandIPv6 is an IPv6 address's eight groups (a dotted IPv4 tail as two).\nfunction expandIPv6(a: string): string[] | null {\n let s = a.toLowerCase();\n const v4 = /(\\d+)\\.(\\d+)\\.(\\d+)\\.(\\d+)$/.exec(s);\n if (v4) {\n const b = v4.slice(1).map(Number);\n s = s.slice(0, v4.index) + ((b[0] << 8) | b[1]).toString(16) + \":\" + ((b[2] << 8) | b[3]).toString(16);\n }\n const halves = s.split(\"::\");\n if (halves.length > 2) return null;\n const head = halves[0] ? halves[0].split(\":\") : [];\n const tail = halves.length === 2 && halves[1] ? halves[1].split(\":\") : [];\n const fill = halves.length === 2 ? 8 - head.length - tail.length : 0;\n const all = [...head, ...Array(Math.max(fill, 0)).fill(\"0\"), ...tail];\n if (all.length !== 8) return null;\n return all.map((g) => parseInt(g, 16).toString(16));\n}\n\n/**\n * The request's client address: with hops trusted proxies in front (each\n * appending to X-Forwarded-For), the entry the outermost one added; entries\n * before it are the client's own claims.\n */\nexport function clientAddr(req: IncomingMessage, hops: number): string {\n // The first X-Forwarded-For header only, as Go's Header.Get reads it.\n const distinct = (req as IncomingMessage & { headersDistinct?: Record<string, string[] | undefined> }).headersDistinct?.[\"x-forwarded-for\"];\n const raw = req.headers[\"x-forwarded-for\"];\n const f = distinct?.[0] ?? (Array.isArray(raw) ? raw[0] : raw);\n if (f && hops > 0) {\n // Only an entry the proxies added: with fewer entries than hops, or one\n // that isn't an address, the header isn't the proxies' (a request that\n // reached the app some other way), so it's ignored.\n const parts = f.split(\",\");\n if (parts.length >= hops) {\n const ip = parts[parts.length - hops].trim();\n if (isIP(ip)) return normalizeAddr(ip);\n }\n }\n return normalizeAddr(req.socket?.remoteAddress ?? \"\");\n}\n\n// normalizeAddr writes an IPv4 address a dual-stack socket reports as\n// ::ffff:a.b.c.d as a.b.c.d, as Go does.\nfunction normalizeAddr(a: string): string {\n const m = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/i.exec(a);\n return m ? m[1] : a;\n}\n"]}
|
package/dist/retry.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/retry.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Wakeshift runs behind Google's load balancer on Cloud Run. For some
|
|
2
|
+
// minutes after a rollout, the load balancer sends a small share of requests
|
|
3
|
+
// to a retired revision, which answers at once with Google's own HTML "500
|
|
4
|
+
// Server Error" page: the request never reached a Wakeshift instance. It
|
|
5
|
+
// happens to every client alike (new connections included), so retrying is
|
|
6
|
+
// what gets the request through.
|
|
7
|
+
/** @internal Pauses before each retry of a refused request (so at most length + 1 attempts). */
|
|
8
|
+
export const RETRY_PAUSES_MS = [250, 1000];
|
|
9
|
+
/**
|
|
10
|
+
* @internal fetchFn, retrying a request the load balancer refused before it
|
|
11
|
+
* reached Wakeshift: Google's HTML 500 page, for any request (it never ran),
|
|
12
|
+
* and a 502, 503 or 504 not from Wakeshift for a GET or HEAD. Wakeshift's own
|
|
13
|
+
* errors (JSON) are never retried, nor is a request whose body can't be sent
|
|
14
|
+
* again (a stream).
|
|
15
|
+
*/
|
|
16
|
+
export function retryRefused(fetchFn, pauses = RETRY_PAUSES_MS) {
|
|
17
|
+
return (async (input, init) => {
|
|
18
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
19
|
+
const resendable = !(input instanceof Request) && (init?.body == null || typeof init.body === "string" || init.body instanceof URLSearchParams);
|
|
20
|
+
for (let attempt = 0;; attempt++) {
|
|
21
|
+
const resp = await fetchFn(input, init);
|
|
22
|
+
if (!resendable || attempt >= pauses.length || !(await refused(method, resp)))
|
|
23
|
+
return resp;
|
|
24
|
+
await resp.body?.cancel().catch(() => { });
|
|
25
|
+
await pause(pauses[attempt], init?.signal ?? undefined);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** @internal Whether resp is the load balancer's refusal, not the service's answer. */
|
|
30
|
+
export async function refused(method, resp) {
|
|
31
|
+
const type = (resp.headers.get("Content-Type") ?? "").split(";")[0].trim().toLowerCase();
|
|
32
|
+
if (resp.status === 500) {
|
|
33
|
+
if (type !== "text/html")
|
|
34
|
+
return false;
|
|
35
|
+
const text = await resp.clone().text().catch(() => "");
|
|
36
|
+
return text.includes("Server Error");
|
|
37
|
+
}
|
|
38
|
+
if (resp.status === 502 || resp.status === 503 || resp.status === 504) {
|
|
39
|
+
return (method === "GET" || method === "HEAD") && type !== "application/json";
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function pause(ms, signal) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
return reject(signal.reason);
|
|
47
|
+
const t = setTimeout(() => {
|
|
48
|
+
signal?.removeEventListener("abort", stop);
|
|
49
|
+
resolve();
|
|
50
|
+
}, ms);
|
|
51
|
+
const stop = () => {
|
|
52
|
+
clearTimeout(t);
|
|
53
|
+
reject(signal.reason);
|
|
54
|
+
};
|
|
55
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"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","sourcesContent":["// Wakeshift runs behind Google's load balancer on Cloud Run. For some\n// minutes after a rollout, the load balancer sends a small share of requests\n// to a retired revision, which answers at once with Google's own HTML \"500\n// Server Error\" page: the request never reached a Wakeshift instance. It\n// happens to every client alike (new connections included), so retrying is\n// what gets the request through.\n\n/** @internal Pauses before each retry of a refused request (so at most length + 1 attempts). */\nexport const RETRY_PAUSES_MS = [250, 1000];\n\n/**\n * @internal fetchFn, retrying a request the load balancer refused before it\n * reached Wakeshift: Google's HTML 500 page, for any request (it never ran),\n * and a 502, 503 or 504 not from Wakeshift for a GET or HEAD. Wakeshift's own\n * errors (JSON) are never retried, nor is a request whose body can't be sent\n * again (a stream).\n */\nexport function retryRefused(fetchFn: typeof fetch, pauses: number[] = RETRY_PAUSES_MS): typeof fetch {\n return (async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {\n const method = (init?.method ?? (input instanceof Request ? input.method : \"GET\")).toUpperCase();\n const resendable = !(input instanceof Request) && (init?.body == null || typeof init.body === \"string\" || init.body instanceof URLSearchParams);\n for (let attempt = 0; ; attempt++) {\n const resp = await fetchFn(input, init);\n if (!resendable || attempt >= pauses.length || !(await refused(method, resp))) return resp;\n await resp.body?.cancel().catch(() => {});\n await pause(pauses[attempt]!, init?.signal ?? undefined);\n }\n }) as typeof fetch;\n}\n\n/** @internal Whether resp is the load balancer's refusal, not the service's answer. */\nexport async function refused(method: string, resp: Response): Promise<boolean> {\n const type = (resp.headers.get(\"Content-Type\") ?? \"\").split(\";\")[0]!.trim().toLowerCase();\n if (resp.status === 500) {\n if (type !== \"text/html\") return false;\n const text = await resp.clone().text().catch(() => \"\");\n return text.includes(\"Server Error\");\n }\n if (resp.status === 502 || resp.status === 503 || resp.status === 504) {\n return (method === \"GET\" || method === \"HEAD\") && type !== \"application/json\";\n }\n return false;\n}\n\nfunction pause(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) return reject(signal.reason);\n const t = setTimeout(() => {\n signal?.removeEventListener(\"abort\", stop);\n resolve();\n }, ms);\n const stop = () => {\n clearTimeout(t);\n reject(signal!.reason);\n };\n signal?.addEventListener(\"abort\", stop, { once: true });\n });\n}\n"]}
|
package/dist/sealed.d.ts
ADDED
package/dist/sealed.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Signed values: base64url JSON, a ".", and its HMAC-SHA256 under key, so
|
|
2
|
+
// only the app can make one and nobody can change one. Signed, not
|
|
3
|
+
// encrypted: whoever holds one can read it. GuestAuth's clients, codes and
|
|
4
|
+
// tokens use these, each with its own key.
|
|
5
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
6
|
+
const B64URL_RE = /^[A-Za-z0-9_-]*$/;
|
|
7
|
+
/** The signed form of v. */
|
|
8
|
+
export function seal(key, v) {
|
|
9
|
+
const p = Buffer.from(JSON.stringify(v)).toString("base64url");
|
|
10
|
+
return p + "." + createHmac("sha256", key).update(p).digest("base64url");
|
|
11
|
+
}
|
|
12
|
+
/** The JSON object s carries, if key signed it; null for anything else. */
|
|
13
|
+
export function unseal(key, s) {
|
|
14
|
+
if (typeof s !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const i = s.indexOf(".");
|
|
17
|
+
if (i < 0)
|
|
18
|
+
return null;
|
|
19
|
+
const p = s.slice(0, i);
|
|
20
|
+
const sig = s.slice(i + 1);
|
|
21
|
+
if (!B64URL_RE.test(p) || !B64URL_RE.test(sig))
|
|
22
|
+
return null;
|
|
23
|
+
const got = Buffer.from(sig, "base64url");
|
|
24
|
+
// Only the canonical encoding: no second spelling of the same signature.
|
|
25
|
+
if (got.toString("base64url") !== sig)
|
|
26
|
+
return null;
|
|
27
|
+
const want = createHmac("sha256", key).update(p).digest();
|
|
28
|
+
if (got.length !== want.length || !timingSafeEqual(got, want))
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const v = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
|
|
32
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) ? v : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=sealed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealed.js","sourceRoot":"","sources":["../src/sealed.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,mEAAmE;AACnE,2EAA2E;AAC3E,2CAA2C;AAE3C,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE1D,MAAM,SAAS,GAAG,kBAAkB,CAAC;AAErC,4BAA4B;AAC5B,MAAM,UAAU,IAAI,CAAC,GAAW,EAAE,CAAU;IAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC/D,OAAO,CAAC,GAAG,GAAG,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAC3E,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,MAAM,CAAI,GAAW,EAAE,CAAU;IAC/C,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACvC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACzB,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IACvB,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACxB,MAAM,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;IAC1C,yEAAyE;IACzE,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACnD,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;IAC1D,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3E,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;QACnE,OAAO,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAE,CAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["// Signed values: base64url JSON, a \".\", and its HMAC-SHA256 under key, so\n// only the app can make one and nobody can change one. Signed, not\n// encrypted: whoever holds one can read it. GuestAuth's clients, codes and\n// tokens use these, each with its own key.\n\nimport { createHmac, timingSafeEqual } from \"node:crypto\";\n\nconst B64URL_RE = /^[A-Za-z0-9_-]*$/;\n\n/** The signed form of v. */\nexport function seal(key: Buffer, v: unknown): string {\n const p = Buffer.from(JSON.stringify(v)).toString(\"base64url\");\n return p + \".\" + createHmac(\"sha256\", key).update(p).digest(\"base64url\");\n}\n\n/** The JSON object s carries, if key signed it; null for anything else. */\nexport function unseal<T>(key: Buffer, s: unknown): T | null {\n if (typeof s !== \"string\") return null;\n const i = s.indexOf(\".\");\n if (i < 0) return null;\n const p = s.slice(0, i);\n const sig = s.slice(i + 1);\n if (!B64URL_RE.test(p) || !B64URL_RE.test(sig)) return null;\n const got = Buffer.from(sig, \"base64url\");\n // Only the canonical encoding: no second spelling of the same signature.\n if (got.toString(\"base64url\") !== sig) return null;\n const want = createHmac(\"sha256\", key).update(p).digest();\n if (got.length !== want.length || !timingSafeEqual(got, want)) return null;\n try {\n const v = JSON.parse(Buffer.from(p, \"base64url\").toString(\"utf8\"));\n return v !== null && typeof v === \"object\" && !Array.isArray(v) ? (v as T) : null;\n } catch {\n return null;\n }\n}\n"]}
|
package/dist/spec.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** The draft these follow. */
|
|
2
|
+
export declare const REVISION = "SEP-3415 (2026-10-16)";
|
|
3
|
+
/** Capability keys: the draft's extension, and the top-level key ChatGPT reads. A server advertises both. */
|
|
4
|
+
export declare const EXTENSION_KEY = "io.modelcontextprotocol/events";
|
|
5
|
+
export declare const LEGACY_KEY = "events";
|
|
6
|
+
/** On every events/* result. */
|
|
7
|
+
export declare const RESULT_TYPE = "complete";
|
|
8
|
+
/** The _meta key of an occurrence's rendered line (Wakeshift adds it on delivery). */
|
|
9
|
+
export declare const TEXT_META = "ai.wakeshift/text";
|
|
10
|
+
/** The Streamable HTTP header events/subscribe and events/unsubscribe carry: params.name. */
|
|
11
|
+
export declare const NAME_HEADER = "mcp-name";
|
|
12
|
+
export type Dialect = "draft" | "chatgpt";
|
|
13
|
+
export interface Codes {
|
|
14
|
+
notFound: number;
|
|
15
|
+
forbidden: number;
|
|
16
|
+
resourceExhausted: number;
|
|
17
|
+
unsupported: number;
|
|
18
|
+
callbackEndpoint: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const INVALID_PARAMS = -32602;
|
|
21
|
+
export declare const HEADER_MISMATCH = -32020;
|
|
22
|
+
export declare const INTERNAL_ERROR = -32603;
|
|
23
|
+
/** A dialect's error codes: the draft's (provisional) or ChatGPT's. */
|
|
24
|
+
export declare function codesFor(dialect: Dialect): Codes;
|
|
25
|
+
/** A client's dialect, by its clientInfo name or User-Agent: ChatGPT's MCP client is openai-mcp. */
|
|
26
|
+
export declare function dialectOf(clientName: string, userAgent: string): Dialect;
|
|
27
|
+
/** Decodes an Mcp-Name header: plain ASCII, or SEP-2243's base64 sentinel (=?base64?…?=). null: a sentinel that doesn't decode. */
|
|
28
|
+
export declare function headerValue(h: string): string | null;
|
package/dist/spec.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The SEP-3415 (MCP Events) constants the SDK uses, in one place, matching
|
|
2
|
+
// Wakeshift's own (internal/mcpevents/event/spec.go, which a test checks
|
|
3
|
+
// these against): the draft is still moving.
|
|
4
|
+
/** The draft these follow. */
|
|
5
|
+
export const REVISION = "SEP-3415 (2026-10-16)";
|
|
6
|
+
/** Capability keys: the draft's extension, and the top-level key ChatGPT reads. A server advertises both. */
|
|
7
|
+
export const EXTENSION_KEY = "io.modelcontextprotocol/events";
|
|
8
|
+
export const LEGACY_KEY = "events";
|
|
9
|
+
/** On every events/* result. */
|
|
10
|
+
export const RESULT_TYPE = "complete";
|
|
11
|
+
/** The _meta key of an occurrence's rendered line (Wakeshift adds it on delivery). */
|
|
12
|
+
export const TEXT_META = "ai.wakeshift/text";
|
|
13
|
+
/** The Streamable HTTP header events/subscribe and events/unsubscribe carry: params.name. */
|
|
14
|
+
export const NAME_HEADER = "mcp-name";
|
|
15
|
+
export const INVALID_PARAMS = -32602;
|
|
16
|
+
export const HEADER_MISMATCH = -32020;
|
|
17
|
+
export const INTERNAL_ERROR = -32603;
|
|
18
|
+
/** A dialect's error codes: the draft's (provisional) or ChatGPT's. */
|
|
19
|
+
export function codesFor(dialect) {
|
|
20
|
+
if (dialect === "chatgpt") {
|
|
21
|
+
return { notFound: -32011, forbidden: -32012, resourceExhausted: INVALID_PARAMS, unsupported: INVALID_PARAMS, callbackEndpoint: -32015 };
|
|
22
|
+
}
|
|
23
|
+
return { notFound: -32023, forbidden: -32024, resourceExhausted: -32025, unsupported: -32026, callbackEndpoint: -32027 };
|
|
24
|
+
}
|
|
25
|
+
/** A client's dialect, by its clientInfo name or User-Agent: ChatGPT's MCP client is openai-mcp. */
|
|
26
|
+
export function dialectOf(clientName, userAgent) {
|
|
27
|
+
return clientName.toLowerCase() === "openai-mcp" || userAgent.toLowerCase().startsWith("openai-mcp") ? "chatgpt" : "draft";
|
|
28
|
+
}
|
|
29
|
+
/** Decodes an Mcp-Name header: plain ASCII, or SEP-2243's base64 sentinel (=?base64?…?=). null: a sentinel that doesn't decode. */
|
|
30
|
+
export function headerValue(h) {
|
|
31
|
+
if (!h.startsWith("=?base64?") || !h.endsWith("?="))
|
|
32
|
+
return h;
|
|
33
|
+
try {
|
|
34
|
+
const b = Buffer.from(h.slice("=?base64?".length, -2), "base64");
|
|
35
|
+
if (b.toString("base64").replace(/=+$/, "") !== h.slice("=?base64?".length, -2).replace(/=+$/, ""))
|
|
36
|
+
return null;
|
|
37
|
+
return b.toString("utf8");
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=spec.js.map
|