agenttwin 0.3.14 → 0.3.15

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.
Files changed (116) hide show
  1. package/dist/api.js +461 -92
  2. package/dist/cli.js +14 -2
  3. package/dist/dashboard/assets/next/static/RRU6kOWIag8OaxBjl6DjU/_buildManifest.js +1 -0
  4. package/dist/dashboard/assets/next/static/chunks/1255-d4ec8dbb0865c655.js +1 -0
  5. package/dist/dashboard/assets/next/static/chunks/1482.e664aba77631265d.js +1 -0
  6. package/dist/dashboard/assets/next/static/chunks/182-71d400a5d9e02879.js +1 -0
  7. package/dist/dashboard/assets/next/static/chunks/3050-bac69e3c6734beee.js +1 -0
  8. package/dist/dashboard/assets/next/static/chunks/3056.ffa9a4d44a274b29.js +1 -0
  9. package/dist/dashboard/assets/next/static/chunks/{2442.9a266da6229b8ae0.js → 4055.1056568a41510e92.js} +1 -1
  10. package/dist/dashboard/assets/next/static/chunks/4343.023a18f52318dbad.js +1 -0
  11. package/dist/dashboard/assets/next/static/chunks/4bd1b696-f785427dddbba9fb.js +1 -0
  12. package/dist/dashboard/assets/next/static/chunks/{5490.0d5d8d5ea3577f20.js → 6751.613adb085c9a5b06.js} +1 -1
  13. package/dist/dashboard/assets/next/static/chunks/6787-c85d7f794360b672.js +1 -0
  14. package/dist/dashboard/assets/next/static/chunks/7772-f2d773cd3e94eea6.js +67 -0
  15. package/dist/dashboard/assets/next/static/chunks/8093-90b99184c0a57c57.js +1 -0
  16. package/dist/dashboard/assets/next/static/chunks/{3454.d29ee7a49c2a4fba.js → 8975.5be81ec8b09e3b45.js} +1 -1
  17. package/dist/dashboard/assets/next/static/chunks/{7280.01b3f749332bb075.js → 9013.0f2be1cc70029afa.js} +2 -2
  18. package/dist/dashboard/assets/next/static/chunks/9376.42da54b90ff9f2d9.js +1 -0
  19. package/dist/dashboard/assets/next/static/chunks/9675.14739028910476e4.js +1 -0
  20. package/dist/dashboard/assets/next/static/chunks/971-f01a5b1fb6756f6c.js +1 -0
  21. package/dist/dashboard/assets/next/static/chunks/app/_not-found/page-43db8aef3fa81958.js +1 -0
  22. package/dist/dashboard/assets/next/static/chunks/app/about/page-3f5dbcdbf95bb2fc.js +1 -0
  23. package/dist/dashboard/assets/next/static/chunks/app/account/password/page-c2ce3c796c817ca1.js +1 -0
  24. package/dist/dashboard/assets/next/static/chunks/app/api-policy/page-3f5dbcdbf95bb2fc.js +1 -0
  25. package/dist/dashboard/assets/next/static/chunks/app/auth/page-3f5dbcdbf95bb2fc.js +1 -0
  26. package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-mockoon/page-ba841d868e5d124a.js +1 -0
  27. package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-pact/page-ba841d868e5d124a.js +1 -0
  28. package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-testcontainers/page-ba841d868e5d124a.js +1 -0
  29. package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-wiremock/page-ba841d868e5d124a.js +1 -0
  30. package/dist/dashboard/assets/next/static/chunks/app/blog/keploy-alternatives-for-database-and-payment-workflows/page-ba841d868e5d124a.js +1 -0
  31. package/dist/dashboard/assets/next/static/chunks/app/blog/layout-563d8763d65d66b5.js +1 -0
  32. package/dist/dashboard/assets/next/static/chunks/app/blog/page-ba841d868e5d124a.js +1 -0
  33. package/dist/dashboard/assets/next/static/chunks/app/blog/reset-test-environment-between-runs/page-ba841d868e5d124a.js +1 -0
  34. package/dist/dashboard/assets/next/static/chunks/app/blog/simulate-timeouts-and-duplicate-events/page-ba841d868e5d124a.js +1 -0
  35. package/dist/dashboard/assets/next/static/chunks/app/compare/page-3f5dbcdbf95bb2fc.js +1 -0
  36. package/dist/dashboard/assets/next/static/chunks/app/contact/page-3f5dbcdbf95bb2fc.js +1 -0
  37. package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-1afe915ed22ac5ed.js +1 -0
  38. package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-d91fa87fbf634bc9.js +1 -0
  39. package/dist/dashboard/assets/next/static/chunks/app/demo/page-4dc63755e0013e7c.js +1 -0
  40. package/dist/dashboard/assets/next/static/chunks/app/developers/page-3f5dbcdbf95bb2fc.js +1 -0
  41. package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-3c0d688140d910bd.js +1 -0
  42. package/dist/dashboard/assets/next/static/chunks/app/guides/page-3f5dbcdbf95bb2fc.js +1 -0
  43. package/dist/dashboard/assets/next/static/chunks/app/layout-993a96283a3ceb07.js +1 -0
  44. package/dist/dashboard/assets/next/static/chunks/app/login/page-d6d4276397724d29.js +1 -0
  45. package/dist/dashboard/assets/next/static/chunks/app/onboarding/page-bc480c9258e5cc4d.js +1 -0
  46. package/dist/dashboard/assets/next/static/chunks/app/page-e4fe99bcc001a517.js +1 -0
  47. package/dist/dashboard/assets/next/static/chunks/app/pricing/page-40f352fe5093a0ff.js +1 -0
  48. package/dist/dashboard/assets/next/static/chunks/app/privacy/page-3f5dbcdbf95bb2fc.js +1 -0
  49. package/dist/dashboard/assets/next/static/chunks/app/sandbox/page-3f5dbcdbf95bb2fc.js +1 -0
  50. package/dist/dashboard/assets/next/static/chunks/app/signup/page-d6d4276397724d29.js +1 -0
  51. package/dist/dashboard/assets/next/static/chunks/app/sitemap.xml/route-ba841d868e5d124a.js +1 -0
  52. package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/layout-a5cf565e017886d3.js +1 -0
  53. package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-ca68ba293cca7d75.js +1 -0
  54. package/dist/dashboard/assets/next/static/chunks/app/v2/layout-abbd9f3b8811f559.js +1 -0
  55. package/dist/dashboard/assets/next/static/chunks/app/v2/login/page-4f4a342a4c28f9de.js +1 -0
  56. package/dist/dashboard/assets/next/static/chunks/app/v2/onboarding/page-f04c0b236abaead3.js +1 -0
  57. package/dist/dashboard/assets/next/static/chunks/app/v2/page-3905446ec0206fb5.js +1 -0
  58. package/dist/dashboard/assets/next/static/chunks/app/v2/signup/page-4f4a342a4c28f9de.js +1 -0
  59. package/dist/dashboard/assets/next/static/chunks/app/what-is-agenttwin/page-3f5dbcdbf95bb2fc.js +1 -0
  60. package/dist/dashboard/assets/next/static/chunks/{48a1527b.6d52e40c5e62d9d5.js → b468fba8.437b212aae4abcf8.js} +1 -1
  61. package/dist/dashboard/assets/next/static/chunks/framework-e54b663a2888b5de.js +1 -0
  62. package/dist/dashboard/assets/next/static/chunks/main-5aa70aa3d6a46aa0.js +1 -0
  63. package/dist/dashboard/assets/next/static/chunks/main-app-1ec13ceef4a34729.js +1 -0
  64. package/dist/dashboard/assets/next/static/chunks/pages/_app-6c8c2371b16a04b8.js +1 -0
  65. package/dist/dashboard/assets/next/static/chunks/pages/_error-94812ad32cad7365.js +1 -0
  66. package/dist/dashboard/assets/next/static/chunks/webpack-1ac08e31899efefb.js +1 -0
  67. package/dist/dashboard/assets/next/static/css/15b28a76469ea7e5.css +3 -0
  68. package/dist/dashboard/assets/next/static/css/{eea0966f05042bde.css → 18afa3e32331c41d.css} +1 -1
  69. package/dist/dashboard/assets/next/static/css/6695e0f2ae2e5aaf.css +1 -0
  70. package/dist/dashboard/assets/next/static/css/ced6baae4dc4cbab.css +1 -0
  71. package/dist/dashboard/assets/next/static/css/d35115cc1039cc68.css +1 -0
  72. package/dist/dashboard/assets/next/static/css/f27d5fbb1f484d9d.css +1 -0
  73. package/dist/dashboard/index.html +2 -2
  74. package/dist/gateway.js +454 -85
  75. package/package.json +14 -2
  76. package/dist/dashboard/assets/next/static/O8JeB5KAl7-h1hCNIyUY7/_buildManifest.js +0 -1
  77. package/dist/dashboard/assets/next/static/chunks/1981-2ce2c5edd154524a.js +0 -1
  78. package/dist/dashboard/assets/next/static/chunks/2529.07927d0600506985.js +0 -1
  79. package/dist/dashboard/assets/next/static/chunks/3916-2698a1a3ecb8649f.js +0 -67
  80. package/dist/dashboard/assets/next/static/chunks/4705-a6d249b4a5dcde26.js +0 -1
  81. package/dist/dashboard/assets/next/static/chunks/4816-45f42396ec2135cc.js +0 -1
  82. package/dist/dashboard/assets/next/static/chunks/650-3092bf29ff938e12.js +0 -1
  83. package/dist/dashboard/assets/next/static/chunks/6f3ce29d-af1469ba72f5d3e4.js +0 -1
  84. package/dist/dashboard/assets/next/static/chunks/794.a624e6be9e42a62b.js +0 -1
  85. package/dist/dashboard/assets/next/static/chunks/app/_not-found/page-471a6c74344187f9.js +0 -1
  86. package/dist/dashboard/assets/next/static/chunks/app/account/password/page-ebb8ab25df1b808e.js +0 -1
  87. package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-f7cd10740b90145d.js +0 -1
  88. package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-a5ad912a1f481008.js +0 -1
  89. package/dist/dashboard/assets/next/static/chunks/app/demo/page-5495d9cb9a3e1431.js +0 -1
  90. package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-9130f219b4b02112.js +0 -1
  91. package/dist/dashboard/assets/next/static/chunks/app/enterprise/page-b7e323d45eaf4952.js +0 -1
  92. package/dist/dashboard/assets/next/static/chunks/app/layout-ed5b209cd1b17365.js +0 -1
  93. package/dist/dashboard/assets/next/static/chunks/app/login/page-3887863343ffce8c.js +0 -1
  94. package/dist/dashboard/assets/next/static/chunks/app/onboarding/page-35795509a58cbc9c.js +0 -1
  95. package/dist/dashboard/assets/next/static/chunks/app/page-4d150b91a7a1abcc.js +0 -1
  96. package/dist/dashboard/assets/next/static/chunks/app/pricing/page-388c0afb0ac51384.js +0 -1
  97. package/dist/dashboard/assets/next/static/chunks/app/signup/page-3887863343ffce8c.js +0 -1
  98. package/dist/dashboard/assets/next/static/chunks/app/sitemap.xml/route-36bcc482b50fcb46.js +0 -1
  99. package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/layout-1430f20bea23253f.js +0 -1
  100. package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-12a8d96f5c18b411.js +0 -1
  101. package/dist/dashboard/assets/next/static/chunks/app/v2/layout-fb241571c2a60362.js +0 -1
  102. package/dist/dashboard/assets/next/static/chunks/app/v2/login/page-7ac4be578a219d6e.js +0 -1
  103. package/dist/dashboard/assets/next/static/chunks/app/v2/onboarding/page-16d9efad4103fb58.js +0 -1
  104. package/dist/dashboard/assets/next/static/chunks/app/v2/page-1d465878abb57f0f.js +0 -1
  105. package/dist/dashboard/assets/next/static/chunks/app/v2/signup/page-7ac4be578a219d6e.js +0 -1
  106. package/dist/dashboard/assets/next/static/chunks/framework-cb37e134fe056fa5.js +0 -1
  107. package/dist/dashboard/assets/next/static/chunks/main-app-c83f3671ad6b824a.js +0 -1
  108. package/dist/dashboard/assets/next/static/chunks/main-b7d6352ee94d0dba.js +0 -1
  109. package/dist/dashboard/assets/next/static/chunks/pages/_app-9dd167be58cf5cbd.js +0 -1
  110. package/dist/dashboard/assets/next/static/chunks/pages/_error-5a8b1eb076dd7c95.js +0 -1
  111. package/dist/dashboard/assets/next/static/chunks/webpack-9ed8651d8c2c40a2.js +0 -1
  112. package/dist/dashboard/assets/next/static/css/0856ee8c880d8b92.css +0 -1
  113. package/dist/dashboard/assets/next/static/css/0cf03e33b963e5cb.css +0 -1
  114. package/dist/dashboard/assets/next/static/css/81a1668ab34a36e8.css +0 -1
  115. package/dist/dashboard/assets/next/static/css/b15fa8a0e168d625.css +0 -1
  116. /package/dist/dashboard/assets/next/static/{O8JeB5KAl7-h1hCNIyUY7 → RRU6kOWIag8OaxBjl6DjU}/_ssgManifest.js +0 -0
