@dupecom/botcha-cloudflare 0.20.0 → 0.20.2
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/dashboard/auth.d.ts +1 -1
- package/dist/dashboard/auth.d.ts.map +1 -1
- package/dist/dashboard/auth.js +11 -4
- package/dist/dashboard/device-code.d.ts +1 -1
- package/dist/dashboard/device-code.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +118 -76
- package/dist/tap-attestation-routes.d.ts.map +1 -1
- package/dist/tap-attestation-routes.js +30 -10
- package/dist/tap-delegation-routes.d.ts.map +1 -1
- package/dist/tap-delegation-routes.js +30 -10
- package/dist/tap-reputation-routes.d.ts.map +1 -1
- package/dist/tap-reputation-routes.js +30 -10
- package/dist/tap-routes.d.ts +10 -1
- package/dist/tap-routes.d.ts.map +1 -1
- package/dist/tap-routes.js +64 -31
- package/package.json +1 -1
package/dist/dashboard/auth.d.ts
CHANGED
|
@@ -113,7 +113,7 @@ export declare function handleDeviceCodeChallenge(c: Context<{
|
|
|
113
113
|
* POST /v1/auth/device-code/verify
|
|
114
114
|
*
|
|
115
115
|
* Agent submits challenge solution. On success, returns a short-lived
|
|
116
|
-
* device code (BOTCHA-
|
|
116
|
+
* device code (BOTCHA-XXXXXX) that a human can enter at /dashboard/code.
|
|
117
117
|
*/
|
|
118
118
|
export declare function handleDeviceCodeVerify(c: Context<{
|
|
119
119
|
Bindings: Bindings;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAOvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAKjD,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAIF;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAc5F;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAQhE;AAID;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAmChG,CAAC;AAIF;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2CpF;AAgDD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEAwBjF;AAID;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEAEjF;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEA4B9E;AAID;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBA2E5E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFA4B9E;AAID;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAyBnE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAGpE;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFA2BxE;AA4BD;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBAgFvE"}
|
package/dist/dashboard/auth.js
CHANGED
|
@@ -4,6 +4,7 @@ import { SignJWT } from 'jose';
|
|
|
4
4
|
import { verifyToken } from '../auth';
|
|
5
5
|
import { validateAppSecret, getAppByEmail } from '../apps';
|
|
6
6
|
import { sendEmail, recoveryEmail } from '../email';
|
|
7
|
+
import { checkRateLimit, getClientIP } from '../rate-limit';
|
|
7
8
|
import { generateDeviceCode, storeDeviceCode, redeemDeviceCode } from './device-code';
|
|
8
9
|
import { LoginLayout, Card, Divider } from './layout';
|
|
9
10
|
// ============ SESSION HELPERS ============
|
|
@@ -195,7 +196,7 @@ export async function handleDeviceCodeChallenge(c) {
|
|
|
195
196
|
* POST /v1/auth/device-code/verify
|
|
196
197
|
*
|
|
197
198
|
* Agent submits challenge solution. On success, returns a short-lived
|
|
198
|
-
* device code (BOTCHA-
|
|
199
|
+
* device code (BOTCHA-XXXXXX) that a human can enter at /dashboard/code.
|
|
199
200
|
*/
|
|
200
201
|
export async function handleDeviceCodeVerify(c) {
|
|
201
202
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -247,12 +248,12 @@ export async function renderDeviceCodePage(c) {
|
|
|
247
248
|
};
|
|
248
249
|
return c.html(_jsxs(LoginLayout, { title: "Enter Device Code - BOTCHA", children: [_jsxs("div", { style: "font-size: 0.875rem; font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; text-align: center; margin-bottom: 2rem;", children: ['>', "_\u00A0BOTCHA"] }), _jsx("form", { method: "post", action: "/dashboard/code", children: _jsxs(Card, { title: "Device Code", children: [emailSent && (_jsx("div", { class: "success-message", style: "background: #f0faf0; border: 1px solid #1a8a2a; color: #1a6a1a; padding: 0.75rem; font-size: 0.75rem; margin-bottom: 1rem;", children: "If an account with that email exists, a login code has been sent. Check your inbox." })), error && errorMap[error] && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: emailSent
|
|
249
250
|
? 'Enter the code from your email below.'
|
|
250
|
-
: 'Your AI agent generated a login code for you. Enter it below to access the dashboard.' }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "code", children: "Code" }), _jsxs("div", { style: "display: flex; align-items: stretch; gap: 0;", children: [_jsx("span", { style: "display: flex; align-items: center; font-size: 1.5rem; font-weight: 700; letter-spacing: 0.15em; padding: 0 0.25rem 0 1rem; border: 1px solid var(--border); border-right: none; background: var(--bg-raised); color: var(--text-muted);", children: "BOTCHA-" }), _jsx("input", { type: "text", id: "code", name: "code", placeholder: "
|
|
251
|
+
: 'Your AI agent generated a login code for you. Enter it below to access the dashboard.' }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "code", children: "Code" }), _jsxs("div", { style: "display: flex; align-items: stretch; gap: 0;", children: [_jsx("span", { style: "display: flex; align-items: center; font-size: 1.5rem; font-weight: 700; letter-spacing: 0.15em; padding: 0 0.25rem 0 1rem; border: 1px solid var(--border); border-right: none; background: var(--bg-raised); color: var(--text-muted);", children: "BOTCHA-" }), _jsx("input", { type: "text", id: "code", name: "code", placeholder: "XXXXXX", value: prefill, required: true, autocomplete: "off", maxlength: 13, style: "font-size: 1.5rem; font-weight: 700; text-align: center; letter-spacing: 0.15em; padding: 1rem; flex: 1; border-left: none;" })] })] }), _jsx("script", { dangerouslySetInnerHTML: { __html: `
|
|
251
252
|
document.getElementById('code').addEventListener('input', function(e) {
|
|
252
253
|
var v = e.target.value.toUpperCase().replace(/[^A-Z0-9-]/g, '');
|
|
253
254
|
if (v.startsWith('BOTCHA-')) v = v.slice(7);
|
|
254
255
|
else if (v.startsWith('BOTCHA')) v = v.slice(6);
|
|
255
|
-
e.target.value = v.slice(0,
|
|
256
|
+
e.target.value = v.slice(0, 6);
|
|
256
257
|
});
|
|
257
258
|
` } }), _jsxs("button", { type: "submit", children: ["Verify Code ", '>'] })] }) }), _jsxs("div", { class: "hint", style: "text-align: center; line-height: 1.8; margin-top: 1.5rem;", children: ["Don't have a code? Ask your AI agent to run:", _jsx("br", {}), _jsx("code", { children: "POST /v1/auth/device-code" }), _jsx("br", {}), _jsx("br", {}), _jsx("a", { href: "/dashboard/login", children: "Back to login" })] })] }));
|
|
258
259
|
}
|
|
@@ -262,12 +263,18 @@ export async function renderDeviceCodePage(c) {
|
|
|
262
263
|
* Human submits device code. If valid, creates session and redirects to dashboard.
|
|
263
264
|
*/
|
|
264
265
|
export async function handleDeviceCodeRedeem(c) {
|
|
266
|
+
// Brute-force guard for device code redemption attempts (20/hour/IP).
|
|
267
|
+
const clientIp = getClientIP(c.req.raw);
|
|
268
|
+
const redemptionRate = await checkRateLimit(c.env.RATE_LIMITS, `device-code:${clientIp}`, 20);
|
|
269
|
+
if (!redemptionRate.allowed) {
|
|
270
|
+
return c.redirect('/dashboard/code?error=invalid');
|
|
271
|
+
}
|
|
265
272
|
const body = await c.req.parseBody();
|
|
266
273
|
let code = (body.code || '').trim().toUpperCase();
|
|
267
274
|
if (!code) {
|
|
268
275
|
return c.redirect('/dashboard/code?error=missing');
|
|
269
276
|
}
|
|
270
|
-
// Normalize: accept both "
|
|
277
|
+
// Normalize: accept both "AR8C2Q" and "BOTCHA-AR8C2Q", always look up with prefix.
|
|
271
278
|
if (!code.startsWith('BOTCHA-')) {
|
|
272
279
|
code = `BOTCHA-${code}`;
|
|
273
280
|
}
|
|
@@ -22,7 +22,7 @@ export interface DeviceCodeData {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Generate a human-friendly device code.
|
|
25
|
-
* Format: BOTCHA-
|
|
25
|
+
* Format: BOTCHA-XXXXXX (6 alphanumeric chars, no ambiguous chars)
|
|
26
26
|
*
|
|
27
27
|
* Uses a restricted alphabet that avoids 0/O, 1/I/l confusion.
|
|
28
28
|
*/
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
// ============ CODE GENERATION ============
|
|
16
16
|
/**
|
|
17
17
|
* Generate a human-friendly device code.
|
|
18
|
-
* Format: BOTCHA-
|
|
18
|
+
* Format: BOTCHA-XXXXXX (6 alphanumeric chars, no ambiguous chars)
|
|
19
19
|
*
|
|
20
20
|
* Uses a restricted alphabet that avoids 0/O, 1/I/l confusion.
|
|
21
21
|
*/
|
|
22
22
|
export function generateDeviceCode() {
|
|
23
23
|
const alphabet = '23456789ABCDEFGHJKMNPQRSTUVWXYZ'; // no 0,O,1,I,L
|
|
24
|
-
const bytes = new Uint8Array(
|
|
24
|
+
const bytes = new Uint8Array(6);
|
|
25
25
|
crypto.getRandomValues(bytes);
|
|
26
26
|
const chars = Array.from(bytes).map(b => alphabet[b % alphabet.length]).join('');
|
|
27
27
|
return `BOTCHA-${chars}`;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAYL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAwDtB,OAAO,EACL,KAAK,sBAAsB,EAM5B,MAAM,aAAa,CAAC;AAGrB,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,WAAW,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,YAAY,CAAC,EAAE;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,iBAAiB,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAG5B,OAAO,EAYL,KAAK,WAAW,EACjB,MAAM,cAAc,CAAC;AAwDtB,OAAO,EACL,KAAK,sBAAsB,EAM5B,MAAM,aAAa,CAAC;AAGrB,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,WAAW,CAAC;IACtB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,WAAW,CAAC;IACtB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,KAAK,SAAS,GAAG;IACf,YAAY,CAAC,EAAE;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,iBAAiB,CAAC;QACxB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH,CAAC;AAEF,QAAA,MAAM,GAAG;cAAwB,QAAQ;eAAa,SAAS;yCAAK,CAAC;AAuzErE,eAAe,GAAG,CAAC;AAGnB,OAAO,EACL,sBAAsB,EACtB,oBAAoB,EACpB,yBAAyB,EACzB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,uBAAuB,EACvB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,sBAAsB,EAAE,MAAM,QAAQ,CAAC;AAC5E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EACL,aAAa,EACb,WAAW,EACX,mBAAmB,EACnB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,KAAK,EACV,KAAK,YAAY,GAClB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -40,7 +40,7 @@ app.route('/dashboard', dashboardRoutes);
|
|
|
40
40
|
// BOTCHA discovery headers
|
|
41
41
|
app.use('*', async (c, next) => {
|
|
42
42
|
await next();
|
|
43
|
-
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.20.
|
|
43
|
+
c.header('X-Botcha-Version', c.env.BOTCHA_VERSION || '0.20.2');
|
|
44
44
|
c.header('X-Botcha-Enabled', 'true');
|
|
45
45
|
c.header('X-Botcha-Methods', 'speed-challenge,reasoning-challenge,hybrid-challenge,standard-challenge,jwt-token');
|
|
46
46
|
c.header('X-Botcha-Docs', 'https://botcha.ai/openapi.json');
|
|
@@ -76,7 +76,13 @@ async function validateAppId(appId, appsKV) {
|
|
|
76
76
|
try {
|
|
77
77
|
const app = await getApp(appsKV, appId);
|
|
78
78
|
if (!app) {
|
|
79
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
valid: false,
|
|
81
|
+
error: `App not found: ${appId}. ` +
|
|
82
|
+
`app_id is OPTIONAL — remove it to get a token without an app. ` +
|
|
83
|
+
`To register an app: POST https://botcha.ai/v1/apps with {"email": "you@example.com", "name": "My App"}. ` +
|
|
84
|
+
`App IDs look like: app_a1b2c3d4e5f6a7b8`
|
|
85
|
+
};
|
|
80
86
|
}
|
|
81
87
|
return { valid: true };
|
|
82
88
|
}
|
|
@@ -125,6 +131,61 @@ async function requireJWT(c, next) {
|
|
|
125
131
|
c.set('tokenPayload', result.payload);
|
|
126
132
|
await next();
|
|
127
133
|
}
|
|
134
|
+
// Resolve app_id from authenticated JWT and enforce optional query consistency.
|
|
135
|
+
async function resolveAuthenticatedAppId(c) {
|
|
136
|
+
const authHeader = c.req.header('authorization');
|
|
137
|
+
const token = extractBearerToken(authHeader);
|
|
138
|
+
if (!token) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
error: 'UNAUTHORIZED',
|
|
142
|
+
message: 'Missing Bearer token. Use POST /v1/token/verify to get a token.',
|
|
143
|
+
status: 401,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const publicKey = getPublicKey(c.env);
|
|
147
|
+
const verification = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
148
|
+
if (!verification.valid || !verification.payload) {
|
|
149
|
+
return {
|
|
150
|
+
ok: false,
|
|
151
|
+
error: 'INVALID_TOKEN',
|
|
152
|
+
message: verification.error || 'Token is invalid or expired',
|
|
153
|
+
status: 401,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const jwtAppId = verification.payload.app_id;
|
|
157
|
+
if (!jwtAppId) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: 'MISSING_APP_ID',
|
|
161
|
+
message: 'Token is missing app_id claim. Request a token scoped to your app.',
|
|
162
|
+
status: 403,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const queryAppId = c.req.query('app_id');
|
|
166
|
+
if (queryAppId && queryAppId !== jwtAppId) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: 'APP_ID_MISMATCH',
|
|
170
|
+
message: 'Query app_id does not match token app_id.',
|
|
171
|
+
status: 403,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const validation = await validateAppId(jwtAppId, c.env.APPS);
|
|
175
|
+
if (!validation.valid) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
error: 'INVALID_APP_ID',
|
|
179
|
+
message: validation.error || 'Invalid app_id',
|
|
180
|
+
status: 400,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
ok: true,
|
|
185
|
+
appId: jwtAppId,
|
|
186
|
+
status: 200,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
128
189
|
// ============ ROOT & INFO ============
|
|
129
190
|
// Detect request preference: 'markdown' | 'json' | 'html'
|
|
130
191
|
// Strategy: only return JSON/markdown when explicitly requested or from
|
|
@@ -192,10 +253,11 @@ app.get('/', async (c) => {
|
|
|
192
253
|
status: 'unverified',
|
|
193
254
|
message: 'Verify yourself to see the full API documentation.',
|
|
194
255
|
verify: {
|
|
195
|
-
step1: `GET ${baseUrl}/v1/token — receive a speed challenge (5 SHA-256 hashes)
|
|
256
|
+
step1: `GET ${baseUrl}/v1/token — receive a speed challenge (5 SHA-256 hashes). No app_id needed.`,
|
|
196
257
|
step2: 'Solve: compute SHA-256 of each number, return first 8 hex chars',
|
|
197
258
|
step3: `POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}`,
|
|
198
259
|
step4: `Revisit: GET ${baseUrl}/ with header Authorization: Bearer <access_token>`,
|
|
260
|
+
note: 'app_id is OPTIONAL. You can get tokens without registering an app. To register one for production use: POST /v1/apps {"email":"you@example.com","name":"My App"}',
|
|
199
261
|
},
|
|
200
262
|
human_handoff: {
|
|
201
263
|
note: 'After verifying, the response includes a human_link. Give it to your human to open in their browser.',
|
|
@@ -217,13 +279,16 @@ app.get('/', async (c) => {
|
|
|
217
279
|
|
|
218
280
|
## Verify Yourself
|
|
219
281
|
|
|
220
|
-
1. \`GET ${baseUrl}/v1/token\` — receive a speed challenge (5 SHA-256 hashes)
|
|
282
|
+
1. \`GET ${baseUrl}/v1/token\` — receive a speed challenge (5 SHA-256 hashes). **No app_id needed.**
|
|
221
283
|
2. Solve: compute SHA-256 of each number, return first 8 hex chars
|
|
222
284
|
3. \`POST ${baseUrl}/v1/token/verify {"id":"<challenge_id>","answers":["hash1","hash2",...]}\`
|
|
223
285
|
4. Revisit: \`GET ${baseUrl}/\` with header \`Authorization: Bearer <access_token>\`
|
|
224
286
|
|
|
225
287
|
You have 500ms. Only a bot can do this.
|
|
226
288
|
|
|
289
|
+
> **Note:** \`app_id\` is optional. You can get tokens without registering an app.
|
|
290
|
+
> To register for production: \`POST /v1/apps {"email":"you@example.com","name":"My App"}\`
|
|
291
|
+
|
|
227
292
|
## Human Handoff
|
|
228
293
|
|
|
229
294
|
After verifying, the response includes a \`human_link\`. Give this URL to your human:
|
|
@@ -710,13 +775,30 @@ app.post('/v1/token/verify', async (c) => {
|
|
|
710
775
|
}
|
|
711
776
|
// Get client IP from request headers
|
|
712
777
|
const clientIp = c.req.header('cf-connecting-ip') || c.req.header('x-forwarded-for') || 'unknown';
|
|
778
|
+
// Enforce challenge-bound app_id; never allow app_id override from request body.
|
|
779
|
+
const challengeAppId = result.app_id;
|
|
780
|
+
if (typeof app_id === 'string') {
|
|
781
|
+
if (!challengeAppId) {
|
|
782
|
+
return c.json({
|
|
783
|
+
success: false,
|
|
784
|
+
error: 'APP_ID_NOT_ALLOWED',
|
|
785
|
+
message: 'This challenge was issued without app_id. Request /v1/token?app_id=... first.',
|
|
786
|
+
}, 400);
|
|
787
|
+
}
|
|
788
|
+
if (app_id !== challengeAppId) {
|
|
789
|
+
return c.json({
|
|
790
|
+
success: false,
|
|
791
|
+
error: 'APP_ID_MISMATCH',
|
|
792
|
+
message: 'Provided app_id does not match the app_id bound to this challenge.',
|
|
793
|
+
}, 403);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
713
796
|
// Generate JWT tokens (access + refresh)
|
|
714
|
-
// Prefer app_id from request body, fall back to challenge's app_id (returned by verifySpeedChallenge)
|
|
715
797
|
const signingKey = getSigningKey(c.env);
|
|
716
798
|
const tokenResult = await generateToken(id, result.solveTimeMs || 0, c.env.JWT_SECRET, c.env, {
|
|
717
799
|
aud: audience,
|
|
718
800
|
clientIp: bind_ip ? clientIp : undefined,
|
|
719
|
-
app_id:
|
|
801
|
+
app_id: challengeAppId,
|
|
720
802
|
}, signingKey);
|
|
721
803
|
// Get badge information (for backward compatibility)
|
|
722
804
|
const baseUrl = new URL(c.req.url).origin;
|
|
@@ -1401,7 +1483,6 @@ app.get('/v1/apps/:id', async (c) => {
|
|
|
1401
1483
|
...(app.name && { name: app.name }),
|
|
1402
1484
|
created_at: new Date(app.created_at).toISOString(),
|
|
1403
1485
|
rate_limit: app.rate_limit,
|
|
1404
|
-
email: app.email,
|
|
1405
1486
|
email_verified: app.email_verified,
|
|
1406
1487
|
},
|
|
1407
1488
|
});
|
|
@@ -1513,27 +1594,30 @@ app.post('/v1/apps/:id/rotate-secret', async (c) => {
|
|
|
1513
1594
|
message: 'Authentication required. Use a dashboard session token (Bearer or cookie).',
|
|
1514
1595
|
}, 401);
|
|
1515
1596
|
}
|
|
1516
|
-
// Verify
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
const { payload } = await jwtVerify(authToken, secret);
|
|
1521
|
-
const tokenAppId = payload.app_id;
|
|
1522
|
-
if (tokenAppId !== app_id) {
|
|
1523
|
-
return c.json({
|
|
1524
|
-
success: false,
|
|
1525
|
-
error: 'FORBIDDEN',
|
|
1526
|
-
message: 'Session token does not match the requested app_id',
|
|
1527
|
-
}, 403);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
catch {
|
|
1597
|
+
// Verify dashboard session token includes this app_id.
|
|
1598
|
+
const publicKey = getPublicKey(c.env);
|
|
1599
|
+
const verification = await verifyToken(authToken, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
1600
|
+
if (!verification.valid || !verification.payload) {
|
|
1531
1601
|
return c.json({
|
|
1532
1602
|
success: false,
|
|
1533
1603
|
error: 'INVALID_TOKEN',
|
|
1534
1604
|
message: 'Invalid or expired session token',
|
|
1535
1605
|
}, 401);
|
|
1536
1606
|
}
|
|
1607
|
+
if (verification.payload.sub !== 'dashboard-session') {
|
|
1608
|
+
return c.json({
|
|
1609
|
+
success: false,
|
|
1610
|
+
error: 'FORBIDDEN',
|
|
1611
|
+
message: 'Dashboard session token required for secret rotation.',
|
|
1612
|
+
}, 403);
|
|
1613
|
+
}
|
|
1614
|
+
if (verification.payload.app_id !== app_id) {
|
|
1615
|
+
return c.json({
|
|
1616
|
+
success: false,
|
|
1617
|
+
error: 'FORBIDDEN',
|
|
1618
|
+
message: 'Session token does not match the requested app_id',
|
|
1619
|
+
}, 403);
|
|
1620
|
+
}
|
|
1537
1621
|
const appData = await getApp(c.env.APPS, app_id);
|
|
1538
1622
|
if (!appData) {
|
|
1539
1623
|
return c.json({ success: false, error: 'App not found' }, 404);
|
|
@@ -1601,36 +1685,15 @@ app.post('/v1/verify/attestation', verifyAttestationRoute);
|
|
|
1601
1685
|
// Register a new agent
|
|
1602
1686
|
app.post('/v1/agents/register', async (c) => {
|
|
1603
1687
|
try {
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
// Try to get from JWT Bearer token
|
|
1607
|
-
let jwtAppId;
|
|
1608
|
-
const authHeader = c.req.header('authorization');
|
|
1609
|
-
const token = extractBearerToken(authHeader);
|
|
1610
|
-
if (token) {
|
|
1611
|
-
const regPublicKey = getPublicKey(c.env);
|
|
1612
|
-
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, regPublicKey);
|
|
1613
|
-
if (result.valid && result.payload) {
|
|
1614
|
-
jwtAppId = result.payload.app_id;
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
const app_id = queryAppId || jwtAppId;
|
|
1618
|
-
if (!app_id) {
|
|
1619
|
-
return c.json({
|
|
1620
|
-
success: false,
|
|
1621
|
-
error: 'MISSING_APP_ID',
|
|
1622
|
-
message: 'app_id is required. Provide it as a query parameter (?app_id=...) or in the JWT token.',
|
|
1623
|
-
}, 401);
|
|
1624
|
-
}
|
|
1625
|
-
// Validate app_id exists
|
|
1626
|
-
const validation = await validateAppId(app_id, c.env.APPS);
|
|
1627
|
-
if (!validation.valid) {
|
|
1688
|
+
const appAccess = await resolveAuthenticatedAppId(c);
|
|
1689
|
+
if (!appAccess.ok) {
|
|
1628
1690
|
return c.json({
|
|
1629
1691
|
success: false,
|
|
1630
|
-
error:
|
|
1631
|
-
message:
|
|
1632
|
-
},
|
|
1692
|
+
error: appAccess.error,
|
|
1693
|
+
message: appAccess.message,
|
|
1694
|
+
}, appAccess.status);
|
|
1633
1695
|
}
|
|
1696
|
+
const app_id = appAccess.appId;
|
|
1634
1697
|
// Parse request body
|
|
1635
1698
|
const body = await c.req.json().catch(() => ({}));
|
|
1636
1699
|
const { name, operator, version } = body;
|
|
@@ -1708,36 +1771,15 @@ app.get('/v1/agents/:id', async (c) => {
|
|
|
1708
1771
|
// List all agents for an app
|
|
1709
1772
|
app.get('/v1/agents', async (c) => {
|
|
1710
1773
|
try {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
// Try to get from JWT Bearer token
|
|
1714
|
-
let jwtAppId;
|
|
1715
|
-
const authHeader = c.req.header('authorization');
|
|
1716
|
-
const token = extractBearerToken(authHeader);
|
|
1717
|
-
if (token) {
|
|
1718
|
-
const listPublicKey = getPublicKey(c.env);
|
|
1719
|
-
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, listPublicKey);
|
|
1720
|
-
if (result.valid && result.payload) {
|
|
1721
|
-
jwtAppId = result.payload.app_id;
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
const app_id = queryAppId || jwtAppId;
|
|
1725
|
-
if (!app_id) {
|
|
1774
|
+
const appAccess = await resolveAuthenticatedAppId(c);
|
|
1775
|
+
if (!appAccess.ok) {
|
|
1726
1776
|
return c.json({
|
|
1727
1777
|
success: false,
|
|
1728
|
-
error:
|
|
1729
|
-
message:
|
|
1730
|
-
},
|
|
1731
|
-
}
|
|
1732
|
-
// Validate app_id exists
|
|
1733
|
-
const validation = await validateAppId(app_id, c.env.APPS);
|
|
1734
|
-
if (!validation.valid) {
|
|
1735
|
-
return c.json({
|
|
1736
|
-
success: false,
|
|
1737
|
-
error: 'INVALID_APP_ID',
|
|
1738
|
-
message: validation.error || 'Invalid app_id',
|
|
1739
|
-
}, 400);
|
|
1778
|
+
error: appAccess.error,
|
|
1779
|
+
message: appAccess.message,
|
|
1780
|
+
}, appAccess.status);
|
|
1740
1781
|
}
|
|
1782
|
+
const app_id = appAccess.appId;
|
|
1741
1783
|
// Get all agents for this app
|
|
1742
1784
|
const agents = await listAgents(c.env.AGENTS, app_id);
|
|
1743
1785
|
return c.json({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tap-attestation-routes.d.ts","sourceRoot":"","sources":["../src/tap-attestation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tap-attestation-routes.d.ts","sourceRoot":"","sources":["../src/tap-attestation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAgEpC;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA4GrD;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAiDnD;AAED;;;;;;GAMG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;kBA8DrD;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAsEtD;AAED;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmFtD;;;;;;;;AAED,wBAME"}
|
|
@@ -11,25 +11,45 @@
|
|
|
11
11
|
* POST /v1/attestations/:id/revoke — Revoke attestation
|
|
12
12
|
* POST /v1/verify/attestation — Verify attestation + check capability
|
|
13
13
|
*/
|
|
14
|
-
import { extractBearerToken, verifyToken } from './auth.js';
|
|
14
|
+
import { extractBearerToken, verifyToken, getSigningPublicKeyJWK } from './auth.js';
|
|
15
15
|
import { issueAttestation, getAttestation, revokeAttestation, verifyAttestationToken, verifyAndCheckCapability, isValidCapabilityPattern, } from './tap-attestation.js';
|
|
16
16
|
// ============ VALIDATION HELPERS ============
|
|
17
|
+
function getVerificationPublicKey(env) {
|
|
18
|
+
const rawSigningKey = env?.JWT_SIGNING_KEY;
|
|
19
|
+
if (!rawSigningKey)
|
|
20
|
+
return undefined;
|
|
21
|
+
try {
|
|
22
|
+
const signingKey = JSON.parse(rawSigningKey);
|
|
23
|
+
return getSigningPublicKeyJWK(signingKey);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
console.error('Failed to parse JWT_SIGNING_KEY for attestation route verification');
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
17
30
|
async function validateAppAccess(c, requireAuth = true) {
|
|
18
31
|
const queryAppId = c.req.query('app_id');
|
|
19
|
-
let jwtAppId;
|
|
20
32
|
const authHeader = c.req.header('authorization');
|
|
21
33
|
const token = extractBearerToken(authHeader);
|
|
22
|
-
if (token) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
jwtAppId = result.payload.app_id;
|
|
34
|
+
if (!token) {
|
|
35
|
+
if (!requireAuth) {
|
|
36
|
+
return { valid: true, appId: queryAppId };
|
|
26
37
|
}
|
|
38
|
+
return { valid: false, error: 'UNAUTHORIZED', status: 401 };
|
|
39
|
+
}
|
|
40
|
+
const publicKey = getVerificationPublicKey(c.env);
|
|
41
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
42
|
+
if (!result.valid || !result.payload) {
|
|
43
|
+
return { valid: false, error: 'INVALID_TOKEN', status: 401 };
|
|
44
|
+
}
|
|
45
|
+
const jwtAppId = result.payload.app_id;
|
|
46
|
+
if (!jwtAppId) {
|
|
47
|
+
return { valid: false, error: 'MISSING_APP_ID', status: 403 };
|
|
27
48
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return { valid: false, error: 'MISSING_APP_ID', status: 401 };
|
|
49
|
+
if (queryAppId && queryAppId !== jwtAppId) {
|
|
50
|
+
return { valid: false, error: 'APP_ID_MISMATCH', status: 403 };
|
|
31
51
|
}
|
|
32
|
-
return { valid: true, appId };
|
|
52
|
+
return { valid: true, appId: jwtAppId };
|
|
33
53
|
}
|
|
34
54
|
// ============ ROUTE HANDLERS ============
|
|
35
55
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tap-delegation-routes.d.ts","sourceRoot":"","sources":["../src/tap-delegation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tap-delegation-routes.d.ts","sourceRoot":"","sources":["../src/tap-delegation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAgEpC;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAsGrD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmDlD;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAqEpD;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAsErD;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkDrD;;;;;;;;AAED,wBAME"}
|
|
@@ -11,26 +11,46 @@
|
|
|
11
11
|
* POST /v1/delegations/:id/revoke — Revoke delegation (cascades)
|
|
12
12
|
* POST /v1/verify/delegation — Verify delegation chain
|
|
13
13
|
*/
|
|
14
|
-
import { extractBearerToken, verifyToken } from './auth.js';
|
|
14
|
+
import { extractBearerToken, verifyToken, getSigningPublicKeyJWK } from './auth.js';
|
|
15
15
|
import { TAP_VALID_ACTIONS } from './tap-agents.js';
|
|
16
16
|
import { createDelegation, getDelegation, listDelegations, revokeDelegation, verifyDelegationChain, } from './tap-delegation.js';
|
|
17
17
|
// ============ VALIDATION HELPERS ============
|
|
18
|
+
function getVerificationPublicKey(env) {
|
|
19
|
+
const rawSigningKey = env?.JWT_SIGNING_KEY;
|
|
20
|
+
if (!rawSigningKey)
|
|
21
|
+
return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const signingKey = JSON.parse(rawSigningKey);
|
|
24
|
+
return getSigningPublicKeyJWK(signingKey);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
console.error('Failed to parse JWT_SIGNING_KEY for delegation route verification');
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
18
31
|
async function validateAppAccess(c, requireAuth = true) {
|
|
19
32
|
const queryAppId = c.req.query('app_id');
|
|
20
|
-
let jwtAppId;
|
|
21
33
|
const authHeader = c.req.header('authorization');
|
|
22
34
|
const token = extractBearerToken(authHeader);
|
|
23
|
-
if (token) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
jwtAppId = result.payload.app_id;
|
|
35
|
+
if (!token) {
|
|
36
|
+
if (!requireAuth) {
|
|
37
|
+
return { valid: true, appId: queryAppId };
|
|
27
38
|
}
|
|
39
|
+
return { valid: false, error: 'UNAUTHORIZED', status: 401 };
|
|
40
|
+
}
|
|
41
|
+
const publicKey = getVerificationPublicKey(c.env);
|
|
42
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
43
|
+
if (!result.valid || !result.payload) {
|
|
44
|
+
return { valid: false, error: 'INVALID_TOKEN', status: 401 };
|
|
45
|
+
}
|
|
46
|
+
const jwtAppId = result.payload.app_id;
|
|
47
|
+
if (!jwtAppId) {
|
|
48
|
+
return { valid: false, error: 'MISSING_APP_ID', status: 403 };
|
|
28
49
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return { valid: false, error: 'MISSING_APP_ID', status: 401 };
|
|
50
|
+
if (queryAppId && queryAppId !== jwtAppId) {
|
|
51
|
+
return { valid: false, error: 'APP_ID_MISMATCH', status: 403 };
|
|
32
52
|
}
|
|
33
|
-
return { valid: true, appId };
|
|
53
|
+
return { valid: true, appId: jwtAppId };
|
|
34
54
|
}
|
|
35
55
|
// ============ ROUTE HANDLERS ============
|
|
36
56
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tap-reputation-routes.d.ts","sourceRoot":"","sources":["../src/tap-reputation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EASL,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"tap-reputation-routes.d.ts","sourceRoot":"","sources":["../src/tap-reputation-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EASL,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,EAC3B,MAAM,qBAAqB,CAAC;AAsD7B;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEA6DlD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,0BAA0B,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA2H1D;AAED;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEA4EzD;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;oEAyDpD;;;;;;;AAED,wBAKE"}
|
|
@@ -10,25 +10,45 @@
|
|
|
10
10
|
* GET /v1/reputation/:agent_id/events — List reputation events
|
|
11
11
|
* POST /v1/reputation/:agent_id/reset — Reset agent reputation (admin)
|
|
12
12
|
*/
|
|
13
|
-
import { extractBearerToken, verifyToken } from './auth.js';
|
|
13
|
+
import { extractBearerToken, verifyToken, getSigningPublicKeyJWK } from './auth.js';
|
|
14
14
|
import { getReputationScore, recordReputationEvent, listReputationEvents, resetReputation, isValidCategory, isValidAction, isValidCategoryAction, } from './tap-reputation.js';
|
|
15
15
|
// ============ VALIDATION HELPERS ============
|
|
16
|
+
function getVerificationPublicKey(env) {
|
|
17
|
+
const rawSigningKey = env?.JWT_SIGNING_KEY;
|
|
18
|
+
if (!rawSigningKey)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
const signingKey = JSON.parse(rawSigningKey);
|
|
22
|
+
return getSigningPublicKeyJWK(signingKey);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
console.error('Failed to parse JWT_SIGNING_KEY for reputation route verification');
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
16
29
|
async function validateAppAccess(c, requireAuth = true) {
|
|
17
30
|
const queryAppId = c.req.query('app_id');
|
|
18
|
-
let jwtAppId;
|
|
19
31
|
const authHeader = c.req.header('authorization');
|
|
20
32
|
const token = extractBearerToken(authHeader);
|
|
21
|
-
if (token) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
jwtAppId = result.payload.app_id;
|
|
33
|
+
if (!token) {
|
|
34
|
+
if (!requireAuth) {
|
|
35
|
+
return { valid: true, appId: queryAppId };
|
|
25
36
|
}
|
|
37
|
+
return { valid: false, error: 'UNAUTHORIZED', status: 401 };
|
|
38
|
+
}
|
|
39
|
+
const publicKey = getVerificationPublicKey(c.env);
|
|
40
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
41
|
+
if (!result.valid || !result.payload) {
|
|
42
|
+
return { valid: false, error: 'INVALID_TOKEN', status: 401 };
|
|
43
|
+
}
|
|
44
|
+
const jwtAppId = result.payload.app_id;
|
|
45
|
+
if (!jwtAppId) {
|
|
46
|
+
return { valid: false, error: 'MISSING_APP_ID', status: 403 };
|
|
26
47
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return { valid: false, error: 'MISSING_APP_ID', status: 401 };
|
|
48
|
+
if (queryAppId && queryAppId !== jwtAppId) {
|
|
49
|
+
return { valid: false, error: 'APP_ID_MISMATCH', status: 403 };
|
|
30
50
|
}
|
|
31
|
-
return { valid: true, appId };
|
|
51
|
+
return { valid: true, appId: jwtAppId };
|
|
32
52
|
}
|
|
33
53
|
// ============ ROUTE HANDLERS ============
|
|
34
54
|
/**
|
package/dist/tap-routes.d.ts
CHANGED
|
@@ -309,13 +309,22 @@ export declare function verifyIOURoute(c: Context): Promise<(Response & import("
|
|
|
309
309
|
error: string;
|
|
310
310
|
message: string;
|
|
311
311
|
}, 404, "json">) | (Response & import("hono").TypedResponse<{
|
|
312
|
+
success: false;
|
|
313
|
+
error: string;
|
|
314
|
+
message: string;
|
|
315
|
+
}, 403, "json">) | (Response & import("hono").TypedResponse<{
|
|
312
316
|
success: false;
|
|
313
317
|
verified: false;
|
|
314
318
|
error: string | undefined;
|
|
315
319
|
}, 400, "json">) | (Response & import("hono").TypedResponse<{
|
|
320
|
+
success: false;
|
|
321
|
+
verified: true;
|
|
322
|
+
error: string;
|
|
323
|
+
message: string;
|
|
324
|
+
}, 502, "json">) | (Response & import("hono").TypedResponse<{
|
|
316
325
|
success: true;
|
|
317
326
|
verified: true;
|
|
318
|
-
access_token: string
|
|
327
|
+
access_token: string;
|
|
319
328
|
expires_at: string | undefined;
|
|
320
329
|
}, import("hono/utils/http-status").ContentfulStatusCode, "json">) | (Response & import("hono").TypedResponse<{
|
|
321
330
|
success: false;
|
package/dist/tap-routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tap-routes.d.ts","sourceRoot":"","sources":["../src/tap-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tap-routes.d.ts","sourceRoot":"","sources":["../src/tap-routes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAmKpC;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwErD;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAwDhD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEAmDlD;AAID;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAsFrD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA2ClD;AAID;;;GAGG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA4D9C;AAID;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;kBAwClD;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;kBAuB/C;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA6E9C;AAID;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqCnD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAsClD;;;;;;;;;;;;;;AAaD,wBAYE"}
|
package/dist/tap-routes.js
CHANGED
|
@@ -4,35 +4,64 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Provides backward-compatible endpoints with optional TAP functionality
|
|
6
6
|
*/
|
|
7
|
-
import { extractBearerToken, verifyToken } from './auth.js';
|
|
7
|
+
import { extractBearerToken, verifyToken, getSigningPublicKeyJWK } from './auth.js';
|
|
8
8
|
import { registerTAPAgent, getTAPAgent, listTAPAgents, createTAPSession, getTAPSession, validateCapability, TAP_VALID_ACTIONS } from './tap-agents.js';
|
|
9
9
|
import { parseTAPIntent } from './tap-verify.js';
|
|
10
10
|
import { createInvoice, getInvoice, verifyPaymentContainer, verifyBrowsingIOU, fulfillInvoice, parsePaymentContainer } from './tap-payment.js';
|
|
11
11
|
import { parseAgenticConsumer, verifyAgenticConsumer } from './tap-consumer.js';
|
|
12
12
|
// ============ VALIDATION HELPERS ============
|
|
13
|
+
function getVerificationPublicKey(env) {
|
|
14
|
+
const rawSigningKey = env?.JWT_SIGNING_KEY;
|
|
15
|
+
if (!rawSigningKey)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const signingKey = JSON.parse(rawSigningKey);
|
|
19
|
+
return getSigningPublicKeyJWK(signingKey);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
console.error('Failed to parse JWT_SIGNING_KEY for TAP verification');
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
13
26
|
async function validateAppAccess(c, requireAuth = true) {
|
|
14
|
-
// Extract app_id from query param or JWT
|
|
15
27
|
const queryAppId = c.req.query('app_id');
|
|
16
|
-
// Try to get from JWT Bearer token
|
|
17
|
-
let jwtAppId;
|
|
18
28
|
const authHeader = c.req.header('authorization');
|
|
19
29
|
const token = extractBearerToken(authHeader);
|
|
20
|
-
if (token) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
jwtAppId = result.payload.app_id;
|
|
30
|
+
if (!token) {
|
|
31
|
+
if (!requireAuth) {
|
|
32
|
+
return { valid: true, appId: queryAppId };
|
|
24
33
|
}
|
|
34
|
+
return {
|
|
35
|
+
valid: false,
|
|
36
|
+
error: 'UNAUTHORIZED',
|
|
37
|
+
status: 401
|
|
38
|
+
};
|
|
25
39
|
}
|
|
26
|
-
const
|
|
27
|
-
|
|
40
|
+
const publicKey = getVerificationPublicKey(c.env);
|
|
41
|
+
const result = await verifyToken(token, c.env.JWT_SECRET, c.env, undefined, publicKey);
|
|
42
|
+
if (!result.valid || !result.payload) {
|
|
28
43
|
return {
|
|
29
44
|
valid: false,
|
|
30
|
-
error: '
|
|
45
|
+
error: 'INVALID_TOKEN',
|
|
31
46
|
status: 401
|
|
32
47
|
};
|
|
33
48
|
}
|
|
34
|
-
|
|
35
|
-
|
|
49
|
+
const jwtAppId = result.payload.app_id;
|
|
50
|
+
if (!jwtAppId) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
error: 'MISSING_APP_ID',
|
|
54
|
+
status: 403
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (queryAppId && queryAppId !== jwtAppId) {
|
|
58
|
+
return {
|
|
59
|
+
valid: false,
|
|
60
|
+
error: 'APP_ID_MISMATCH',
|
|
61
|
+
status: 403
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { valid: true, appId: jwtAppId };
|
|
36
65
|
}
|
|
37
66
|
function validateTAPRegistration(body) {
|
|
38
67
|
if (!body.name || typeof body.name !== 'string') {
|
|
@@ -515,33 +544,37 @@ export async function verifyIOURoute(c) {
|
|
|
515
544
|
if (!agentKeyId) {
|
|
516
545
|
return c.json({ success: false, error: 'MISSING_KEY_ID', message: 'browsingIOU must include kid' }, 400);
|
|
517
546
|
}
|
|
518
|
-
//
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (agentIndex) {
|
|
523
|
-
const agentIds = JSON.parse(agentIndex);
|
|
524
|
-
for (const agentId of agentIds) {
|
|
525
|
-
const agentData = await c.env.AGENTS.get(`agent:${agentId}`, 'text');
|
|
526
|
-
if (agentData) {
|
|
527
|
-
const agent = JSON.parse(agentData);
|
|
528
|
-
if (agent.public_key) {
|
|
529
|
-
publicKey = agent.public_key;
|
|
530
|
-
break;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
547
|
+
// Resolve key by kid (agent/key identifier) and enforce same-app ownership.
|
|
548
|
+
const agentData = await c.env.AGENTS.get(`agent:${agentKeyId}`, 'text');
|
|
549
|
+
if (!agentData) {
|
|
550
|
+
return c.json({ success: false, error: 'KEY_NOT_FOUND', message: `No TAP key found for kid: ${agentKeyId}` }, 404);
|
|
534
551
|
}
|
|
535
|
-
|
|
552
|
+
const agent = JSON.parse(agentData);
|
|
553
|
+
if (agent.app_id !== invoiceResult.invoice.app_id) {
|
|
554
|
+
return c.json({
|
|
555
|
+
success: false,
|
|
556
|
+
error: 'KEY_APP_MISMATCH',
|
|
557
|
+
message: 'Key does not belong to the app that issued this invoice',
|
|
558
|
+
}, 403);
|
|
559
|
+
}
|
|
560
|
+
if (!agent.public_key) {
|
|
536
561
|
return c.json({ success: false, error: 'KEY_NOT_FOUND', message: 'Could not resolve public key for verification' }, 404);
|
|
537
562
|
}
|
|
538
563
|
// Verify the IOU
|
|
539
|
-
const iouResult = await verifyBrowsingIOU(body.browsingIOU, invoiceResult.invoice,
|
|
564
|
+
const iouResult = await verifyBrowsingIOU(body.browsingIOU, invoiceResult.invoice, agent.public_key, body.browsingIOU.alg || agent.signature_algorithm || 'ES256');
|
|
540
565
|
if (!iouResult.valid) {
|
|
541
566
|
return c.json({ success: false, verified: false, error: iouResult.error }, 400);
|
|
542
567
|
}
|
|
543
568
|
// Fulfill the invoice
|
|
544
569
|
const fulfillResult = await fulfillInvoice(c.env.INVOICES, invoiceId, body.browsingIOU);
|
|
570
|
+
if (!fulfillResult.success || !fulfillResult.access_token) {
|
|
571
|
+
return c.json({
|
|
572
|
+
success: false,
|
|
573
|
+
verified: true,
|
|
574
|
+
error: 'INVOICE_FULFILLMENT_FAILED',
|
|
575
|
+
message: fulfillResult.error || 'Invoice could not be fulfilled',
|
|
576
|
+
}, 502);
|
|
577
|
+
}
|
|
545
578
|
return c.json({
|
|
546
579
|
success: true,
|
|
547
580
|
verified: true,
|