@kya-os/mcp-i-cloudflare 1.9.7 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +47 -16
- package/dist/adapter.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +181 -50
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +86 -3
- package/dist/app.js.map +1 -1
- package/dist/config.d.ts +54 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -1
- package/dist/constants/storage-keys.d.ts +6 -0
- package/dist/constants/storage-keys.d.ts.map +1 -1
- package/dist/constants/storage-keys.js +6 -0
- package/dist/constants/storage-keys.js.map +1 -1
- package/dist/delegation-http/anchor-do.d.ts +84 -0
- package/dist/delegation-http/anchor-do.d.ts.map +1 -0
- package/dist/delegation-http/anchor-do.js +253 -0
- package/dist/delegation-http/anchor-do.js.map +1 -0
- package/dist/delegation-http/anchor-routing.d.ts +20 -0
- package/dist/delegation-http/anchor-routing.d.ts.map +1 -0
- package/dist/delegation-http/anchor-routing.js +27 -0
- package/dist/delegation-http/anchor-routing.js.map +1 -0
- package/dist/delegation-http/bearer-retry.d.ts +54 -0
- package/dist/delegation-http/bearer-retry.d.ts.map +1 -0
- package/dist/delegation-http/bearer-retry.js +195 -0
- package/dist/delegation-http/bearer-retry.js.map +1 -0
- package/dist/delegation-http/challenge-middleware.d.ts +28 -0
- package/dist/delegation-http/challenge-middleware.d.ts.map +1 -0
- package/dist/delegation-http/challenge-middleware.js +190 -0
- package/dist/delegation-http/challenge-middleware.js.map +1 -0
- package/dist/delegation-http/challenge.d.ts +86 -0
- package/dist/delegation-http/challenge.d.ts.map +1 -0
- package/dist/delegation-http/challenge.js +173 -0
- package/dist/delegation-http/challenge.js.map +1 -0
- package/dist/delegation-http/consent-anchor-bridge.d.ts +35 -0
- package/dist/delegation-http/consent-anchor-bridge.d.ts.map +1 -0
- package/dist/delegation-http/consent-anchor-bridge.js +355 -0
- package/dist/delegation-http/consent-anchor-bridge.js.map +1 -0
- package/dist/delegation-http/constants.d.ts +51 -0
- package/dist/delegation-http/constants.d.ts.map +1 -0
- package/dist/delegation-http/constants.js +60 -0
- package/dist/delegation-http/constants.js.map +1 -0
- package/dist/delegation-http/hash.d.ts +21 -0
- package/dist/delegation-http/hash.d.ts.map +1 -0
- package/dist/delegation-http/hash.js +92 -0
- package/dist/delegation-http/hash.js.map +1 -0
- package/dist/delegation-http/pickup-routes.d.ts +42 -0
- package/dist/delegation-http/pickup-routes.d.ts.map +1 -0
- package/dist/delegation-http/pickup-routes.js +79 -0
- package/dist/delegation-http/pickup-routes.js.map +1 -0
- package/dist/delegation-http/register.d.ts +19 -0
- package/dist/delegation-http/register.d.ts.map +1 -0
- package/dist/delegation-http/register.js +72 -0
- package/dist/delegation-http/register.js.map +1 -0
- package/dist/delegation-http/sid.d.ts +20 -0
- package/dist/delegation-http/sid.d.ts.map +1 -0
- package/dist/delegation-http/sid.js +32 -0
- package/dist/delegation-http/sid.js.map +1 -0
- package/dist/delegation-http/types.d.ts +64 -0
- package/dist/delegation-http/types.d.ts.map +1 -0
- package/dist/delegation-http/types.js +11 -0
- package/dist/delegation-http/types.js.map +1 -0
- package/dist/helpers/env-mapper.d.ts.map +1 -1
- package/dist/helpers/env-mapper.js +24 -0
- package/dist/helpers/env-mapper.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/providers/crypto.d.ts +1 -1
- package/dist/providers/crypto.d.ts.map +1 -1
- package/dist/providers/crypto.js +1 -1
- package/dist/providers/crypto.js.map +1 -1
- package/dist/providers/kv-identity.d.ts +2 -2
- package/dist/providers/kv-identity.d.ts.map +1 -1
- package/dist/providers/kv-identity.js +1 -1
- package/dist/providers/kv-identity.js.map +1 -1
- package/dist/providers/storage.d.ts +1 -1
- package/dist/providers/storage.d.ts.map +1 -1
- package/dist/providers/storage.js +1 -1
- package/dist/providers/storage.js.map +1 -1
- package/dist/runtime/audit-logger.d.ts +1 -1
- package/dist/runtime/audit-logger.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +31 -3
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/runtime/oidc/authorize-url.d.ts +19 -0
- package/dist/runtime/oidc/authorize-url.d.ts.map +1 -0
- package/dist/runtime/oidc/authorize-url.js +94 -0
- package/dist/runtime/oidc/authorize-url.js.map +1 -0
- package/dist/runtime/oidc/token-exchange.d.ts +62 -0
- package/dist/runtime/oidc/token-exchange.d.ts.map +1 -0
- package/dist/runtime/oidc/token-exchange.js +75 -0
- package/dist/runtime/oidc/token-exchange.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/services/consent-audit.service.d.ts +1 -1
- package/dist/services/consent-audit.service.d.ts.map +1 -1
- package/dist/services/consent-audit.service.js.map +1 -1
- package/dist/services/consent.service.d.ts +2 -2
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +39 -50
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/delegation.service.d.ts +9 -0
- package/dist/services/delegation.service.d.ts.map +1 -1
- package/dist/services/delegation.service.js +21 -7
- package/dist/services/delegation.service.js.map +1 -1
- package/dist/services/oauth-security.service.d.ts.map +1 -1
- package/dist/services/oauth-security.service.js +6 -10
- package/dist/services/oauth-security.service.js.map +1 -1
- package/dist/services/provider-loader.service.js +1 -1
- package/dist/services/provider-loader.service.js.map +1 -1
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/known-clients.d.ts.map +1 -1
- package/dist/utils/known-clients.js +19 -10
- package/dist/utils/known-clients.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draft-kya-http-02 delegation layer — §8.1 challenge emitter middleware.
|
|
3
|
+
*
|
|
4
|
+
* Triggers on JSON-RPC responses whose `result._meta.errorType ===
|
|
5
|
+
* "authorization_required"` (the existing `DelegationRequiredError` → `result._meta`
|
|
6
|
+
* mapping in `adapter.ts`), mints a sid, creates the pickup anchor (which MUST exist
|
|
7
|
+
* before the 401 leaves), signs the `challenge_proof`, and emits the §8.1 401 in
|
|
8
|
+
* `negotiated`/`always` mode. Returns `null` to pass the original response through.
|
|
9
|
+
*
|
|
10
|
+
* @module @kya-os/mcp-i-cloudflare/delegation-http/challenge-middleware
|
|
11
|
+
*/
|
|
12
|
+
import { sha256Hex } from "./hash";
|
|
13
|
+
import { INTERNAL_PICKUP_CREATE } from "./constants";
|
|
14
|
+
import { buildChallengeResponse, buildFetcherChallengeResponse, buildConsentUri, signChallengeProof, } from "./challenge";
|
|
15
|
+
/**
|
|
16
|
+
* Inspect a cloned JSON-RPC response body for the `authorization_required` trigger.
|
|
17
|
+
* Returns the parsed meta when present, otherwise `null` (pass through untouched).
|
|
18
|
+
*/
|
|
19
|
+
async function readAuthorizationRequiredMeta(response) {
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
parsed = await response.clone().json();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Non-JSON or unreadable body: not a delegation error.
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const result = parsed
|
|
29
|
+
?.result;
|
|
30
|
+
const meta = result?._meta;
|
|
31
|
+
if (!meta || meta.errorType !== "authorization_required") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return meta;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* INTERACTIVE AI-agent fetcher User-Agents — agents that fetch on behalf of a human in
|
|
38
|
+
* a live session (so they can relay a consent link) but collapse 4xx/5xx bodies and
|
|
39
|
+
* never surface a 401 challenge to the LLM. Deliberately EXCLUDES indexing crawlers
|
|
40
|
+
* (e.g. GPTBot, OAI-SearchBot, PerplexityBot): a crawler has no user to relay consent
|
|
41
|
+
* to, so it stays on the spec path — matching the engine PEPs' `ai_agent` (NOT bot)
|
|
42
|
+
* contract. TRANSITIONAL worker-local heuristic; the durable signal is the
|
|
43
|
+
* `KYA-Accept-Challenge` opt-in header, and this list retires as frameworks adopt it.
|
|
44
|
+
*/
|
|
45
|
+
const AI_AGENT_FETCHER_UA = /\b(claude-user|chatgpt-user)\b/i;
|
|
46
|
+
/**
|
|
47
|
+
* A cooperative agent fetcher that should receive the body-readable HTTP 200 envelope
|
|
48
|
+
* instead of a 401 it would silently drop: the caller opts in via `KYA-Accept-Challenge`
|
|
49
|
+
* (any non-empty value — the durable, primary signal), OR its User-Agent matches a known
|
|
50
|
+
* agent fetcher (the transitional heuristic). Mirrors the other PEPs' `serveBodyReadable200`
|
|
51
|
+
* predicate. A cooperative-UX bridge, NOT an access control.
|
|
52
|
+
*/
|
|
53
|
+
function callerWantsBodyReadable200(request) {
|
|
54
|
+
const accept = request.headers.get("KYA-Accept-Challenge");
|
|
55
|
+
if (accept && accept.trim().length > 0) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
const ua = request.headers.get("User-Agent") ?? "";
|
|
59
|
+
return AI_AGENT_FETCHER_UA.test(ua);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A signature-capable caller (RFC 9421 `Signature-Input` with a `kya` label) that can
|
|
63
|
+
* read a 4xx and handle the dual `WWW-Authenticate` wire — it keeps the spec 401.
|
|
64
|
+
*/
|
|
65
|
+
function callerIsSignatureCapable(request) {
|
|
66
|
+
const signatureInput = request.headers.get("Signature-Input");
|
|
67
|
+
return !!signatureInput && /(^|,)\s*kya\s*=/.test(signatureInput);
|
|
68
|
+
}
|
|
69
|
+
/** Pull the required scopes out of the adapter meta, defaulting to an empty list. */
|
|
70
|
+
function readRequiredScopes(meta) {
|
|
71
|
+
if (Array.isArray(meta.requiredScopes)) {
|
|
72
|
+
return meta.requiredScopes.filter((scope) => typeof scope === "string");
|
|
73
|
+
}
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Derive a deterministic anchor sid (`kya-` + 32 hex) from the MCP session, the
|
|
78
|
+
* required scopes, AND the `minAssurance` (W1 creation-bounding). Repeated
|
|
79
|
+
* `authorization_required` results for the same caller+scopes+assurance reuse one
|
|
80
|
+
* `pickup:<sid>` anchor instead of minting a fresh durable object per request. A
|
|
81
|
+
* session-less caller collapses to one anchor per (scope-set, assurance); the MCP
|
|
82
|
+
* session id (when present) separates clients.
|
|
83
|
+
*
|
|
84
|
+
* `minAssurance` is part of the key because `reduceCreate` reuses a still-live anchor
|
|
85
|
+
* without mutating its fields: if it were omitted, a later challenge advertising a new
|
|
86
|
+
* `min_assurance` would reuse an anchor (and consent surface) still carrying the old
|
|
87
|
+
* value — so a changed assurance MUST derive a distinct anchor.
|
|
88
|
+
*/
|
|
89
|
+
function deriveAnchorSid(request, requiredScopes, minAssurance) {
|
|
90
|
+
const session = request.headers.get("mcp-session-id") ?? "";
|
|
91
|
+
const scopeKey = [...requiredScopes].sort().join(" ");
|
|
92
|
+
return `kya-${sha256Hex(`${session}\n${scopeKey}\n${minAssurance ?? ""}`).slice(0, 32)}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Emit a §8.1 delegation challenge in place of an `authorization_required` MCP
|
|
96
|
+
* response, or return `null` to pass the original response through untouched.
|
|
97
|
+
*
|
|
98
|
+
* `negotiated` (default): when the caller is NOT kya-capable, the ORIGINAL MCP error
|
|
99
|
+
* response is returned (so existing MCP/OAuth clients still see the unmodified error),
|
|
100
|
+
* never a 401. `always`: every `authorization_required` becomes a 401.
|
|
101
|
+
*/
|
|
102
|
+
export async function maybeEmitDelegationChallenge(request, response, env, cfg, deps) {
|
|
103
|
+
const meta = await readAuthorizationRequiredMeta(response);
|
|
104
|
+
if (!meta) {
|
|
105
|
+
// Not a delegation error — pass through untouched.
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// Selection: a cooperative agent fetcher gets the body-readable 200; a
|
|
109
|
+
// signature-capable client gets the spec 401. In `negotiated` mode a caller that is
|
|
110
|
+
// NEITHER is handed back the original MCP error (never a challenge it can't read).
|
|
111
|
+
const wantsBodyReadable200 = callerWantsBodyReadable200(request);
|
|
112
|
+
const signatureCapable = callerIsSignatureCapable(request);
|
|
113
|
+
if (cfg.mode === "negotiated" && !wantsBodyReadable200 && !signatureCapable) {
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
// W4: pin the challenge host to the configured realm when set, falling back to the
|
|
117
|
+
// request Host only when unset — so a Host-header-reflecting deployment cannot get
|
|
118
|
+
// an org-signed challenge (and consent/pickup links) pointing at an attacker host.
|
|
119
|
+
const host = cfg.realm || new URL(request.url).host;
|
|
120
|
+
const requiredScopes = readRequiredScopes(meta);
|
|
121
|
+
const minAssurance = typeof meta.minAssurance === "string" ? meta.minAssurance : undefined;
|
|
122
|
+
const sid = deriveAnchorSid(request, requiredScopes, minAssurance);
|
|
123
|
+
const verdict = {
|
|
124
|
+
requiredScopes,
|
|
125
|
+
minAssurance,
|
|
126
|
+
authorizationHost: host,
|
|
127
|
+
realm: host,
|
|
128
|
+
sid,
|
|
129
|
+
pollIntervalS: cfg.pollIntervalS,
|
|
130
|
+
};
|
|
131
|
+
// The anchor MUST exist before the 401 leaves: a caller could poll the pickup URL
|
|
132
|
+
// the instant it reads the challenge. Create-then-respond ordering is enforced here
|
|
133
|
+
// by awaiting the DO create — AND verifying it persisted (201) — before
|
|
134
|
+
// signing/building the response. If the create fails (non-201) or the durable
|
|
135
|
+
// object is unreachable, we suppress the challenge and fall through (return null)
|
|
136
|
+
// to the original MCP `authorization_required` error rather than advertise a
|
|
137
|
+
// consent/pickup URI for an anchor that was never persisted (clients would poll a
|
|
138
|
+
// §13 404/invalid-session forever).
|
|
139
|
+
const anchorStub = deps.getAnchorStub(env, sid);
|
|
140
|
+
let anchorPersisted = false;
|
|
141
|
+
try {
|
|
142
|
+
const createResponse = await anchorStub.fetch(`https://do${INTERNAL_PICKUP_CREATE}`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
sid,
|
|
147
|
+
requiredScopes: verdict.requiredScopes,
|
|
148
|
+
...(verdict.minAssurance !== undefined
|
|
149
|
+
? { minAssurance: verdict.minAssurance }
|
|
150
|
+
: {}),
|
|
151
|
+
pollIntervalS: cfg.pollIntervalS,
|
|
152
|
+
anchorTtlS: cfg.anchorTtlS,
|
|
153
|
+
delegationExpiresInS: cfg.delegationExpiresInS,
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
// reduceCreate returns 201 for both a fresh anchor and the idempotent reuse of a
|
|
157
|
+
// still-live one (W1 deterministic sid), so 201 is the only success status.
|
|
158
|
+
anchorPersisted = createResponse.status === 201;
|
|
159
|
+
if (!anchorPersisted) {
|
|
160
|
+
console.warn(`[kya-http] pickup anchor create returned ${createResponse.status} (sid=${sid}); suppressing §8.1 challenge`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.warn(`[kya-http] pickup anchor create failed (sid=${sid}); suppressing §8.1 challenge:`, error instanceof Error ? error.message : String(error));
|
|
165
|
+
anchorPersisted = false;
|
|
166
|
+
}
|
|
167
|
+
if (!anchorPersisted) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const challengeProof = await signChallengeProof(await deps.proofGenerator(), {
|
|
172
|
+
sid,
|
|
173
|
+
consent_uri: buildConsentUri(verdict),
|
|
174
|
+
required_scopes: verdict.requiredScopes,
|
|
175
|
+
});
|
|
176
|
+
// Cooperative fetchers get the body-readable 200; everyone else the spec 401.
|
|
177
|
+
return wantsBodyReadable200
|
|
178
|
+
? buildFetcherChallengeResponse(verdict, challengeProof)
|
|
179
|
+
: buildChallengeResponse(verdict, challengeProof);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
// The anchor persisted, but signing/building the challenge failed (e.g. a proof
|
|
183
|
+
// generator outage). Suppress the 401 and fall through to the original MCP error
|
|
184
|
+
// rather than throwing into a bare 500 — symmetric with the create-failure path
|
|
185
|
+
// above. The orphaned anchor is harmless: it expires via its ≤600s TTL.
|
|
186
|
+
console.warn(`[kya-http] challenge proof signing failed (sid=${sid}); suppressing §8.1 challenge:`, error instanceof Error ? error.message : String(error));
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
//# sourceMappingURL=challenge-middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge-middleware.js","sourceRoot":"","sources":["../../src/delegation-http/challenge-middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draft-kya-http-02 delegation layer — §8.1 challenge emitter (builders + signing).
|
|
3
|
+
*
|
|
4
|
+
* Byte-exact against the vendored golden fixture
|
|
5
|
+
* `src/__tests__/fixtures/kya-http/challenge.delegation.full.json`. The wire format
|
|
6
|
+
* is frozen: param order is `CHALLENGE_PARAM_ORDER`, the separator is `", "`, every
|
|
7
|
+
* value is a quoted string, `scope` is space-joined, and `min_assurance` is omitted
|
|
8
|
+
* ENTIRELY when absent (the whole `min_assurance="…"` token is dropped, not emitted
|
|
9
|
+
* empty).
|
|
10
|
+
*
|
|
11
|
+
* The worker is its own Tier-2 authorization host: `consent_uri`/`pickup_uri` point
|
|
12
|
+
* at `authorizationHost`, while the Bearer challenge's `resource_metadata` points at
|
|
13
|
+
* the `realm` (the protected resource origin).
|
|
14
|
+
*
|
|
15
|
+
* @module @kya-os/mcp-i-cloudflare/delegation-http/challenge
|
|
16
|
+
*/
|
|
17
|
+
import type { ChallengeVerdictInput } from "./types";
|
|
18
|
+
import type { CloudflareProofGenerator } from "../proof-generator";
|
|
19
|
+
/**
|
|
20
|
+
* Build the `WWW-Authenticate: Delegation …` header value (§8.1), byte-exact.
|
|
21
|
+
*
|
|
22
|
+
* Params are emitted in `CHALLENGE_PARAM_ORDER`, joined by `", "`, every value
|
|
23
|
+
* quoted. `min_assurance` is dropped entirely when `i.minAssurance` is absent.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildWWWAuthenticateDelegation(i: ChallengeVerdictInput): string;
|
|
26
|
+
/**
|
|
27
|
+
* Build the companion `WWW-Authenticate: Bearer …` header value. `resource_metadata`
|
|
28
|
+
* points at the protected resource (`realm`), not the authorization host.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildWWWAuthenticateBearer(realm: string): string;
|
|
31
|
+
/** The consent surface URL on the worker's authorization host: `…/consent?sid=<sid>`. */
|
|
32
|
+
export declare function buildConsentUri(i: ChallengeVerdictInput): string;
|
|
33
|
+
/** The pickup endpoint URL on the worker's authorization host: `…/api/delegation/pickup/<sid>`. */
|
|
34
|
+
export declare function buildPickupUri(i: ChallengeVerdictInput): string;
|
|
35
|
+
/**
|
|
36
|
+
* Build the human-facing `instructions` string (§8.1). Carries the four REQUIRED
|
|
37
|
+
* elements: (1) consent-uri relay, (2) pickup-uri + interval, (3) pinning
|
|
38
|
+
* requirement, (4) retry presentation. Byte-exact vs the fixture given its inputs.
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildInstructions(i: ChallengeVerdictInput): string;
|
|
41
|
+
/**
|
|
42
|
+
* Build the §8.1 401 JSON body. Key order mirrors the golden fixture's `http.body`.
|
|
43
|
+
* `min_assurance` is omitted when absent.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildChallengeBody(i: ChallengeVerdictInput, challengeProof: string): object;
|
|
46
|
+
/**
|
|
47
|
+
* Build the full §8.1 401 `Response`: dual `WWW-Authenticate` challenges (Delegation
|
|
48
|
+
* first, then Bearer), `Retry-After`, JSON content type, and `Cache-Control:
|
|
49
|
+
* no-store`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildChallengeResponse(i: ChallengeVerdictInput, challengeProof: string): Response;
|
|
52
|
+
/**
|
|
53
|
+
* Build the body-readable §8.1 **200** `Response` for AI-agent fetchers that drop 4xx
|
|
54
|
+
* bodies (draft-kya-http-02 fetcher-compatibility envelope). REUSES `buildChallengeBody`
|
|
55
|
+
* so the JSON body is BYTE-IDENTICAL to the 401 — the org-signed `challenge_proof` binds
|
|
56
|
+
* the body content, not the HTTP envelope, so the same proof verifies at 200 or 401.
|
|
57
|
+
*
|
|
58
|
+
* Differs from the 401 (`buildChallengeResponse`) only in the envelope:
|
|
59
|
+
* - status `200`, not `401`
|
|
60
|
+
* - NO `WWW-Authenticate` (a 200 carrying it is reinterpreted as a 401 by some fetchers,
|
|
61
|
+
* re-dropping the body)
|
|
62
|
+
* - NO `Retry-After` (cadence lives in the body `poll_interval_s` + instructions)
|
|
63
|
+
* - adds the consent hint headers `KYA-Auth-Required`/`KYA-Auth-Url`/`Link` and the
|
|
64
|
+
* `KYA-Action: challenge` discriminator.
|
|
65
|
+
*
|
|
66
|
+
* The authoritative consent target is the proof-covered body `consent_uri`; the
|
|
67
|
+
* `KYA-Auth-Url`/`Link` headers are UNSIGNED hints for fetchers that only surface
|
|
68
|
+
* headers. Selection is a cooperative-UX bridge, NOT an access control.
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildFetcherChallengeResponse(i: ChallengeVerdictInput, challengeProof: string): Response;
|
|
71
|
+
/**
|
|
72
|
+
* Sign the `challenge_proof`: a verifiable 3-segment compact JWS whose `requestHash`
|
|
73
|
+
* commits to `{ sid, consent_uri, required_scopes }`.
|
|
74
|
+
*
|
|
75
|
+
* Implemented over the public `CloudflareProofGenerator.generateProof` seam: the
|
|
76
|
+
* challenge fields are canonicalized into the proof's request, so the signed
|
|
77
|
+
* `requestHash` cryptographically covers them. A verifier recomputes
|
|
78
|
+
* `sha256(canonical({ method: "kyaos/challenge", params }))` and checks it equals the
|
|
79
|
+
* payload's `requestHash`, then verifies the Ed25519 signature.
|
|
80
|
+
*/
|
|
81
|
+
export declare function signChallengeProof(gen: CloudflareProofGenerator, payload: {
|
|
82
|
+
sid: string;
|
|
83
|
+
consent_uri: string;
|
|
84
|
+
required_scopes: string[];
|
|
85
|
+
}): Promise<string>;
|
|
86
|
+
//# sourceMappingURL=challenge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../../src/delegation-http/challenge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAUnE;;;;;GAKG;AACH,wBAAgB,8BAA8B,CAC5C,CAAC,EAAE,qBAAqB,GACvB,MAAM,CAqBR;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,yFAAyF;AACzF,wBAAgB,eAAe,CAAC,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAEhE;AAED,mGAAmG;AACnG,wBAAgB,cAAc,CAAC,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAE/D;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAUlE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,CAAC,EAAE,qBAAqB,EACxB,cAAc,EAAE,MAAM,GACrB,MAAM,CAkBR;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,CAAC,EAAE,qBAAqB,EACxB,cAAc,EAAE,MAAM,GACrB,QAAQ,CAYV;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAC3C,CAAC,EAAE,qBAAqB,EACxB,cAAc,EAAE,MAAM,GACrB,QAAQ,CAcV;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,wBAAwB,EAC7B,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,EAAE,CAAA;CAAE,GACvE,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draft-kya-http-02 delegation layer — §8.1 challenge emitter (builders + signing).
|
|
3
|
+
*
|
|
4
|
+
* Byte-exact against the vendored golden fixture
|
|
5
|
+
* `src/__tests__/fixtures/kya-http/challenge.delegation.full.json`. The wire format
|
|
6
|
+
* is frozen: param order is `CHALLENGE_PARAM_ORDER`, the separator is `", "`, every
|
|
7
|
+
* value is a quoted string, `scope` is space-joined, and `min_assurance` is omitted
|
|
8
|
+
* ENTIRELY when absent (the whole `min_assurance="…"` token is dropped, not emitted
|
|
9
|
+
* empty).
|
|
10
|
+
*
|
|
11
|
+
* The worker is its own Tier-2 authorization host: `consent_uri`/`pickup_uri` point
|
|
12
|
+
* at `authorizationHost`, while the Bearer challenge's `resource_metadata` points at
|
|
13
|
+
* the `realm` (the protected resource origin).
|
|
14
|
+
*
|
|
15
|
+
* @module @kya-os/mcp-i-cloudflare/delegation-http/challenge
|
|
16
|
+
*/
|
|
17
|
+
import { CHALLENGE_PARAM_ORDER, KYAOS_ERRORS } from "./constants";
|
|
18
|
+
/**
|
|
19
|
+
* The JSON-RPC method label committed inside the `challenge_proof` JWS. The proof's
|
|
20
|
+
* `requestHash` is `sha256(canonical({ method, params: { sid, consent_uri,
|
|
21
|
+
* required_scopes } }))`, so the signed proof cryptographically covers the three
|
|
22
|
+
* challenge fields. Verifiers recompute this hash to confirm coverage.
|
|
23
|
+
*/
|
|
24
|
+
const CHALLENGE_PROOF_METHOD = "kyaos/challenge";
|
|
25
|
+
/**
|
|
26
|
+
* Build the `WWW-Authenticate: Delegation …` header value (§8.1), byte-exact.
|
|
27
|
+
*
|
|
28
|
+
* Params are emitted in `CHALLENGE_PARAM_ORDER`, joined by `", "`, every value
|
|
29
|
+
* quoted. `min_assurance` is dropped entirely when `i.minAssurance` is absent.
|
|
30
|
+
*/
|
|
31
|
+
export function buildWWWAuthenticateDelegation(i) {
|
|
32
|
+
const values = {
|
|
33
|
+
realm: i.realm,
|
|
34
|
+
sid: i.sid,
|
|
35
|
+
consent_uri: buildConsentUri(i),
|
|
36
|
+
pickup_uri: buildPickupUri(i),
|
|
37
|
+
scope: i.requiredScopes.join(" "),
|
|
38
|
+
min_assurance: i.minAssurance,
|
|
39
|
+
interval: String(i.pollIntervalS),
|
|
40
|
+
kind: "delegation",
|
|
41
|
+
};
|
|
42
|
+
const params = CHALLENGE_PARAM_ORDER.filter(
|
|
43
|
+
// min_assurance is the only optional param: omit the whole token when absent.
|
|
44
|
+
(key) => values[key] !== undefined).map((key) => `${key}="${values[key]}"`);
|
|
45
|
+
return `Delegation ${params.join(", ")}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the companion `WWW-Authenticate: Bearer …` header value. `resource_metadata`
|
|
49
|
+
* points at the protected resource (`realm`), not the authorization host.
|
|
50
|
+
*/
|
|
51
|
+
export function buildWWWAuthenticateBearer(realm) {
|
|
52
|
+
return `Bearer resource_metadata="https://${realm}/.well-known/oauth-protected-resource"`;
|
|
53
|
+
}
|
|
54
|
+
/** The consent surface URL on the worker's authorization host: `…/consent?sid=<sid>`. */
|
|
55
|
+
export function buildConsentUri(i) {
|
|
56
|
+
return `https://${i.authorizationHost}/consent?sid=${i.sid}`;
|
|
57
|
+
}
|
|
58
|
+
/** The pickup endpoint URL on the worker's authorization host: `…/api/delegation/pickup/<sid>`. */
|
|
59
|
+
export function buildPickupUri(i) {
|
|
60
|
+
return `https://${i.authorizationHost}/api/delegation/pickup/${i.sid}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the human-facing `instructions` string (§8.1). Carries the four REQUIRED
|
|
64
|
+
* elements: (1) consent-uri relay, (2) pickup-uri + interval, (3) pinning
|
|
65
|
+
* requirement, (4) retry presentation. Byte-exact vs the fixture given its inputs.
|
|
66
|
+
*/
|
|
67
|
+
export function buildInstructions(i) {
|
|
68
|
+
const consentUri = buildConsentUri(i);
|
|
69
|
+
const pickupUri = buildPickupUri(i);
|
|
70
|
+
return (`This action requires human authorization. Show your user this link and ask them to approve: ${consentUri}. ` +
|
|
71
|
+
`Then poll ${pickupUri} every ${i.pollIntervalS} seconds. ` +
|
|
72
|
+
`If you can sign HTTP requests, sign each poll with an ephemeral did:key; ` +
|
|
73
|
+
`otherwise generate one random 32-byte token and send it as X-KYA-Pickup-Token on EVERY poll. ` +
|
|
74
|
+
`When the response status is 200, take the 'delegation' value and retry your original request with header 'Authorization: Bearer <delegation>'.`);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Build the §8.1 401 JSON body. Key order mirrors the golden fixture's `http.body`.
|
|
78
|
+
* `min_assurance` is omitted when absent.
|
|
79
|
+
*/
|
|
80
|
+
export function buildChallengeBody(i, challengeProof) {
|
|
81
|
+
const kyaos = {
|
|
82
|
+
sid: i.sid,
|
|
83
|
+
consent_uri: buildConsentUri(i),
|
|
84
|
+
pickup_uri: buildPickupUri(i),
|
|
85
|
+
required_scopes: i.requiredScopes,
|
|
86
|
+
};
|
|
87
|
+
if (i.minAssurance !== undefined) {
|
|
88
|
+
kyaos.min_assurance = i.minAssurance;
|
|
89
|
+
}
|
|
90
|
+
kyaos.poll_interval_s = i.pollIntervalS;
|
|
91
|
+
kyaos.challenge_proof = challengeProof;
|
|
92
|
+
return {
|
|
93
|
+
error: KYAOS_ERRORS.authorizationRequired,
|
|
94
|
+
kyaos,
|
|
95
|
+
instructions: buildInstructions(i),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build the full §8.1 401 `Response`: dual `WWW-Authenticate` challenges (Delegation
|
|
100
|
+
* first, then Bearer), `Retry-After`, JSON content type, and `Cache-Control:
|
|
101
|
+
* no-store`.
|
|
102
|
+
*/
|
|
103
|
+
export function buildChallengeResponse(i, challengeProof) {
|
|
104
|
+
const headers = new Headers();
|
|
105
|
+
headers.append("WWW-Authenticate", buildWWWAuthenticateDelegation(i));
|
|
106
|
+
headers.append("WWW-Authenticate", buildWWWAuthenticateBearer(i.realm));
|
|
107
|
+
headers.set("Retry-After", String(i.pollIntervalS));
|
|
108
|
+
headers.set("Content-Type", "application/json");
|
|
109
|
+
headers.set("Cache-Control", "no-store");
|
|
110
|
+
return new Response(JSON.stringify(buildChallengeBody(i, challengeProof)), {
|
|
111
|
+
status: 401,
|
|
112
|
+
headers,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Build the body-readable §8.1 **200** `Response` for AI-agent fetchers that drop 4xx
|
|
117
|
+
* bodies (draft-kya-http-02 fetcher-compatibility envelope). REUSES `buildChallengeBody`
|
|
118
|
+
* so the JSON body is BYTE-IDENTICAL to the 401 — the org-signed `challenge_proof` binds
|
|
119
|
+
* the body content, not the HTTP envelope, so the same proof verifies at 200 or 401.
|
|
120
|
+
*
|
|
121
|
+
* Differs from the 401 (`buildChallengeResponse`) only in the envelope:
|
|
122
|
+
* - status `200`, not `401`
|
|
123
|
+
* - NO `WWW-Authenticate` (a 200 carrying it is reinterpreted as a 401 by some fetchers,
|
|
124
|
+
* re-dropping the body)
|
|
125
|
+
* - NO `Retry-After` (cadence lives in the body `poll_interval_s` + instructions)
|
|
126
|
+
* - adds the consent hint headers `KYA-Auth-Required`/`KYA-Auth-Url`/`Link` and the
|
|
127
|
+
* `KYA-Action: challenge` discriminator.
|
|
128
|
+
*
|
|
129
|
+
* The authoritative consent target is the proof-covered body `consent_uri`; the
|
|
130
|
+
* `KYA-Auth-Url`/`Link` headers are UNSIGNED hints for fetchers that only surface
|
|
131
|
+
* headers. Selection is a cooperative-UX bridge, NOT an access control.
|
|
132
|
+
*/
|
|
133
|
+
export function buildFetcherChallengeResponse(i, challengeProof) {
|
|
134
|
+
const consentUri = buildConsentUri(i);
|
|
135
|
+
const headers = new Headers();
|
|
136
|
+
headers.set("Content-Type", "application/json");
|
|
137
|
+
headers.set("Cache-Control", "no-store");
|
|
138
|
+
headers.set("KYA-Auth-Required", "true");
|
|
139
|
+
headers.set("KYA-Auth-Url", consentUri);
|
|
140
|
+
headers.set("Link", `<${consentUri}>; rel="kya-authorize"`);
|
|
141
|
+
headers.set("KYA-Action", "challenge");
|
|
142
|
+
return new Response(JSON.stringify(buildChallengeBody(i, challengeProof)), {
|
|
143
|
+
status: 200,
|
|
144
|
+
headers,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Sign the `challenge_proof`: a verifiable 3-segment compact JWS whose `requestHash`
|
|
149
|
+
* commits to `{ sid, consent_uri, required_scopes }`.
|
|
150
|
+
*
|
|
151
|
+
* Implemented over the public `CloudflareProofGenerator.generateProof` seam: the
|
|
152
|
+
* challenge fields are canonicalized into the proof's request, so the signed
|
|
153
|
+
* `requestHash` cryptographically covers them. A verifier recomputes
|
|
154
|
+
* `sha256(canonical({ method: "kyaos/challenge", params }))` and checks it equals the
|
|
155
|
+
* payload's `requestHash`, then verifies the Ed25519 signature.
|
|
156
|
+
*/
|
|
157
|
+
export async function signChallengeProof(gen, payload) {
|
|
158
|
+
const proof = await gen.generateProof({
|
|
159
|
+
method: CHALLENGE_PROOF_METHOD,
|
|
160
|
+
params: {
|
|
161
|
+
sid: payload.sid,
|
|
162
|
+
consent_uri: payload.consent_uri,
|
|
163
|
+
required_scopes: payload.required_scopes,
|
|
164
|
+
},
|
|
165
|
+
}, { data: { sid: payload.sid } }, {
|
|
166
|
+
// The session binds the proof to this anchor; sid is the natural nonce.
|
|
167
|
+
nonce: payload.sid,
|
|
168
|
+
audience: payload.consent_uri,
|
|
169
|
+
sessionId: payload.sid,
|
|
170
|
+
});
|
|
171
|
+
return proof.jws;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=challenge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.js","sourceRoot":"","sources":["../../src/delegation-http/challenge.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* draft-kya-http-02 delegation layer — consent → pickup bridge.
|
|
3
|
+
*
|
|
4
|
+
* The worker is its own Tier-2 authorization host: the §8.1 challenge points the
|
|
5
|
+
* caller's user at `consent_uri = /consent?sid=kya-…`, served here. This module is
|
|
6
|
+
* the route-order interception that `register.ts` wires BEFORE the legacy `/consent`
|
|
7
|
+
* routes; it only ever handles requests whose query carries an anchor sid (the
|
|
8
|
+
* `next()` fall-through for everything else lives in `register.ts`), so the legacy
|
|
9
|
+
* consent service is untouched.
|
|
10
|
+
*
|
|
11
|
+
* - `GET /consent?sid=kya-…` reads the anchor via `INTERNAL_PICKUP_INFO`. An unknown
|
|
12
|
+
* sid → 404 page; an expired sid → a "link expired" page; otherwise a minimal
|
|
13
|
+
* approve form (the consent-only scope list + a CSRF-protected POST).
|
|
14
|
+
* - `POST /consent/approve?sid=kya-…` mints a delegation VC-JWT using the SAME
|
|
15
|
+
* `@kya-os/mcp` primitives `consent.service.ts` uses (`wrapDelegationAsVC` →
|
|
16
|
+
* `createUnsignedVCJWT` → sign → `completeVCJWT`) — calling them directly here, NOT
|
|
17
|
+
* editing the service — then parks it on the anchor via `INTERNAL_PICKUP_APPROVE`.
|
|
18
|
+
* If the anchor is gone (DO 410/404) the approval FAILS with an error page and
|
|
19
|
+
* mints nothing reachable: consent on an EXPIRED anchor MUST fail.
|
|
20
|
+
*
|
|
21
|
+
* @module @kya-os/mcp-i-cloudflare/delegation-http/consent-anchor-bridge
|
|
22
|
+
*/
|
|
23
|
+
import type { HonoContext } from "../runtime/oauth-handler";
|
|
24
|
+
import type { CloudflareEnv } from "../types";
|
|
25
|
+
import type { HttpChallengeConfig } from "../config";
|
|
26
|
+
/**
|
|
27
|
+
* Render the anchor consent surface for `GET /consent?sid=kya-…`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function handleAnchorConsentGet(c: HonoContext, sid: string, env: CloudflareEnv, _cfg: Required<HttpChallengeConfig>): Promise<Response>;
|
|
30
|
+
/**
|
|
31
|
+
* Handle anchor approval for `POST /consent/approve?sid=kya-…`. Mints a VC-JWT and
|
|
32
|
+
* parks it on the anchor; an expired/unknown anchor makes the approval FAIL.
|
|
33
|
+
*/
|
|
34
|
+
export declare function handleAnchorConsentApprove(c: HonoContext, sid: string, env: CloudflareEnv, cfg: Required<HttpChallengeConfig>): Promise<Response>;
|
|
35
|
+
//# sourceMappingURL=consent-anchor-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"consent-anchor-bridge.d.ts","sourceRoot":"","sources":["../../src/delegation-http/consent-anchor-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAkWrD;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,CAAC,EAAE,WAAW,EACd,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,aAAa,EAClB,IAAI,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GAClC,OAAO,CAAC,QAAQ,CAAC,CAiBnB;AAED;;;GAGG;AACH,wBAAsB,0BAA0B,CAC9C,CAAC,EAAE,WAAW,EACd,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,aAAa,EAClB,GAAG,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GACjC,OAAO,CAAC,QAAQ,CAAC,CAyEnB"}
|