@bytesbrains/cruise 0.12.0 → 0.13.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.
Files changed (36) hide show
  1. package/dist/{budget-FIRNSN67.mjs → budget-IIC4NJUU.mjs} +6 -5
  2. package/dist/{chunk-FGLMW5IY.mjs → chunk-2Z62Z53P.mjs} +2 -2
  3. package/dist/chunk-5AFYJTLS.mjs +62 -0
  4. package/dist/{chunk-WS6NFRIU.mjs → chunk-5LRJO4Z3.mjs} +1 -1
  5. package/dist/chunk-7ZUKADIS.mjs +131 -0
  6. package/dist/{chunk-LALZUKEC.mjs → chunk-A4DIVNYV.mjs} +1 -0
  7. package/dist/chunk-CDBZLSL7.mjs +65 -0
  8. package/dist/chunk-D3K25SBO.mjs +69 -0
  9. package/dist/chunk-NR4K7AUA.mjs +17 -0
  10. package/dist/{chunk-GUZNI3NS.mjs → chunk-PNJVYZ3Y.mjs} +3 -56
  11. package/dist/chunk-TJR2U73N.mjs +12 -0
  12. package/dist/{chunk-THEXUHQC.mjs → chunk-UBM65ZNR.mjs} +1 -0
  13. package/dist/{chunk-JZPUTXM7.mjs → chunk-Y2I46CTD.mjs} +113 -1
  14. package/dist/{compress-3DWG35BY.mjs → compress-QAEEBNVF.mjs} +1 -2
  15. package/dist/{conformance-NPFDYCQL.mjs → conformance-H2DRKMVX.mjs} +4 -5
  16. package/dist/cruise.mjs +132 -32
  17. package/dist/demo-seed-PRVDIWCM.mjs +59 -0
  18. package/dist/doctor-LVMXEX35.mjs +802 -0
  19. package/dist/{fallback-P5KK55M4.mjs → fallback-HRVI5XCK.mjs} +7 -6
  20. package/dist/journey-JXQ3VYCT.mjs +136 -0
  21. package/dist/{keys-FWPQPMTW.mjs → keys-S5QOOVMS.mjs} +3 -4
  22. package/dist/{ledger-XBGWXY6C.mjs → ledger-DWOMA7WD.mjs} +2 -3
  23. package/dist/{member-ZBV4YG2M.mjs → member-R7HUJK45.mjs} +1 -2
  24. package/dist/metrics-LCO4YNPW.mjs +144 -0
  25. package/dist/{report-VRYJ3W5L.mjs → report-CLNJPK6Z.mjs} +2 -3
  26. package/dist/secrets-check-6PWXXJM7.mjs +65 -0
  27. package/dist/{service-key-N2RZ7FMZ.mjs → service-key-E6SGTLWA.mjs} +1 -2
  28. package/dist/{smoke-ZNYX6RO5.mjs → smoke-DFNUPTZ7.mjs} +1 -2
  29. package/dist/staging-seed-647DRUW5.mjs +41 -0
  30. package/dist/{sweep-SZX7OIDK.mjs → sweep-5O32ZU4T.mjs} +2 -3
  31. package/dist/{tenant-4YDGI23Q.mjs → tenant-DRN6BAOI.mjs} +13 -9
  32. package/dist/{tier-4L3SPLFP.mjs → tier-JSY5OU65.mjs} +1 -2
  33. package/package.json +3 -3
  34. package/dist/chunk-7NQHBXVZ.mjs +0 -104
  35. package/dist/chunk-ICJVJSTC.mjs +0 -42
  36. package/dist/chunk-N3YKWKOA.mjs +0 -9
@@ -2,21 +2,23 @@
2
2
  // Built by packages/cli/build.ts.
3
3
  import {
4
4
  MAX_CHAIN
5
- } from "./chunk-FGLMW5IY.mjs";
5
+ } from "./chunk-2Z62Z53P.mjs";
6
6
  import "./chunk-4447IUTF.mjs";
7
7
  import "./chunk-XOH3XKM3.mjs";
8
8
  import "./chunk-42ERHAJD.mjs";
9
9
  import "./chunk-BNP63KB2.mjs";
10
- import "./chunk-ICJVJSTC.mjs";
11
10
  import "./chunk-UNXM6FSM.mjs";
12
11
  import "./chunk-SYTYOTO4.mjs";
13
12
  import "./chunk-O7TNRKSA.mjs";
