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/gateway.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 });
@@ -1550,9 +1550,9 @@ function normalizedEmail(value) {
1550
1550
  return email;
1551
1551
  }
1552
1552
  function createEmailVerification(options, secret) {
1553
- const hash3 = (value) => createHmac("sha256", secret).update(value).digest("hex");
1554
- const matches2 = (value, expected) => {
1555
- const a = Buffer.from(hash3(value), "hex"), b = Buffer.from(expected, "hex");
1553
+ const hash4 = (value) => createHmac("sha256", secret).update(value).digest("hex");
1554
+ const matches3 = (value, expected) => {
1555
+ const a = Buffer.from(hash4(value), "hex"), b = Buffer.from(expected, "hex");
1556
1556
  return a.length === b.length && timingSafeEqual2(a, b);
1557
1557
  };
1558
1558
  return {
@@ -1560,16 +1560,16 @@ function createEmailVerification(options, secret) {
1560
1560
  challenge(email, origin) {
1561
1561
  const id = randomBytes2(24).toString("hex"), binding = randomBytes2(32).toString("hex");
1562
1562
  const code = String(randomInt(0, 1e8)).padStart(8, "0");
1563
- const challenge = { email, origin, binding: hash3(binding), codeHash: hash3(id + ":" + code), expires: Date.now() + 10 * 6e4, attempts: 0, sentAt: Date.now() };
1563
+ const challenge = { email, origin, binding: hash4(binding), codeHash: hash4(id + ":" + code), expires: Date.now() + 10 * 6e4, attempts: 0, sentAt: Date.now() };
1564
1564
  return { id, binding, code, challenge };
1565
1565
  },
1566
1566
  bound(challenge, binding, origin) {
1567
- return !!challenge && challenge.expires > Date.now() && challenge.origin === origin && matches2(binding, challenge.binding);
1567
+ return !!challenge && challenge.expires > Date.now() && challenge.origin === origin && matches3(binding, challenge.binding);
1568
1568
  },
1569
1569
  verify(id, challenge, code) {
1570
1570
  if (challenge.verified || challenge.attempts >= 5 || challenge.expires <= Date.now()) return false;
1571
1571
  challenge.attempts++;
1572
- if (typeof code !== "string" || !/^\d{8}$/.test(code) || !matches2(id + ":" + code, challenge.codeHash)) return false;
1572
+ if (typeof code !== "string" || !/^\d{8}$/.test(code) || !matches3(id + ":" + code, challenge.codeHash)) return false;
1573
1573
  challenge.verified = true;
1574
1574
  return true;
1575
1575
  },
@@ -1588,6 +1588,249 @@ It expires in 10 minutes. Do not share this code. If you did not request it, ign
1588
1588
  };
1589
1589
  }
1590
1590
 
1591
+ // src/runtime/request-budget.ts
1592
+ function requestBudget(limit = 1200) {
1593
+ if (!Number.isSafeInteger(limit) || limit < 1)
1594
+ throw new Error("Invalid API request limit");
1595
+ let until = 0, count = 0;
1596
+ return (res) => {
1597
+ const now = Date.now();
1598
+ if (now >= until) {
1599
+ until = now + 6e4;
1600
+ count = 0;
1601
+ }
1602
+ count++;
1603
+ const reset = Math.max(1, Math.ceil((until - now) / 1e3));
1604
+ res.setHeader("RateLimit-Policy", `"gateway";q=${limit};w=60`);
1605
+ res.setHeader(
1606
+ "RateLimit",
1607
+ `"gateway";r=${Math.max(0, limit - count)};t=${reset}`
1608
+ );
1609
+ if (count > limit) {
1610
+ res.setHeader("Retry-After", String(reset));
1611
+ return false;
1612
+ }
1613
+ return true;
1614
+ };
1615
+ }
1616
+
1617
+ // src/runtime/oauth.ts
1618
+ import { createHash as createHash2, randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual3 } from "crypto";
1619
+ var oauthScopes = {
1620
+ "read:projects": "List this client\u2019s one authorized project",
1621
+ "read:discovery": "Read discovery metadata for the authorized project",
1622
+ "read:runs": "Read test run status and recorded evidence for the authorized project",
1623
+ "read:workflows": "Read saved test configurations for the authorized project",
1624
+ "write:runs": "Submit and cancel reviewed cloud tests for the authorized project",
1625
+ "write:workflows": "Save test configuration drafts for the authorized project"
1626
+ };
1627
+ var hash2 = (v) => createHash2("sha256").update(v).digest("hex");
1628
+ var matches = (a, b) => timingSafeEqual3(Buffer.from(hash2(a)), Buffer.from(b));
1629
+ function newOAuthClient(accountId, label, project, scopes) {
1630
+ if (!Array.isArray(scopes) || !scopes.length || scopes.length > Object.keys(oauthScopes).length || scopes.some(
1631
+ (s) => typeof s !== "string" || !Object.hasOwn(oauthScopes, s)
1632
+ ) || new Set(scopes).size !== scopes.length)
1633
+ throw new Error("Choose explicit supported OAuth scopes");
1634
+ const secret = randomBytes3(32).toString("base64url");
1635
+ const client = {
1636
+ id: "at_client_" + randomBytes3(16).toString("hex"),
1637
+ secretHash: hash2(secret),
1638
+ accountId,
1639
+ label,
1640
+ project,
1641
+ scopes,
1642
+ active: true,
1643
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1644
+ };
1645
+ return { client, secret };
1646
+ }
1647
+ function oauthMetadata(issuer) {
1648
+ return {
1649
+ issuer,
1650
+ token_endpoint: issuer + "/oauth/token",
1651
+ revocation_endpoint: issuer + "/oauth/revoke",
1652
+ grant_types_supported: ["client_credentials"],
1653
+ response_types_supported: [],
1654
+ token_endpoint_auth_methods_supported: ["client_secret_basic"],
1655
+ revocation_endpoint_auth_methods_supported: ["client_secret_basic"],
1656
+ scopes_supported: Object.keys(oauthScopes),
1657
+ service_documentation: "https://agenttwin.si/auth.md"
1658
+ };
1659
+ }
1660
+ function resourceMetadata(issuer) {
1661
+ return {
1662
+ resource: issuer,
1663
+ authorization_servers: [issuer],
1664
+ scopes_supported: Object.keys(oauthScopes),
1665
+ bearer_methods_supported: ["header"],
1666
+ resource_documentation: "https://agenttwin.si/auth.md"
1667
+ };
1668
+ }
1669
+ function oauthGrant(store, raw) {
1670
+ if (!raw.startsWith("at_oauth_")) return void 0;
1671
+ const token = store.oauthTokens?.find(
1672
+ (t) => t.hash === hash2(raw) && t.expires > Date.now()
1673
+ );
1674
+ const client = token && store.oauthClients?.find((c) => c.id === token.clientId && c.active);
1675
+ if (!token || !client || !store.accounts.some((a) => a.id === client.accountId && a.active))
1676
+ return void 0;
1677
+ return { token, client };
1678
+ }
1679
+ function requiredOAuthScope(method, path2) {
1680
+ if (method === "GET") {
1681
+ if (path2 === "/v1/projects") return "read:projects";
1682
+ if (path2 === "/v1/discovery") return "read:discovery";
1683
+ if (path2 === "/v1/workflows") return "read:workflows";
1684
+ if (path2 === "/v1/runs" || /^\/v1\/runs\/[a-f0-9-]{36}$/.test(path2))
1685
+ return "read:runs";
1686
+ }
1687
+ if (method === "POST" && path2 === "/v1/workflows") return "write:workflows";
1688
+ if (method === "POST" && path2 === "/v1/runs" || method === "DELETE" && /^\/v1\/runs\/[a-f0-9-]{36}$/.test(path2))
1689
+ return "write:runs";
1690
+ return void 0;
1691
+ }
1692
+ function createOAuth(store, save, issuer) {
1693
+ const attempts = /* @__PURE__ */ new Map();
1694
+ const reply = (res, status, data) => {
1695
+ res.writeHead(status, {
1696
+ "content-type": "application/json",
1697
+ "cache-control": "no-store",
1698
+ pragma: "no-cache"
1699
+ });
1700
+ res.end(JSON.stringify(data));
1701
+ };
1702
+ return async (req, res, path2) => {
1703
+ if (!["/oauth/token", "/oauth/revoke"].includes(path2)) return false;
1704
+ if (req.method === "OPTIONS") {
1705
+ res.writeHead(204);
1706
+ res.end();
1707
+ return true;
1708
+ }
1709
+ if (req.method !== "POST") {
1710
+ res.setHeader("Allow", "POST, OPTIONS");
1711
+ reply(res, 405, { error: "invalid_request" });
1712
+ return true;
1713
+ }
1714
+ const now = Date.now(), peer = req.socket.remoteAddress ?? "unknown";
1715
+ for (const [key, value] of attempts)
1716
+ if (value.until <= now) attempts.delete(key);
1717
+ const bucket = attempts.get(peer) ?? { count: 0, until: now + 6e4 };
1718
+ bucket.count++;
1719
+ attempts.set(peer, bucket);
1720
+ res.setHeader("RateLimit-Policy", '"oauth";q=120;w=60');
1721
+ res.setHeader(
1722
+ "RateLimit",
1723
+ `"oauth";r=${Math.max(0, 120 - bucket.count)};t=${Math.max(1, Math.ceil((bucket.until - now) / 1e3))}`
1724
+ );
1725
+ if (bucket.count > 120 || attempts.size > 1e4) {
1726
+ res.setHeader("Retry-After", "60");
1727
+ reply(res, 429, { error: "temporarily_unavailable" });
1728
+ return true;
1729
+ }
1730
+ const basic = req.headers.authorization?.match(
1731
+ /^Basic ([A-Za-z0-9+/]+=*)$/i
1732
+ );
1733
+ let client;
1734
+ try {
1735
+ const decoded = basic ? Buffer.from(basic[1], "base64").toString("utf8") : "";
1736
+ const colon = decoded.indexOf(":");
1737
+ const id = decodeURIComponent(
1738
+ decoded.slice(0, colon).replace(/\+/g, " ")
1739
+ ), secret = decodeURIComponent(
1740
+ decoded.slice(colon + 1).replace(/\+/g, " ")
1741
+ );
1742
+ client = store.oauthClients?.find((c) => c.id === id && c.active);
1743
+ if (colon < 1 || !client || !matches(secret, client.secretHash) || !store.accounts.some((a) => a.id === client.accountId && a.active))
1744
+ client = void 0;
1745
+ } catch {
1746
+ client = void 0;
1747
+ }
1748
+ if (!client) {
1749
+ res.setHeader("WWW-Authenticate", 'Basic realm="AgentTwin OAuth"');
1750
+ reply(res, 401, { error: "invalid_client" });
1751
+ return true;
1752
+ }
1753
+ if (!/^application\/x-www-form-urlencoded(?:;|$)/i.test(
1754
+ req.headers["content-type"] ?? ""
1755
+ )) {
1756
+ reply(res, 400, {
1757
+ error: "invalid_request",
1758
+ error_description: "Use application/x-www-form-urlencoded"
1759
+ });
1760
+ return true;
1761
+ }
1762
+ let raw = "";
1763
+ for await (const chunk of req) {
1764
+ raw += chunk.toString("utf8");
1765
+ if (Buffer.byteLength(raw) > 8192) {
1766
+ reply(res, 400, { error: "invalid_request" });
1767
+ return true;
1768
+ }
1769
+ }
1770
+ const form = new URLSearchParams(raw);
1771
+ if ([...new Set(form.keys())].some((k) => form.getAll(k).length !== 1) || form.has("client_id") || form.has("client_secret")) {
1772
+ reply(res, 400, { error: "invalid_request" });
1773
+ return true;
1774
+ }
1775
+ if (!client.active || !store.accounts.some((a) => a.id === client.accountId && a.active)) {
1776
+ reply(res, 401, { error: "invalid_client" });
1777
+ return true;
1778
+ }
1779
+ if (path2 === "/oauth/revoke") {
1780
+ if (!form.get("token")) {
1781
+ reply(res, 400, { error: "invalid_request" });
1782
+ return true;
1783
+ }
1784
+ if (form.has("token_type_hint") && form.get("token_type_hint") !== "access_token") {
1785
+ reply(res, 400, { error: "unsupported_token_type" });
1786
+ return true;
1787
+ }
1788
+ store.oauthTokens = (store.oauthTokens ?? []).filter(
1789
+ (t) => t.clientId !== client.id || t.hash !== hash2(form.get("token"))
1790
+ );
1791
+ await save();
1792
+ reply(res, 200, {});
1793
+ return true;
1794
+ }
1795
+ if (form.get("grant_type") !== "client_credentials") {
1796
+ reply(res, 400, { error: "unsupported_grant_type" });
1797
+ return true;
1798
+ }
1799
+ if (form.has("resource") && form.get("resource") !== issuer) {
1800
+ reply(res, 400, { error: "invalid_target" });
1801
+ return true;
1802
+ }
1803
+ const scopes = form.has("scope") ? form.get("scope").split(" ") : client.scopes;
1804
+ if (!scopes.length || scopes.some((s) => !s || !client.scopes.includes(s)) || new Set(scopes).size !== scopes.length) {
1805
+ reply(res, 400, { error: "invalid_scope" });
1806
+ return true;
1807
+ }
1808
+ store.oauthTokens = (store.oauthTokens ?? []).filter(
1809
+ (t) => t.expires > now
1810
+ );
1811
+ if (store.oauthTokens.length >= 1e5 || store.oauthTokens.filter((t) => t.clientId === client.id).length >= 100) {
1812
+ res.setHeader("Retry-After", "60");
1813
+ reply(res, 429, { error: "temporarily_unavailable" });
1814
+ return true;
1815
+ }
1816
+ const access_token = "at_oauth_" + randomBytes3(32).toString("base64url");
1817
+ store.oauthTokens.push({
1818
+ hash: hash2(access_token),
1819
+ clientId: client.id,
1820
+ scopes,
1821
+ expires: now + 36e5
1822
+ });
1823
+ await save();
1824
+ reply(res, 200, {
1825
+ access_token,
1826
+ token_type: "Bearer",
1827
+ expires_in: 3600,
1828
+ scope: scopes.join(" ")
1829
+ });
1830
+ return true;
1831
+ };
1832
+ }
1833
+
1591
1834
  // src/runtime/workspace-profile.ts
1592
1835
  function validateWorkspaceProfile(input) {
1593
1836
  const allowed = ["name", "workspace", "company", "website", "role", "teamSize", "useCase", "complete"];
@@ -1615,18 +1858,18 @@ function validateWorkspaceProfile(input) {
1615
1858
  }
1616
1859
 
1617
1860
  // src/runtime/auth.ts
1618
- import { randomBytes as randomBytes3, scrypt, timingSafeEqual as timingSafeEqual3, createHash as createHash2 } from "crypto";
1619
- var digest = (value) => createHash2("sha256").update(value).digest("hex");
1861
+ import { randomBytes as randomBytes4, scrypt, timingSafeEqual as timingSafeEqual4, createHash as createHash3 } from "crypto";
1862
+ var digest = (value) => createHash3("sha256").update(value).digest("hex");
1620
1863
  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)));
