@azx-pbc/helix-cli 0.2.0 → 0.3.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 +31 -6
  2. package/dist/helix.js +943 -429
  3. package/package.json +5 -4
package/dist/helix.js CHANGED
@@ -11,6 +11,7 @@ var options = {
11
11
  promote: { type: "boolean" },
12
12
  "display-name": { type: "string" },
13
13
  visibility: { type: "string" },
14
+ path: { type: "string" },
14
15
  help: { type: "boolean" }
15
16
  };
16
17
  function parseCliArgs(argv) {
@@ -19,33 +20,85 @@ function parseCliArgs(argv) {
19
20
  }
20
21
 
21
22
  // src/client.ts
22
- import { z as z19 } from "zod";
23
+ import { z as z22 } from "zod";
23
24
 
24
25
  // ../shared/src/visibility.ts
25
26
  import { z } from "zod";
27
+ var MAX_VISIBILITY_GROUPS = 10;
28
+ var VisibilityGroupIdsSchema = z.array(z.string().min(1)).max(MAX_VISIBILITY_GROUPS);
26
29
  var VisibilitySchema = z.discriminatedUnion("mode", [
27
30
  z.object({ mode: z.literal("internal") }),
28
- z.object({ mode: z.literal("group"), groupId: z.string().min(1) }),
31
+ z.object({ mode: z.literal("group"), groupIds: VisibilityGroupIdsSchema }),
29
32
  z.object({ mode: z.literal("password") }),
30
33
  z.object({ mode: z.literal("public") })
31
34
  ]);
32
35
  var VISIBILITY_MODES = ["internal", "group", "password", "public"];
33
36
  var VisibilityModeSchema = z.enum(VISIBILITY_MODES);
37
+ function assertWritableGroupIds(ids) {
38
+ return ids.every((id) => !/[,\s]/.test(id));
39
+ }
40
+ var WRITABLE_GROUP_ID_MESSAGE = "a group id cannot contain a comma or whitespace (check for a pasted list)";
41
+ var WritableVisibilitySchema = VisibilitySchema.refine(
42
+ (v) => v.mode !== "group" || assertWritableGroupIds(v.groupIds),
43
+ { message: WRITABLE_GROUP_ID_MESSAGE, path: ["groupIds"] }
44
+ );
34
45
 
35
- // ../shared/src/app.ts
46
+ // ../shared/src/directory.ts
36
47
  import { z as z2 } from "zod";
48
+ var DirectoryGroupSchema = z2.object({
49
+ /** Object id — a GUID under Entra, a readable fixture id in dev. */
50
+ id: z2.string().min(1),
51
+ displayName: z2.string().min(1),
52
+ /**
53
+ * Whether this is a security group — the only kind the `groups` claim carries,
54
+ * so the only kind worth scoping an app to. Reported rather than filtered so
55
+ * the picker can mark an ineligible group with a reason instead of hiding it;
56
+ * "the group I searched for isn't listed" is a worse failure than a greyed-out
57
+ * row.
58
+ *
59
+ * **Absent means "not read", not "false".** The batch id→name resolve does not
60
+ * always return it, and defaulting an unread flag to `true` made the same group
61
+ * show as eligible in one view and ineligible in another depending on which
62
+ * query resolved last. A client should treat absent as eligible-but-unknown and
63
+ * never let it overwrite a value it does know.
64
+ */
65
+ securityEnabled: z2.boolean().optional()
66
+ });
67
+ var DIRECTORY_UNAVAILABLE_REASONS = [
68
+ "no-consent",
69
+ "no-credential",
70
+ "not-configured"
71
+ ];
72
+ var DirectoryUnavailableReasonSchema = z2.enum(DIRECTORY_UNAVAILABLE_REASONS);
73
+ var DirectoryGroupsResponseSchema = z2.discriminatedUnion("available", [
74
+ z2.object({
75
+ available: z2.literal(true),
76
+ groups: z2.array(DirectoryGroupSchema)
77
+ }),
78
+ z2.object({
79
+ available: z2.literal(false),
80
+ reason: DirectoryUnavailableReasonSchema,
81
+ /** Operator-facing sentence; the SPA shows it in the degradation banner. */
82
+ detail: z2.string().min(1),
83
+ /** The Graph permission an administrator would have to grant. */
84
+ missingPermission: z2.string().min(1).optional()
85
+ })
86
+ ]);
87
+
88
+ // ../shared/src/app.ts
89
+ import { z as z3 } from "zod";
37
90
  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),
91
+ var AppSchema = z3.object({
92
+ id: z3.uuid(),
93
+ slug: z3.string().min(1).max(63).regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
94
+ displayName: z3.string().min(1).max(200),
42
95
  visibility: VisibilitySchema,
43
96
  /** The version currently served; null before the first deploy. */
44
- currentVersionId: z2.uuid().nullable(),
97
+ currentVersionId: z3.uuid().nullable(),
45
98
  /** 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(),
99
+ archivedAt: z3.iso.datetime().nullable(),
100
+ createdAt: z3.iso.datetime(),
101
+ updatedAt: z3.iso.datetime(),
49
102
  /**
50
103
  * Where this app is served, computed control-plane-side from the deployment's
51
104
  * apps base (`APP_PUBLIC_BASE`) — so clients render a URL instead of
@@ -54,55 +107,130 @@ var AppSchema = z2.object({
54
107
  * CLI against an older portal. Clients that lack it fall back to composing the
55
108
  * slug onto `appPublicBase` from `GET /api/v1/config`.
56
109
  */
57
- url: z2.url().optional()
58
- });
110
+ url: z3.url().optional(),
111
+ /**
112
+ * Who owns this app — the portal actor that created it (`actor.sub`). The
113
+ * **identity** half: this is what `scope=mine` and `ownsApp` compare against,
114
+ * and it is never what a screen renders.
115
+ *
116
+ * Optional for the same reason as `url`: the CLI parses this schema, and rows
117
+ * predating the approvals work carry no owner at all.
118
+ */
119
+ ownerId: z3.string().optional(),
120
+ /**
121
+ * The owner's claims as captured at create time. The **display** half: render
122
+ * these, never compare them. `ownerId` only looks like an email by accident of
123
+ * the portal verifier collapsing the subject to
124
+ * `email ?? preferred_username ?? sub`, and it is slated to be re-based onto an
125
+ * opaque directory id — so display travels separately. Absent for rows created
126
+ * before the columns existed; fall back to `ownerId`.
127
+ */
128
+ ownerName: z3.string().optional(),
129
+ ownerEmail: z3.string().optional()
130
+ });
131
+ var AppListItemSchema = AppSchema.extend({
132
+ /** When the most recent version was created; null before the first deploy. */
133
+ lastDeployAt: z3.iso.datetime().nullable(),
134
+ /** `number` of the version currently served; null before the first promote. */
135
+ liveVersionNumber: z3.int().positive().nullable(),
136
+ /** Highest `preview`-status version; null when none exists. */
137
+ latestPreviewNumber: z3.int().positive().nullable()
138
+ });
139
+ var APP_LIST_SCOPES = ["mine", "all"];
140
+ var AppListScopeSchema = z3.enum(APP_LIST_SCOPES);
59
141
 
60
142
  // ../shared/src/version.ts
61
- import { z as z3 } from "zod";
143
+ import { z as z4 } from "zod";
62
144
  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(),
145
+ var VersionStatusSchema = z4.enum(VERSION_STATUSES);
146
+ var DEPLOY_OUTCOMES = [
147
+ "canonical",
148
+ "rerooted",
149
+ "nested",
150
+ "ambiguous",
151
+ "unsalvageable"
152
+ ];
153
+ var DeployReportSchema = z4.object({
154
+ /** The planner revision that produced this, for reading old rows later. */
155
+ plannerVersion: z4.int().nonnegative(),
156
+ outcome: z4.enum(DEPLOY_OUTCOMES),
157
+ /** The chosen bundle root (`""` = archive root). */
158
+ root: z4.string().max(1024),
159
+ /** How many files were uploaded after re-rooting. */
160
+ fileCount: z4.int().nonnegative(),
161
+ /** Count of dropped files, keyed by reason (junk/outside-root/…). */
162
+ drops: z4.record(z4.string().max(64), z4.int().nonnegative()).default({}),
163
+ /** Problem kinds surfaced (missing-reference, secret-dropped, …). */
164
+ problems: z4.array(z4.string().max(64)).max(50).default([]),
165
+ /** Candidate roots the planner considered. */
166
+ candidates: z4.array(z4.string().max(1024)).max(20).default([])
167
+ });
168
+ var VersionSchema = z4.object({
169
+ id: z4.uuid(),
170
+ appId: z4.uuid(),
67
171
  /** Monotonic per app, 1-based. */
68
- number: z3.int().positive(),
172
+ number: z4.int().positive(),
69
173
  /** Blob key prefix for this version's assets, e.g. `apps/<appId>/7/`. */
70
- blobPrefix: z3.string().min(1),
174
+ blobPrefix: z4.string().min(1),
71
175
  status: VersionStatusSchema,
72
- createdAt: z3.iso.datetime()
176
+ createdAt: z4.iso.datetime(),
177
+ /** Client-asserted salvage provenance (ADR-0038); absent on CLI uploads. */
178
+ deployReport: DeployReportSchema.optional()
73
179
  });
74
180
 
75
181
  // ../shared/src/manifest.ts
