@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.
@@ -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-XXXX) that a human can enter at /dashboard/code.
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;AAMvD,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,gFAqB9E;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"}
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"}
@@ -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-XXXX) that a human can enter at /dashboard/code.
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: "XXXX", value: prefill, required: true, autocomplete: "off", maxlength: 11, 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
+ : '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, 4);
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 "AR8C" and "BOTCHA-AR8C", always look up with prefix
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-XXXX (4 alphanumeric chars, no ambiguous chars)
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-XXXX (4 alphanumeric chars, no ambiguous chars)
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(4);
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}`;
@@ -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;AAowErE,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"}
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.0');
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 { valid: false, error: `App not found: ${appId}` };
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: app_id || result.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 the session token includes this app_id
1517
- const { jwtVerify, createLocalJWKSet } = await import('jose');
1518
- try {
1519
- const secret = new TextEncoder().encode(c.env.JWT_SECRET);
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
- // Extract app_id from query param or JWT
1605
- const queryAppId = c.req.query('app_id');
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: 'INVALID_APP_ID',
1631
- message: validation.error || 'Invalid app_id',
1632
- }, 400);
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
- // Extract app_id from query param or JWT
1712
- const queryAppId = c.req.query('app_id');
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: 'MISSING_APP_ID',
1729
- message: 'app_id is required. Provide it as a query parameter (?app_id=...) or in the JWT token.',
1730
- }, 401);
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;AA4CpC;;;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"}
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
- const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
24
- if (result.valid && result.payload) {
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
- const appId = queryAppId || jwtAppId;
29
- if (requireAuth && !appId) {
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;AA4CpC;;;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"}
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
- const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
25
- if (result.valid && result.payload) {
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
- const appId = queryAppId || jwtAppId;
30
- if (requireAuth && !appId) {
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;AAkC7B;;;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"}
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
- const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
23
- if (result.valid && result.payload) {
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
- const appId = queryAppId || jwtAppId;
28
- if (requireAuth && !appId) {
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
  /**
@@ -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 | undefined;
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;
@@ -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;AAsIpC;;;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;;;;;;;;;;;;;;;;;;;;;kBAwE9C;AAID;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAqCnD;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,CAAC,EAAE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAsClD;;;;;;;;;;;;;;AAaD,wBAYE"}
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"}
@@ -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
- const result = await verifyToken(token, c.env.JWT_SECRET, c.env);
22
- if (result.valid && result.payload) {
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 appId = queryAppId || jwtAppId;
27
- if (requireAuth && !appId) {
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: 'MISSING_APP_ID',
45
+ error: 'INVALID_TOKEN',
31
46
  status: 401
32
47
  };
33
48
  }
34
- // TODO: Validate app exists (integrate with existing app validation)
35
- return { valid: true, appId };
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
- // For now, try to find the key in our agent registry
519
- // Future: also check federated sources
520
- let publicKey = null;
521
- const agentIndex = await c.env.AGENTS.get(`app_agents:${invoiceResult.invoice.app_id}`, 'text');
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
- if (!publicKey) {
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, publicKey, body.browsingIOU.alg || 'ES256');
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dupecom/botcha-cloudflare",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "BOTCHA for Cloudflare Workers - Prove you're a bot. Humans need not apply.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",