1621
1864
  async function passwordHash(password) {
1622
- const salt = randomBytes3(16).toString("hex");
1865
+ const salt = randomBytes4(16).toString("hex");
1623
1866
  return `${salt}:${(await derive(password, salt)).toString("hex")}`;
1624
1867
  }
1625
1868
  async function passwordMatches(password, stored) {
1626
1869
  const [salt, key] = stored.split(":");
1627
1870
  const actual = await derive(password, salt);
1628
1871
  const expected = Buffer.from(key, "hex");
1629
- return expected.length === actual.length && timingSafeEqual3(expected, actual);
1872
+ return expected.length === actual.length && timingSafeEqual4(expected, actual);
1630
1873
  }
1631
1874
  function credentials(input) {
1632
1875
  if (!input || typeof input !== "object") throw new Error("Enter your email and password.");
@@ -1645,7 +1888,7 @@ var challengeStore = (map, key, value, ttlMs) => {
1645
1888
  init_dashboard_transfer();
1646
1889
  import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from "@simplewebauthn/server";
1647
1890
  import { createServer as createServer2 } from "http";
1648
- import { createHash as createHash9, randomBytes as randomBytes6, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual4 } from "crypto";
1891
+ import { createHash as createHash10, randomBytes as randomBytes7, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual5 } from "crypto";
1649
1892
  import { mkdir as mkdir7, readFile as readFile12, writeFile as writeFile9, rename, mkdtemp as mkdtemp3, rm as rm4 } from "fs/promises";
1650
1893
  import { resolve as resolve4, join as join13, dirname as dirname2, extname } from "path";
1651
1894
  import { tmpdir as tmpdir3 } from "os";
@@ -1952,7 +2195,7 @@ import { mkdtemp as mkdtemp2, mkdir as mkdir6, readFile as readFile11, writeFile
1952
2195
  import { tmpdir as tmpdir2 } from "os";
1953
2196
  import { basename as basename2, dirname, isAbsolute, join as join12, relative as relative3, resolve as resolve3, sep as sep2 } from "path";
1954
2197
  import { fileURLToPath as fileURLToPath2 } from "url";
1955
- import { createHash as createHash7, randomUUID } from "crypto";
2198
+ import { createHash as createHash8, randomUUID } from "crypto";
1956
2199
 
1957
2200
  // src/runtime/credentials.ts
1958
2201
  import { homedir } from "os";
@@ -2204,10 +2447,10 @@ function stripeOperation(method, path2, params, data, sequence) {
2204
2447
  }
2205
2448
 
2206
2449
  // src/runtime/rest-adapters.ts
2207
- import { createHash as createHash3 } from "crypto";
2450
+ import { createHash as createHash4 } from "crypto";
2208
2451
  var EPOCH = 17e8;
2209
2452
  var iso = (epoch = EPOCH) => new Date(epoch * 1e3).toISOString();
2210
- var sha = (value) => createHash3("sha256").update(value).digest("hex");
2453
+ var sha = (value) => createHash4("sha256").update(value).digest("hex");
2211
2454
  var detUuid = (seed) => {
2212
2455
  const hex = sha(seed).slice(0, 32).split("");
2213
2456
  hex[12] = "4";
@@ -3090,21 +3333,21 @@ function restUnsupportedBody(adapter) {
3090
3333
  var REST_ADAPTER_NAMES = Object.keys(REST_ADAPTERS);
3091
3334
 
3092
3335
  // src/runtime/seed.ts
3093
- import { createHash as createHash4, randomBytes as randomBytes4 } from "crypto";
3336
+ import { createHash as createHash5, randomBytes as randomBytes5 } from "crypto";
3094
3337
  var SEED_PATTERN = /^[a-f0-9]{32}$/;
3095
3338
  var normalizeHex = (value) => value.trim().toLowerCase();
3096
3339
  function normalizeSeed(seed) {
3097
- if (seed === void 0 || seed.trim() === "") return randomBytes4(16).toString("hex");
3340
+ if (seed === void 0 || seed.trim() === "") return randomBytes5(16).toString("hex");
3098
3341
  const text = normalizeHex(seed);
3099
3342
  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)}`);
3100
3343
  if (text.length === 32) return text;
3101
- return createHash4("sha256").update(text).digest("hex").slice(0, 32);
3344
+ return createHash5("sha256").update(text).digest("hex").slice(0, 32);
3102
3345
  }
3103
3346
  function deriveSeed(base, index) {
3104
3347
  if (!SEED_PATTERN.test(base)) throw new Error(`Invalid base seed: ${base}`);
3105
3348
  if (!Number.isInteger(index) || index < 0) throw new Error("Seed index must be a nonnegative integer");
3106
3349
  if (index === 0) return base;
3107
- return createHash4("sha256").update(`agenttwin-seed|${base}|${index}`).digest("hex").slice(0, 32);
3350
+ return createHash5("sha256").update(`agenttwin-seed|${base}|${index}`).digest("hex").slice(0, 32);
3108
3351
  }
3109
3352
  var SeedStream = class {
3110
3353
  constructor(seed, purpose) {
@@ -3117,7 +3360,7 @@ var SeedStream = class {
3117
3360
  purpose;
3118
3361
  counter = 0;
3119
3362
  draw() {
3120
- const value = createHash4("sha256").update(`agenttwin-stream|${this.seed}|${this.purpose}|${this.counter}`).digest("hex");
3363
+ const value = createHash5("sha256").update(`agenttwin-stream|${this.seed}|${this.purpose}|${this.counter}`).digest("hex");
3121
3364
  this.counter++;
3122
3365
  return value;
3123
3366
  }
@@ -3510,12 +3753,12 @@ var HttpWorld = class {
3510
3753
 
3511
3754
  // src/runtime/evaluation.ts
3512
3755
  import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
3513
- var matches = (row, where) => !where || Object.entries(where).every(([k, v]) => isDeepStrictEqual3(row[k], v));
3756
+ var matches2 = (row, where) => !where || Object.entries(where).every(([k, v]) => isDeepStrictEqual3(row[k], v));
3514
3757
  function evaluateState(spec, initial, final, events) {
3515
3758
  const checks = spec.assertions.map((a, index) => {
3516
3759
  const before = initial[a.service]?.[a.path];
3517
3760
  const after = final[a.service]?.[a.path];
3518
- const actual = after?.filter((row) => matches(row, a.where)).length;
3761
+ const actual = after?.filter((row) => matches2(row, a.where)).length;
3519
3762
  return {
3520
3763
  id: `assertion_${index + 1}`,
3521
3764
  kind: "required-outcome",
@@ -3527,13 +3770,13 @@ function evaluateState(spec, initial, final, events) {
3527
3770
  evidence: {
3528
3771
  initial: `/initialState/${escape(a.service)}/${escape(a.path)}`,
3529
3772
  final: `/finalState/${escape(a.service)}/${escape(a.path)}`,
3530
- beforeCount: before?.filter((row) => matches(row, a.where)).length ?? null,
3773
+ beforeCount: before?.filter((row) => matches2(row, a.where)).length ?? null,
3531
3774
  eventSequences: events.filter((e) => e.service === a.service && e.path === a.path).map((e) => e.sequence)
3532
3775
  }
3533
3776
  };
3534
3777
  });
3535
3778
  const forbidden = (spec.forbiddenEffects ?? []).map((policy) => {
3536
- 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)));
3779
+ 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)));
3537
3780
  const unknown = events.some((e) => e.service === policy.service && e.path === policy.path && e.operation === policy.operation && policy.where && !e.record);
3538
3781
  return {
3539
3782
  id: policy.id,
@@ -3549,7 +3792,7 @@ function evaluateState(spec, initial, final, events) {
3549
3792
  const preserved = (spec.preservedState ?? []).map((policy) => {
3550
3793
  const before = initial[policy.service]?.[policy.path];
3551
3794
  const after = final[policy.service]?.[policy.path];
3552
- const passed = !!before && !!after && isDeepStrictEqual3(before.filter((row) => matches(row, policy.where)), after.filter((row) => matches(row, policy.where)));
3795
+ const passed = !!before && !!after && isDeepStrictEqual3(before.filter((row) => matches2(row, policy.where)), after.filter((row) => matches2(row, policy.where)));
3553
3796
  return {
3554
3797
  id: policy.id,
3555
3798
  kind: "preserved-state",
@@ -3572,7 +3815,7 @@ function evaluateProperty(policy, final, events) {
3572
3815
  const base = { id: policy.id, kind: "property", propertyKind: policy.kind, service: policy.service, path: policy.path, passed: false };
3573
3816
  const rows2 = final[policy.service]?.[policy.path];
3574
3817
  if (!rows2) return { ...base, expected: "intact", actual: "missing collection", evidence: { evidenceGap: `final state has no collection "${policy.path}" in "${policy.service}"` } };
3575
- 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)));
3818
+ 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)));
3576
3819
  if (policy.kind === "unique-per-act") {
3577
3820
  const field = String(policy.params?.field);
3578
3821
  const keyed = /* @__PURE__ */ new Map();
@@ -3717,7 +3960,7 @@ init_git();
3717
3960
  // src/runtime/e2b.ts
3718
3961
  import { Sandbox } from "e2b";
3719
3962
  import { readFile as readFile4 } from "fs/promises";
3720
- import { randomBytes as randomBytes5 } from "crypto";
3963
+ import { randomBytes as randomBytes6 } from "crypto";
3721
3964
  import { homedir as homedir2 } from "os";
3722
3965
  import { join as join5 } from "path";
3723
3966
  async function executeE2B(stage, spec, log, signal) {
@@ -3757,7 +4000,7 @@ async function executeE2B(stage, spec, log, signal) {
3757
4000
  await sandbox.commands.run("chmod -R a+rX /input", { user: "root" });
3758
4001
  const runtimeEnv = {};
3759
4002
  if (spec.postgres) {
3760
- const password = randomBytes5(24).toString("hex");
4003
+ const password = randomBytes6(24).toString("hex");
3761
4004
  secrets.push(password);
3762
4005
  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" });
3763
4006
  await sandbox.files.write("/tmp/agenttwin-init.sql", `CREATE ROLE agenttwin LOGIN PASSWORD '${password}';
@@ -3771,7 +4014,7 @@ CREATE DATABASE agenttwin OWNER agenttwin;
3771
4014
  }
3772
4015
  }
3773
4016
  if (spec.redis) {
3774
- const password = randomBytes5(24).toString("hex");
4017
+ const password = randomBytes6(24).toString("hex");
3775
4018
  secrets.push(password);
3776
4019
  await sandbox.files.write("/tmp/agenttwin-redis.conf", `bind 127.0.0.1
3777
4020
  port 6379
@@ -3811,14 +4054,14 @@ logfile /tmp/agenttwin-redis.log
3811
4054
  }
3812
4055
 
3813
4056
  // src/runtime/scenarios.ts
3814
- import { createHash as createHash5 } from "crypto";
4057
+ import { createHash as createHash6 } from "crypto";
3815
4058
  import { mkdir as mkdir2, readFile as readFile5, readdir as readdir2, writeFile as writeFile3 } from "fs/promises";
3816
4059
  import { join as join6 } from "path";
3817
4060
  import { isDeepStrictEqual as isDeepStrictEqual4 } from "util";
3818
4061
  var identifier = /^[a-z][a-z0-9_]{0,39}$/;
3819
4062
  var object2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
3820
4063
  var sort = (v) => Array.isArray(v) ? v.map(sort) : object2(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sort(v[k])])) : v;
3821
- var scenarioDigest = (scenario) => createHash5("sha256").update(JSON.stringify(sort(scenario))).digest("hex");
4064
+ var scenarioDigest = (scenario) => createHash6("sha256").update(JSON.stringify(sort(scenario))).digest("hex");
3822
4065
  function parseScenario(value) {
3823
4066
  const fail = (detail) => {
3824
4067
  throw new Error(`Invalid scenario: ${detail}`);
@@ -3870,14 +4113,14 @@ function applyScenario(world, scenario) {
3870
4113
  }
3871
4114
 
3872
4115
  // src/runtime/matrix.ts
3873
- import { createHash as createHash6 } from "crypto";
4116
+ import { createHash as createHash7 } from "crypto";
3874
4117
  import { mkdir as mkdir3, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
3875
4118
  import { join as join7, relative, resolve as resolve2 } from "path";
3876
4119
  var MAX_SEEDS = 128;
3877
4120
  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.";
3878
4121
  function filesDigest(sourceDigests) {
3879
4122
  const canonical2 = Object.keys(sourceDigests).sort().map((file) => `${file}:${sourceDigests[file]}`).join("\n");
3880
- return createHash6("sha256").update(canonical2).digest("hex");
4123
+ return createHash7("sha256").update(canonical2).digest("hex");
3881
4124
  }
3882
4125
  function checksOfReceipt(receipt) {
3883
4126
  const checks = (receipt.evaluation?.checks ?? []).map((c) => ({
@@ -3962,7 +4205,7 @@ function buildMatrixReport(input) {
3962
4205
  async function runSeedMatrix(cwd, opts) {
3963
4206
  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 });
3964
4207
  const baseSeed = normalizeSeed(opts.seed);
3965
- const matrixName = `matrix-${createHash6("sha256").update(`${baseSeed}:${opts.seeds}`).digest("hex").slice(0, 8)}`;
4208
+ const matrixName = `matrix-${createHash7("sha256").update(`${baseSeed}:${opts.seeds}`).digest("hex").slice(0, 8)}`;
3966
4209
  const matrixDir = resolve2(cwd, opts.out ?? join7(".agenttwin/runs", matrixName));
3967
4210
  await mkdir3(matrixDir, { recursive: true, mode: 448 });
3968
4211
  const results = [];
@@ -4019,7 +4262,7 @@ async function stageApplication(root, files, destination) {
4019
4262
  const target = join12(destination, file);
4020
4263
  await mkdir6(dirname(target), { recursive: true });
4021
4264
  await writeFile8(target, contents, { mode: 420 });
4022
- digests[file] = createHash7("sha256").update(contents).digest("hex");
4265
+ digests[file] = createHash8("sha256").update(contents).digest("hex");
4023
4266
  }
4024
4267
  return digests;
4025
4268
  }
@@ -4081,7 +4324,7 @@ async function runWorld(cwd, opts) {
4081
4324
  opts.log("Testing through the CLI with in-process service twins. No cloud VM or containers.");
4082
4325
  const report2 = await executeLocal(join12(stage, "app"), spec, opts.signal, void 0, suppliedSeed);
4083
4326
  const runId2 = randomUUID();
4084
- 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" };
4327
+ 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" };
4085
4328
  const out2 = resolve3(cwd, opts.out ?? `.agenttwin/runs/${runId2}`);
4086
4329
  await mkdir6(out2, { recursive: true });
4087
4330
  const evidencePath = join12(out2, "run.json"), reportPath = join12(out2, "report.md");
@@ -4102,7 +4345,7 @@ ${receipt2.reproducibility ?? ""}
4102
4345
  if (runtime === "hosted") {
4103
4346
  const receipt2 = await executeRemote(stage, spec, opts.log, { git, project });
4104
4347
  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");
4105
- const expectedSpec = createHash7("sha256").update(JSON.stringify(spec)).digest("hex");
4348
+ const expectedSpec = createHash8("sha256").update(JSON.stringify(spec)).digest("hex");
4106
4349
  const reportedSources = receipt2.sourceDigests;
4107
4350
  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");
4108
4351
  const out2 = resolve3(cwd, opts.out ?? `.agenttwin/runs/${receipt2.runId}`);
@@ -4140,7 +4383,7 @@ See run.json for assertions, service state, application output and isolation met
4140
4383
  image,
4141
4384
  templateBuildId,
4142
4385
  sourceDigests,
4143
- specDigest: createHash7("sha256").update(JSON.stringify(spec)).digest("hex"),
4386
+ specDigest: createHash8("sha256").update(JSON.stringify(spec)).digest("hex"),
4144
4387
  isolation
4145
4388
  };
4146
4389
  const out = resolve3(cwd, opts.out ?? `.agenttwin/runs/${runId}`);
@@ -4156,7 +4399,7 @@ See run.json for assertions, service state, application output and isolation met
4156
4399
  }
4157
4400
 
4158
4401
  // src/runtime/analytics.ts
4159
- import { createHash as createHash8 } from "crypto";
4402
+ import { createHash as createHash9 } from "crypto";
4160
4403
  function analyticsConfiguration() {
4161
4404
  const key = process.env.POSTHOG_API_KEY;
4162
4405
  const host = process.env.POSTHOG_API_HOST || "https://us.i.posthog.com";
@@ -4191,7 +4434,7 @@ function captureAnalytics(event) {
4191
4434
  }
4192
4435
 
4193
4436
  // src/runtime/gateway.ts
4194
- var hash2 = (value) => createHash9("sha256").update(value).digest("hex");
4437
+ var hash3 = (value) => createHash10("sha256").update(value).digest("hex");
4195
4438
  function uploadPath(path2) {
4196
4439
  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");
4197
4440
  return path2;
@@ -4207,6 +4450,11 @@ async function body(req, limit = 28 * 1024 * 1024) {
4207
4450
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
4208
4451
  }
4209
4452
  function send(res, code, value) {
4453
+ if (code === 429 && !res.hasHeader("Retry-After")) {
4454
+ const error = value?.error ?? "";
4455
+ const wait = error.startsWith("daily_") ? 86400 - Math.floor(Date.now() / 1e3) % 86400 : 60;
4456
+ res.setHeader("Retry-After", String(wait));
4457
+ }
4210
4458
  res.writeHead(code, {
4211
4459
  "content-type": "application/json",
4212
4460
  "cache-control": "no-store",
@@ -4218,11 +4466,14 @@ function send(res, code, value) {
4218
4466
  res.end(JSON.stringify(value));
4219
4467
  }
4220
4468
  function equal(a, b) {
4221
- const x = Buffer.from(hash2(a)), y = Buffer.from(hash2(b));
4222
- return timingSafeEqual4(x, y);
4469
+ const x = Buffer.from(hash3(a)), y = Buffer.from(hash3(b));
4470
+ return timingSafeEqual5(x, y);
4223
4471
  }
4224
4472
  async function startGateway(opts) {
4225
4473
  if (opts.adminToken.length < 40) throw new Error("AGENTTWIN_ADMIN_TOKEN must have at least 40 characters");
4474
+ const issuer = opts.publicUrl ?? "https://api.agenttwin.si";
4475
+ const issuerUrl = new URL(issuer);
4476
+ 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");
4226
4477
  const socialAuth = createSocialAuth(opts.socialAuth);
4227
4478
  const emailVerification = createEmailVerification(opts.emailVerification, opts.adminToken);
4228
4479
  const allowedOrigins = new Set(opts.allowedOrigins ?? []);
@@ -4268,6 +4519,7 @@ async function startGateway(opts) {
4268
4519
  });
4269
4520
  return writes;
4270
4521
  };
4522
+ const oauth = createOAuth(store, save, issuer);
4271
4523
  for (const thread of store.chats ?? []) {
4272
4524
  const last = thread.messages.at(-1);
4273
4525
  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 });
@@ -4339,7 +4591,9 @@ async function startGateway(opts) {
4339
4591
  const passkeyChallenges = {};
4340
4592
  const devices = /* @__PURE__ */ new Map();
4341
4593
  let hashing = 0;
4342
- const dummyPassword = randomBytes6(16).toString("hex") + ":" + randomBytes6(64).toString("hex");
4594
+ let localOnboardingUntil = 0;
4595
+ const dummyPassword = randomBytes7(16).toString("hex") + ":" + randomBytes7(64).toString("hex");
4596
+ const budget = requestBudget(opts.requestsPerMinute);
4343
4597
  const server = createServer2(async (req, res) => {
4344
4598
  try {
4345
4599
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
@@ -4366,6 +4620,11 @@ async function startGateway(opts) {
4366
4620
  }
4367
4621
  return;
4368
4622
  }
4623
+ if (!budget(res)) {
4624
+ send(res, 429, { error: "rate_limit_exceeded" });
4625
+ return;
4626
+ }
4627
+ res.setHeader("Access-Control-Expose-Headers", "RateLimit, RateLimit-Policy, Retry-After, WWW-Authenticate, Location");
4369
4628
  const origin = req.headers.origin;
4370
4629
  if (origin) {
4371
4630
  if (!allowedOrigins.has(origin)) {
@@ -4377,13 +4636,26 @@ async function startGateway(opts) {
4377
4636
  res.setHeader("Access-Control-Allow-Credentials", "true");
4378
4637
  if (req.method === "OPTIONS") {
4379
4638
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
4380
- res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
4639
+ res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type, Idempotency-Key");
4381
4640
  res.setHeader("Access-Control-Max-Age", "600");
4382
4641
  res.writeHead(204);
4383
4642
  res.end();
4384
4643
  return;
4385
4644
  }
4386
4645
  }
4646
+ if (req.method === "GET" && path2 === "/.well-known/oauth-authorization-server") {
4647
+ send(res, 200, oauthMetadata(issuer));
4648
+ return;
4649
+ }
4650
+ if (req.method === "GET" && path2 === "/.well-known/oauth-protected-resource") {
4651
+ send(res, 200, resourceMetadata(issuer));
4652
+ return;
4653
+ }
4654
+ if (!storageHealthy) {
4655
+ send(res, 503, { error: "storage_unavailable" });
4656
+ return;
4657
+ }
4658
+ if (await oauth(req, res, path2)) return;
4387
4659
  if (path2 === "/healthz" && req.method === "GET") {
4388
4660
  send(res, 200, { status: stopping ? "stopping" : "ok" });
4389
4661
  return;
@@ -4392,7 +4664,26 @@ async function startGateway(opts) {
4392
4664
  send(res, 503, { error: "storage_unavailable" });
4393
4665
  return;
4394
4666
  }
4395
- const token = req.headers.authorization?.replace(/^Bearer /, "") ?? "";
4667
+ const token = req.headers.authorization?.match(/^Bearer (.+)$/i)?.[1] ?? "";
4668
+ const grant = oauthGrant(store, token);
4669
+ if (token.startsWith("at_oauth_") && !grant) {
4670
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${issuer}/.well-known/oauth-protected-resource", error="invalid_token"`);
4671
+ send(res, 401, { error: "invalid_token" });
4672
+ return;
4673
+ }
4674
+ if (grant) {
4675
+ const required = requiredOAuthScope(req.method ?? "", path2);
4676
+ if (!required || !grant.token.scopes.includes(required)) {
4677
+ res.setHeader("WWW-Authenticate", `Bearer error="insufficient_scope"${required ? `, scope="${required}"` : ""}`);
4678
+ send(res, 403, { error: "insufficient_scope" });
4679
+ return;
4680
+ }
4681
+ if (requestUrl.searchParams.has("project") && requestUrl.searchParams.get("project") !== grant.client.project) {
4682
+ send(res, 403, { error: "project_not_authorized" });
4683
+ return;
4684
+ }
4685
+ requestUrl.searchParams.set("project", grant.client.project);
4686
+ }
4396
4687
  const cookieToken = req.headers.cookie?.split(";").map((v) => v.trim()).find((v) => v.startsWith("at_session="))?.slice(11) ?? "";
4397
4688
  const session = store.sessions?.find((s) => s.hash === digest(cookieToken) && s.expires > Date.now());
4398
4689
  if (cookieToken && !token && !["GET", "HEAD", "OPTIONS"].includes(req.method ?? "") && (!origin || !allowedOrigins.has(origin))) {
@@ -4405,7 +4696,7 @@ async function startGateway(opts) {
4405
4696
  };
4406
4697
  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") });
4407
4698
  if (path2 === "/v1/dev/workspace" && req.method === "GET" && opts.localDevelopment && ["127.0.0.1", "localhost"].includes(opts.host ?? "") && verificationUrl === "http://localhost:3000/onboarding/") {
4408
- send(res, 200, { schema: "agenttwin/local-workspace-v1", dashboard: verificationUrl });
4699
+ send(res, 200, { schema: "agenttwin/local-workspace-v1", dashboard: verificationUrl, onboarding: localOnboardingUntil > Date.now() });
4409
4700
  return;
4410
4701
  }
4411
4702
  if (path2 === "/v1/auth/providers" && req.method === "GET") {
@@ -4535,12 +4826,12 @@ async function startGateway(opts) {
4535
4826
  return;
4536
4827
  }
4537
4828
  if (store.accounts.length >= 1e4) throw new Error("account_capacity");
4538
- 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 } : {} };
4829
+ 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 } : {} };
4539
4830
  store.accounts.push(owner);
