agenttwin 0.3.13 → 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 +472 -93
  2. package/dist/cli.js +126 -19
  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 +465 -86
  75. package/package.json +14 -2
  76. package/dist/dashboard/assets/next/static/BbTZs0rKnsmj45peSnlwJ/_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-aaee6711085b485d.js +0 -1
  87. package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-09cb13657f62384f.js +0 -1
  88. package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-945c916b24f48675.js +0 -1
  89. package/dist/dashboard/assets/next/static/chunks/app/demo/page-3cba0aab460c8677.js +0 -1
  90. package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-9f7b8dca8b973b15.js +0 -1
  91. package/dist/dashboard/assets/next/static/chunks/app/enterprise/page-4986698442bddc6b.js +0 -1
  92. package/dist/dashboard/assets/next/static/chunks/app/layout-09868e53e4f0541f.js +0 -1
  93. package/dist/dashboard/assets/next/static/chunks/app/login/page-2e29d4b17645c34c.js +0 -1
  94. package/dist/dashboard/assets/next/static/chunks/app/onboarding/page-af2c4768dc34ec92.js +0 -1
  95. package/dist/dashboard/assets/next/static/chunks/app/page-507229409812cedd.js +0 -1
  96. package/dist/dashboard/assets/next/static/chunks/app/pricing/page-c27282cd7bfb0fcf.js +0 -1
  97. package/dist/dashboard/assets/next/static/chunks/app/signup/page-2e29d4b17645c34c.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-77a57cd622171076.js +0 -1
  100. package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-df34bcef0394d10d.js +0 -1
  101. package/dist/dashboard/assets/next/static/chunks/app/v2/layout-d1d715b5b2dae3cd.js +0 -1
  102. package/dist/dashboard/assets/next/static/chunks/app/v2/login/page-4f60d050bf954194.js +0 -1
  103. package/dist/dashboard/assets/next/static/chunks/app/v2/onboarding/page-abfe2f36213848d7.js +0 -1
  104. package/dist/dashboard/assets/next/static/chunks/app/v2/page-df6a34eff716d18e.js +0 -1
  105. package/dist/dashboard/assets/next/static/chunks/app/v2/signup/page-4f60d050bf954194.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-44457f45df749a89.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/{BbTZs0rKnsmj45peSnlwJ → 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 });
