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