4540
4831
  captureAnalytics({ event: "account_created", distinctId: owner.id, properties: { method: identity.provider, realm: identity.organization || identity.issuer } });
4541
4832
  }
4542
4833
  if (!owner.active) throw new Error("account_inactive");
4543
- const secret = randomBytes6(32).toString("hex");
4834
+ const secret = randomBytes7(32).toString("hex");
4544
4835
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4545
4836
  const owned = store.sessions.filter((s) => s.accountId === owner.id);
4546
4837
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
@@ -4561,7 +4852,7 @@ async function startGateway(opts) {
4561
4852
  send(res, 429, { error: "Try again shortly." });
4562
4853
  return;
4563
4854
  }
4564
- const secret = randomBytes6(32).toString("hex"), code = randomBytes6(5).toString("hex").toUpperCase();
4855
+ const secret = randomBytes7(32).toString("hex"), code = randomBytes7(5).toString("hex").toUpperCase();
4565
4856
  devices.set(digest(secret), { code, expires: Date.now() + 6e5, lastPoll: 0 });
4566
4857
  send(res, 200, { deviceCode: secret, userCode: code, verificationUrl, expiresIn: 600 });
4567
4858
  return;
@@ -4582,7 +4873,7 @@ async function startGateway(opts) {
4582
4873
  }