@@ -4290,7 +4542,9 @@ async function startGateway(opts) {
4290
4542
  const passkeyChallenges = {};
4291
4543
  const devices = /* @__PURE__ */ new Map();
4292
4544
  let hashing = 0;
4293
- const dummyPassword = randomBytes6(16).toString("hex") + ":" + randomBytes6(64).toString("hex");
4545
+ let localOnboardingUntil = 0;
4546
+ const dummyPassword = randomBytes7(16).toString("hex") + ":" + randomBytes7(64).toString("hex");
4547
+ const budget = requestBudget(opts.requestsPerMinute);
4294
4548
  const server = createServer2(async (req, res) => {
4295
4549
  try {
4296
4550
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
@@ -4317,6 +4571,11 @@ async function startGateway(opts) {
4317
4571
  }
4318
4572
  return;
4319
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");
4320
4579
  const origin = req.headers.origin;
4321
4580
  if (origin) {
4322
4581
  if (!allowedOrigins.has(origin)) {
@@ -4328,13 +4587,26 @@ async function startGateway(opts) {
4328
4587
  res.setHeader("Access-Control-Allow-Credentials", "true");
4329
4588
  if (req.method === "OPTIONS") {
4330
4589
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
4331
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
4590
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Idempotency-Key");
4332
4591
  res.setHeader("Access-Control-Max-Age", "600");
4333
4592
  res.writeHead(204);
4334
4593
  res.end();
4335
4594
  return;
4336
4595
  }
4337
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;
4338
4610
  if (path2 === "/healthz" && req.method === "GET") {
4339
4611
  send(res, 200, { status: stopping ? "stopping" : "ok" });
4340
4612
  return;
@@ -4343,7 +4615,26 @@ async function startGateway(opts) {
4343
4615
  send(res, 503, { error: "storage_unavailable" });
4344
4616
  return;
4345
4617
  }
4346
- 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
+ }
4347
4638
  const cookieToken = req.headers.cookie?.split(";").map((v) => v.trim()).find((v) => v.startsWith("at_session="))?.slice(11) ?? "";
4348
4639
  const session = store.sessions?.find((s) => s.hash === digest(cookieToken) && s.expires > Date.now());
4349
4640
  if (cookieToken && !token && !["GET", "HEAD", "OPTIONS"].includes(req.method ?? "") && (!origin || !allowedOrigins.has(origin))) {
@@ -4356,7 +4647,7 @@ async function startGateway(opts) {
4356
4647
  };
4357
4648
  const profile = (a) => ({ accountId: a.id, email: a.identity?.email, name: a.identity?.name ?? a.label, workspace: a.label, organization: a.organization, workspaceProfile: a.workspaceProfile, passwordEnabled: !!a.identity?.passwordHash, passwordSetupAllowed: !a.identity?.passwordHash && !!a.externalIdentities?.some((i) => i.provider === "google") });
4358
4649
  if (path2 === "/v1/dev/workspace" && req.method === "GET" && opts.localDevelopment && ["127.0.0.1", "localhost"].includes(opts.host ?? "") && verificationUrl === "http://localhost:3000/onboarding/") {
4359
- send(res, 200, { schema: "agenttwin/local-workspace-v1", dashboard: verificationUrl });
4650
+ send(res, 200, { schema: "agenttwin/local-workspace-v1", dashboard: verificationUrl, onboarding: localOnboardingUntil > Date.now() });
4360
4651
  return;
4361
4652
  }
4362
4653
  if (path2 === "/v1/auth/providers" && req.method === "GET") {
@@ -4486,12 +4777,12 @@ async function startGateway(opts) {
4486
4777
  return;
4487
4778
  }
4488
4779
  if (store.accounts.length >= 1e4) throw new Error("account_capacity");
4489
- 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 } : {} };
4490
4781
  store.accounts.push(owner);
4491
4782
  captureAnalytics({ event: "account_created", distinctId: owner.id, properties: { method: identity.provider, realm: identity.organization || identity.issuer } });
4492
4783
  }
4493
4784
  if (!owner.active) throw new Error("account_inactive");
4494
- const secret = randomBytes6(32).toString("hex");
4785
+ const secret = randomBytes7(32).toString("hex");
4495
4786
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4496
4787
  const owned = store.sessions.filter((s) => s.accountId === owner.id);
4497
4788
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
@@ -4512,7 +4803,7 @@ async function startGateway(opts) {
4512
4803
  send(res, 429, { error: "Try again shortly." });
4513
4804
  return;
4514
4805
  }
4515
- 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();
4516
4807
  devices.set(digest(secret), { code, expires: Date.now() + 6e5, lastPoll: 0 });
4517
4808
  send(res, 200, { deviceCode: secret, userCode: code, verificationUrl, expiresIn: 600 });
4518
4809
  return;
@@ -4533,7 +4824,7 @@ async function startGateway(opts) {
4533
4824
  }
4534
4825
  const rpID = requestUrl.hostname === "localhost" || requestUrl.hostname === "127.0.0.1" ? "localhost" : new URL(origin).hostname;
4535
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 })) });
4536
- const challengeId = randomBytes6(16).toString("hex");
4827
+ const challengeId = randomBytes7(16).toString("hex");
4537
4828
  challengeStore(passkeyChallenges, challengeId, options.challenge, 12e4);
4538
4829
  send(res, 200, { options, challengeId });
4539
4830
  return;
@@ -4580,7 +4871,7 @@ async function startGateway(opts) {
4580
4871
  const credentialId = typeof input.credentialId === "string" ? input.credentialId.slice(0, 1024) : "";
4581
4872
  const owner = credentialId ? store.accounts.find((a) => a.active && (a.passkeys ?? []).some((k) => k.id === credentialId)) : void 0;
4582
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" });
4583
- const challengeId = randomBytes6(16).toString("hex");
4874
+ const challengeId = randomBytes7(16).toString("hex");
4584
4875
  challengeStore(passkeyChallenges, challengeId, options.challenge, 12e4);
4585
4876
  send(res, 200, { options, challengeId });
4586
4877
  return;
@@ -4611,7 +4902,7 @@ async function startGateway(opts) {
4611
4902
  return;
4612
4903
  }
4613
4904
  key.counter = verification.authenticationInfo.newCounter;
4614
- const secret = randomBytes6(32).toString("hex");
4905
+ const secret = randomBytes7(32).toString("hex");
4615
4906
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4616
4907
  const owned = store.sessions.filter((s) => s.accountId === owner.id);
4617
4908
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
@@ -4672,7 +4963,8 @@ async function startGateway(opts) {
4672
4963
  send(res, 400, { error: "Use a valid email and a password with 12\u2013128 characters." });
4673
4964
  return;
4674
4965
  }
4675
- 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))) {
4676
4968
  send(res, 403, { error: "Verify your email with a one-time code before continuing.", code: "email_verification_required" });
4677
4969
  return;
4678
4970
  }
@@ -4707,21 +4999,21 @@ async function startGateway(opts) {
4707
4999
  send(res, 429, { error: "account_capacity" });
4708
5000
  return;
4709
5001
  }
4710
- 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 } };
4711
5003
  store.accounts.push(a);
