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.
- package/dist/api.js +472 -93
- package/dist/cli.js +126 -19
- package/dist/dashboard/assets/next/static/RRU6kOWIag8OaxBjl6DjU/_buildManifest.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/1255-d4ec8dbb0865c655.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/1482.e664aba77631265d.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/182-71d400a5d9e02879.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/3050-bac69e3c6734beee.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/3056.ffa9a4d44a274b29.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/{2442.9a266da6229b8ae0.js → 4055.1056568a41510e92.js} +1 -1
- package/dist/dashboard/assets/next/static/chunks/4343.023a18f52318dbad.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/4bd1b696-f785427dddbba9fb.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/{5490.0d5d8d5ea3577f20.js → 6751.613adb085c9a5b06.js} +1 -1
- package/dist/dashboard/assets/next/static/chunks/6787-c85d7f794360b672.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/7772-f2d773cd3e94eea6.js +67 -0
- package/dist/dashboard/assets/next/static/chunks/8093-90b99184c0a57c57.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/{3454.d29ee7a49c2a4fba.js → 8975.5be81ec8b09e3b45.js} +1 -1
- package/dist/dashboard/assets/next/static/chunks/{7280.01b3f749332bb075.js → 9013.0f2be1cc70029afa.js} +2 -2
- package/dist/dashboard/assets/next/static/chunks/9376.42da54b90ff9f2d9.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/9675.14739028910476e4.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/971-f01a5b1fb6756f6c.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/_not-found/page-43db8aef3fa81958.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/about/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/account/password/page-c2ce3c796c817ca1.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/api-policy/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/auth/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-mockoon/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-pact/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-testcontainers/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/agenttwin-vs-wiremock/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/keploy-alternatives-for-database-and-payment-workflows/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/layout-563d8763d65d66b5.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/reset-test-environment-between-runs/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/blog/simulate-timeouts-and-duplicate-events/page-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/compare/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/contact/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-1afe915ed22ac5ed.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-d91fa87fbf634bc9.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/demo/page-4dc63755e0013e7c.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/developers/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-3c0d688140d910bd.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/guides/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/layout-993a96283a3ceb07.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/login/page-d6d4276397724d29.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/onboarding/page-bc480c9258e5cc4d.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/page-e4fe99bcc001a517.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/pricing/page-40f352fe5093a0ff.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/privacy/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/sandbox/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/signup/page-d6d4276397724d29.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/sitemap.xml/route-ba841d868e5d124a.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/layout-a5cf565e017886d3.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-ca68ba293cca7d75.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/layout-abbd9f3b8811f559.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/login/page-4f4a342a4c28f9de.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/onboarding/page-f04c0b236abaead3.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/page-3905446ec0206fb5.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/v2/signup/page-4f4a342a4c28f9de.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/app/what-is-agenttwin/page-3f5dbcdbf95bb2fc.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/{48a1527b.6d52e40c5e62d9d5.js → b468fba8.437b212aae4abcf8.js} +1 -1
- package/dist/dashboard/assets/next/static/chunks/framework-e54b663a2888b5de.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/main-5aa70aa3d6a46aa0.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/main-app-1ec13ceef4a34729.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/pages/_app-6c8c2371b16a04b8.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/pages/_error-94812ad32cad7365.js +1 -0
- package/dist/dashboard/assets/next/static/chunks/webpack-1ac08e31899efefb.js +1 -0
- package/dist/dashboard/assets/next/static/css/15b28a76469ea7e5.css +3 -0
- package/dist/dashboard/assets/next/static/css/{eea0966f05042bde.css → 18afa3e32331c41d.css} +1 -1
- package/dist/dashboard/assets/next/static/css/6695e0f2ae2e5aaf.css +1 -0
- package/dist/dashboard/assets/next/static/css/ced6baae4dc4cbab.css +1 -0
- package/dist/dashboard/assets/next/static/css/d35115cc1039cc68.css +1 -0
- package/dist/dashboard/assets/next/static/css/f27d5fbb1f484d9d.css +1 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/gateway.js +465 -86
- package/package.json +14 -2
- package/dist/dashboard/assets/next/static/BbTZs0rKnsmj45peSnlwJ/_buildManifest.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/1981-2ce2c5edd154524a.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/2529.07927d0600506985.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/3916-2698a1a3ecb8649f.js +0 -67
- package/dist/dashboard/assets/next/static/chunks/4705-a6d249b4a5dcde26.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/4816-45f42396ec2135cc.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/650-3092bf29ff938e12.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/6f3ce29d-af1469ba72f5d3e4.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/794.a624e6be9e42a62b.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/_not-found/page-471a6c74344187f9.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/account/password/page-aaee6711085b485d.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/layout-09cb13657f62384f.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/dashboard/page-945c916b24f48675.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/demo/page-3cba0aab460c8677.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/docs/[[...slug]]/page-9f7b8dca8b973b15.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/enterprise/page-4986698442bddc6b.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/layout-09868e53e4f0541f.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/login/page-2e29d4b17645c34c.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/onboarding/page-af2c4768dc34ec92.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/page-507229409812cedd.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/pricing/page-c27282cd7bfb0fcf.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/signup/page-2e29d4b17645c34c.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/sitemap.xml/route-36bcc482b50fcb46.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/layout-77a57cd622171076.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/dashboard/page-df34bcef0394d10d.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/layout-d1d715b5b2dae3cd.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/login/page-4f60d050bf954194.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/onboarding/page-abfe2f36213848d7.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/page-df6a34eff716d18e.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/app/v2/signup/page-4f60d050bf954194.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/framework-cb37e134fe056fa5.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/main-app-c83f3671ad6b824a.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/main-b7d6352ee94d0dba.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/pages/_app-9dd167be58cf5cbd.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/pages/_error-5a8b1eb076dd7c95.js +0 -1
- package/dist/dashboard/assets/next/static/chunks/webpack-44457f45df749a89.js +0 -1
- package/dist/dashboard/assets/next/static/css/0856ee8c880d8b92.css +0 -1
- package/dist/dashboard/assets/next/static/css/0cf03e33b963e5cb.css +0 -1
- package/dist/dashboard/assets/next/static/css/81a1668ab34a36e8.css +0 -1
- package/dist/dashboard/assets/next/static/css/b15fa8a0e168d625.css +0 -1
- /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
|
|
1233
|
-
if (!
|
|
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 ?
|
|
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
|
|
1554
|
-
const
|
|
1555
|
-
const a = Buffer.from(
|
|
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:
|
|
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 &&
|
|
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) || !
|
|
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
|
|
1619
|
-
var digest = (value) =>
|
|
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 =
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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 &&
|
|
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) =>
|
|
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 &&
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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-${
|
|
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] =
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
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
|
|
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(
|
|
4222
|
-
return
|
|
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
|
-
|
|
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?.
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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
|
|
4764
|
-
if (!a?.active || !a.identity?.passwordHash || !
|
|
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 =
|
|
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_${
|
|
4880
|
-
const account2 = { id: randomUUID2(), tokenHash:
|
|
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 ===
|
|
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 =
|
|
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 !==
|
|
4972
|
-
if (account.tokenHash ===
|
|
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_${
|
|
4986
|
-
account.cliTokens = [...(account.cliTokens ?? []).slice(-9),
|
|
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
|
-
|
|
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
|
-
|
|
5142
|
-
|
|
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 (
|
|
5146
|
-
send(res,
|
|
5502
|
+
if (grant && input.project !== grant.client.project) {
|
|
5503
|
+
send(res, 403, { error: "project_not_authorized" });
|
|
5147
5504
|
return;
|
|
5148
5505
|
}
|
|
5149
|
-
const
|
|
5150
|
-
if (
|
|
5151
|
-
|
|
5152
|
-
|
|
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
|
-
|
|
5155
|
-
|
|
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 ?
|
|
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, () => {
|