@azx-pbc/helix-cli 0.0.0 → 0.2.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 (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -7
  3. package/dist/helix.js +1553 -0
  4. package/package.json +32 -6
package/dist/helix.js ADDED
@@ -0,0 +1,1553 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/args.ts
4
+ import { parseArgs } from "node:util";
5
+ var options = {
6
+ slug: { type: "string" },
7
+ "portal-url": { type: "string" },
8
+ dir: { type: "string" },
9
+ bundle: { type: "string" },
10
+ token: { type: "string" },
11
+ promote: { type: "boolean" },
12
+ "display-name": { type: "string" },
13
+ visibility: { type: "string" },
14
+ help: { type: "boolean" }
15
+ };
16
+ function parseCliArgs(argv) {
17
+ const args = argv[0] === "--" ? argv.slice(1) : argv;
18
+ return parseArgs({ args, allowPositionals: true, options });
19
+ }
20
+
21
+ // src/client.ts
22
+ import { z as z19 } from "zod";
23
+
24
+ // ../shared/src/visibility.ts
25
+ import { z } from "zod";
26
+ var VisibilitySchema = z.discriminatedUnion("mode", [
27
+ z.object({ mode: z.literal("internal") }),
28
+ z.object({ mode: z.literal("group"), groupId: z.string().min(1) }),
29
+ z.object({ mode: z.literal("password") }),
30
+ z.object({ mode: z.literal("public") })
31
+ ]);
32
+ var VISIBILITY_MODES = ["internal", "group", "password", "public"];
33
+ var VisibilityModeSchema = z.enum(VISIBILITY_MODES);
34
+
35
+ // ../shared/src/app.ts
36
+ import { z as z2 } from "zod";
37
+ var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
38
+ var AppSchema = z2.object({
39
+ id: z2.uuid(),
40
+ slug: z2.string().min(1).max(63).regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
41
+ displayName: z2.string().min(1).max(200),
42
+ visibility: VisibilitySchema,
43
+ /** The version currently served; null before the first deploy. */
44
+ currentVersionId: z2.uuid().nullable(),
45
+ /** When set, the app is archived: the edge serves 410 + Clear-Site-Data (§7). */
46
+ archivedAt: z2.iso.datetime().nullable(),
47
+ createdAt: z2.iso.datetime(),
48
+ updatedAt: z2.iso.datetime(),
49
+ /**
50
+ * Where this app is served, computed control-plane-side from the deployment's
51
+ * apps base (`APP_PUBLIC_BASE`) — so clients render a URL instead of
52
+ * templating `<slug>.<domain>` themselves and drifting per deployment. Optional
53
+ * on the wire: the CLI parses this schema, so requiring it would break a newer
54
+ * CLI against an older portal. Clients that lack it fall back to composing the
55
+ * slug onto `appPublicBase` from `GET /api/v1/config`.
56
+ */
57
+ url: z2.url().optional()
58
+ });
59
+
60
+ // ../shared/src/version.ts
61
+ import { z as z3 } from "zod";
62
+ var VERSION_STATUSES = ["preview", "live", "archived"];
63
+ var VersionStatusSchema = z3.enum(VERSION_STATUSES);
64
+ var VersionSchema = z3.object({
65
+ id: z3.uuid(),
66
+ appId: z3.uuid(),
67
+ /** Monotonic per app, 1-based. */
68
+ number: z3.int().positive(),
69
+ /** Blob key prefix for this version's assets, e.g. `apps/<appId>/7/`. */
70
+ blobPrefix: z3.string().min(1),
71
+ status: VersionStatusSchema,
72
+ createdAt: z3.iso.datetime()
73
+ });
74
+
75
+ // ../shared/src/manifest.ts
76
+ import { z as z4 } from "zod";
77
+ var LlmCapabilitySchema = z4.object({
78
+ models: z4.array(z4.string()).default([]),
79
+ /**
80
+ * Per-app daily LLM spend cap in USD; unset ⇒ unbounded. Denominated in
81
+ * dollars (not tokens) so the cap means the same thing across models — the
82
+ * edge prices each call via `@azx-pbc/shared` pricing and enforces a daily +
83
+ * rolling-hour burst window off the frozen `costMicroUsd` ledger column.
84
+ */
85
+ dollarsPerDay: z4.number().positive().optional()
86
+ });
87
+ var DataCapabilitySchema = z4.object({
88
+ user: z4.boolean().default(false),
89
+ collections: z4.array(z4.string().min(1)).default([]),
90
+ sharedRead: z4.array(z4.string().min(1)).default([]),
91
+ sharedWrite: z4.array(z4.string().min(1)).default([]),
92
+ writesPerDay: z4.int().positive().optional(),
93
+ bytesPerDay: z4.int().positive().optional()
94
+ });
95
+ var FetchConnectionSchema = z4.object({
96
+ origin: z4.url(),
97
+ connection: z4.string().min(1).optional()
98
+ });
99
+ var FetchCapabilitySchema = z4.object({
100
+ /** Opt-in transparent `fetch` shim injected at serve time (fetch-proxy §3.2). */
101
+ shim: z4.boolean().default(false),
102
+ /** Origins reached through the proxy (mode `proxy`); direct stays in `externalOrigins`. */
103
+ origins: z4.array(FetchConnectionSchema).default([]),
104
+ /** Per-app daily proxied-request budget; unset ⇒ unbounded (fetch-proxy §7). */
105
+ requestsPerDay: z4.int().positive().optional()
106
+ });
107
+ var SCOPE_SEGMENT = /^[A-Za-z0-9\-._~]+$/;
108
+ function isValidServiceWorkerScope(scope) {
109
+ if (!scope.startsWith("/") || !scope.endsWith("/")) return false;
110
+ const segments = scope.slice(1, -1).split("/");
111
+ const first = segments[0];
112
+ if (first === void 0) return false;
113
+ if (segments.some((s) => s === "" || s === "." || s === ".." || !SCOPE_SEGMENT.test(s))) {
114
+ return false;
115
+ }
116
+ return !first.startsWith("_");
117
+ }
118
+ var OfflineCapabilitySchema = z4.object({
119
+ /**
120
+ * URL path prefix the worker controls, e.g. `/app/`. Leading and trailing
121
+ * slash required; never root, never a `_`-prefixed platform namespace.
122
+ */
123
+ scope: z4.string().refine(isValidServiceWorkerScope, {
124
+ message: "scope must be a non-root path prefix with a leading and trailing slash, and must not start with a reserved `_` segment (e.g. `/app/`)"
125
+ })
126
+ });
127
+ var CapabilitiesSchema = z4.object({
128
+ llm: LlmCapabilitySchema.optional(),
129
+ data: DataCapabilitySchema.optional(),
130
+ /** Platform-registered MCP servers this app may reach (§6.1, exposed as REST). */
131
+ mcp: z4.array(z4.string()).default([]),
132
+ /** Extra CSP `connect-src` origins for **direct** browser calls (§4.4). */
133
+ externalOrigins: z4.array(z4.url()).default([]),
134
+ /** Governed outbound HTTP via the fetch-proxy / egress plane (in build, M4.5). */
135
+ fetch: FetchCapabilitySchema.optional(),
136
+ /** Platform-owned, scope-confined service worker for offline cold boot (ADR-0035). */
137
+ offline: OfflineCapabilitySchema.optional()
138
+ });
139
+ var AppManifestSchema = z4.object({
140
+ /** App slug; matches `App.slug`. */
141
+ app: z4.string().min(1),
142
+ visibility: VisibilitySchema,
143
+ capabilities: CapabilitiesSchema.default({ mcp: [], externalOrigins: [] })
144
+ });
145
+
146
+ // ../shared/src/approval.ts
147
+ import { z as z5 } from "zod";
148
+
149
+ // ../shared/src/pricing.ts
150
+ var MODEL_PRICING = {
151
+ // Anthropic. NB `structuredOutputs` is deliberately absent on 4-7/4-6/sonnet-4-6:
152
+ // structured outputs are supported on Fable 5, Opus 4.8 and Haiku 4.5 but not on
153
+ // those three, so the flag is opt-in per model rather than per provider.
154
+ "claude-fable-5": {
155
+ inputPerMTok: 10,
156
+ outputPerMTok: 50,
157
+ provider: "anthropic",
158
+ structuredOutputs: true
159
+ },
160
+ "claude-opus-5": {
161
+ inputPerMTok: 5,
162
+ outputPerMTok: 25,
163
+ provider: "anthropic",
164
+ structuredOutputs: true
165
+ },
166
+ // NB list rates. Sonnet 5 has promotional $2/$10 pricing through 2026-08-31; this
167
+ // table drives the **cost gate**, so the list price is the safe number — it
168
+ // over-estimates spend during the promo rather than under-billing once it lapses.
169
+ "claude-sonnet-5": {
170
+ inputPerMTok: 3,
171
+ outputPerMTok: 15,
172
+ provider: "anthropic",
173
+ structuredOutputs: true
174
+ },
175
+ "claude-opus-4-8": {
176
+ inputPerMTok: 5,
177
+ outputPerMTok: 25,
178
+ provider: "anthropic",
179
+ structuredOutputs: true
180
+ },
181
+ "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
182
+ "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
183
+ "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15, provider: "anthropic" },
184
+ "claude-haiku-4-5": {
185
+ inputPerMTok: 1,
186
+ outputPerMTok: 5,
187
+ provider: "anthropic",
188
+ structuredOutputs: true
189
+ },
190
+ // OpenAI — VERIFY against current published rates before production billing.
191
+ // Every model here resolves to a snapshot new enough for `response_format`
192
+ // json_schema, so `structuredOutputs` is set across the board.
193
+ "gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10, provider: "openai", structuredOutputs: true },
194
+ "gpt-4o-mini": {
195
+ inputPerMTok: 0.15,
196
+ outputPerMTok: 0.6,
197
+ provider: "openai",
198
+ structuredOutputs: true
199
+ },
200
+ "gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8, provider: "openai", structuredOutputs: true },
201
+ "gpt-4.1-mini": {
202
+ inputPerMTok: 0.4,
203
+ outputPerMTok: 1.6,
204
+ provider: "openai",
205
+ structuredOutputs: true
206
+ },
207
+ "gpt-4.1-nano": {
208
+ inputPerMTok: 0.1,
209
+ outputPerMTok: 0.4,
210
+ provider: "openai",
211
+ structuredOutputs: true
212
+ },
213
+ o3: {
214
+ inputPerMTok: 2,
215
+ outputPerMTok: 8,
216
+ provider: "openai",
217
+ reasoning: true,
218
+ minCompletionTokens: 25e3,
219
+ structuredOutputs: true
220
+ },
221
+ "o4-mini": {
222
+ inputPerMTok: 1.1,
223
+ outputPerMTok: 4.4,
224
+ provider: "openai",
225
+ reasoning: true,
226
+ minCompletionTokens: 25e3,
227
+ structuredOutputs: true
228
+ }
229
+ };
230
+
231
+ // ../shared/src/approval.ts
232
+ var RISK_LEVELS = ["low", "med", "high"];
233
+ var RiskSchema = z5.enum(RISK_LEVELS);
234
+ var APPROVAL_STATUSES = [
235
+ "pending",
236
+ "approved",
237
+ "denied",
238
+ "withdrawn",
239
+ "needs_changes"
240
+ ];
241
+ var ApprovalStatusSchema = z5.enum(APPROVAL_STATUSES);
242
+ var DeltaSchema = z5.object({
243
+ path: z5.string(),
244
+ from: z5.union([z5.string(), z5.number(), z5.boolean()]).optional(),
245
+ to: z5.union([z5.string(), z5.number(), z5.boolean()]).optional()
246
+ });
247
+ var ApprovalRequestSchema = z5.object({
248
+ id: z5.string(),
249
+ appId: z5.string(),
250
+ /** Joined for the admin queue / banner; not a column. */
251
+ appSlug: z5.string().optional(),
252
+ appDisplayName: z5.string().optional(),
253
+ status: ApprovalStatusSchema,
254
+ risk: RiskSchema,
255
+ deltas: z5.array(DeltaSchema),
256
+ baseSnapshot: z5.unknown(),
257
+ requestedBy: z5.string(),
258
+ reason: z5.string().nullable().optional(),
259
+ decidedBy: z5.string().nullable().optional(),
260
+ decisionNote: z5.string().nullable().optional(),
261
+ createdAt: z5.string(),
262
+ decidedAt: z5.string().nullable().optional()
263
+ });
264
+ var ManifestUpdateResultSchema = z5.object({
265
+ manifest: AppManifestSchema,
266
+ applied: z5.array(DeltaSchema),
267
+ pending: z5.string().nullable()
268
+ });
269
+ var VisibilityUpdateResultSchema = z5.object({
270
+ app: AppSchema,
271
+ applied: z5.array(DeltaSchema),
272
+ pending: z5.string().nullable()
273
+ });
274
+ var ApprovalDecisionRequestSchema = z5.object({
275
+ note: z5.string().max(2e3).optional()
276
+ });
277
+ var CURATED_LLM_MODELS = Object.keys(MODEL_PRICING);
278
+
279
+ // ../shared/src/health.ts
280
+ import { z as z6 } from "zod";
281
+ var HealthStateSchema = z6.enum(["ok", "degraded", "error"]);
282
+ var HealthCheckSchema = z6.object({
283
+ /** Stable identifier an alert rule can key on, e.g. `registry-projection`. */
284
+ name: z6.string(),
285
+ status: HealthStateSchema,
286
+ /** Operator-facing one-liner: what is wrong, and for how long. */
287
+ detail: z6.string().optional(),
288
+ /** Wall-clock instant this check last succeeded. Report-only — never the
289
+ * basis of a staleness decision (see the monotonic note in the edge's
290
+ * registry projection). */
291
+ lastSuccessAt: z6.iso.datetime().optional(),
292
+ /** Numeric facts a dashboard or a log-based metric can key on. */
293
+ metrics: z6.record(z6.string(), z6.number()).optional()
294
+ });
295
+ var HealthStatusSchema = z6.object({
296
+ /** The roll-up: the worst state across `checks` (see `worstHealthState`). */
297
+ status: HealthStateSchema,
298
+ service: z6.string(),
299
+ /** Process uptime in seconds. */
300
+ uptime: z6.number().nonnegative(),
301
+ /** Absent means the service reports liveness only (portal and egress today). */
302
+ checks: z6.array(HealthCheckSchema).optional()
303
+ });
304
+
305
+ // ../shared/src/api.ts
306
+ import { z as z7 } from "zod";
307
+ var CreateAppRequestSchema = z7.object({
308
+ slug: AppSchema.shape.slug,
309
+ displayName: AppSchema.shape.displayName,
310
+ visibility: VisibilitySchema.default({ mode: "internal" }),
311
+ /** Optional per-app capability grant set at create time (architecture §6.3). */
312
+ capabilities: CapabilitiesSchema.optional()
313
+ });
314
+ var SetManifestRequestSchema = z7.object({
315
+ capabilities: CapabilitiesSchema,
316
+ /** Justification carried onto an approval request for any elevated deltas. */
317
+ reason: z7.string().max(2e3).optional()
318
+ });
319
+ var SetVisibilityRequestSchema = z7.object({
320
+ visibility: VisibilitySchema,
321
+ /** Justification carried onto the approval request when going public. */
322
+ reason: z7.string().max(2e3).optional()
323
+ });
324
+ var MIN_PASSWORD_LENGTH = 12;
325
+ var SetPasswordRequestSchema = z7.object({
326
+ password: z7.string().min(MIN_PASSWORD_LENGTH).optional()
327
+ });
328
+ var PasswordCredentialResponseSchema = z7.object({
329
+ /** The shared passphrase, in cleartext, for the owner to copy/share. */
330
+ password: z7.string().min(1),
331
+ /** The app's public URL, prebuilt so the UI can offer a one-click copy. */
332
+ url: z7.url(),
333
+ /** When the current password was set (ISO 8601). */
334
+ setAt: z7.string()
335
+ });
336
+ var CspWarningSchema = z7.object({
337
+ file: z7.string(),
338
+ origin: z7.string(),
339
+ hint: z7.string()
340
+ });
341
+ var OriginGrantRequestSchema = z7.object({
342
+ origin: z7.url(),
343
+ reason: z7.string().max(2e3).optional()
344
+ });
345
+ var CspViolationSchema = z7.object({
346
+ appId: z7.string(),
347
+ appSlug: z7.string().nullable(),
348
+ directive: z7.string(),
349
+ blockedUri: z7.string(),
350
+ count: z7.number().int(),
351
+ lastSeen: z7.string(),
352
+ /**
353
+ * The blocked origin is already permitted by the app's *current* manifest, so
354
+ * this historical report is no longer actionable. Derived at read time
355
+ * (directive-aware — only `connect-src`/`img-src` are widened by an
356
+ * `externalOrigins` grant); the row itself is never deleted.
357
+ */
358
+ resolved: z7.boolean()
359
+ });
360
+ var CspViolationsPageSchema = z7.object({
361
+ violations: z7.array(CspViolationSchema)
362
+ });
363
+ var UploadVersionResponseSchema = z7.object({
364
+ version: VersionSchema,
365
+ warnings: z7.array(CspWarningSchema)
366
+ });
367
+ var RollbackRequestSchema = z7.object({
368
+ toNumber: z7.int().positive().optional()
369
+ });
370
+ var API_ERROR_CODES = [
371
+ "validation_failed",
372
+ "not_found",
373
+ "slug_taken",
374
+ "bundle_invalid",
375
+ "unauthorized",
376
+ /** Authenticated but not allowed — reserved for v1 RBAC. */
377
+ "forbidden",
378
+ "conflict",
379
+ /** Gateway: requested model is not in the app's manifest allowlist (§6.3). */
380
+ "model_not_allowed",
381
+ /** Gateway: the app's daily token budget is exhausted (§6.1). */
382
+ "quota_exceeded",
383
+ /**
384
+ * Gateway: the anonymous tier's per-IP request budget is exhausted on a
385
+ * `public` app (app-data design §7). Distinct from `quota_exceeded` (per-app
386
+ * daily budget) so the app can tell per-IP throttling apart from running out
387
+ * of its own budget. HTTP 429.
388
+ */
389
+ "rate_limited",
390
+ /** Gateway: a configured capability is not available on this edge. */
391
+ "capability_unavailable",
392
+ "internal"
393
+ ];
394
+ var ApiErrorCodeSchema = z7.enum(API_ERROR_CODES);
395
+ var ApiErrorSchema = z7.object({
396
+ error: z7.object({
397
+ code: ApiErrorCodeSchema,
398
+ message: z7.string(),
399
+ details: z7.unknown().optional()
400
+ })
401
+ });
402
+
403
+ // ../shared/src/auth.ts
404
+ import { z as z8 } from "zod";
405
+ var MeResponseSchema = z8.object({
406
+ user: z8.object({
407
+ /** IdP subject (Entra object id) — stable, safe to key app data on. */
408
+ id: z8.string(),
409
+ displayName: z8.string()
410
+ })
411
+ });
412
+ var PortalMeResponseSchema = z8.object({
413
+ sub: z8.string(),
414
+ /** How the actor was established: `oidc` or `dev-token`. */
415
+ via: z8.string(),
416
+ name: z8.string().optional(),
417
+ email: z8.string().optional(),
418
+ /**
419
+ * Whether the actor holds the `platform-admin` role. Computed server-side from
420
+ * the actor's group/role claim — the raw ids never cross to the browser. Drives
421
+ * admin nav + route gating in the SPA (and `helix whoami`).
422
+ */
423
+ isAdmin: z8.boolean()
424
+ });
425
+ var AuthConfigResponseSchema = z8.object({
426
+ issuer: z8.url(),
427
+ cliClientId: z8.string().min(1),
428
+ /** Public client the portal SPA uses for code+PKCE in the browser. */
429
+ webClientId: z8.string().min(1).optional(),
430
+ /** Expected token audience — part of what the CLI binds cached tokens to. */
431
+ audience: z8.string().min(1).optional(),
432
+ /**
433
+ * Whether this deployment permits `public` (anonymous) apps. Drives the SPA's
434
+ * visibility UI — it hides the public option when false. Absent = forbidden
435
+ * (older portal / dev-token-only where this endpoint 404s — open surfaces are
436
+ * opt-in). Server-side enforcement is independent of this hint (portal routes
437
+ * + edge serving).
438
+ */
439
+ allowPublicApps: z8.boolean().optional(),
440
+ /** Whether this deployment permits `password` (shared-passphrase) apps. Same shape as {@link allowPublicApps}. */
441
+ allowPasswordApps: z8.boolean().optional()
442
+ });
443
+ function portalApiScope(audience) {
444
+ if (!audience || audience.startsWith("urn:")) return null;
445
+ const appIdUri = audience.startsWith("api://") ? audience : `api://${audience}`;
446
+ return `${appIdUri.replace(/\/+$/, "")}/access`;
447
+ }
448
+
449
+ // ../shared/src/deployment.ts
450
+ import { z as z9 } from "zod";
451
+ var DeploymentConfigResponseSchema = z9.object({
452
+ /**
453
+ * Scheme + host + (non-default) port where apps are served, as reachable by
454
+ * this browser (architecture §4.1). The app slug is prepended as a subdomain:
455
+ * `https://<slug>.<host>`.
456
+ */
457
+ appPublicBase: z9.url(),
458
+ /**
459
+ * Base of the opt-in dev gateway (dev-mode design §3), absent when it is not
460
+ * deployed. Unlike {@link appPublicBase} the slug goes in the *path*, not the
461
+ * host. Absent ⇒ dev mode is unavailable here, so the UI says so instead of
462
+ * printing an unreachable host.
463
+ */
464
+ devApiBase: z9.url().optional(),
465
+ /**
466
+ * Month-to-date platform spend ceiling (USD) for the admin budget watch line.
467
+ * Display-only — the gateway is the choke point, so the rollup is exact, but
468
+ * nothing enforces this. Absent ⇒ no ceiling shown.
469
+ */
470
+ platformMonthlyUsdCap: z9.number().positive().optional(),
471
+ /**
472
+ * Deploy bundle size caps in megabytes (`DEPLOY_MAX_FILE_MB` /
473
+ * `DEPLOY_MAX_BUNDLE_MB` on the portal) — `deployMaxFileMb` is per file,
474
+ * `deployMaxBundleMb` the whole-archive total. A current portal always sends
475
+ * both; optional only to tolerate one that predates the fields, same as `url`
476
+ * on {@link AppSchema}. Absent means "don't state a number" — a client must not
477
+ * substitute a default, because printing the wrong cap to an agent sends it
478
+ * chasing a rejection it can't see the cause of.
479
+ */
480
+ deployMaxFileMb: z9.number().positive().optional(),
481
+ deployMaxBundleMb: z9.number().positive().optional()
482
+ });
483
+
484
+ // ../shared/src/scrypt.ts
485
+ var SCRYPT_PARAMS = {
486
+ /** CPU/memory cost. OWASP floor for scrypt. */
487
+ N: 2 ** 17,
488
+ /** Block size. */
489
+ r: 8,
490
+ /** Parallelization. */
491
+ p: 1,
492
+ /** 192 MiB — headroom above the ~128 MiB working set at these params. */
493
+ maxmem: 192 * 1024 * 1024
494
+ };
495
+
496
+ // ../shared/src/llm.ts
497
+ import { z as z10 } from "zod";
498
+ var LlmMessageSchema = z10.object({
499
+ role: z10.enum(["user", "assistant"]),
500
+ content: z10.string().min(1)
501
+ });
502
+ var LlmUsageSchema = z10.object({
503
+ inputTokens: z10.int().nonnegative(),
504
+ outputTokens: z10.int().nonnegative(),
505
+ cacheReadInputTokens: z10.int().nonnegative().default(0),
506
+ cacheCreationInputTokens: z10.int().nonnegative().default(0)
507
+ });
508
+ var MAX_SCHEMA_CHARS = 32768;
509
+ var MAX_SCHEMA_DEPTH = 12;
510
+ function schemaDepth(value, depth = 1) {
511
+ if (value === null || typeof value !== "object") return depth;
512
+ let deepest = depth;
513
+ for (const child of Object.values(value)) {
514
+ const d = schemaDepth(child, depth + 1);
515
+ if (d > deepest) deepest = d;
516
+ if (deepest > MAX_SCHEMA_DEPTH) return deepest;
517
+ }
518
+ return deepest;
519
+ }
520
+ function withinSchemaBudget(schema) {
521
+ let serialized;
522
+ try {
523
+ serialized = JSON.stringify(schema);
524
+ } catch {
525
+ return false;
526
+ }
527
+ return serialized.length <= MAX_SCHEMA_CHARS && schemaDepth(schema) <= MAX_SCHEMA_DEPTH;
528
+ }
529
+ var LlmResponseFormatSchema = z10.object({
530
+ type: z10.literal("json_schema"),
531
+ /** Schema name. OpenAI requires one; defaulted at translation when omitted. */
532
+ name: z10.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1-64 chars of [A-Za-z0-9_-]").optional(),
533
+ schema: z10.record(z10.string(), z10.unknown()).refine((s) => s.type === "object", 'schema root must be `{"type":"object"}`').refine(
534
+ withinSchemaBudget,
535
+ `schema must serialize to <= ${MAX_SCHEMA_CHARS} characters and nest <= ${MAX_SCHEMA_DEPTH} levels`
536
+ )
537
+ // NB there is deliberately no `strict` knob. Anthropic's `output_config.format`
538
+ // always enforces and has no best-effort mode, so a `strict:false` could only be
539
+ // honored on one vendor — the same request would then yield schema-violating JSON
540
+ // on `gpt-*` but not `claude-*`, which is exactly the provider leak this seam
541
+ // exists to prevent. The platform always enforces; see ADR-0034.
542
+ });
543
+ var LlmChatRequestSchema = z10.object({
544
+ model: z10.string().min(1),
545
+ messages: z10.array(LlmMessageSchema).min(1),
546
+ /** Optional system prompt; maps to the vendor's system channel. */
547
+ system: z10.string().optional(),
548
+ maxTokens: z10.int().positive().max(128e3).optional(),
549
+ /** Sampling temperature; forwarded as-is (the vendor validates its own range). */
550
+ temperature: z10.number().optional(),
551
+ /** Nucleus sampling; forwarded as-is. */
552
+ topP: z10.number().optional(),
553
+ /** Stop sequences; normalized to a list at the boundary. */
554
+ stop: z10.array(z10.string()).optional(),
555
+ /**
556
+ * Constrain the completion to a JSON schema (ADR-0034). Refused up front when
557
+ * the requested model can't enforce it (`ModelPrice.structuredOutputs`). The
558
+ * JSON still arrives as ordinary text, so `content` and the SSE `delta` frames
559
+ * are unchanged — callers `JSON.parse` the result.
560
+ */
561
+ responseFormat: LlmResponseFormatSchema.optional(),
562
+ /** SSE streaming (default) vs a single JSON body. */
563
+ stream: z10.boolean().default(true)
564
+ });
565
+ var LlmChatResponseSchema = z10.object({
566
+ model: z10.string(),
567
+ content: z10.string(),
568
+ stopReason: z10.string(),
569
+ usage: LlmUsageSchema
570
+ });
571
+ var LlmStreamDeltaSchema = z10.object({ text: z10.string() });
572
+ var LlmStreamDoneSchema = z10.object({
573
+ stopReason: z10.string(),
574
+ usage: LlmUsageSchema
575
+ });
576
+ var LlmStreamErrorSchema = z10.object({
577
+ code: z10.string(),
578
+ message: z10.string()
579
+ });
580
+
581
+ // ../shared/src/llmOpenai.ts
582
+ import { z as z11 } from "zod";
583
+ var OpenAiRoleSchema = z11.enum(["system", "developer", "user", "assistant", "tool"]);
584
+ var OpenAiMessageSchema = z11.object({
585
+ role: OpenAiRoleSchema,
586
+ content: z11.union([z11.string(), z11.array(z11.unknown())]).nullish(),
587
+ /** Present only on assistant tool-call turns — parsed to reject, never honored. */
588
+ tool_calls: z11.array(z11.unknown()).optional(),
589
+ tool_call_id: z11.string().optional(),
590
+ name: z11.string().optional()
591
+ });
592
+ var OpenAiStreamOptionsSchema = z11.object({
593
+ include_usage: z11.boolean().optional()
594
+ });
595
+ var OpenAiResponseFormatSchema = z11.discriminatedUnion("type", [
596
+ z11.object({
597
+ type: z11.literal("json_schema"),
598
+ json_schema: z11.object({
599
+ name: z11.string().optional(),
600
+ schema: z11.record(z11.string(), z11.unknown()),
601
+ strict: z11.boolean().nullish(),
602
+ /**
603
+ * Declared so the codec can reject it rather than zod silently stripping it.
604
+ * It is honorable on the OpenAI path but has no Anthropic equivalent, so
605
+ * forwarding it would make behaviour depend on the backing vendor — the same
606
+ * provider leak that got `json_object` rejected (ADR-0034).
607
+ */
608
+ description: z11.string().optional()
609
+ })
610
+ }),
611
+ z11.object({ type: z11.literal("json_object") }),
612
+ z11.object({ type: z11.literal("text") })
613
+ ]);
614
+ var OpenAiChatCompletionRequestSchema = z11.object({
615
+ model: z11.string().min(1),
616
+ messages: z11.array(OpenAiMessageSchema).min(1),
617
+ max_tokens: z11.int().positive().max(128e3).optional(),
618
+ max_completion_tokens: z11.int().positive().max(128e3).optional(),
619
+ temperature: z11.number().optional(),
620
+ top_p: z11.number().optional(),
621
+ stop: z11.union([z11.string(), z11.array(z11.string())]).optional(),
622
+ stream: z11.boolean().optional(),
623
+ stream_options: OpenAiStreamOptionsSchema.optional(),
624
+ /** Declared so the codec can reject tool use in v1 (not supported). */
625
+ tools: z11.array(z11.unknown()).optional(),
626
+ tool_choice: z11.unknown().optional(),
627
+ /**
628
+ * Structured output (ADR-0034). Parsed, not rejected — see the union above.
629
+ * `.nullish()` because clients and proxies that serialize every field send
630
+ * `"response_format": null` to mean "no structured output", which is a request
631
+ * this surface can serve; refusing it at the envelope would be gratuitous.
632
+ */
633
+ response_format: OpenAiResponseFormatSchema.nullish(),
634
+ // Behaviour-changing params the platform does not honor in v1. Declared (not
635
+ // stripped) so the codec can reject them with a 400 rather than silently drop
636
+ // them — the same "reject, never silently drop" contract as `tools`.
637
+ n: z11.unknown().optional(),
638
+ seed: z11.unknown().optional(),
639
+ logit_bias: z11.unknown().optional(),
640
+ presence_penalty: z11.unknown().optional(),
641
+ frequency_penalty: z11.unknown().optional(),
642
+ logprobs: z11.unknown().optional(),
643
+ top_logprobs: z11.unknown().optional()
644
+ });
645
+ var OpenAiUsageSchema = z11.object({
646
+ prompt_tokens: z11.int().nonnegative(),
647
+ completion_tokens: z11.int().nonnegative(),
648
+ total_tokens: z11.int().nonnegative()
649
+ });
650
+ var OpenAiChatCompletionResponseSchema = z11.object({
651
+ id: z11.string(),
652
+ object: z11.literal("chat.completion"),
653
+ created: z11.int(),
654
+ model: z11.string(),
655
+ choices: z11.array(
656
+ z11.object({
657
+ index: z11.int(),
658
+ message: z11.object({
659
+ role: z11.literal("assistant"),
660
+ content: z11.string()
661
+ }),
662
+ finish_reason: z11.string()
663
+ })
664
+ ),
665
+ usage: OpenAiUsageSchema
666
+ });
667
+ var OpenAiChatCompletionChunkSchema = z11.object({
668
+ id: z11.string(),
669
+ object: z11.literal("chat.completion.chunk"),
670
+ created: z11.int(),
671
+ model: z11.string(),
672
+ choices: z11.array(
673
+ z11.object({
674
+ index: z11.int(),
675
+ delta: z11.object({
676
+ role: z11.literal("assistant").optional(),
677
+ content: z11.string().optional()
678
+ }),
679
+ finish_reason: z11.string().nullable()
680
+ })
681
+ ),
682
+ usage: OpenAiUsageSchema.nullish()
683
+ });
684
+ var OpenAiModelSchema = z11.object({
685
+ id: z11.string(),
686
+ object: z11.literal("model"),
687
+ created: z11.int(),
688
+ owned_by: z11.string()
689
+ });
690
+ var OpenAiModelListSchema = z11.object({
691
+ object: z11.literal("list"),
692
+ data: z11.array(OpenAiModelSchema)
693
+ });
694
+
695
+ // ../shared/src/usage.ts
696
+ import { z as z12 } from "zod";
697
+ var GATEWAY_OUTCOMES = ["ok", "error", "refusal", "quota_blocked"];
698
+ var GatewayOutcomeSchema = z12.enum(GATEWAY_OUTCOMES);
699
+ var USAGE_RANGES = ["24h", "7d", "30d"];
700
+ var UsageRangeSchema = z12.enum(USAGE_RANGES);
701
+ var PLATFORM_RANGES = ["7d", "30d", "90d"];
702
+ var PlatformRangeSchema = z12.enum(PLATFORM_RANGES);
703
+ var UsageSeriesPointSchema = z12.object({
704
+ bucket: z12.iso.datetime(),
705
+ costUsd: z12.number().nonnegative(),
706
+ tokens: z12.int().nonnegative(),
707
+ requests: z12.int().nonnegative()
708
+ });
709
+ var UsageSummarySchema = z12.object({
710
+ appId: z12.uuid(),
711
+ /** The rolling range these figures cover. */
712
+ range: UsageRangeSchema,
713
+ requests: z12.int().nonnegative(),
714
+ inputTokens: z12.int().nonnegative(),
715
+ outputTokens: z12.int().nonnegative(),
716
+ /** Cache-aware input token totals (0 until prompt caching is enabled). */
717
+ cacheReadInputTokens: z12.int().nonnegative(),
718
+ cacheCreationInputTokens: z12.int().nonnegative(),
719
+ /** Estimated spend in USD over the window at current rates (./pricing.ts). */
720
+ costUsd: z12.number().nonnegative(),
721
+ /** 95th-percentile upstream latency (ms) over the window; null when no timed calls. */
722
+ latencyP95Ms: z12.number().nonnegative().nullable(),
723
+ /** Fraction of calls in the window whose outcome was not `ok` (0..1). */
724
+ errorRate: z12.number().min(0).max(1),
725
+ /** Count of calls keyed by outcome (`ok` / `error` / `refusal` / `quota_blocked`). */
726
+ byOutcome: z12.record(z12.string(), z12.int().nonnegative()),
727
+ byModel: z12.array(
728
+ z12.object({
729
+ model: z12.string(),
730
+ tokens: z12.int().nonnegative(),
731
+ requests: z12.int().nonnegative(),
732
+ /** Estimated spend in USD for this model over the window. */
733
+ costUsd: z12.number().nonnegative()
734
+ })
735
+ ),
736
+ /** Dense, zero-filled buckets across the range, oldest-first, for the trend chart. */
737
+ series: z12.array(UsageSeriesPointSchema),
738
+ /**
739
+ * Today-since-midnight totals, independent of `range` — backs the daily-cap
740
+ * gauge (the budget the edge enforces is per calendar day).
741
+ */
742
+ today: z12.object({
743
+ tokens: z12.int().nonnegative(),
744
+ costUsd: z12.number().nonnegative()
745
+ })
746
+ });
747
+ var GatewayCallSchema = z12.object({
748
+ id: z12.uuid(),
749
+ appId: z12.uuid(),
750
+ /** App slug at read time; null when the app row no longer exists. */
751
+ slug: z12.string().nullable(),
752
+ userOid: z12.string(),
753
+ capability: z12.string(),
754
+ model: z12.string(),
755
+ inputTokens: z12.int().nonnegative(),
756
+ outputTokens: z12.int().nonnegative(),
757
+ cacheReadInputTokens: z12.int().nonnegative(),
758
+ cacheCreationInputTokens: z12.int().nonnegative(),
759
+ /** Estimated spend in USD for this single call at current rates. */
760
+ costUsd: z12.number().nonnegative(),
761
+ /** Upstream round-trip latency in ms (0 when not measured). */
762
+ durationMs: z12.int().nonnegative(),
763
+ /** Upstream/egress HTTP status — set for `fetch`; null for streamed `llm`. */
764
+ statusCode: z12.int().nullable(),
765
+ /** LLM stop reason; null for non-LLM calls. */
766
+ stopReason: z12.string().nullable(),
767
+ /** Short upstream error string; null on success. */
768
+ errorDetail: z12.string().nullable(),
769
+ outcome: GatewayOutcomeSchema,
770
+ createdAt: z12.iso.datetime()
771
+ });
772
+ var GatewayAuditPageSchema = z12.object({
773
+ rows: z12.array(GatewayCallSchema),
774
+ /** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
775
+ nextBefore: z12.iso.datetime().optional()
776
+ });
777
+ var PlatformUsageSchema = z12.object({
778
+ /** The rolling range the series + breakdowns cover. */
779
+ range: PlatformRangeSchema,
780
+ /** Dense, zero-filled daily buckets across the range, oldest-first. */
781
+ series: z12.array(UsageSeriesPointSchema),
782
+ /** Per-app rollup over the range, busiest-first. */
783
+ byApp: z12.array(
784
+ z12.object({
785
+ slug: z12.string().nullable(),
786
+ tokens: z12.int().nonnegative(),
787
+ requests: z12.int().nonnegative(),
788
+ /** Estimated spend in USD for this app over the range. */
789
+ costUsd: z12.number().nonnegative()
790
+ })
791
+ ),
792
+ /** Month-to-date headline KPIs (independent of `range`). */
793
+ totals: z12.object({
794
+ tokensMTD: z12.int().nonnegative(),
795
+ requestsMTD: z12.int().nonnegative(),
796
+ /** Estimated month-to-date spend in USD across all apps. */
797
+ costMTD: z12.number().nonnegative(),
798
+ /** Distinct `userOid`s seen month-to-date. */
799
+ activeUsers: z12.int().nonnegative()
800
+ }),
801
+ /** Token + cost share by capability over the range (essentially all `llm` in M4). */
802
+ capabilityMix: z12.array(
803
+ z12.object({
804
+ capability: z12.string(),
805
+ tokens: z12.int().nonnegative(),
806
+ /** Estimated spend in USD for this capability over the range. */
807
+ costUsd: z12.number().nonnegative()
808
+ })
809
+ )
810
+ });
811
+
812
+ // ../shared/src/data.ts
813
+ import { z as z14 } from "zod";
814
+
815
+ // ../shared/src/env.ts
816
+ import { z as z13 } from "zod";
817
+ var ENVS = ["prod", "dev"];
818
+ var EnvSchema = z13.enum(ENVS);
819
+
820
+ // ../shared/src/data.ts
821
+ var CollectionItemSchema = z14.object({
822
+ id: z14.uuid(),
823
+ collection: z14.string(),
824
+ /**
825
+ * Which tier collected the row (dev-mode §5). The runtime roles are RLS-pinned
826
+ * to one tier each, but the portal reads across both, so the discriminator has
827
+ * to travel — otherwise a developer's dev-mode test submissions are
828
+ * indistinguishable from real prod leads in the drain.
829
+ */
830
+ env: EnvSchema,
831
+ /** The submitting user, if authenticated; null for anonymous/public visitors. */
832
+ userOid: z14.string().nullable(),
833
+ item: z14.unknown(),
834
+ /** Hashed IP / truncated UA for abuse triage; null if not captured. */
835
+ meta: z14.unknown().nullable(),
836
+ createdAt: z14.iso.datetime()
837
+ });
838
+ var CollectionItemsPageSchema = z14.object({
839
+ rows: z14.array(CollectionItemSchema),
840
+ /** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
841
+ nextBefore: z14.iso.datetime().optional()
842
+ });
843
+ var CollectionSummarySchema = z14.object({
844
+ name: z14.string(),
845
+ env: EnvSchema,
846
+ count: z14.int().nonnegative(),
847
+ /** Newest row's timestamp; null only if the group is somehow empty. */
848
+ lastAt: z14.iso.datetime().nullable()
849
+ });
850
+
851
+ // ../shared/src/collectionTable.ts
852
+ var BOM = String.fromCharCode(65279);
853
+
854
+ // ../shared/src/secrets.ts
855
+ import { z as z15 } from "zod";
856
+ var FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
857
+ "host",
858
+ "content-length",
859
+ "transfer-encoding",
860
+ "connection",
861
+ "te",
862
+ "upgrade",
863
+ "expect",
864
+ "trailer"
865
+ ]);
866
+ var HEADER_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
867
+ var HEADER_VALUE_SAFE = /^[\t\x20-\x7e]*$/;
868
+ var HEADER_VALUE_WIRE = /^[\t\x20-\x7e\x80-\xff]*$/;
869
+ var headerName = (from) => {
870
+ const base = z15.string().min(1);
871
+ return (from === "request" ? base.max(64).regex(HEADER_TOKEN, "must be an RFC 7230 token") : base).transform((n) => n.trim().toLowerCase()).refine((n) => !FORBIDDEN_HEADER_NAMES.has(n), "reserved header name").refine((n) => !n.startsWith("x-helix-"), "reserved header prefix");
872
+ };
873
+ var headerTemplate = (from) => from === "request" ? z15.string().max(512).regex(HEADER_VALUE_SAFE) : z15.string().regex(HEADER_VALUE_WIRE);
874
+ var injectionRecipe = (from) => z15.discriminatedUnion("kind", [
875
+ /** `Authorization: Bearer <secret>` — the common case. */
876
+ z15.object({ kind: z15.literal("header-bearer") }),
877
+ /** Arbitrary header; `{}` in `template` is replaced with the secret. */
878
+ z15.object({
879
+ kind: z15.literal("header"),
880
+ name: headerName(from),
881
+ template: headerTemplate(from).default("{}")
882
+ }),
883
+ /** Query parameter `?<param>=<secret>`. */
884
+ z15.object({ kind: z15.literal("query"), param: z15.string().min(1) }),
885
+ /**
886
+ * HMAC over a timestamp. The signed input is the timestamp string **alone** —
887
+ * not the method, path, query, or body — so injection is a pure function of
888
+ * (private key, now) and needs no request context.
889
+ *
890
+ * The canonical form lives in the KIND NAME, deliberately. A scheme that signs
891
+ * method+path+body is a *sibling kind* (`hmac-request`) — a code change with
892
+ * tests, reviewed — never an admin-editable canonical-string template.
893
+ * Canonicalization is where implementations of this family go wrong, and it
894
+ * does not belong in a text box.
895
+ *
896
+ * SHA-256, lowercase-hex, and ISO-8601-with-milliseconds are fixed rather than
897
+ * configurable. Each would carry a default, and `app_secrets.injection` is a
898
+ * schemaless JSON column, so adding a knob later is purely additive with no
899
+ * migration — while fixing them now removes the weak-algorithm and
900
+ * unencodable-digest failure classes outright.
901
+ *
902
+ * The stored value is a JSON blob carrying both halves of the key pair
903
+ * ({@link HmacCredentialSchema}): regenerating the pair changes both, and a
904
+ * blob rotates atomically through the existing rotate route.
905
+ */
906
+ z15.object({
907
+ kind: z15.literal("hmac-timestamp"),
908
+ /** Header carrying the timestamp that is also the entire signed input. */
909
+ timestampHeader: headerName(from),
910
+ /** Header carrying the rendered credential + signature. */
911
+ authHeader: headerName(from).default("authorization"),
912
+ /**
913
+ * Value written to `authHeader`. `{credential}` and `{signature}` are
914
+ * substituted. Named rather than the `header` kind's bare `{}` because there
915
+ * are two substitutions: the convention is one value ⇒ `{}`, more than one ⇒
916
+ * named placeholders. Positional `{}` here would let a swapped template
917
+ * produce a well-formed header that silently fails to authenticate.
918
+ *
919
+ * Both placeholders are required. This recipe writes exactly two headers,
920
+ * one of them the timestamp, so there is no configuration where the public
921
+ * credential id travels elsewhere — omitting `{credential}` means the
922
+ * upstream can never identify the key and every call 401s.
923
+ */
924
+ template: headerTemplate(from).refine((t) => t.includes("{signature}"), "template must contain {signature}").refine((t) => t.includes("{credential}"), "template must contain {credential}")
925
+ }).refine(
926
+ (r) => r.timestampHeader !== r.authHeader,
927
+ "timestampHeader and authHeader must differ \u2014 the second write overwrites the first"
928
+ )
929
+ ]);
930
+ var InjectionRecipeSchema = injectionRecipe("request");
931
+ var StoredInjectionRecipeSchema = injectionRecipe("stored");
932
+ var HmacCredentialSchema = z15.object({
933
+ credential: z15.string().min(1),
934
+ key: z15.string().min(1)
935
+ });
936
+ var SECRET_SCOPES = ["app", "global", "platform"];
937
+ var SecretScopeSchema = z15.enum(SECRET_SCOPES);
938
+ var SecretMetadataSchema = z15.object({
939
+ id: z15.string(),
940
+ name: z15.string().min(1),
941
+ scope: SecretScopeSchema,
942
+ /** Partition tier (dev-mode §6): a dev fetch injects only `dev` connection secrets. */
943
+ env: EnvSchema,
944
+ /**
945
+ * The **stored** parser, not the strict one — this field describes a row, and
946
+ * the SPA re-parses this schema on every response. Using the strict parser here
947
+ * would let the portal return a name the browser then refuses, moving the read
948
+ * failure from the server to the client, where it presents as a dead page.
949
+ *
950
+ * `null` = the stored recipe is unreadable (it names a reserved header, or is
951
+ * not a recipe at all). The credential still exists and is still deletable; it
952
+ * cannot be rotated, and egress fails its hop closed. Recipes are immutable by
953
+ * design, so recovery is delete-and-recreate.
954
+ */
955
+ injection: StoredInjectionRecipeSchema.nullable(),
956
+ createdBy: z15.string(),
957
+ createdAt: z15.string(),
958
+ rotatedAt: z15.string().nullable().optional(),
959
+ lastUsedAt: z15.string().nullable().optional(),
960
+ boundApps: z15.array(z15.string()).default([])
961
+ });
962
+ var SecretCreateRequestSchema = z15.object({
963
+ name: z15.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and hyphens"),
964
+ value: z15.string().min(1),
965
+ /** Target tier (dev-mode §6). Defaults `prod`; `dev` configures a dev-tier credential. */
966
+ env: EnvSchema.default("prod"),
967
+ injection: InjectionRecipeSchema.default({ kind: "header-bearer" })
968
+ });
969
+ var SecretRotateRequestSchema = z15.object({ value: z15.string().min(1) });
970
+ var SecretGrantRequestSchema = z15.object({ appSlug: z15.string().min(1) });
971
+
972
+ // ../shared/src/devTokens.ts
973
+ import { z as z16 } from "zod";
974
+ function isValidDevOrigin(input) {
975
+ if (input.includes("*")) return false;
976
+ let url;
977
+ try {
978
+ url = new URL(input);
979
+ } catch {
980
+ return false;
981
+ }
982
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
983
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return false;
984
+ if (url.username !== "" || url.password !== "") return false;
985
+ return input === url.origin || input === `${url.origin}/`;
986
+ }
987
+ var DevOriginSchema = z16.string().refine(
988
+ isValidDevOrigin,
989
+ "must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
990
+ ).transform((o) => new URL(o).origin);
991
+ var TtlDaysSchema = z16.number().int().min(1).max(365).optional();
992
+ var DevTokenMintRequestSchema = z16.object({
993
+ origins: z16.array(DevOriginSchema).min(1).max(20),
994
+ ttlDays: TtlDaysSchema
995
+ });
996
+ var DevTokenRotateRequestSchema = z16.object({
997
+ ttlDays: TtlDaysSchema
998
+ });
999
+ var DevTokenMetadataSchema = z16.object({
1000
+ id: z16.string(),
1001
+ developerOid: z16.string(),
1002
+ origins: z16.array(z16.string()),
1003
+ expiresAt: z16.string(),
1004
+ revokedAt: z16.string().nullable().optional(),
1005
+ createdAt: z16.string()
1006
+ });
1007
+ var DevTokenMintResponseSchema = z16.object({
1008
+ token: z16.string(),
1009
+ metadata: DevTokenMetadataSchema
1010
+ });
1011
+
1012
+ // ../shared/src/instruction.ts
1013
+ import { z as z17 } from "zod";
1014
+ var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
1015
+ var InstructionCapabilitySchema = z17.enum(INSTRUCTION_CAPABILITIES);
1016
+ var AttestedInstructionSchema = z17.object({
1017
+ /** App the call is attributed to (registry app id). */
1018
+ appId: z17.string().min(1),
1019
+ /** Authenticated user, or the anonymous sentinel on `public` apps. */
1020
+ userOid: z17.string().min(1),
1021
+ capability: InstructionCapabilitySchema,
1022
+ /** The allowlisted origin the edge authorized (scheme + host + port). */
1023
+ origin: z17.url(),
1024
+ /** Connection (secret) name to inject, if this is a secret-backed call. */
1025
+ connection: z17.string().min(1).optional(),
1026
+ /** Correlates the edge audit row with the egress call. */
1027
+ requestId: z17.string().min(1),
1028
+ /**
1029
+ * The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
1030
+ * Egress refuses a mismatched verb/resource, so a captured instruction can't be
1031
+ * redirected to a different request on the same origin (origin is already bound
1032
+ * above; the `jti` burn already blocks replay — this closes the residual
1033
+ * same-origin-different-request gap). Bound to `pathname` only (not query): the
1034
+ * app controls the query, and origin + jti already constrain the call.
1035
+ *
1036
+ * Optional for rollout safety (like `env`): within one instance edge+egress
1037
+ * deploy together and instructions live 30 s, but a rolling restart may briefly
1038
+ * verify an old-edge token that lacks these. Only the edge can sign, so absence
1039
+ * means "old edge", not tampering — egress asserts ONLY when the claim is
1040
+ * present. Make required once a fleet is reliably past deploy.
1041
+ */
1042
+ method: z17.string().min(1).optional(),
1043
+ path: z17.string().optional(),
1044
+ /**
1045
+ * Environment tier this call is scoped to (dev-mode design §6). Egress resolves
1046
+ * the connection secret within this tier — a `dev` instruction can never reach a
1047
+ * `prod` connection secret and vice-versa. Carried by the attested (signed)
1048
+ * claim, never an app/request parameter; defaults `prod` so any instruction
1049
+ * minted before this field existed verifies as production.
1050
+ */
1051
+ env: EnvSchema.default("prod")
1052
+ });
1053
+ var INSTRUCTION_TTL_SECONDS = 30;
1054
+ var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
1055
+
1056
+ // ../shared/src/fetch.ts
1057
+ import { z as z18 } from "zod";
1058
+ var FETCH_ERROR_CODES = [
1059
+ "forbidden",
1060
+ "rate_limited",
1061
+ "bad_target",
1062
+ "blocked",
1063
+ "too_large",
1064
+ "replay",
1065
+ "upstream_error"
1066
+ ];
1067
+ var FetchErrorCodeSchema = z18.enum(FETCH_ERROR_CODES);
1068
+ var FetchProxyErrorSchema = z18.object({
1069
+ code: FetchErrorCodeSchema,
1070
+ message: z18.string()
1071
+ });
1072
+
1073
+ // src/client.ts
1074
+ var CliError = class extends Error {
1075
+ code;
1076
+ constructor(message, code) {
1077
+ super(message);
1078
+ this.name = "CliError";
1079
+ this.code = code;
1080
+ }
1081
+ };
1082
+ var VersionListSchema = z19.array(VersionSchema);
1083
+ var PortalClient = class {
1084
+ #baseUrl;
1085
+ #tokenProvider;
1086
+ /** `token` may be a static string (dev token) or an async provider (OIDC). */
1087
+ constructor(baseUrl, token) {
1088
+ this.#baseUrl = baseUrl.replace(/\/+$/, "");
1089
+ this.#tokenProvider = typeof token === "string" ? async () => token : token;
1090
+ }
1091
+ createApp(input) {
1092
+ return this.#json(AppSchema, "POST", "/api/v1/apps", { auth: true, body: input });
1093
+ }
1094
+ /** Public IdP discovery info — how `helix login` finds the issuer. */
1095
+ getAuthConfig() {
1096
+ return this.#json(AuthConfigResponseSchema, "GET", "/api/v1/auth/config", {});
1097
+ }
1098
+ /** The authenticated actor, per the portal — powers `helix whoami`. */
1099
+ me() {
1100
+ return this.#json(PortalMeResponseSchema, "GET", "/api/v1/me", { auth: true });
1101
+ }
1102
+ listVersions(slug) {
1103
+ return this.#json(VersionListSchema, "GET", `/api/v1/apps/${enc(slug)}/versions`, {
1104
+ auth: true
1105
+ });
1106
+ }
1107
+ promote(slug, number) {
1108
+ return this.#json(AppSchema, "POST", `/api/v1/apps/${enc(slug)}/versions/${number}/promote`, {
1109
+ auth: true
1110
+ });
1111
+ }
1112
+ rollback(slug, toNumber) {
1113
+ return this.#json(AppSchema, "POST", `/api/v1/apps/${enc(slug)}/rollback`, {
1114
+ auth: true,
1115
+ body: toNumber !== void 0 ? { toNumber } : {}
1116
+ });
1117
+ }
1118
+ async uploadVersion(slug, zip, filename = "bundle.zip") {
1119
+ const form = new FormData();
1120
+ form.append("bundle", new Blob([new Uint8Array(zip)]), filename);
1121
+ const res = await fetch(this.#url(`/api/v1/apps/${enc(slug)}/versions`), {
1122
+ method: "POST",
1123
+ headers: await this.#authHeaders(true),
1124
+ body: form
1125
+ });
1126
+ return this.#parse(UploadVersionResponseSchema, res);
1127
+ }
1128
+ #url(path3) {
1129
+ return `${this.#baseUrl}${path3}`;
1130
+ }
1131
+ async #authHeaders(auth) {
1132
+ if (!auth) return {};
1133
+ const token = await this.#tokenProvider?.();
1134
+ if (!token) {
1135
+ throw new CliError("not signed in; run `helix login` (or set HELIX_TOKEN / pass --token)");
1136
+ }
1137
+ return { authorization: `Bearer ${token}` };
1138
+ }
1139
+ async #json(schema, method, path3, opts) {
1140
+ const headers = await this.#authHeaders(opts.auth ?? false);
1141
+ if (opts.body !== void 0) headers["content-type"] = "application/json";
1142
+ const res = await fetch(this.#url(path3), {
1143
+ method,
1144
+ headers,
1145
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
1146
+ });
1147
+ return this.#parse(schema, res);
1148
+ }
1149
+ async #parse(schema, res) {
1150
+ const text = await res.text();
1151
+ const data = text ? JSON.parse(text) : void 0;
1152
+ if (!res.ok) {
1153
+ const parsed = ApiErrorSchema.safeParse(data);
1154
+ if (parsed.success) throw new CliError(parsed.data.error.message, parsed.data.error.code);
1155
+ throw new CliError(`request failed (HTTP ${res.status})`);
1156
+ }
1157
+ return schema.parse(data);
1158
+ }
1159
+ };
1160
+ function enc(segment) {
1161
+ return encodeURIComponent(segment);
1162
+ }
1163
+
1164
+ // src/commands.ts
1165
+ import { readFile as readFile2 } from "node:fs/promises";
1166
+
1167
+ // src/zip.ts
1168
+ import { ZipArchive } from "archiver";
1169
+ function zipDirectory(dir) {
1170
+ return new Promise((resolve, reject) => {
1171
+ const chunks = [];
1172
+ const archive = new ZipArchive({ zlib: { level: 9 } });
1173
+ archive.on("data", (c) => chunks.push(c));
1174
+ archive.on("error", reject);
1175
+ archive.on("end", () => resolve(Buffer.concat(chunks)));
1176
+ archive.directory(dir, false);
1177
+ void archive.finalize();
1178
+ });
1179
+ }
1180
+
1181
+ // src/auth/deviceFlow.ts
1182
+ import * as oidc from "openid-client";
1183
+ var DEVICE_SCOPES = "openid profile email offline_access";
1184
+ function deviceScopes(audience) {
1185
+ const apiScope = portalApiScope(audience);
1186
+ return apiScope ? `${DEVICE_SCOPES} ${apiScope}` : DEVICE_SCOPES;
1187
+ }
1188
+ async function discover(issuer, clientId) {
1189
+ const url = new URL(issuer);
1190
+ return oidc.discovery(
1191
+ url,
1192
+ clientId,
1193
+ void 0,
1194
+ void 0,
1195
+ // The local dev IdP is plain http on localhost; real issuers are https.
1196
+ url.protocol === "http:" ? { execute: [oidc.allowInsecureRequests] } : void 0
1197
+ );
1198
+ }
1199
+ function toStoredTokens(tokens, clientId) {
1200
+ if (!tokens.access_token) throw new Error("token response carried no access token");
1201
+ return {
1202
+ accessToken: tokens.access_token,
1203
+ refreshToken: tokens.refresh_token,
1204
+ expiresAt: Date.now() + (tokens.expires_in ?? 300) * 1e3,
1205
+ clientId
1206
+ };
1207
+ }
1208
+ async function runDeviceLogin(opts) {
1209
+ const config = await discover(opts.issuer, opts.clientId);
1210
+ const handle = await oidc.initiateDeviceAuthorization(config, {
1211
+ scope: deviceScopes(opts.audience)
1212
+ });
1213
+ opts.log("To sign in, open this URL in a browser:");
1214
+ opts.log(` ${handle.verification_uri_complete ?? handle.verification_uri}`);
1215
+ opts.log(`and confirm the code: ${handle.user_code}`);
1216
+ opts.log("Waiting for approval\u2026");
1217
+ const tokens = await oidc.pollDeviceAuthorizationGrant(config, handle);
1218
+ return toStoredTokens(tokens, opts.clientId);
1219
+ }
1220
+ async function refreshGrant(issuer, clientId, refreshToken) {
1221
+ const config = await discover(issuer, clientId);
1222
+ const tokens = await oidc.refreshTokenGrant(config, refreshToken);
1223
+ const stored = toStoredTokens(tokens, clientId);
1224
+ return { ...stored, refreshToken: stored.refreshToken ?? refreshToken };
1225
+ }
1226
+
1227
+ // src/auth/tokenStore.ts
1228
+ import { mkdir, readFile, rename, writeFile, chmod, rm } from "node:fs/promises";
1229
+ import { randomBytes } from "node:crypto";
1230
+ import os from "node:os";
1231
+ import path from "node:path";
1232
+ function portalOrigin(portalUrl) {
1233
+ return new URL(portalUrl).origin;
1234
+ }
1235
+ function defaultTokenPath(env = process.env) {
1236
+ const base = env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
1237
+ return path.join(base, "helix", "tokens.json");
1238
+ }
1239
+ async function readFileTolerant(file) {
1240
+ const empty = { version: 2, byPortal: {} };
1241
+ try {
1242
+ const parsed = JSON.parse(await readFile(file, "utf8"));
1243
+ if (parsed.version !== 2 || typeof parsed.byPortal !== "object" || !parsed.byPortal) {
1244
+ return empty;
1245
+ }
1246
+ return parsed;
1247
+ } catch {
1248
+ return empty;
1249
+ }
1250
+ }
1251
+ async function readTokens(key, file = defaultTokenPath()) {
1252
+ const data = await readFileTolerant(file);
1253
+ const entry = data.byPortal[portalOrigin(key.portalUrl)];
1254
+ if (!entry || entry.issuer !== key.issuer) return void 0;
1255
+ const tokens = entry.tokens;
1256
+ if (!tokens || typeof tokens.accessToken !== "string" || typeof tokens.expiresAt !== "number") {
1257
+ return void 0;
1258
+ }
1259
+ return tokens;
1260
+ }
1261
+ async function writeFile0600(file, content) {
1262
+ await mkdir(path.dirname(file), { recursive: true, mode: 448 });
1263
+ const tmp = `${file}.${randomBytes(4).toString("hex")}.tmp`;
1264
+ await writeFile(tmp, content, { mode: 384 });
1265
+ await rename(tmp, file);
1266
+ await chmod(file, 384);
1267
+ }
1268
+ async function writeTokens(key, tokens, file = defaultTokenPath(), opts = {}) {
1269
+ const data = await readFileTolerant(file);
1270
+ data.byPortal[portalOrigin(key.portalUrl)] = {
1271
+ issuer: key.issuer,
1272
+ ...opts.audience ? { audience: opts.audience } : {},
1273
+ tokens
1274
+ };
1275
+ await writeFile0600(file, JSON.stringify(data, null, 2));
1276
+ }
1277
+ async function deleteTokens(portalUrl, file = defaultTokenPath()) {
1278
+ const data = await readFileTolerant(file);
1279
+ const origin = portalOrigin(portalUrl);
1280
+ if (!(origin in data.byPortal)) return false;
1281
+ delete data.byPortal[origin];
1282
+ if (Object.keys(data.byPortal).length === 0) {
1283
+ await rm(file, { force: true });
1284
+ } else {
1285
+ await writeFile0600(file, JSON.stringify(data, null, 2));
1286
+ }
1287
+ return true;
1288
+ }
1289
+
1290
+ // src/commands.ts
1291
+ function requireSlug(config) {
1292
+ if (!config.slug) {
1293
+ throw new CliError("no app slug; set it in helix.json or pass --slug");
1294
+ }
1295
+ return config.slug;
1296
+ }
1297
+ function parseVisibility(input) {
1298
+ if (!input) return void 0;
1299
+ if (input.startsWith("group:")) {
1300
+ const groupId = input.slice("group:".length);
1301
+ if (!groupId) throw new CliError("group visibility needs an id: group:<id>");
1302
+ return { mode: "group", groupId };
1303
+ }
1304
+ if (input === "internal" || input === "password" || input === "public") {
1305
+ return { mode: input };
1306
+ }
1307
+ if (input === "private") {
1308
+ throw new CliError(
1309
+ 'visibility "private" was renamed to "internal" (the mode never checked which user signed in, only that someone had). Use --visibility internal; the name "private" is reserved for a future owner-only mode, so it is not accepted as an alias.'
1310
+ );
1311
+ }
1312
+ throw new CliError(`invalid visibility "${input}" (internal | group:<id> | password | public)`);
1313
+ }
1314
+ function printVersion(v) {
1315
+ console.log(` version ${v.number} (${v.status}) \u2014 ${v.id}`);
1316
+ console.log(` assets: ${v.blobPrefix}`);
1317
+ }
1318
+ function printApp(app) {
1319
+ console.log(` app ${app.slug} \u2014 live version: ${app.currentVersionId ?? "(none)"}`);
1320
+ }
1321
+ async function createCommand(client, config, opts) {
1322
+ const slug = requireSlug(config);
1323
+ const app = await client.createApp({
1324
+ slug,
1325
+ displayName: opts.displayName ?? slug,
1326
+ visibility: parseVisibility(opts.visibility)
1327
+ });
1328
+ console.log(`Created app "${app.slug}".`);
1329
+ printApp(app);
1330
+ }
1331
+ async function deployCommand(client, config, opts) {
1332
+ const slug = requireSlug(config);
1333
+ const zip = config.bundle ? await readFile2(config.bundle) : await zipDirectory(config.dir);
1334
+ console.log(`Uploading bundle to "${slug}"\u2026`);
1335
+ const { version, warnings } = await client.uploadVersion(slug, zip);
1336
+ console.log(`Uploaded as preview:`);
1337
+ printVersion(version);
1338
+ if (warnings.length > 0) {
1339
+ console.log(`
1340
+ ${warnings.length} CSP warning(s):`);
1341
+ for (const w of warnings) console.log(` - ${w.file}: ${w.hint}`);
1342
+ }
1343
+ if (opts.promote) {
1344
+ const app = await client.promote(slug, version.number);
1345
+ console.log(`
1346
+ Promoted version ${version.number} to live.`);
1347
+ printApp(app);
1348
+ } else {
1349
+ console.log(`
1350
+ Not live yet \u2014 promote with: helix promote ${version.number}`);
1351
+ }
1352
+ }
1353
+ async function versionsCommand(client, config) {
1354
+ const slug = requireSlug(config);
1355
+ const versions = await client.listVersions(slug);
1356
+ if (versions.length === 0) {
1357
+ console.log(`No versions for "${slug}".`);
1358
+ return;
1359
+ }
1360
+ console.log(`Versions for "${slug}" (newest first):`);
1361
+ for (const v of versions) console.log(` ${v.number} ${v.status} ${v.id}`);
1362
+ }
1363
+ async function promoteCommand(client, config, number) {
1364
+ const slug = requireSlug(config);
1365
+ const app = await client.promote(slug, number);
1366
+ console.log(`Promoted version ${number} to live.`);
1367
+ printApp(app);
1368
+ }
1369
+ async function rollbackCommand(client, config, toNumber) {
1370
+ const slug = requireSlug(config);
1371
+ const app = await client.rollback(slug, toNumber);
1372
+ console.log(
1373
+ toNumber ? `Rolled back to version ${toNumber}.` : `Rolled back to previous version.`
1374
+ );
1375
+ printApp(app);
1376
+ }
1377
+ async function loginCommand(client, config) {
1378
+ const { issuer, cliClientId, audience } = await client.getAuthConfig();
1379
+ const tokens = await runDeviceLogin({
1380
+ issuer,
1381
+ clientId: cliClientId,
1382
+ audience,
1383
+ log: console.log
1384
+ });
1385
+ await writeTokens({ portalUrl: config.portalUrl, issuer }, tokens, void 0, { audience });
1386
+ const authed = new PortalClient(config.portalUrl, tokens.accessToken);
1387
+ const me = await authed.me();
1388
+ console.log(`Logged in as ${me.name ?? me.sub} (${me.sub}).`);
1389
+ }
1390
+ async function logoutCommand(config) {
1391
+ const forgot = await deleteTokens(config.portalUrl);
1392
+ console.log(forgot ? "Logged out (local tokens forgotten)." : "Already logged out.");
1393
+ }
1394
+ async function whoamiCommand(client) {
1395
+ const me = await client.me();
1396
+ console.log(`${me.sub} (via ${me.via}${me.name ? `, ${me.name}` : ""})`);
1397
+ }
1398
+
1399
+ // src/auth/session.ts
1400
+ var REFRESH_MARGIN_MS = 6e4;
1401
+ async function fetchAuthConfig(portalUrl) {
1402
+ const res = await fetch(`${portalUrl.replace(/\/+$/, "")}/api/v1/auth/config`);
1403
+ if (!res.ok) {
1404
+ throw new Error(
1405
+ `portal has no OIDC configured (GET /api/v1/auth/config \u2192 ${res.status}); use HELIX_TOKEN / --token instead`
1406
+ );
1407
+ }
1408
+ return AuthConfigResponseSchema.parse(await res.json());
1409
+ }
1410
+ var defaultDeps = {
1411
+ getAuthConfig: fetchAuthConfig,
1412
+ refresh: refreshGrant,
1413
+ storePath: defaultTokenPath()
1414
+ };
1415
+ function makeTokenProvider(opts, deps = defaultDeps) {
1416
+ return async function getAccessToken() {
1417
+ if (opts.staticToken) return opts.staticToken;
1418
+ const { issuer, cliClientId } = await deps.getAuthConfig(opts.portalUrl);
1419
+ const key = { portalUrl: opts.portalUrl, issuer };
1420
+ const stored = await readTokens(key, deps.storePath);
1421
+ if (!stored) return void 0;
1422
+ if (stored.expiresAt - REFRESH_MARGIN_MS > Date.now()) {
1423
+ return stored.accessToken;
1424
+ }
1425
+ if (!stored.refreshToken) return void 0;
1426
+ try {
1427
+ const renewed = await deps.refresh(
1428
+ issuer,
1429
+ stored.clientId || cliClientId,
1430
+ stored.refreshToken
1431
+ );
1432
+ await writeTokens(key, renewed, deps.storePath);
1433
+ return renewed.accessToken;
1434
+ } catch {
1435
+ return void 0;
1436
+ }
1437
+ };
1438
+ }
1439
+
1440
+ // src/config.ts
1441
+ import { readFile as readFile3 } from "node:fs/promises";
1442
+ import path2 from "node:path";
1443
+ var DEFAULT_PORTAL_URL = "http://localhost:3001";
1444
+ var DEFAULT_DIR = "dist";
1445
+ var CONFIG_FILENAMES = ["helix.json", "azx.json"];
1446
+ async function readConfigFile(cwd) {
1447
+ for (const name of CONFIG_FILENAMES) {
1448
+ try {
1449
+ return JSON.parse(await readFile3(path2.join(cwd, name), "utf8"));
1450
+ } catch (err) {
1451
+ if (err.code === "ENOENT") continue;
1452
+ throw err;
1453
+ }
1454
+ }
1455
+ return {};
1456
+ }
1457
+ async function resolveConfig(flags, env = process.env, cwd = process.cwd()) {
1458
+ const file = await readConfigFile(cwd);
1459
+ return {
1460
+ portalUrl: flags.portalUrl ?? env.HELIX_PORTAL_URL ?? env.AZX_PORTAL_URL ?? file.portalUrl ?? DEFAULT_PORTAL_URL,
1461
+ token: flags.token ?? env.HELIX_TOKEN ?? env.AZX_TOKEN,
1462
+ slug: flags.slug ?? file.slug,
1463
+ dir: flags.dir ?? file.dir ?? DEFAULT_DIR,
1464
+ bundle: flags.bundle
1465
+ };
1466
+ }
1467
+
1468
+ // src/bin.ts
1469
+ var USAGE = `helix \u2014 Helix deploy CLI
1470
+
1471
+ Usage:
1472
+ helix login # sign in via the browser (OIDC device flow)
1473
+ helix logout
1474
+ helix whoami
1475
+ helix deploy [--dir <dir>] [--bundle <zip>] [--promote]
1476
+ helix create [--display-name <name>] [--visibility <v>]
1477
+ helix versions
1478
+ helix promote <number>
1479
+ helix rollback [number]
1480
+
1481
+ Common flags: --slug <slug> --portal-url <url> --token <token>
1482
+ Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; CI/scripts).
1483
+ Config file: helix.json { slug, portalUrl, dir }
1484
+ Visibility: internal | group:<id> | password | public
1485
+ `;
1486
+ async function main() {
1487
+ const { values, positionals } = parseCliArgs(process.argv.slice(2));
1488
+ const command = positionals[0];
1489
+ if (!command || values.help) {
1490
+ console.log(USAGE);
1491
+ return;
1492
+ }
1493
+ const config = await resolveConfig({
1494
+ slug: values.slug,
1495
+ portalUrl: values["portal-url"],
1496
+ dir: values.dir,
1497
+ bundle: values.bundle,
1498
+ token: values.token
1499
+ });
1500
+ const client = new PortalClient(
1501
+ config.portalUrl,
1502
+ makeTokenProvider({ portalUrl: config.portalUrl, staticToken: config.token })
1503
+ );
1504
+ switch (command) {
1505
+ case "login":
1506
+ await loginCommand(client, config);
1507
+ break;
1508
+ case "logout":
1509
+ await logoutCommand(config);
1510
+ break;
1511
+ case "whoami":
1512
+ await whoamiCommand(client);
1513
+ break;
1514
+ case "deploy":
1515
+ await deployCommand(client, config, { promote: values.promote });
1516
+ break;
1517
+ case "create":
1518
+ await createCommand(client, config, {
1519
+ displayName: values["display-name"],
1520
+ visibility: values.visibility
1521
+ });
1522
+ break;
1523
+ case "versions":
1524
+ await versionsCommand(client, config);
1525
+ break;
1526
+ case "promote": {
1527
+ const number = Number(positionals[1]);
1528
+ if (!Number.isInteger(number)) throw new CliError("usage: helix promote <number>");
1529
+ await promoteCommand(client, config, number);
1530
+ break;
1531
+ }
1532
+ case "rollback": {
1533
+ const number = positionals[1] !== void 0 ? Number(positionals[1]) : void 0;
1534
+ if (number !== void 0 && !Number.isInteger(number)) {
1535
+ throw new CliError("usage: helix rollback [number]");
1536
+ }
1537
+ await rollbackCommand(client, config, number);
1538
+ break;
1539
+ }
1540
+ default:
1541
+ throw new CliError(`unknown command "${command}"
1542
+
1543
+ ${USAGE}`);
1544
+ }
1545
+ }
1546
+ main().catch((err) => {
1547
+ if (err instanceof CliError) {
1548
+ console.error(`error: ${err.message}${err.code ? ` (${err.code})` : ""}`);
1549
+ } else {
1550
+ console.error(err);
1551
+ }
1552
+ process.exitCode = 1;
1553
+ });