@llui/agent 0.0.34 → 0.0.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/agentConnect.d.ts +1 -1
- package/dist/client/agentConnect.d.ts.map +1 -1
- package/dist/client/agentConnect.js +12 -8
- package/dist/client/agentConnect.js.map +1 -1
- package/dist/protocol.d.ts +17 -6
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/server/cloudflare/durable-object.d.ts +11 -4
- package/dist/server/cloudflare/durable-object.d.ts.map +1 -1
- package/dist/server/cloudflare/durable-object.js.map +1 -1
- package/dist/server/cloudflare/index.d.ts +8 -4
- package/dist/server/cloudflare/index.d.ts.map +1 -1
- package/dist/server/cloudflare/index.js +8 -4
- package/dist/server/cloudflare/index.js.map +1 -1
- package/dist/server/cloudflare/worker.d.ts +10 -2
- package/dist/server/cloudflare/worker.d.ts.map +1 -1
- package/dist/server/cloudflare/worker.js +13 -6
- package/dist/server/cloudflare/worker.js.map +1 -1
- package/dist/server/core-entry.d.ts +2 -2
- package/dist/server/core-entry.d.ts.map +1 -1
- package/dist/server/core-entry.js +1 -1
- package/dist/server/core-entry.js.map +1 -1
- package/dist/server/core.d.ts +1 -3
- package/dist/server/core.d.ts.map +1 -1
- package/dist/server/core.js +13 -12
- package/dist/server/core.js.map +1 -1
- package/dist/server/factory.d.ts +1 -1
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +1 -2
- package/dist/server/factory.js.map +1 -1
- package/dist/server/http/mint.d.ts +6 -1
- package/dist/server/http/mint.d.ts.map +1 -1
- package/dist/server/http/mint.js +14 -6
- package/dist/server/http/mint.js.map +1 -1
- package/dist/server/http/resume.d.ts +3 -1
- package/dist/server/http/resume.d.ts.map +1 -1
- package/dist/server/http/resume.js +9 -7
- package/dist/server/http/resume.js.map +1 -1
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/lap/confirm-result.d.ts +0 -1
- package/dist/server/lap/confirm-result.d.ts.map +1 -1
- package/dist/server/lap/confirm-result.js +1 -1
- package/dist/server/lap/confirm-result.js.map +1 -1
- package/dist/server/lap/describe.d.ts +13 -2
- package/dist/server/lap/describe.d.ts.map +1 -1
- package/dist/server/lap/describe.js +23 -6
- package/dist/server/lap/describe.js.map +1 -1
- package/dist/server/lap/forward.d.ts +0 -1
- package/dist/server/lap/forward.d.ts.map +1 -1
- package/dist/server/lap/forward.js +2 -2
- package/dist/server/lap/forward.js.map +1 -1
- package/dist/server/lap/message.d.ts +0 -1
- package/dist/server/lap/message.d.ts.map +1 -1
- package/dist/server/lap/message.js +1 -1
- package/dist/server/lap/message.js.map +1 -1
- package/dist/server/lap/observe.d.ts +0 -1
- package/dist/server/lap/observe.d.ts.map +1 -1
- package/dist/server/lap/observe.js +1 -1
- package/dist/server/lap/observe.js.map +1 -1
- package/dist/server/lap/wait.d.ts +0 -1
- package/dist/server/lap/wait.d.ts.map +1 -1
- package/dist/server/lap/wait.js +1 -1
- package/dist/server/lap/wait.js.map +1 -1
- package/dist/server/options.d.ts +7 -5
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js.map +1 -1
- package/dist/server/token-store.d.ts +22 -0
- package/dist/server/token-store.d.ts.map +1 -1
- package/dist/server/token-store.js +24 -0
- package/dist/server/token-store.js.map +1 -1
- package/dist/server/token.d.ts +32 -17
- package/dist/server/token.d.ts.map +1 -1
- package/dist/server/token.js +40 -103
- package/dist/server/token.js.map +1 -1
- package/dist/server/web/upgrade.d.ts +1 -1
- package/dist/server/web/upgrade.js +1 -1
- package/dist/server/web/upgrade.js.map +1 -1
- package/dist/server/ws/upgrade.d.ts +0 -1
- package/dist/server/ws/upgrade.d.ts.map +1 -1
- package/dist/server/ws/upgrade.js +12 -4
- package/dist/server/ws/upgrade.js.map +1 -1
- package/package.json +1 -1
package/dist/server/token.js
CHANGED
|
@@ -1,117 +1,54 @@
|
|
|
1
1
|
const PREFIX = 'llui-agent_';
|
|
2
|
+
const TOKEN_BYTES = 32;
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Mint an opaque random bearer token + the SHA-256 hash the server
|
|
5
|
+
* stores as a lookup key. Tokens are 32 bytes of CSPRNG entropy (256
|
|
6
|
+
* bits) base64url-encoded with the `llui-agent_` prefix — total
|
|
7
|
+
* 54–55 chars, vs the previous JWT format's ~250.
|
|
8
|
+
*
|
|
9
|
+
* The token itself never persists; only the hash does. A leaked store
|
|
10
|
+
* therefore does not compromise live tokens, since the bearer secret
|
|
11
|
+
* isn't recoverable from the hash. This matches the standard "session
|
|
12
|
+
* cookie / API key" pattern.
|
|
13
|
+
*
|
|
14
|
+
* The opaque form is the only token format the server understands as
|
|
15
|
+
* of 0.0.35. The previous HMAC-signed JWT format is gone; clients
|
|
16
|
+
* carrying old tokens will fail with `unknown` on first call and need
|
|
17
|
+
* to remint. See CHANGELOG.
|
|
9
18
|
*/
|
|
10
|
-
function
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
function toKeyBytes(key) {
|
|
18
|
-
if (typeof key === 'string') {
|
|
19
|
-
if (key.length < 32)
|
|
20
|
-
throw new Error('signingKey must be at least 32 bytes');
|
|
21
|
-
}
|
|
22
|
-
else if (key.byteLength < 32) {
|
|
23
|
-
throw new Error('signingKey must be at least 32 bytes');
|
|
24
|
-
}
|
|
25
|
-
return toBytes(key);
|
|
19
|
+
export async function mintToken() {
|
|
20
|
+
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
21
|
+
crypto.getRandomValues(bytes);
|
|
22
|
+
const token = (PREFIX + toBase64Url(bytes));
|
|
23
|
+
const tokenHash = await sha256Hex(token);
|
|
24
|
+
return { token, tokenHash };
|
|
26
25
|
}
|
|
27
26
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* Compute the SHA-256 hash of a presented bearer token. Returns `null`
|
|
28
|
+
* when the prefix is missing — the verify path uses that to fail-fast
|
|
29
|
+
* on garbage-shaped Authorization headers without a crypto round-trip.
|
|
30
|
+
* Hash is hex-encoded for portability across stores (Postgres `text`,
|
|
31
|
+
* KV string, etc.).
|
|
32
32
|
*/
|
|
33
|
-
async function
|
|
34
|
-
|
|
33
|
+
export async function tokenHashOf(token) {
|
|
34
|
+
if (!token.startsWith(PREFIX))
|
|
35
|
+
return null;
|
|
36
|
+
return sha256Hex(token);
|
|
37
|
+
}
|
|
38
|
+
async function sha256Hex(s) {
|
|
39
|
+
const bytes = new TextEncoder().encode(s);
|
|
40
|
+
const buf = await crypto.subtle.digest('SHA-256', bytes);
|
|
41
|
+
const arr = new Uint8Array(buf);
|
|
42
|
+
let out = '';
|
|
43
|
+
for (let i = 0; i < arr.length; i++) {
|
|
44
|
+
out += arr[i].toString(16).padStart(2, '0');
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
35
47
|
}
|
|
36
48
|
function toBase64Url(bytes) {
|
|
37
|
-
// btoa needs a binary string; build it manually to avoid ArrayBuffer/Uint8Array quirks.
|
|
38
49
|
let bin = '';
|
|
39
50
|
for (let i = 0; i < bytes.byteLength; i++)
|
|
40
51
|
bin += String.fromCharCode(bytes[i]);
|
|
41
52
|
return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
42
53
|
}
|
|
43
|
-
function fromBase64Url(s) {
|
|
44
|
-
try {
|
|
45
|
-
const b64 = s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (s.length % 4)) % 4);
|
|
46
|
-
const bin = atob(b64);
|
|
47
|
-
const buf = new ArrayBuffer(bin.length);
|
|
48
|
-
const bytes = new Uint8Array(buf);
|
|
49
|
-
for (let i = 0; i < bin.length; i++)
|
|
50
|
-
bytes[i] = bin.charCodeAt(i);
|
|
51
|
-
return bytes;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Serialize a payload to `llui-agent_<base64url(json)>.<base64url(hmac)>`.
|
|
59
|
-
* See spec §6.1. Async because WebCrypto's HMAC sign/verify is the
|
|
60
|
-
* cross-runtime standard; Node, Cloudflare, Deno, and Bun all expose
|
|
61
|
-
* `crypto.subtle` identically.
|
|
62
|
-
*/
|
|
63
|
-
export async function signToken(payload, key) {
|
|
64
|
-
const cryptoKey = await importHmacKey(key, ['sign']);
|
|
65
|
-
const jsonBytes = toBytes(JSON.stringify(payload));
|
|
66
|
-
const payloadPart = toBase64Url(jsonBytes);
|
|
67
|
-
const macBuf = await crypto.subtle.sign('HMAC', cryptoKey, toBytes(payloadPart));
|
|
68
|
-
const sigPart = toBase64Url(new Uint8Array(macBuf));
|
|
69
|
-
return (PREFIX + payloadPart + '.' + sigPart);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Verify the signature, parse the payload, and check expiry.
|
|
73
|
-
* `crypto.subtle.verify` does the constant-time compare internally,
|
|
74
|
-
* so we don't need a separate `timingSafeEqual`.
|
|
75
|
-
*/
|
|
76
|
-
export async function verifyToken(token, key, nowSec = Math.floor(Date.now() / 1000)) {
|
|
77
|
-
if (!token.startsWith(PREFIX))
|
|
78
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
79
|
-
const body = token.slice(PREFIX.length);
|
|
80
|
-
const dot = body.indexOf('.');
|
|
81
|
-
if (dot < 0)
|
|
82
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
83
|
-
const payloadPart = body.slice(0, dot);
|
|
84
|
-
const sigPart = body.slice(dot + 1);
|
|
85
|
-
const sigBytes = fromBase64Url(sigPart);
|
|
86
|
-
if (!sigBytes)
|
|
87
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
88
|
-
const cryptoKey = await importHmacKey(key, ['verify']);
|
|
89
|
-
const ok = await crypto.subtle.verify('HMAC', cryptoKey, sigBytes, toBytes(payloadPart));
|
|
90
|
-
if (!ok)
|
|
91
|
-
return { kind: 'invalid', reason: 'bad-signature' };
|
|
92
|
-
const jsonBytes = fromBase64Url(payloadPart);
|
|
93
|
-
if (!jsonBytes)
|
|
94
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
95
|
-
let parsed;
|
|
96
|
-
try {
|
|
97
|
-
parsed = JSON.parse(new TextDecoder().decode(jsonBytes));
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
101
|
-
}
|
|
102
|
-
if (!isTokenPayload(parsed))
|
|
103
|
-
return { kind: 'invalid', reason: 'malformed' };
|
|
104
|
-
if (parsed.exp <= nowSec)
|
|
105
|
-
return { kind: 'invalid', reason: 'expired' };
|
|
106
|
-
return { kind: 'ok', payload: parsed };
|
|
107
|
-
}
|
|
108
|
-
function isTokenPayload(x) {
|
|
109
|
-
if (!x || typeof x !== 'object')
|
|
110
|
-
return false;
|
|
111
|
-
const o = x;
|
|
112
|
-
return (typeof o.tid === 'string' &&
|
|
113
|
-
typeof o.iat === 'number' &&
|
|
114
|
-
typeof o.exp === 'number' &&
|
|
115
|
-
o.scope === 'agent');
|
|
116
|
-
}
|
|
117
54
|
//# sourceMappingURL=token.js.map
|
package/dist/server/token.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/server/token.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,aAAa,CAAA;AAC5B,MAAM,WAAW,GAAG,EAAE,CAAA;AAYtB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS;IAC7B,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,WAAW,CAAC,CAAA;IACzC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAA;IAC7B,MAAM,KAAK,GAAG,CAAC,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAe,CAAA;IACzD,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,KAAK,CAAC,CAAA;IACxC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAa;IAC7C,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAA;IAC1C,OAAO,SAAS,CAAC,KAAK,CAAC,CAAA;AACzB,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,CAAS;IAChC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IACzC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CAAA;IACxD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,GAAG,CAAC,CAAA;IAC/B,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;IAC9C,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,WAAW,CAAC,KAAiB;IACpC,IAAI,GAAG,GAAG,EAAE,CAAA;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC,EAAE;QAAE,GAAG,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAA;IAChF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;AAC7E,CAAC","sourcesContent":["import type { AgentToken } from '../protocol.js'\n\nconst PREFIX = 'llui-agent_'\nconst TOKEN_BYTES = 32\n\n/**\n * Result of looking up a presented token. The `expired` reason is\n * returned by the verify path when the token's record exists but its\n * hard-expiry has passed; `unknown` covers both \"no record\" and\n * \"wrong hash\" so a probe-by-hash leak surface is uniform.\n */\nexport type VerifyResult =\n | { kind: 'ok'; tid: string }\n | { kind: 'invalid'; reason: 'malformed' | 'unknown' | 'expired' }\n\n/**\n * Mint an opaque random bearer token + the SHA-256 hash the server\n * stores as a lookup key. Tokens are 32 bytes of CSPRNG entropy (256\n * bits) base64url-encoded with the `llui-agent_` prefix — total\n * 54–55 chars, vs the previous JWT format's ~250.\n *\n * The token itself never persists; only the hash does. A leaked store\n * therefore does not compromise live tokens, since the bearer secret\n * isn't recoverable from the hash. This matches the standard \"session\n * cookie / API key\" pattern.\n *\n * The opaque form is the only token format the server understands as\n * of 0.0.35. The previous HMAC-signed JWT format is gone; clients\n * carrying old tokens will fail with `unknown` on first call and need\n * to remint. See CHANGELOG.\n */\nexport async function mintToken(): Promise<{ token: AgentToken; tokenHash: string }> {\n const bytes = new Uint8Array(TOKEN_BYTES)\n crypto.getRandomValues(bytes)\n const token = (PREFIX + toBase64Url(bytes)) as AgentToken\n const tokenHash = await sha256Hex(token)\n return { token, tokenHash }\n}\n\n/**\n * Compute the SHA-256 hash of a presented bearer token. Returns `null`\n * when the prefix is missing — the verify path uses that to fail-fast\n * on garbage-shaped Authorization headers without a crypto round-trip.\n * Hash is hex-encoded for portability across stores (Postgres `text`,\n * KV string, etc.).\n */\nexport async function tokenHashOf(token: string): Promise<string | null> {\n if (!token.startsWith(PREFIX)) return null\n return sha256Hex(token)\n}\n\nasync function sha256Hex(s: string): Promise<string> {\n const bytes = new TextEncoder().encode(s)\n const buf = await crypto.subtle.digest('SHA-256', bytes)\n const arr = new Uint8Array(buf)\n let out = ''\n for (let i = 0; i < arr.length; i++) {\n out += arr[i]!.toString(16).padStart(2, '0')\n }\n return out\n}\n\nfunction toBase64Url(bytes: Uint8Array): string {\n let bin = ''\n for (let i = 0; i < bytes.byteLength; i++) bin += String.fromCharCode(bytes[i]!)\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n"]}
|
|
@@ -13,7 +13,7 @@ export declare function extractToken(req: Request): string | null;
|
|
|
13
13
|
*
|
|
14
14
|
* Usage:
|
|
15
15
|
* ```ts
|
|
16
|
-
* const agent = createLluiAgentCore(
|
|
16
|
+
* const agent = createLluiAgentCore()
|
|
17
17
|
* export default {
|
|
18
18
|
* async fetch(req, env) {
|
|
19
19
|
* const url = new URL(req.url)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/web/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentCoreHandle } from '../core.js'\nimport { createWHATWGPairingConnection } from './adapter.js'\n\n/**\n * Extract the bearer token from a LAP WebSocket upgrade request.\n * Accepts the token on either `?token=` or `Authorization: Bearer` —\n * query-string is the common pattern because browsers can't set\n * arbitrary headers on WebSocket construction.\n */\nexport function extractToken(req: Request): string | null {\n const url = new URL(req.url)\n const q = url.searchParams.get('token')\n if (q) return q\n const auth = req.headers.get('authorization')\n if (auth?.startsWith('Bearer ')) return auth.slice('Bearer '.length)\n return null\n}\n\n/**\n * Cloudflare Workers handler. Accepts a WebSocket upgrade using\n * `WebSocketPair`, validates the token via\n * `agent.acceptConnection`, and returns the 101 upgrade Response.\n *\n * Usage:\n * ```ts\n * const agent = createLluiAgentCore(
|
|
1
|
+
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/web/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentCoreHandle } from '../core.js'\nimport { createWHATWGPairingConnection } from './adapter.js'\n\n/**\n * Extract the bearer token from a LAP WebSocket upgrade request.\n * Accepts the token on either `?token=` or `Authorization: Bearer` —\n * query-string is the common pattern because browsers can't set\n * arbitrary headers on WebSocket construction.\n */\nexport function extractToken(req: Request): string | null {\n const url = new URL(req.url)\n const q = url.searchParams.get('token')\n if (q) return q\n const auth = req.headers.get('authorization')\n if (auth?.startsWith('Bearer ')) return auth.slice('Bearer '.length)\n return null\n}\n\n/**\n * Cloudflare Workers handler. Accepts a WebSocket upgrade using\n * `WebSocketPair`, validates the token via\n * `agent.acceptConnection`, and returns the 101 upgrade Response.\n *\n * Usage:\n * ```ts\n * const agent = createLluiAgentCore()\n * export default {\n * async fetch(req, env) {\n * const url = new URL(req.url)\n * if (url.pathname === '/agent/ws') return handleCloudflareUpgrade(req, agent)\n * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })\n * },\n * }\n * ```\n */\nexport async function handleCloudflareUpgrade(\n req: Request,\n agent: AgentCoreHandle,\n): Promise<Response> {\n if (req.headers.get('upgrade') !== 'websocket') {\n return new Response('Expected upgrade: websocket', { status: 426 })\n }\n const token = extractToken(req)\n if (!token) return new Response('Unauthorized', { status: 401 })\n\n // `WebSocketPair` is a Cloudflare Workers global. We reference it\n // through `globalThis` so importing this module in non-CF runtimes\n // (e.g. during type-checking on Node) doesn't crash.\n const Pair = (\n globalThis as unknown as { WebSocketPair?: new () => { 0: WebSocket; 1: WebSocket } }\n ).WebSocketPair\n if (!Pair) {\n return new Response('WebSocketPair unavailable in this runtime', { status: 501 })\n }\n const pair = new Pair()\n const client = pair[0]\n const server = pair[1]!\n // `accept()` on the server half is Cloudflare-specific — it tells\n // the runtime the Worker will handle the WebSocket itself.\n ;(server as unknown as { accept: () => void }).accept()\n\n const conn = createWHATWGPairingConnection(server)\n const result = await agent.acceptConnection(token, conn)\n if (!result.ok) {\n conn.close()\n return new Response(result.code, { status: result.status })\n }\n\n // `webSocket` on ResponseInit is Cloudflare-specific; cast to satisfy\n // the standard lib types.\n return new Response(null, { status: 101, webSocket: client } as ResponseInit & {\n webSocket: WebSocket\n })\n}\n\n/**\n * Deno handler. Uses `Deno.upgradeWebSocket(req)` to produce the\n * response + socket pair, then plugs the socket into the registry.\n *\n * Usage:\n * ```ts\n * Deno.serve(async (req) => {\n * const url = new URL(req.url)\n * if (url.pathname === '/agent/ws') return handleDenoUpgrade(req, agent)\n * return (await agent.router(req)) ?? new Response('Not Found', { status: 404 })\n * })\n * ```\n */\nexport async function handleDenoUpgrade(req: Request, agent: AgentCoreHandle): Promise<Response> {\n const token = extractToken(req)\n if (!token) return new Response('Unauthorized', { status: 401 })\n\n const Deno_ = (\n globalThis as unknown as {\n Deno?: {\n upgradeWebSocket: (req: Request) => { socket: WebSocket; response: Response }\n }\n }\n ).Deno\n if (!Deno_) {\n return new Response('Deno.upgradeWebSocket unavailable in this runtime', { status: 501 })\n }\n\n const { socket, response } = Deno_.upgradeWebSocket(req)\n const conn = createWHATWGPairingConnection(socket)\n\n // Deno opens the socket asynchronously; validate the token first,\n // then register on `open` so frames aren't missed.\n socket.addEventListener(\n 'open',\n () => {\n void agent.acceptConnection(token, conn).then((result) => {\n if (!result.ok) conn.close()\n })\n },\n { once: true },\n )\n\n return response\n}\n"]}
|
|
@@ -4,7 +4,6 @@ import type { PairingRegistry } from './pairing-registry.js';
|
|
|
4
4
|
import type { TokenStore } from '../token-store.js';
|
|
5
5
|
import type { AuditSink } from '../audit.js';
|
|
6
6
|
export type UpgradeDeps = {
|
|
7
|
-
signingKey: string | Uint8Array;
|
|
8
7
|
tokenStore: TokenStore;
|
|
9
8
|
registry: PairingRegistry;
|
|
10
9
|
auditSink: AuditSink;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../src/server/ws/upgrade.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,eAAe,EAAqB,MAAM,uBAAuB,CAAA;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAI5C,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../../src/server/ws/upgrade.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,eAAe,EAAqB,MAAM,uBAAuB,CAAA;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAI5C,MAAM,MAAM,WAAW,GAAG;IACxB,UAAU,EAAE,UAAU,CAAA;IACtB,QAAQ,EAAE,eAAe,CAAA;IACzB,SAAS,EAAE,SAAS,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,WAAW,IAIxC,KAAK,eAAe,EAAE,QAAQ,MAAM,EAAE,MAAM,MAAM,KAAG,OAAO,CAAC,IAAI,CAAC,CA4EjF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebSocketServer } from 'ws';
|
|
2
|
-
import {
|
|
2
|
+
import { tokenHashOf } from '../token.js';
|
|
3
3
|
/**
|
|
4
4
|
* Returns a handler for `server.on('upgrade', ...)`. Validates the token
|
|
5
5
|
* from the query string, attaches to the registry, wires frame/close
|
|
@@ -33,13 +33,21 @@ export function createWsUpgradeHandler(deps) {
|
|
|
33
33
|
socket.destroy();
|
|
34
34
|
return;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Same hash-lookup verify path as the LAP HTTP handlers — see
|
|
37
|
+
// describe.ts:verifyAndReadTid for the rationale.
|
|
38
|
+
const hash = await tokenHashOf(token);
|
|
39
|
+
if (!hash) {
|
|
38
40
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
39
41
|
socket.destroy();
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
|
-
const
|
|
44
|
+
const rec = await deps.tokenStore.findByTokenHash(hash);
|
|
45
|
+
if (!rec || rec.expiresAt <= now()) {
|
|
46
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
47
|
+
socket.destroy();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const tid = rec.tid;
|
|
43
51
|
// Perform upgrade, wire to registry
|
|
44
52
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
45
53
|
const conn = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/ws/upgrade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,IAAI,CAAA;AAOpC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"upgrade.js","sourceRoot":"","sources":["../../../src/server/ws/upgrade.ts"],"names":[],"mappings":"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","sourcesContent":["import { WebSocketServer } from 'ws'\nimport type { WebSocket } from 'ws'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport type { PairingRegistry, PairingConnection } from './pairing-registry.js'\nimport type { TokenStore } from '../token-store.js'\nimport type { AuditSink } from '../audit.js'\nimport { tokenHashOf } from '../token.js'\nimport type { ClientFrame, ServerFrame } from '../../protocol.js'\n\nexport type UpgradeDeps = {\n tokenStore: TokenStore\n registry: PairingRegistry\n auditSink: AuditSink\n now?: () => number\n}\n\n/**\n * Returns a handler for `server.on('upgrade', ...)`. Validates the token\n * from the query string, attaches to the registry, wires frame/close\n * routing. Unauthorized paths/tokens get a bare HTTP error response on\n * the raw socket (per RFC 6455 the response must be sent before the\n * socket is torn down).\n *\n * Spec §10.2, §10.4.\n */\nexport function createWsUpgradeHandler(deps: UpgradeDeps) {\n const wss = new WebSocketServer({ noServer: true })\n const now = deps.now ?? (() => Date.now())\n\n return async (req: IncomingMessage, socket: Duplex, head: Buffer): Promise<void> => {\n // Path check\n const url = new URL(req.url ?? '/', 'http://localhost')\n if (url.pathname !== '/agent/ws') {\n socket.write('HTTP/1.1 404 Not Found\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n // Token — try query string first, then Authorization header\n let token = url.searchParams.get('token')\n if (!token) {\n const auth = req.headers['authorization']\n if (typeof auth === 'string' && auth.startsWith('Bearer ')) {\n token = auth.slice('Bearer '.length)\n }\n }\n if (!token) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n\n // Same hash-lookup verify path as the LAP HTTP handlers — see\n // describe.ts:verifyAndReadTid for the rationale.\n const hash = await tokenHashOf(token)\n if (!hash) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n const rec = await deps.tokenStore.findByTokenHash(hash)\n if (!rec || rec.expiresAt <= now()) {\n socket.write('HTTP/1.1 401 Unauthorized\\r\\n\\r\\n')\n socket.destroy()\n return\n }\n const tid = rec.tid\n\n // Perform upgrade, wire to registry\n wss.handleUpgrade(req, socket, head, (ws: WebSocket) => {\n const conn: PairingConnection = {\n send(frame: ServerFrame) {\n ws.send(JSON.stringify(frame))\n },\n onFrame(handler) {\n ws.on('message', (data: Buffer | string) => {\n const raw = typeof data === 'string' ? data : data.toString('utf8')\n try {\n const parsed = JSON.parse(raw) as ClientFrame\n handler(parsed)\n } catch {\n // Ignore malformed frames.\n }\n })\n },\n onClose(handler) {\n ws.on('close', handler)\n },\n close() {\n ws.close()\n },\n }\n deps.registry.register(tid, conn)\n\n // Transition status: browser WS is now live, waiting for Claude's first call.\n void deps.tokenStore.markAwaitingClaude(tid, now())\n void deps.auditSink.write({\n at: now(),\n tid,\n uid: null,\n event: 'claim',\n detail: { transport: 'ws' },\n })\n })\n }\n}\n"]}
|