4712
5004
  captureAnalytics({ event: "account_created", distinctId: a.id, properties: { method: "email", realm: "password" } });
4713
5005
  } else {
4714
- const matches2 = await passwordMatches(parsed.password, a?.identity?.passwordHash ?? dummyPassword);
4715
- 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) {
4716
5008
  send(res, 401, { error: "Email or password is incorrect." });
4717
5009
  return;
4718
5010
  }
4719
5011
  }
4720
- const secret = randomBytes6(32).toString("hex");
5012
+ const secret = randomBytes7(32).toString("hex");
4721
5013
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4722
5014
  const owned = store.sessions.filter((s) => s.accountId === a.id);
4723
5015
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
4724
- if (emailVerification.enabled) {
5016
+ if (emailVerification.enabled && !demoLogin) {
4725
5017
  if (store.emailChallenges?.[emailChallengeId] !== emailGrant || !emailVerification.bound(emailGrant, emailBinding ?? "", origin)) {
4726
5018
  send(res, 403, { error: "Email verification expired. Request a new code." });
4727
5019
  return;
@@ -4827,8 +5119,8 @@ async function startGateway(opts) {
4827
5119
  send(res, 400, { error: "label_required" });
4828
5120
  return;
4829
5121
  }
4830
- const accessToken = `at_user_${randomBytes6(32).toString("hex")}`;
4831
- 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 };
4832
5124
  if (store.accounts.length >= 1e4) {
4833
5125
  send(res, 429, { error: "account_capacity" });
4834
5126
  return;
@@ -4854,11 +5146,21 @@ async function startGateway(opts) {
4854
5146
  send(res, 200, { revoked: true });
4855
5147
  return;
4856
5148
  }
4857
- 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));
4858
5150
  if (!account) {
5151
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
4859
5152
  send(res, 401, { error: "unauthorized" });
4860
5153
  return;
4861
5154
  }
5155
+ if (path2 === "/v1/dev/workspace" && req.method === "POST" && opts.localDevelopment && ["127.0.0.1", "localhost"].includes(opts.host ?? "") && verificationUrl === "http://localhost:3000/onboarding/") {
5156
+ if (token || !session || origin !== "http://localhost:3000") {
5157
+ send(res, 403, { error: "browser_session_required" });
5158
+ return;
5159
+ }
5160
+ localOnboardingUntil = Date.now() + 10 * 6e4;
5161
+ send(res, 200, { waiting: true });
5162
+ return;
5163
+ }
4862
5164
  if (path2 === "/v1/auth/password" && req.method === "POST") {
4863
5165
  if (token || !session || !account.identity || !origin || !allowedOrigins.has(origin)) {
4864
5166
  send(res, 403, { error: "browser_session_required" });
@@ -4897,7 +5199,7 @@ async function startGateway(opts) {
4897
5199
  return;
4898
5200
  }
4899
5201
  const previousIdentity = account.identity, previousSessions = store.sessions;
4900
- const secret = randomBytes6(32).toString("hex");
5202
+ const secret = randomBytes7(32).toString("hex");
4901
5203
  account.identity = { ...account.identity, passwordHash: encoded };
4902
5204
  store.sessions = [...store.sessions.filter((s) => s.accountId !== account.id), { accountId: account.id, hash: digest(secret), expires: Date.now() + 7 * 864e5 }];
4903
5205
  try {
@@ -4919,8 +5221,8 @@ async function startGateway(opts) {
4919
5221
  send(res, 400, { error: "terminal_token_required" });
4920
5222
  return;
4921
5223
  }
4922
- account.cliTokens = (account.cliTokens ?? []).filter((t) => t !== hash2(token));
4923
- 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"));
4924
5226
  await save();
4925
5227
  send(res, 200, { signedOut: true });
4926
5228
  return;
@@ -4933,8 +5235,8 @@ async function startGateway(opts) {
4933
5235
  send(res, 400, { error: "Code not found or expired. Run agenttwin login again." });
4934
5236
  return;
4935
5237
  }
4936
- const accessToken = `at_user_${randomBytes6(32).toString("hex")}`;
4937
- 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)];
4938
5240
  device.accountId = account.id;
4939
5241
  await save();
4940
5242
  device.token = accessToken;
@@ -4981,6 +5283,47 @@ async function startGateway(opts) {
4981
5283
  const projectInventories = [...store.projectDiscoveries?.[account.id] ?? []];
4982
5284
  const latestInventory = store.discoveries?.[account.id];
4983
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
+ }
4984
5327
  const requestedProject = requestUrl.searchParams.get("project");
4985
5328
  const selectedInventory = requestedProject === null ? latestInventory : projectInventories.find((p) => p.name === requestedProject);
4986
5329
  if (path2.startsWith("/v1/projects/") && req.method === "DELETE") {
@@ -5023,7 +5366,7 @@ async function startGateway(opts) {
5023
5366
  return;
5024
5367
  }
5025
5368
  if (path2 === "/v1/projects" && req.method === "GET") {
5026
- 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 });
5027
5370
  return;
5028
5371
  }
5029
5372
  if (path2 === "/v1/discovery" && req.method === "GET") {
@@ -5081,7 +5424,20 @@ async function startGateway(opts) {
5081
5424
  return;
5082
5425
  }
5083
5426
  if (path2 === "/v1/runs" && req.method === "GET") {
5084
- 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 });
5085
5441
  return;
5086
5442
  }