4583
4874
  const rpID = requestUrl.hostname === "localhost" || requestUrl.hostname === "127.0.0.1" ? "localhost" : new URL(origin).hostname;
4584
4875
  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 })) });
4585
- const challengeId = randomBytes6(16).toString("hex");
4876
+ const challengeId = randomBytes7(16).toString("hex");
4586
4877
  challengeStore(passkeyChallenges, challengeId, options.challenge, 12e4);
4587
4878
  send(res, 200, { options, challengeId });
4588
4879
  return;
@@ -4629,7 +4920,7 @@ async function startGateway(opts) {
4629
4920
  const credentialId = typeof input.credentialId === "string" ? input.credentialId.slice(0, 1024) : "";
4630
4921
  const owner = credentialId ? store.accounts.find((a) => a.active && (a.passkeys ?? []).some((k) => k.id === credentialId)) : void 0;
4631
4922
  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" });
4632
- const challengeId = randomBytes6(16).toString("hex");
4923
+ const challengeId = randomBytes7(16).toString("hex");
4633
4924
  challengeStore(passkeyChallenges, challengeId, options.challenge, 12e4);
4634
4925
  send(res, 200, { options, challengeId });
4635
4926
  return;
@@ -4660,7 +4951,7 @@ async function startGateway(opts) {
4660
4951
  return;
4661
4952
  }
4662
4953
  key.counter = verification.authenticationInfo.newCounter;
4663
- const secret = randomBytes6(32).toString("hex");
4954
+ const secret = randomBytes7(32).toString("hex");
4664
4955
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4665
4956
  const owned = store.sessions.filter((s) => s.accountId === owner.id);
4666
4957
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
@@ -4721,7 +5012,8 @@ async function startGateway(opts) {
4721
5012
  send(res, 400, { error: "Use a valid email and a password with 12\u2013128 characters." });
4722
5013
  return;
4723
5014
  }
4724
- if (emailVerification.enabled && (!emailGrant?.verified || emailGrant.email !== parsed.email || !emailVerification.bound(emailGrant, emailBinding ?? "", origin))) {
5015
+ const demoLogin = path2 === "/v1/auth/login" && parsed.email === "demo@agenttwin.si" && store.accounts.some((a) => a.identity?.email === parsed.email && a.identity.emailVerified === true);
5016
+ if (emailVerification.enabled && !demoLogin && (!emailGrant?.verified || emailGrant.email !== parsed.email || !emailVerification.bound(emailGrant, emailBinding ?? "", origin))) {
4725
5017
  send(res, 403, { error: "Verify your email with a one-time code before continuing.", code: "email_verification_required" });
4726
5018
  return;
4727
5019
  }
@@ -4756,21 +5048,21 @@ async function startGateway(opts) {
4756
5048
  send(res, 429, { error: "account_capacity" });
4757
5049
  return;
4758
5050
  }
4759
- a = { id: randomUUID2(), tokenHash: hash2(randomBytes6(32).toString("hex")), label: workspace, active: true, usageDay: "", runs: 0, identity: { email: parsed.email, name, passwordHash: encoded } };
5051
+ a = { id: randomUUID2(), tokenHash: hash3(randomBytes7(32).toString("hex")), label: workspace, active: true, usageDay: "", runs: 0, identity: { email: parsed.email, name, passwordHash: encoded } };
4760
5052
  store.accounts.push(a);
4761
5053
  captureAnalytics({ event: "account_created", distinctId: a.id, properties: { method: "email", realm: "password" } });
4762
5054
  } else {
4763
- const matches2 = await passwordMatches(parsed.password, a?.identity?.passwordHash ?? dummyPassword);
4764
- if (!a?.active || !a.identity?.passwordHash || !matches2) {
5055
+ const matches3 = await passwordMatches(parsed.password, a?.identity?.passwordHash ?? dummyPassword);
5056
+ if (!a?.active || !a.identity?.passwordHash || !matches3) {
4765
5057
  send(res, 401, { error: "Email or password is incorrect." });
4766
5058
  return;
4767
5059
  }
4768
5060
  }
4769
- const secret = randomBytes6(32).toString("hex");
5061
+ const secret = randomBytes7(32).toString("hex");
4770
5062
  store.sessions = (store.sessions ?? []).filter((s) => s.expires > Date.now());
4771
5063
  const owned = store.sessions.filter((s) => s.accountId === a.id);
4772
5064
  if (owned.length >= 10) store.sessions = store.sessions.filter((s) => s !== owned[0]);
4773
- if (emailVerification.enabled) {
5065
+ if (emailVerification.enabled && !demoLogin) {
4774
5066
  if (store.emailChallenges?.[emailChallengeId] !== emailGrant || !emailVerification.bound(emailGrant, emailBinding ?? "", origin)) {
4775
5067
  send(res, 403, { error: "Email verification expired. Request a new code." });
4776
5068
  return;
@@ -4876,8 +5168,8 @@ async function startGateway(opts) {
4876
5168
  send(res, 400, { error: "label_required" });
4877
5169
  return;
4878
5170
  }
4879
- const accessToken = `at_user_${randomBytes6(32).toString("hex")}`;
4880
- const account2 = { id: randomUUID2(), tokenHash: hash2(accessToken), label: data.label, active: true, usageDay: "", runs: 0 };
5171
+ const accessToken = `at_user_${randomBytes7(32).toString("hex")}`;
5172
+ const account2 = { id: randomUUID2(), tokenHash: hash3(accessToken), label: data.label, active: true, usageDay: "", runs: 0 };
4881
5173
  if (store.accounts.length >= 1e4) {
4882
5174
  send(res, 429, { error: "account_capacity" });
4883
5175
  return;
@@ -4903,11 +5195,21 @@ async function startGateway(opts) {
4903
5195
  send(res, 200, { revoked: true });
4904
5196
  return;
4905
5197
  }
4906
- const account = store.accounts.find((a) => a.active && (token ? a.tokenHash === hash2(token) || a.cliTokens?.includes(hash2(token)) : a.id === session?.accountId));
5198
+ 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));
4907
5199
  if (!account) {
5200
+ res.setHeader("WWW-Authenticate", `Bearer resource_metadata="${issuer}/.well-known/oauth-protected-resource"`);
4908
5201
  send(res, 401, { error: "unauthorized" });
4909
5202
  return;
4910
5203
  }
5204
+ if (path2 === "/v1/dev/workspace" && req.method === "POST" && opts.localDevelopment && ["127.0.0.1", "localhost"].includes(opts.host ?? "") && verificationUrl === "http://localhost:3000/onboarding/") {
5205
+ if (token || !session || origin !== "http://localhost:3000") {
5206
+ send(res, 403, { error: "browser_session_required" });
5207
+ return;
5208
+ }
5209
+ localOnboardingUntil = Date.now() + 10 * 6e4;
5210
+ send(res, 200, { waiting: true });
5211
+ return;
5212
+ }
4911
5213
  if (path2 === "/v1/auth/password" && req.method === "POST") {
4912
5214
  if (token || !session || !account.identity || !origin || !allowedOrigins.has(origin)) {
4913
5215
  send(res, 403, { error: "browser_session_required" });
@@ -4946,7 +5248,7 @@ async function startGateway(opts) {
4946
5248
  return;
4947
5249
  }
4948
5250
  const previousIdentity = account.identity, previousSessions = store.sessions;
4949
- const secret = randomBytes6(32).toString("hex");
5251
+ const secret = randomBytes7(32).toString("hex");
4950
5252
  account.identity = { ...account.identity, passwordHash: encoded };
4951
5253
  store.sessions = [...store.sessions.filter((s) => s.accountId !== account.id), { accountId: account.id, hash: digest(secret), expires: Date.now() + 7 * 864e5 }];
4952
5254
  try {
@@ -4968,8 +5270,8 @@ async function startGateway(opts) {
4968
5270
  send(res, 400, { error: "terminal_token_required" });
4969
5271
  return;
4970
5272
  }
4971
- account.cliTokens = (account.cliTokens ?? []).filter((t) => t !== hash2(token));
4972
- if (account.tokenHash === hash2(token)) account.tokenHash = hash2(randomBytes6(32).toString("hex"));
5273
+ account.cliTokens = (account.cliTokens ?? []).filter((t) => t !== hash3(token));
5274
+ if (account.tokenHash === hash3(token)) account.tokenHash = hash3(randomBytes7(32).toString("hex"));
4973
5275
  await save();
4974
5276
  send(res, 200, { signedOut: true });
4975
5277
  return;
@@ -4982,8 +5284,8 @@ async function startGateway(opts) {
4982
5284
  send(res, 400, { error: "Code not found or expired. Run agenttwin login again." });
4983
5285
  return;
4984
5286
  }
4985
- const accessToken = `at_user_${randomBytes6(32).toString("hex")}`;
4986
- account.cliTokens = [...(account.cliTokens ?? []).slice(-9), hash2(accessToken)];
5287
+ const accessToken = `at_user_${randomBytes7(32).toString("hex")}`;
5288
+ account.cliTokens = [...(account.cliTokens ?? []).slice(-9), hash3(accessToken)];
4987
5289
  device.accountId = account.id;
4988
5290
  await save();
4989
5291
  device.token = accessToken;
@@ -5030,6 +5332,47 @@ async function startGateway(opts) {
5030
5332
  const projectInventories = [...store.projectDiscoveries?.[account.id] ?? []];
5031
5333
  const latestInventory = store.discoveries?.[account.id];
5032
5334
  if (latestInventory && !projectInventories.some((p) => p.name === latestInventory.name)) projectInventories.push(latestInventory);
5335
+ if (path2 === "/v1/oauth/clients" && req.method === "POST") {
5336
+ const input = await body(req, 8192);
5337
+ if (typeof input.label !== "string" || !input.label.trim() || input.label.length > 100 || typeof input.project !== "string" || !projectInventories.some((p) => p.name === input.project)) {
5338
+ send(res, 400, { error: "valid_label_and_synced_project_required" });
5339
+ return;
5340
+ }
5341
+ if ((store.oauthClients ?? []).filter((c) => c.accountId === account.id && c.active).length >= 20) {
5342
+ send(res, 429, { error: "client_capacity" });
5343
+ return;
5344
+ }
5345
+ if (!account.active) {
5346
+ send(res, 401, { error: "unauthorized" });
5347
+ return;
5348
+ }
5349
+ if ((store.oauthClients ?? []).length >= 1e4) {
5350
+ send(res, 429, { error: "client_capacity" });
5351
+ return;
5352
+ }
5353
+ const { client, secret } = newOAuthClient(account.id, input.label.trim(), input.project, input.scopes);
5354
+ store.oauthClients ??= [];
5355
+ store.oauthClients.push(client);
5356
+ await save();
5357
+ send(res, 201, { client_id: client.id, client_secret: secret, project: client.project, scope: client.scopes.join(" "), token_endpoint: issuer + "/oauth/token" });
5358
+ return;
5359
+ }
5360
+ if (path2 === "/v1/oauth/clients" && req.method === "GET") {
5361
+ send(res, 200, { clients: (store.oauthClients ?? []).filter((c) => c.accountId === account.id && c.active).map(({ secretHash, accountId, ...client }) => client) });
5362
+ return;
5363
+ }
5364
+ if (/^\/v1\/oauth\/clients\/at_client_[a-f0-9]{32}$/.test(path2) && req.method === "DELETE") {
5365
+ const client = store.oauthClients?.find((c) => c.id === path2.split("/").at(-1) && c.accountId === account.id);
5366
+ if (!client) {
5367
+ send(res, 404, { error: "not_found" });
5368
+ return;
5369
+ }
5370
+ client.active = false;
5371
+ store.oauthTokens = (store.oauthTokens ?? []).filter((t) => t.clientId !== client.id);
5372
+ await save();
5373
+ send(res, 200, { revoked: true });
5374
+ return;
5375
+ }
5033
5376
  const requestedProject = requestUrl.searchParams.get("project");
5034
5377
  const selectedInventory = requestedProject === null ? latestInventory : projectInventories.find((p) => p.name === requestedProject);
5035
5378
  if (path2.startsWith("/v1/projects/") && req.method === "DELETE") {
@@ -5072,7 +5415,7 @@ async function startGateway(opts) {
5072
5415
  return;
5073
5416
  }
5074
5417
  if (path2 === "/v1/projects" && req.method === "GET") {
5075
- send(res, 200, { projects: projectInventories.map((p) => ({ name: p.name, tools: p.tools.length, workflows: p.workflows.length })), latest: latestInventory?.name ?? null });
5418
+ 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 });
5076
5419
  return;
5077
5420
  }
5078
5421
  if (path2 === "/v1/discovery" && req.method === "GET") {
@@ -5130,7 +5473,20 @@ async function startGateway(opts) {
5130
5473
  return;
5131
5474
  }
5132
5475
  if (path2 === "/v1/runs" && req.method === "GET") {
5133
- 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) })) });
5476
+ const limitText = requestUrl.searchParams.get("limit") ?? "100";
5477
+ if (!/^[1-9][0-9]?$|^100$/.test(limitText)) {
5478
+ send(res, 400, { error: "invalid_page_limit" });
5479
+ return;
5480
+ }
5481
+ const limit = Number(limitText), project = requestUrl.searchParams.get("project");
5482
+ 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));
5483
+ const cursor = requestUrl.searchParams.get("cursor"), offset = cursor === null ? 0 : jobs.findIndex((j) => j.id === cursor) + 1;
5484
+ if (cursor !== null && offset === 0) {
5485
+ send(res, 400, { error: "invalid_page_cursor" });
5486
+ return;
5487
+ }
5488
+ const page = jobs.slice(offset, offset + limit);
5489
+ 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 });
5134
5490
  return;
