@azx-pbc/helix-cli 0.1.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.
- package/README.md +37 -6
- package/dist/helix.js +1219 -337
- 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
|
|
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
|
-
z.object({ mode: z.literal("
|
|
28
|
-
z.object({ mode: z.literal("group"),
|
|
30
|
+
z.object({ mode: z.literal("internal") }),
|
|
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
|
-
var VISIBILITY_MODES = ["
|
|
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/
|
|
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 =
|
|
39
|
-
id:
|
|
40
|
-
slug:
|
|
41
|
-
displayName:
|
|
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:
|
|
97
|
+
currentVersionId: z3.uuid().nullable(),
|
|
45
98
|
/** When set, the app is archived: the edge serves 410 + Clear-Site-Data (§7). */
|
|
46
|
-
archivedAt:
|
|
47
|
-
createdAt:
|
|
48
|
-
updatedAt:
|
|
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,89 +107,333 @@ 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:
|
|
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()
|
|
58
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
|
|
143
|
+
import { z as z4 } from "zod";
|
|
62
144
|
var VERSION_STATUSES = ["preview", "live", "archived"];
|
|
63
|
-
var VersionStatusSchema =
|
|
64
|
-
var
|
|
65
|
-
|
|
66
|
-
|
|
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:
|
|
172
|
+
number: z4.int().positive(),
|
|
69
173
|
/** Blob key prefix for this version's assets, e.g. `apps/<appId>/7/`. */
|
|
70
|
-
blobPrefix:
|
|
174
|
+
blobPrefix: z4.string().min(1),
|
|
71
175
|
status: VersionStatusSchema,
|
|
72
|
-
createdAt:
|
|
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
|
|
77
|
-
var LlmCapabilitySchema =
|
|
78
|
-
models:
|
|
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:
|
|
191
|
+
dollarsPerDay: z5.number().positive().optional()
|
|
86
192
|
});
|
|
87
|
-
var
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 =
|
|
96
|
-
origin:
|
|
97
|
-
connection:
|
|
223
|
+
var FetchConnectionSchema = z5.object({
|
|
224
|
+
origin: z5.url(),
|
|
225
|
+
connection: z5.string().min(1).optional()
|
|
98
226
|
});
|
|
99
|
-
var FetchCapabilitySchema =
|
|
227
|
+
var FetchCapabilitySchema = z5.object({
|
|
100
228
|
/** Opt-in transparent `fetch` shim injected at serve time (fetch-proxy §3.2). */
|
|
101
|
-
shim:
|
|
229
|
+
shim: z5.boolean().default(false),
|
|
102
230
|
/** Origins reached through the proxy (mode `proxy`); direct stays in `externalOrigins`. */
|
|
103
|
-
origins:
|
|
231
|
+
origins: z5.array(FetchConnectionSchema).default([]),
|
|
104
232
|
/** Per-app daily proxied-request budget; unset ⇒ unbounded (fetch-proxy §7). */
|
|
105
|
-
requestsPerDay:
|
|
233
|
+
requestsPerDay: z5.int().positive().optional()
|
|
106
234
|
});
|
|
107
|
-
var
|
|
235
|
+
var SCOPE_SEGMENT = /^[A-Za-z0-9\-._~]+$/;
|
|
236
|
+
function isValidServiceWorkerScope(scope) {
|
|
237
|
+
if (!scope.startsWith("/") || !scope.endsWith("/")) return false;
|
|
238
|
+
const segments = scope.slice(1, -1).split("/");
|
|
239
|
+
const first = segments[0];
|
|
240
|
+
if (first === void 0) return false;
|
|
241
|
+
if (segments.some((s) => s === "" || s === "." || s === ".." || !SCOPE_SEGMENT.test(s))) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return !first.startsWith("_");
|
|
245
|
+
}
|
|
246
|
+
var OfflineCapabilitySchema = z5.object({
|
|
247
|
+
/**
|
|
248
|
+
* URL path prefix the worker controls, e.g. `/app/`. Leading and trailing
|
|
249
|
+
* slash required; never root, never a `_`-prefixed platform namespace.
|
|
250
|
+
*/
|
|
251
|
+
scope: z5.string().refine(isValidServiceWorkerScope, {
|
|
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/`)"
|
|
253
|
+
})
|
|
254
|
+
});
|
|
255
|
+
var CapabilitiesSchema = z5.object({
|
|
108
256
|
llm: LlmCapabilitySchema.optional(),
|
|
109
257
|
data: DataCapabilitySchema.optional(),
|
|
110
258
|
/** Platform-registered MCP servers this app may reach (§6.1, exposed as REST). */
|
|
111
|
-
mcp:
|
|
259
|
+
mcp: z5.array(z5.string()).default([]),
|
|
112
260
|
/** Extra CSP `connect-src` origins for **direct** browser calls (§4.4). */
|
|
113
|
-
externalOrigins:
|
|
261
|
+
externalOrigins: z5.array(z5.url()).default([]),
|
|
114
262
|
/** Governed outbound HTTP via the fetch-proxy / egress plane (in build, M4.5). */
|
|
115
|
-
fetch: FetchCapabilitySchema.optional()
|
|
263
|
+
fetch: FetchCapabilitySchema.optional(),
|
|
264
|
+
/** Platform-owned, scope-confined service worker for offline cold boot (ADR-0035). */
|
|
265
|
+
offline: OfflineCapabilitySchema.optional()
|
|
116
266
|
});
|
|
117
|
-
var AppManifestSchema =
|
|
267
|
+
var AppManifestSchema = z5.object({
|
|
118
268
|
/** App slug; matches `App.slug`. */
|
|
119
|
-
app:
|
|
269
|
+
app: z5.string().min(1),
|
|
120
270
|
visibility: VisibilitySchema,
|
|
121
271
|
capabilities: CapabilitiesSchema.default({ mcp: [], externalOrigins: [] })
|
|
122
272
|
});
|
|
123
273
|
|
|
124
274
|
// ../shared/src/approval.ts
|
|
125
|
-
import { z as
|
|
275
|
+
import { z as z6 } from "zod";
|
|
126
276
|
|
|
127
277
|
// ../shared/src/pricing.ts
|
|
128
278
|
var MODEL_PRICING = {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
279
|
+
// Anthropic. NB `structuredOutputs` is deliberately absent on 4-7/4-6/sonnet-4-6:
|
|
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
|
+
},
|
|
293
|
+
"claude-fable-5": {
|
|
294
|
+
inputPerMTok: 10,
|
|
295
|
+
outputPerMTok: 50,
|
|
296
|
+
provider: "anthropic",
|
|
297
|
+
structuredOutputs: true
|
|
298
|
+
},
|
|
299
|
+
"claude-opus-5": {
|
|
300
|
+
inputPerMTok: 5,
|
|
301
|
+
outputPerMTok: 25,
|
|
302
|
+
provider: "anthropic",
|
|
303
|
+
structuredOutputs: true
|
|
304
|
+
},
|
|
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.
|
|
308
|
+
"claude-sonnet-5": {
|
|
309
|
+
inputPerMTok: 2,
|
|
310
|
+
outputPerMTok: 10,
|
|
311
|
+
provider: "anthropic",
|
|
312
|
+
structuredOutputs: true
|
|
313
|
+
},
|
|
314
|
+
"claude-opus-4-8": {
|
|
315
|
+
inputPerMTok: 5,
|
|
316
|
+
outputPerMTok: 25,
|
|
317
|
+
provider: "anthropic",
|
|
318
|
+
structuredOutputs: true
|
|
319
|
+
},
|
|
320
|
+
"claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
|
|
321
|
+
"claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
|
|
322
|
+
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15, provider: "anthropic" },
|
|
323
|
+
"claude-haiku-4-5": {
|
|
324
|
+
inputPerMTok: 1,
|
|
325
|
+
outputPerMTok: 5,
|
|
326
|
+
provider: "anthropic",
|
|
327
|
+
structuredOutputs: true
|
|
328
|
+
},
|
|
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.
|
|
396
|
+
"gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10, provider: "openai", structuredOutputs: true },
|
|
397
|
+
"gpt-4o-mini": {
|
|
398
|
+
inputPerMTok: 0.15,
|
|
399
|
+
outputPerMTok: 0.6,
|
|
400
|
+
provider: "openai",
|
|
401
|
+
structuredOutputs: true
|
|
402
|
+
},
|
|
403
|
+
"gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8, provider: "openai", structuredOutputs: true },
|
|
404
|
+
"gpt-4.1-mini": {
|
|
405
|
+
inputPerMTok: 0.4,
|
|
406
|
+
outputPerMTok: 1.6,
|
|
407
|
+
provider: "openai",
|
|
408
|
+
structuredOutputs: true
|
|
409
|
+
},
|
|
410
|
+
"gpt-4.1-nano": {
|
|
411
|
+
inputPerMTok: 0.1,
|
|
412
|
+
outputPerMTok: 0.4,
|
|
413
|
+
provider: "openai",
|
|
414
|
+
structuredOutputs: true
|
|
415
|
+
},
|
|
416
|
+
o3: {
|
|
417
|
+
inputPerMTok: 2,
|
|
418
|
+
outputPerMTok: 8,
|
|
419
|
+
provider: "openai",
|
|
420
|
+
reasoning: true,
|
|
421
|
+
minCompletionTokens: 25e3,
|
|
422
|
+
structuredOutputs: true
|
|
423
|
+
},
|
|
424
|
+
"o4-mini": {
|
|
425
|
+
inputPerMTok: 1.1,
|
|
426
|
+
outputPerMTok: 4.4,
|
|
427
|
+
provider: "openai",
|
|
428
|
+
reasoning: true,
|
|
429
|
+
minCompletionTokens: 25e3,
|
|
430
|
+
structuredOutputs: true
|
|
431
|
+
}
|
|
135
432
|
};
|
|
136
433
|
|
|
137
434
|
// ../shared/src/approval.ts
|
|
138
435
|
var RISK_LEVELS = ["low", "med", "high"];
|
|
139
|
-
var RiskSchema =
|
|
436
|
+
var RiskSchema = z6.enum(RISK_LEVELS);
|
|
140
437
|
var APPROVAL_STATUSES = [
|
|
141
438
|
"pending",
|
|
142
439
|
"approved",
|
|
@@ -144,134 +441,151 @@ var APPROVAL_STATUSES = [
|
|
|
144
441
|
"withdrawn",
|
|
145
442
|
"needs_changes"
|
|
146
443
|
];
|
|
147
|
-
var ApprovalStatusSchema =
|
|
148
|
-
var DeltaSchema =
|
|
149
|
-
path:
|
|
150
|
-
from:
|
|
151
|
-
to:
|
|
152
|
-
});
|
|
153
|
-
var
|
|
154
|
-
|
|
155
|
-
|
|
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(),
|
|
156
468
|
/** Joined for the admin queue / banner; not a column. */
|
|
157
|
-
appSlug:
|
|
158
|
-
appDisplayName:
|
|
469
|
+
appSlug: z6.string().optional(),
|
|
470
|
+
appDisplayName: z6.string().optional(),
|
|
159
471
|
status: ApprovalStatusSchema,
|
|
160
472
|
risk: RiskSchema,
|
|
161
|
-
deltas:
|
|
162
|
-
baseSnapshot:
|
|
163
|
-
requestedBy:
|
|
164
|
-
reason:
|
|
165
|
-
decidedBy:
|
|
166
|
-
decisionNote:
|
|
167
|
-
createdAt:
|
|
168
|
-
decidedAt:
|
|
169
|
-
|
|
170
|
-
|
|
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({
|
|
171
485
|
manifest: AppManifestSchema,
|
|
172
|
-
applied:
|
|
173
|
-
pending:
|
|
486
|
+
applied: z6.array(DeltaSchema),
|
|
487
|
+
pending: z6.string().nullable()
|
|
174
488
|
});
|
|
175
|
-
var VisibilityUpdateResultSchema =
|
|
489
|
+
var VisibilityUpdateResultSchema = z6.object({
|
|
176
490
|
app: AppSchema,
|
|
177
|
-
applied:
|
|
178
|
-
pending:
|
|
491
|
+
applied: z6.array(DeltaSchema),
|
|
492
|
+
pending: z6.string().nullable()
|
|
179
493
|
});
|
|
180
|
-
var ApprovalDecisionRequestSchema =
|
|
181
|
-
note:
|
|
494
|
+
var ApprovalDecisionRequestSchema = z6.object({
|
|
495
|
+
note: z6.string().max(2e3).optional()
|
|
182
496
|
});
|
|
183
497
|
var CURATED_LLM_MODELS = Object.keys(MODEL_PRICING);
|
|
184
498
|
|
|
185
499
|
// ../shared/src/health.ts
|
|
186
|
-
import { z as
|
|
187
|
-
var HealthStateSchema =
|
|
188
|
-
var HealthCheckSchema =
|
|
500
|
+
import { z as z7 } from "zod";
|
|
501
|
+
var HealthStateSchema = z7.enum(["ok", "degraded", "error"]);
|
|
502
|
+
var HealthCheckSchema = z7.object({
|
|
189
503
|
/** Stable identifier an alert rule can key on, e.g. `registry-projection`. */
|
|
190
|
-
name:
|
|
504
|
+
name: z7.string(),
|
|
191
505
|
status: HealthStateSchema,
|
|
192
506
|
/** Operator-facing one-liner: what is wrong, and for how long. */
|
|
193
|
-
detail:
|
|
507
|
+
detail: z7.string().optional(),
|
|
194
508
|
/** Wall-clock instant this check last succeeded. Report-only — never the
|
|
195
509
|
* basis of a staleness decision (see the monotonic note in the edge's
|
|
196
510
|
* registry projection). */
|
|
197
|
-
lastSuccessAt:
|
|
511
|
+
lastSuccessAt: z7.iso.datetime().optional(),
|
|
198
512
|
/** Numeric facts a dashboard or a log-based metric can key on. */
|
|
199
|
-
metrics:
|
|
513
|
+
metrics: z7.record(z7.string(), z7.number()).optional()
|
|
200
514
|
});
|
|
201
|
-
var HealthStatusSchema =
|
|
515
|
+
var HealthStatusSchema = z7.object({
|
|
202
516
|
/** The roll-up: the worst state across `checks` (see `worstHealthState`). */
|
|
203
517
|
status: HealthStateSchema,
|
|
204
|
-
service:
|
|
518
|
+
service: z7.string(),
|
|
205
519
|
/** Process uptime in seconds. */
|
|
206
|
-
uptime:
|
|
520
|
+
uptime: z7.number().nonnegative(),
|
|
207
521
|
/** Absent means the service reports liveness only (portal and egress today). */
|
|
208
|
-
checks:
|
|
522
|
+
checks: z7.array(HealthCheckSchema).optional()
|
|
209
523
|
});
|
|
210
524
|
|
|
211
525
|
// ../shared/src/api.ts
|
|
212
|
-
import { z as
|
|
213
|
-
var CreateAppRequestSchema =
|
|
526
|
+
import { z as z8 } from "zod";
|
|
527
|
+
var CreateAppRequestSchema = z8.object({
|
|
214
528
|
slug: AppSchema.shape.slug,
|
|
215
529
|
displayName: AppSchema.shape.displayName,
|
|
216
|
-
visibility:
|
|
530
|
+
visibility: WritableVisibilitySchema.default({ mode: "internal" }),
|
|
217
531
|
/** Optional per-app capability grant set at create time (architecture §6.3). */
|
|
218
532
|
capabilities: CapabilitiesSchema.optional()
|
|
219
533
|
});
|
|
220
|
-
var SetManifestRequestSchema =
|
|
534
|
+
var SetManifestRequestSchema = z8.object({
|
|
221
535
|
capabilities: CapabilitiesSchema,
|
|
222
536
|
/** Justification carried onto an approval request for any elevated deltas. */
|
|
223
|
-
reason:
|
|
537
|
+
reason: z8.string().max(2e3).optional()
|
|
224
538
|
});
|
|
225
|
-
var SetVisibilityRequestSchema =
|
|
226
|
-
visibility:
|
|
539
|
+
var SetVisibilityRequestSchema = z8.object({
|
|
540
|
+
visibility: WritableVisibilitySchema,
|
|
227
541
|
/** Justification carried onto the approval request when going public. */
|
|
228
|
-
reason:
|
|
542
|
+
reason: z8.string().max(2e3).optional()
|
|
229
543
|
});
|
|
230
544
|
var MIN_PASSWORD_LENGTH = 12;
|
|
231
|
-
var SetPasswordRequestSchema =
|
|
232
|
-
password:
|
|
545
|
+
var SetPasswordRequestSchema = z8.object({
|
|
546
|
+
password: z8.string().min(MIN_PASSWORD_LENGTH).optional()
|
|
233
547
|
});
|
|
234
|
-
var PasswordCredentialResponseSchema =
|
|
548
|
+
var PasswordCredentialResponseSchema = z8.object({
|
|
235
549
|
/** The shared passphrase, in cleartext, for the owner to copy/share. */
|
|
236
|
-
password:
|
|
550
|
+
password: z8.string().min(1),
|
|
237
551
|
/** The app's public URL, prebuilt so the UI can offer a one-click copy. */
|
|
238
|
-
url:
|
|
552
|
+
url: z8.url(),
|
|
239
553
|
/** When the current password was set (ISO 8601). */
|
|
240
|
-
setAt:
|
|
241
|
-
});
|
|
242
|
-
var CspWarningSchema =
|
|
243
|
-
file:
|
|
244
|
-
origin:
|
|
245
|
-
hint:
|
|
246
|
-
});
|
|
247
|
-
var OriginGrantRequestSchema =
|
|
248
|
-
origin:
|
|
249
|
-
reason:
|
|
250
|
-
});
|
|
251
|
-
var CspViolationSchema =
|
|
252
|
-
appId:
|
|
253
|
-
appSlug:
|
|
254
|
-
directive:
|
|
255
|
-
blockedUri:
|
|
256
|
-
count:
|
|
257
|
-
lastSeen:
|
|
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(),
|
|
258
572
|
/**
|
|
259
573
|
* The blocked origin is already permitted by the app's *current* manifest, so
|
|
260
574
|
* this historical report is no longer actionable. Derived at read time
|
|
261
575
|
* (directive-aware — only `connect-src`/`img-src` are widened by an
|
|
262
576
|
* `externalOrigins` grant); the row itself is never deleted.
|
|
263
577
|
*/
|
|
264
|
-
resolved:
|
|
578
|
+
resolved: z8.boolean()
|
|
265
579
|
});
|
|
266
|
-
var CspViolationsPageSchema =
|
|
267
|
-
violations:
|
|
580
|
+
var CspViolationsPageSchema = z8.object({
|
|
581
|
+
violations: z8.array(CspViolationSchema)
|
|
268
582
|
});
|
|
269
|
-
var UploadVersionResponseSchema =
|
|
583
|
+
var UploadVersionResponseSchema = z8.object({
|
|
270
584
|
version: VersionSchema,
|
|
271
|
-
warnings:
|
|
585
|
+
warnings: z8.array(CspWarningSchema)
|
|
272
586
|
});
|
|
273
|
-
var RollbackRequestSchema =
|
|
274
|
-
toNumber:
|
|
587
|
+
var RollbackRequestSchema = z8.object({
|
|
588
|
+
toNumber: z8.int().positive().optional()
|
|
275
589
|
});
|
|
276
590
|
var API_ERROR_CODES = [
|
|
277
591
|
"validation_failed",
|
|
@@ -293,48 +607,121 @@ var API_ERROR_CODES = [
|
|
|
293
607
|
* of its own budget. HTTP 429.
|
|
294
608
|
*/
|
|
295
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",
|
|
296
618
|
/** Gateway: a configured capability is not available on this edge. */
|
|
297
619
|
"capability_unavailable",
|
|
298
620
|
"internal"
|
|
299
621
|
];
|
|
300
|
-
var ApiErrorCodeSchema =
|
|
301
|
-
var ApiErrorSchema =
|
|
302
|
-
error:
|
|
622
|
+
var ApiErrorCodeSchema = z8.enum(API_ERROR_CODES);
|
|
623
|
+
var ApiErrorSchema = z8.object({
|
|
624
|
+
error: z8.object({
|
|
303
625
|
code: ApiErrorCodeSchema,
|
|
304
|
-
message:
|
|
305
|
-
details:
|
|
626
|
+
message: z8.string(),
|
|
627
|
+
details: z8.unknown().optional()
|
|
306
628
|
})
|
|
307
629
|
});
|
|
308
630
|
|
|
309
631
|
// ../shared/src/auth.ts
|
|
310
|
-
import { z as
|
|
311
|
-
var MeResponseSchema =
|
|
312
|
-
user:
|
|
632
|
+
import { z as z9 } from "zod";
|
|
633
|
+
var MeResponseSchema = z9.object({
|
|
634
|
+
user: z9.object({
|
|
313
635
|
/** IdP subject (Entra object id) — stable, safe to key app data on. */
|
|
314
|
-
id:
|
|
315
|
-
displayName:
|
|
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()
|
|
316
653
|
})
|
|
317
654
|
});
|
|
318
|
-
var PortalMeResponseSchema =
|
|
319
|
-
sub:
|
|
655
|
+
var PortalMeResponseSchema = z9.object({
|
|
656
|
+
sub: z9.string(),
|
|
320
657
|
/** How the actor was established: `oidc` or `dev-token`. */
|
|
321
|
-
via:
|
|
322
|
-
name:
|
|
323
|
-
email:
|
|
658
|
+
via: z9.string(),
|
|
659
|
+
name: z9.string().optional(),
|
|
660
|
+
email: z9.string().optional(),
|
|
324
661
|
/**
|
|
325
662
|
* Whether the actor holds the `platform-admin` role. Computed server-side from
|
|
326
663
|
* the actor's group/role claim — the raw ids never cross to the browser. Drives
|
|
327
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.
|
|
328
671
|
*/
|
|
329
|
-
isAdmin:
|
|
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.
|
|
697
|
+
*/
|
|
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()
|
|
330
717
|
});
|
|
331
|
-
var AuthConfigResponseSchema =
|
|
332
|
-
issuer:
|
|
333
|
-
cliClientId:
|
|
718
|
+
var AuthConfigResponseSchema = z9.object({
|
|
719
|
+
issuer: z9.url(),
|
|
720
|
+
cliClientId: z9.string().min(1),
|
|
334
721
|
/** Public client the portal SPA uses for code+PKCE in the browser. */
|
|
335
|
-
webClientId:
|
|
722
|
+
webClientId: z9.string().min(1).optional(),
|
|
336
723
|
/** Expected token audience — part of what the CLI binds cached tokens to. */
|
|
337
|
-
audience:
|
|
724
|
+
audience: z9.string().min(1).optional(),
|
|
338
725
|
/**
|
|
339
726
|
* Whether this deployment permits `public` (anonymous) apps. Drives the SPA's
|
|
340
727
|
* visibility UI — it hides the public option when false. Absent = forbidden
|
|
@@ -342,9 +729,9 @@ var AuthConfigResponseSchema = z8.object({
|
|
|
342
729
|
* opt-in). Server-side enforcement is independent of this hint (portal routes
|
|
343
730
|
* + edge serving).
|
|
344
731
|
*/
|
|
345
|
-
allowPublicApps:
|
|
732
|
+
allowPublicApps: z9.boolean().optional(),
|
|
346
733
|
/** Whether this deployment permits `password` (shared-passphrase) apps. Same shape as {@link allowPublicApps}. */
|
|
347
|
-
allowPasswordApps:
|
|
734
|
+
allowPasswordApps: z9.boolean().optional()
|
|
348
735
|
});
|
|
349
736
|
function portalApiScope(audience) {
|
|
350
737
|
if (!audience || audience.startsWith("urn:")) return null;
|
|
@@ -353,27 +740,159 @@ function portalApiScope(audience) {
|
|
|
353
740
|
}
|
|
354
741
|
|
|
355
742
|
// ../shared/src/deployment.ts
|
|
356
|
-
import { z as
|
|
357
|
-
var DeploymentConfigResponseSchema =
|
|
743
|
+
import { z as z10 } from "zod";
|
|
744
|
+
var DeploymentConfigResponseSchema = z10.object({
|
|
358
745
|
/**
|
|
359
746
|
* Scheme + host + (non-default) port where apps are served, as reachable by
|
|
360
747
|
* this browser (architecture §4.1). The app slug is prepended as a subdomain:
|
|
361
748
|
* `https://<slug>.<host>`.
|
|
362
749
|
*/
|
|
363
|
-
appPublicBase:
|
|
750
|
+
appPublicBase: z10.url(),
|
|
364
751
|
/**
|
|
365
752
|
* Base of the opt-in dev gateway (dev-mode design §3), absent when it is not
|
|
366
753
|
* deployed. Unlike {@link appPublicBase} the slug goes in the *path*, not the
|
|
367
754
|
* host. Absent ⇒ dev mode is unavailable here, so the UI says so instead of
|
|
368
755
|
* printing an unreachable host.
|
|
369
756
|
*/
|
|
370
|
-
devApiBase:
|
|
757
|
+
devApiBase: z10.url().optional(),
|
|
371
758
|
/**
|
|
372
759
|
* Month-to-date platform spend ceiling (USD) for the admin budget watch line.
|
|
373
760
|
* Display-only — the gateway is the choke point, so the rollup is exact, but
|
|
374
761
|
* nothing enforces this. Absent ⇒ no ceiling shown.
|
|
375
762
|
*/
|
|
376
|
-
platformMonthlyUsdCap:
|
|
763
|
+
platformMonthlyUsdCap: z10.number().positive().optional(),
|
|
764
|
+
/**
|
|
765
|
+
* Deploy bundle size caps in megabytes (`DEPLOY_MAX_FILE_MB` /
|
|
766
|
+
* `DEPLOY_MAX_BUNDLE_MB` on the portal) — `deployMaxFileMb` is per file,
|
|
767
|
+
* `deployMaxBundleMb` the whole-archive total. A current portal always sends
|
|
768
|
+
* both; optional only to tolerate one that predates the fields, same as `url`
|
|
769
|
+
* on {@link AppSchema}. Absent means "don't state a number" — a client must not
|
|
770
|
+
* substitute a default, because printing the wrong cap to an agent sends it
|
|
771
|
+
* chasing a rejection it can't see the cause of.
|
|
772
|
+
*/
|
|
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
|
+
})
|
|
377
896
|
});
|
|
378
897
|
|
|
379
898
|
// ../shared/src/scrypt.ts
|
|
@@ -389,225 +908,531 @@ var SCRYPT_PARAMS = {
|
|
|
389
908
|
};
|
|
390
909
|
|
|
391
910
|
// ../shared/src/llm.ts
|
|
392
|
-
import { z as
|
|
393
|
-
var LlmMessageSchema =
|
|
394
|
-
role:
|
|
395
|
-
content:
|
|
396
|
-
});
|
|
397
|
-
var LlmUsageSchema =
|
|
398
|
-
inputTokens:
|
|
399
|
-
outputTokens:
|
|
400
|
-
cacheReadInputTokens:
|
|
401
|
-
cacheCreationInputTokens:
|
|
402
|
-
});
|
|
403
|
-
var
|
|
404
|
-
|
|
405
|
-
|
|
911
|
+
import { z as z12 } from "zod";
|
|
912
|
+
var LlmMessageSchema = z12.object({
|
|
913
|
+
role: z12.enum(["user", "assistant"]),
|
|
914
|
+
content: z12.string().min(1)
|
|
915
|
+
});
|
|
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)
|
|
921
|
+
});
|
|
922
|
+
var MAX_SCHEMA_CHARS = 32768;
|
|
923
|
+
var MAX_SCHEMA_DEPTH = 12;
|
|
924
|
+
function schemaDepth(value, depth = 1) {
|
|
925
|
+
if (value === null || typeof value !== "object") return depth;
|
|
926
|
+
let deepest = depth;
|
|
927
|
+
for (const child of Object.values(value)) {
|
|
928
|
+
const d = schemaDepth(child, depth + 1);
|
|
929
|
+
if (d > deepest) deepest = d;
|
|
930
|
+
if (deepest > MAX_SCHEMA_DEPTH) return deepest;
|
|
931
|
+
}
|
|
932
|
+
return deepest;
|
|
933
|
+
}
|
|
934
|
+
function withinSchemaBudget(schema) {
|
|
935
|
+
let serialized;
|
|
936
|
+
try {
|
|
937
|
+
serialized = JSON.stringify(schema);
|
|
938
|
+
} catch {
|
|
939
|
+
return false;
|
|
940
|
+
}
|
|
941
|
+
return serialized.length <= MAX_SCHEMA_CHARS && schemaDepth(schema) <= MAX_SCHEMA_DEPTH;
|
|
942
|
+
}
|
|
943
|
+
var LlmResponseFormatSchema = z12.object({
|
|
944
|
+
type: z12.literal("json_schema"),
|
|
945
|
+
/** Schema name. OpenAI requires one; defaulted at translation when omitted. */
|
|
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(
|
|
948
|
+
withinSchemaBudget,
|
|
949
|
+
`schema must serialize to <= ${MAX_SCHEMA_CHARS} characters and nest <= ${MAX_SCHEMA_DEPTH} levels`
|
|
950
|
+
)
|
|
951
|
+
// NB there is deliberately no `strict` knob. Anthropic's `output_config.format`
|
|
952
|
+
// always enforces and has no best-effort mode, so a `strict:false` could only be
|
|
953
|
+
// honored on one vendor — the same request would then yield schema-violating JSON
|
|
954
|
+
// on `gpt-*` but not `claude-*`, which is exactly the provider leak this seam
|
|
955
|
+
// exists to prevent. The platform always enforces; see ADR-0034.
|
|
956
|
+
});
|
|
957
|
+
var LlmChatRequestSchema = z12.object({
|
|
958
|
+
model: z12.string().min(1),
|
|
959
|
+
messages: z12.array(LlmMessageSchema).min(1),
|
|
406
960
|
/** Optional system prompt; maps to the vendor's system channel. */
|
|
407
|
-
system:
|
|
408
|
-
maxTokens:
|
|
961
|
+
system: z12.string().optional(),
|
|
962
|
+
maxTokens: z12.int().positive().max(128e3).optional(),
|
|
963
|
+
/** Sampling temperature; forwarded as-is (the vendor validates its own range). */
|
|
964
|
+
temperature: z12.number().optional(),
|
|
965
|
+
/** Nucleus sampling; forwarded as-is. */
|
|
966
|
+
topP: z12.number().optional(),
|
|
967
|
+
/** Stop sequences; normalized to a list at the boundary. */
|
|
968
|
+
stop: z12.array(z12.string()).optional(),
|
|
969
|
+
/**
|
|
970
|
+
* Constrain the completion to a JSON schema (ADR-0034). Refused up front when
|
|
971
|
+
* the requested model can't enforce it (`ModelPrice.structuredOutputs`). The
|
|
972
|
+
* JSON still arrives as ordinary text, so `content` and the SSE `delta` frames
|
|
973
|
+
* are unchanged — callers `JSON.parse` the result.
|
|
974
|
+
*/
|
|
975
|
+
responseFormat: LlmResponseFormatSchema.optional(),
|
|
409
976
|
/** SSE streaming (default) vs a single JSON body. */
|
|
410
|
-
stream:
|
|
977
|
+
stream: z12.boolean().default(true)
|
|
411
978
|
});
|
|
412
|
-
var LlmChatResponseSchema =
|
|
413
|
-
model:
|
|
414
|
-
content:
|
|
415
|
-
stopReason:
|
|
979
|
+
var LlmChatResponseSchema = z12.object({
|
|
980
|
+
model: z12.string(),
|
|
981
|
+
content: z12.string(),
|
|
982
|
+
stopReason: z12.string(),
|
|
416
983
|
usage: LlmUsageSchema
|
|
417
984
|
});
|
|
418
|
-
var LlmStreamDeltaSchema =
|
|
419
|
-
var LlmStreamDoneSchema =
|
|
420
|
-
stopReason:
|
|
985
|
+
var LlmStreamDeltaSchema = z12.object({ text: z12.string() });
|
|
986
|
+
var LlmStreamDoneSchema = z12.object({
|
|
987
|
+
stopReason: z12.string(),
|
|
421
988
|
usage: LlmUsageSchema
|
|
422
989
|
});
|
|
423
|
-
var LlmStreamErrorSchema =
|
|
424
|
-
code:
|
|
425
|
-
message:
|
|
990
|
+
var LlmStreamErrorSchema = z12.object({
|
|
991
|
+
code: z12.string(),
|
|
992
|
+
message: z12.string()
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
// ../shared/src/llmOpenai.ts
|
|
996
|
+
import { z as z13 } from "zod";
|
|
997
|
+
var OpenAiRoleSchema = z13.enum(["system", "developer", "user", "assistant", "tool"]);
|
|
998
|
+
var OpenAiMessageSchema = z13.object({
|
|
999
|
+
role: OpenAiRoleSchema,
|
|
1000
|
+
content: z13.union([z13.string(), z13.array(z13.unknown())]).nullish(),
|
|
1001
|
+
/** Present only on assistant tool-call turns — parsed to reject, never honored. */
|
|
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(),
|
|
1016
|
+
/**
|
|
1017
|
+
* Declared so the codec can reject it rather than zod silently stripping it.
|
|
1018
|
+
* It is honorable on the OpenAI path but has no Anthropic equivalent, so
|
|
1019
|
+
* forwarding it would make behaviour depend on the backing vendor — the same
|
|
1020
|
+
* provider leak that got `json_object` rejected (ADR-0034).
|
|
1021
|
+
*/
|
|
1022
|
+
description: z13.string().optional()
|
|
1023
|
+
})
|
|
1024
|
+
}),
|
|
1025
|
+
z13.object({ type: z13.literal("json_object") }),
|
|
1026
|
+
z13.object({ type: z13.literal("text") })
|
|
1027
|
+
]);
|
|
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(),
|
|
1037
|
+
stream_options: OpenAiStreamOptionsSchema.optional(),
|
|
1038
|
+
/** Declared so the codec can reject tool use in v1 (not supported). */
|
|
1039
|
+
tools: z13.array(z13.unknown()).optional(),
|
|
1040
|
+
tool_choice: z13.unknown().optional(),
|
|
1041
|
+
/**
|
|
1042
|
+
* Structured output (ADR-0034). Parsed, not rejected — see the union above.
|
|
1043
|
+
* `.nullish()` because clients and proxies that serialize every field send
|
|
1044
|
+
* `"response_format": null` to mean "no structured output", which is a request
|
|
1045
|
+
* this surface can serve; refusing it at the envelope would be gratuitous.
|
|
1046
|
+
*/
|
|
1047
|
+
response_format: OpenAiResponseFormatSchema.nullish(),
|
|
1048
|
+
// Behaviour-changing params the platform does not honor in v1. Declared (not
|
|
1049
|
+
// stripped) so the codec can reject them with a 400 rather than silently drop
|
|
1050
|
+
// them — the same "reject, never silently drop" contract as `tools`.
|
|
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()
|
|
1075
|
+
}),
|
|
1076
|
+
finish_reason: z13.string()
|
|
1077
|
+
})
|
|
1078
|
+
),
|
|
1079
|
+
usage: OpenAiUsageSchema
|
|
1080
|
+
});
|
|
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()
|
|
1092
|
+
}),
|
|
1093
|
+
finish_reason: z13.string().nullable()
|
|
1094
|
+
})
|
|
1095
|
+
),
|
|
1096
|
+
usage: OpenAiUsageSchema.nullish()
|
|
1097
|
+
});
|
|
1098
|
+
var OpenAiModelSchema = z13.object({
|
|
1099
|
+
id: z13.string(),
|
|
1100
|
+
object: z13.literal("model"),
|
|
1101
|
+
created: z13.int(),
|
|
1102
|
+
owned_by: z13.string()
|
|
1103
|
+
});
|
|
1104
|
+
var OpenAiModelListSchema = z13.object({
|
|
1105
|
+
object: z13.literal("list"),
|
|
1106
|
+
data: z13.array(OpenAiModelSchema)
|
|
426
1107
|
});
|
|
427
1108
|
|
|
428
1109
|
// ../shared/src/usage.ts
|
|
429
|
-
import { z as
|
|
430
|
-
|
|
431
|
-
|
|
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);
|
|
432
1127
|
var USAGE_RANGES = ["24h", "7d", "30d"];
|
|
433
|
-
var UsageRangeSchema =
|
|
1128
|
+
var UsageRangeSchema = z15.enum(USAGE_RANGES);
|
|
434
1129
|
var PLATFORM_RANGES = ["7d", "30d", "90d"];
|
|
435
|
-
var PlatformRangeSchema =
|
|
436
|
-
var UsageSeriesPointSchema =
|
|
437
|
-
bucket:
|
|
438
|
-
costUsd:
|
|
439
|
-
tokens:
|
|
440
|
-
requests:
|
|
441
|
-
});
|
|
442
|
-
var UsageSummarySchema =
|
|
443
|
-
appId:
|
|
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(),
|
|
444
1139
|
/** The rolling range these figures cover. */
|
|
445
1140
|
range: UsageRangeSchema,
|
|
446
|
-
requests:
|
|
447
|
-
inputTokens:
|
|
448
|
-
outputTokens:
|
|
1141
|
+
requests: z15.int().nonnegative(),
|
|
1142
|
+
inputTokens: z15.int().nonnegative(),
|
|
1143
|
+
outputTokens: z15.int().nonnegative(),
|
|
449
1144
|
/** Cache-aware input token totals (0 until prompt caching is enabled). */
|
|
450
|
-
cacheReadInputTokens:
|
|
451
|
-
cacheCreationInputTokens:
|
|
1145
|
+
cacheReadInputTokens: z15.int().nonnegative(),
|
|
1146
|
+
cacheCreationInputTokens: z15.int().nonnegative(),
|
|
452
1147
|
/** Estimated spend in USD over the window at current rates (./pricing.ts). */
|
|
453
|
-
costUsd:
|
|
1148
|
+
costUsd: z15.number().nonnegative(),
|
|
454
1149
|
/** 95th-percentile upstream latency (ms) over the window; null when no timed calls. */
|
|
455
|
-
latencyP95Ms:
|
|
1150
|
+
latencyP95Ms: z15.number().nonnegative().nullable(),
|
|
456
1151
|
/** Fraction of calls in the window whose outcome was not `ok` (0..1). */
|
|
457
|
-
errorRate:
|
|
458
|
-
/** Count of calls keyed by outcome
|
|
459
|
-
byOutcome:
|
|
460
|
-
byModel:
|
|
461
|
-
|
|
462
|
-
model:
|
|
463
|
-
tokens:
|
|
464
|
-
requests:
|
|
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(),
|
|
465
1160
|
/** Estimated spend in USD for this model over the window. */
|
|
466
|
-
costUsd:
|
|
1161
|
+
costUsd: z15.number().nonnegative()
|
|
467
1162
|
})
|
|
468
1163
|
),
|
|
469
1164
|
/** Dense, zero-filled buckets across the range, oldest-first, for the trend chart. */
|
|
470
|
-
series:
|
|
1165
|
+
series: z15.array(UsageSeriesPointSchema),
|
|
471
1166
|
/**
|
|
472
1167
|
* Today-since-midnight totals, independent of `range` — backs the daily-cap
|
|
473
1168
|
* gauge (the budget the edge enforces is per calendar day).
|
|
474
1169
|
*/
|
|
475
|
-
today:
|
|
476
|
-
tokens:
|
|
477
|
-
costUsd:
|
|
1170
|
+
today: z15.object({
|
|
1171
|
+
tokens: z15.int().nonnegative(),
|
|
1172
|
+
costUsd: z15.number().nonnegative()
|
|
478
1173
|
})
|
|
479
1174
|
});
|
|
480
|
-
var GatewayCallSchema =
|
|
481
|
-
id:
|
|
482
|
-
appId:
|
|
1175
|
+
var GatewayCallSchema = z15.object({
|
|
1176
|
+
id: z15.uuid(),
|
|
1177
|
+
appId: z15.uuid(),
|
|
483
1178
|
/** App slug at read time; null when the app row no longer exists. */
|
|
484
|
-
slug:
|
|
485
|
-
userOid:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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(),
|
|
492
1203
|
/** Estimated spend in USD for this single call at current rates. */
|
|
493
|
-
costUsd:
|
|
1204
|
+
costUsd: z15.number().nonnegative(),
|
|
494
1205
|
/** Upstream round-trip latency in ms (0 when not measured). */
|
|
495
|
-
durationMs:
|
|
1206
|
+
durationMs: z15.int().nonnegative(),
|
|
496
1207
|
/** Upstream/egress HTTP status — set for `fetch`; null for streamed `llm`. */
|
|
497
|
-
statusCode:
|
|
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(),
|
|
498
1218
|
/** LLM stop reason; null for non-LLM calls. */
|
|
499
|
-
stopReason:
|
|
1219
|
+
stopReason: z15.string().nullable(),
|
|
500
1220
|
/** Short upstream error string; null on success. */
|
|
501
|
-
errorDetail:
|
|
1221
|
+
errorDetail: z15.string().nullable(),
|
|
502
1222
|
outcome: GatewayOutcomeSchema,
|
|
503
|
-
createdAt:
|
|
1223
|
+
createdAt: z15.iso.datetime()
|
|
504
1224
|
});
|
|
505
|
-
var GatewayAuditPageSchema =
|
|
506
|
-
rows:
|
|
1225
|
+
var GatewayAuditPageSchema = z15.object({
|
|
1226
|
+
rows: z15.array(GatewayCallSchema),
|
|
507
1227
|
/** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
|
|
508
|
-
nextBefore:
|
|
1228
|
+
nextBefore: z15.iso.datetime().optional()
|
|
509
1229
|
});
|
|
510
|
-
var PlatformUsageSchema =
|
|
1230
|
+
var PlatformUsageSchema = z15.object({
|
|
511
1231
|
/** The rolling range the series + breakdowns cover. */
|
|
512
1232
|
range: PlatformRangeSchema,
|
|
513
1233
|
/** Dense, zero-filled daily buckets across the range, oldest-first. */
|
|
514
|
-
series:
|
|
1234
|
+
series: z15.array(UsageSeriesPointSchema),
|
|
515
1235
|
/** Per-app rollup over the range, busiest-first. */
|
|
516
|
-
byApp:
|
|
517
|
-
|
|
518
|
-
slug:
|
|
519
|
-
tokens:
|
|
520
|
-
requests:
|
|
1236
|
+
byApp: z15.array(
|
|
1237
|
+
z15.object({
|
|
1238
|
+
slug: z15.string().nullable(),
|
|
1239
|
+
tokens: z15.int().nonnegative(),
|
|
1240
|
+
requests: z15.int().nonnegative(),
|
|
521
1241
|
/** Estimated spend in USD for this app over the range. */
|
|
522
|
-
costUsd:
|
|
1242
|
+
costUsd: z15.number().nonnegative()
|
|
523
1243
|
})
|
|
524
1244
|
),
|
|
525
1245
|
/** Month-to-date headline KPIs (independent of `range`). */
|
|
526
|
-
totals:
|
|
527
|
-
tokensMTD:
|
|
528
|
-
requestsMTD:
|
|
1246
|
+
totals: z15.object({
|
|
1247
|
+
tokensMTD: z15.int().nonnegative(),
|
|
1248
|
+
requestsMTD: z15.int().nonnegative(),
|
|
529
1249
|
/** Estimated month-to-date spend in USD across all apps. */
|
|
530
|
-
costMTD:
|
|
1250
|
+
costMTD: z15.number().nonnegative(),
|
|
531
1251
|
/** Distinct `userOid`s seen month-to-date. */
|
|
532
|
-
activeUsers:
|
|
1252
|
+
activeUsers: z15.int().nonnegative()
|
|
533
1253
|
}),
|
|
534
1254
|
/** Token + cost share by capability over the range (essentially all `llm` in M4). */
|
|
535
|
-
capabilityMix:
|
|
536
|
-
|
|
537
|
-
capability:
|
|
538
|
-
tokens:
|
|
1255
|
+
capabilityMix: z15.array(
|
|
1256
|
+
z15.object({
|
|
1257
|
+
capability: z15.string(),
|
|
1258
|
+
tokens: z15.int().nonnegative(),
|
|
539
1259
|
/** Estimated spend in USD for this capability over the range. */
|
|
540
|
-
costUsd:
|
|
1260
|
+
costUsd: z15.number().nonnegative()
|
|
541
1261
|
})
|
|
542
1262
|
)
|
|
543
1263
|
});
|
|
544
1264
|
|
|
545
1265
|
// ../shared/src/data.ts
|
|
546
|
-
import { z as
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
1266
|
+
import { z as z17 } from "zod";
|
|
1267
|
+
|
|
1268
|
+
// ../shared/src/env.ts
|
|
1269
|
+
import { z as z16 } from "zod";
|
|
1270
|
+
var ENVS = ["prod", "dev"];
|
|
1271
|
+
var EnvSchema = z16.enum(ENVS);
|
|
1272
|
+
|
|
1273
|
+
// ../shared/src/data.ts
|
|
1274
|
+
var CollectionItemSchema = z17.object({
|
|
1275
|
+
id: z17.uuid(),
|
|
1276
|
+
collection: z17.string(),
|
|
1277
|
+
/**
|
|
1278
|
+
* Which tier collected the row (dev-mode §5). The runtime roles are RLS-pinned
|
|
1279
|
+
* to one tier each, but the portal reads across both, so the discriminator has
|
|
1280
|
+
* to travel — otherwise a developer's dev-mode test submissions are
|
|
1281
|
+
* indistinguishable from real prod leads in the drain.
|
|
1282
|
+
*/
|
|
1283
|
+
env: EnvSchema,
|
|
550
1284
|
/** The submitting user, if authenticated; null for anonymous/public visitors. */
|
|
551
|
-
userOid:
|
|
552
|
-
|
|
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(),
|
|
553
1296
|
/** Hashed IP / truncated UA for abuse triage; null if not captured. */
|
|
554
|
-
meta:
|
|
555
|
-
createdAt:
|
|
1297
|
+
meta: z17.unknown().nullable(),
|
|
1298
|
+
createdAt: z17.iso.datetime()
|
|
556
1299
|
});
|
|
557
|
-
var CollectionItemsPageSchema =
|
|
558
|
-
rows:
|
|
1300
|
+
var CollectionItemsPageSchema = z17.object({
|
|
1301
|
+
rows: z17.array(CollectionItemSchema),
|
|
559
1302
|
/** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
|
|
560
|
-
nextBefore:
|
|
1303
|
+
nextBefore: z17.iso.datetime().optional()
|
|
1304
|
+
});
|
|
1305
|
+
var CollectionSummarySchema = z17.object({
|
|
1306
|
+
name: z17.string(),
|
|
1307
|
+
env: EnvSchema,
|
|
1308
|
+
count: z17.int().nonnegative(),
|
|
1309
|
+
/** Newest row's timestamp; null only if the group is somehow empty. */
|
|
1310
|
+
lastAt: z17.iso.datetime().nullable()
|
|
561
1311
|
});
|
|
562
1312
|
|
|
563
|
-
// ../shared/src/
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
// ../shared/src/env.ts
|
|
567
|
-
import { z as z13 } from "zod";
|
|
568
|
-
var ENVS = ["prod", "dev"];
|
|
569
|
-
var EnvSchema = z13.enum(ENVS);
|
|
1313
|
+
// ../shared/src/collectionTable.ts
|
|
1314
|
+
var BOM = String.fromCharCode(65279);
|
|
570
1315
|
|
|
571
1316
|
// ../shared/src/secrets.ts
|
|
572
|
-
|
|
1317
|
+
import { z as z18 } from "zod";
|
|
1318
|
+
var FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
1319
|
+
"host",
|
|
1320
|
+
"content-length",
|
|
1321
|
+
"transfer-encoding",
|
|
1322
|
+
"connection",
|
|
1323
|
+
"te",
|
|
1324
|
+
"upgrade",
|
|
1325
|
+
"expect",
|
|
1326
|
+
"trailer"
|
|
1327
|
+
]);
|
|
1328
|
+
var HEADER_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
1329
|
+
var HEADER_VALUE_SAFE = /^[\t\x20-\x7e]*$/;
|
|
1330
|
+
var HEADER_VALUE_WIRE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
1331
|
+
var headerName = (from) => {
|
|
1332
|
+
const base = z18.string().min(1);
|
|
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");
|
|
1334
|
+
};
|
|
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", [
|
|
573
1337
|
/** `Authorization: Bearer <secret>` — the common case. */
|
|
574
|
-
|
|
1338
|
+
z18.object({ kind: z18.literal("header-bearer") }),
|
|
575
1339
|
/** Arbitrary header; `{}` in `template` is replaced with the secret. */
|
|
576
|
-
|
|
577
|
-
kind:
|
|
578
|
-
name:
|
|
579
|
-
template:
|
|
1340
|
+
z18.object({
|
|
1341
|
+
kind: z18.literal("header"),
|
|
1342
|
+
name: headerName(from),
|
|
1343
|
+
template: headerTemplate(from).default("{}")
|
|
580
1344
|
}),
|
|
581
1345
|
/** Query parameter `?<param>=<secret>`. */
|
|
582
|
-
|
|
1346
|
+
z18.object({ kind: z18.literal("query"), param: z18.string().min(1) }),
|
|
1347
|
+
/**
|
|
1348
|
+
* HMAC over a timestamp. The signed input is the timestamp string **alone** —
|
|
1349
|
+
* not the method, path, query, or body — so injection is a pure function of
|
|
1350
|
+
* (private key, now) and needs no request context.
|
|
1351
|
+
*
|
|
1352
|
+
* The canonical form lives in the KIND NAME, deliberately. A scheme that signs
|
|
1353
|
+
* method+path+body is a *sibling kind* (`hmac-request`) — a code change with
|
|
1354
|
+
* tests, reviewed — never an admin-editable canonical-string template.
|
|
1355
|
+
* Canonicalization is where implementations of this family go wrong, and it
|
|
1356
|
+
* does not belong in a text box.
|
|
1357
|
+
*
|
|
1358
|
+
* SHA-256, lowercase-hex, and ISO-8601-with-milliseconds are fixed rather than
|
|
1359
|
+
* configurable. Each would carry a default, and `app_secrets.injection` is a
|
|
1360
|
+
* schemaless JSON column, so adding a knob later is purely additive with no
|
|
1361
|
+
* migration — while fixing them now removes the weak-algorithm and
|
|
1362
|
+
* unencodable-digest failure classes outright.
|
|
1363
|
+
*
|
|
1364
|
+
* The stored value is a JSON blob carrying both halves of the key pair
|
|
1365
|
+
* ({@link HmacCredentialSchema}): regenerating the pair changes both, and a
|
|
1366
|
+
* blob rotates atomically through the existing rotate route.
|
|
1367
|
+
*/
|
|
1368
|
+
z18.object({
|
|
1369
|
+
kind: z18.literal("hmac-timestamp"),
|
|
1370
|
+
/** Header carrying the timestamp that is also the entire signed input. */
|
|
1371
|
+
timestampHeader: headerName(from),
|
|
1372
|
+
/** Header carrying the rendered credential + signature. */
|
|
1373
|
+
authHeader: headerName(from).default("authorization"),
|
|
1374
|
+
/**
|
|
1375
|
+
* Value written to `authHeader`. `{credential}` and `{signature}` are
|
|
1376
|
+
* substituted. Named rather than the `header` kind's bare `{}` because there
|
|
1377
|
+
* are two substitutions: the convention is one value ⇒ `{}`, more than one ⇒
|
|
1378
|
+
* named placeholders. Positional `{}` here would let a swapped template
|
|
1379
|
+
* produce a well-formed header that silently fails to authenticate.
|
|
1380
|
+
*
|
|
1381
|
+
* Both placeholders are required. This recipe writes exactly two headers,
|
|
1382
|
+
* one of them the timestamp, so there is no configuration where the public
|
|
1383
|
+
* credential id travels elsewhere — omitting `{credential}` means the
|
|
1384
|
+
* upstream can never identify the key and every call 401s.
|
|
1385
|
+
*/
|
|
1386
|
+
template: headerTemplate(from).refine((t) => t.includes("{signature}"), "template must contain {signature}").refine((t) => t.includes("{credential}"), "template must contain {credential}")
|
|
1387
|
+
}).refine(
|
|
1388
|
+
(r) => r.timestampHeader !== r.authHeader,
|
|
1389
|
+
"timestampHeader and authHeader must differ \u2014 the second write overwrites the first"
|
|
1390
|
+
)
|
|
583
1391
|
]);
|
|
1392
|
+
var InjectionRecipeSchema = injectionRecipe("request");
|
|
1393
|
+
var StoredInjectionRecipeSchema = injectionRecipe("stored");
|
|
1394
|
+
var HmacCredentialSchema = z18.object({
|
|
1395
|
+
credential: z18.string().min(1),
|
|
1396
|
+
key: z18.string().min(1)
|
|
1397
|
+
});
|
|
584
1398
|
var SECRET_SCOPES = ["app", "global", "platform"];
|
|
585
|
-
var SecretScopeSchema =
|
|
586
|
-
var SecretMetadataSchema =
|
|
587
|
-
id:
|
|
588
|
-
name:
|
|
1399
|
+
var SecretScopeSchema = z18.enum(SECRET_SCOPES);
|
|
1400
|
+
var SecretMetadataSchema = z18.object({
|
|
1401
|
+
id: z18.string(),
|
|
1402
|
+
name: z18.string().min(1),
|
|
589
1403
|
scope: SecretScopeSchema,
|
|
590
1404
|
/** Partition tier (dev-mode §6): a dev fetch injects only `dev` connection secrets. */
|
|
591
1405
|
env: EnvSchema,
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
1406
|
+
/**
|
|
1407
|
+
* The **stored** parser, not the strict one — this field describes a row, and
|
|
1408
|
+
* the SPA re-parses this schema on every response. Using the strict parser here
|
|
1409
|
+
* would let the portal return a name the browser then refuses, moving the read
|
|
1410
|
+
* failure from the server to the client, where it presents as a dead page.
|
|
1411
|
+
*
|
|
1412
|
+
* `null` = the stored recipe is unreadable (it names a reserved header, or is
|
|
1413
|
+
* not a recipe at all). The credential still exists and is still deletable; it
|
|
1414
|
+
* cannot be rotated, and egress fails its hop closed. Recipes are immutable by
|
|
1415
|
+
* design, so recovery is delete-and-recreate.
|
|
1416
|
+
*/
|
|
1417
|
+
injection: StoredInjectionRecipeSchema.nullable(),
|
|
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),
|
|
602
1427
|
/** Target tier (dev-mode §6). Defaults `prod`; `dev` configures a dev-tier credential. */
|
|
603
1428
|
env: EnvSchema.default("prod"),
|
|
604
1429
|
injection: InjectionRecipeSchema.default({ kind: "header-bearer" })
|
|
605
1430
|
});
|
|
606
|
-
var SecretRotateRequestSchema =
|
|
607
|
-
var SecretGrantRequestSchema =
|
|
1431
|
+
var SecretRotateRequestSchema = z18.object({ value: z18.string().min(1) });
|
|
1432
|
+
var SecretGrantRequestSchema = z18.object({ appSlug: z18.string().min(1) });
|
|
608
1433
|
|
|
609
1434
|
// ../shared/src/devTokens.ts
|
|
610
|
-
import { z as
|
|
1435
|
+
import { z as z19 } from "zod";
|
|
611
1436
|
function isValidDevOrigin(input) {
|
|
612
1437
|
if (input.includes("*")) return false;
|
|
613
1438
|
let url;
|
|
@@ -621,47 +1446,47 @@ function isValidDevOrigin(input) {
|
|
|
621
1446
|
if (url.username !== "" || url.password !== "") return false;
|
|
622
1447
|
return input === url.origin || input === `${url.origin}/`;
|
|
623
1448
|
}
|
|
624
|
-
var DevOriginSchema =
|
|
1449
|
+
var DevOriginSchema = z19.string().refine(
|
|
625
1450
|
isValidDevOrigin,
|
|
626
1451
|
"must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
|
|
627
1452
|
).transform((o) => new URL(o).origin);
|
|
628
|
-
var TtlDaysSchema =
|
|
629
|
-
var DevTokenMintRequestSchema =
|
|
630
|
-
origins:
|
|
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),
|
|
631
1456
|
ttlDays: TtlDaysSchema
|
|
632
1457
|
});
|
|
633
|
-
var DevTokenRotateRequestSchema =
|
|
1458
|
+
var DevTokenRotateRequestSchema = z19.object({
|
|
634
1459
|
ttlDays: TtlDaysSchema
|
|
635
1460
|
});
|
|
636
|
-
var DevTokenMetadataSchema =
|
|
637
|
-
id:
|
|
638
|
-
developerOid:
|
|
639
|
-
origins:
|
|
640
|
-
expiresAt:
|
|
641
|
-
revokedAt:
|
|
642
|
-
createdAt:
|
|
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()
|
|
643
1468
|
});
|
|
644
|
-
var DevTokenMintResponseSchema =
|
|
645
|
-
token:
|
|
1469
|
+
var DevTokenMintResponseSchema = z19.object({
|
|
1470
|
+
token: z19.string(),
|
|
646
1471
|
metadata: DevTokenMetadataSchema
|
|
647
1472
|
});
|
|
648
1473
|
|
|
649
1474
|
// ../shared/src/instruction.ts
|
|
650
|
-
import { z as
|
|
1475
|
+
import { z as z20 } from "zod";
|
|
651
1476
|
var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
|
|
652
|
-
var InstructionCapabilitySchema =
|
|
653
|
-
var AttestedInstructionSchema =
|
|
1477
|
+
var InstructionCapabilitySchema = z20.enum(INSTRUCTION_CAPABILITIES);
|
|
1478
|
+
var AttestedInstructionSchema = z20.object({
|
|
654
1479
|
/** App the call is attributed to (registry app id). */
|
|
655
|
-
appId:
|
|
1480
|
+
appId: z20.string().min(1),
|
|
656
1481
|
/** Authenticated user, or the anonymous sentinel on `public` apps. */
|
|
657
|
-
userOid:
|
|
1482
|
+
userOid: z20.string().min(1),
|
|
658
1483
|
capability: InstructionCapabilitySchema,
|
|
659
1484
|
/** The allowlisted origin the edge authorized (scheme + host + port). */
|
|
660
|
-
origin:
|
|
1485
|
+
origin: z20.url(),
|
|
661
1486
|
/** Connection (secret) name to inject, if this is a secret-backed call. */
|
|
662
|
-
connection:
|
|
1487
|
+
connection: z20.string().min(1).optional(),
|
|
663
1488
|
/** Correlates the edge audit row with the egress call. */
|
|
664
|
-
requestId:
|
|
1489
|
+
requestId: z20.string().min(1),
|
|
665
1490
|
/**
|
|
666
1491
|
* The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
|
|
667
1492
|
* Egress refuses a mismatched verb/resource, so a captured instruction can't be
|
|
@@ -676,8 +1501,8 @@ var AttestedInstructionSchema = z16.object({
|
|
|
676
1501
|
* means "old edge", not tampering — egress asserts ONLY when the claim is
|
|
677
1502
|
* present. Make required once a fleet is reliably past deploy.
|
|
678
1503
|
*/
|
|
679
|
-
method:
|
|
680
|
-
path:
|
|
1504
|
+
method: z20.string().min(1).optional(),
|
|
1505
|
+
path: z20.string().optional(),
|
|
681
1506
|
/**
|
|
682
1507
|
* Environment tier this call is scoped to (dev-mode design §6). Egress resolves
|
|
683
1508
|
* the connection secret within this tier — a `dev` instruction can never reach a
|
|
@@ -691,7 +1516,7 @@ var INSTRUCTION_TTL_SECONDS = 30;
|
|
|
691
1516
|
var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
|
|
692
1517
|
|
|
693
1518
|
// ../shared/src/fetch.ts
|
|
694
|
-
import { z as
|
|
1519
|
+
import { z as z21 } from "zod";
|
|
695
1520
|
var FETCH_ERROR_CODES = [
|
|
696
1521
|
"forbidden",
|
|
697
1522
|
"rate_limited",
|
|
@@ -701,10 +1526,10 @@ var FETCH_ERROR_CODES = [
|
|
|
701
1526
|
"replay",
|
|
702
1527
|
"upstream_error"
|
|
703
1528
|
];
|
|
704
|
-
var FetchErrorCodeSchema =
|
|
705
|
-
var FetchProxyErrorSchema =
|
|
1529
|
+
var FetchErrorCodeSchema = z21.enum(FETCH_ERROR_CODES);
|
|
1530
|
+
var FetchProxyErrorSchema = z21.object({
|
|
706
1531
|
code: FetchErrorCodeSchema,
|
|
707
|
-
message:
|
|
1532
|
+
message: z21.string()
|
|
708
1533
|
});
|
|
709
1534
|
|
|
710
1535
|
// src/client.ts
|
|
@@ -716,7 +1541,7 @@ var CliError = class extends Error {
|
|
|
716
1541
|
this.code = code;
|
|
717
1542
|
}
|
|
718
1543
|
};
|
|
719
|
-
var VersionListSchema =
|
|
1544
|
+
var VersionListSchema = z22.array(VersionSchema);
|
|
720
1545
|
var PortalClient = class {
|
|
721
1546
|
#baseUrl;
|
|
722
1547
|
#tokenProvider;
|
|
@@ -736,6 +1561,15 @@ var PortalClient = class {
|
|
|
736
1561
|
me() {
|
|
737
1562
|
return this.#json(PortalMeResponseSchema, "GET", "/api/v1/me", { auth: true });
|
|
738
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
|
+
}
|
|
739
1573
|
listVersions(slug) {
|
|
740
1574
|
return this.#json(VersionListSchema, "GET", `/api/v1/apps/${enc(slug)}/versions`, {
|
|
741
1575
|
auth: true
|
|
@@ -793,23 +1627,47 @@ var PortalClient = class {
|
|
|
793
1627
|
}
|
|
794
1628
|
return schema.parse(data);
|
|
795
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
|
+
}
|
|
796
1643
|
};
|
|
797
1644
|
function enc(segment) {
|
|
798
1645
|
return encodeURIComponent(segment);
|
|
799
1646
|
}
|
|
1647
|
+
function safeJsonParse(value) {
|
|
1648
|
+
try {
|
|
1649
|
+
return JSON.parse(value);
|
|
1650
|
+
} catch {
|
|
1651
|
+
return void 0;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
800
1654
|
|
|
801
1655
|
// src/commands.ts
|
|
802
|
-
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";
|
|
803
1661
|
|
|
804
1662
|
// src/zip.ts
|
|
805
1663
|
import { ZipArchive } from "archiver";
|
|
806
1664
|
function zipDirectory(dir) {
|
|
807
|
-
return new Promise((
|
|
1665
|
+
return new Promise((resolve2, reject) => {
|
|
808
1666
|
const chunks = [];
|
|
809
1667
|
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
810
1668
|
archive.on("data", (c) => chunks.push(c));
|
|
811
1669
|
archive.on("error", reject);
|
|
812
|
-
archive.on("end", () =>
|
|
1670
|
+
archive.on("end", () => resolve2(Buffer.concat(chunks)));
|
|
813
1671
|
archive.directory(dir, false);
|
|
814
1672
|
void archive.finalize();
|
|
815
1673
|
});
|
|
@@ -934,14 +1792,28 @@ function requireSlug(config) {
|
|
|
934
1792
|
function parseVisibility(input) {
|
|
935
1793
|
if (!input) return void 0;
|
|
936
1794
|
if (input.startsWith("group:")) {
|
|
937
|
-
const
|
|
938
|
-
if (
|
|
939
|
-
|
|
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 };
|
|
940
1805
|
}
|
|
941
|
-
if (input === "
|
|
1806
|
+
if (input === "internal" || input === "password" || input === "public") {
|
|
942
1807
|
return { mode: input };
|
|
943
1808
|
}
|
|
944
|
-
|
|
1809
|
+
if (input === "private") {
|
|
1810
|
+
throw new CliError(
|
|
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.'
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
throw new CliError(
|
|
1815
|
+
`invalid visibility "${input}" (internal | group:<id>[,<id>\u2026] | password | public)`
|
|
1816
|
+
);
|
|
945
1817
|
}
|
|
946
1818
|
function printVersion(v) {
|
|
947
1819
|
console.log(` version ${v.number} (${v.status}) \u2014 ${v.id}`);
|
|
@@ -1027,6 +1899,12 @@ async function whoamiCommand(client) {
|
|
|
1027
1899
|
const me = await client.me();
|
|
1028
1900
|
console.log(`${me.sub} (via ${me.via}${me.name ? `, ${me.name}` : ""})`);
|
|
1029
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
|
+
}
|
|
1030
1908
|
|
|
1031
1909
|
// src/auth/session.ts
|
|
1032
1910
|
var REFRESH_MARGIN_MS = 6e4;
|
|
@@ -1109,11 +1987,12 @@ Usage:
|
|
|
1109
1987
|
helix versions
|
|
1110
1988
|
helix promote <number>
|
|
1111
1989
|
helix rollback [number]
|
|
1990
|
+
helix skill [--path <file>] # write this deployment's agent skill to disk
|
|
1112
1991
|
|
|
1113
1992
|
Common flags: --slug <slug> --portal-url <url> --token <token>
|
|
1114
1993
|
Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; CI/scripts).
|
|
1115
1994
|
Config file: helix.json { slug, portalUrl, dir }
|
|
1116
|
-
Visibility:
|
|
1995
|
+
Visibility: internal | group:<id>[,<id>...] | password | public
|
|
1117
1996
|
`;
|
|
1118
1997
|
async function main() {
|
|
1119
1998
|
const { values, positionals } = parseCliArgs(process.argv.slice(2));
|
|
@@ -1143,6 +2022,9 @@ async function main() {
|
|
|
1143
2022
|
case "whoami":
|
|
1144
2023
|
await whoamiCommand(client);
|
|
1145
2024
|
break;
|
|
2025
|
+
case "skill":
|
|
2026
|
+
await skillCommand(client, config, { path: values.path });
|
|
2027
|
+
break;
|
|
1146
2028
|
case "deploy":
|
|
1147
2029
|
await deployCommand(client, config, { promote: values.promote });
|
|
1148
2030
|
break;
|