package/dist/api.js CHANGED
@@ -1229,13 +1229,13 @@ async function projectProducts(cwd, data, base) {
1229
1229
  const source = path2 ? await files.read(path2, 2e5) : void 0;
1230
1230
  const entry = source && fn ? exportedEntrypoints(source).find((e) => e.name === fn) : void 0;
1231
1231
  const body2 = source && entry ? source.split("\n").slice(entry.startLine - 1, entry.endLine).join("\n") : fn ? "" : source ?? "";
1232
- const matches2 = products.filter((p) => found.has(p.key) && p.pattern.test(body2)).map((p) => google.id + ":" + p.key);
1233
- if (!matches2.length) {
1232
+ const matches3 = products.filter((p) => found.has(p.key) && p.pattern.test(body2)).map((p) => google.id + ":" + p.key);
1233
+ if (!matches3.length) {
1234
1234
  unresolved = true;
1235
1235
  finalWorkflows.push(w);
1236
1236
  continue;
1237
1237
  }
1238
- finalWorkflows.push({ ...w, tools: [...new Set(w.tools.flatMap((id) => id === google.id ? matches2 : [id]))], steps: w.steps.flatMap((s) => s.tool === google.id ? matches2.map((tool) => ({ ...s, tool })) : [s]) });
1238
+ finalWorkflows.push({ ...w, tools: [...new Set(w.tools.flatMap((id) => id === google.id ? matches3 : [id]))], steps: w.steps.flatMap((s) => s.tool === google.id ? matches3.map((tool) => ({ ...s, tool })) : [s]) });
1239
1239
  }
1240
1240
  const productTools = products.filter((p) => found.has(p.key)).map((p) => ({ ...google, id: google.id + ":" + p.key, title: p.title }));
1241
1241
  return discoveryTransfer({ ...data, tools: [...tools.filter((t) => t.id !== google.id || unresolved || !productTools.length).map((t) => t.id === google.id ? { ...t, title: "Google API (unspecified)" } : t), ...productTools], workflows: finalWorkflows });
@@ -1381,6 +1381,249 @@ var init_session = __esm({
1381
1381
  }
1382
1382
  });
1383
1383
 
1384
+ // src/runtime/request-budget.ts
1385
+ function requestBudget(limit = 1200) {
1386
+ if (!Number.isSafeInteger(limit) || limit < 1)
1387
+ throw new Error("Invalid API request limit");
1388
+ let until = 0, count = 0;
1389
+ return (res) => {
1390
+ const now = Date.now();
1391
+ if (now >= until) {
1392
+ until = now + 6e4;
1393
+ count = 0;
1394
+ }
1395
+ count++;
1396
+ const reset = Math.max(1, Math.ceil((until - now) / 1e3));
1397
+ res.setHeader("RateLimit-Policy", `"gateway";q=${limit};w=60`);
1398
+ res.setHeader(
1399
+ "RateLimit",
1400
+ `"gateway";r=${Math.max(0, limit - count)};t=${reset}`
1401
+ );
1402
+ if (count > limit) {
1403
+ res.setHeader("Retry-After", String(reset));
1404
+ return false;
1405
+ }
1406
+ return true;
1407
+ };
1408
+ }
1409
+
1410
+ // src/runtime/oauth.ts
1411
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
1412
+ var oauthScopes = {
1413
+ "read:projects": "List this client\u2019s one authorized project",
1414
+ "read:discovery": "Read discovery metadata for the authorized project",
1415
+ "read:runs": "Read test run status and recorded evidence for the authorized project",
1416
+ "read:workflows": "Read saved test configurations for the authorized project",
1417
+ "write:runs": "Submit and cancel reviewed cloud tests for the authorized project",
1418
+ "write:workflows": "Save test configuration drafts for the authorized project"
1419
+ };
1420
+ var hash = (v) => createHash("sha256").update(v).digest("hex");
1421
+ var matches = (a, b) => timingSafeEqual(Buffer.from(hash(a)), Buffer.from(b));
1422
+ function newOAuthClient(accountId, label, project, scopes) {
1423
+ if (!Array.isArray(scopes) || !scopes.length || scopes.length > Object.keys(oauthScopes).length || scopes.some(
1424
+ (s) => typeof s !== "string" || !Object.hasOwn(oauthScopes, s)
1425
+ ) || new Set(scopes).size !== scopes.length)
1426
+ throw new Error("Choose explicit supported OAuth scopes");
1427
+ const secret = randomBytes(32).toString("base64url");
1428
+ const client = {
1429
+ id: "at_client_" + randomBytes(16).toString("hex"),
1430
+ secretHash: hash(secret),
1431
+ accountId,
1432
+ label,
1433
+ project,
1434
+ scopes,
1435
+ active: true,
1436
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1437
+ };
1438
+ return { client, secret };
1439
+ }
1440
+ function oauthMetadata(issuer) {
1441
+ return {
1442
+ issuer,
1443
+ token_endpoint: issuer + "/oauth/token",
1444
+ revocation_endpoint: issuer + "/oauth/revoke",
1445
+ grant_types_supported: ["client_credentials"],
1446
+ response_types_supported: [],
1447
+ token_endpoint_auth_methods_supported: ["client_secret_basic"],
1448
+ revocation_endpoint_auth_methods_supported: ["client_secret_basic"],
1449
+ scopes_supported: Object.keys(oauthScopes),
1450
+ service_documentation: "https://agenttwin.si/auth.md"
1451
+ };
1452
+ }
1453
+ function resourceMetadata(issuer) {
1454
+ return {
1455
+ resource: issuer,
1456
+ authorization_servers: [issuer],
1457
+ scopes_supported: Object.keys(oauthScopes),
1458
+ bearer_methods_supported: ["header"],
1459
+ resource_documentation: "https://agenttwin.si/auth.md"
1460
+ };
1461
+ }
1462
+ function oauthGrant(store, raw) {
1463
+ if (!raw.startsWith("at_oauth_")) return void 0;
1464
+ const token = store.oauthTokens?.find(
1465
+ (t) => t.hash === hash(raw) && t.expires > Date.now()
1466
+ );
1467
+ const client = token && store.oauthClients?.find((c) => c.id === token.clientId && c.active);
1468
+ if (!token || !client || !store.accounts.some((a) => a.id === client.accountId && a.active))
1469
+ return void 0;
1470
+ return { token, client };
1471
+ }
1472
+ function requiredOAuthScope(method, path2) {
1473
+ if (method === "GET") {
1474
+ if (path2 === "/v1/projects") return "read:projects";
1475
+ if (path2 === "/v1/discovery") return "read:discovery";
1476
+ if (path2 === "/v1/workflows") return "read:workflows";
1477
+ if (path2 === "/v1/runs" || /^\/v1\/runs\/[a-f0-9-]{36}$/.test(path2))
1478
+ return "read:runs";
1479
+ }
1480
+ if (method === "POST" && path2 === "/v1/workflows") return "write:workflows";
1481
+ if (method === "POST" && path2 === "/v1/runs" || method === "DELETE" && /^\/v1\/runs\/[a-f0-9-]{36}$/.test(path2))
1482
+ return "write:runs";
1483
+ return void 0;
1484
+ }
1485
+ function createOAuth(store, save, issuer) {
1486
+ const attempts = /* @__PURE__ */ new Map();
1487
+ const reply = (res, status, data) => {
1488
+ res.writeHead(status, {
1489
+ "content-type": "application/json",
1490
+ "cache-control": "no-store",
1491
+ pragma: "no-cache"
1492
+ });
1493
+ res.end(JSON.stringify(data));
1494
+ };
1495
+ return async (req, res, path2) => {
1496
+ if (!["/oauth/token", "/oauth/revoke"].includes(path2)) return false;
1497
+ if (req.method === "OPTIONS") {
1498
+ res.writeHead(204);
1499
+ res.end();
1500
+ return true;
1501
+ }
1502
+ if (req.method !== "POST") {
1503
+ res.setHeader("Allow", "POST, OPTIONS");
1504
+ reply(res, 405, { error: "invalid_request" });
1505
+ return true;
1506
+ }
1507
+ const now = Date.now(), peer = req.socket.remoteAddress ?? "unknown";
1508
+ for (const [key, value] of attempts)
1509
+ if (value.until <= now) attempts.delete(key);
1510
+ const bucket = attempts.get(peer) ?? { count: 0, until: now + 6e4 };
1511
+ bucket.count++;
1512
+ attempts.set(peer, bucket);
1513
+ res.setHeader("RateLimit-Policy", '"oauth";q=120;w=60');
1514
+ res.setHeader(
1515
+ "RateLimit",
1516
+ `"oauth";r=${Math.max(0, 120 - bucket.count)};t=${Math.max(1, Math.ceil((bucket.until - now) / 1e3))}`
1517
+ );
1518
+ if (bucket.count > 120 || attempts.size > 1e4) {
1519
+ res.setHeader("Retry-After", "60");
1520
+ reply(res, 429, { error: "temporarily_unavailable" });
1521
+ return true;
1522
+ }
1523
+ const basic = req.headers.authorization?.match(
1524
+ /^Basic ([A-Za-z0-9+/]+=*)$/i
1525
+ );
1526
+ let client;
1527
+ try {
1528
+ const decoded = basic ? Buffer.from(basic[1], "base64").toString("utf8") : "";
1529
+ const colon = decoded.indexOf(":");
1530
+ const id = decodeURIComponent(
1531
+ decoded.slice(0, colon).replace(/\+/g, " ")
1532
+ ), secret = decodeURIComponent(
1533
+ decoded.slice(colon + 1).replace(/\+/g, " ")
1534
+ );
1535
+ client = store.oauthClients?.find((c) => c.id === id && c.active);
1536
+ if (colon < 1 || !client || !matches(secret, client.secretHash) || !store.accounts.some((a) => a.id === client.accountId && a.active))
1537
+ client = void 0;
1538
+ } catch {
1539
+ client = void 0;
1540
+ }
1541
+ if (!client) {
1542
+ res.setHeader("WWW-Authenticate", 'Basic realm="AgentTwin OAuth"');
1543
+ reply(res, 401, { error: "invalid_client" });
1544
+ return true;
1545
+ }
1546
+ if (!/^application\/x-www-form-urlencoded(?:;|$)/i.test(
1547
+ req.headers["content-type"] ?? ""
1548
+ )) {
1549
+ reply(res, 400, {
1550
+ error: "invalid_request",
1551
+ error_description: "Use application/x-www-form-urlencoded"
1552
+ });
1553
+ return true;
1554
+ }
1555
+ let raw = "";
1556
+ for await (const chunk of req) {
1557
+ raw += chunk.toString("utf8");
1558
+ if (Buffer.byteLength(raw) > 8192) {
1559
+ reply(res, 400, { error: "invalid_request" });
1560
+ return true;
1561
+ }
1562
+ }
1563
+ const form = new URLSearchParams(raw);
1564
+ if ([...new Set(form.keys())].some((k) => form.getAll(k).length !== 1) || form.has("client_id") || form.has("client_secret")) {
1565
+ reply(res, 400, { error: "invalid_request" });
1566
+ return true;
1567
+ }
1568
+ if (!client.active || !store.accounts.some((a) => a.id === client.accountId && a.active)) {
1569
+ reply(res, 401, { error: "invalid_client" });
1570
+ return true;
1571
+ }
1572
+ if (path2 === "/oauth/revoke") {
1573
+ if (!form.get("token")) {
1574
+ reply(res, 400, { error: "invalid_request" });
1575
+ return true;
1576
+ }
1577
+ if (form.has("token_type_hint") && form.get("token_type_hint") !== "access_token") {
1578
+ reply(res, 400, { error: "unsupported_token_type" });
1579
+ return true;
1580
+ }
1581
+ store.oauthTokens = (store.oauthTokens ?? []).filter(
1582
+ (t) => t.clientId !== client.id || t.hash !== hash(form.get("token"))
1583
+ );
1584
+ await save();
1585
+ reply(res, 200, {});
1586
+ return true;
1587
+ }
1588
+ if (form.get("grant_type") !== "client_credentials") {
1589
+ reply(res, 400, { error: "unsupported_grant_type" });
1590
+ return true;
1591
+ }
1592
+ if (form.has("resource") && form.get("resource") !== issuer) {
1593
+ reply(res, 400, { error: "invalid_target" });
1594
+ return true;
1595
+ }
1596
+ const scopes = form.has("scope") ? form.get("scope").split(" ") : client.scopes;
1597
+ if (!scopes.length || scopes.some((s) => !s || !client.scopes.includes(s)) || new Set(scopes).size !== scopes.length) {
1598
+ reply(res, 400, { error: "invalid_scope" });
1599
+ return true;
1600
+ }
1601
+ store.oauthTokens = (store.oauthTokens ?? []).filter(
1602
+ (t) => t.expires > now
1603
+ );
1604
+ if (store.oauthTokens.length >= 1e5 || store.oauthTokens.filter((t) => t.clientId === client.id).length >= 100) {
1605
+ res.setHeader("Retry-After", "60");
1606
+ reply(res, 429, { error: "temporarily_unavailable" });
1607
+ return true;
1608
+ }
1609
+ const access_token = "at_oauth_" + randomBytes(32).toString("base64url");
1610
+ store.oauthTokens.push({
1611
+ hash: hash(access_token),
1612
+ clientId: client.id,
1613
+ scopes,
1614
+ expires: now + 36e5
1615
+ });
1616
+ await save();
1617
+ reply(res, 200, {
1618
+ access_token,
1619
+ token_type: "Bearer",
1620
+ expires_in: 3600,
1621
+ scope: scopes.join(" ")
1622
+ });
1623
+ return true;
1624
+ };
1625
+ }
1626
+
1384
1627
  // src/runtime/workspace-profile.ts