13
+ import "./chunk-O7TNRKSA.mjs";
14
+ import "./chunk-7ZUKADIS.mjs";
14
15
  import "./chunk-T3ICD43U.mjs";
15
16
  import "./chunk-RT4R2W6H.mjs";
16
- import "./chunk-THEXUHQC.mjs";
17
+ import "./chunk-NR4K7AUA.mjs";
18
+ import "./chunk-UBM65ZNR.mjs";
17
19
  import {
18
20
  lookupModel
19
- } from "./chunk-LALZUKEC.mjs";
21
+ } from "./chunk-A4DIVNYV.mjs";
20
22
  import "./chunk-5TVD3ZAI.mjs";
21
23
  import {
22
24
  connect,
@@ -24,8 +26,7 @@ import {
24
26
  parseArgs,
25
27
  requireString,
26
28
  run
27
- } from "./chunk-7NQHBXVZ.mjs";
28
- import "./chunk-JZPUTXM7.mjs";
29
+ } from "./chunk-Y2I46CTD.mjs";
29
30
 
30
31
  // src/fallbacks-write.ts
31
32
  async function setFallbacks(db, modelId, fallbacks, actor) {
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ // Built by packages/cli/build.ts.
3
+ import {
4
+ formatUsd
5
+ } from "./chunk-UBM65ZNR.mjs";
6
+ import {
7
+ connect,
8
+ environmentOf,
9
+ optionalNumber,
10
+ parseArgs,
11
+ run
12
+ } from "./chunk-Y2I46CTD.mjs";
13
+
14
+ // src/journey.ts
15
+ async function tenantJourneys(db) {
16
+ const { results } = await db.prepare(
17
+ `SELECT t.id AS tenantId,
18
+ t.name AS name,
19
+ t.created_at AS provisionedAt,
20
+ (SELECT min(k.created_at) FROM virtual_keys k
21
+ JOIN projects p ON p.id = k.project_id WHERE p.tenant_id = t.id) AS firstKeyAt,
22
+ (SELECT min(l.at) FROM ledger l WHERE l.tenant_id = t.id) AS firstRequestAt,
23
+ (SELECT min(l.at) FROM ledger l WHERE l.tenant_id = t.id AND l.outcome = 'served') AS firstServedAt,
24
+ (SELECT min(l.at) FROM ledger l WHERE l.tenant_id = t.id
25
+ AND date(l.at) > (SELECT date(min(l2.at)) FROM ledger l2 WHERE l2.tenant_id = t.id)) AS returnedAt,
26
+ (SELECT min(n.created_at) FROM lanes n WHERE n.tenant_id = t.id) AS firstLaneAt,
27
+ (SELECT max(l.at) FROM ledger l WHERE l.tenant_id = t.id) AS lastRequestAt,
28
+ (SELECT count(*) FROM ledger l WHERE l.tenant_id = t.id) AS requests,
29
+ (SELECT count(*) FROM ledger l WHERE l.tenant_id = t.id AND l.outcome = 'served') AS served,
30
+ (SELECT coalesce(sum(l.cost_micros), 0) FROM ledger l WHERE l.tenant_id = t.id) AS spendMicros,
31
+ (SELECT count(*) FROM projects p WHERE p.tenant_id = t.id
32
+ AND NOT EXISTS (SELECT 1 FROM budgets b WHERE b.scope = 'project' AND b.subject = p.id)) AS uncappedProjects
33
+ FROM tenants t
34
+ ORDER BY t.created_at ASC`
35
+ ).all();
36
+ return results;
37
+ }
38
+ var STAGES = [
39
+ { key: "provisioned", of: (j) => j.provisionedAt },
40
+ { key: "key issued", of: (j) => j.firstKeyAt },
41
+ { key: "first request", of: (j) => j.firstRequestAt },
42
+ { key: "first served", of: (j) => j.firstServedAt },
43
+ { key: "came back", of: (j) => j.returnedAt }
44
+ ];
45
+ function funnel(journeys) {
46
+ let previous = null;
47
+ return STAGES.map(({ key, of }) => {
48
+ const tenants = journeys.filter((journey) => of(journey) !== null).length;
49
+ const stage = { stage: key, tenants, lost: previous === null ? 0 : previous - tenants };
50
+ previous = tenants;
51
+ return stage;
52
+ });
53
+ }
54
+ var STALL_HOURS = 24;
55
+ function stalled(journeys, now, thresholdHours = STALL_HOURS) {
56
+ const stalls = [];
57
+ for (const journey of journeys) {
58
+ let reachedAt = null;
59
+ let waitingFor = null;
60
+ for (const { key, of } of STAGES) {
61
+ const at2 = of(journey);
62
+ if (at2 !== null) {
63
+ reachedAt = at2;
64
+ waitingFor = null;
65
+ } else if (waitingFor === null) waitingFor = key;
66
+ }
67
+ if (waitingFor === null || reachedAt === null) continue;
68
+ const since = Date.parse(`${reachedAt.replace(" ", "T")}Z`);
69
+ const hours = Math.floor((now.getTime() - since) / 36e5);
70
+ if (hours >= thresholdHours) {
71
+ stalls.push({ tenantId: journey.tenantId, name: journey.name, waitingFor, hours });
72
+ }
73
+ }
74
+ return stalls.sort((a, b) => b.hours - a.hours);
75
+ }
76
+
77
+ // scripts/journey.ts
78
+ var at = (value) => value ?? "-";
79
+ run(async () => {
80
+ const args = parseArgs(process.argv.slice(2));
81
+ const db = connect(environmentOf(args));
82
+ const thresholdHours = optionalNumber(args, "stall-hours") ?? STALL_HOURS;
83
+ const journeys = await tenantJourneys(db);
84
+ const now = /* @__PURE__ */ new Date();
85
+ console.log("Cruise \u2014 tenant journeys\n");
86
+ console.log("funnel \u2014 each step's population contains the next");
87
+ console.log("step tenants lost here");
88
+ for (const stage of funnel(journeys)) {
89
+ console.log(
90
+ `${stage.stage.padEnd(18)} ${String(stage.tenants).padStart(7)} ${(stage.lost === 0 ? "-" : String(stage.lost)).padStart(9)}`
91
+ );
92
+ }
93
+ console.log("\n `came back` is a request on a later UTC day than the first. Anyone can make");
94
+ console.log(" one call; coming back is the thing a trial does not do.");
95
+ console.log(" A lane of their own is not a step here \u2014 most tenants use the global lanes,");
96
+ console.log(" and a step most successful tenants skip is a feature, not a funnel.");
97
+ const stalls = stalled(journeys, now, thresholdHours);
98
+ console.log(`
99
+
100
+ stalled \u2014 at the same step for ${thresholdHours}h or more`);
101
+ console.log("tenant waiting for for");
102
+ if (stalls.length === 0) console.log(" (nobody)");
103
+ for (const stall of stalls) {
104
+ console.log(
105
+ `${stall.tenantId.slice(0, 20).padEnd(21)} ${stall.waitingFor.padEnd(18)} ${`${stall.hours}h`.padStart(6)}`
106
+ );
107
+ }
108
+ console.log("\n A tenant that has passed every step is never listed here however quiet it");
109
+ console.log(" has since gone: churn is a different question, and `last request` below is");
110
+ console.log(" the column that answers it.");
111
+ console.log("\n\nmilestones, oldest tenant first");
112
+ console.log("tenant provisioned first key first served last request reqs spend");
113
+ if (journeys.length === 0) console.log(" (no tenants)");
114
+ for (const journey of journeys) {
115
+ console.log(
116
+ `${journey.tenantId.slice(0, 20).padEnd(21)} ${at(journey.provisionedAt).padEnd(20)} ${at(journey.firstKeyAt).padEnd(20)} ${at(journey.firstServedAt).padEnd(20)} ${at(journey.lastRequestAt).padEnd(20)} ${String(journey.requests).padStart(9)} ${formatUsd(journey.spendMicros, 4).padStart(9)}`
117
+ );
118
+ }
119
+ const uncapped = journeys.filter((journey) => journey.uncappedProjects > 0);
120
+ if (uncapped.length > 0) {
121
+ console.log("\n\n \u26A0 uncapped projects \u2014 no `budgets` row, so serving with no hard stop");
122
+ for (const journey of uncapped) {
123
+ console.log(` ${journey.tenantId}: ${journey.uncappedProjects}`);
124
+ }
125
+ console.log(" `npm run budget:set` gives each one a cap. Provisioning writes one in the");
126
+ console.log(" same batch, so these are tenants provisioned before it did, or by hand.");
127
+ }
128
+ const served = journeys.filter((journey) => journey.firstServedAt !== null).length;
129
+ console.log(
130
+ `
131
+
132
+ ${journeys.length} tenant${journeys.length === 1 ? "" : "s"}, ${served} of which ${served === 1 ? "has" : "have"} ever had a request served.`
133
+ );
134
+ console.log("Where the ones that never did stalled is `npm run metrics` \u2014 refusals carry no");
135
+ console.log("tenant, so the two reports are read beside each other rather than joined.");
136
+ });
@@ -2,7 +2,7 @@
2
2
  // Built by packages/cli/build.ts.
3
3
  import {
4
4
  listFlag
5
- } from "./chunk-WS6NFRIU.mjs";
5
+ } from "./chunk-5LRJO4Z3.mjs";
6
6
  import {
7
7
  issueKey,
8
8
  rateLimitProblem,
@@ -13,7 +13,7 @@ import {
13
13
  DEFAULT_CACHE_POOL,
14
14
  isCachePool
15
15
  } from "./chunk-T3ICD43U.mjs";
16
- import "./chunk-LALZUKEC.mjs";
16
+ import "./chunk-A4DIVNYV.mjs";
17
17
  import {
18
18
  keyEnvironmentFor
19
19
  } from "./chunk-5TVD3ZAI.mjs";
@@ -24,8 +24,7 @@ import {
24
24
  parseArgs,
25
25
  requireString,
26
26
  run
27
- } from "./chunk-7NQHBXVZ.mjs";
28
- import "./chunk-JZPUTXM7.mjs";
27
+ } from "./chunk-Y2I46CTD.mjs";
29
28
 