5087
5443
  if (path2 === "/v1/runs" && req.method === "POST") {
@@ -5089,24 +5445,38 @@ async function startGateway(opts) {
5089
5445
  send(res, 503, { error: "stopping" });
5090
5446
  return;
5091
5447
  }
5092
- if (!process.env.E2B_API_KEY || !process.env.E2B_TEMPLATE_ID) {
5093
- 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" });
5094
5451
  return;
5095
5452
  }
5096
- if (busy(account.id)) {
5097
- 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" });
5098
5455
  return;
5099
5456
  }
5100
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5101
- if (account.usageDay !== today) {
5102
- account.usageDay = today;
5103
- 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;
5104
5461
  }
5105
- if (account.runs >= dailyLimit) {
5106
- 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." });
5107
5478
  return;
5108
5479
  }
5109
- const input = await body(req);
5110
5480
  const spec = parseWorld(input.spec);
5111
5481
  if (spec.browser || spec.background) {
5112
5482
  send(res, 422, { error: "unsupported_runtime", message: "Browser recipes and background completion checks currently require the local runtime. No cloud job was queued." });
@@ -5114,6 +5484,10 @@ async function startGateway(opts) {
5114
5484
  }
5115
5485
  const git = validateGit(input.git);
5116
5486
  const project = input.project;
5487
+ if (grant && project !== grant.client.project) {
5488
+ send(res, 403, { error: "project_not_authorized" });
5489
+ return;
5490
+ }
5117
5491
  if (project !== void 0 && (typeof project !== "string" || !projectInventories.some((p) => p.name === project))) {
5118
5492
  send(res, 409, { error: "sync_project_before_run" });
5119
5493
  return;
@@ -5146,11 +5520,12 @@ async function startGateway(opts) {
5146
5520
  return;
5147
5521
  }
5148
5522
  store.jobs = store.jobs.filter((j) => Date.parse(j.createdAt) > Date.now() - 30 * 864e5);
5149
- 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 } : {} };
5150
5524
  account.runs++;
5151
5525
  store.jobs.push(job);
5152
5526
  await save();
5153
5527
  captureAnalytics({ event: "run_submitted", distinctId: account.id, properties: { jobId: job.id, project } });
5528
+ res.setHeader("Location", issuer + "/v1/runs/" + job.id);
5154
5529
  controllers.set(job.id, new AbortController());
5155
5530
  queue.push({ job, spec, files: input.files, git, project });
5156
5531
  schedule();
@@ -5172,6 +5547,10 @@ async function startGateway(opts) {
5172
5547
  return;
5173
5548
  }
5174
5549
  const input = await body(req);
5550
+ if (grant && !oauthGrant(store, token)) {
5551
+ send(res, 401, { error: "invalid_token" });
5552
+ return;
5553
+ }
5175
5554
  const project = selectedInventory?.name;
5176
5555
  if (!project || input.project !== project) {
5177
5556
  send(res, 409, { error: "sync_project_before_saving" });
@@ -5337,7 +5716,7 @@ async function startGateway(opts) {
5337
5716
  const controller = new AbortController();
5338
5717
  chatControllers.set(account.id, controller);
5339
5718
  const timeout = setTimeout(() => controller.abort(), 45e3);
5340
- 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 };
5341
5720
  thread.messages.push({ id: randomUUID2(), role: "user", content: input.content.trim(), requestId: input.requestId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), context });
5342
5721
  try {
5343
5722
  await save();
@@ -5374,7 +5753,7 @@ async function startGateway(opts) {
5374
5753
  }
5375
5754
  const match = path2.match(/^\/v1\/runs\/([a-f0-9-]{36})$/);
5376
5755
  if (match && req.method === "DELETE") {
5377
- 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));
5378
5757
  if (!job) {
5379
5758
  send(res, 404, { error: "not_found" });
5380
5759
  return;
@@ -5397,7 +5776,7 @@ async function startGateway(opts) {
5397
5776
  return;
5398
5777
  }
5399
5778
  if (match && req.method === "GET") {
5400
- 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));
5401
5780
  if (!job) {
5402
5781
  send(res, 404, { error: "not_found" });
5403
5782
  return;