1385
1628
  function validateWorkspaceProfile(input) {
1386
1629
  const allowed = ["name", "workspace", "company", "website", "role", "teamSize", "useCase", "complete"];
@@ -1408,33 +1651,33 @@ function validateWorkspaceProfile(input) {
1408
1651
  }
1409
1652
 
1410
1653
  // src/runtime/email-verification.ts
1411
- import { createHmac, randomBytes, randomInt, timingSafeEqual } from "crypto";
1654
+ import { createHmac, randomBytes as randomBytes2, randomInt, timingSafeEqual as timingSafeEqual2 } from "crypto";
1412
1655
  function normalizedEmail(value) {
1413
1656
  const email = typeof value === "string" ? value.trim().toLowerCase() : "";
1414
1657
  if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw new Error("Enter a valid email address.");
1415
1658
  return email;
1416
1659
  }
1417
1660
  function createEmailVerification(options, secret) {
1418
- const hash3 = (value) => createHmac("sha256", secret).update(value).digest("hex");
1419
- const matches2 = (value, expected) => {
1420
- const a = Buffer.from(hash3(value), "hex"), b = Buffer.from(expected, "hex");
1421
- return a.length === b.length && timingSafeEqual(a, b);
1661
+ const hash4 = (value) => createHmac("sha256", secret).update(value).digest("hex");
1662
+ const matches3 = (value, expected) => {
1663
+ const a = Buffer.from(hash4(value), "hex"), b = Buffer.from(expected, "hex");
1664
+ return a.length === b.length && timingSafeEqual2(a, b);
1422
1665
  };
1423
1666
  return {
1424
1667
  enabled: !!options,
1425
1668
  challenge(email, origin) {
1426
- const id = randomBytes(24).toString("hex"), binding = randomBytes(32).toString("hex");
1669
+ const id = randomBytes2(24).toString("hex"), binding = randomBytes2(32).toString("hex");
1427
1670
  const code = String(randomInt(0, 1e8)).padStart(8, "0");
1428
- const challenge = { email, origin, binding: hash3(binding), codeHash: hash3(id + ":" + code), expires: Date.now() + 10 * 6e4, attempts: 0, sentAt: Date.now() };
1671
+ const challenge = { email, origin, binding: hash4(binding), codeHash: hash4(id + ":" + code), expires: Date.now() + 10 * 6e4, attempts: 0, sentAt: Date.now() };
1429
1672
  return { id, binding, code, challenge };
1430
1673
  },
1431
1674
  bound(challenge, binding, origin) {
1432
- return !!challenge && challenge.expires > Date.now() && challenge.origin === origin && matches2(binding, challenge.binding);
1675
+ return !!challenge && challenge.expires > Date.now() && challenge.origin === origin && matches3(binding, challenge.binding);
1433
1676
  },
1434
1677
  verify(id, challenge, code) {
1435
1678
  if (challenge.verified || challenge.attempts >= 5 || challenge.expires <= Date.now()) return false;
1436
1679
  challenge.attempts++;
1437
- if (typeof code !== "string" || !/^\d{8}$/.test(code) || !matches2(id + ":" + code, challenge.codeHash)) return false;
1680
+ if (typeof code !== "string" || !/^\d{8}$/.test(code) || !matches3(id + ":" + code, challenge.codeHash)) return false;
1438
1681
  challenge.verified = true;
1439
1682
  return true;
1440
1683
  },
@@ -1455,8 +1698,8 @@ It expires in 10 minutes. Do not share this code. If you did not request it, ign
1455
1698
 
1456
1699
  // src/runtime/social-auth.ts
1457
1700
  import * as oidc from "openid-client";
1458
- import { randomBytes as randomBytes2, createHash, timingSafeEqual as timingSafeEqual2 } from "crypto";
1459
- var hash = (value) => createHash("sha256").update(value).digest();
1701
+ import { randomBytes as randomBytes3, createHash as createHash2, timingSafeEqual as timingSafeEqual3 } from "crypto";
1702
+ var hash2 = (value) => createHash2("sha256").update(value).digest();
1460
1703
  var validUrl = (value) => {
1461
1704
  const url = new URL(value);
1462
1705
  if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname))) throw new Error("Authentication URLs must use HTTPS (or local HTTP).");
@@ -1498,7 +1741,7 @@ function createSocialAuth(options) {
1498
1741
  if (provider === "sso" && !matchedRealm) throw new Error(typeof realm === "string" && realm ? "unknown_realm" : "provider_not_configured");
1499
1742
  for (const [key, value] of transactions) if (value.expires < Date.now()) transactions.delete(key);
1500
1743
  if (transactions.size >= 1e3) throw new Error("auth_busy");
1501
- const state = oidc.randomState(), verifier = oidc.randomPKCECodeVerifier(), nonce = oidc.randomNonce(), binding = randomBytes2(32).toString("hex");
1744
+ const state = oidc.randomState(), verifier = oidc.randomPKCECodeVerifier(), nonce = oidc.randomNonce(), binding = randomBytes3(32).toString("hex");
1502
1745
  const challenge = await oidc.calculatePKCECodeChallenge(verifier);
1503
1746
  let url;
1504
1747
  const parameters = { redirect_uri: callback(provider), state, code_challenge: challenge, code_challenge_method: "S256" };
@@ -1510,14 +1753,14 @@ function createSocialAuth(options) {
1510
1753
  url = oidc.buildAuthorizationUrl(configuration, { ...parameters, nonce, scope: "openid email profile" });
1511
1754
  }
1512
1755
  if (transactions.size >= 1e3) throw new Error("auth_busy");
1513
- transactions.set(state, { provider, origin, signup, onboarding, realm: matchedRealm?.id, verifier, nonce, binding: hash(binding).toString("hex"), expires: Date.now() + 6e5 });
1756
+ transactions.set(state, { provider, origin, signup, onboarding, realm: matchedRealm?.id, verifier, nonce, binding: hash2(binding).toString("hex"), expires: Date.now() + 6e5 });
1514
1757
  return { url: url.href, binding, secure: base.protocol === "https:" };
1515
1758
  },
1516
1759
  // Bind the state to this browser before exchanging any provider code. Consume once.
1517
1760
  take(provider, query, cookie) {
1518
1761
  if (query.getAll("state").length !== 1) throw new Error("invalid_state");
1519
1762
  const state = query.get("state"), transaction = transactions.get(state);
1520
- if (!transaction || transaction.provider !== provider || transaction.expires < Date.now() || !timingSafeEqual2(hash(cookie), Buffer.from(transaction.binding, "hex"))) throw new Error("invalid_state");
1763
+ if (!transaction || transaction.provider !== provider || transaction.expires < Date.now() || !timingSafeEqual3(hash2(cookie), Buffer.from(transaction.binding, "hex"))) throw new Error("invalid_state");
1521
1764
  transactions.delete(state);
1522
1765
  return transaction;
1523
1766
  },
@@ -1566,18 +1809,18 @@ function createSocialAuth(options) {
1566
1809
  }
1567
1810
 
1568
1811
  // src/runtime/auth.ts
1569
- import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual3, createHash as createHash2 } from "crypto";
1570
- var digest = (value) => createHash2("sha256").update(value).digest("hex");
1812
+ import { randomBytes as randomBytes4, scrypt, timingSafeEqual as timingSafeEqual4, createHash as createHash3 } from "crypto";
1813
+ var digest = (value) => createHash3("sha256").update(value).digest("hex");
1571
1814
  var derive = (password, salt) => new Promise((resolve5, reject) => scrypt(password, salt, 64, { N: 131072, r: 8, p: 1, maxmem: 160 * 1024 * 1024 }, (error, key) => error ? reject(error) : resolve5(key)));
1572
1815
  async function passwordHash(password) {
1573
- const salt = randomBytes3(16).toString("hex");
1816
+ const salt = randomBytes4(16).toString("hex");
1574
1817
  return `${salt}:${(await derive(password, salt)).toString("hex")}`;
1575
1818
  }
1576
1819
  async function passwordMatches(password, stored) {
1577
1820
  const [salt, key] = stored.split(":");
1578
1821
  const actual = await derive(password, salt);
1579
1822
  const expected = Buffer.from(key, "hex");
1580
- return expected.length === actual.length && timingSafeEqual3(expected, actual);
1823
+ return expected.length === actual.length && timingSafeEqual4(expected, actual);
1581
1824
  }
1582
1825
  function credentials(input) {
1583
1826
  if (!input || typeof input !== "object") throw new Error("Enter your email and password.");
@@ -1596,7 +1839,7 @@ var challengeStore = (map, key, value, ttlMs) => {
1596
1839
  init_dashboard_transfer();
1597
1840
  import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
1598
1841
  import { createServer as createServer2 } from "http";
1599
- import { createHash as createHash9, randomBytes as randomBytes6, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual4 } from "crypto";
1842
+ import { createHash as createHash10, randomBytes as randomBytes7, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual5 } from "crypto";
1600
1843
  import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile9, rename, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
1601
1844
  import { resolve as resolve4, join as join13, dirname as dirname2, extname } from "path";
1602
1845
  import { tmpdir as tmpdir3 } from "os";
@@ -1903,7 +2146,7 @@ import { mkdtemp as mkdtemp2, mkdir as mkdir6, readFile as readFile11, writeFile
1903
2146
  import { tmpdir as tmpdir2 } from "os";
1904
2147
  import { basename as basename2, dirname, isAbsolute, join as join12, relative as relative3, resolve as resolve3, sep as sep2 } from "path";
1905
2148
  import { fileURLToPath as fileURLToPath2 } from "url";
1906
- import { createHash as createHash7, randomUUID } from "crypto";
2149
+ import { createHash as createHash8, randomUUID } from "crypto";
1907
2150
 
1908
2151
  // src/runtime/credentials.ts
1909
2152
  import { homedir } from "os";
@@ -2155,10 +2398,10 @@ function stripeOperation(method, path2, params, data, sequence) {
2155
2398
  }
2156
2399
 
2157
2400
  // src/runtime/rest-adapters.ts
2158
- import { createHash as createHash3 } from "crypto";
2401
+ import { createHash as createHash4 } from "crypto";
2159
2402
  var EPOCH = 17e8;
2160
2403
  var iso = (epoch = EPOCH) => new Date(epoch * 1e3).toISOString();
2161
- var sha = (value) => createHash3("sha256").update(value).digest("hex");
2404
+ var sha = (value) => createHash4("sha256").update(value).digest("hex");
2162
2405
  var detUuid = (seed) => {
2163
2406
  const hex = sha(seed).slice(0, 32).split("");
2164
2407
  hex[12] = "4";
@@ -3041,21 +3284,21 @@ function restUnsupportedBody(adapter) {
3041
3284
  var REST_ADAPTER_NAMES = Object.keys(REST_ADAPTERS);
3042
3285
 
3043
3286
  // src/runtime/seed.ts
3044
- import { createHash as createHash4, randomBytes as randomBytes4 } from "crypto";
3287
+ import { createHash as createHash5, randomBytes as randomBytes5 } from "crypto";
3045
3288
  var SEED_PATTERN = /^[a-f0-9]{32}$/;
3046
3289
  var normalizeHex = (value) => value.trim().toLowerCase();
3047
3290
  function normalizeSeed(seed) {
3048
- if (seed === void 0 || seed.trim() === "") return randomBytes4(16).toString("hex");
3291
+ if (seed === void 0 || seed.trim() === "") return randomBytes5(16).toString("hex");
3049
3292
  const text = normalizeHex(seed);
3050
3293
  if (!/^[a-f0-9]+$/.test(text) || text.length > 256) throw new Error(`Seed must be up to 256 hex characters or empty to generate; got ${seed.slice(0, 40)}`);
3051
3294
  if (text.length === 32) return text;
3052
- return createHash4("sha256").update(text).digest("hex").slice(0, 32);
3295
+ return createHash5("sha256").update(text).digest("hex").slice(0, 32);
3053
3296
  }
3054
3297
  function deriveSeed(base, index) {
3055
3298
  if (!SEED_PATTERN.test(base)) throw new Error(`Invalid base seed: ${base}`);
3056
3299
  if (!Number.isInteger(index) || index < 0) throw new Error("Seed index must be a nonnegative integer");
3057
3300
  if (index === 0) return base;
3058
- return createHash4("sha256").update(`agenttwin-seed|${base}|${index}`).digest("hex").slice(0, 32);
3301
+ return createHash5("sha256").update(`agenttwin-seed|${base}|${index}`).digest("hex").slice(0, 32);
3059
3302
  }
3060
3303
  var SeedStream = class {
3061
3304
  constructor(seed, purpose) {
@@ -3068,7 +3311,7 @@ var SeedStream = class {
3068
3311
  purpose;
3069
3312
  counter = 0;
3070
3313
  draw() {
3071
- const value = createHash4("sha256").update(`agenttwin-stream|${this.seed}|${this.purpose}|${this.counter}`).digest("hex");
3314
+ const value = createHash5("sha256").update(`agenttwin-stream|${this.seed}|${this.purpose}|${this.counter}`).digest("hex");
3072
3315
  this.counter++;
3073
3316
  return value;
3074
3317
  }
@@ -3461,12 +3704,12 @@ var HttpWorld = class {
3461
3704
 
3462
3705
  // src/runtime/evaluation.ts
3463
3706
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
3464
- var matches = (row, where) => !where || Object.entries(where).every(([k, v]) => isDeepStrictEqual3(row[k], v));
3707
+ var matches2 = (row, where) => !where || Object.entries(where).every(([k, v]) => isDeepStrictEqual3(row[k], v));
3465
3708
  function evaluateState(spec, initial, final, events) {
3466
3709
  const checks = spec.assertions.map((a, index) => {
3467
3710
  const before = initial[a.service]?.[a.path];
3468
3711
  const after = final[a.service]?.[a.path];
3469
- const actual = after?.filter((row) => matches(row, a.where)).length;
3712
+ const actual = after?.filter((row) => matches2(row, a.where)).length;
3470
3713
  return {
3471
3714
  id: `assertion_${index + 1}`,
3472
3715
  kind: "required-outcome",
@@ -3478,13 +3721,13 @@ function evaluateState(spec, initial, final, events) {
3478
3721
  evidence: {
3479
3722
  initial: `/initialState/${escape(a.service)}/${escape(a.path)}`,
3480
3723
  final: `/finalState/${escape(a.service)}/${escape(a.path)}`,
3481
- beforeCount: before?.filter((row) => matches(row, a.where)).length ?? null,
3724
+ beforeCount: before?.filter((row) => matches2(row, a.where)).length ?? null,
3482
3725
  eventSequences: events.filter((e) => e.service === a.service && e.path === a.path).map((e) => e.sequence)
3483
3726
  }
3484
3727
  };
3485
3728
  });
3486
3729
  const forbidden = (spec.forbiddenEffects ?? []).map((policy) => {
3487
- const hits = events.filter((e) => e.service === policy.service && e.path === policy.path && e.operation === policy.operation && (!policy.where || e.record !== void 0 && matches(e.record, policy.where)));
3730
+ const hits = events.filter((e) => e.service === policy.service && e.path === policy.path && e.operation === policy.operation && (!policy.where || e.record !== void 0 && matches2(e.record, policy.where)));
3488
3731
  const unknown = events.some((e) => e.service === policy.service && e.path === policy.path && e.operation === policy.operation && policy.where && !e.record);
3489
3732
  return {
3490
3733
  id: policy.id,
@@ -3500,7 +3743,7 @@ function evaluateState(spec, initial, final, events) {
3500
3743
  const preserved = (spec.preservedState ?? []).map((policy) => {
3501
3744
  const before = initial[policy.service]?.[policy.path];
3502
3745
  const after = final[policy.service]?.[policy.path];
3503
- const passed = !!before && !!after && isDeepStrictEqual3(before.filter((row) => matches(row, policy.where)), after.filter((row) => matches(row, policy.where)));
3746
+ const passed = !!before && !!after && isDeepStrictEqual3(before.filter((row) => matches2(row, policy.where)), after.filter((row) => matches2(row, policy.where)));
3504
3747
  return {
3505
3748
  id: policy.id,
3506
3749
  kind: "preserved-state",
@@ -3523,7 +3766,7 @@ function evaluateProperty(policy, final, events) {
3523
3766
  const base = { id: policy.id, kind: "property", propertyKind: policy.kind, service: policy.service, path: policy.path, passed: false };
3524
3767
  const rows2 = final[policy.service]?.[policy.path];
3525
3768
  if (!rows2) return { ...base, expected: "intact", actual: "missing collection", evidence: { evidenceGap: `final state has no collection "${policy.path}" in "${policy.service}"` } };
3526
- const eventsFor = (operation2, where2) => events.filter((e) => e.service === policy.service && e.path === policy.path && e.operation === operation2 && (!where2 || e.record !== void 0 && matches(e.record, where2)));
3769
+ const eventsFor = (operation2, where2) => events.filter((e) => e.service === policy.service && e.path === policy.path && e.operation === operation2 && (!where2 || e.record !== void 0 && matches2(e.record, where2)));
3527
3770
  if (policy.kind === "unique-per-act") {
3528
3771
  const field = String(policy.params?.field);
3529
3772
  const keyed = /* @__PURE__ */ new Map();
@@ -3668,7 +3911,7 @@ init_git();
3668
3911
  // src/runtime/e2b.ts
3669
3912
  import { Sandbox } from "e2b";
3670
3913
  import { readFile as readFile4 } from "fs/promises";
3671
- import { randomBytes as randomBytes5 } from "crypto";
3914
+ import { randomBytes as randomBytes6 } from "crypto";
3672
3915
  import { homedir as homedir2 } from "os";
3673
3916
  import { join as join5 } from "path";
3674
3917
  async function executeE2B(stage, spec, log, signal) {
@@ -3708,7 +3951,7 @@ async function executeE2B(stage, spec, log, signal) {
3708
3951
  await sandbox.commands.run("chmod -R a+rX /input", { user: "root" });
3709
3952
  const runtimeEnv = {};
3710
3953
  if (spec.postgres) {
3711
- const password = randomBytes5(24).toString("hex");
3954
+ const password = randomBytes6(24).toString("hex");
3712
3955
  secrets.push(password);
3713
3956
  await sandbox.commands.run("pg_ctlcluster --skip-systemctl-redirect 15 main start -o '-c ssl=off -c listen_addresses=127.0.0.1'", { user: "root" });
3714
3957
  await sandbox.files.write("/tmp/agenttwin-init.sql", `CREATE ROLE agenttwin LOGIN PASSWORD '${password}';
@@ -3722,7 +3965,7 @@ CREATE DATABASE agenttwin OWNER agenttwin;
3722
3965
  }
3723
3966
  }
3724
3967
  if (spec.redis) {
3725
- const password = randomBytes5(24).toString("hex");
3968
+ const password = randomBytes6(24).toString("hex");
3726
3969
  secrets.push(password);
3727
3970
  await sandbox.files.write("/tmp/agenttwin-redis.conf", `bind 127.0.0.1
3728
3971
  port 6379
@@ -3762,14 +4005,14 @@ logfile /tmp/agenttwin-redis.log
3762
4005
  }
3763
4006
 
3764
4007
  // src/runtime/scenarios.ts
3765
- import { createHash as createHash5 } from "crypto";
4008
+ import { createHash as createHash6 } from "crypto";
3766
4009
  import { mkdir as mkdir2, readFile as readFile5, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
3767
4010
  import { join as join6 } from "path";
3768
4011
  import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
3769
4012
  var identifier = /^[a-z][a-z0-9_]{0,39}$/;
3770
4013
  var object2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3771
4014
  var sort = (v) => Array.isArray(v) ? v.map(sort) : object2(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])])) : v;
3772
- var scenarioDigest = (scenario) => createHash5("sha256").update(JSON.stringify(sort(scenario))).digest("hex");
4015
+ var scenarioDigest = (scenario) => createHash6("sha256").update(JSON.stringify(sort(scenario))).digest("hex");
3773
4016
  function parseScenario(value) {
3774
4017
  const fail = (detail) => {
3775
4018
  throw new Error(`Invalid scenario: ${detail}`);
@@ -3821,14 +4064,14 @@ function applyScenario(world, scenario) {
3821
4064
  }
3822
4065
 
3823
4066
  // src/runtime/matrix.ts
3824
- import { createHash as createHash6 } from "crypto";
4067
+ import { createHash as createHash7 } from "crypto";
3825
4068
  import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
3826
4069
  import { join as join7, relative, resolve as resolve2 } from "path";
3827
4070
  var MAX_SEEDS = 128;
3828
4071
  var APP_RANDOMNESS_NOTE = "The seed controls twin-side behavior only: fault scheduling, latency jitter and test credentials. The application's own randomness, wall-clock behavior and threading are not controlled.";
3829
4072
  function filesDigest(sourceDigests) {
3830
4073
  const canonical2 = Object.keys(sourceDigests).sort().map((file) => `${file}:${sourceDigests[file]}`).join("\n");
3831
- return createHash6("sha256").update(canonical2).digest("hex");
4074
+ return createHash7("sha256").update(canonical2).digest("hex");
3832
4075
  }
3833
4076
  function checksOfReceipt(receipt) {
3834
4077
  const checks = (receipt.evaluation?.checks ?? []).map((c) => ({
@@ -3913,7 +4156,7 @@ function buildMatrixReport(input) {
3913
4156
  async function runSeedMatrix(cwd, opts) {
3914
4157
  if (!Number.isInteger(opts.seeds) || opts.seeds < 1 || opts.seeds > MAX_SEEDS) throw new CliError(`--seeds must be an integer between 1 and ${MAX_SEEDS}`, { code: EXIT.USAGE });
3915
4158
  const baseSeed = normalizeSeed(opts.seed);
3916
- const matrixName = `matrix-${createHash6("sha256").update(`${baseSeed}:${opts.seeds}`).digest("hex").slice(0, 8)}`;
4159
+ const matrixName = `matrix-${createHash7("sha256").update(`${baseSeed}:${opts.seeds}`).digest("hex").slice(0, 8)}`;
3917
4160
  const matrixDir = resolve2(cwd, opts.out ?? join7(".agenttwin/runs", matrixName));
3918
4161
  await mkdir3(matrixDir, { recursive: true, mode: 448 });
3919
4162
  const results = [];
@@ -3970,7 +4213,7 @@ async function stageApplication(root, files, destination) {
3970
4213
  const target = join12(destination, file);
3971
4214
  await mkdir6(dirname(target), { recursive: true });
3972
4215
  await writeFile8(target, contents, { mode: 420 });
3973
- digests[file] = createHash7("sha256").update(contents).digest("hex");
4216
+ digests[file] = createHash8("sha256").update(contents).digest("hex");
3974
4217
  }
3975
4218
  return digests;
3976
4219
  }
@@ -4032,7 +4275,7 @@ async function runWorld(cwd, opts) {
4032
4275
  opts.log("Testing through the CLI with in-process service twins. No cloud VM or containers.");
4033
4276
  const report2 = await executeLocal(join12(stage, "app"), spec, opts.signal, void 0, suppliedSeed);
4034
4277
  const runId2 = randomUUID();
4035
- const receipt2 = { ...report2, sourceDirectory: relative3(cwd, dirname(file)).split(sep2).join("/"), runId: runId2, runtime, project, git, createdAt: (/* @__PURE__ */ new Date()).toISOString(), sourceDigests, specDigest: createHash7("sha256").update(JSON.stringify(spec)).digest("hex"), provenance: "CLI-reported execution" };
4278
+ const receipt2 = { ...report2, sourceDirectory: relative3(cwd, dirname(file)).split(sep2).join("/"), runId: runId2, runtime, project, git, createdAt: (/* @__PURE__ */ new Date()).toISOString(), sourceDigests, specDigest: createHash8("sha256").update(JSON.stringify(spec)).digest("hex"), provenance: "CLI-reported execution" };
4036
4279
  const out2 = resolve3(cwd, opts.out ?? `.agenttwin/runs/${runId2}`);
4037
4280
  await mkdir6(out2, { recursive: true });
4038
4281
  const evidencePath = join12(out2, "run.json"), reportPath = join12(out2, "report.md");
@@ -4053,7 +4296,7 @@ ${receipt2.reproducibility ?? ""}
4053
4296
  if (runtime === "hosted") {
4054
4297
  const receipt2 = await executeRemote(stage, spec, opts.log, { git, project });
4055
4298
  if (receipt2.schema !== "agenttwin/run-v1" || typeof receipt2.runId !== "string" || !/^[a-f0-9-]{36}$/.test(receipt2.runId) || !["passed", "failed", "error", "incomplete"].includes(String(receipt2.status))) throw new CliError("Invalid hosted receipt");
4056
- const expectedSpec = createHash7("sha256").update(JSON.stringify(spec)).digest("hex");
4299
+ const expectedSpec = createHash8("sha256").update(JSON.stringify(spec)).digest("hex");
4057
4300
  const reportedSources = receipt2.sourceDigests;
4058
4301
  if (receipt2.specDigest !== expectedSpec || !reportedSources || Object.keys(reportedSources).length !== Object.keys(sourceDigests).length || Object.entries(sourceDigests).some(([path2, digest2]) => reportedSources[path2] !== digest2)) throw new CliError("Hosted receipt does not match the submitted source/specification");
4059
4302
  const out2 = resolve3(cwd, opts.out ?? `.agenttwin/runs/${receipt2.runId}`);
@@ -4091,7 +4334,7 @@ See run.json for assertions, service state, application output and isolation met
4091
4334
  image,
4092
4335
  templateBuildId,
4093
4336
  sourceDigests,
4094
- specDigest: createHash7("sha256").update(JSON.stringify(spec)).digest("hex"),
4337
+ specDigest: createHash8("sha256").update(JSON.stringify(spec)).digest("hex"),
4095
4338
  isolation
4096
4339
  };
4097
4340
  const out = resolve3(cwd, opts.out ?? `.agenttwin/runs/${runId}`);
@@ -4107,7 +4350,7 @@ See run.json for assertions, service state, application output and isolation met
4107
4350
  }
4108
4351
 
4109
4352
  // src/runtime/analytics.ts
4110
- import { createHash as createHash8 } from "crypto";
4353
+ import { createHash as createHash9 } from "crypto";
4111
4354
  function analyticsConfiguration() {
4112
4355
  const key = process.env.POSTHOG_API_KEY;
4113
4356
  const host = process.env.POSTHOG_API_HOST || "https://us.i.posthog.com";
@@ -4142,7 +4385,7 @@ function captureAnalytics(event) {
4142
4385
  }
4143
4386
 
4144
4387
  // src/runtime/gateway.ts
4145
- var hash2 = (value) => createHash9("sha256").update(value).digest("hex");
4388
+ var hash3 = (value) => createHash10("sha256").update(value).digest("hex");
4146
4389
  function uploadPath(path2) {
4147
4390
  if (!path2 || path2.startsWith("/") || path2.includes("\\") || path2.includes("\0") || path2.split("/").some((p) => !p || p === "." || p === ".." || /^(\.env.*|\.git|\.ssh|\.aws|\.azure|\.config|\.npmrc|\.netrc|credentials.*)$/i.test(p)) || /\.(pem|key|p12|pfx)$/i.test(path2)) throw new Error("Unsafe uploaded file path");
4148
4391
  return path2;
@@ -4158,6 +4401,11 @@ async function body(req, limit = 28 * 1024 * 1024) {
4158
4401
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
4159
4402
  }
4160
4403
  function send(res, code, value) {
4404
+ if (code === 429 && !res.hasHeader("Retry-After")) {
4405
+ const error = value?.error ?? "";
4406
+ const wait = error.startsWith("daily_") ? 86400 - Math.floor(Date.now() / 1e3) % 86400 : 60;
4407
+ res.setHeader("Retry-After", String(wait));
4408
+ }
4161
4409
  res.writeHead(code, {
4162
4410
  "content-type": "application/json",
4163
4411
  "cache-control": "no-store",
@@ -4169,11 +4417,14 @@ function send(res, code, value) {
4169
4417
  res.end(JSON.stringify(value));
4170
4418
  }
4171
4419
  function equal(a, b) {
4172
- const x = Buffer.from(hash2(a)), y = Buffer.from(hash2(b));
4173
- return timingSafeEqual4(x, y);
4420
+ const x = Buffer.from(hash3(a)), y = Buffer.from(hash3(b));
4421
+ return timingSafeEqual5(x, y);
4174
4422
  }
4175
4423
  async function startGateway(opts) {
4176
4424
  if (opts.adminToken.length < 40) throw new Error("AGENTTWIN_ADMIN_TOKEN must have at least 40 characters");
4425
+ const issuer = opts.publicUrl ?? "https://api.agenttwin.si";
4426
+ const issuerUrl = new URL(issuer);
4427
+ if (issuerUrl.origin !== issuer || issuerUrl.protocol !== "https:" && !(issuerUrl.protocol === "http:" && ["localhost", "127.0.0.1"].includes(issuerUrl.hostname))) throw new Error("Invalid API public URL");
4177
4428
  const socialAuth = createSocialAuth(opts.socialAuth);
4178
4429
  const emailVerification = createEmailVerification(opts.emailVerification, opts.adminToken);
4179
4430
  const allowedOrigins = new Set(opts.allowedOrigins ?? []);
@@ -4219,6 +4470,7 @@ async function startGateway(opts) {
4219
4470
  });
4220
4471
  return writes;
4221
4472
  };
4473
+ const oauth = createOAuth(store, save, issuer);
4222
4474
  for (const thread of store.chats ?? []) {
4223
4475
  const last = thread.messages.at(-1);
4224
4476
  if (last?.role === "user") thread.messages.push({ id: randomUUID2(), role: "assistant", content: "The API restarted before this response completed. Your message is saved.", error: "model_interrupted", requestId: last.requestId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), context: last.context });
@@ -4291,7 +4543,8 @@ async function startGateway(opts) {
4291
4543
  const devices = /* @__PURE__ */ new Map();
4292
4544
  let hashing = 0;
4293
4545
  let localOnboardingUntil = 0;
4294
- const dummyPassword = randomBytes6(16).toString("hex") + ":" + randomBytes6(64).toString("hex");
4546
+ const dummyPassword = randomBytes7(16).toString("hex") + ":" + randomBytes7(64).toString("hex");
4547
+ const budget = requestBudget(opts.requestsPerMinute);
4295
4548
  const server = createServer2(async (req, res) => {
4296
4549
  try {
4297
4550
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
@@ -4318,6 +4571,11 @@ async function startGateway(opts) {
4318
4571
  }
4319
4572
  return;
4320
4573
  }
4574
+ if (!budget(res)) {
4575
+ send(res, 429, { error: "rate_limit_exceeded" });
4576
+ return;
4577
+ }
4578
+ res.setHeader("Access-Control-Expose-Headers", "RateLimit, RateLimit-Policy, Retry-After, WWW-Authenticate, Location");
4321
4579
  const origin = req.headers.origin;
4322
4580
  if (origin) {
4323
4581
  if (!allowedOrigins.has(origin)) {
@@ -4329,13 +4587,26 @@ async function startGateway(opts) {
4329
4587
  res.setHeader("Access-Control-Allow-Credentials", "true");
4330
4588
  if (req.method === "OPTIONS") {
4331
4589
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
4332
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
4590
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Idempotency-Key");
4333
4591
  res.setHeader("Access-Control-Max-Age", "600");
4334
4592
  res.writeHead(204);
4335
4593
  res.end();
4336
4594
  return;
4337
4595
  }
4338
4596
  }
4597
+ if (req.method === "GET" && path2 === "/.well-known/oauth-authorization-server") {
4598
+ send(res, 200, oauthMetadata(issuer));
4599
+ return;
4600
+ }
4601
+ if (req.method === "GET" && path2 === "/.well-known/oauth-protected-resource") {
4602
+ send(res, 200, resourceMetadata(issuer));
4603
+ return;
4604
+ }
4605
+ if (!storageHealthy) {
4606
+ send(res, 503, { error: "storage_unavailable" });
4607
+ return;
4608
+ }
4609
+ if (await oauth(req, res, path2)) return;
4339
4610
  if (path2 === "/healthz" && req.method === "GET") {
4340
4611
  send(res, 200, { status: stopping ? "stopping" : "ok" });
4341
4612
  return;
@@ -4344,7 +4615,26 @@ async function startGateway(opts) {
4344
4615
  send(res, 503, { error: "storage_unavailable" });
4345
4616
  return;
4346
4617
  }
4347
- const token = req.headers.authorization?.replace(/^Bearer /, "") ?? "";
4618
+ const token = req.headers.authorization?.match(/^Bearer (.+)$/i)?.[1] ?? "";
4619
+ const grant = oauthGrant(store, token);
4620
+ if (token.startsWith("at_oauth_") && !grant) {
4621
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${issuer}/.well-known/oauth-protected-resource", error="invalid_token"`);
4622
+ send(res, 401, { error: "invalid_token" });
4623
+ return;
4624
+ }
4625
+ if (grant) {
4626
+ const required = requiredOAuthScope(req.method ?? "", path2);
4627
+ if (!required || !grant.token.scopes.includes(required)) {
4628
+ res.setHeader("WWW-Authenticate", `Bearer error="insufficient_scope"${required ? `, scope="${required}"` : ""}`);
4629
+ send(res, 403, { error: "insufficient_scope" });
4630
+ return;
4631
+ }
4632
+ if (requestUrl.searchParams.has("project") && requestUrl.searchParams.get("project") !== grant.client.project) {
4633
+ send(res, 403, { error: "project_not_authorized" });
4634
+ return;
4635
+ }
4636
+ requestUrl.searchParams.set("project", grant.client.project);
4637
+ }
4348
4638
  const cookieToken = req.headers.cookie?.split(";").map((v) => v.trim()).find((v) => v.startsWith("at_session="))?.slice(11) ?? "";
4349
4639
  const session = store.sessions?.find((s) => s.hash === digest(cookieToken) && s.expires > Date.now());
4350
4640
  if (cookieToken && !token && !["GET", "HEAD", "OPTIONS"].includes(req.method ?? "") && (!origin || !allowedOrigins.has(origin))) {
@@ -4487,12 +4777,12 @@ async function startGateway(opts) {
4487
4777
  return;
4488
4778
  }
4489
4779
  if (store.accounts.length >= 1e4) throw new Error("account_capacity");
4490
- owner = { id: randomUUID2(), tokenHash: hash2(randomBytes6(32).toString("hex")), label: identity.name + "'s workspace", active: true, usageDay: "", runs: 0, identity: { email: identity.email, name: identity.name }, externalIdentities: [{ provider: identity.provider, issuer: identity.issuer, subject: identity.subject }], ...identity.organization ? { organization: identity.organization } : {} };
4780
+ owner = { id: randomUUID2(), tokenHash: hash3(randomBytes7(32).toString("hex")), label: identity.name + "'s workspace", active: true, usageDay: "", runs: 0, identity: { email: identity.email, name: identity.name }, externalIdentities: [{ provider: identity.provider, issuer: identity.issuer, subject: identity.subject }], ...identity.organization ? { organization: identity.organization } : {} };
4491
4781
  store.accounts.push(owner);
4492
4782
  captureAnalytics({ event: "account_created", distinctId: owner.id, properties: { method: identity.provider, realm: identity.organization || identity.issuer } });
4493
4783
  }
4494
4784
  if (!owner.active) throw new Error("account_inactive");
4495
- const secret = randomBytes6(32).toString("hex");
4785
+ const secret = randomBytes7(32).toString("hex");
4496
4786
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4497
4787
  const owned = store.sessions.filter((s) => s.accountId === owner.id);
4498
4788
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
@@ -4513,7 +4803,7 @@ async function startGateway(opts) {
4513
4803
  send(res, 429, { error: "Try again shortly." });
4514
4804
  return;
4515
4805
  }
4516
- const secret = randomBytes6(32).toString("hex"), code = randomBytes6(5).toString("hex").toUpperCase();
4806
+ const secret = randomBytes7(32).toString("hex"), code = randomBytes7(5).toString("hex").toUpperCase();
4517
4807
  devices.set(digest(secret), { code, expires: Date.now() + 6e5, lastPoll: 0 });
4518
4808
  send(res, 200, { deviceCode: secret, userCode: code, verificationUrl, expiresIn: 600 });
4519
4809
  return;
@@ -4534,7 +4824,7 @@ async function startGateway(opts) {
4534
4824
  }
4535
4825
  const rpID = requestUrl.hostname === "localhost" || requestUrl.hostname === "127.0.0.1" ? "localhost" : new URL(origin).hostname;
4536
4826
  const options = await generateRegistrationOptions({ rpName: "AgentTwin", rpID, userName: owner.identity?.email ?? owner.label, userDisplayName: owner.identity?.name ?? owner.label, excludeCredentials: (owner.passkeys ?? []).map((k) => ({ id: k.id })) });
4537
- const challengeId = randomBytes6(16).toString("hex");
4827
+ const challengeId = randomBytes7(16).toString("hex");
4538
4828
  challengeStore(passkeyChallenges, challengeId, options.challenge, 12e4);
4539
4829
  send(res, 200, { options, challengeId });
4540
4830
  return;
@@ -4581,7 +4871,7 @@ async function startGateway(opts) {
4581
4871
  const credentialId = typeof input.credentialId === "string" ? input.credentialId.slice(0, 1024) : "";
4582
4872
  const owner = credentialId ? store.accounts.find((a) => a.active && (a.passkeys ?? []).some((k) => k.id === credentialId)) : void 0;
4583
4873
  const options = await generateAuthenticationOptions({ rpID: requestUrl.hostname === "localhost" || requestUrl.hostname === "127.0.0.1" ? "localhost" : new URL(origin).hostname, allowCredentials: credentialId && owner ? [{ id: credentialId }] : void 0, userVerification: "preferred" });
4584
- const challengeId = randomBytes6(16).toString("hex");
4874
+ const challengeId = randomBytes7(16).toString("hex");
4585
4875
  challengeStore(passkeyChallenges, challengeId, options.challenge, 12e4);
4586
4876
  send(res, 200, { options, challengeId });
4587
4877
  return;
@@ -4612,7 +4902,7 @@ async function startGateway(opts) {
4612
4902
  return;
4613
4903
  }
4614
4904
  key.counter = verification.authenticationInfo.newCounter;
4615
- const secret = randomBytes6(32).toString("hex");
4905
+ const secret = randomBytes7(32).toString("hex");
4616
4906
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4617
4907
  const owned = store.sessions.filter((s) => s.accountId === owner.id);
4618
4908
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
@@ -4673,7 +4963,8 @@ async function startGateway(opts) {
4673
4963
  send(res, 400, { error: "Use a valid email and a password with 12\u2013128 characters." });
4674
4964
  return;
4675
4965
  }
4676
- if (emailVerification.enabled && (!emailGrant?.verified || emailGrant.email !== parsed.email || !emailVerification.bound(emailGrant, emailBinding ?? "", origin))) {
4966
+ const demoLogin = path2 === "/v1/auth/login" && parsed.email === "demo@agenttwin.si" && store.accounts.some((a) => a.identity?.email === parsed.email && a.identity.emailVerified === true);
4967
+ if (emailVerification.enabled && !demoLogin && (!emailGrant?.verified || emailGrant.email !== parsed.email || !emailVerification.bound(emailGrant, emailBinding ?? "", origin))) {
4677
4968
  send(res, 403, { error: "Verify your email with a one-time code before continuing.", code: "email_verification_required" });
4678
4969
  return;
4679
4970
  }
@@ -4708,21 +4999,21 @@ async function startGateway(opts) {
4708
4999
  send(res, 429, { error: "account_capacity" });
4709
5000
  return;
4710
5001
  }
4711
- a = { id: randomUUID2(), tokenHash: hash2(randomBytes6(32).toString("hex")), label: workspace, active: true, usageDay: "", runs: 0, identity: { email: parsed.email, name, passwordHash: encoded } };
5002
+ a = { id: randomUUID2(), tokenHash: hash3(randomBytes7(32).toString("hex")), label: workspace, active: true, usageDay: "", runs: 0, identity: { email: parsed.email, name, passwordHash: encoded } };
4712
5003
  store.accounts.push(a);
4713
5004
  captureAnalytics({ event: "account_created", distinctId: a.id, properties: { method: "email", realm: "password" } });
4714
5005
  } else {
4715
- const matches2 = await passwordMatches(parsed.password, a?.identity?.passwordHash ?? dummyPassword);
4716
- if (!a?.active || !a.identity?.passwordHash || !matches2) {
5006
+ const matches3 = await passwordMatches(parsed.password, a?.identity?.passwordHash ?? dummyPassword);
5007
+ if (!a?.active || !a.identity?.passwordHash || !matches3) {
4717
5008
  send(res, 401, { error: "Email or password is incorrect." });
4718
5009
  return;
4719
5010
  }
4720
5011
  }
4721
- const secret = randomBytes6(32).toString("hex");
5012
+ const secret = randomBytes7(32).toString("hex");
4722
5013
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4723
5014
  const owned = store.sessions.filter((s) => s.accountId === a.id);
4724
5015
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
4725
- if (emailVerification.enabled) {
5016
+ if (emailVerification.enabled && !demoLogin) {
4726
5017
  if (store.emailChallenges?.[emailChallengeId] !== emailGrant || !emailVerification.bound(emailGrant, emailBinding ?? "", origin)) {
4727
5018
  send(res, 403, { error: "Email verification expired. Request a new code." });
4728
5019
  return;
@@ -4828,8 +5119,8 @@ async function startGateway(opts) {
4828
5119
  send(res, 400, { error: "label_required" });
4829
5120
  return;
4830
5121
  }
4831
- const accessToken = `at_user_${randomBytes6(32).toString("hex")}`;
4832
- const account2 = { id: randomUUID2(), tokenHash: hash2(accessToken), label: data.label, active: true, usageDay: "", runs: 0 };
5122
+ const accessToken = `at_user_${randomBytes7(32).toString("hex")}`;
5123
+ const account2 = { id: randomUUID2(), tokenHash: hash3(accessToken), label: data.label, active: true, usageDay: "", runs: 0 };
4833
5124
  if (store.accounts.length >= 1e4) {
4834
5125
  send(res, 429, { error: "account_capacity" });
4835
5126
  return;
@@ -4855,8 +5146,9 @@ async function startGateway(opts) {
4855
5146
  send(res, 200, { revoked: true });
4856
5147
  return;
4857
5148
  }
4858
- const account = store.accounts.find((a) => a.active && (token ? a.tokenHash === hash2(token) || a.cliTokens?.includes(hash2(token)) : a.id === session?.accountId));
5149
+ const account = store.accounts.find((a) => a.active && (grant ? a.id === grant.client.accountId : token ? a.tokenHash === hash3(token) || a.cliTokens?.includes(hash3(token)) : a.id === session?.accountId));
4859
5150
  if (!account) {
5151
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
4860
5152
  send(res, 401, { error: "unauthorized" });
4861
5153
  return;
4862
5154
  }
@@ -4907,7 +5199,7 @@ async function startGateway(opts) {
4907
5199
  return;
4908
5200
  }
4909
5201
  const previousIdentity = account.identity, previousSessions = store.sessions;
4910
- const secret = randomBytes6(32).toString("hex");
5202
+ const secret = randomBytes7(32).toString("hex");
4911
5203
  account.identity = { ...account.identity, passwordHash: encoded };
4912
5204
  store.sessions = [...store.sessions.filter((s) => s.accountId !== account.id), { accountId: account.id, hash: digest(secret), expires: Date.now() + 7 * 864e5 }];
4913
5205
  try {
@@ -4929,8 +5221,8 @@ async function startGateway(opts) {
4929
5221
  send(res, 400, { error: "terminal_token_required" });
4930
5222
  return;
4931
5223
  }
4932
- account.cliTokens = (account.cliTokens ?? []).filter((t) => t !== hash2(token));
4933
- if (account.tokenHash === hash2(token)) account.tokenHash = hash2(randomBytes6(32).toString("hex"));
5224
+ account.cliTokens = (account.cliTokens ?? []).filter((t) => t !== hash3(token));
5225
+ if (account.tokenHash === hash3(token)) account.tokenHash = hash3(randomBytes7(32).toString("hex"));
4934
5226
  await save();
4935
5227
  send(res, 200, { signedOut: true });
4936
5228
  return;
@@ -4943,8 +5235,8 @@ async function startGateway(opts) {
4943
5235
  send(res, 400, { error: "Code not found or expired. Run agenttwin login again." });
4944
5236
  return;
4945
5237
  }
4946
- const accessToken = `at_user_${randomBytes6(32).toString("hex")}`;
4947
- account.cliTokens = [...(account.cliTokens ?? []).slice(-9), hash2(accessToken)];
5238
+ const accessToken = `at_user_${randomBytes7(32).toString("hex")}`;
5239
+ account.cliTokens = [...(account.cliTokens ?? []).slice(-9), hash3(accessToken)];
4948
5240
  device.accountId = account.id;
4949
5241
  await save();
4950
5242
  device.token = accessToken;
@@ -4991,6 +5283,47 @@ async function startGateway(opts) {
4991
5283
  const projectInventories = [...store.projectDiscoveries?.[account.id] ?? []];
4992
5284
  const latestInventory = store.discoveries?.[account.id];
4993
5285
  if (latestInventory && !projectInventories.some((p) => p.name === latestInventory.name)) projectInventories.push(latestInventory);
5286
+ if (path2 === "/v1/oauth/clients" && req.method === "POST") {
5287
+ const input = await body(req, 8192);
5288
+ if (typeof input.label !== "string" || !input.label.trim() || input.label.length > 100 || typeof input.project !== "string" || !projectInventories.some((p) => p.name === input.project)) {
5289
+ send(res, 400, { error: "valid_label_and_synced_project_required" });
5290
+ return;
5291
+ }
5292
+ if ((store.oauthClients ?? []).filter((c) => c.accountId === account.id && c.active).length >= 20) {
5293
+ send(res, 429, { error: "client_capacity" });
5294
+ return;
5295
+ }
5296
+ if (!account.active) {
5297
+ send(res, 401, { error: "unauthorized" });
5298
+ return;
5299
+ }
5300
+ if ((store.oauthClients ?? []).length >= 1e4) {
5301
+ send(res, 429, { error: "client_capacity" });
5302
+ return;
5303
+ }
5304
+ const { client, secret } = newOAuthClient(account.id, input.label.trim(), input.project, input.scopes);
5305
+ store.oauthClients ??= [];
5306
+ store.oauthClients.push(client);
5307
+ await save();
5308
+ send(res, 201, { client_id: client.id, client_secret: secret, project: client.project, scope: client.scopes.join(" "), token_endpoint: issuer + "/oauth/token" });
5309
+ return;
5310
+ }
5311
+ if (path2 === "/v1/oauth/clients" && req.method === "GET") {
5312
+ send(res, 200, { clients: (store.oauthClients ?? []).filter((c) => c.accountId === account.id && c.active).map(({ secretHash, accountId, ...client }) => client) });
5313
+ return;
5314
+ }
5315
+ if (/^\/v1\/oauth\/clients\/at_client_[a-f0-9]{32}$/.test(path2) && req.method === "DELETE") {
5316
+ const client = store.oauthClients?.find((c) => c.id === path2.split("/").at(-1) && c.accountId === account.id);
5317
+ if (!client) {
5318
+ send(res, 404, { error: "not_found" });
5319
+ return;
5320
+ }
5321
+ client.active = false;
5322
+ store.oauthTokens = (store.oauthTokens ?? []).filter((t) => t.clientId !== client.id);
5323
+ await save();
5324
+ send(res, 200, { revoked: true });
5325
+ return;
5326
+ }
4994
5327
  const requestedProject = requestUrl.searchParams.get("project");
4995
5328
  const selectedInventory = requestedProject === null ? latestInventory : projectInventories.find((p) => p.name === requestedProject);
4996
5329
  if (path2.startsWith("/v1/projects/") && req.method === "DELETE") {
@@ -5033,7 +5366,7 @@ async function startGateway(opts) {
5033
5366
  return;
5034
5367
  }
5035
5368
  if (path2 === "/v1/projects" && req.method === "GET") {
5036
- send(res, 200, { projects: projectInventories.map((p) => ({ name: p.name, tools: p.tools.length, workflows: p.workflows.length })), latest: latestInventory?.name ?? null });
5369
+ send(res, 200, { projects: projectInventories.filter((p) => !grant || p.name === grant.client.project).map((p) => ({ name: p.name, tools: p.tools.length, workflows: p.workflows.length })), latest: grant ? grant.client.project : latestInventory?.name ?? null });
5037
5370
  return;
5038
5371
  }
5039
5372
  if (path2 === "/v1/discovery" && req.method === "GET") {
@@ -5091,7 +5424,20 @@ async function startGateway(opts) {
5091
5424
  return;
5092
5425
  }
5093
5426
  if (path2 === "/v1/runs" && req.method === "GET") {
5094
- send(res, 200, { jobs: store.jobs.filter((j) => j.accountId === account.id).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)).slice(0, 100).map((job) => ({ id: job.id, status: job.status, createdAt: job.createdAt, name: job.name ?? (typeof job.receipt?.name === "string" ? job.receipt.name : void 0), workflowId: job.workflowId ?? (typeof job.receipt?.workflowId === "string" ? job.receipt.workflowId : void 0), project: job.project ?? (typeof job.receipt?.project === "string" ? job.receipt.project : void 0) })) });
5427
+ const limitText = requestUrl.searchParams.get("limit") ?? "100";
5428
+ if (!/^[1-9][0-9]?$|^100$/.test(limitText)) {
5429
+ send(res, 400, { error: "invalid_page_limit" });
5430
+ return;
5431
+ }
5432
+ const limit = Number(limitText), project = requestUrl.searchParams.get("project");
5433
+ const jobs = store.jobs.filter((j) => j.accountId === account.id && (project === null || (j.project ?? j.receipt?.project) === project)).sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt) || b.id.localeCompare(a.id));
5434
+ const cursor = requestUrl.searchParams.get("cursor"), offset = cursor === null ? 0 : jobs.findIndex((j) => j.id === cursor) + 1;
5435
+ if (cursor !== null && offset === 0) {
5436
+ send(res, 400, { error: "invalid_page_cursor" });
5437
+ return;
5438
+ }
5439
+ const page = jobs.slice(offset, offset + limit);
5440
+ send(res, 200, { jobs: page.map((job) => ({ id: job.id, status: job.status, createdAt: job.createdAt, name: job.name ?? job.receipt?.name, workflowId: job.workflowId ?? job.receipt?.workflowId, project: job.project ?? job.receipt?.project })), nextCursor: offset + limit < jobs.length ? page.at(-1).id : null });
5095
5441
  return;
5096
5442
  }
5097
5443
  if (path2 === "/v1/runs" && req.method === "POST") {
@@ -5099,24 +5445,38 @@ async function startGateway(opts) {
5099
5445
  send(res, 503, { error: "stopping" });
5100
5446
  return;
5101
5447
  }
5102
- if (!process.env.E2B_API_KEY || !process.env.E2B_TEMPLATE_ID) {
5103
- send(res, 503, { error: "cloud_runner_not_configured", message: "Run through the CLI, or configure a cloud runner when this workflow needs one." });
5448
+ const input = await body(req);
5449
+ if (grant && !oauthGrant(store, token)) {
5450
+ send(res, 401, { error: "invalid_token" });
5104
5451
  return;
5105
5452
  }
5106
- if (busy(account.id)) {
5107
- send(res, 429, { error: "run_in_progress_or_queue_full" });
5453
+ if (grant && input.project !== grant.client.project) {
5454
+ send(res, 403, { error: "project_not_authorized" });
5108
5455
  return;
5109
5456
  }
5110
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5111
- if (account.usageDay !== today) {
5112
- account.usageDay = today;
5113
- account.runs = 0;
5457
+ const idempotencyKey = req.headers["idempotency-key"];
5458
+ if (idempotencyKey !== void 0 && (typeof idempotencyKey !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey))) {
5459
+ send(res, 400, { error: "invalid_idempotency_key" });
5460
+ return;
5114
5461
  }
5115
- if (account.runs >= dailyLimit) {
5116
- send(res, 429, { error: "daily_run_limit" });
5462
+ const requestDigest = hash3(JSON.stringify(input));
5463
+ if (idempotencyKey) {
5464
+ const prior = store.jobs.find((j) => j.accountId === account.id && j.idempotencyKey === idempotencyKey);
5465
+ if (prior) {
5466
+ if (prior.requestDigest !== requestDigest) {
5467
+ send(res, 409, { error: "idempotency_key_conflict" });
5468
+ return;
5469
+ }
5470
+ res.setHeader("Location", issuer + "/v1/runs/" + prior.id);
5471
+ res.setHeader("Idempotency-Replayed", "true");
5472
+ send(res, 202, { id: prior.id, status: prior.status });
5473
+ return;
5474
+ }
5475
+ }
5476
+ if (!process.env.E2B_API_KEY || !process.env.E2B_TEMPLATE_ID) {
5477
+ send(res, 503, { error: "cloud_runner_not_configured", message: "Run through the CLI, or configure a cloud runner when this workflow needs one." });
5117
5478
  return;
5118
5479
  }
5119
- const input = await body(req);
5120
5480
  const spec = parseWorld(input.spec);
5121
5481
  if (spec.browser || spec.background) {
5122
5482
  send(res, 422, { error: "unsupported_runtime", message: "Browser recipes and background completion checks currently require the local runtime. No cloud job was queued." });
@@ -5124,6 +5484,10 @@ async function startGateway(opts) {
5124
5484
  }
5125
5485
  const git = validateGit(input.git);
5126
5486
  const project = input.project;
5487
+ if (grant && project !== grant.client.project) {
5488
+ send(res, 403, { error: "project_not_authorized" });
5489
+ return;
5490
+ }
5127
5491
  if (project !== void 0 && (typeof project !== "string" || !projectInventories.some((p) => p.name === project))) {
5128
5492
  send(res, 409, { error: "sync_project_before_run" });
5129
5493
  return;
@@ -5156,11 +5520,12 @@ async function startGateway(opts) {
5156
5520
  return;
5157
5521
  }
5158
5522
  store.jobs = store.jobs.filter((j) => Date.parse(j.createdAt) > Date.now() - 30 * 864e5);
5159
- const job = { id: randomUUID2(), accountId: account.id, status: "queued", createdAt: (/* @__PURE__ */ new Date()).toISOString(), name: spec.name, workflowId: spec.workflowId, project };
5523
+ const job = { id: randomUUID2(), accountId: account.id, status: "queued", createdAt: (/* @__PURE__ */ new Date()).toISOString(), name: spec.name, workflowId: spec.workflowId, project, ...idempotencyKey ? { idempotencyKey, requestDigest } : {} };
5160
5524
  account.runs++;
5161
5525
  store.jobs.push(job);
5162
5526
  await save();
5163
5527
  captureAnalytics({ event: "run_submitted", distinctId: account.id, properties: { jobId: job.id, project } });
5528
+ res.setHeader("Location", issuer + "/v1/runs/" + job.id);
5164
5529
  controllers.set(job.id, new AbortController());
5165
5530
  queue.push({ job, spec, files: input.files, git, project });
5166
5531
  schedule();
@@ -5182,6 +5547,10 @@ async function startGateway(opts) {
5182
5547
  return;
5183
5548
  }
5184
5549
  const input = await body(req);
5550
+ if (grant && !oauthGrant(store, token)) {
5551
+ send(res, 401, { error: "invalid_token" });
5552
+ return;
5553
+ }
5185
5554
  const project = selectedInventory?.name;
5186
5555
  if (!project || input.project !== project) {
5187
5556
  send(res, 409, { error: "sync_project_before_saving" });
@@ -5347,7 +5716,7 @@ async function startGateway(opts) {
5347
5716
  const controller = new AbortController();
5348
5717
  chatControllers.set(account.id, controller);
5349
5718
  const timeout = setTimeout(() => controller.abort(), 45e3);
5350
- const context = { project, discoveryDigest: inventory ? hash2(JSON.stringify(inventory)) : null, jobId: job?.id ?? null };
5719
+ const context = { project, discoveryDigest: inventory ? hash3(JSON.stringify(inventory)) : null, jobId: job?.id ?? null };
5351
5720
  thread.messages.push({ id: randomUUID2(), role: "user", content: input.content.trim(), requestId: input.requestId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), context });
5352
5721
  try {
5353
5722
  await save();
@@ -5384,7 +5753,7 @@ async function startGateway(opts) {
5384
5753
  }
5385
5754
  const match = path2.match(/^\/v1\/runs\/([a-f0-9-]{36})$/);
5386
5755
  if (match && req.method === "DELETE") {
5387
- const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id);
5756
+ const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id && (!grant || (j.project ?? j.receipt?.project) === grant.client.project));
5388
5757
  if (!job) {
5389
5758
  send(res, 404, { error: "not_found" });
5390
5759
  return;
@@ -5407,7 +5776,7 @@ async function startGateway(opts) {
5407
5776
  return;
5408
5777
  }
5409
5778
  if (match && req.method === "GET") {
5410
- const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id);
5779
+ const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id && (!grant || (j.project ?? j.receipt?.project) === grant.client.project));
5411
5780
  if (!job) {
5412
5781
  send(res, 404, { error: "not_found" });
5413
5782
  return;