30
29
  // scripts/keys.ts
31
30
  run(async () => {
@@ -15,15 +15,14 @@ import "./chunk-O7TNRKSA.mjs";
15
15
  import "./chunk-T3ICD43U.mjs";
16
16
  import {
17
17
  formatUsd
18
- } from "./chunk-THEXUHQC.mjs";
18
+ } from "./chunk-UBM65ZNR.mjs";
19
19
  import "./chunk-5TVD3ZAI.mjs";
20
20
  import {
21
21
  connect,
22
22
  environmentOf,
23
23
  parseArgs,
24
24
  run
25
- } from "./chunk-7NQHBXVZ.mjs";
26
- import "./chunk-JZPUTXM7.mjs";
25
+ } from "./chunk-Y2I46CTD.mjs";
27
26
 
28
27
  // scripts/ledger.ts
29
28
  run(async () => {
@@ -12,8 +12,7 @@ import {
12
12
  parseArgs,
13
13
  requireString,
14
14
  run
15
- } from "./chunk-7NQHBXVZ.mjs";
16
- import "./chunk-JZPUTXM7.mjs";
15
+ } from "./chunk-Y2I46CTD.mjs";
17
16
 
18
17
  // scripts/member.ts
19
18
  run(async () => {
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env node
2
+ // Built by packages/cli/build.ts.
3
+ import {
4
+ formatUsd
5
+ } from "./chunk-UBM65ZNR.mjs";
6
+ import {
7
+ environmentOf,
8
+ loadWranglerConfig,
9
+ parseArgs,
10
+ resolveDataset,
11
+ run,
12
+ workerScript
13
+ } from "./chunk-Y2I46CTD.mjs";
14
+
15
+ // src/analytics-sql.ts
16
+ var OUTCOMES = "'served', 'refusal', 'empty', 'unpriced', 'no_usage', 'upstream_error'";
17
+ function window(days) {
18
+ const whole = Math.max(1, Math.floor(days));
19
+ return `timestamp > NOW() - INTERVAL '${whole}' DAY`;
20
+ }
21
+ var DATASET = /^[a-z][a-z0-9_]{0,63}$/;
22
+ function assertDataset(dataset) {
23
+ if (!DATASET.test(dataset)) throw new Error(`Not a dataset name: ${dataset}`);
24
+ return dataset;
25
+ }
26
+ function controlPlaneLatencySql(dataset, days) {
27
+ return `SELECT blob2 AS route,
28
+ blob3 AS method,
29
+ blob5 AS warmth,
30
+ sum(_sample_interval) AS requests,
31
+ quantileWeighted(0.50)(double1, _sample_interval) AS p50_ms,
32
+ quantileWeighted(0.95)(double1, _sample_interval) AS p95_ms,
33
+ quantileWeighted(0.99)(double1, _sample_interval) AS p99_ms
34
+ FROM ${assertDataset(dataset)}
35
+ WHERE blob1 = 'control-plane' AND ${window(days)}
36
+ GROUP BY route, method, warmth
37
+ ORDER BY requests DESC
38
+ FORMAT JSON`;
39
+ }
40
+ function refusalsSql(dataset, days) {
41
+ return `SELECT blob3 AS code,
42
+ blob2 AS route,
43
+ blob4 AS status,
44
+ sum(_sample_interval) AS refusals
45
+ FROM ${assertDataset(dataset)}
46
+ WHERE blob1 = 'cruise:refusal' AND ${window(days)}
47
+ GROUP BY code, route, status
48
+ ORDER BY refusals DESC
49
+ FORMAT JSON`;
50
+ }
51
+ function spendByModelSql(dataset, days) {
52
+ return `SELECT blob3 AS model,
53
+ blob7 AS outcome,
54
+ sum(_sample_interval) AS requests,
55
+ sum(double1 * _sample_interval) AS micros,
56
+ quantileWeighted(0.95)(double4, _sample_interval) AS upstream_p95_ms
57
+ FROM ${assertDataset(dataset)}
58
+ WHERE blob7 IN (${OUTCOMES}) AND ${window(days)}
59
+ GROUP BY model, outcome
60
+ ORDER BY requests DESC
61
+ FORMAT JSON`;
62
+ }
63
+ function trafficByProjectSql(dataset, days) {
64
+ return `SELECT blob2 AS project,
65
+ blob5 AS traffic_class,
66
+ sum(_sample_interval) AS requests,
67
+ sum(double1 * _sample_interval) AS micros
68
+ FROM ${assertDataset(dataset)}
69
+ WHERE blob7 IN (${OUTCOMES}) AND ${window(days)}
70
+ GROUP BY project, traffic_class
71
+ ORDER BY requests DESC
72
+ FORMAT JSON`;
73
+ }
74
+
75
+ // scripts/metrics.ts
76
+ var DEFAULT_DAYS = 7;
77
+ async function query(accountId, token, sql) {
78
+ const response = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`, {
79
+ method: "POST",
80
+ headers: { authorization: `Bearer ${token}`, "content-type": "text/plain" },
81
+ body: sql
82
+ });
83
+ const text = await response.text();
84
+ if (!response.ok) {
85
+ throw new Error(
86
+ `Analytics Engine SQL ${response.status}: ${text.trim()}
87
+
88
+ ` + (response.status === 403 ? "That is usually the token: this endpoint needs Account Analytics: Read.\n\n" : "") + sql
89
+ );
90
+ }
91
+ return JSON.parse(text);
92
+ }
93
+ function table(result, empty) {
94
+ if (result.data.length === 0) {
95
+ console.log(` ${empty}`);
96
+ return;
97
+ }
98
+ const columns = result.meta.map((column) => column.name);
99
+ const width = new Map(
100
+ columns.map((name) => [
101
+ name,
102
+ Math.max(name.length, ...result.data.map((row) => String(row[name] ?? "-").length))
103
+ ])
104
+ );
105
+ console.log(columns.map((name) => name.padEnd(width.get(name))).join(" "));
106
+ for (const row of result.data) {
107
+ console.log(columns.map((name) => String(row[name] ?? "-").padEnd(width.get(name))).join(" "));
108
+ }
109
+ }
110
+ run(async () => {
111
+ const args = parseArgs(process.argv.slice(2));
112
+ const environment = environmentOf(args);
113
+ const { accountId, token } = workerScript(environment);
114
+ const dataset = resolveDataset(loadWranglerConfig(), environment);
115
+ const days = typeof args.days === "string" ? Number(args.days) : DEFAULT_DAYS;
116
+ console.log(`Cruise \u2014 Analytics Engine, ${environment}
117
+ `);
118
+ console.log(`dataset ${dataset}`);
119
+ console.log(`window last ${days} day${days === 1 ? "" : "s"}`);
120
+ console.log("\n Counts are re-weighted by `_sample_interval`: the dataset samples under");
121
+ console.log(" load, and an unweighted count reads lowest exactly when traffic is highest.");
122
+ console.log(" Money here is approximate for the same reason \u2014 the ledger in D1 is the");
123
+ console.log(" store of record, and `npm run report` is what an invoice is checked against.");
124
+ console.log("\n\ncontrol-plane latency, by route \u2014 cold and warm never blended (#146)");
125
+ table(await query(accountId, token, controlPlaneLatencySql(dataset, days)), "(no control-plane traffic in the window)");
126
+ console.log("\n A cold isolate is our first call after idle. The dashboard's own cold start");
127
+ console.log(" sits on top of it and is not visible here; the two are additive.");
128
+ console.log("\n\nrefusals, by code \u2014 where callers stall (#490)");
129
+ table(await query(accountId, token, refusalsSql(dataset, days)), "(nothing has been refused in the window)");
130
+ console.log("\n Ordered by count, which is not ordered by importance: a rate-limit storm is");
131
+ console.log(" one caller's bad afternoon, and a single `measurement_stale` is a model that");
132
+ console.log(" left the catalogue under everybody. No tenant is recorded here by design \u2014");
133
+ console.log(" `npm run journey` answers who, from the ledger.");
134
+ console.log("\n\nserved traffic, by model and outcome");
135
+ table(await query(accountId, token, spendByModelSql(dataset, days)), "(no served traffic in the window)");
136
+ console.log("\n\ntraffic by project and class");
137
+ const traffic = await query(accountId, token, trafficByProjectSql(dataset, days));
138
+ table(traffic, "(no served traffic in the window)");
139
+ const micros = traffic.data.reduce((total, row) => total + Number(row.micros ?? 0), 0);
140
+ if (traffic.data.length > 0) {
141
+ console.log(`
142
+ \u2248 ${formatUsd(Math.round(micros), 4)} over the window, sampled. The ledger is exact.`);
143
+ }
144
+ });
@@ -15,15 +15,14 @@ import "./chunk-O7TNRKSA.mjs";
15
15
  import "./chunk-T3ICD43U.mjs";
16
16
  import {
17
17
  formatUsd
18
- } from "./chunk-THEXUHQC.mjs";
18
+ } from "./chunk-UBM65ZNR.mjs";
19
19
  import "./chunk-5TVD3ZAI.mjs";
20
20
  import {
21
21
  connect,
22
22
  environmentOf,
23
23
  parseArgs,
24
24
  run
25
- } from "./chunk-7NQHBXVZ.mjs";
26
- import "./chunk-JZPUTXM7.mjs";
25
+ } from "./chunk-Y2I46CTD.mjs";
27
26
 
28
27
  // src/report-cache.ts
29
28
  async function byLaneCache(db, month, tenantId) {
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Built by packages/cli/build.ts.
3
+ import {
4
+ HEALTH_BILLING_SECRETS
5
+ } from "./chunk-TJR2U73N.mjs";
6
+ import "./chunk-O7TNRKSA.mjs";
7
+ import {
8
+ CREDENTIAL_FREE_ENVIRONMENTS,
9
+ PROVIDER_KEY_PREFIX,
10
+ REQUIRED_SECRETS
11
+ } from "./chunk-NR4K7AUA.mjs";
12
+ import "./chunk-UBM65ZNR.mjs";
13
+ import "./chunk-A4DIVNYV.mjs";
14
+ import "./chunk-5TVD3ZAI.mjs";
15
+ import {
16
+ environmentOf,
17
+ parseArgs,
18
+ run,
19
+ workerScript
20
+ } from "./chunk-Y2I46CTD.mjs";
21
+
22
+ // src/upstream-credits.ts
23
+ var BILLING_OPENAI = "PROVIDER_BILLING_KEY_OPENAI";
24
+ var BILLING_ANTHROPIC = "PROVIDER_BILLING_KEY_ANTHROPIC";
25
+ var BILLING_CF = "CF_BILLING_TOKEN";
26
+ var CREDITS_BILLING_SECRETS = [BILLING_OPENAI, BILLING_ANTHROPIC, BILLING_CF];
27
+
28
+ // scripts/secrets-check.ts
29
+ run(async () => {
30
+ const args = parseArgs(process.argv.slice(2));
31
+ const environment = environmentOf(args);
32
+ const { accountId, scriptName, token } = workerScript(environment);
33
+ const response = await fetch(
34
+ `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}/secrets`,
35
+ { headers: { authorization: `Bearer ${token}` } }
36
+ );
37
+ const body = await response.json();
38
+ if (!response.ok || !body.success) {
39
+ const detail = body.errors?.map((e) => e.message).join("; ") ?? `HTTP ${response.status}`;
40
+ throw new Error(`Could not list secrets for ${scriptName}: ${detail}`);
41
+ }
42
+ const present = new Set((body.result ?? []).map((secret) => secret.name));
43
+ const missing = REQUIRED_SECRETS.filter((name) => !present.has(name));
44
+ if (missing.length > 0) {
45
+ throw new Error(
46
+ `${scriptName} is missing ${missing.length} required secret(s): ${missing.join(", ")}
47
+ ` + missing.map((name) => ` npx wrangler secret put ${name} --env ${environment}`).join("\n") + `
48
+ See docs/secrets.md for what each one is and what breaks without it.`
49
+ );
50
+ }
51
+ const credentialFree = CREDENTIAL_FREE_ENVIRONMENTS.includes(environment);
52
+ const billingForbidden = /* @__PURE__ */ new Set([...CREDITS_BILLING_SECRETS, ...HEALTH_BILLING_SECRETS]);
53
+ const forbidden = [...present].filter((name) => name.startsWith(PROVIDER_KEY_PREFIX) || billingForbidden.has(name)).sort();
54
+ if (credentialFree && forbidden.length > 0) {
55
+ throw new Error(
56
+ `${scriptName} holds ${forbidden.length} provider credential(s) it must not: ${forbidden.join(", ")}
57
+ ` + forbidden.map((name) => ` npx wrangler secret delete ${name} --env ${environment}`).join("\n") + `
58
+ The demo answers from the emulator and calls no provider. Being unable to
59
+ spend is the property the deployment rests on, and it is this absence.`
60
+ );
61
+ }
62
+ console.log(
63
+ `${scriptName}: all ${REQUIRED_SECRETS.length} required secret(s) present (${REQUIRED_SECRETS.join(", ")})` + (credentialFree ? `, and no ${PROVIDER_KEY_PREFIX}* / billing tokens \u2014 it can reach no provider` : "")
64
+ );
65
+ });
@@ -9,8 +9,7 @@ import {
9
9
  parseArgs,
10
10
  requireString,
11
11
  run
12
- } from "./chunk-7NQHBXVZ.mjs";
13
- import "./chunk-JZPUTXM7.mjs";
12
+ } from "./chunk-Y2I46CTD.mjs";
14
13
 
15
14
  // scripts/service-key.ts
16
15
  run(async () => {
@@ -4,8 +4,7 @@ import {
4
4
  parseArgs,
5
5
  requireString,
6
6
  run
7
- } from "./chunk-7NQHBXVZ.mjs";
8
- import "./chunk-JZPUTXM7.mjs";
7
+ } from "./chunk-Y2I46CTD.mjs";
9
8
 
10
9
  // src/smoke.ts
11
10
  var NOT_CHECKED = "not checked \u2014 the catalogue did not list";
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ // Built by packages/cli/build.ts.
3
+ import {
4
+ STAGING_ENVIRONMENT,
5
+ mirrorStaging,
6
+ productionRoutable,
7
+ requireSource
8
+ } from "./chunk-CDBZLSL7.mjs";
9
+ import "./chunk-A4DIVNYV.mjs";
10
+ import {
11
+ connect,
12
+ environmentOf,
13
+ parseArgs,
14
+ run
15
+ } from "./chunk-Y2I46CTD.mjs";
16
+
17
+ // scripts/staging-seed.ts
18
+ var SOURCE = "production";
19
+ run(async () => {
20
+ const args = parseArgs(process.argv.slice(2));
21
+ const environment = environmentOf(args);
22
+ if (environment !== STAGING_ENVIRONMENT) {
23
+ throw new Error(`staging:seed refuses --env ${environment}. Pass --env ${STAGING_ENVIRONMENT}, or omit it.`);
24
+ }
25
+ const measured = requireSource(await productionRoutable(connect(SOURCE)), SOURCE);
26
+ console.log(`Cruise \u2014 staging measurements, from ${SOURCE}
27
+ `);
28
+ console.log(` routable ${measured.length} model(s) in ${SOURCE}`);
29
+ if (args["dry-run"] === true) {
30
+ for (const m of measured) console.log(` ${m.measured_at} ${m.modality.padEnd(10)} ${m.id}`);
31
+ console.log("\n --dry-run: nothing written.");
32
+ return;
33
+ }
34
+ const summary = await mirrorStaging(connect(environment), measured);
35
+ console.log(` measured ${summary.routable.length} model(s) now carry ${SOURCE}'s measurement on staging`);
36
+ if (summary.absent.length > 0) {
37
+ console.log(`
38
+ ${summary.absent.length} model(s) ${SOURCE} serves are NOT ENABLED HERE \u2014 run migrate:staging:`);
39
+ for (const id of summary.absent) console.log(` ${id}`);
40
+ }
41
+ });
@@ -9,15 +9,14 @@ import {
9
9
  import "./chunk-SYTYOTO4.mjs";
10
10
  import "./chunk-O7TNRKSA.mjs";
11
11
  import "./chunk-T3ICD43U.mjs";
12
- import "./chunk-THEXUHQC.mjs";
12
+ import "./chunk-UBM65ZNR.mjs";
13
13
  import "./chunk-5TVD3ZAI.mjs";
14
14
  import {
15
15
  connect,
16
16
  environmentOf,
17
17
  parseArgs,
18
18
  run
19
- } from "./chunk-7NQHBXVZ.mjs";
20
- import "./chunk-JZPUTXM7.mjs";
19
+ } from "./chunk-Y2I46CTD.mjs";
21
20
 
22
21
  // scripts/sweep.ts
23
22
  run(async () => {
@@ -3,29 +3,34 @@
3
3
  import {
4
4
  assertGateway,
5
5
  listTenants,
6
+ provisionTenant
7
+ } from "./chunk-PNJVYZ3Y.mjs";
8
+ import {
6
9
  prepaidOn,
7
10
  prepaySignups,
8
- provisionTenant,
9
11
  unprepaidSignups
10
- } from "./chunk-GUZNI3NS.mjs";
11
- import "./chunk-N3YKWKOA.mjs";
12
+ } from "./chunk-5AFYJTLS.mjs";
13
+ import "./chunk-D3K25SBO.mjs";
14
+ import "./chunk-CDBZLSL7.mjs";
12
15
  import "./chunk-2NVDAK5J.mjs";
13
- import "./chunk-FGLMW5IY.mjs";
16
+ import "./chunk-2Z62Z53P.mjs";
14
17
  import "./chunk-4447IUTF.mjs";
15
18
  import "./chunk-XOH3XKM3.mjs";
16
19
  import "./chunk-42ERHAJD.mjs";
17
20
  import "./chunk-BNP63KB2.mjs";
18
- import "./chunk-ICJVJSTC.mjs";
19
21
  import "./chunk-UNXM6FSM.mjs";
20
22
  import "./chunk-SYTYOTO4.mjs";
21
23
  import "./chunk-O7TNRKSA.mjs";
24
+ import "./chunk-O7TNRKSA.mjs";
25
+ import "./chunk-7ZUKADIS.mjs";
22
26
  import "./chunk-T3ICD43U.mjs";
23
27
  import "./chunk-RT4R2W6H.mjs";
28
+ import "./chunk-NR4K7AUA.mjs";
24
29
  import {
25
30
  formatUsd,
26
31
  usdToMicros
27
- } from "./chunk-THEXUHQC.mjs";
28
- import "./chunk-LALZUKEC.mjs";
32
+ } from "./chunk-UBM65ZNR.mjs";
33
+ import "./chunk-A4DIVNYV.mjs";
29
34
  import "./chunk-5TVD3ZAI.mjs";
30
35
  import {
31
36
  connect,
@@ -34,8 +39,7 @@ import {
34
39
  parseArgs,
35
40
  requireString,
36
41
  run
37
- } from "./chunk-7NQHBXVZ.mjs";
38
- import "./chunk-JZPUTXM7.mjs";
42
+ } from "./chunk-Y2I46CTD.mjs";
39
43
 
40
44
  // scripts/tenant.ts
41
45
  function describe(tenant) {
@@ -6,8 +6,7 @@ import {
6
6
  parseArgs,
7
7
  requireString,
8
8
  run
9
- } from "./chunk-7NQHBXVZ.mjs";
10
- import "./chunk-JZPUTXM7.mjs";
9
+ } from "./chunk-Y2I46CTD.mjs";
11
10
 
12
11
  // src/tiers.ts
13
12
  var TierRefused = class extends Error {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytesbrains/cruise",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Run Cruise — one OpenAI-compatible endpoint in front of every model provider, with budgets that stop — in Docker or on your own Cloudflare account.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://gitlab.com/bytesbrains-oss/cruise.git",
21
+ "url": "git+https://gitlab.com/bytesbrains-oss/cruise.git",
22
22
  "directory": "packages/cli"
23
23
  },
24
24
  "homepage": "https://gitlab.com/bytesbrains-oss/cruise",
@@ -35,6 +35,6 @@
35
35
  "wrangler": "4.135.0"
36
36
  },
37
37
  "dependencies": {
38
- "@bytesbrains/cruise-worker": "0.12.0"
38
+ "@bytesbrains/cruise-worker": "0.13.0"
39
39
  }
40
40
  }