76
- import { z as z4 } from "zod";
77
- var LlmCapabilitySchema = z4.object({
78
- models: z4.array(z4.string()).default([]),
182
+ import { z as z5 } from "zod";
183
+ var LlmCapabilitySchema = z5.object({
184
+ models: z5.array(z5.string()).default([]),
79
185
  /**
80
186
  * Per-app daily LLM spend cap in USD; unset ⇒ unbounded. Denominated in
81
187
  * dollars (not tokens) so the cap means the same thing across models — the
82
188
  * edge prices each call via `@azx-pbc/shared` pricing and enforces a daily +
83
189
  * rolling-hour burst window off the frozen `costMicroUsd` ledger column.
84
190
  */
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()
191
+ dollarsPerDay: z5.number().positive().optional()
192
+ });
193
+ var KEY_MAX_CHARS = 256;
194
+ var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
195
+ function isValidDataKey(key) {
196
+ return key.length > 0 && key.length <= KEY_MAX_CHARS && PRINTABLE_ASCII.test(key) && // After the ASCII test this can only be a leading/trailing 0x20 — but it
197
+ // is the one character in the allowlist that is invisible at a grant's
198
+ // edges, so it gets its own rule.
199
+ key === key.trim();
200
+ }
201
+ var DATA_KEY_RULE = "must be 1\u2013256 printable ASCII characters (0x20\u20130x7E) with no leading or trailing space";
202
+ var DataKeySchema = z5.string().refine(isValidDataKey, { message: DATA_KEY_RULE });
203
+ var DataCapabilitySchema = z5.object({
204
+ user: z5.boolean().default(false),
205
+ collections: z5.array(DataKeySchema).default([]),
206
+ sharedRead: z5.array(DataKeySchema).default([]),
207
+ sharedWrite: z5.array(DataKeySchema).default([]),
208
+ /** ADR-0042: authorize every key starting with one of these prefixes (read). */
209
+ sharedReadPrefixes: z5.array(DataKeySchema).default([]),
210
+ /** ADR-0042: authorize every key starting with one of these prefixes (write). */
211
+ sharedWritePrefixes: z5.array(DataKeySchema).default([]),
212
+ writesPerDay: z5.int().positive().optional(),
213
+ bytesPerDay: z5.int().positive().optional()
214
+ }).superRefine((d, ctx) => {
215
+ if (d.sharedWritePrefixes.length > 0 && d.writesPerDay === void 0) {
216
+ ctx.addIssue({
217
+ code: "custom",
218
+ path: ["writesPerDay"],
219
+ message: "a writesPerDay budget is required alongside sharedWritePrefixes \u2014 a prefix grant allows runtime row creation, so the grant and its bound are approved together (ADR-0042)"
220
+ });
221
+ }
94
222
  });
95
- var FetchConnectionSchema = z4.object({
96
- origin: z4.url(),
97
- connection: z4.string().min(1).optional()
223
+ var FetchConnectionSchema = z5.object({
224
+ origin: z5.url(),
225
+ connection: z5.string().min(1).optional()
98
226
  });
99
- var FetchCapabilitySchema = z4.object({
227
+ var FetchCapabilitySchema = z5.object({
100
228
  /** Opt-in transparent `fetch` shim injected at serve time (fetch-proxy §3.2). */
101
- shim: z4.boolean().default(false),
229
+ shim: z5.boolean().default(false),
102
230
  /** Origins reached through the proxy (mode `proxy`); direct stays in `externalOrigins`. */
103
- origins: z4.array(FetchConnectionSchema).default([]),
231
+ origins: z5.array(FetchConnectionSchema).default([]),
104
232
  /** Per-app daily proxied-request budget; unset ⇒ unbounded (fetch-proxy §7). */
105
- requestsPerDay: z4.int().positive().optional()
233
+ requestsPerDay: z5.int().positive().optional()
106
234
  });
107
235
  var SCOPE_SEGMENT = /^[A-Za-z0-9\-._~]+$/;
108
236
  function isValidServiceWorkerScope(scope) {
@@ -115,42 +243,53 @@ function isValidServiceWorkerScope(scope) {
115
243
  }
116
244
  return !first.startsWith("_");
117
245
  }
118
- var OfflineCapabilitySchema = z4.object({
246
+ var OfflineCapabilitySchema = z5.object({
119
247
  /**
120
248
  * URL path prefix the worker controls, e.g. `/app/`. Leading and trailing
121
249
  * slash required; never root, never a `_`-prefixed platform namespace.
122
250
  */
123
- scope: z4.string().refine(isValidServiceWorkerScope, {
251
+ scope: z5.string().refine(isValidServiceWorkerScope, {
124
252
  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
253
  })
126
254
  });
127
- var CapabilitiesSchema = z4.object({
255
+ var CapabilitiesSchema = z5.object({
128
256
  llm: LlmCapabilitySchema.optional(),
129
257
  data: DataCapabilitySchema.optional(),
130
258
  /** Platform-registered MCP servers this app may reach (§6.1, exposed as REST). */
131
- mcp: z4.array(z4.string()).default([]),
259
+ mcp: z5.array(z5.string()).default([]),
132
260
  /** Extra CSP `connect-src` origins for **direct** browser calls (§4.4). */
133
- externalOrigins: z4.array(z4.url()).default([]),
261
+ externalOrigins: z5.array(z5.url()).default([]),
134
262
  /** Governed outbound HTTP via the fetch-proxy / egress plane (in build, M4.5). */
135
263
  fetch: FetchCapabilitySchema.optional(),
136
264
  /** Platform-owned, scope-confined service worker for offline cold boot (ADR-0035). */
137
265
  offline: OfflineCapabilitySchema.optional()
138
266
  });
139
- var AppManifestSchema = z4.object({
267
+ var AppManifestSchema = z5.object({
140
268
  /** App slug; matches `App.slug`. */
141
- app: z4.string().min(1),
269
+ app: z5.string().min(1),
142
270
  visibility: VisibilitySchema,
143
271
  capabilities: CapabilitiesSchema.default({ mcp: [], externalOrigins: [] })
144
272
  });
145
273
 
146
274
  // ../shared/src/approval.ts
147
- import { z as z5 } from "zod";
275
+ import { z as z6 } from "zod";
148
276
 
149
277
  // ../shared/src/pricing.ts
150
278
  var MODEL_PRICING = {
151
279
  // 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.
280
+ // structured outputs are supported on the Fable 5 line, Opus 5, Sonnet 5, Opus 4.8
281
+ // and Haiku 4.5 but not on those three, so the flag is opt-in per model rather
282
+ // than per provider.
283
+ // Fable 5.1 sits above Fable 5 at the same per-token rate, but its cache *reads*
284
+ // bill at 0.025x base input rather than the 0.1x every other model uses — the
285
+ // one reason `cacheReadMultiplier` exists.
286
+ "claude-fable-5-1": {
287
+ inputPerMTok: 10,
288
+ outputPerMTok: 50,
289
+ provider: "anthropic",
290
+ cacheReadMultiplier: 0.025,
291
+ structuredOutputs: true
292
+ },
154
293
  "claude-fable-5": {
155
294
  inputPerMTok: 10,
156
295
  outputPerMTok: 50,
@@ -163,12 +302,12 @@ var MODEL_PRICING = {
163
302
  provider: "anthropic",
164
303
  structuredOutputs: true
165
304
  },
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.
305
+ // Sonnet 5's $2/$10 was introductory pricing through 2026-08-31; Anthropic made
306
+ // it the standard price and cancelled the scheduled rise to $3/$15. This is now
307
+ // the list rate, so the cost gate no longer over-estimates Sonnet 5 spend.
169
308
  "claude-sonnet-5": {
170
- inputPerMTok: 3,
171
- outputPerMTok: 15,
309
+ inputPerMTok: 2,
310
+ outputPerMTok: 10,
172
311
  provider: "anthropic",
173
312
  structuredOutputs: true
174
313
  },
@@ -187,9 +326,73 @@ var MODEL_PRICING = {
187
326
  provider: "anthropic",
188
327
  structuredOutputs: true
189
328
  },
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.
329
+ // OpenAI. Every model here resolves to a snapshot new enough for `response_format`
330
+ // json_schema (that floor is `gpt-4o-2024-08-06`), so `structuredOutputs` is set
331
+ // across the board.
332
+ //
333
+ // The current generation — GPT-6 and GPT-5.x — is reasoning models throughout, so
334
+ // `reasoning` is set on all of them. On chat/completions that flag means exactly
335
+ // one thing here: send `max_completion_tokens`, not `max_tokens`. OpenAI has
336
+ // deprecated `max_tokens` outright and reasoning models reject it, so the flag is
337
+ // the forward-compatible setting even where a model would still accept both.
338
+ "gpt-6-astra": {
339
+ inputPerMTok: 10,
340
+ outputPerMTok: 50,
341
+ provider: "openai",
342
+ reasoning: true,
343
+ minCompletionTokens: 25e3,
344
+ structuredOutputs: true
345
+ },
346
+ "gpt-5.6-sol": {
347
+ inputPerMTok: 4,
348
+ outputPerMTok: 20,
349
+ provider: "openai",
350
+ reasoning: true,
351
+ minCompletionTokens: 25e3,
352
+ structuredOutputs: true
353
+ },
354
+ "gpt-5.6-terra": {
355
+ inputPerMTok: 2,
356
+ outputPerMTok: 12,
357
+ provider: "openai",
358
+ reasoning: true,
359
+ minCompletionTokens: 25e3,
360
+ structuredOutputs: true
361
+ },
362
+ "gpt-5.6-luna": {
363
+ inputPerMTok: 0.2,
364
+ outputPerMTok: 1.2,
365
+ provider: "openai",
366
+ reasoning: true,
367
+ minCompletionTokens: 25e3,
368
+ structuredOutputs: true
369
+ },
370
+ "gpt-5.1": {
371
+ inputPerMTok: 1.25,
372
+ outputPerMTok: 10,
373
+ provider: "openai",
374
+ reasoning: true,
375
+ minCompletionTokens: 25e3,
376
+ structuredOutputs: true
377
+ },
378
+ "gpt-5-mini": {
379
+ inputPerMTok: 0.25,
380
+ outputPerMTok: 2,
381
+ provider: "openai",
382
+ reasoning: true,
383
+ minCompletionTokens: 25e3,
384
+ structuredOutputs: true
385
+ },
386
+ "gpt-5-nano": {
387
+ inputPerMTok: 0.05,
388
+ outputPerMTok: 0.4,
389
+ provider: "openai",
390
+ reasoning: true,
391
+ minCompletionTokens: 25e3,
392
+ structuredOutputs: true
393
+ },
394
+ // Previous generations, kept curated so a deployed app's manifest keeps working.
395
+ // Rates re-verified 2026-09-14 and unchanged.
193
396
  "gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10, provider: "openai", structuredOutputs: true },
194
397
  "gpt-4o-mini": {
195
398
  inputPerMTok: 0.15,
@@ -230,7 +433,7 @@ var MODEL_PRICING = {
230
433
 
231
434
  // ../shared/src/approval.ts
232
435
  var RISK_LEVELS = ["low", "med", "high"];
233
- var RiskSchema = z5.enum(RISK_LEVELS);
436
+ var RiskSchema = z6.enum(RISK_LEVELS);
234
437
  var APPROVAL_STATUSES = [
235
438
  "pending",
236
439
  "approved",
@@ -238,134 +441,151 @@ var APPROVAL_STATUSES = [
238
441
  "withdrawn",
239
442
  "needs_changes"
240
443
  ];
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(),
444
+ var ApprovalStatusSchema = z6.enum(APPROVAL_STATUSES);
445
+ var DeltaSchema = z6.object({
446
+ path: z6.string(),
447
+ from: z6.union([z6.string(), z6.number(), z6.boolean()]).optional(),
448
+ to: z6.union([z6.string(), z6.number(), z6.boolean()]).optional()
449
+ });
450
+ var PriorDecisionsSchema = z6.object({
451
+ /** Prior *decided* (non-pending) requests on this app — the history count. */
452
+ total: z6.number().int().nonnegative(),
453
+ /** Prior denied requests touching an overlapping policy area (quiet signal). */
454
+ deniedSameArea: z6.number().int().nonnegative(),
455
+ /** Prior denied requests sharing an exact delta path (loud signal; ⊆ area). */
456
+ deniedSameGrant: z6.number().int().nonnegative(),
457
+ /** The most recent decided request (any status), for the "last decision" line. */
458
+ last: z6.object({
459
+ status: ApprovalStatusSchema,
460
+ note: z6.string().nullable(),
461
+ decidedBy: z6.string().nullable(),
462
+ decidedAt: z6.string()
463
+ }).optional()
464
+ });
465
+ var ApprovalRequestSchema = z6.object({
466
+ id: z6.string(),
467
+ appId: z6.string(),
250
468
  /** Joined for the admin queue / banner; not a column. */
251
- appSlug: z5.string().optional(),
252
- appDisplayName: z5.string().optional(),
469
+ appSlug: z6.string().optional(),
470
+ appDisplayName: z6.string().optional(),
253
471
  status: ApprovalStatusSchema,
254
472
  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({
473
+ deltas: z6.array(DeltaSchema),
474
+ baseSnapshot: z6.unknown(),
475
+ requestedBy: z6.string(),
476
+ reason: z6.string().nullable().optional(),
477
+ decidedBy: z6.string().nullable().optional(),
478
+ decisionNote: z6.string().nullable().optional(),
479
+ createdAt: z6.string(),
480
+ decidedAt: z6.string().nullable().optional(),
481
+ /** Sibling-decision context for the admin queue (issue #26); read-side only. */
482
+ priorDecisions: PriorDecisionsSchema.optional()
483
+ });
484
+ var ManifestUpdateResultSchema = z6.object({
265
485
  manifest: AppManifestSchema,
266
- applied: z5.array(DeltaSchema),
267
- pending: z5.string().nullable()
486
+ applied: z6.array(DeltaSchema),
487
+ pending: z6.string().nullable()
268
488
  });
269
- var VisibilityUpdateResultSchema = z5.object({
489
+ var VisibilityUpdateResultSchema = z6.object({
270
490
  app: AppSchema,
271
- applied: z5.array(DeltaSchema),
272
- pending: z5.string().nullable()
491
+ applied: z6.array(DeltaSchema),
492
+ pending: z6.string().nullable()
273
493
  });
274
- var ApprovalDecisionRequestSchema = z5.object({
275
- note: z5.string().max(2e3).optional()
494
+ var ApprovalDecisionRequestSchema = z6.object({
495
+ note: z6.string().max(2e3).optional()
276
496
  });
277
497
  var CURATED_LLM_MODELS = Object.keys(MODEL_PRICING);
278
498
 
279
499
  // ../shared/src/health.ts
280
- import { z as z6 } from "zod";
281
- var HealthStateSchema = z6.enum(["ok", "degraded", "error"]);
282
- var HealthCheckSchema = z6.object({
500
+ import { z as z7 } from "zod";
501
+ var HealthStateSchema = z7.enum(["ok", "degraded", "error"]);
502
+ var HealthCheckSchema = z7.object({
283
503
  /** Stable identifier an alert rule can key on, e.g. `registry-projection`. */
284
- name: z6.string(),
504
+ name: z7.string(),
285
505
  status: HealthStateSchema,
286
506
  /** Operator-facing one-liner: what is wrong, and for how long. */
287
- detail: z6.string().optional(),
507
+ detail: z7.string().optional(),
288
508
  /** Wall-clock instant this check last succeeded. Report-only — never the
289
509
  * basis of a staleness decision (see the monotonic note in the edge's
290
510
  * registry projection). */
291
- lastSuccessAt: z6.iso.datetime().optional(),
511
+ lastSuccessAt: z7.iso.datetime().optional(),
292
512
  /** Numeric facts a dashboard or a log-based metric can key on. */
293
- metrics: z6.record(z6.string(), z6.number()).optional()
513
+ metrics: z7.record(z7.string(), z7.number()).optional()
294
514
  });
295
- var HealthStatusSchema = z6.object({
515
+ var HealthStatusSchema = z7.object({
296
516
  /** The roll-up: the worst state across `checks` (see `worstHealthState`). */
297
517
  status: HealthStateSchema,
298
- service: z6.string(),
518
+ service: z7.string(),
299
519
  /** Process uptime in seconds. */
300
- uptime: z6.number().nonnegative(),
520
+ uptime: z7.number().nonnegative(),
301
521
  /** Absent means the service reports liveness only (portal and egress today). */
302
- checks: z6.array(HealthCheckSchema).optional()
522
+ checks: z7.array(HealthCheckSchema).optional()
303
523
  });
304
524
 
305
525
  // ../shared/src/api.ts
306
- import { z as z7 } from "zod";
307
- var CreateAppRequestSchema = z7.object({
526
+ import { z as z8 } from "zod";
527
+ var CreateAppRequestSchema = z8.object({
308
528
  slug: AppSchema.shape.slug,
309
529
  displayName: AppSchema.shape.displayName,
310
- visibility: VisibilitySchema.default({ mode: "internal" }),
530
+ visibility: WritableVisibilitySchema.default({ mode: "internal" }),
311
531
  /** Optional per-app capability grant set at create time (architecture §6.3). */
312
532
  capabilities: CapabilitiesSchema.optional()
313
533
  });
314
- var SetManifestRequestSchema = z7.object({
534
+ var SetManifestRequestSchema = z8.object({
315
535
  capabilities: CapabilitiesSchema,
316
536
  /** Justification carried onto an approval request for any elevated deltas. */
317
- reason: z7.string().max(2e3).optional()
537
+ reason: z8.string().max(2e3).optional()
318
538
  });
319
- var SetVisibilityRequestSchema = z7.object({
320
- visibility: VisibilitySchema,
539
+ var SetVisibilityRequestSchema = z8.object({
540
+ visibility: WritableVisibilitySchema,
321
541
  /** Justification carried onto the approval request when going public. */
322
- reason: z7.string().max(2e3).optional()
542
+ reason: z8.string().max(2e3).optional()
323
543
  });
324
544
  var MIN_PASSWORD_LENGTH = 12;
325
- var SetPasswordRequestSchema = z7.object({
326
- password: z7.string().min(MIN_PASSWORD_LENGTH).optional()
545
+ var SetPasswordRequestSchema = z8.object({
546
+ password: z8.string().min(MIN_PASSWORD_LENGTH).optional()
327
547
  });
328
- var PasswordCredentialResponseSchema = z7.object({
548
+ var PasswordCredentialResponseSchema = z8.object({
329
549
  /** The shared passphrase, in cleartext, for the owner to copy/share. */
330
- password: z7.string().min(1),
550
+ password: z8.string().min(1),
331
551
  /** The app's public URL, prebuilt so the UI can offer a one-click copy. */
332
- url: z7.url(),
552
+ url: z8.url(),
333
553
  /** 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(),
554
+ setAt: z8.string()
555
+ });
556
+ var CspWarningSchema = z8.object({
557
+ file: z8.string(),
558
+ origin: z8.string(),
559
+ hint: z8.string()
560
+ });
561
+ var OriginGrantRequestSchema = z8.object({
562
+ origin: z8.url(),
563
+ reason: z8.string().max(2e3).optional()
564
+ });
565
+ var CspViolationSchema = z8.object({
566
+ appId: z8.string(),
567
+ appSlug: z8.string().nullable(),
568
+ directive: z8.string(),
569
+ blockedUri: z8.string(),
570
+ count: z8.number().int(),
571
+ lastSeen: z8.string(),
352
572
  /**
353
573
  * The blocked origin is already permitted by the app's *current* manifest, so
354
574
  * this historical report is no longer actionable. Derived at read time
355
575
  * (directive-aware — only `connect-src`/`img-src` are widened by an
356
576
  * `externalOrigins` grant); the row itself is never deleted.
357
577
  */
358
- resolved: z7.boolean()
578
+ resolved: z8.boolean()
359
579
  });
360
- var CspViolationsPageSchema = z7.object({
361
- violations: z7.array(CspViolationSchema)
580
+ var CspViolationsPageSchema = z8.object({
581
+ violations: z8.array(CspViolationSchema)
362
582
  });
363
- var UploadVersionResponseSchema = z7.object({
583
+ var UploadVersionResponseSchema = z8.object({
364
584
  version: VersionSchema,
365
- warnings: z7.array(CspWarningSchema)
585
+ warnings: z8.array(CspWarningSchema)
366
586
  });
367
- var RollbackRequestSchema = z7.object({
368
- toNumber: z7.int().positive().optional()
587
+ var RollbackRequestSchema = z8.object({
588
+ toNumber: z8.int().positive().optional()
369
589
  });
370
590
  var API_ERROR_CODES = [
371
591
  "validation_failed",
@@ -387,48 +607,121 @@ var API_ERROR_CODES = [
387
607
  * of its own budget. HTTP 429.
388
608
  */
389
609
  "rate_limited",
610
+ /**
611
+ * Gateway: a `shared` app-data write carried no concurrency precondition
612
+ * (`If-Match` / `If-None-Match`), which that scope requires (ADR-0041).
613
+ * Distinct from `conflict` (a precondition that was stated and lost) because
614
+ * the two demand opposite responses: `conflict` says re-read and retry,
615
+ * `precondition_required` says the app's code is wrong. HTTP 428.
616
+ */
617
+ "precondition_required",
390
618
  /** Gateway: a configured capability is not available on this edge. */
391
619
  "capability_unavailable",
392
620
  "internal"
393
621
  ];
394
- var ApiErrorCodeSchema = z7.enum(API_ERROR_CODES);
395
- var ApiErrorSchema = z7.object({
396
- error: z7.object({
622
+ var ApiErrorCodeSchema = z8.enum(API_ERROR_CODES);
623
+ var ApiErrorSchema = z8.object({
624
+ error: z8.object({
397
625
  code: ApiErrorCodeSchema,
398
- message: z7.string(),
399
- details: z7.unknown().optional()
626
+ message: z8.string(),
627
+ details: z8.unknown().optional()
400
628
  })
401
629
  });
402
630
 
403
631
  // ../shared/src/auth.ts
404
- import { z as z8 } from "zod";
405
- var MeResponseSchema = z8.object({
406
- user: z8.object({
632
+ import { z as z9 } from "zod";
633
+ var MeResponseSchema = z9.object({
634
+ user: z9.object({
407
635
  /** IdP subject (Entra object id) — stable, safe to key app data on. */
408
- id: z8.string(),
409
- displayName: z8.string()
636
+ id: z9.string(),
637
+ displayName: z9.string(),
638
+ /**
639
+ * The captured address claim, or `null` when there isn't one — **nullable,
640
+ * not optional, and the difference is the contract**. A missing key would
641
+ * conflate "this platform doesn't send emails" with "we have no address for
642
+ * this person", and the second is routine: shared-password sessions are a
643
+ * `Guest` with no directory profile at all, and an issuer that sends no
644
+ * `email` and a non-addressable `preferred_username` yields null too
645
+ * (`captureEmail`). Apps must handle null; sending it explicitly is what
646
+ * makes them.
647
+ *
648
+ * Never a delivery target or an identity key — `id` is the only thing safe
649
+ * to join on. Entra addresses are reassignable and B2B guests carry their
650
+ * home-tenant address.
651
+ */
652
+ email: z9.string().nullable()
410
653
  })
411
654
  });
412
- var PortalMeResponseSchema = z8.object({
413
- sub: z8.string(),
655
+ var PortalMeResponseSchema = z9.object({
656
+ sub: z9.string(),
414
657
  /** How the actor was established: `oidc` or `dev-token`. */
415
- via: z8.string(),
416
- name: z8.string().optional(),
417
- email: z8.string().optional(),
658
+ via: z9.string(),
659
+ name: z9.string().optional(),
660
+ email: z9.string().optional(),
418
661
  /**
419
662
  * Whether the actor holds the `platform-admin` role. Computed server-side from
420
663
  * the actor's group/role claim — the raw ids never cross to the browser. Drives
421
664
  * admin nav + route gating in the SPA (and `helix whoami`).
665
+ *
666
+ * **Defaulted, not required**, for the same reason as
667
+ * {@link canSearchDirectory} below: this schema parses `PortalClient.me()` in
668
+ * the *published* CLI, which routinely talks to portals older than itself. The
669
+ * polarity is the cautious one — a portal that never sent the field is not
670
+ * telling us the caller is an admin.
671
+ */
672
+ isAdmin: z9.boolean().default(false),
673
+ /**
674
+ * Whether this caller may run a tenant-wide group search (ADR-0040 decision
675
+ * 11). Computed server-side from the deployment's `PORTAL_DIRECTORY_SEARCH`
676
+ * tier and the actor's admin-ness. A caller who *may* search learns nothing
677
+ * else; one who may not gets {@link searchRestriction} alongside.
678
+ *
679
+ * Sent so the group picker can **avoid issuing a search it is not allowed to
680
+ * make**, rather than firing one and interpreting the 403. That matters
681
+ * because a refused search is not a broken directory: the caller's own groups
682
+ * and their app's stored groups still resolve to names, so the picker stays
683
+ * useful and must not fall back to the "directory unavailable" banner. Server
684
+ * enforcement is independent of this hint (`GET /api/v1/directory/groups`
685
+ * refuses on its own).
686
+ *
687
+ * **Defaults to `true` when absent, and the polarity is the whole point.** The
688
+ * CLI bundles this schema (ADR-0032) and portals are customer-deployed and
689
+ * version independently (ADR-0028), so a new CLI against an older portal is
690
+ * routine — and it was a hard break while this was required: `helix whoami`
691
+ * failed outright, and `helix login` failed at its final "greet the actor"
692
+ * step, which runs *after* the tokens are written, so a login that genuinely
693
+ * succeeded reported an error. A portal old enough to omit this has no tier
694
+ * enforcement at all and therefore behaves exactly like `everyone`, so `true`
695
+ * describes the portal actually being talked to. Defaulting to `false` would
696
+ * instead tell a client that search is unavailable where it works.
422
697
  */
423
- isAdmin: z8.boolean()
698
+ canSearchDirectory: z9.boolean().default(true),
699
+ /**
700
+ * Why search was refused, when it was — **present only when
701
+ * {@link canSearchDirectory} is false**, and never `everyone`, which by
702
+ * construction refuses nobody.
703
+ *
704
+ * Exists because "restricted" and "restricted *to platform admins*" are
705
+ * different sentences and only one of them is true under the `none` tier. The
706
+ * picker has to be able to say which, and telling a platform admin that search
707
+ * is "limited to platform admins" while refusing them sends them off to audit
708
+ * a `PORTAL_ADMIN_GROUP_ID` that is perfectly correct.
709
+ *
710
+ * This is a deliberate, bounded narrowing of decision 11's "the browser learns
711
+ * the answer, never the tier": a *permitted* caller still learns nothing, and a
712
+ * refused one learns only which rule refused them — which they can already
713
+ * infer from the 403 they would get by asking. The admin group id still never
714
+ * crosses.
715
+ */
716
+ searchRestriction: z9.enum(["admins", "none"]).optional()
424
717
  });
425
- var AuthConfigResponseSchema = z8.object({
426
- issuer: z8.url(),
427
- cliClientId: z8.string().min(1),
718
+ var AuthConfigResponseSchema = z9.object({
719
+ issuer: z9.url(),
720
+ cliClientId: z9.string().min(1),
428
721
  /** Public client the portal SPA uses for code+PKCE in the browser. */
429
- webClientId: z8.string().min(1).optional(),
722
+ webClientId: z9.string().min(1).optional(),
430
723
  /** Expected token audience — part of what the CLI binds cached tokens to. */
431
- audience: z8.string().min(1).optional(),
724
+ audience: z9.string().min(1).optional(),
432
725
  /**
433
726
  * Whether this deployment permits `public` (anonymous) apps. Drives the SPA's
434
727
  * visibility UI — it hides the public option when false. Absent = forbidden
@@ -436,9 +729,9 @@ var AuthConfigResponseSchema = z8.object({
436
729
  * opt-in). Server-side enforcement is independent of this hint (portal routes
437
730
  * + edge serving).
438
731
  */
439
- allowPublicApps: z8.boolean().optional(),
732
+ allowPublicApps: z9.boolean().optional(),
440
733
  /** Whether this deployment permits `password` (shared-passphrase) apps. Same shape as {@link allowPublicApps}. */
441
- allowPasswordApps: z8.boolean().optional()
734
+ allowPasswordApps: z9.boolean().optional()
442
735
  });
443
736
  function portalApiScope(audience) {
444
737
  if (!audience || audience.startsWith("urn:")) return null;
@@ -447,27 +740,27 @@ function portalApiScope(audience) {
447
740
  }
448
741
 
449
742
  // ../shared/src/deployment.ts
450
- import { z as z9 } from "zod";
451
- var DeploymentConfigResponseSchema = z9.object({
743
+ import { z as z10 } from "zod";
744
+ var DeploymentConfigResponseSchema = z10.object({
452
745
  /**
453
746
  * Scheme + host + (non-default) port where apps are served, as reachable by
454
747
  * this browser (architecture §4.1). The app slug is prepended as a subdomain:
455
748
  * `https://<slug>.<host>`.
456
749
  */
457
- appPublicBase: z9.url(),
750
+ appPublicBase: z10.url(),
458
751
  /**
459
752
  * Base of the opt-in dev gateway (dev-mode design §3), absent when it is not
460
753
  * deployed. Unlike {@link appPublicBase} the slug goes in the *path*, not the
461
754
  * host. Absent ⇒ dev mode is unavailable here, so the UI says so instead of
462
755
  * printing an unreachable host.
463
756
  */
464
- devApiBase: z9.url().optional(),
757
+ devApiBase: z10.url().optional(),
465
758
  /**
466
759
  * Month-to-date platform spend ceiling (USD) for the admin budget watch line.
467
760
  * Display-only — the gateway is the choke point, so the rollup is exact, but
468
761
  * nothing enforces this. Absent ⇒ no ceiling shown.
469
762
  */
470
- platformMonthlyUsdCap: z9.number().positive().optional(),
763
+ platformMonthlyUsdCap: z10.number().positive().optional(),
471
764
  /**
472
765
  * Deploy bundle size caps in megabytes (`DEPLOY_MAX_FILE_MB` /
473
766
  * `DEPLOY_MAX_BUNDLE_MB` on the portal) — `deployMaxFileMb` is per file,
@@ -477,8 +770,129 @@ var DeploymentConfigResponseSchema = z9.object({
477
770
  * substitute a default, because printing the wrong cap to an agent sends it
478
771
  * chasing a rejection it can't see the cause of.
479
772
  */
480
- deployMaxFileMb: z9.number().positive().optional(),
481
- deployMaxBundleMb: z9.number().positive().optional()
773
+ deployMaxFileMb: z10.number().positive().optional(),
774
+ deployMaxBundleMb: z10.number().positive().optional()
775
+ });
776
+
777
+ // ../shared/src/catalogue.ts
778
+ import { z as z11 } from "zod";
779
+ var CapabilityCatalogueSchema = z11.object({
780
+ /**
781
+ * Visibility modes an app may declare here. `internal` is always present
782
+ * (SSO, any directory principal — the baseline). `group` is present only when
783
+ * an IdP with group claims is configured: on a dev-token-only portal it is
784
+ * absent, matching `/api/v1/auth/config`'s 404. `password` / `public` are
785
+ * present only when the operator policy permits them
786
+ * (`PORTAL_ALLOW_*_APPS`). A mode absent from this list is not requestable.
787
+ */
788
+ visibility: z11.object({
789
+ modes: z11.array(VisibilityModeSchema).min(1)
790
+ }),
791
+ /**
792
+ * LLM: the **servable** model ids, not merely the curated catalog. A model is
793
+ * servable when its upstream family is wired on this edge **and** the matching
794
+ * `platform` secret is seeded; a curated-but-unseeded model (e.g. a `gpt-*`
795
+ * model whose `openai` platform key was never added) is omitted, so an agent
796
+ * that reads this list never designs an app that 502s at call time. Every id
797
+ * in this list is also curated, so requesting one is baseline (auto-approve).
798
+ *
799
+ * `baselineDollarsPerDay` is the spend cap at/under which an LLM grant
800
+ * auto-approves; above it queues. From `@azx-pbc/shared/approval.ts`, so the
801
+ * catalogue and the classifier cannot drift.
802
+ */
803
+ llm: z11.object({
804
+ models: z11.array(z11.string().min(1)),
805
+ baselineDollarsPerDay: z11.number().positive()
806
+ }),
807
+ /**
808
+ * App-data: whether the three scopes are provisioned here, and the daily
809
+ * budgets at/under which a data grant auto-approves. `provisioned` is `true`
810
+ * today (the storage tables exist unconditionally); the field is the seam for
811
+ * a future deployment-level toggle, present before it is needed. The
812
+ * baselines come from `@azx-pbc/shared/approval.ts`.
813
+ */
814
+ data: z11.object({
815
+ provisioned: z11.boolean(),
816
+ baselineWritesPerDay: z11.number().positive(),
817
+ baselineBytesPerDay: z11.number().positive()
818
+ }),
819
+ /**
820
+ * The fetch-proxy (`/_api/fetch`) and direct-CSP surfaces.
821
+ *
822
+ * `externalOriginsPermitted` is `true` when an app may request a direct-CSP
823
+ * origin grant at all (the surface exists; any entry still queues for
824
+ * approval). `connections` names the stored `global`-scope secrets an app may
825
+ * reference from `capabilities.fetch.origins[].connection` — names only, never
826
+ * values, and without the origin each one fronts: a global secret has no
827
+ * stored origin (the origin is declared per-app in each manifest), so listing
828
+ * one here would be a guess. An agent learns "this connection exists and is
829
+ * referenceable" and supplies the origin itself. `baselineRequestsPerDay` is
830
+ * the proxied-request budget at/under which a fetch grant auto-approves.
831
+ */
832
+ fetch: z11.object({
833
+ externalOriginsPermitted: z11.boolean(),
834
+ connections: z11.array(z11.object({ name: z11.string().min(1) })),
835
+ baselineRequestsPerDay: z11.number().positive()
836
+ }),
837
+ /**
838
+ * MCP is carried in the manifest but **not enforced** — no transport exists
839
+ * today. Surfaced as `false` so an agent does not assume a working `mcp`
840
+ * grant; declaring any server queues for approval regardless.
841
+ */
842
+ mcp: z11.object({
843
+ enforced: z11.boolean()
844
+ }),
845
+ /**
846
+ * Offline (ADR-0035): whether the platform service-worker grant is available,
847
+ * and the rule a `scope` must satisfy (not the domain root, not a `/_…`
848
+ * reserved path). Any grant or rescope queues for approval.
849
+ */
850
+ offline: z11.object({
851
+ available: z11.boolean(),
852
+ /** Human-facing summary of the scope rule; not machine-parsed. */
853
+ scopeRule: z11.string().min(1)
854
+ }),
855
+ /** Deploy bundle size caps in megabytes, as enforced on upload. */
856
+ deploy: z11.object({
857
+ maxFileMb: z11.number().positive(),
858
+ maxBundleMb: z11.number().positive()
859
+ }),
860
+ /**
861
+ * The opt-in dev-gateway base, absent when it is not deployed here (same
862
+ * convention as `DeploymentConfigResponseSchema.devApiBase`).
863
+ */
864
+ devApiBase: z11.url().optional(),
865
+ /**
866
+ * The approval half of the catalogue (decision 3). The four baselines mirror
867
+ * `@azx-pbc/shared/approval.ts` exactly, and `elevationTriggers` enumerates
868
+ * the requests that **queue for a human** rather than auto-applying — the
869
+ * thing an agent reads to design an app that ships immediately. A trigger
870
+ * absent from this list never queues.
871
+ */
872
+ approval: z11.object({
873
+ baselines: z11.object({
874
+ dollarsPerDay: z11.number().positive(),
875
+ writesPerDay: z11.number().positive(),
876
+ bytesPerDay: z11.number().positive(),
877
+ fetchRequestsPerDay: z11.number().positive()
878
+ }),
879
+ elevationTriggers: z11.array(
880
+ z11.enum([
881
+ /** Any `externalOrigins` entry — widens CSP for a direct browser call. */
882
+ "externalOrigins",
883
+ /** Any `fetch.origins` entry — a proxied outbound origin. */
884
+ "fetchOrigins",
885
+ /** Any `mcp` server — no transport is enforced today. */
886
+ "mcp",
887
+ /** `visibility: public` — crosses the tenant boundary. */
888
+ "publicVisibility",
889
+ /** Any budget above its baseline (LLM spend, data writes/bytes, fetch requests). */
890
+ "budgetAboveBaseline",
891
+ /** An LLM model not in this deployment's servable (curated) set. */
892
+ "uncuratedLlmModel"
893
+ ])
894
+ )
895
+ })
482
896
  });
483
897
 
484
898
  // ../shared/src/scrypt.ts
@@ -494,16 +908,16 @@ var SCRYPT_PARAMS = {
494
908
  };
495
909
 
496
910
  // ../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)
911
+ import { z as z12 } from "zod";
912
+ var LlmMessageSchema = z12.object({
913
+ role: z12.enum(["user", "assistant"]),
914
+ content: z12.string().min(1)
501
915
  });
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)
916
+ var LlmUsageSchema = z12.object({
917
+ inputTokens: z12.int().nonnegative(),
918
+ outputTokens: z12.int().nonnegative(),
919
+ cacheReadInputTokens: z12.int().nonnegative().default(0),
920
+ cacheCreationInputTokens: z12.int().nonnegative().default(0)
507
921
  });
508
922
  var MAX_SCHEMA_CHARS = 32768;
509
923
  var MAX_SCHEMA_DEPTH = 12;
@@ -526,11 +940,11 @@ function withinSchemaBudget(schema) {
526
940
  }
527
941
  return serialized.length <= MAX_SCHEMA_CHARS && schemaDepth(schema) <= MAX_SCHEMA_DEPTH;
528
942
  }
529
- var LlmResponseFormatSchema = z10.object({
530
- type: z10.literal("json_schema"),
943
+ var LlmResponseFormatSchema = z12.object({
944
+ type: z12.literal("json_schema"),
531
945
  /** 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(
946
+ name: z12.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1-64 chars of [A-Za-z0-9_-]").optional(),
947
+ schema: z12.record(z12.string(), z12.unknown()).refine((s) => s.type === "object", 'schema root must be `{"type":"object"}`').refine(
534
948
  withinSchemaBudget,
535
949
  `schema must serialize to <= ${MAX_SCHEMA_CHARS} characters and nest <= ${MAX_SCHEMA_DEPTH} levels`
536
950
  )
@@ -540,18 +954,18 @@ var LlmResponseFormatSchema = z10.object({
540
954
  // on `gpt-*` but not `claude-*`, which is exactly the provider leak this seam
541
955
  // exists to prevent. The platform always enforces; see ADR-0034.
542
956
  });
543
- var LlmChatRequestSchema = z10.object({
544
- model: z10.string().min(1),
545
- messages: z10.array(LlmMessageSchema).min(1),
957
+ var LlmChatRequestSchema = z12.object({
958
+ model: z12.string().min(1),
959
+ messages: z12.array(LlmMessageSchema).min(1),
546
960
  /** Optional system prompt; maps to the vendor's system channel. */
547
- system: z10.string().optional(),
548
- maxTokens: z10.int().positive().max(128e3).optional(),
961
+ system: z12.string().optional(),
962
+ maxTokens: z12.int().positive().max(128e3).optional(),
549
963
  /** Sampling temperature; forwarded as-is (the vendor validates its own range). */
550
- temperature: z10.number().optional(),
964
+ temperature: z12.number().optional(),
551
965
  /** Nucleus sampling; forwarded as-is. */
552
- topP: z10.number().optional(),
966
+ topP: z12.number().optional(),
553
967
  /** Stop sequences; normalized to a list at the boundary. */
554
- stop: z10.array(z10.string()).optional(),
968
+ stop: z12.array(z12.string()).optional(),
555
969
  /**
556
970
  * Constrain the completion to a JSON schema (ADR-0034). Refused up front when
557
971
  * the requested model can't enforce it (`ModelPrice.structuredOutputs`). The
@@ -560,70 +974,70 @@ var LlmChatRequestSchema = z10.object({
560
974
  */
561
975
  responseFormat: LlmResponseFormatSchema.optional(),
562
976
  /** SSE streaming (default) vs a single JSON body. */
563
- stream: z10.boolean().default(true)
977
+ stream: z12.boolean().default(true)
564
978
  });
565
- var LlmChatResponseSchema = z10.object({
566
- model: z10.string(),
567
- content: z10.string(),
568
- stopReason: z10.string(),
979
+ var LlmChatResponseSchema = z12.object({
980
+ model: z12.string(),
981
+ content: z12.string(),
982
+ stopReason: z12.string(),
569
983
  usage: LlmUsageSchema
570
984
  });
571
- var LlmStreamDeltaSchema = z10.object({ text: z10.string() });
572
- var LlmStreamDoneSchema = z10.object({
573
- stopReason: z10.string(),
985
+ var LlmStreamDeltaSchema = z12.object({ text: z12.string() });
986
+ var LlmStreamDoneSchema = z12.object({
987
+ stopReason: z12.string(),
574
988
  usage: LlmUsageSchema
575
989
  });
576
- var LlmStreamErrorSchema = z10.object({
577
- code: z10.string(),
578
- message: z10.string()
990
+ var LlmStreamErrorSchema = z12.object({
991
+ code: z12.string(),
992
+ message: z12.string()
579
993
  });
580
994
 
581
995
  // ../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({
996
+ import { z as z13 } from "zod";
997
+ var OpenAiRoleSchema = z13.enum(["system", "developer", "user", "assistant", "tool"]);
998
+ var OpenAiMessageSchema = z13.object({
585
999
  role: OpenAiRoleSchema,
586
- content: z11.union([z11.string(), z11.array(z11.unknown())]).nullish(),
1000
+ content: z13.union([z13.string(), z13.array(z13.unknown())]).nullish(),
587
1001
  /** 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(),
1002
+ tool_calls: z13.array(z13.unknown()).optional(),
1003
+ tool_call_id: z13.string().optional(),
1004
+ name: z13.string().optional()
1005
+ });
1006
+ var OpenAiStreamOptionsSchema = z13.object({
1007
+ include_usage: z13.boolean().optional()
1008
+ });
1009
+ var OpenAiResponseFormatSchema = z13.discriminatedUnion("type", [
1010
+ z13.object({
1011
+ type: z13.literal("json_schema"),
1012
+ json_schema: z13.object({
1013
+ name: z13.string().optional(),
1014
+ schema: z13.record(z13.string(), z13.unknown()),
1015
+ strict: z13.boolean().nullish(),
602
1016
  /**
603
1017
  * Declared so the codec can reject it rather than zod silently stripping it.
604
1018
  * It is honorable on the OpenAI path but has no Anthropic equivalent, so
605
1019
  * forwarding it would make behaviour depend on the backing vendor — the same
606
1020
  * provider leak that got `json_object` rejected (ADR-0034).
607
1021
  */
608
- description: z11.string().optional()
1022
+ description: z13.string().optional()
609
1023
  })
610
1024
  }),
611
- z11.object({ type: z11.literal("json_object") }),
612
- z11.object({ type: z11.literal("text") })
1025
+ z13.object({ type: z13.literal("json_object") }),
1026
+ z13.object({ type: z13.literal("text") })
613
1027
  ]);
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(),
1028
+ var OpenAiChatCompletionRequestSchema = z13.object({
1029
+ model: z13.string().min(1),
1030
+ messages: z13.array(OpenAiMessageSchema).min(1),
1031
+ max_tokens: z13.int().positive().max(128e3).optional(),
1032
+ max_completion_tokens: z13.int().positive().max(128e3).optional(),
1033
+ temperature: z13.number().optional(),
1034
+ top_p: z13.number().optional(),
1035
+ stop: z13.union([z13.string(), z13.array(z13.string())]).optional(),
1036
+ stream: z13.boolean().optional(),
623
1037
  stream_options: OpenAiStreamOptionsSchema.optional(),
624
1038
  /** 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(),
1039
+ tools: z13.array(z13.unknown()).optional(),
1040
+ tool_choice: z13.unknown().optional(),
627
1041
  /**
628
1042
  * Structured output (ADR-0034). Parsed, not rejected — see the union above.
629
1043
  * `.nullish()` because clients and proxies that serialize every field send
@@ -634,193 +1048,232 @@ var OpenAiChatCompletionRequestSchema = z11.object({
634
1048
  // Behaviour-changing params the platform does not honor in v1. Declared (not
635
1049
  // stripped) so the codec can reject them with a 400 rather than silently drop
636
1050
  // 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()
1051
+ n: z13.unknown().optional(),
1052
+ seed: z13.unknown().optional(),
1053
+ logit_bias: z13.unknown().optional(),
1054
+ presence_penalty: z13.unknown().optional(),
1055
+ frequency_penalty: z13.unknown().optional(),
1056
+ logprobs: z13.unknown().optional(),
1057
+ top_logprobs: z13.unknown().optional()
1058
+ });
1059
+ var OpenAiUsageSchema = z13.object({
1060
+ prompt_tokens: z13.int().nonnegative(),
1061
+ completion_tokens: z13.int().nonnegative(),
1062
+ total_tokens: z13.int().nonnegative()
1063
+ });
1064
+ var OpenAiChatCompletionResponseSchema = z13.object({
1065
+ id: z13.string(),
1066
+ object: z13.literal("chat.completion"),
1067
+ created: z13.int(),
1068
+ model: z13.string(),
1069
+ choices: z13.array(
1070
+ z13.object({
1071
+ index: z13.int(),
1072
+ message: z13.object({
1073
+ role: z13.literal("assistant"),
1074
+ content: z13.string()
661
1075
  }),
662
- finish_reason: z11.string()
1076
+ finish_reason: z13.string()
663
1077
  })
664
1078
  ),
665
1079
  usage: OpenAiUsageSchema
666
1080
  });
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()
1081
+ var OpenAiChatCompletionChunkSchema = z13.object({
1082
+ id: z13.string(),
1083
+ object: z13.literal("chat.completion.chunk"),
1084
+ created: z13.int(),
1085
+ model: z13.string(),
1086
+ choices: z13.array(
1087
+ z13.object({
1088
+ index: z13.int(),
1089
+ delta: z13.object({
1090
+ role: z13.literal("assistant").optional(),
1091
+ content: z13.string().optional()
678
1092
  }),
679
- finish_reason: z11.string().nullable()
1093
+ finish_reason: z13.string().nullable()
680
1094
  })
681
1095
  ),
682
1096
  usage: OpenAiUsageSchema.nullish()
683
1097
  });
684
- var OpenAiModelSchema = z11.object({
685
- id: z11.string(),
686
- object: z11.literal("model"),
687
- created: z11.int(),
688
- owned_by: z11.string()
1098
+ var OpenAiModelSchema = z13.object({
1099
+ id: z13.string(),
1100
+ object: z13.literal("model"),
1101
+ created: z13.int(),
1102
+ owned_by: z13.string()
689
1103
  });
690
- var OpenAiModelListSchema = z11.object({
691
- object: z11.literal("list"),
692
- data: z11.array(OpenAiModelSchema)
1104
+ var OpenAiModelListSchema = z13.object({
1105
+ object: z13.literal("list"),
1106
+ data: z13.array(OpenAiModelSchema)
693
1107
  });
694
1108
 
695
1109
  // ../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);
1110
+ import { z as z15 } from "zod";
1111
+
1112
+ // ../shared/src/principal.ts
1113
+ import { z as z14 } from "zod";
1114
+ var PRINCIPAL_KINDS = ["user", "password", "anon", "dev"];
1115
+ var PrincipalKindSchema = z14.enum(PRINCIPAL_KINDS);
1116
+
1117
+ // ../shared/src/usage.ts
1118
+ var GATEWAY_OUTCOMES = [
1119
+ "ok",
1120
+ "error",
1121
+ "refusal",
1122
+ "quota_blocked",
1123
+ "conflict",
1124
+ "forbidden"
1125
+ ];
1126
+ var GatewayOutcomeSchema = z15.enum(GATEWAY_OUTCOMES);
699
1127
  var USAGE_RANGES = ["24h", "7d", "30d"];
700
- var UsageRangeSchema = z12.enum(USAGE_RANGES);
1128
+ var UsageRangeSchema = z15.enum(USAGE_RANGES);
701
1129
  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(),
1130
+ var PlatformRangeSchema = z15.enum(PLATFORM_RANGES);
1131
+ var UsageSeriesPointSchema = z15.object({
1132
+ bucket: z15.iso.datetime(),
1133
+ costUsd: z15.number().nonnegative(),
1134
+ tokens: z15.int().nonnegative(),
1135
+ requests: z15.int().nonnegative()
1136
+ });
1137
+ var UsageSummarySchema = z15.object({
1138
+ appId: z15.uuid(),
711
1139
  /** The rolling range these figures cover. */
712
1140
  range: UsageRangeSchema,
713
- requests: z12.int().nonnegative(),
714
- inputTokens: z12.int().nonnegative(),
715
- outputTokens: z12.int().nonnegative(),
1141
+ requests: z15.int().nonnegative(),
1142
+ inputTokens: z15.int().nonnegative(),
1143
+ outputTokens: z15.int().nonnegative(),
716
1144
  /** Cache-aware input token totals (0 until prompt caching is enabled). */
717
- cacheReadInputTokens: z12.int().nonnegative(),
718
- cacheCreationInputTokens: z12.int().nonnegative(),
1145
+ cacheReadInputTokens: z15.int().nonnegative(),
1146
+ cacheCreationInputTokens: z15.int().nonnegative(),
719
1147
  /** Estimated spend in USD over the window at current rates (./pricing.ts). */
720
- costUsd: z12.number().nonnegative(),
1148
+ costUsd: z15.number().nonnegative(),
721
1149
  /** 95th-percentile upstream latency (ms) over the window; null when no timed calls. */
722
- latencyP95Ms: z12.number().nonnegative().nullable(),
1150
+ latencyP95Ms: z15.number().nonnegative().nullable(),
723
1151
  /** 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(),
1152
+ errorRate: z15.number().min(0).max(1),
1153
+ /** Count of calls keyed by outcome — see {@link GATEWAY_OUTCOMES}. */
1154
+ byOutcome: z15.record(z15.string(), z15.int().nonnegative()),
1155
+ byModel: z15.array(
1156
+ z15.object({
1157
+ model: z15.string(),
1158
+ tokens: z15.int().nonnegative(),
1159
+ requests: z15.int().nonnegative(),
732
1160
  /** Estimated spend in USD for this model over the window. */
733
- costUsd: z12.number().nonnegative()
1161
+ costUsd: z15.number().nonnegative()
734
1162
  })
735
1163
  ),
736
1164
  /** Dense, zero-filled buckets across the range, oldest-first, for the trend chart. */
737
- series: z12.array(UsageSeriesPointSchema),
1165
+ series: z15.array(UsageSeriesPointSchema),
738
1166
  /**
739
1167
  * Today-since-midnight totals, independent of `range` — backs the daily-cap
740
1168
  * gauge (the budget the edge enforces is per calendar day).
741
1169
  */
742
- today: z12.object({
743
- tokens: z12.int().nonnegative(),
744
- costUsd: z12.number().nonnegative()
1170
+ today: z15.object({
1171
+ tokens: z15.int().nonnegative(),
1172
+ costUsd: z15.number().nonnegative()
745
1173
  })
746
1174
  });
747
- var GatewayCallSchema = z12.object({
748
- id: z12.uuid(),
749
- appId: z12.uuid(),
1175
+ var GatewayCallSchema = z15.object({
1176
+ id: z15.uuid(),
1177
+ appId: z15.uuid(),
750
1178
  /** 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(),
1179
+ slug: z15.string().nullable(),
1180
+ userOid: z15.string(),
1181
+ /**
1182
+ * The caller's claims as captured when the call was made — the **display half**
1183
+ * (`App.ownerName`/`ownerEmail` applied to the ledger). Render these; never
1184
+ * compare them, and never join on them. Null for anonymous, shared-password and
1185
+ * dev-token callers, and for rows predating the columns, in which case the UI
1186
+ * falls back to `userOid` — which is Entra's pairwise `sub` and identifies
1187
+ * nobody, so it is a last resort rather than a label.
1188
+ */
1189
+ userName: z15.string().nullable(),
1190
+ userEmail: z15.string().nullable(),
1191
+ /**
1192
+ * Which kind of principal made the call. Read this instead of parsing
1193
+ * `userOid` — see {@link PrincipalKindSchema} for why the obvious shape test is
1194
+ * unsound. Null on rows written before the column, which render from `userOid`.
1195
+ */
1196
+ userKind: PrincipalKindSchema.nullable(),
1197
+ capability: z15.string(),
1198
+ model: z15.string(),
1199
+ inputTokens: z15.int().nonnegative(),
1200
+ outputTokens: z15.int().nonnegative(),
1201
+ cacheReadInputTokens: z15.int().nonnegative(),
1202
+ cacheCreationInputTokens: z15.int().nonnegative(),
759
1203
  /** Estimated spend in USD for this single call at current rates. */
760
- costUsd: z12.number().nonnegative(),
1204
+ costUsd: z15.number().nonnegative(),
761
1205
  /** Upstream round-trip latency in ms (0 when not measured). */
762
- durationMs: z12.int().nonnegative(),
1206
+ durationMs: z15.int().nonnegative(),
763
1207
  /** Upstream/egress HTTP status — set for `fetch`; null for streamed `llm`. */
764
- statusCode: z12.int().nullable(),
1208
+ statusCode: z15.int().nullable(),
1209
+ /**
1210
+ * Request path of the proxied call — set for `fetch`; null otherwise. The
1211
+ * target's query string is excluded (see `redactFetchTarget` in `./logging.ts`)
1212
+ * because that is where credentials are conventionally placed — but a path can
1213
+ * carry one too, and nothing here detects that. See ADR-0021.
1214
+ */
1215
+ path: z15.string().nullable(),
1216
+ /** HTTP method of the proxied call — set for `fetch`; null otherwise. */
1217
+ method: z15.string().nullable(),
765
1218
  /** LLM stop reason; null for non-LLM calls. */
766
- stopReason: z12.string().nullable(),
1219
+ stopReason: z15.string().nullable(),
767
1220
  /** Short upstream error string; null on success. */
768
- errorDetail: z12.string().nullable(),
1221
+ errorDetail: z15.string().nullable(),
769
1222
  outcome: GatewayOutcomeSchema,
770
- createdAt: z12.iso.datetime()
1223
+ createdAt: z15.iso.datetime()
771
1224
  });
772
- var GatewayAuditPageSchema = z12.object({
773
- rows: z12.array(GatewayCallSchema),
1225
+ var GatewayAuditPageSchema = z15.object({
1226
+ rows: z15.array(GatewayCallSchema),
774
1227
  /** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
775
- nextBefore: z12.iso.datetime().optional()
1228
+ nextBefore: z15.iso.datetime().optional()
776
1229
  });
777
- var PlatformUsageSchema = z12.object({
1230
+ var PlatformUsageSchema = z15.object({
778
1231
  /** The rolling range the series + breakdowns cover. */
779
1232
  range: PlatformRangeSchema,
780
1233
  /** Dense, zero-filled daily buckets across the range, oldest-first. */
781
- series: z12.array(UsageSeriesPointSchema),
1234
+ series: z15.array(UsageSeriesPointSchema),
782
1235
  /** 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(),
1236
+ byApp: z15.array(
1237
+ z15.object({
1238
+ slug: z15.string().nullable(),
1239
+ tokens: z15.int().nonnegative(),
1240
+ requests: z15.int().nonnegative(),
788
1241
  /** Estimated spend in USD for this app over the range. */
789
- costUsd: z12.number().nonnegative()
1242
+ costUsd: z15.number().nonnegative()
790
1243
  })
791
1244
  ),
792
1245
  /** Month-to-date headline KPIs (independent of `range`). */
793
- totals: z12.object({
794
- tokensMTD: z12.int().nonnegative(),
795
- requestsMTD: z12.int().nonnegative(),
1246
+ totals: z15.object({
1247
+ tokensMTD: z15.int().nonnegative(),
1248
+ requestsMTD: z15.int().nonnegative(),
796
1249
  /** Estimated month-to-date spend in USD across all apps. */
797
- costMTD: z12.number().nonnegative(),
1250
+ costMTD: z15.number().nonnegative(),
798
1251
  /** Distinct `userOid`s seen month-to-date. */
799
- activeUsers: z12.int().nonnegative()
1252
+ activeUsers: z15.int().nonnegative()
800
1253
  }),
801
1254
  /** 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(),
1255
+ capabilityMix: z15.array(
1256
+ z15.object({
1257
+ capability: z15.string(),
1258
+ tokens: z15.int().nonnegative(),
806
1259
  /** Estimated spend in USD for this capability over the range. */
807
- costUsd: z12.number().nonnegative()
1260
+ costUsd: z15.number().nonnegative()
808
1261
  })
809
1262
  )
810
1263
  });
811
1264
 
812
1265
  // ../shared/src/data.ts
813
- import { z as z14 } from "zod";
1266
+ import { z as z17 } from "zod";
814
1267
 
815
1268
  // ../shared/src/env.ts
816
- import { z as z13 } from "zod";
1269
+ import { z as z16 } from "zod";
817
1270
  var ENVS = ["prod", "dev"];
818
- var EnvSchema = z13.enum(ENVS);
1271
+ var EnvSchema = z16.enum(ENVS);
819
1272
 
820
1273
  // ../shared/src/data.ts
821
- var CollectionItemSchema = z14.object({
822
- id: z14.uuid(),
823
- collection: z14.string(),
1274
+ var CollectionItemSchema = z17.object({
1275
+ id: z17.uuid(),
1276
+ collection: z17.string(),
824
1277
  /**
825
1278
  * Which tier collected the row (dev-mode §5). The runtime roles are RLS-pinned
826
1279
  * to one tier each, but the portal reads across both, so the discriminator has
@@ -829,30 +1282,39 @@ var CollectionItemSchema = z14.object({
829
1282
  */
830
1283
  env: EnvSchema,
831
1284
  /** The submitting user, if authenticated; null for anonymous/public visitors. */
832
- userOid: z14.string().nullable(),
833
- item: z14.unknown(),
1285
+ userOid: z17.string().nullable(),
1286
+ /**
1287
+ * The submitter's captured claims — display half, rendered but never compared
1288
+ * (see `GatewayCallSchema.userName`). Null whenever `userOid` is, and for
1289
+ * password/dev-token principals.
1290
+ */
1291
+ userName: z17.string().nullable(),
1292
+ userEmail: z17.string().nullable(),
1293
+ /** The submitter's kind; null whenever `userOid` is, and on pre-column rows. */
1294
+ userKind: PrincipalKindSchema.nullable(),
1295
+ item: z17.unknown(),
834
1296
  /** Hashed IP / truncated UA for abuse triage; null if not captured. */
835
- meta: z14.unknown().nullable(),
836
- createdAt: z14.iso.datetime()
1297
+ meta: z17.unknown().nullable(),
1298
+ createdAt: z17.iso.datetime()
837
1299
  });
838
- var CollectionItemsPageSchema = z14.object({
839
- rows: z14.array(CollectionItemSchema),
1300
+ var CollectionItemsPageSchema = z17.object({
1301
+ rows: z17.array(CollectionItemSchema),
840
1302
  /** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
841
- nextBefore: z14.iso.datetime().optional()
1303
+ nextBefore: z17.iso.datetime().optional()
842
1304
  });
843
- var CollectionSummarySchema = z14.object({
844
- name: z14.string(),
1305
+ var CollectionSummarySchema = z17.object({
1306
+ name: z17.string(),
845
1307
  env: EnvSchema,
846
- count: z14.int().nonnegative(),
1308
+ count: z17.int().nonnegative(),
847
1309
  /** Newest row's timestamp; null only if the group is somehow empty. */
848
- lastAt: z14.iso.datetime().nullable()
1310
+ lastAt: z17.iso.datetime().nullable()
849
1311
  });
850
1312
 
851
1313
  // ../shared/src/collectionTable.ts
852
1314
  var BOM = String.fromCharCode(65279);
853
1315
 
854
1316
  // ../shared/src/secrets.ts
855
- import { z as z15 } from "zod";
1317
+ import { z as z18 } from "zod";
856
1318
  var FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
857
1319
  "host",
858
1320
  "content-length",
@@ -867,21 +1329,21 @@ var HEADER_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
867
1329
  var HEADER_VALUE_SAFE = /^[\t\x20-\x7e]*$/;
868
1330
  var HEADER_VALUE_WIRE = /^[\t\x20-\x7e\x80-\xff]*$/;
869
1331
  var headerName = (from) => {
870
- const base = z15.string().min(1);
1332
+ const base = z18.string().min(1);
871
1333
  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
1334
  };
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", [
1335
+ var headerTemplate = (from) => from === "request" ? z18.string().max(512).regex(HEADER_VALUE_SAFE) : z18.string().regex(HEADER_VALUE_WIRE);
1336
+ var injectionRecipe = (from) => z18.discriminatedUnion("kind", [
875
1337
  /** `Authorization: Bearer <secret>` — the common case. */
876
- z15.object({ kind: z15.literal("header-bearer") }),
1338
+ z18.object({ kind: z18.literal("header-bearer") }),
877
1339
  /** Arbitrary header; `{}` in `template` is replaced with the secret. */
878
- z15.object({
879
- kind: z15.literal("header"),
1340
+ z18.object({
1341
+ kind: z18.literal("header"),
880
1342
  name: headerName(from),
881
1343
  template: headerTemplate(from).default("{}")
882
1344
  }),
883
1345
  /** Query parameter `?<param>=<secret>`. */
884
- z15.object({ kind: z15.literal("query"), param: z15.string().min(1) }),
1346
+ z18.object({ kind: z18.literal("query"), param: z18.string().min(1) }),
885
1347
  /**
886
1348
  * HMAC over a timestamp. The signed input is the timestamp string **alone** —
887
1349
  * not the method, path, query, or body — so injection is a pure function of
@@ -903,8 +1365,8 @@ var injectionRecipe = (from) => z15.discriminatedUnion("kind", [
903
1365
  * ({@link HmacCredentialSchema}): regenerating the pair changes both, and a
904
1366
  * blob rotates atomically through the existing rotate route.
905
1367
  */
906
- z15.object({
907
- kind: z15.literal("hmac-timestamp"),
1368
+ z18.object({
1369
+ kind: z18.literal("hmac-timestamp"),
908
1370
  /** Header carrying the timestamp that is also the entire signed input. */
909
1371
  timestampHeader: headerName(from),
910
1372
  /** Header carrying the rendered credential + signature. */
@@ -929,15 +1391,15 @@ var injectionRecipe = (from) => z15.discriminatedUnion("kind", [
929
1391
  ]);
930
1392
  var InjectionRecipeSchema = injectionRecipe("request");
931
1393
  var StoredInjectionRecipeSchema = injectionRecipe("stored");
932
- var HmacCredentialSchema = z15.object({
933
- credential: z15.string().min(1),
934
- key: z15.string().min(1)
1394
+ var HmacCredentialSchema = z18.object({
1395
+ credential: z18.string().min(1),
1396
+ key: z18.string().min(1)
935
1397
  });
936
1398
  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),
1399
+ var SecretScopeSchema = z18.enum(SECRET_SCOPES);
1400
+ var SecretMetadataSchema = z18.object({
1401
+ id: z18.string(),
1402
+ name: z18.string().min(1),
941
1403
  scope: SecretScopeSchema,
942
1404
  /** Partition tier (dev-mode §6): a dev fetch injects only `dev` connection secrets. */
943
1405
  env: EnvSchema,
@@ -953,24 +1415,24 @@ var SecretMetadataSchema = z15.object({
953
1415
  * design, so recovery is delete-and-recreate.
954
1416
  */
955
1417
  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),
1418
+ createdBy: z18.string(),
1419
+ createdAt: z18.string(),
1420
+ rotatedAt: z18.string().nullable().optional(),
1421
+ lastUsedAt: z18.string().nullable().optional(),
1422
+ boundApps: z18.array(z18.string()).default([])
1423
+ });
1424
+ var SecretCreateRequestSchema = z18.object({
1425
+ name: z18.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and hyphens"),
1426
+ value: z18.string().min(1),
965
1427
  /** Target tier (dev-mode §6). Defaults `prod`; `dev` configures a dev-tier credential. */
966
1428
  env: EnvSchema.default("prod"),
967
1429
  injection: InjectionRecipeSchema.default({ kind: "header-bearer" })
968
1430
  });
969
- var SecretRotateRequestSchema = z15.object({ value: z15.string().min(1) });
970
- var SecretGrantRequestSchema = z15.object({ appSlug: z15.string().min(1) });
1431
+ var SecretRotateRequestSchema = z18.object({ value: z18.string().min(1) });
1432
+ var SecretGrantRequestSchema = z18.object({ appSlug: z18.string().min(1) });
971
1433
 
972
1434
  // ../shared/src/devTokens.ts
973
- import { z as z16 } from "zod";
1435
+ import { z as z19 } from "zod";
974
1436
  function isValidDevOrigin(input) {
975
1437
  if (input.includes("*")) return false;
976
1438
  let url;
@@ -984,47 +1446,47 @@ function isValidDevOrigin(input) {
984
1446
  if (url.username !== "" || url.password !== "") return false;
985
1447
  return input === url.origin || input === `${url.origin}/`;
986
1448
  }
987
- var DevOriginSchema = z16.string().refine(
1449
+ var DevOriginSchema = z19.string().refine(
988
1450
  isValidDevOrigin,
989
1451
  "must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
990
1452
  ).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),
1453
+ var TtlDaysSchema = z19.number().int().min(1).max(365).optional();
1454
+ var DevTokenMintRequestSchema = z19.object({
1455
+ origins: z19.array(DevOriginSchema).min(1).max(20),
994
1456
  ttlDays: TtlDaysSchema
995
1457
  });
996
- var DevTokenRotateRequestSchema = z16.object({
1458
+ var DevTokenRotateRequestSchema = z19.object({
997
1459
  ttlDays: TtlDaysSchema
998
1460
  });
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()
1461
+ var DevTokenMetadataSchema = z19.object({
1462
+ id: z19.string(),
1463
+ developerOid: z19.string(),
1464
+ origins: z19.array(z19.string()),
1465
+ expiresAt: z19.string(),
1466
+ revokedAt: z19.string().nullable().optional(),
1467
+ createdAt: z19.string()
1006
1468
  });
1007
- var DevTokenMintResponseSchema = z16.object({
1008
- token: z16.string(),
1469
+ var DevTokenMintResponseSchema = z19.object({
1470
+ token: z19.string(),
1009
1471
  metadata: DevTokenMetadataSchema
1010
1472
  });
1011
1473
 
1012
1474
  // ../shared/src/instruction.ts
1013
- import { z as z17 } from "zod";
1475
+ import { z as z20 } from "zod";
1014
1476
  var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
1015
- var InstructionCapabilitySchema = z17.enum(INSTRUCTION_CAPABILITIES);
1016
- var AttestedInstructionSchema = z17.object({
1477
+ var InstructionCapabilitySchema = z20.enum(INSTRUCTION_CAPABILITIES);
1478
+ var AttestedInstructionSchema = z20.object({
1017
1479
  /** App the call is attributed to (registry app id). */
1018
- appId: z17.string().min(1),
1480
+ appId: z20.string().min(1),
1019
1481
  /** Authenticated user, or the anonymous sentinel on `public` apps. */
1020
- userOid: z17.string().min(1),
1482
+ userOid: z20.string().min(1),
1021
1483
  capability: InstructionCapabilitySchema,
1022
1484
  /** The allowlisted origin the edge authorized (scheme + host + port). */
1023
- origin: z17.url(),
1485
+ origin: z20.url(),
1024
1486
  /** Connection (secret) name to inject, if this is a secret-backed call. */
1025
- connection: z17.string().min(1).optional(),
1487
+ connection: z20.string().min(1).optional(),
1026
1488
  /** Correlates the edge audit row with the egress call. */
1027
- requestId: z17.string().min(1),
1489
+ requestId: z20.string().min(1),
1028
1490
  /**
1029
1491
  * The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
1030
1492
  * Egress refuses a mismatched verb/resource, so a captured instruction can't be
@@ -1039,8 +1501,8 @@ var AttestedInstructionSchema = z17.object({
1039
1501
  * means "old edge", not tampering — egress asserts ONLY when the claim is
1040
1502
  * present. Make required once a fleet is reliably past deploy.
1041
1503
  */
1042
- method: z17.string().min(1).optional(),
1043
- path: z17.string().optional(),
1504
+ method: z20.string().min(1).optional(),
1505
+ path: z20.string().optional(),
1044
1506
  /**
1045
1507
  * Environment tier this call is scoped to (dev-mode design §6). Egress resolves
1046
1508
  * the connection secret within this tier — a `dev` instruction can never reach a
@@ -1054,7 +1516,7 @@ var INSTRUCTION_TTL_SECONDS = 30;
1054
1516
  var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
1055
1517
 
1056
1518
  // ../shared/src/fetch.ts
1057
- import { z as z18 } from "zod";
1519
+ import { z as z21 } from "zod";
1058
1520
  var FETCH_ERROR_CODES = [
1059
1521
  "forbidden",
1060
1522
  "rate_limited",
@@ -1064,10 +1526,10 @@ var FETCH_ERROR_CODES = [
1064
1526
  "replay",
1065
1527
  "upstream_error"
1066
1528
  ];
1067
- var FetchErrorCodeSchema = z18.enum(FETCH_ERROR_CODES);
1068
- var FetchProxyErrorSchema = z18.object({
1529
+ var FetchErrorCodeSchema = z21.enum(FETCH_ERROR_CODES);
1530
+ var FetchProxyErrorSchema = z21.object({
1069
1531
  code: FetchErrorCodeSchema,
1070
- message: z18.string()
1532
+ message: z21.string()
1071
1533
  });
1072
1534
 
1073
1535
  // src/client.ts
@@ -1079,7 +1541,7 @@ var CliError = class extends Error {
1079
1541
  this.code = code;
1080
1542
  }
1081
1543
  };
1082
- var VersionListSchema = z19.array(VersionSchema);
1544
+ var VersionListSchema = z22.array(VersionSchema);
1083
1545
  var PortalClient = class {
1084
1546
  #baseUrl;
1085
1547
  #tokenProvider;
@@ -1099,6 +1561,15 @@ var PortalClient = class {
1099
1561
  me() {
1100
1562
  return this.#json(PortalMeResponseSchema, "GET", "/api/v1/me", { auth: true });
1101
1563
  }
1564
+ /**
1565
+ * The deployment-rendered agent skill (`GET /api/v1/skill`, ADR-0036) —
1566
+ * `text/markdown`, already substituted with this deployment's hosts, servable
1567
+ * models, and approval baselines. Authed like every other `/api/v1` read.
1568
+ * Powers `helix skill`.
1569
+ */
1570
+ getSkill() {
1571
+ return this.#text("/api/v1/skill", { auth: true });
1572
+ }
1102
1573
  listVersions(slug) {
1103
1574
  return this.#json(VersionListSchema, "GET", `/api/v1/apps/${enc(slug)}/versions`, {
1104
1575
  auth: true
@@ -1156,23 +1627,47 @@ var PortalClient = class {
1156
1627
  }
1157
1628
  return schema.parse(data);
1158
1629
  }
1630
+ /** A non-JSON GET (the markdown skill) — errors still typed via the envelope. */
1631
+ async #text(path3, opts) {
1632
+ const headers = await this.#authHeaders(opts.auth ?? false);
1633
+ const res = await fetch(this.#url(path3), { method: "GET", headers });
1634
+ const body = await res.text();
1635
+ if (!res.ok) {
1636
+ const data = body ? safeJsonParse(body) : void 0;
1637
+ const parsed = ApiErrorSchema.safeParse(data);
1638
+ if (parsed.success) throw new CliError(parsed.data.error.message, parsed.data.error.code);
1639
+ throw new CliError(`request failed (HTTP ${res.status})`);
1640
+ }
1641
+ return body;
1642
+ }
1159
1643
  };
1160
1644
  function enc(segment) {
1161
1645
  return encodeURIComponent(segment);
1162
1646
  }
1647
+ function safeJsonParse(value) {
1648
+ try {
1649
+ return JSON.parse(value);
1650
+ } catch {
1651
+ return void 0;
1652
+ }
1653
+ }
1163
1654
 
1164
1655
  // src/commands.ts
1165
- import { readFile as readFile2 } from "node:fs/promises";
1656
+ import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1657
+ import { resolve } from "node:path";
1658
+
1659
+ // ../deploy-skill/src/index.ts
1660
+ var SKILL_FILENAME = "SKILL.md";
1166
1661
 
1167
1662
  // src/zip.ts
1168
1663
  import { ZipArchive } from "archiver";
1169
1664
  function zipDirectory(dir) {
1170
- return new Promise((resolve, reject) => {
1665
+ return new Promise((resolve2, reject) => {
1171
1666
  const chunks = [];
1172
1667
  const archive = new ZipArchive({ zlib: { level: 9 } });
1173
1668
  archive.on("data", (c) => chunks.push(c));
1174
1669
  archive.on("error", reject);
1175
- archive.on("end", () => resolve(Buffer.concat(chunks)));
1670
+ archive.on("end", () => resolve2(Buffer.concat(chunks)));
1176
1671
  archive.directory(dir, false);
1177
1672
  void archive.finalize();
1178
1673
  });
@@ -1297,9 +1792,16 @@ function requireSlug(config) {
1297
1792
  function parseVisibility(input) {
1298
1793
  if (!input) return void 0;
1299
1794
  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 };
1795
+ const groupIds = input.slice("group:".length).split(",").map((id) => id.trim()).filter((id) => id.length > 0);
1796
+ if (groupIds.length === 0) {
1797
+ throw new CliError("group visibility needs at least one id: group:<id>[,<id>\u2026]");
1798
+ }
1799
+ if (groupIds.length > MAX_VISIBILITY_GROUPS) {
1800
+ throw new CliError(
1801
+ `group visibility takes at most ${MAX_VISIBILITY_GROUPS} ids (got ${groupIds.length})`
1802
+ );
1803
+ }
1804
+ return { mode: "group", groupIds };
1303
1805
  }
1304
1806
  if (input === "internal" || input === "password" || input === "public") {
1305
1807
  return { mode: input };
@@ -1309,7 +1811,9 @@ function parseVisibility(input) {
1309
1811
  '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
1812
  );
1311
1813
  }
1312
- throw new CliError(`invalid visibility "${input}" (internal | group:<id> | password | public)`);
1814
+ throw new CliError(
1815
+ `invalid visibility "${input}" (internal | group:<id>[,<id>\u2026] | password | public)`
1816
+ );
1313
1817
  }
1314
1818
  function printVersion(v) {
1315
1819
  console.log(` version ${v.number} (${v.status}) \u2014 ${v.id}`);
@@ -1395,6 +1899,12 @@ async function whoamiCommand(client) {
1395
1899
  const me = await client.me();
1396
1900
  console.log(`${me.sub} (via ${me.via}${me.name ? `, ${me.name}` : ""})`);
1397
1901
  }
1902
+ async function skillCommand(client, _config, opts) {
1903
+ const skill = await client.getSkill();
1904
+ const outPath = resolve(opts.path ?? SKILL_FILENAME);
1905
+ await writeFile2(outPath, skill, "utf8");
1906
+ console.log(`Wrote skill to ${outPath}`);
1907
+ }
1398
1908
 
1399
1909
  // src/auth/session.ts
1400
1910
  var REFRESH_MARGIN_MS = 6e4;
@@ -1477,11 +1987,12 @@ Usage:
1477
1987
  helix versions
1478
1988
  helix promote <number>
1479
1989
  helix rollback [number]
1990
+ helix skill [--path <file>] # write this deployment's agent skill to disk
1480
1991
 
1481
1992
  Common flags: --slug <slug> --portal-url <url> --token <token>
1482
1993
  Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; CI/scripts).
1483
1994
  Config file: helix.json { slug, portalUrl, dir }
1484
- Visibility: internal | group:<id> | password | public
1995
+ Visibility: internal | group:<id>[,<id>...] | password | public
1485
1996
  `;
1486
1997
  async function main() {
1487
1998
  const { values, positionals } = parseCliArgs(process.argv.slice(2));
@@ -1511,6 +2022,9 @@ async function main() {
1511
2022
  case "whoami":
1512
2023
  await whoamiCommand(client);
1513
2024
  break;
2025
+ case "skill":
2026
+ await skillCommand(client, config, { path: values.path });
2027
+ break;
1514
2028
  case "deploy":
1515
2029
  await deployCommand(client, config, { promote: values.promote });
1516
2030
  break;