@bytesbrains/cruise 0.0.0-stage → 0.12.0
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/LICENSE +202 -0
- package/README.md +59 -2
- package/dist/budget-FIRNSN67.mjs +132 -0
- package/dist/chunk-2FB2T3UI.mjs +122 -0
- package/dist/chunk-2NVDAK5J.mjs +76 -0
- package/dist/chunk-42ERHAJD.mjs +79 -0
- package/dist/chunk-4447IUTF.mjs +7 -0
- package/dist/chunk-5TVD3ZAI.mjs +69 -0
- package/dist/chunk-7NQHBXVZ.mjs +104 -0
- package/dist/chunk-BNP63KB2.mjs +42 -0
- package/dist/chunk-FGLMW5IY.mjs +575 -0
- package/dist/chunk-GUZNI3NS.mjs +267 -0
- package/dist/chunk-HQDQTXRX.mjs +57 -0
- package/dist/chunk-ICJVJSTC.mjs +42 -0
- package/dist/chunk-JZPUTXM7.mjs +236 -0
- package/dist/chunk-LALZUKEC.mjs +137 -0
- package/dist/chunk-N3YKWKOA.mjs +9 -0
- package/dist/chunk-O7TNRKSA.mjs +2 -0
- package/dist/chunk-RT4R2W6H.mjs +21 -0
- package/dist/chunk-SYTYOTO4.mjs +18 -0
- package/dist/chunk-T3ICD43U.mjs +15 -0
- package/dist/chunk-THEXUHQC.mjs +98 -0
- package/dist/chunk-UNXM6FSM.mjs +83 -0
- package/dist/chunk-WS6NFRIU.mjs +66 -0
- package/dist/chunk-XOH3XKM3.mjs +126 -0
- package/dist/compress-3DWG35BY.mjs +49 -0
- package/dist/conformance-NPFDYCQL.mjs +922 -0
- package/dist/cruise.mjs +693 -0
- package/dist/fallback-P5KK55M4.mjs +139 -0
- package/dist/keys-FWPQPMTW.mjs +127 -0
- package/dist/ledger-XBGWXY6C.mjs +97 -0
- package/dist/member-ZBV4YG2M.mjs +74 -0
- package/dist/report-VRYJ3W5L.mjs +414 -0
- package/dist/service-key-N2RZ7FMZ.mjs +59 -0
- package/dist/smoke-ZNYX6RO5.mjs +105 -0
- package/dist/sweep-SZX7OIDK.mjs +33 -0
- package/dist/tenant-4YDGI23Q.mjs +122 -0
- package/dist/tier-4L3SPLFP.mjs +116 -0
- package/package.json +38 -4
- package/templates/docker-compose.yml +66 -0
- package/templates/env.example +44 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
|
|
4
|
+
// src/keys.ts
|
|
5
|
+
var NAMESPACE = "cru";
|
|
6
|
+
var PUBLIC_LEN = 8;
|
|
7
|
+
var SECRET_LEN = 32;
|
|
8
|
+
var ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
9
|
+
var REJECT_AT = 248;
|
|
10
|
+
var KEY_ENVIRONMENTS = ["live", "test", "demo"];
|
|
11
|
+
var KEY_KINDS = [...KEY_ENVIRONMENTS, "svc"];
|
|
12
|
+
var KEY_PATTERN = `cru_(${KEY_KINDS.join("|")})_[A-Za-z0-9]{${PUBLIC_LEN + SECRET_LEN}}`;
|
|
13
|
+
function keyEnvironmentFor(environment) {
|
|
14
|
+
if (environment === "production" || environment === "selfhosted" || environment === "local") return "live";
|
|
15
|
+
return KEY_ENVIRONMENTS.includes(environment) ? environment : "test";
|
|
16
|
+
}
|
|
17
|
+
var DEMO_KEY_RATE_LIMIT_PER_MIN = 60;
|
|
18
|
+
function keyRateLimitFor(environment, requested) {
|
|
19
|
+
if (environment !== "demo") return requested ?? null;
|
|
20
|
+
return requested == null ? DEMO_KEY_RATE_LIMIT_PER_MIN : Math.min(requested, DEMO_KEY_RATE_LIMIT_PER_MIN);
|
|
21
|
+
}
|
|
22
|
+
var CREDENTIAL_SHAPED = new RegExp(`cru_(${KEY_KINDS.join("|")})_`);
|
|
23
|
+
function randomString(length) {
|
|
24
|
+
let out = "";
|
|
25
|
+
const buf = new Uint8Array(length * 2);
|
|
26
|
+
while (out.length < length) {
|
|
27
|
+
crypto.getRandomValues(buf);
|
|
28
|
+
for (const byte of buf) {
|
|
29
|
+
if (out.length === length) break;
|
|
30
|
+
if (byte < REJECT_AT) out += ALPHABET[byte % ALPHABET.length];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
async function sha256Hex(input) {
|
|
36
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
37
|
+
return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
38
|
+
}
|
|
39
|
+
async function generateKey(environment) {
|
|
40
|
+
const publicPart = randomString(PUBLIC_LEN);
|
|
41
|
+
const secretPart = randomString(SECRET_LEN);
|
|
42
|
+
const key = `${NAMESPACE}_${environment}_${publicPart}${secretPart}`;
|
|
43
|
+
return { key, prefix: `${NAMESPACE}_${environment}_${publicPart}`, hash: await sha256Hex(key) };
|
|
44
|
+
}
|
|
45
|
+
async function generateServiceKey() {
|
|
46
|
+
const publicPart = randomString(PUBLIC_LEN);
|
|
47
|
+
const secretPart = randomString(SECRET_LEN);
|
|
48
|
+
const key = `${NAMESPACE}_svc_${publicPart}${secretPart}`;
|
|
49
|
+
return { key, prefix: `${NAMESPACE}_svc_${publicPart}`, hash: await sha256Hex(key) };
|
|
50
|
+
}
|
|
51
|
+
function parseKeyPrefix(presented) {
|
|
52
|
+
const parts = presented.split("_");
|
|
53
|
+
if (parts.length !== 3) return null;
|
|
54
|
+
const [namespace, environment, tail] = parts;
|
|
55
|
+
if (namespace !== NAMESPACE) return null;
|
|
56
|
+
if (environment === void 0 || !KEY_ENVIRONMENTS.includes(environment)) return null;
|
|
57
|
+
if (!tail || tail.length !== PUBLIC_LEN + SECRET_LEN) return null;
|
|
58
|
+
if (![...tail].every((c) => ALPHABET.includes(c))) return null;
|
|
59
|
+
return `${namespace}_${environment}_${tail.slice(0, PUBLIC_LEN)}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
keyEnvironmentFor,
|
|
64
|
+
keyRateLimitFor,
|
|
65
|
+
sha256Hex,
|
|
66
|
+
generateKey,
|
|
67
|
+
generateServiceKey,
|
|
68
|
+
parseKeyPrefix
|
|
69
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
import {
|
|
4
|
+
HttpD1,
|
|
5
|
+
LocalD1
|
|
6
|
+
} from "./chunk-JZPUTXM7.mjs";
|
|
7
|
+
|
|
8
|
+
// scripts/cli-support.ts
|
|
9
|
+
import { readFileSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
// scripts/wrangler-config.ts
|
|
13
|
+
function stripJsonc(raw) {
|
|
14
|
+
return JSON.parse(raw.replace(/(^|[^:])\/\/.*$/gm, "$1").replace(/\/\*[\s\S]*?\*\//g, ""));
|
|
15
|
+
}
|
|
16
|
+
function resolveScope(config, environment) {
|
|
17
|
+
const root = config;
|
|
18
|
+
const scope = environment === "top" ? root : root.env?.[environment];
|
|
19
|
+
if (!scope) throw new Error(`No such environment in wrangler.jsonc: ${environment}`);
|
|
20
|
+
return scope;
|
|
21
|
+
}
|
|
22
|
+
function resolveWorker(config, environment) {
|
|
23
|
+
const root = config;
|
|
24
|
+
const scope = resolveScope(config, environment);
|
|
25
|
+
const scriptName = scope.name ?? root.name;
|
|
26
|
+
if (!scriptName) throw new Error(`No worker name for environment: ${environment}`);
|
|
27
|
+
if (!root.account_id) throw new Error("No account_id in wrangler.jsonc");
|
|
28
|
+
return { accountId: root.account_id, scriptName };
|
|
29
|
+
}
|
|
30
|
+
function resolveDatabaseId(config, environment) {
|
|
31
|
+
const databaseId = resolveScope(config, environment).d1_databases?.[0]?.database_id;
|
|
32
|
+
if (!databaseId) throw new Error(`No d1_databases entry for environment: ${environment}`);
|
|
33
|
+
return databaseId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// scripts/cli-support.ts
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const args = {};
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const token = argv[i];
|
|
41
|
+
if (!token?.startsWith("--")) continue;
|
|
42
|
+
const name = token.slice(2);
|
|
43
|
+
const next = argv[i + 1];
|
|
44
|
+
if (next === void 0 || next.startsWith("--")) args[name] = true;
|
|
45
|
+
else {
|
|
46
|
+
args[name] = next;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
function requireString(args, name) {
|
|
53
|
+
const value = args[name];
|
|
54
|
+
if (typeof value !== "string" || value.length === 0) throw new Error(`Missing required --${name}`);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function optionalNumber(args, name) {
|
|
58
|
+
const value = args[name];
|
|
59
|
+
if (typeof value !== "string") return null;
|
|
60
|
+
const parsed = Number(value);
|
|
61
|
+
if (!Number.isFinite(parsed)) throw new Error(`--${name} must be a number, got ${value}`);
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
function loadWranglerConfig() {
|
|
65
|
+
return stripJsonc(readFileSync(path.join(import.meta.dirname, "..", "wrangler.jsonc"), "utf8"));
|
|
66
|
+
}
|
|
67
|
+
function requireToken() {
|
|
68
|
+
const token = process.env.CLOUDFLARE_API_TOKEN;
|
|
69
|
+
if (!token) throw new Error("CLOUDFLARE_API_TOKEN is not set");
|
|
70
|
+
return token;
|
|
71
|
+
}
|
|
72
|
+
function connect(environment) {
|
|
73
|
+
if (environment === LOCAL_ENVIRONMENT) return new LocalD1();
|
|
74
|
+
const token = requireToken();
|
|
75
|
+
if (environment === CLOUDFLARE_ENVIRONMENT) {
|
|
76
|
+
const accountId2 = process.env.CRUISE_D1_ACCOUNT_ID;
|
|
77
|
+
const databaseId = process.env.CRUISE_D1_DATABASE_ID;
|
|
78
|
+
if (!accountId2 || !databaseId) throw new Error("--env cloudflare needs CRUISE_D1_ACCOUNT_ID and CRUISE_D1_DATABASE_ID");
|
|
79
|
+
return new HttpD1(accountId2, databaseId, token);
|
|
80
|
+
}
|
|
81
|
+
const config = loadWranglerConfig();
|
|
82
|
+
const { accountId } = resolveWorker(config, environment);
|
|
83
|
+
return new HttpD1(accountId, resolveDatabaseId(config, environment), token);
|
|
84
|
+
}
|
|
85
|
+
var LOCAL_ENVIRONMENT = "local";
|
|
86
|
+
var CLOUDFLARE_ENVIRONMENT = "cloudflare";
|
|
87
|
+
function environmentOf(args) {
|
|
88
|
+
return typeof args.env === "string" ? args.env : "staging";
|
|
89
|
+
}
|
|
90
|
+
function run(main) {
|
|
91
|
+
main().catch((error) => {
|
|
92
|
+
console.error(error instanceof Error ? error.message : error);
|
|
93
|
+
process.exitCode = 1;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
parseArgs,
|
|
99
|
+
requireString,
|
|
100
|
+
optionalNumber,
|
|
101
|
+
connect,
|
|
102
|
+
environmentOf,
|
|
103
|
+
run
|
|
104
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Built by packages/cli/build.ts.
|
|
3
|
+
|
|
4
|
+
// src/ledger-attempts.ts
|
|
5
|
+
async function rescuesBy(db, query) {
|
|
6
|
+
const scope = `(?1 = 'all' OR strftime('%Y-%m', l.at) = ?1)
|
|
7
|
+
AND (?2 IS NULL OR l.tenant_id = ?2)
|
|
8
|
+
AND (?3 IS NULL OR l.project_id = ?3)`;
|
|
9
|
+
const binds = [query.month, query.tenantId ?? null, query.projectId ?? null];
|
|
10
|
+
const [totals, rows] = await db.batch([
|
|
11
|
+
db.prepare(
|
|
12
|
+
`SELECT count(*) AS requests,
|
|
13
|
+
sum(CASE WHEN l.http_status BETWEEN 200 AND 299
|
|
14
|
+
AND EXISTS (SELECT 1 FROM ledger_attempts a WHERE a.ledger_id = l.id) THEN 1 ELSE 0 END) AS rescued
|
|
15
|
+
FROM ledger l WHERE ${scope}`
|
|
16
|
+
).bind(...binds),
|
|
17
|
+
// The first failure per rescued request, by the lowest position it wrote.
|
|
18
|
+
db.prepare(
|
|
19
|
+
`SELECT l.lane, a.model AS failed_model, a.upstream_id AS failed_upstream, a.http_status AS failed_status,
|
|
20
|
+
l.model AS served_model, count(*) AS rescued
|
|
21
|
+
FROM ledger l
|
|
22
|
+
JOIN ledger_attempts a ON a.ledger_id = l.id
|
|
23
|
+
WHERE ${scope}
|
|
24
|
+
AND l.http_status BETWEEN 200 AND 299
|
|
25
|
+
AND a.position = (SELECT min(position) FROM ledger_attempts f WHERE f.ledger_id = l.id)
|
|
26
|
+
GROUP BY l.lane, a.model, a.upstream_id, a.http_status, l.model
|
|
27
|
+
ORDER BY rescued DESC`
|
|
28
|
+
).bind(...binds)
|
|
29
|
+
]);
|
|
30
|
+
const total = totals.results[0] ?? { requests: 0, rescued: 0 };
|
|
31
|
+
const rescued = total.rescued ?? 0;
|
|
32
|
+
return {
|
|
33
|
+
requests: total.requests,
|
|
34
|
+
rescued,
|
|
35
|
+
rescued_share: total.requests === 0 ? 0 : Math.round(rescued / total.requests * 1e3) / 1e3,
|
|
36
|
+
rows: rows.results
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
rescuesBy
|
|
42
|
+
};
|