5135
5491
  }
5136
5492
  if (path2 === "/v1/runs" && req.method === "POST") {
@@ -5138,24 +5494,38 @@ async function startGateway(opts) {
5138
5494
  send(res, 503, { error: "stopping" });
5139
5495
  return;
5140
5496
  }
5141
- if (!process.env.E2B_API_KEY || !process.env.E2B_TEMPLATE_ID) {
5142
- send(res, 503, { error: "cloud_runner_not_configured", message: "Run through the CLI, or configure a cloud runner when this workflow needs one." });
5497
+ const input = await body(req);
5498
+ if (grant && !oauthGrant(store, token)) {
5499
+ send(res, 401, { error: "invalid_token" });
5143
5500
  return;
5144
5501
  }
5145
- if (busy(account.id)) {
5146
- send(res, 429, { error: "run_in_progress_or_queue_full" });
5502
+ if (grant && input.project !== grant.client.project) {
5503
+ send(res, 403, { error: "project_not_authorized" });
5147
5504
  return;
5148
5505
  }
5149
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5150
- if (account.usageDay !== today) {
5151
- account.usageDay = today;
5152
- account.runs = 0;
5506
+ const idempotencyKey = req.headers["idempotency-key"];
5507
+ if (idempotencyKey !== void 0 && (typeof idempotencyKey !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey))) {
5508
+ send(res, 400, { error: "invalid_idempotency_key" });
5509
+ return;
5153
5510
  }
5154
- if (account.runs >= dailyLimit) {
5155
- send(res, 429, { error: "daily_run_limit" });
5511
+ const requestDigest = hash3(JSON.stringify(input));
5512
+ if (idempotencyKey) {
5513
+ const prior = store.jobs.find((j) => j.accountId === account.id && j.idempotencyKey === idempotencyKey);
5514
+ if (prior) {
5515
+ if (prior.requestDigest !== requestDigest) {
5516
+ send(res, 409, { error: "idempotency_key_conflict" });
5517
+ return;
5518
+ }
5519
+ res.setHeader("Location", issuer + "/v1/runs/" + prior.id);
5520
+ res.setHeader("Idempotency-Replayed", "true");
5521
+ send(res, 202, { id: prior.id, status: prior.status });
5522
+ return;
5523
+ }
5524
+ }
5525
+ if (!process.env.E2B_API_KEY || !process.env.E2B_TEMPLATE_ID) {
5526
+ send(res, 503, { error: "cloud_runner_not_configured", message: "Run through the CLI, or configure a cloud runner when this workflow needs one." });
5156
5527
  return;
5157
5528
  }
5158
- const input = await body(req);
5159
5529
  const spec = parseWorld(input.spec);
5160
5530
  if (spec.browser || spec.background) {
5161
5531
  send(res, 422, { error: "unsupported_runtime", message: "Browser recipes and background completion checks currently require the local runtime. No cloud job was queued." });
@@ -5163,6 +5533,10 @@ async function startGateway(opts) {
5163
5533
  }
5164
5534
  const git = validateGit(input.git);
5165
5535
  const project = input.project;
5536
+ if (grant && project !== grant.client.project) {
5537
+ send(res, 403, { error: "project_not_authorized" });
5538
+ return;
5539
+ }
5166
5540
  if (project !== void 0 && (typeof project !== "string" || !projectInventories.some((p) => p.name === project))) {
5167
5541
  send(res, 409, { error: "sync_project_before_run" });
5168
5542
  return;
@@ -5195,11 +5569,12 @@ async function startGateway(opts) {
5195
5569
  return;
5196
5570
  }
5197
5571
  store.jobs = store.jobs.filter((j) => Date.parse(j.createdAt) > Date.now() - 30 * 864e5);
5198
- const job = { id: randomUUID2(), accountId: account.id, status: "queued", createdAt: (/* @__PURE__ */ new Date()).toISOString(), name: spec.name, workflowId: spec.workflowId, project };
5572
+ const job = { id: randomUUID2(), accountId: account.id, status: "queued", createdAt: (/* @__PURE__ */ new Date()).toISOString(), name: spec.name, workflowId: spec.workflowId, project, ...idempotencyKey ? { idempotencyKey, requestDigest } : {} };
5199
5573
  account.runs++;
5200
5574
  store.jobs.push(job);
5201
5575
  await save();
5202
5576
  captureAnalytics({ event: "run_submitted", distinctId: account.id, properties: { jobId: job.id, project } });
5577
+ res.setHeader("Location", issuer + "/v1/runs/" + job.id);
5203
5578
  controllers.set(job.id, new AbortController());
5204
5579
  queue.push({ job, spec, files: input.files, git, project });
5205
5580
  schedule();
@@ -5221,6 +5596,10 @@ async function startGateway(opts) {
5221
5596
  return;
5222
5597
  }
5223
5598
  const input = await body(req);
5599
+ if (grant && !oauthGrant(store, token)) {
5600
+ send(res, 401, { error: "invalid_token" });
5601
+ return;
5602
+ }
5224
5603
  const project = selectedInventory?.name;
5225
5604
  if (!project || input.project !== project) {
5226
5605
  send(res, 409, { error: "sync_project_before_saving" });
@@ -5386,7 +5765,7 @@ async function startGateway(opts) {
5386
5765
  const controller = new AbortController();
5387
5766
  chatControllers.set(account.id, controller);
5388
5767
  const timeout = setTimeout(() => controller.abort(), 45e3);
5389
- const context = { project, discoveryDigest: inventory ? hash2(JSON.stringify(inventory)) : null, jobId: job?.id ?? null };
5768
+ const context = { project, discoveryDigest: inventory ? hash3(JSON.stringify(inventory)) : null, jobId: job?.id ?? null };
5390
5769
  thread.messages.push({ id: randomUUID2(), role: "user", content: input.content.trim(), requestId: input.requestId, createdAt: (/* @__PURE__ */ new Date()).toISOString(), context });
5391
5770
  try {
5392
5771
  await save();
@@ -5423,7 +5802,7 @@ async function startGateway(opts) {
5423
5802
  }
5424
5803
  const match = path2.match(/^\/v1\/runs\/([a-f0-9-]{36})$/);
5425
5804
  if (match && req.method === "DELETE") {
5426
- const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id);
5805
+ const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id && (!grant || (j.project ?? j.receipt?.project) === grant.client.project));
5427
5806
  if (!job) {
5428
5807
  send(res, 404, { error: "not_found" });
5429
5808
  return;
@@ -5446,7 +5825,7 @@ async function startGateway(opts) {
5446
5825
  return;
5447
5826
  }
5448
5827
  if (match && req.method === "GET") {
5449
- const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id);
5828
+ const job = store.jobs.find((j) => j.id === match[1] && j.accountId === account.id && (!grant || (j.project ?? j.receipt?.project) === grant.client.project));
5450
5829
  if (!job) {
5451
5830
  send(res, 404, { error: "not_found" });
5452
5831
  return;
@@ -5484,7 +5863,7 @@ async function startGateway(opts) {
5484
5863
  import { fileURLToPath as fileURLToPath3 } from "url";
5485
5864
  var port = Number(process.env.PORT ?? 8080);
5486
5865
  if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid PORT");
5487
- var gateway = await startGateway({ emailVerification: emailVerificationFromEnv(process.env), socialAuth: socialAuthFromEnv(process.env), directory: process.env.AGENTTWIN_STATE_DIR ?? "/data/agenttwin", adminToken: process.env.AGENTTWIN_ADMIN_TOKEN ?? "", host: "0.0.0.0", dashboardDirectory: fileURLToPath3(new URL("./dashboard/", import.meta.url)), allowedOrigins: (process.env.AGENTTWIN_DASHBOARD_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter(Boolean), port });
5866
+ var gateway = await startGateway({ publicUrl: process.env.AGENTTWIN_API_PUBLIC_URL, emailVerification: emailVerificationFromEnv(process.env), socialAuth: socialAuthFromEnv(process.env), directory: process.env.AGENTTWIN_STATE_DIR ?? "/data/agenttwin", adminToken: process.env.AGENTTWIN_ADMIN_TOKEN ?? "", host: "0.0.0.0", dashboardDirectory: fileURLToPath3(new URL("./dashboard/", import.meta.url)), allowedOrigins: (process.env.AGENTTWIN_DASHBOARD_ORIGINS ?? "").split(",").map((origin) => origin.trim()).filter(Boolean), port });
5488
5867
  process.stdout.write(`AgentTwin API listening on port ${port}
5489
5868
  `);
5490
5869
  for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => {