@azx-pbc/helix-cli 0.1.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 (3) hide show
  1. package/README.md +7 -1
  2. package/dist/helix.js +525 -157
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,9 +51,15 @@ helix rollback [number] # revert the live p
51
51
  ```
52
52
 
53
53
  `deploy` uploads the bundle as a **preview**; `--promote` flips it live in the
54
- same step (architecture §5.1). `visibility` is `private | group:<id> | password
54
+ same step (architecture §5.1). `visibility` is `internal | group:<id> | password
55
55
  | public`.
56
56
 
57
+ > **Breaking in 0.2.0:** the `private` visibility mode was renamed to `internal`.
58
+ > It never checked _which_ user signed in, only that someone had. Passing the old
59
+ > value now **errors** rather than mapping to the new one, deliberately: the name
60
+ > is reserved for a future owner-only mode, so a silent alias would come to mean
61
+ > the opposite of what it says. Update scripts to use `internal`.
62
+
57
63
  ## Authentication (M3)
58
64
 
59
65
  Two paths, in precedence order:
package/dist/helix.js CHANGED
@@ -19,17 +19,17 @@ function parseCliArgs(argv) {
19
19
  }
20
20
 
21
21
  // src/client.ts
22
- import { z as z18 } from "zod";
22
+ import { z as z19 } from "zod";
23
23
 
24
24
  // ../shared/src/visibility.ts
25
25
  import { z } from "zod";
26
26
  var VisibilitySchema = z.discriminatedUnion("mode", [
27
- z.object({ mode: z.literal("private") }),
27
+ z.object({ mode: z.literal("internal") }),
28
28
  z.object({ mode: z.literal("group"), groupId: z.string().min(1) }),
29
29
  z.object({ mode: z.literal("password") }),
30
30
  z.object({ mode: z.literal("public") })
31
31
  ]);
32
- var VISIBILITY_MODES = ["private", "group", "password", "public"];
32
+ var VISIBILITY_MODES = ["internal", "group", "password", "public"];
33
33
  var VisibilityModeSchema = z.enum(VISIBILITY_MODES);
34
34
 
35
35
  // ../shared/src/app.ts
@@ -104,6 +104,26 @@ var FetchCapabilitySchema = z4.object({
104
104
  /** Per-app daily proxied-request budget; unset ⇒ unbounded (fetch-proxy §7). */
105
105
  requestsPerDay: z4.int().positive().optional()
106
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
+ });
107
127
  var CapabilitiesSchema = z4.object({
108
128
  llm: LlmCapabilitySchema.optional(),
109
129
  data: DataCapabilitySchema.optional(),
@@ -112,7 +132,9 @@ var CapabilitiesSchema = z4.object({
112
132
  /** Extra CSP `connect-src` origins for **direct** browser calls (§4.4). */
113
133
  externalOrigins: z4.array(z4.url()).default([]),
114
134
  /** Governed outbound HTTP via the fetch-proxy / egress plane (in build, M4.5). */
115
- fetch: FetchCapabilitySchema.optional()
135
+ fetch: FetchCapabilitySchema.optional(),
136
+ /** Platform-owned, scope-confined service worker for offline cold boot (ADR-0035). */
137
+ offline: OfflineCapabilitySchema.optional()
116
138
  });
117
139
  var AppManifestSchema = z4.object({
118
140
  /** App slug; matches `App.slug`. */
@@ -126,12 +148,84 @@ import { z as z5 } from "zod";
126
148
 
127
149
  // ../shared/src/pricing.ts
128
150
  var MODEL_PRICING = {
129
- "claude-fable-5": { inputPerMTok: 10, outputPerMTok: 50 },
130
- "claude-opus-4-8": { inputPerMTok: 5, outputPerMTok: 25 },
131
- "claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25 },
132
- "claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25 },
133
- "claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15 },
134
- "claude-haiku-4-5": { inputPerMTok: 1, outputPerMTok: 5 }
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
+ }
135
229
  };
136
230
 
137
231
  // ../shared/src/approval.ts
@@ -213,7 +307,7 @@ import { z as z7 } from "zod";
213
307
  var CreateAppRequestSchema = z7.object({
214
308
  slug: AppSchema.shape.slug,
215
309
  displayName: AppSchema.shape.displayName,
216
- visibility: VisibilitySchema.default({ mode: "private" }),
310
+ visibility: VisibilitySchema.default({ mode: "internal" }),
217
311
  /** Optional per-app capability grant set at create time (architecture §6.3). */
218
312
  capabilities: CapabilitiesSchema.optional()
219
313
  });
@@ -373,7 +467,18 @@ var DeploymentConfigResponseSchema = z9.object({
373
467
  * Display-only — the gateway is the choke point, so the rollup is exact, but
374
468
  * nothing enforces this. Absent ⇒ no ceiling shown.
375
469
  */
376
- platformMonthlyUsdCap: z9.number().positive().optional()
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()
377
482
  });
378
483
 
379
484
  // ../shared/src/scrypt.ts
@@ -400,12 +505,60 @@ var LlmUsageSchema = z10.object({
400
505
  cacheReadInputTokens: z10.int().nonnegative().default(0),
401
506
  cacheCreationInputTokens: z10.int().nonnegative().default(0)
402
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
+ });
403
543
  var LlmChatRequestSchema = z10.object({
404
544
  model: z10.string().min(1),
405
545
  messages: z10.array(LlmMessageSchema).min(1),
406
546
  /** Optional system prompt; maps to the vendor's system channel. */
407
547
  system: z10.string().optional(),
408
- maxTokens: z10.int().positive().max(128e3).default(1024),
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(),
409
562
  /** SSE streaming (default) vs a single JSON body. */
410
563
  stream: z10.boolean().default(true)
411
564
  });
@@ -425,142 +578,238 @@ var LlmStreamErrorSchema = z10.object({
425
578
  message: z10.string()
426
579
  });
427
580
 
428
- // ../shared/src/usage.ts
581
+ // ../shared/src/llmOpenai.ts
429
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";
430
697
  var GATEWAY_OUTCOMES = ["ok", "error", "refusal", "quota_blocked"];
431
- var GatewayOutcomeSchema = z11.enum(GATEWAY_OUTCOMES);
698
+ var GatewayOutcomeSchema = z12.enum(GATEWAY_OUTCOMES);
432
699
  var USAGE_RANGES = ["24h", "7d", "30d"];
433
- var UsageRangeSchema = z11.enum(USAGE_RANGES);
700
+ var UsageRangeSchema = z12.enum(USAGE_RANGES);
434
701
  var PLATFORM_RANGES = ["7d", "30d", "90d"];
435
- var PlatformRangeSchema = z11.enum(PLATFORM_RANGES);
436
- var UsageSeriesPointSchema = z11.object({
437
- bucket: z11.iso.datetime(),
438
- costUsd: z11.number().nonnegative(),
439
- tokens: z11.int().nonnegative(),
440
- requests: z11.int().nonnegative()
441
- });
442
- var UsageSummarySchema = z11.object({
443
- appId: z11.uuid(),
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(),
444
711
  /** The rolling range these figures cover. */
445
712
  range: UsageRangeSchema,
446
- requests: z11.int().nonnegative(),
447
- inputTokens: z11.int().nonnegative(),
448
- outputTokens: z11.int().nonnegative(),
713
+ requests: z12.int().nonnegative(),
714
+ inputTokens: z12.int().nonnegative(),
715
+ outputTokens: z12.int().nonnegative(),
449
716
  /** Cache-aware input token totals (0 until prompt caching is enabled). */
450
- cacheReadInputTokens: z11.int().nonnegative(),
451
- cacheCreationInputTokens: z11.int().nonnegative(),
717
+ cacheReadInputTokens: z12.int().nonnegative(),
718
+ cacheCreationInputTokens: z12.int().nonnegative(),
452
719
  /** Estimated spend in USD over the window at current rates (./pricing.ts). */
453
- costUsd: z11.number().nonnegative(),
720
+ costUsd: z12.number().nonnegative(),
454
721
  /** 95th-percentile upstream latency (ms) over the window; null when no timed calls. */
455
- latencyP95Ms: z11.number().nonnegative().nullable(),
722
+ latencyP95Ms: z12.number().nonnegative().nullable(),
456
723
  /** Fraction of calls in the window whose outcome was not `ok` (0..1). */
457
- errorRate: z11.number().min(0).max(1),
724
+ errorRate: z12.number().min(0).max(1),
458
725
  /** Count of calls keyed by outcome (`ok` / `error` / `refusal` / `quota_blocked`). */
459
- byOutcome: z11.record(z11.string(), z11.int().nonnegative()),
460
- byModel: z11.array(
461
- z11.object({
462
- model: z11.string(),
463
- tokens: z11.int().nonnegative(),
464
- requests: z11.int().nonnegative(),
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(),
465
732
  /** Estimated spend in USD for this model over the window. */
466
- costUsd: z11.number().nonnegative()
733
+ costUsd: z12.number().nonnegative()
467
734
  })
468
735
  ),
469
736
  /** Dense, zero-filled buckets across the range, oldest-first, for the trend chart. */
470
- series: z11.array(UsageSeriesPointSchema),
737
+ series: z12.array(UsageSeriesPointSchema),
471
738
  /**
472
739
  * Today-since-midnight totals, independent of `range` — backs the daily-cap
473
740
  * gauge (the budget the edge enforces is per calendar day).
474
741
  */
475
- today: z11.object({
476
- tokens: z11.int().nonnegative(),
477
- costUsd: z11.number().nonnegative()
742
+ today: z12.object({
743
+ tokens: z12.int().nonnegative(),
744
+ costUsd: z12.number().nonnegative()
478
745
  })
479
746
  });
480
- var GatewayCallSchema = z11.object({
481
- id: z11.uuid(),
482
- appId: z11.uuid(),
747
+ var GatewayCallSchema = z12.object({
748
+ id: z12.uuid(),
749
+ appId: z12.uuid(),
483
750
  /** App slug at read time; null when the app row no longer exists. */
484
- slug: z11.string().nullable(),
485
- userOid: z11.string(),
486
- capability: z11.string(),
487
- model: z11.string(),
488
- inputTokens: z11.int().nonnegative(),
489
- outputTokens: z11.int().nonnegative(),
490
- cacheReadInputTokens: z11.int().nonnegative(),
491
- cacheCreationInputTokens: z11.int().nonnegative(),
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(),
492
759
  /** Estimated spend in USD for this single call at current rates. */
493
- costUsd: z11.number().nonnegative(),
760
+ costUsd: z12.number().nonnegative(),
494
761
  /** Upstream round-trip latency in ms (0 when not measured). */
495
- durationMs: z11.int().nonnegative(),
762
+ durationMs: z12.int().nonnegative(),
496
763
  /** Upstream/egress HTTP status — set for `fetch`; null for streamed `llm`. */
497
- statusCode: z11.int().nullable(),
764
+ statusCode: z12.int().nullable(),
498
765
  /** LLM stop reason; null for non-LLM calls. */
499
- stopReason: z11.string().nullable(),
766
+ stopReason: z12.string().nullable(),
500
767
  /** Short upstream error string; null on success. */
501
- errorDetail: z11.string().nullable(),
768
+ errorDetail: z12.string().nullable(),
502
769
  outcome: GatewayOutcomeSchema,
503
- createdAt: z11.iso.datetime()
770
+ createdAt: z12.iso.datetime()
504
771
  });
505
- var GatewayAuditPageSchema = z11.object({
506
- rows: z11.array(GatewayCallSchema),
772
+ var GatewayAuditPageSchema = z12.object({
773
+ rows: z12.array(GatewayCallSchema),
507
774
  /** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
508
- nextBefore: z11.iso.datetime().optional()
775
+ nextBefore: z12.iso.datetime().optional()
509
776
  });
510
- var PlatformUsageSchema = z11.object({
777
+ var PlatformUsageSchema = z12.object({
511
778
  /** The rolling range the series + breakdowns cover. */
512
779
  range: PlatformRangeSchema,
513
780
  /** Dense, zero-filled daily buckets across the range, oldest-first. */
514
- series: z11.array(UsageSeriesPointSchema),
781
+ series: z12.array(UsageSeriesPointSchema),
515
782
  /** Per-app rollup over the range, busiest-first. */
516
- byApp: z11.array(
517
- z11.object({
518
- slug: z11.string().nullable(),
519
- tokens: z11.int().nonnegative(),
520
- requests: z11.int().nonnegative(),
783
+ byApp: z12.array(
784
+ z12.object({
785
+ slug: z12.string().nullable(),
786
+ tokens: z12.int().nonnegative(),
787
+ requests: z12.int().nonnegative(),
521
788
  /** Estimated spend in USD for this app over the range. */
522
- costUsd: z11.number().nonnegative()
789
+ costUsd: z12.number().nonnegative()
523
790
  })
524
791
  ),
525
792
  /** Month-to-date headline KPIs (independent of `range`). */
526
- totals: z11.object({
527
- tokensMTD: z11.int().nonnegative(),
528
- requestsMTD: z11.int().nonnegative(),
793
+ totals: z12.object({
794
+ tokensMTD: z12.int().nonnegative(),
795
+ requestsMTD: z12.int().nonnegative(),
529
796
  /** Estimated month-to-date spend in USD across all apps. */
530
- costMTD: z11.number().nonnegative(),
797
+ costMTD: z12.number().nonnegative(),
531
798
  /** Distinct `userOid`s seen month-to-date. */
532
- activeUsers: z11.int().nonnegative()
799
+ activeUsers: z12.int().nonnegative()
533
800
  }),
534
801
  /** Token + cost share by capability over the range (essentially all `llm` in M4). */
535
- capabilityMix: z11.array(
536
- z11.object({
537
- capability: z11.string(),
538
- tokens: z11.int().nonnegative(),
802
+ capabilityMix: z12.array(
803
+ z12.object({
804
+ capability: z12.string(),
805
+ tokens: z12.int().nonnegative(),
539
806
  /** Estimated spend in USD for this capability over the range. */
540
- costUsd: z11.number().nonnegative()
807
+ costUsd: z12.number().nonnegative()
541
808
  })
542
809
  )
543
810
  });
544
811
 
545
812
  // ../shared/src/data.ts
546
- import { z as z12 } from "zod";
547
- var CollectionItemSchema = z12.object({
548
- id: z12.uuid(),
549
- collection: z12.string(),
550
- /** The submitting user, if authenticated; null for anonymous/public visitors. */
551
- userOid: z12.string().nullable(),
552
- item: z12.unknown(),
553
- /** Hashed IP / truncated UA for abuse triage; null if not captured. */
554
- meta: z12.unknown().nullable(),
555
- createdAt: z12.iso.datetime()
556
- });
557
- var CollectionItemsPageSchema = z12.object({
558
- rows: z12.array(CollectionItemSchema),
559
- /** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
560
- nextBefore: z12.iso.datetime().optional()
561
- });
562
-
563
- // ../shared/src/secrets.ts
564
813
  import { z as z14 } from "zod";
565
814
 
566
815
  // ../shared/src/env.ts
@@ -568,46 +817,160 @@ import { z as z13 } from "zod";
568
817
  var ENVS = ["prod", "dev"];
569
818
  var EnvSchema = z13.enum(ENVS);
570
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
+
571
854
  // ../shared/src/secrets.ts
572
- var InjectionRecipeSchema = z14.discriminatedUnion("kind", [
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", [
573
875
  /** `Authorization: Bearer <secret>` — the common case. */
574
- z14.object({ kind: z14.literal("header-bearer") }),
876
+ z15.object({ kind: z15.literal("header-bearer") }),
575
877
  /** Arbitrary header; `{}` in `template` is replaced with the secret. */
576
- z14.object({
577
- kind: z14.literal("header"),
578
- name: z14.string().min(1),
579
- template: z14.string().default("{}")
878
+ z15.object({
879
+ kind: z15.literal("header"),
880
+ name: headerName(from),
881
+ template: headerTemplate(from).default("{}")
580
882
  }),
581
883
  /** Query parameter `?<param>=<secret>`. */
582
- z14.object({ kind: z14.literal("query"), param: z14.string().min(1) })
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
+ )
583
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
+ });
584
936
  var SECRET_SCOPES = ["app", "global", "platform"];
585
- var SecretScopeSchema = z14.enum(SECRET_SCOPES);
586
- var SecretMetadataSchema = z14.object({
587
- id: z14.string(),
588
- name: z14.string().min(1),
937
+ var SecretScopeSchema = z15.enum(SECRET_SCOPES);
938
+ var SecretMetadataSchema = z15.object({
939
+ id: z15.string(),
940
+ name: z15.string().min(1),
589
941
  scope: SecretScopeSchema,
590
942
  /** Partition tier (dev-mode §6): a dev fetch injects only `dev` connection secrets. */
591
943
  env: EnvSchema,
592
- injection: InjectionRecipeSchema,
593
- createdBy: z14.string(),
594
- createdAt: z14.string(),
595
- rotatedAt: z14.string().nullable().optional(),
596
- lastUsedAt: z14.string().nullable().optional(),
597
- boundApps: z14.array(z14.string()).default([])
598
- });
599
- var SecretCreateRequestSchema = z14.object({
600
- name: z14.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and hyphens"),
601
- value: z14.string().min(1),
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),
602
965
  /** Target tier (dev-mode §6). Defaults `prod`; `dev` configures a dev-tier credential. */
603
966
  env: EnvSchema.default("prod"),
604
967
  injection: InjectionRecipeSchema.default({ kind: "header-bearer" })
605
968
  });
606
- var SecretRotateRequestSchema = z14.object({ value: z14.string().min(1) });
607
- var SecretGrantRequestSchema = z14.object({ appSlug: z14.string().min(1) });
969
+ var SecretRotateRequestSchema = z15.object({ value: z15.string().min(1) });
970
+ var SecretGrantRequestSchema = z15.object({ appSlug: z15.string().min(1) });
608
971
 
609
972
  // ../shared/src/devTokens.ts
610
- import { z as z15 } from "zod";
973
+ import { z as z16 } from "zod";
611
974
  function isValidDevOrigin(input) {
612
975
  if (input.includes("*")) return false;
613
976
  let url;
@@ -621,47 +984,47 @@ function isValidDevOrigin(input) {
621
984
  if (url.username !== "" || url.password !== "") return false;
622
985
  return input === url.origin || input === `${url.origin}/`;
623
986
  }
624
- var DevOriginSchema = z15.string().refine(
987
+ var DevOriginSchema = z16.string().refine(
625
988
  isValidDevOrigin,
626
989
  "must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
627
990
  ).transform((o) => new URL(o).origin);
628
- var TtlDaysSchema = z15.number().int().min(1).max(365).optional();
629
- var DevTokenMintRequestSchema = z15.object({
630
- origins: z15.array(DevOriginSchema).min(1).max(20),
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),
631
994
  ttlDays: TtlDaysSchema
632
995
  });
633
- var DevTokenRotateRequestSchema = z15.object({
996
+ var DevTokenRotateRequestSchema = z16.object({
634
997
  ttlDays: TtlDaysSchema
635
998
  });
636
- var DevTokenMetadataSchema = z15.object({
637
- id: z15.string(),
638
- developerOid: z15.string(),
639
- origins: z15.array(z15.string()),
640
- expiresAt: z15.string(),
641
- revokedAt: z15.string().nullable().optional(),
642
- createdAt: z15.string()
643
- });
644
- var DevTokenMintResponseSchema = z15.object({
645
- token: z15.string(),
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(),
646
1009
  metadata: DevTokenMetadataSchema
647
1010
  });
648
1011
 
649
1012
  // ../shared/src/instruction.ts
650
- import { z as z16 } from "zod";
1013
+ import { z as z17 } from "zod";
651
1014
  var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
652
- var InstructionCapabilitySchema = z16.enum(INSTRUCTION_CAPABILITIES);
653
- var AttestedInstructionSchema = z16.object({
1015
+ var InstructionCapabilitySchema = z17.enum(INSTRUCTION_CAPABILITIES);
1016
+ var AttestedInstructionSchema = z17.object({
654
1017
  /** App the call is attributed to (registry app id). */
655
- appId: z16.string().min(1),
1018
+ appId: z17.string().min(1),
656
1019
  /** Authenticated user, or the anonymous sentinel on `public` apps. */
657
- userOid: z16.string().min(1),
1020
+ userOid: z17.string().min(1),
658
1021
  capability: InstructionCapabilitySchema,
659
1022
  /** The allowlisted origin the edge authorized (scheme + host + port). */
660
- origin: z16.url(),
1023
+ origin: z17.url(),
661
1024
  /** Connection (secret) name to inject, if this is a secret-backed call. */
662
- connection: z16.string().min(1).optional(),
1025
+ connection: z17.string().min(1).optional(),
663
1026
  /** Correlates the edge audit row with the egress call. */
664
- requestId: z16.string().min(1),
1027
+ requestId: z17.string().min(1),
665
1028
  /**
666
1029
  * The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
667
1030
  * Egress refuses a mismatched verb/resource, so a captured instruction can't be
@@ -676,8 +1039,8 @@ var AttestedInstructionSchema = z16.object({
676
1039
  * means "old edge", not tampering — egress asserts ONLY when the claim is
677
1040
  * present. Make required once a fleet is reliably past deploy.
678
1041
  */
679
- method: z16.string().min(1).optional(),
680
- path: z16.string().optional(),
1042
+ method: z17.string().min(1).optional(),
1043
+ path: z17.string().optional(),
681
1044
  /**
682
1045
  * Environment tier this call is scoped to (dev-mode design §6). Egress resolves
683
1046
  * the connection secret within this tier — a `dev` instruction can never reach a
@@ -691,7 +1054,7 @@ var INSTRUCTION_TTL_SECONDS = 30;
691
1054
  var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
692
1055
 
693
1056
  // ../shared/src/fetch.ts
694
- import { z as z17 } from "zod";
1057
+ import { z as z18 } from "zod";
695
1058
  var FETCH_ERROR_CODES = [
696
1059
  "forbidden",
697
1060
  "rate_limited",
@@ -701,10 +1064,10 @@ var FETCH_ERROR_CODES = [
701
1064
  "replay",
702
1065
  "upstream_error"
703
1066
  ];
704
- var FetchErrorCodeSchema = z17.enum(FETCH_ERROR_CODES);
705
- var FetchProxyErrorSchema = z17.object({
1067
+ var FetchErrorCodeSchema = z18.enum(FETCH_ERROR_CODES);
1068
+ var FetchProxyErrorSchema = z18.object({
706
1069
  code: FetchErrorCodeSchema,
707
- message: z17.string()
1070
+ message: z18.string()
708
1071
  });
709
1072
 
710
1073
  // src/client.ts
@@ -716,7 +1079,7 @@ var CliError = class extends Error {
716
1079
  this.code = code;
717
1080
  }
718
1081
  };
719
- var VersionListSchema = z18.array(VersionSchema);
1082
+ var VersionListSchema = z19.array(VersionSchema);
720
1083
  var PortalClient = class {
721
1084
  #baseUrl;
722
1085
  #tokenProvider;
@@ -938,10 +1301,15 @@ function parseVisibility(input) {
938
1301
  if (!groupId) throw new CliError("group visibility needs an id: group:<id>");
939
1302
  return { mode: "group", groupId };
940
1303
  }
941
- if (input === "private" || input === "password" || input === "public") {
1304
+ if (input === "internal" || input === "password" || input === "public") {
942
1305
  return { mode: input };
943
1306
  }
944
- throw new CliError(`invalid visibility "${input}" (private | group:<id> | password | public)`);
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)`);
945
1313
  }
946
1314
  function printVersion(v) {
947
1315
  console.log(` version ${v.number} (${v.status}) \u2014 ${v.id}`);
@@ -1113,7 +1481,7 @@ Usage:
1113
1481
  Common flags: --slug <slug> --portal-url <url> --token <token>
1114
1482
  Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; CI/scripts).
1115
1483
  Config file: helix.json { slug, portalUrl, dir }
1116
- Visibility: private | group:<id> | password | public
1484
+ Visibility: internal | group:<id> | password | public
1117
1485
  `;
1118
1486
  async function main() {
1119
1487
  const { values, positionals } = parseCliArgs(process.argv.slice(2));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azx-pbc/helix-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Deploy CLI for Helix, the AZX App Platform",
5
5
  "license": "MIT",
6
6
  "repository": {