@azx-pbc/helix-cli 0.2.0 → 0.4.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 +44 -13
- package/dist/helix.js +1734 -550
- package/package.json +4 -3
package/dist/helix.js
CHANGED
|
@@ -10,7 +10,9 @@ var options = {
|
|
|
10
10
|
token: { type: "string" },
|
|
11
11
|
promote: { type: "boolean" },
|
|
12
12
|
"display-name": { type: "string" },
|
|
13
|
+
description: { type: "string" },
|
|
13
14
|
visibility: { type: "string" },
|
|
15
|
+
path: { type: "string" },
|
|
14
16
|
help: { type: "boolean" }
|
|
15
17
|
};
|
|
16
18
|
function parseCliArgs(argv) {
|
|
@@ -19,33 +21,94 @@ function parseCliArgs(argv) {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
// src/client.ts
|
|
22
|
-
import { z as
|
|
24
|
+
import { z as z27 } from "zod";
|
|
23
25
|
|
|
24
26
|
// ../shared/src/visibility.ts
|
|
25
27
|
import { z } from "zod";
|
|
28
|
+
var MAX_VISIBILITY_GROUPS = 10;
|
|
29
|
+
var VisibilityGroupIdsSchema = z.array(z.string().min(1)).max(MAX_VISIBILITY_GROUPS);
|
|
26
30
|
var VisibilitySchema = z.discriminatedUnion("mode", [
|
|
27
31
|
z.object({ mode: z.literal("internal") }),
|
|
28
|
-
z.object({ mode: z.literal("group"),
|
|
32
|
+
z.object({ mode: z.literal("group"), groupIds: VisibilityGroupIdsSchema }),
|
|
29
33
|
z.object({ mode: z.literal("password") }),
|
|
30
34
|
z.object({ mode: z.literal("public") })
|
|
31
35
|
]);
|
|
32
36
|
var VISIBILITY_MODES = ["internal", "group", "password", "public"];
|
|
33
37
|
var VisibilityModeSchema = z.enum(VISIBILITY_MODES);
|
|
38
|
+
function assertWritableGroupIds(ids) {
|
|
39
|
+
return ids.every((id) => !/[,\s]/.test(id));
|
|
40
|
+
}
|
|
41
|
+
var WRITABLE_GROUP_ID_MESSAGE = "a group id cannot contain a comma or whitespace (check for a pasted list)";
|
|
42
|
+
var WritableVisibilitySchema = VisibilitySchema.refine(
|
|
43
|
+
(v) => v.mode !== "group" || assertWritableGroupIds(v.groupIds),
|
|
44
|
+
{ message: WRITABLE_GROUP_ID_MESSAGE, path: ["groupIds"] }
|
|
45
|
+
);
|
|
34
46
|
|
|
35
|
-
// ../shared/src/
|
|
47
|
+
// ../shared/src/directory.ts
|
|
36
48
|
import { z as z2 } from "zod";
|
|
49
|
+
var DirectoryGroupSchema = z2.object({
|
|
50
|
+
/** Object id — a GUID under Entra, a readable fixture id in dev. */
|
|
51
|
+
id: z2.string().min(1),
|
|
52
|
+
displayName: z2.string().min(1),
|
|
53
|
+
/**
|
|
54
|
+
* Whether this is a security group — the only kind the `groups` claim carries,
|
|
55
|
+
* so the only kind worth scoping an app to. Reported rather than filtered so
|
|
56
|
+
* the picker can mark an ineligible group with a reason instead of hiding it;
|
|
57
|
+
* "the group I searched for isn't listed" is a worse failure than a greyed-out
|
|
58
|
+
* row.
|
|
59
|
+
*
|
|
60
|
+
* **Absent means "not read", not "false".** The batch id→name resolve does not
|
|
61
|
+
* always return it, and defaulting an unread flag to `true` made the same group
|
|
62
|
+
* show as eligible in one view and ineligible in another depending on which
|
|
63
|
+
* query resolved last. A client should treat absent as eligible-but-unknown and
|
|
64
|
+
* never let it overwrite a value it does know.
|
|
65
|
+
*/
|
|
66
|
+
securityEnabled: z2.boolean().optional()
|
|
67
|
+
});
|
|
68
|
+
var DIRECTORY_UNAVAILABLE_REASONS = [
|
|
69
|
+
"no-consent",
|
|
70
|
+
"no-credential",
|
|
71
|
+
"not-configured"
|
|
72
|
+
];
|
|
73
|
+
var DirectoryUnavailableReasonSchema = z2.enum(DIRECTORY_UNAVAILABLE_REASONS);
|
|
74
|
+
var DirectoryGroupsResponseSchema = z2.discriminatedUnion("available", [
|
|
75
|
+
z2.object({
|
|
76
|
+
available: z2.literal(true),
|
|
77
|
+
groups: z2.array(DirectoryGroupSchema)
|
|
78
|
+
}),
|
|
79
|
+
z2.object({
|
|
80
|
+
available: z2.literal(false),
|
|
81
|
+
reason: DirectoryUnavailableReasonSchema,
|
|
82
|
+
/** Operator-facing sentence; the SPA shows it in the degradation banner. */
|
|
83
|
+
detail: z2.string().min(1),
|
|
84
|
+
/** The Graph permission an administrator would have to grant. */
|
|
85
|
+
missingPermission: z2.string().min(1).optional()
|
|
86
|
+
})
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// ../shared/src/app.ts
|
|
90
|
+
import { z as z3 } from "zod";
|
|
37
91
|
var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
38
|
-
var
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
92
|
+
var APP_DESCRIPTION_MAX = 500;
|
|
93
|
+
var AppSchema = z3.object({
|
|
94
|
+
id: z3.uuid(),
|
|
95
|
+
slug: z3.string().min(1).max(63).regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
96
|
+
displayName: z3.string().min(1).max(200),
|
|
97
|
+
/**
|
|
98
|
+
* What the app is for, in one sentence. Plain text (≤ {@link APP_DESCRIPTION_MAX}
|
|
99
|
+
* chars) — it renders as a subtitle in list rows and the detail header, so
|
|
100
|
+
* markdown would invite a wall of text in a table cell. Optional on the wire
|
|
101
|
+
* like `url`/`ownerId`: the CLI parses this schema, and every row predating
|
|
102
|
+
* the field carries none. Never `null` — an unset description omits the key.
|
|
103
|
+
*/
|
|
104
|
+
description: z3.string().max(APP_DESCRIPTION_MAX).optional(),
|
|
42
105
|
visibility: VisibilitySchema,
|
|
43
106
|
/** The version currently served; null before the first deploy. */
|
|
44
|
-
currentVersionId:
|
|
107
|
+
currentVersionId: z3.uuid().nullable(),
|
|
45
108
|
/** When set, the app is archived: the edge serves 410 + Clear-Site-Data (§7). */
|
|
46
|
-
archivedAt:
|
|
47
|
-
createdAt:
|
|
48
|
-
updatedAt:
|
|
109
|
+
archivedAt: z3.iso.datetime().nullable(),
|
|
110
|
+
createdAt: z3.iso.datetime(),
|
|
111
|
+
updatedAt: z3.iso.datetime(),
|
|
49
112
|
/**
|
|
50
113
|
* Where this app is served, computed control-plane-side from the deployment's
|
|
51
114
|
* apps base (`APP_PUBLIC_BASE`) — so clients render a URL instead of
|
|
@@ -54,55 +117,514 @@ var AppSchema = z2.object({
|
|
|
54
117
|
* CLI against an older portal. Clients that lack it fall back to composing the
|
|
55
118
|
* slug onto `appPublicBase` from `GET /api/v1/config`.
|
|
56
119
|
*/
|
|
57
|
-
url:
|
|
58
|
-
|
|
120
|
+
url: z3.url().optional(),
|
|
121
|
+
/**
|
|
122
|
+
* Who owns this app — the creator's Entra `oid` (ADR-0048). The **identity**
|
|
123
|
+
* half: this is what `scope=mine` and `ownsApp` compare against, and it is
|
|
124
|
+
* never what a screen renders.
|
|
125
|
+
*
|
|
126
|
+
* Optional for the same reason as `url`: the CLI parses this schema, and rows
|
|
127
|
+
* predating the approvals work carry no owner at all.
|
|
128
|
+
*/
|
|
129
|
+
ownerId: z3.string().optional(),
|
|
130
|
+
/**
|
|
131
|
+
* The owner's claims as captured at create time. The **display** half: render
|
|
132
|
+
* these, never compare them. `ownerId` is the Entra `oid` (ADR-0048) — an
|
|
133
|
+
* opaque directory object id since the re-base, opaque-by-accident (an
|
|
134
|
+
* email) on rows created before it — so display travels separately. Absent
|
|
135
|
+
* for rows created before the columns existed; fall back to `ownerId` (the
|
|
136
|
+
* pre-re-base rows where it is still an email are the one case where the
|
|
137
|
+
* fallback renders something readable).
|
|
138
|
+
*/
|
|
139
|
+
ownerName: z3.string().optional(),
|
|
140
|
+
ownerEmail: z3.string().optional()
|
|
141
|
+
});
|
|
142
|
+
var AppListItemSchema = AppSchema.extend({
|
|
143
|
+
/** When the most recent version was created; null before the first deploy. */
|
|
144
|
+
lastDeployAt: z3.iso.datetime().nullable(),
|
|
145
|
+
/** `number` of the version currently served; null before the first promote. */
|
|
146
|
+
liveVersionNumber: z3.int().positive().nullable(),
|
|
147
|
+
/** Highest `preview`-status version; null when none exists. */
|
|
148
|
+
latestPreviewNumber: z3.int().positive().nullable()
|
|
149
|
+
});
|
|
150
|
+
var APP_LIST_SCOPES = ["mine", "all"];
|
|
151
|
+
var AppListScopeSchema = z3.enum(APP_LIST_SCOPES);
|
|
59
152
|
|
|
60
153
|
// ../shared/src/version.ts
|
|
61
|
-
import { z as
|
|
154
|
+
import { z as z4 } from "zod";
|
|
62
155
|
var VERSION_STATUSES = ["preview", "live", "archived"];
|
|
63
|
-
var VersionStatusSchema =
|
|
64
|
-
var
|
|
65
|
-
|
|
66
|
-
|
|
156
|
+
var VersionStatusSchema = z4.enum(VERSION_STATUSES);
|
|
157
|
+
var DEPLOY_OUTCOMES = [
|
|
158
|
+
"canonical",
|
|
159
|
+
"rerooted",
|
|
160
|
+
"nested",
|
|
161
|
+
"ambiguous",
|
|
162
|
+
"unsalvageable"
|
|
163
|
+
];
|
|
164
|
+
var DeployReportSchema = z4.object({
|
|
165
|
+
/** The planner revision that produced this, for reading old rows later. */
|
|
166
|
+
plannerVersion: z4.int().nonnegative(),
|
|
167
|
+
outcome: z4.enum(DEPLOY_OUTCOMES),
|
|
168
|
+
/** The chosen bundle root (`""` = archive root). */
|
|
169
|
+
root: z4.string().max(1024),
|
|
170
|
+
/** How many files were uploaded after re-rooting. */
|
|
171
|
+
fileCount: z4.int().nonnegative(),
|
|
172
|
+
/** Count of dropped files, keyed by reason (junk/outside-root/…). */
|
|
173
|
+
drops: z4.record(z4.string().max(64), z4.int().nonnegative()).default({}),
|
|
174
|
+
/** Problem kinds surfaced (missing-reference, secret-dropped, …). */
|
|
175
|
+
problems: z4.array(z4.string().max(64)).max(50).default([]),
|
|
176
|
+
/** Candidate roots the planner considered. */
|
|
177
|
+
candidates: z4.array(z4.string().max(1024)).max(20).default([])
|
|
178
|
+
});
|
|
179
|
+
var VersionSchema = z4.object({
|
|
180
|
+
id: z4.uuid(),
|
|
181
|
+
appId: z4.uuid(),
|
|
67
182
|
/** Monotonic per app, 1-based. */
|
|
68
|
-
number:
|
|
183
|
+
number: z4.int().positive(),
|
|
69
184
|
/** Blob key prefix for this version's assets, e.g. `apps/<appId>/7/`. */
|
|
70
|
-
blobPrefix:
|
|
185
|
+
blobPrefix: z4.string().min(1),
|
|
71
186
|
status: VersionStatusSchema,
|
|
72
|
-
createdAt:
|
|
187
|
+
createdAt: z4.iso.datetime(),
|
|
188
|
+
/** Client-asserted salvage provenance (ADR-0038); absent on CLI uploads. */
|
|
189
|
+
deployReport: DeployReportSchema.optional()
|
|
73
190
|
});
|
|
74
191
|
|
|
75
192
|
// ../shared/src/manifest.ts
|
|
76
|
-
import { z as
|
|
77
|
-
|
|
78
|
-
|
|
193
|
+
import { z as z8 } from "zod";
|
|
194
|
+
|
|
195
|
+
// ../shared/src/providers.ts
|
|
196
|
+
import { z as z7 } from "zod";
|
|
197
|
+
|
|
198
|
+
// ../shared/src/env.ts
|
|
199
|
+
import { z as z5 } from "zod";
|
|
200
|
+
var ENVS = ["prod", "dev"];
|
|
201
|
+
var EnvSchema = z5.enum(ENVS);
|
|
202
|
+
|
|
203
|
+
// ../shared/src/secrets.ts
|
|
204
|
+
import { z as z6 } from "zod";
|
|
205
|
+
var FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
206
|
+
"host",
|
|
207
|
+
"content-length",
|
|
208
|
+
"transfer-encoding",
|
|
209
|
+
"connection",
|
|
210
|
+
"te",
|
|
211
|
+
"upgrade",
|
|
212
|
+
"expect",
|
|
213
|
+
"trailer"
|
|
214
|
+
]);
|
|
215
|
+
var HEADER_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
216
|
+
var HEADER_VALUE_SAFE = /^[\t\x20-\x7e]*$/;
|
|
217
|
+
var HEADER_VALUE_WIRE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
218
|
+
var headerName = (from) => {
|
|
219
|
+
const base = z6.string().min(1);
|
|
220
|
+
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");
|
|
221
|
+
};
|
|
222
|
+
var headerTemplate = (from) => from === "request" ? z6.string().max(512).regex(HEADER_VALUE_SAFE) : z6.string().regex(HEADER_VALUE_WIRE);
|
|
223
|
+
var injectionRecipe = (from) => z6.discriminatedUnion("kind", [
|
|
224
|
+
/** `Authorization: Bearer <secret>` — the common case. */
|
|
225
|
+
z6.object({ kind: z6.literal("header-bearer") }),
|
|
226
|
+
/** Arbitrary header; `{}` in `template` is replaced with the secret. */
|
|
227
|
+
z6.object({
|
|
228
|
+
kind: z6.literal("header"),
|
|
229
|
+
name: headerName(from),
|
|
230
|
+
template: headerTemplate(from).default("{}")
|
|
231
|
+
}),
|
|
232
|
+
/** Query parameter `?<param>=<secret>`. */
|
|
233
|
+
z6.object({ kind: z6.literal("query"), param: z6.string().min(1) }),
|
|
234
|
+
/**
|
|
235
|
+
* HMAC over a timestamp. The signed input is the timestamp string **alone** —
|
|
236
|
+
* not the method, path, query, or body — so injection is a pure function of
|
|
237
|
+
* (private key, now) and needs no request context.
|
|
238
|
+
*
|
|
239
|
+
* The canonical form lives in the KIND NAME, deliberately. A scheme that signs
|
|
240
|
+
* method+path+body is a *sibling kind* (`hmac-request`) — a code change with
|
|
241
|
+
* tests, reviewed — never an admin-editable canonical-string template.
|
|
242
|
+
* Canonicalization is where implementations of this family go wrong, and it
|
|
243
|
+
* does not belong in a text box.
|
|
244
|
+
*
|
|
245
|
+
* SHA-256, lowercase-hex, and ISO-8601-with-milliseconds are fixed rather than
|
|
246
|
+
* configurable. Each would carry a default, and `app_secrets.injection` is a
|
|
247
|
+
* schemaless JSON column, so adding a knob later is purely additive with no
|
|
248
|
+
* migration — while fixing them now removes the weak-algorithm and
|
|
249
|
+
* unencodable-digest failure classes outright.
|
|
250
|
+
*
|
|
251
|
+
* The stored value is a JSON blob carrying both halves of the key pair
|
|
252
|
+
* ({@link HmacCredentialSchema}): regenerating the pair changes both, and a
|
|
253
|
+
* blob rotates atomically through the existing rotate route.
|
|
254
|
+
*/
|
|
255
|
+
z6.object({
|
|
256
|
+
kind: z6.literal("hmac-timestamp"),
|
|
257
|
+
/** Header carrying the timestamp that is also the entire signed input. */
|
|
258
|
+
timestampHeader: headerName(from),
|
|
259
|
+
/** Header carrying the rendered credential + signature. */
|
|
260
|
+
authHeader: headerName(from).default("authorization"),
|
|
261
|
+
/**
|
|
262
|
+
* Value written to `authHeader`. `{credential}` and `{signature}` are
|
|
263
|
+
* substituted. Named rather than the `header` kind's bare `{}` because there
|
|
264
|
+
* are two substitutions: the convention is one value ⇒ `{}`, more than one ⇒
|
|
265
|
+
* named placeholders. Positional `{}` here would let a swapped template
|
|
266
|
+
* produce a well-formed header that silently fails to authenticate.
|
|
267
|
+
*
|
|
268
|
+
* Both placeholders are required. This recipe writes exactly two headers,
|
|
269
|
+
* one of them the timestamp, so there is no configuration where the public
|
|
270
|
+
* credential id travels elsewhere — omitting `{credential}` means the
|
|
271
|
+
* upstream can never identify the key and every call 401s.
|
|
272
|
+
*/
|
|
273
|
+
template: headerTemplate(from).refine((t) => t.includes("{signature}"), "template must contain {signature}").refine((t) => t.includes("{credential}"), "template must contain {credential}")
|
|
274
|
+
}).refine(
|
|
275
|
+
(r) => r.timestampHeader !== r.authHeader,
|
|
276
|
+
"timestampHeader and authHeader must differ \u2014 the second write overwrites the first"
|
|
277
|
+
)
|
|
278
|
+
]);
|
|
279
|
+
var InjectionRecipeSchema = injectionRecipe("request");
|
|
280
|
+
var StoredInjectionRecipeSchema = injectionRecipe("stored");
|
|
281
|
+
var HeaderNameSchema = headerName("request");
|
|
282
|
+
var HmacCredentialSchema = z6.object({
|
|
283
|
+
credential: z6.string().min(1),
|
|
284
|
+
key: z6.string().min(1)
|
|
285
|
+
});
|
|
286
|
+
var SECRET_SCOPES = ["app", "global", "platform"];
|
|
287
|
+
var SecretScopeSchema = z6.enum(SECRET_SCOPES);
|
|
288
|
+
var SecretMetadataSchema = z6.object({
|
|
289
|
+
id: z6.string(),
|
|
290
|
+
name: z6.string().min(1),
|
|
291
|
+
scope: SecretScopeSchema,
|
|
292
|
+
/** Partition tier (dev-mode §6): a dev fetch injects only `dev` connection secrets. */
|
|
293
|
+
env: EnvSchema,
|
|
294
|
+
/**
|
|
295
|
+
* The **stored** parser, not the strict one — this field describes a row, and
|
|
296
|
+
* the SPA re-parses this schema on every response. Using the strict parser here
|
|
297
|
+
* would let the portal return a name the browser then refuses, moving the read
|
|
298
|
+
* failure from the server to the client, where it presents as a dead page.
|
|
299
|
+
*
|
|
300
|
+
* `null` = the stored recipe is unreadable (it names a reserved header, or is
|
|
301
|
+
* not a recipe at all). The credential still exists and is still deletable; it
|
|
302
|
+
* cannot be rotated, and egress fails its hop closed. Recipes are immutable by
|
|
303
|
+
* design, so recovery is delete-and-recreate.
|
|
304
|
+
*/
|
|
305
|
+
injection: StoredInjectionRecipeSchema.nullable(),
|
|
306
|
+
createdBy: z6.string(),
|
|
307
|
+
createdAt: z6.string(),
|
|
308
|
+
rotatedAt: z6.string().nullable().optional(),
|
|
309
|
+
lastUsedAt: z6.string().nullable().optional(),
|
|
310
|
+
boundApps: z6.array(z6.string()).default([])
|
|
311
|
+
});
|
|
312
|
+
var SecretCreateRequestSchema = z6.object({
|
|
313
|
+
name: z6.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and hyphens"),
|
|
314
|
+
value: z6.string().min(1),
|
|
315
|
+
/** Target tier (dev-mode §6). Defaults `prod`; `dev` configures a dev-tier credential. */
|
|
316
|
+
env: EnvSchema.default("prod"),
|
|
317
|
+
injection: InjectionRecipeSchema.default({ kind: "header-bearer" })
|
|
318
|
+
});
|
|
319
|
+
var SecretRotateRequestSchema = z6.object({ value: z6.string().min(1) });
|
|
320
|
+
var SecretGrantRequestSchema = z6.object({ appSlug: z6.string().min(1) });
|
|
321
|
+
|
|
322
|
+
// ../shared/src/providers.ts
|
|
323
|
+
var PROVIDER_KINDS = ["rest-delegated"];
|
|
324
|
+
var ProviderKindSchema = z7.enum(PROVIDER_KINDS);
|
|
325
|
+
var PROVIDER_REF_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
326
|
+
var PROVIDER_REF_MAX = 64;
|
|
327
|
+
var ProviderRefSchema = z7.string().min(1).max(PROVIDER_REF_MAX).regex(PROVIDER_REF_PATTERN, "lowercase letters, digits, and hyphens");
|
|
328
|
+
var TokenPlacementSchema = z7.discriminatedUnion("kind", [
|
|
329
|
+
/** `Authorization: Bearer <access token>` — the default placement. */
|
|
330
|
+
z7.strictObject({ kind: z7.literal("header-bearer") }),
|
|
331
|
+
/** The access token verbatim in the named header (no template machinery). */
|
|
332
|
+
z7.strictObject({ kind: z7.literal("header"), name: HeaderNameSchema })
|
|
333
|
+
]);
|
|
334
|
+
var ScopeTokenSchema = z7.string().min(1).max(256).regex(/^[\x21\x23-\x5b\x5d-\x7e]+$/, "must be an RFC 6749 scope token");
|
|
335
|
+
var API_ORIGIN_RULE = "an API destination is an origin \u2014 scheme://host[:port], no path, query, credentials, or fragment";
|
|
336
|
+
var BARE_ORIGIN = /^https?:\/\/[^/?#@\\]*(?:\/)?$/i;
|
|
337
|
+
var ApiOriginSchema = z7.url({ protocol: /^https?$/ }).superRefine((value, ctx) => {
|
|
338
|
+
if (!BARE_ORIGIN.test(value)) {
|
|
339
|
+
ctx.addIssue({ code: "custom", message: API_ORIGIN_RULE });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
let url;
|
|
343
|
+
try {
|
|
344
|
+
url = new URL(value);
|
|
345
|
+
} catch {
|
|
346
|
+
ctx.addIssue({ code: "custom", message: API_ORIGIN_RULE });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
350
|
+
ctx.addIssue({ code: "custom", message: API_ORIGIN_RULE });
|
|
351
|
+
}
|
|
352
|
+
}).transform((value) => new URL(value).origin);
|
|
353
|
+
var URL_PARSER_CONTROLS = /[\t\n\r]/g;
|
|
354
|
+
var ENDPOINT_USERINFO = /^https?:\/\/[/\\]*[^/?#\\]*@/i;
|
|
355
|
+
var OAuthEndpointSchema = z7.url({ protocol: /^https?$/ }).refine(
|
|
356
|
+
(value) => !ENDPOINT_USERINFO.test(value.replace(URL_PARSER_CONTROLS, "")),
|
|
357
|
+
"a vendor endpoint URL must not carry userinfo \u2014 client auth is never configured in the URL"
|
|
358
|
+
);
|
|
359
|
+
var MAX_API_ORIGINS = 16;
|
|
360
|
+
var MAX_REQUESTED_SCOPES = 32;
|
|
361
|
+
var noDuplicates = (values) => new Set(values).size === values.length;
|
|
362
|
+
var ApiOriginsSchema = z7.array(ApiOriginSchema).min(1, "a provider needs at least one API destination \u2014 nothing can bind to it otherwise").max(MAX_API_ORIGINS).refine(noDuplicates, "duplicate API destination");
|
|
363
|
+
var RequestedScopesSchema = z7.array(ScopeTokenSchema).max(MAX_REQUESTED_SCOPES).refine(noDuplicates, "duplicate requested scope");
|
|
364
|
+
var ProviderDisplayNameSchema = z7.string().min(1).max(200);
|
|
365
|
+
var ProviderEditableFieldsSchema = z7.strictObject({
|
|
366
|
+
displayName: ProviderDisplayNameSchema,
|
|
367
|
+
authorizeEndpoint: OAuthEndpointSchema,
|
|
368
|
+
tokenEndpoint: OAuthEndpointSchema,
|
|
369
|
+
requestedScopes: RequestedScopesSchema.default([]),
|
|
370
|
+
apiOrigins: ApiOriginsSchema,
|
|
371
|
+
tokenPlacement: TokenPlacementSchema.default({ kind: "header-bearer" })
|
|
372
|
+
});
|
|
373
|
+
var ProviderConfigSchema = ProviderEditableFieldsSchema.extend({
|
|
374
|
+
ref: ProviderRefSchema,
|
|
375
|
+
kind: ProviderKindSchema.default("rest-delegated")
|
|
376
|
+
});
|
|
377
|
+
var ProviderCreateRequestSchema = ProviderConfigSchema.extend({
|
|
378
|
+
env: EnvSchema,
|
|
379
|
+
clientId: z7.string().min(1),
|
|
380
|
+
clientSecret: z7.string().min(1)
|
|
381
|
+
});
|
|
382
|
+
var undefinedKeysMeanAbsence = (body) => {
|
|
383
|
+
if (typeof body !== "object" || body === null) return body;
|
|
384
|
+
const withoutUndefined = { ...body };
|
|
385
|
+
for (const key of Object.keys(withoutUndefined)) {
|
|
386
|
+
if (withoutUndefined[key] === void 0) delete withoutUndefined[key];
|
|
387
|
+
}
|
|
388
|
+
return withoutUndefined;
|
|
389
|
+
};
|
|
390
|
+
var OWN_PROTO_KEY_RULE = "an own __proto__ key is not a provider field \u2014 the body is refused whole";
|
|
391
|
+
var ProviderUpdateRequestSchema = z7.preprocess(
|
|
392
|
+
(body, ctx) => {
|
|
393
|
+
const normalized = undefinedKeysMeanAbsence(body);
|
|
394
|
+
if (typeof normalized === "object" && normalized !== null && Object.hasOwn(normalized, "__proto__")) {
|
|
395
|
+
ctx.addIssue({ code: "custom", message: OWN_PROTO_KEY_RULE });
|
|
396
|
+
}
|
|
397
|
+
return normalized;
|
|
398
|
+
},
|
|
399
|
+
ProviderEditableFieldsSchema.extend({
|
|
400
|
+
requestedScopes: RequestedScopesSchema,
|
|
401
|
+
tokenPlacement: TokenPlacementSchema,
|
|
402
|
+
revision: z7.int().positive(),
|
|
403
|
+
clientId: z7.string().min(1).optional(),
|
|
404
|
+
clientSecret: z7.string().min(1).optional(),
|
|
405
|
+
/**
|
|
406
|
+
* The invalidation acknowledgement ({@link CONFIRM_INVALIDATION_FIELD}):
|
|
407
|
+
* the client sets it to `true` when submitting a sensitive delta through
|
|
408
|
+
* the review panel. Absent or
|
|
409
|
+
* `false` against a sensitive delta is the 409
|
|
410
|
+
* `confirmation_required` rejection — nothing is applied; against a
|
|
411
|
+
* non-sensitive edit it is irrelevant and ignored. Not stored: it is
|
|
412
|
+
* consent for the mutation, never provider state.
|
|
413
|
+
*/
|
|
414
|
+
confirmInvalidation: z7.boolean().optional()
|
|
415
|
+
})
|
|
416
|
+
);
|
|
417
|
+
var ConnectionProviderSchema = ProviderEditableFieldsSchema.extend({
|
|
418
|
+
requestedScopes: RequestedScopesSchema,
|
|
419
|
+
tokenPlacement: TokenPlacementSchema,
|
|
420
|
+
id: z7.uuid(),
|
|
421
|
+
revision: z7.int().positive(),
|
|
422
|
+
ref: ProviderRefSchema,
|
|
423
|
+
kind: ProviderKindSchema,
|
|
424
|
+
env: EnvSchema,
|
|
425
|
+
clientIdMaterial: z7.string().min(1),
|
|
426
|
+
clientSecretMaterial: z7.string().min(1),
|
|
427
|
+
createdAt: z7.iso.datetime(),
|
|
428
|
+
updatedAt: z7.iso.datetime()
|
|
429
|
+
});
|
|
430
|
+
var ProviderMetadataSchema = ConnectionProviderSchema.omit({
|
|
431
|
+
clientIdMaterial: true,
|
|
432
|
+
clientSecretMaterial: true
|
|
433
|
+
});
|
|
434
|
+
var ProviderListResponseSchema = z7.strictObject({
|
|
435
|
+
callbackUrl: z7.url(),
|
|
436
|
+
providers: z7.array(ProviderMetadataSchema)
|
|
437
|
+
});
|
|
438
|
+
var ProviderExportDocumentSchema = z7.strictObject({
|
|
439
|
+
version: z7.literal(1),
|
|
440
|
+
provider: ProviderConfigSchema
|
|
441
|
+
});
|
|
442
|
+
var CatalogueProviderSchema = z7.strictObject({
|
|
443
|
+
ref: ProviderRefSchema,
|
|
444
|
+
kind: ProviderKindSchema,
|
|
445
|
+
displayName: z7.string().min(1).max(200),
|
|
446
|
+
apiOrigins: ApiOriginsSchema,
|
|
447
|
+
env: EnvSchema
|
|
448
|
+
});
|
|
449
|
+
var SENSITIVE_PROVIDER_FIELDS = [
|
|
450
|
+
"clientId",
|
|
451
|
+
"authorizeEndpoint",
|
|
452
|
+
"tokenEndpoint",
|
|
453
|
+
"requestedScopes",
|
|
454
|
+
"apiOrigins",
|
|
455
|
+
"tokenPlacement"
|
|
456
|
+
];
|
|
457
|
+
var CONFIRM_INVALIDATION_FIELD = "confirmInvalidation";
|
|
458
|
+
var SensitiveProviderFieldSchema = z7.enum(SENSITIVE_PROVIDER_FIELDS);
|
|
459
|
+
var PROVIDER_DIFF_FIELDS = [
|
|
460
|
+
"displayName",
|
|
461
|
+
"authorizeEndpoint",
|
|
462
|
+
"tokenEndpoint",
|
|
463
|
+
"requestedScopes",
|
|
464
|
+
"apiOrigins",
|
|
465
|
+
"tokenPlacement"
|
|
466
|
+
];
|
|
467
|
+
var ProviderDiffFieldSchema = z7.enum(PROVIDER_DIFF_FIELDS);
|
|
468
|
+
var ProviderImportPreviewRequestSchema = z7.strictObject({
|
|
469
|
+
document: ProviderExportDocumentSchema,
|
|
470
|
+
mode: z7.enum(["create", "update"]).optional(),
|
|
471
|
+
env: EnvSchema.optional(),
|
|
472
|
+
targetId: z7.uuid().optional()
|
|
473
|
+
});
|
|
474
|
+
var ProviderImportCollisionSchema = z7.strictObject({
|
|
475
|
+
providerId: z7.uuid(),
|
|
476
|
+
ref: ProviderRefSchema,
|
|
477
|
+
env: EnvSchema
|
|
478
|
+
});
|
|
479
|
+
var ProviderImportPreviewTargetSchema = z7.strictObject({
|
|
480
|
+
providerId: z7.uuid(),
|
|
481
|
+
ref: ProviderRefSchema,
|
|
482
|
+
env: EnvSchema,
|
|
483
|
+
displayName: z7.string().min(1).max(200),
|
|
484
|
+
revision: z7.int().positive()
|
|
485
|
+
});
|
|
486
|
+
var ProviderImportPreviewDiffEntrySchema = z7.strictObject({
|
|
487
|
+
field: ProviderDiffFieldSchema,
|
|
488
|
+
current: z7.union([z7.string(), z7.array(z7.string()), TokenPlacementSchema]),
|
|
489
|
+
imported: z7.union([z7.string(), z7.array(z7.string()), TokenPlacementSchema])
|
|
490
|
+
});
|
|
491
|
+
var ProviderImportPreviewResponseSchema = z7.discriminatedUnion("mode", [
|
|
492
|
+
z7.strictObject({ mode: z7.literal(null), provider: ProviderConfigSchema }),
|
|
493
|
+
z7.strictObject({
|
|
494
|
+
mode: z7.literal("create"),
|
|
495
|
+
env: EnvSchema,
|
|
496
|
+
provider: ProviderConfigSchema,
|
|
497
|
+
collision: ProviderImportCollisionSchema.nullable()
|
|
498
|
+
}),
|
|
499
|
+
z7.strictObject({
|
|
500
|
+
mode: z7.literal("update"),
|
|
501
|
+
target: ProviderImportPreviewTargetSchema,
|
|
502
|
+
diff: z7.array(ProviderImportPreviewDiffEntrySchema),
|
|
503
|
+
sensitiveFields: z7.array(SensitiveProviderFieldSchema)
|
|
504
|
+
})
|
|
505
|
+
]);
|
|
506
|
+
var ProviderImportCreateRequestSchema = z7.strictObject({
|
|
507
|
+
mode: z7.literal("create"),
|
|
508
|
+
document: ProviderExportDocumentSchema,
|
|
509
|
+
env: EnvSchema,
|
|
510
|
+
clientId: z7.string().min(1),
|
|
511
|
+
clientSecret: z7.string().min(1)
|
|
512
|
+
});
|
|
513
|
+
var ProviderImportUpdateRequestSchema = z7.strictObject({
|
|
514
|
+
mode: z7.literal("update"),
|
|
515
|
+
document: ProviderExportDocumentSchema,
|
|
516
|
+
targetId: z7.uuid(),
|
|
517
|
+
revision: z7.int().positive(),
|
|
518
|
+
clientId: z7.string().min(1).optional(),
|
|
519
|
+
clientSecret: z7.string().min(1).optional(),
|
|
520
|
+
[CONFIRM_INVALIDATION_FIELD]: z7.boolean().optional()
|
|
521
|
+
});
|
|
522
|
+
var ProviderImportRequestSchema = z7.discriminatedUnion("mode", [
|
|
523
|
+
ProviderImportCreateRequestSchema,
|
|
524
|
+
ProviderImportUpdateRequestSchema
|
|
525
|
+
]);
|
|
526
|
+
var ProviderImportResponseSchema = z7.strictObject({
|
|
527
|
+
outcome: z7.enum(["created", "updated"]),
|
|
528
|
+
provider: ProviderMetadataSchema
|
|
529
|
+
});
|
|
530
|
+
var BoundAppSchema = z7.strictObject({
|
|
531
|
+
id: z7.uuid(),
|
|
532
|
+
slug: z7.string().max(63).regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
533
|
+
displayName: z7.string().min(1).max(200)
|
|
534
|
+
});
|
|
535
|
+
var ProviderImpactSchema = z7.strictObject({
|
|
536
|
+
providerId: z7.uuid(),
|
|
537
|
+
ref: ProviderRefSchema,
|
|
538
|
+
env: EnvSchema,
|
|
539
|
+
displayName: z7.string().min(1).max(200),
|
|
540
|
+
revision: z7.int().positive(),
|
|
541
|
+
boundApps: z7.array(BoundAppSchema),
|
|
542
|
+
connections: z7.number().int().nonnegative(),
|
|
543
|
+
pendingAttempts: z7.number().int().nonnegative()
|
|
544
|
+
});
|
|
545
|
+
var ConfirmationRequiredDetailsSchema = z7.strictObject({
|
|
546
|
+
impact: ProviderImpactSchema,
|
|
547
|
+
sensitiveFields: z7.array(SensitiveProviderFieldSchema).optional()
|
|
548
|
+
});
|
|
549
|
+
var ProviderDeleteRequestSchema = z7.strictObject({
|
|
550
|
+
[CONFIRM_INVALIDATION_FIELD]: z7.boolean().optional()
|
|
551
|
+
});
|
|
552
|
+
var ProviderDeleteResponseSchema = z7.strictObject({
|
|
553
|
+
outcome: z7.enum(["deleted", "already_removed"])
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// ../shared/src/manifest.ts
|
|
557
|
+
var LlmCapabilitySchema = z8.object({
|
|
558
|
+
models: z8.array(z8.string()).default([]),
|
|
79
559
|
/**
|
|
80
560
|
* Per-app daily LLM spend cap in USD; unset ⇒ unbounded. Denominated in
|
|
81
561
|
* dollars (not tokens) so the cap means the same thing across models — the
|
|
82
562
|
* edge prices each call via `@azx-pbc/shared` pricing and enforces a daily +
|
|
83
563
|
* rolling-hour burst window off the frozen `costMicroUsd` ledger column.
|
|
84
564
|
*/
|
|
85
|
-
dollarsPerDay:
|
|
86
|
-
});
|
|
87
|
-
var
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
var
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
565
|
+
dollarsPerDay: z8.number().positive().optional()
|
|
566
|
+
});
|
|
567
|
+
var KEY_MAX_CHARS = 256;
|
|
568
|
+
var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
569
|
+
function isValidDataKey(key) {
|
|
570
|
+
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
|
|
571
|
+
// is the one character in the allowlist that is invisible at a grant's
|
|
572
|
+
// edges, so it gets its own rule.
|
|
573
|
+
key === key.trim();
|
|
574
|
+
}
|
|
575
|
+
var DATA_KEY_RULE = "must be 1\u2013256 printable ASCII characters (0x20\u20130x7E) with no leading or trailing space";
|
|
576
|
+
var DataKeySchema = z8.string().refine(isValidDataKey, { message: DATA_KEY_RULE });
|
|
577
|
+
var DataCapabilitySchema = z8.object({
|
|
578
|
+
user: z8.boolean().default(false),
|
|
579
|
+
collections: z8.array(DataKeySchema).default([]),
|
|
580
|
+
sharedRead: z8.array(DataKeySchema).default([]),
|
|
581
|
+
sharedWrite: z8.array(DataKeySchema).default([]),
|
|
582
|
+
/** ADR-0042: authorize every key starting with one of these prefixes (read). */
|
|
583
|
+
sharedReadPrefixes: z8.array(DataKeySchema).default([]),
|
|
584
|
+
/** ADR-0042: authorize every key starting with one of these prefixes (write). */
|
|
585
|
+
sharedWritePrefixes: z8.array(DataKeySchema).default([]),
|
|
586
|
+
writesPerDay: z8.int().positive().optional(),
|
|
587
|
+
bytesPerDay: z8.int().positive().optional()
|
|
588
|
+
}).superRefine((d, ctx) => {
|
|
589
|
+
if (d.sharedWritePrefixes.length > 0 && d.writesPerDay === void 0) {
|
|
590
|
+
ctx.addIssue({
|
|
591
|
+
code: "custom",
|
|
592
|
+
path: ["writesPerDay"],
|
|
593
|
+
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)"
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
var FetchConnectionSchema = z8.strictObject({
|
|
598
|
+
origin: z8.url(),
|
|
599
|
+
connection: z8.string().min(1).optional(),
|
|
600
|
+
provider: ProviderRefSchema.optional(),
|
|
601
|
+
required: z8.boolean().optional()
|
|
602
|
+
}).superRefine((c, ctx) => {
|
|
603
|
+
if (c.connection !== void 0 && c.provider !== void 0) {
|
|
604
|
+
ctx.addIssue({
|
|
605
|
+
code: "custom",
|
|
606
|
+
path: ["provider"],
|
|
607
|
+
message: "an origin binds at most one credential source \u2014 declare a stored secret (connection) or an OAuth provider (provider), not both"
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
var FetchCapabilitySchema = z8.object({
|
|
612
|
+
/**
|
|
613
|
+
* Legacy view of the fetch-shim grant. The grant is declared first-class as
|
|
614
|
+
* {@link ShimCapabilitySchema} (`capabilities.shim.fetch`); this boolean is
|
|
615
|
+
* the pre-`capabilities.shim` spelling, still accepted on input and kept synchronized
|
|
616
|
+
* with the canonical block on every parse (see {@link CapabilitiesSchema}) —
|
|
617
|
+
* new manifests should declare `capabilities.shim`, not this field.
|
|
618
|
+
*/
|
|
619
|
+
shim: z8.boolean().default(false),
|
|
102
620
|
/** Origins reached through the proxy (mode `proxy`); direct stays in `externalOrigins`. */
|
|
103
|
-
origins:
|
|
621
|
+
origins: z8.array(FetchConnectionSchema).default([]),
|
|
104
622
|
/** Per-app daily proxied-request budget; unset ⇒ unbounded (fetch-proxy §7). */
|
|
105
|
-
requestsPerDay:
|
|
623
|
+
requestsPerDay: z8.int().positive().optional()
|
|
624
|
+
});
|
|
625
|
+
var ShimCapabilitySchema = z8.object({
|
|
626
|
+
fetch: z8.boolean().default(false),
|
|
627
|
+
connect: z8.boolean().default(false)
|
|
106
628
|
});
|
|
107
629
|
var SCOPE_SEGMENT = /^[A-Za-z0-9\-._~]+$/;
|
|
108
630
|
function isValidServiceWorkerScope(scope) {
|
|
@@ -115,42 +637,84 @@ function isValidServiceWorkerScope(scope) {
|
|
|
115
637
|
}
|
|
116
638
|
return !first.startsWith("_");
|
|
117
639
|
}
|
|
118
|
-
var OfflineCapabilitySchema =
|
|
640
|
+
var OfflineCapabilitySchema = z8.object({
|
|
119
641
|
/**
|
|
120
642
|
* URL path prefix the worker controls, e.g. `/app/`. Leading and trailing
|
|
121
643
|
* slash required; never root, never a `_`-prefixed platform namespace.
|
|
122
644
|
*/
|
|
123
|
-
scope:
|
|
645
|
+
scope: z8.string().refine(isValidServiceWorkerScope, {
|
|
124
646
|
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
647
|
})
|
|
126
648
|
});
|
|
127
|
-
var CapabilitiesSchema =
|
|
649
|
+
var CapabilitiesSchema = z8.object({
|
|
128
650
|
llm: LlmCapabilitySchema.optional(),
|
|
129
651
|
data: DataCapabilitySchema.optional(),
|
|
130
652
|
/** Platform-registered MCP servers this app may reach (§6.1, exposed as REST). */
|
|
131
|
-
mcp:
|
|
653
|
+
mcp: z8.array(z8.string()).default([]),
|
|
132
654
|
/** Extra CSP `connect-src` origins for **direct** browser calls (§4.4). */
|
|
133
|
-
externalOrigins:
|
|
655
|
+
externalOrigins: z8.array(z8.url()).default([]),
|
|
134
656
|
/** Governed outbound HTTP via the fetch-proxy / egress plane (in build, M4.5). */
|
|
135
657
|
fetch: FetchCapabilitySchema.optional(),
|
|
136
658
|
/** Platform-owned, scope-confined service worker for offline cold boot (ADR-0035). */
|
|
137
|
-
offline: OfflineCapabilitySchema.optional()
|
|
659
|
+
offline: OfflineCapabilitySchema.optional(),
|
|
660
|
+
/** Injected platform helpers — the first-class shim grant. */
|
|
661
|
+
shim: ShimCapabilitySchema.optional()
|
|
662
|
+
}).transform((caps) => {
|
|
663
|
+
const legacyShim = caps.fetch?.shim ?? false;
|
|
664
|
+
const fetchOn = legacyShim || (caps.shim?.fetch ?? false);
|
|
665
|
+
const connectOn = caps.shim?.connect ?? false;
|
|
666
|
+
const fetch2 = caps.fetch ?? (fetchOn ? { shim: fetchOn, origins: [] } : void 0);
|
|
667
|
+
return {
|
|
668
|
+
...caps,
|
|
669
|
+
fetch: fetch2 === void 0 ? void 0 : { ...fetch2, shim: fetchOn },
|
|
670
|
+
...fetchOn || connectOn ? { shim: { fetch: fetchOn, connect: connectOn } } : {}
|
|
671
|
+
};
|
|
672
|
+
});
|
|
673
|
+
var ProviderBindingStatusSchema = z8.strictObject({
|
|
674
|
+
/** The manifest origin as declared (matches `FetchConnection.origin`). */
|
|
675
|
+
origin: z8.string(),
|
|
676
|
+
/** The bound provider reference. */
|
|
677
|
+
ref: ProviderRefSchema,
|
|
678
|
+
/**
|
|
679
|
+
* Whether an approval that granted this binding still matches the provider
|
|
680
|
+
* row's current identity — the {@link isProviderBindingEffective} rule
|
|
681
|
+
* (the one definition; the portal never re-derives the comparison). `false`
|
|
682
|
+
* means the provider was deleted or a sensitive edit stale-dated the stamp.
|
|
683
|
+
*/
|
|
684
|
+
effective: z8.boolean()
|
|
138
685
|
});
|
|
139
|
-
var AppManifestSchema =
|
|
686
|
+
var AppManifestSchema = z8.object({
|
|
140
687
|
/** App slug; matches `App.slug`. */
|
|
141
|
-
app:
|
|
688
|
+
app: z8.string().min(1),
|
|
142
689
|
visibility: VisibilitySchema,
|
|
143
|
-
capabilities: CapabilitiesSchema.default({ mcp: [], externalOrigins: [] })
|
|
690
|
+
capabilities: CapabilitiesSchema.default({ mcp: [], externalOrigins: [] }),
|
|
691
|
+
/**
|
|
692
|
+
* Effectiveness of every provider-bound origin in `capabilities`, computed
|
|
693
|
+
* server-side on the read (never stored). Absent when the manifest declares
|
|
694
|
+
* no provider binding — every payload without one keeps its exact shape.
|
|
695
|
+
*/
|
|
696
|
+
providerBindings: z8.array(ProviderBindingStatusSchema).optional()
|
|
144
697
|
});
|
|
145
698
|
|
|
146
699
|
// ../shared/src/approval.ts
|
|
147
|
-
import { z as
|
|
700
|
+
import { z as z9 } from "zod";
|
|
148
701
|
|
|
149
702
|
// ../shared/src/pricing.ts
|
|
150
703
|
var MODEL_PRICING = {
|
|
151
704
|
// Anthropic. NB `structuredOutputs` is deliberately absent on 4-7/4-6/sonnet-4-6:
|
|
152
|
-
// structured outputs are supported on Fable 5, Opus
|
|
153
|
-
// those three, so the flag is opt-in per model rather
|
|
705
|
+
// structured outputs are supported on the Fable 5 line, Opus 5, Sonnet 5, Opus 4.8
|
|
706
|
+
// and Haiku 4.5 but not on those three, so the flag is opt-in per model rather
|
|
707
|
+
// than per provider.
|
|
708
|
+
// Fable 5.1 sits above Fable 5 at the same per-token rate, but its cache *reads*
|
|
709
|
+
// bill at 0.025x base input rather than the 0.1x every other model uses — the
|
|
710
|
+
// one reason `cacheReadMultiplier` exists.
|
|
711
|
+
"claude-fable-5-1": {
|
|
712
|
+
inputPerMTok: 10,
|
|
713
|
+
outputPerMTok: 50,
|
|
714
|
+
provider: "anthropic",
|
|
715
|
+
cacheReadMultiplier: 0.025,
|
|
716
|
+
structuredOutputs: true
|
|
717
|
+
},
|
|
154
718
|
"claude-fable-5": {
|
|
155
719
|
inputPerMTok: 10,
|
|
156
720
|
outputPerMTok: 50,
|
|
@@ -163,12 +727,12 @@ var MODEL_PRICING = {
|
|
|
163
727
|
provider: "anthropic",
|
|
164
728
|
structuredOutputs: true
|
|
165
729
|
},
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
730
|
+
// Sonnet 5's $2/$10 was introductory pricing through 2026-08-31; Anthropic made
|
|
731
|
+
// it the standard price and cancelled the scheduled rise to $3/$15. This is now
|
|
732
|
+
// the list rate, so the cost gate no longer over-estimates Sonnet 5 spend.
|
|
169
733
|
"claude-sonnet-5": {
|
|
170
|
-
inputPerMTok:
|
|
171
|
-
outputPerMTok:
|
|
734
|
+
inputPerMTok: 2,
|
|
735
|
+
outputPerMTok: 10,
|
|
172
736
|
provider: "anthropic",
|
|
173
737
|
structuredOutputs: true
|
|
174
738
|
},
|
|
@@ -181,56 +745,108 @@ var MODEL_PRICING = {
|
|
|
181
745
|
"claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
|
|
182
746
|
"claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
|
|
183
747
|
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15, provider: "anthropic" },
|
|
748
|
+
// Near-term churn: haiku-4-5's only two Foundry versions both stop serving
|
|
749
|
+
// 2026-10-19 (measured eastus2 2026-09-16). Not a cut — first-party serving
|
|
750
|
+
// is unaffected — but expect the Foundry deployments to need a successor.
|
|
184
751
|
"claude-haiku-4-5": {
|
|
185
752
|
inputPerMTok: 1,
|
|
186
753
|
outputPerMTok: 5,
|
|
187
754
|
provider: "anthropic",
|
|
188
755
|
structuredOutputs: true
|
|
189
756
|
},
|
|
190
|
-
// OpenAI
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
757
|
+
// OpenAI. Every model here resolves to a snapshot new enough for `response_format`
|
|
758
|
+
// json_schema (that floor is `gpt-4o-2024-08-06`), so `structuredOutputs` is set
|
|
759
|
+
// across the board.
|
|
760
|
+
//
|
|
761
|
+
// The current generation — GPT-6 and GPT-5.x — is reasoning models throughout, so
|
|
762
|
+
// `reasoning` is set on all of them. On chat/completions that flag means exactly
|
|
763
|
+
// one thing here: send `max_completion_tokens`, not `max_tokens`. OpenAI has
|
|
764
|
+
// deprecated `max_tokens` outright and reasoning models reject it, so the flag is
|
|
765
|
+
// the forward-compatible setting even where a model would still accept both.
|
|
766
|
+
"gpt-6-astra": {
|
|
767
|
+
inputPerMTok: 10,
|
|
768
|
+
outputPerMTok: 50,
|
|
197
769
|
provider: "openai",
|
|
770
|
+
reasoning: true,
|
|
771
|
+
minCompletionTokens: 25e3,
|
|
198
772
|
structuredOutputs: true
|
|
199
773
|
},
|
|
200
|
-
"gpt-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
outputPerMTok: 1.6,
|
|
774
|
+
"gpt-5.6-sol": {
|
|
775
|
+
inputPerMTok: 4,
|
|
776
|
+
outputPerMTok: 20,
|
|
204
777
|
provider: "openai",
|
|
778
|
+
reasoning: true,
|
|
779
|
+
minCompletionTokens: 25e3,
|
|
205
780
|
structuredOutputs: true
|
|
206
781
|
},
|
|
207
|
-
"gpt-
|
|
208
|
-
inputPerMTok:
|
|
209
|
-
outputPerMTok:
|
|
782
|
+
"gpt-5.6-terra": {
|
|
783
|
+
inputPerMTok: 2,
|
|
784
|
+
outputPerMTok: 12,
|
|
210
785
|
provider: "openai",
|
|
786
|
+
reasoning: true,
|
|
787
|
+
minCompletionTokens: 25e3,
|
|
211
788
|
structuredOutputs: true
|
|
212
789
|
},
|
|
213
|
-
|
|
214
|
-
inputPerMTok: 2,
|
|
215
|
-
outputPerMTok:
|
|
790
|
+
"gpt-5.6-luna": {
|
|
791
|
+
inputPerMTok: 0.2,
|
|
792
|
+
outputPerMTok: 1.2,
|
|
793
|
+
provider: "openai",
|
|
794
|
+
reasoning: true,
|
|
795
|
+
minCompletionTokens: 25e3,
|
|
796
|
+
structuredOutputs: true
|
|
797
|
+
},
|
|
798
|
+
"gpt-5.1": {
|
|
799
|
+
inputPerMTok: 1.25,
|
|
800
|
+
outputPerMTok: 10,
|
|
801
|
+
provider: "openai",
|
|
802
|
+
reasoning: true,
|
|
803
|
+
minCompletionTokens: 25e3,
|
|
804
|
+
structuredOutputs: true
|
|
805
|
+
},
|
|
806
|
+
"gpt-5-mini": {
|
|
807
|
+
inputPerMTok: 0.25,
|
|
808
|
+
outputPerMTok: 2,
|
|
216
809
|
provider: "openai",
|
|
217
810
|
reasoning: true,
|
|
218
811
|
minCompletionTokens: 25e3,
|
|
219
812
|
structuredOutputs: true
|
|
220
813
|
},
|
|
221
|
-
"
|
|
222
|
-
inputPerMTok:
|
|
223
|
-
outputPerMTok:
|
|
814
|
+
"gpt-5-nano": {
|
|
815
|
+
inputPerMTok: 0.05,
|
|
816
|
+
outputPerMTok: 0.4,
|
|
224
817
|
provider: "openai",
|
|
225
818
|
reasoning: true,
|
|
226
819
|
minCompletionTokens: 25e3,
|
|
227
820
|
structuredOutputs: true
|
|
821
|
+
},
|
|
822
|
+
// Previous generations, kept curated so a deployed app's manifest keeps working.
|
|
823
|
+
// Rates re-verified 2026-09-14 and unchanged.
|
|
824
|
+
//
|
|
825
|
+
// `gpt-4o-mini`, `o3` and `o4-mini` were deliberately cut (2026-09-16) rather
|
|
826
|
+
// than retained on that principle: Azure AI Foundry's RP refuses any model
|
|
827
|
+
// whose *default* version is `Deprecating` (`ServiceModelDeprecating`), so
|
|
828
|
+
// they could never ride the one-flag Foundry path, and the current generation
|
|
829
|
+
// covers their price points first-party. A manifest still naming one gets the
|
|
830
|
+
// edge's `model_not_allowed` (no price configured) — the cut is the break.
|
|
831
|
+
"gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10, provider: "openai", structuredOutputs: true },
|
|
832
|
+
"gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8, provider: "openai", structuredOutputs: true },
|
|
833
|
+
"gpt-4.1-mini": {
|
|
834
|
+
inputPerMTok: 0.4,
|
|
835
|
+
outputPerMTok: 1.6,
|
|
836
|
+
provider: "openai",
|
|
837
|
+
structuredOutputs: true
|
|
838
|
+
},
|
|
839
|
+
"gpt-4.1-nano": {
|
|
840
|
+
inputPerMTok: 0.1,
|
|
841
|
+
outputPerMTok: 0.4,
|
|
842
|
+
provider: "openai",
|
|
843
|
+
structuredOutputs: true
|
|
228
844
|
}
|
|
229
845
|
};
|
|
230
846
|
|
|
231
847
|
// ../shared/src/approval.ts
|
|
232
848
|
var RISK_LEVELS = ["low", "med", "high"];
|
|
233
|
-
var RiskSchema =
|
|
849
|
+
var RiskSchema = z9.enum(RISK_LEVELS);
|
|
234
850
|
var APPROVAL_STATUSES = [
|
|
235
851
|
"pending",
|
|
236
852
|
"approved",
|
|
@@ -238,134 +854,195 @@ var APPROVAL_STATUSES = [
|
|
|
238
854
|
"withdrawn",
|
|
239
855
|
"needs_changes"
|
|
240
856
|
];
|
|
241
|
-
var ApprovalStatusSchema =
|
|
242
|
-
var
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
857
|
+
var ApprovalStatusSchema = z9.enum(APPROVAL_STATUSES);
|
|
858
|
+
var ProviderStampSchema = z9.strictObject({
|
|
859
|
+
/** The env-unique reference the manifest binds (the catalogue/manifest key). */
|
|
860
|
+
ref: ProviderRefSchema,
|
|
861
|
+
/** Which partition the stamped row lives in — a ref may exist in both. */
|
|
862
|
+
env: EnvSchema,
|
|
863
|
+
/** The surrogate row id — delete+recreate mints a new one, dangling old stamps. */
|
|
864
|
+
providerId: z9.uuid(),
|
|
865
|
+
/** The row's revision at filing; a sensitive edit advances it. */
|
|
866
|
+
revision: z9.int().positive()
|
|
867
|
+
});
|
|
868
|
+
var DeltaSchema = z9.object({
|
|
869
|
+
path: z9.string(),
|
|
870
|
+
from: z9.union([z9.string(), z9.number(), z9.boolean()]).optional(),
|
|
871
|
+
to: z9.union([z9.string(), z9.number(), z9.boolean()]).optional(),
|
|
872
|
+
/**
|
|
873
|
+
* The provider stamps recorded **at filing** for a provider-bound origin
|
|
874
|
+
* delta — one per env-partitioned `connection_providers` row that
|
|
875
|
+
* existed under the bound ref when the request was filed (an
|
|
876
|
+
* approval records the provider's `ref` + `providerId` + `revision`, and
|
|
877
|
+
* env partitioning makes a ref's full identity the set of its rows).
|
|
878
|
+
*
|
|
879
|
+
* This is the payload half of the apply-time conflict: approving a request
|
|
880
|
+
* whose stamps no longer match the provider's current identity/revision
|
|
881
|
+
* approves nothing — an old pending approval cannot approve access to a
|
|
882
|
+
* newer configuration. Consumers read the rule itself from
|
|
883
|
+
* {@link isProviderBindingEffective}; none re-derives the comparison.
|
|
884
|
+
*/
|
|
885
|
+
providerStamps: z9.array(ProviderStampSchema).optional(),
|
|
886
|
+
/**
|
|
887
|
+
* The requesting app's visibility **at filing**: `true` when the app was
|
|
888
|
+
* `public`, `false` (stamped as not-public) otherwise. The
|
|
889
|
+
* warning — a public app's anonymous visitors can never connect a vendor
|
|
890
|
+
* account — is data on the request, so the queue card needs no extra fetch.
|
|
891
|
+
* The flag never auto-rejects.
|
|
892
|
+
*/
|
|
893
|
+
publicApp: z9.boolean().optional(),
|
|
894
|
+
/**
|
|
895
|
+
* This delta's own severity as classified at filing — what the request-level
|
|
896
|
+
* `risk` is the max of. Absent on requests filed before the field existed
|
|
897
|
+
* (deltas are stored as JSON, so nothing backfills them); renderers fall
|
|
898
|
+
* back to the aggregate rather than guessing.
|
|
899
|
+
*/
|
|
900
|
+
risk: RiskSchema.optional()
|
|
901
|
+
});
|
|
902
|
+
var PriorDecisionsSchema = z9.object({
|
|
903
|
+
/** Prior *decided* (non-pending) requests on this app — the history count. */
|
|
904
|
+
total: z9.number().int().nonnegative(),
|
|
905
|
+
/** Prior denied requests touching an overlapping policy area (quiet signal). */
|
|
906
|
+
deniedSameArea: z9.number().int().nonnegative(),
|
|
907
|
+
/** Prior denied requests sharing an exact delta path (loud signal; ⊆ area). */
|
|
908
|
+
deniedSameGrant: z9.number().int().nonnegative(),
|
|
909
|
+
/** The most recent decided request (any status), for the "last decision" line. */
|
|
910
|
+
last: z9.object({
|
|
911
|
+
status: ApprovalStatusSchema,
|
|
912
|
+
note: z9.string().nullable(),
|
|
913
|
+
decidedBy: z9.string().nullable(),
|
|
914
|
+
decidedAt: z9.string()
|
|
915
|
+
}).optional()
|
|
916
|
+
});
|
|
917
|
+
var ApprovalRequestSchema = z9.object({
|
|
918
|
+
id: z9.string(),
|
|
919
|
+
appId: z9.string(),
|
|
250
920
|
/** Joined for the admin queue / banner; not a column. */
|
|
251
|
-
appSlug:
|
|
252
|
-
appDisplayName:
|
|
921
|
+
appSlug: z9.string().optional(),
|
|
922
|
+
appDisplayName: z9.string().optional(),
|
|
253
923
|
status: ApprovalStatusSchema,
|
|
254
924
|
risk: RiskSchema,
|
|
255
|
-
deltas:
|
|
256
|
-
baseSnapshot:
|
|
257
|
-
requestedBy:
|
|
258
|
-
reason:
|
|
259
|
-
decidedBy:
|
|
260
|
-
decisionNote:
|
|
261
|
-
createdAt:
|
|
262
|
-
decidedAt:
|
|
263
|
-
|
|
264
|
-
|
|
925
|
+
deltas: z9.array(DeltaSchema),
|
|
926
|
+
baseSnapshot: z9.unknown(),
|
|
927
|
+
requestedBy: z9.string(),
|
|
928
|
+
reason: z9.string().nullable().optional(),
|
|
929
|
+
decidedBy: z9.string().nullable().optional(),
|
|
930
|
+
decisionNote: z9.string().nullable().optional(),
|
|
931
|
+
createdAt: z9.string(),
|
|
932
|
+
decidedAt: z9.string().nullable().optional(),
|
|
933
|
+
/** Sibling-decision context for the admin queue (issue #26); read-side only. */
|
|
934
|
+
priorDecisions: PriorDecisionsSchema.optional()
|
|
935
|
+
});
|
|
936
|
+
var ManifestUpdateResultSchema = z9.object({
|
|
265
937
|
manifest: AppManifestSchema,
|
|
266
|
-
applied:
|
|
267
|
-
pending:
|
|
938
|
+
applied: z9.array(DeltaSchema),
|
|
939
|
+
pending: z9.string().nullable()
|
|
268
940
|
});
|
|
269
|
-
var VisibilityUpdateResultSchema =
|
|
941
|
+
var VisibilityUpdateResultSchema = z9.object({
|
|
270
942
|
app: AppSchema,
|
|
271
|
-
applied:
|
|
272
|
-
pending:
|
|
943
|
+
applied: z9.array(DeltaSchema),
|
|
944
|
+
pending: z9.string().nullable()
|
|
273
945
|
});
|
|
274
|
-
var ApprovalDecisionRequestSchema =
|
|
275
|
-
note:
|
|
946
|
+
var ApprovalDecisionRequestSchema = z9.object({
|
|
947
|
+
note: z9.string().max(2e3).optional()
|
|
276
948
|
});
|
|
277
949
|
var CURATED_LLM_MODELS = Object.keys(MODEL_PRICING);
|
|
278
950
|
|
|
279
951
|
// ../shared/src/health.ts
|
|
280
|
-
import { z as
|
|
281
|
-
var HealthStateSchema =
|
|
282
|
-
var HealthCheckSchema =
|
|
952
|
+
import { z as z10 } from "zod";
|
|
953
|
+
var HealthStateSchema = z10.enum(["ok", "degraded", "error"]);
|
|
954
|
+
var HealthCheckSchema = z10.object({
|
|
283
955
|
/** Stable identifier an alert rule can key on, e.g. `registry-projection`. */
|
|
284
|
-
name:
|
|
956
|
+
name: z10.string(),
|
|
285
957
|
status: HealthStateSchema,
|
|
286
958
|
/** Operator-facing one-liner: what is wrong, and for how long. */
|
|
287
|
-
detail:
|
|
959
|
+
detail: z10.string().optional(),
|
|
288
960
|
/** Wall-clock instant this check last succeeded. Report-only — never the
|
|
289
961
|
* basis of a staleness decision (see the monotonic note in the edge's
|
|
290
962
|
* registry projection). */
|
|
291
|
-
lastSuccessAt:
|
|
963
|
+
lastSuccessAt: z10.iso.datetime().optional(),
|
|
292
964
|
/** Numeric facts a dashboard or a log-based metric can key on. */
|
|
293
|
-
metrics:
|
|
965
|
+
metrics: z10.record(z10.string(), z10.number()).optional()
|
|
294
966
|
});
|
|
295
|
-
var HealthStatusSchema =
|
|
967
|
+
var HealthStatusSchema = z10.object({
|
|
296
968
|
/** The roll-up: the worst state across `checks` (see `worstHealthState`). */
|
|
297
969
|
status: HealthStateSchema,
|
|
298
|
-
service:
|
|
970
|
+
service: z10.string(),
|
|
299
971
|
/** Process uptime in seconds. */
|
|
300
|
-
uptime:
|
|
972
|
+
uptime: z10.number().nonnegative(),
|
|
301
973
|
/** Absent means the service reports liveness only (portal and egress today). */
|
|
302
|
-
checks:
|
|
974
|
+
checks: z10.array(HealthCheckSchema).optional()
|
|
303
975
|
});
|
|
304
976
|
|
|
305
977
|
// ../shared/src/api.ts
|
|
306
|
-
import { z as
|
|
307
|
-
var CreateAppRequestSchema =
|
|
978
|
+
import { z as z11 } from "zod";
|
|
979
|
+
var CreateAppRequestSchema = z11.object({
|
|
308
980
|
slug: AppSchema.shape.slug,
|
|
309
981
|
displayName: AppSchema.shape.displayName,
|
|
310
|
-
|
|
982
|
+
/** Optional one-line plain-text summary; rendered as a subtitle in the portal. */
|
|
983
|
+
description: AppSchema.shape.description,
|
|
984
|
+
visibility: WritableVisibilitySchema.default({ mode: "internal" }),
|
|
311
985
|
/** Optional per-app capability grant set at create time (architecture §6.3). */
|
|
312
986
|
capabilities: CapabilitiesSchema.optional()
|
|
313
987
|
});
|
|
314
|
-
var
|
|
988
|
+
var UpdateAppRequestSchema = z11.object({
|
|
989
|
+
description: z11.string().max(APP_DESCRIPTION_MAX).nullable().optional()
|
|
990
|
+
});
|
|
991
|
+
var SetManifestRequestSchema = z11.object({
|
|
315
992
|
capabilities: CapabilitiesSchema,
|
|
316
993
|
/** Justification carried onto an approval request for any elevated deltas. */
|
|
317
|
-
reason:
|
|
994
|
+
reason: z11.string().max(2e3).optional()
|
|
318
995
|
});
|
|
319
|
-
var SetVisibilityRequestSchema =
|
|
320
|
-
visibility:
|
|
996
|
+
var SetVisibilityRequestSchema = z11.object({
|
|
997
|
+
visibility: WritableVisibilitySchema,
|
|
321
998
|
/** Justification carried onto the approval request when going public. */
|
|
322
|
-
reason:
|
|
999
|
+
reason: z11.string().max(2e3).optional()
|
|
323
1000
|
});
|
|
324
1001
|
var MIN_PASSWORD_LENGTH = 12;
|
|
325
|
-
var SetPasswordRequestSchema =
|
|
326
|
-
password:
|
|
1002
|
+
var SetPasswordRequestSchema = z11.object({
|
|
1003
|
+
password: z11.string().min(MIN_PASSWORD_LENGTH).optional()
|
|
327
1004
|
});
|
|
328
|
-
var PasswordCredentialResponseSchema =
|
|
1005
|
+
var PasswordCredentialResponseSchema = z11.object({
|
|
329
1006
|
/** The shared passphrase, in cleartext, for the owner to copy/share. */
|
|
330
|
-
password:
|
|
1007
|
+
password: z11.string().min(1),
|
|
331
1008
|
/** The app's public URL, prebuilt so the UI can offer a one-click copy. */
|
|
332
|
-
url:
|
|
1009
|
+
url: z11.url(),
|
|
333
1010
|
/** When the current password was set (ISO 8601). */
|
|
334
|
-
setAt:
|
|
335
|
-
});
|
|
336
|
-
var CspWarningSchema =
|
|
337
|
-
file:
|
|
338
|
-
origin:
|
|
339
|
-
hint:
|
|
340
|
-
});
|
|
341
|
-
var OriginGrantRequestSchema =
|
|
342
|
-
origin:
|
|
343
|
-
reason:
|
|
344
|
-
});
|
|
345
|
-
var CspViolationSchema =
|
|
346
|
-
appId:
|
|
347
|
-
appSlug:
|
|
348
|
-
directive:
|
|
349
|
-
blockedUri:
|
|
350
|
-
count:
|
|
351
|
-
lastSeen:
|
|
1011
|
+
setAt: z11.string()
|
|
1012
|
+
});
|
|
1013
|
+
var CspWarningSchema = z11.object({
|
|
1014
|
+
file: z11.string(),
|
|
1015
|
+
origin: z11.string(),
|
|
1016
|
+
hint: z11.string()
|
|
1017
|
+
});
|
|
1018
|
+
var OriginGrantRequestSchema = z11.object({
|
|
1019
|
+
origin: z11.url(),
|
|
1020
|
+
reason: z11.string().max(2e3).optional()
|
|
1021
|
+
});
|
|
1022
|
+
var CspViolationSchema = z11.object({
|
|
1023
|
+
appId: z11.string(),
|
|
1024
|
+
appSlug: z11.string().nullable(),
|
|
1025
|
+
directive: z11.string(),
|
|
1026
|
+
blockedUri: z11.string(),
|
|
1027
|
+
count: z11.number().int(),
|
|
1028
|
+
lastSeen: z11.string(),
|
|
352
1029
|
/**
|
|
353
1030
|
* The blocked origin is already permitted by the app's *current* manifest, so
|
|
354
1031
|
* this historical report is no longer actionable. Derived at read time
|
|
355
1032
|
* (directive-aware — only `connect-src`/`img-src` are widened by an
|
|
356
1033
|
* `externalOrigins` grant); the row itself is never deleted.
|
|
357
1034
|
*/
|
|
358
|
-
resolved:
|
|
1035
|
+
resolved: z11.boolean()
|
|
359
1036
|
});
|
|
360
|
-
var CspViolationsPageSchema =
|
|
361
|
-
violations:
|
|
1037
|
+
var CspViolationsPageSchema = z11.object({
|
|
1038
|
+
violations: z11.array(CspViolationSchema)
|
|
362
1039
|
});
|
|
363
|
-
var UploadVersionResponseSchema =
|
|
1040
|
+
var UploadVersionResponseSchema = z11.object({
|
|
364
1041
|
version: VersionSchema,
|
|
365
|
-
warnings:
|
|
1042
|
+
warnings: z11.array(CspWarningSchema)
|
|
366
1043
|
});
|
|
367
|
-
var RollbackRequestSchema =
|
|
368
|
-
toNumber:
|
|
1044
|
+
var RollbackRequestSchema = z11.object({
|
|
1045
|
+
toNumber: z11.int().positive().optional()
|
|
369
1046
|
});
|
|
370
1047
|
var API_ERROR_CODES = [
|
|
371
1048
|
"validation_failed",
|
|
@@ -376,6 +1053,14 @@ var API_ERROR_CODES = [
|
|
|
376
1053
|
/** Authenticated but not allowed — reserved for v1 RBAC. */
|
|
377
1054
|
"forbidden",
|
|
378
1055
|
"conflict",
|
|
1056
|
+
/**
|
|
1057
|
+
* Portal: a sensitive provider edit (or a deletion) arrived without its
|
|
1058
|
+
* invalidation acknowledgement. HTTP 409 like `conflict`, but
|
|
1059
|
+
* distinguishable: the SPA's
|
|
1060
|
+
* review panel opens on this code, and `details` carries the impact payload
|
|
1061
|
+
* (`ConfirmationRequiredDetailsSchema` in providers.ts) it renders.
|
|
1062
|
+
*/
|
|
1063
|
+
"confirmation_required",
|
|
379
1064
|
/** Gateway: requested model is not in the app's manifest allowlist (§6.3). */
|
|
380
1065
|
"model_not_allowed",
|
|
381
1066
|
/** Gateway: the app's daily token budget is exhausted (§6.1). */
|
|
@@ -387,48 +1072,141 @@ var API_ERROR_CODES = [
|
|
|
387
1072
|
* of its own budget. HTTP 429.
|
|
388
1073
|
*/
|
|
389
1074
|
"rate_limited",
|
|
1075
|
+
/**
|
|
1076
|
+
* Gateway: a `shared` app-data write carried no concurrency precondition
|
|
1077
|
+
* (`If-Match` / `If-None-Match`), which that scope requires (ADR-0041).
|
|
1078
|
+
* Distinct from `conflict` (a precondition that was stated and lost) because
|
|
1079
|
+
* the two demand opposite responses: `conflict` says re-read and retry,
|
|
1080
|
+
* `precondition_required` says the app's code is wrong. HTTP 428.
|
|
1081
|
+
*/
|
|
1082
|
+
"precondition_required",
|
|
390
1083
|
/** Gateway: a configured capability is not available on this edge. */
|
|
391
1084
|
"capability_unavailable",
|
|
392
1085
|
"internal"
|
|
393
1086
|
];
|
|
394
|
-
var ApiErrorCodeSchema =
|
|
395
|
-
var ApiErrorSchema =
|
|
396
|
-
error:
|
|
1087
|
+
var ApiErrorCodeSchema = z11.enum(API_ERROR_CODES);
|
|
1088
|
+
var ApiErrorSchema = z11.object({
|
|
1089
|
+
error: z11.object({
|
|
397
1090
|
code: ApiErrorCodeSchema,
|
|
398
|
-
message:
|
|
399
|
-
details:
|
|
1091
|
+
message: z11.string(),
|
|
1092
|
+
details: z11.unknown().optional()
|
|
400
1093
|
})
|
|
401
1094
|
});
|
|
402
1095
|
|
|
403
1096
|
// ../shared/src/auth.ts
|
|
404
|
-
import { z as
|
|
405
|
-
var MeResponseSchema =
|
|
406
|
-
user:
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
|
|
1097
|
+
import { z as z12 } from "zod";
|
|
1098
|
+
var MeResponseSchema = z12.object({
|
|
1099
|
+
user: z12.object({
|
|
1100
|
+
/**
|
|
1101
|
+
* The principal id — Entra's `oid` claim (the directory object id), stable
|
|
1102
|
+
* for the life of the user object and identical across every app
|
|
1103
|
+
* registration in the tenant (ADR-0048): safe to key app data on, and the
|
|
1104
|
+
* user scope partitions on exactly this value. Opaque by design — never
|
|
1105
|
+
* render it; `displayName` and `email` are the half fit to display.
|
|
1106
|
+
*/
|
|
1107
|
+
id: z12.string(),
|
|
1108
|
+
displayName: z12.string(),
|
|
1109
|
+
/**
|
|
1110
|
+
* The captured address claim, or `null` when there isn't one — **nullable,
|
|
1111
|
+
* not optional, and the difference is the contract**. A missing key would
|
|
1112
|
+
* conflate "this platform doesn't send emails" with "we have no address for
|
|
1113
|
+
* this person", and the second is routine: shared-password sessions are a
|
|
1114
|
+
* `Guest` with no directory profile at all, and an issuer that sends no
|
|
1115
|
+
* `email` and a non-addressable `preferred_username` yields null too
|
|
1116
|
+
* (`captureEmail`). Apps must handle null; sending it explicitly is what
|
|
1117
|
+
* makes them.
|
|
1118
|
+
*
|
|
1119
|
+
* Never a delivery target or an identity key — `id` is the only thing safe
|
|
1120
|
+
* to join on. Entra addresses are reassignable and B2B guests carry their
|
|
1121
|
+
* home-tenant address.
|
|
1122
|
+
*/
|
|
1123
|
+
email: z12.string().nullable()
|
|
410
1124
|
})
|
|
411
1125
|
});
|
|
412
|
-
var PortalMeResponseSchema =
|
|
413
|
-
|
|
1126
|
+
var PortalMeResponseSchema = z12.object({
|
|
1127
|
+
/**
|
|
1128
|
+
* The canonical principal id — Entra's `oid` claim from the access token
|
|
1129
|
+
* (ADR-0048). This is what `App.ownerId` stores and what ownership compares;
|
|
1130
|
+
* `sub` below is the display/audit half, never a join key.
|
|
1131
|
+
*
|
|
1132
|
+
* **Optional, not required**, for the same cross-version reason as the
|
|
1133
|
+
* defaulted fields below: this schema parses `PortalClient.me()` in the
|
|
1134
|
+
* *published* CLI, which routinely talks to portals older than itself, and
|
|
1135
|
+
* portals are customer-deployed and version independently (ADR-0028). An
|
|
1136
|
+
* older portal has no `oid` to echo — pre-ADR-0048 it has no such concept —
|
|
1137
|
+
* and nothing in the CLI keys on it. A current portal always sends it (the
|
|
1138
|
+
* dev-token actor gets a fixed synthetic value).
|
|
1139
|
+
*/
|
|
1140
|
+
oid: z12.string().optional(),
|
|
1141
|
+
sub: z12.string(),
|
|
414
1142
|
/** How the actor was established: `oidc` or `dev-token`. */
|
|
415
|
-
via:
|
|
416
|
-
name:
|
|
417
|
-
email:
|
|
1143
|
+
via: z12.string(),
|
|
1144
|
+
name: z12.string().optional(),
|
|
1145
|
+
email: z12.string().optional(),
|
|
418
1146
|
/**
|
|
419
1147
|
* Whether the actor holds the `platform-admin` role. Computed server-side from
|
|
420
1148
|
* the actor's group/role claim — the raw ids never cross to the browser. Drives
|
|
421
1149
|
* admin nav + route gating in the SPA (and `helix whoami`).
|
|
1150
|
+
*
|
|
1151
|
+
* **Defaulted, not required**, for the same reason as
|
|
1152
|
+
* {@link canSearchDirectory} below: this schema parses `PortalClient.me()` in
|
|
1153
|
+
* the *published* CLI, which routinely talks to portals older than itself. The
|
|
1154
|
+
* polarity is the cautious one — a portal that never sent the field is not
|
|
1155
|
+
* telling us the caller is an admin.
|
|
422
1156
|
*/
|
|
423
|
-
isAdmin:
|
|
1157
|
+
isAdmin: z12.boolean().default(false),
|
|
1158
|
+
/**
|
|
1159
|
+
* Whether this caller may run a tenant-wide group search (ADR-0040 decision
|
|
1160
|
+
* 11). Computed server-side from the deployment's `PORTAL_DIRECTORY_SEARCH`
|
|
1161
|
+
* tier and the actor's admin-ness. A caller who *may* search learns nothing
|
|
1162
|
+
* else; one who may not gets {@link searchRestriction} alongside.
|
|
1163
|
+
*
|
|
1164
|
+
* Sent so the group picker can **avoid issuing a search it is not allowed to
|
|
1165
|
+
* make**, rather than firing one and interpreting the 403. That matters
|
|
1166
|
+
* because a refused search is not a broken directory: the caller's own groups
|
|
1167
|
+
* and their app's stored groups still resolve to names, so the picker stays
|
|
1168
|
+
* useful and must not fall back to the "directory unavailable" banner. Server
|
|
1169
|
+
* enforcement is independent of this hint (`GET /api/v1/directory/groups`
|
|
1170
|
+
* refuses on its own).
|
|
1171
|
+
*
|
|
1172
|
+
* **Defaults to `true` when absent, and the polarity is the whole point.** The
|
|
1173
|
+
* CLI bundles this schema (ADR-0032) and portals are customer-deployed and
|
|
1174
|
+
* version independently (ADR-0028), so a new CLI against an older portal is
|
|
1175
|
+
* routine — and it was a hard break while this was required: `helix whoami`
|
|
1176
|
+
* failed outright, and `helix login` failed at its final "greet the actor"
|
|
1177
|
+
* step, which runs *after* the tokens are written, so a login that genuinely
|
|
1178
|
+
* succeeded reported an error. A portal old enough to omit this has no tier
|
|
1179
|
+
* enforcement at all and therefore behaves exactly like `everyone`, so `true`
|
|
1180
|
+
* describes the portal actually being talked to. Defaulting to `false` would
|
|
1181
|
+
* instead tell a client that search is unavailable where it works.
|
|
1182
|
+
*/
|
|
1183
|
+
canSearchDirectory: z12.boolean().default(true),
|
|
1184
|
+
/**
|
|
1185
|
+
* Why search was refused, when it was — **present only when
|
|
1186
|
+
* {@link canSearchDirectory} is false**, and never `everyone`, which by
|
|
1187
|
+
* construction refuses nobody.
|
|
1188
|
+
*
|
|
1189
|
+
* Exists because "restricted" and "restricted *to platform admins*" are
|
|
1190
|
+
* different sentences and only one of them is true under the `none` tier. The
|
|
1191
|
+
* picker has to be able to say which, and telling a platform admin that search
|
|
1192
|
+
* is "limited to platform admins" while refusing them sends them off to audit
|
|
1193
|
+
* a `PORTAL_ADMIN_GROUP_ID` that is perfectly correct.
|
|
1194
|
+
*
|
|
1195
|
+
* This is a deliberate, bounded narrowing of decision 11's "the browser learns
|
|
1196
|
+
* the answer, never the tier": a *permitted* caller still learns nothing, and a
|
|
1197
|
+
* refused one learns only which rule refused them — which they can already
|
|
1198
|
+
* infer from the 403 they would get by asking. The admin group id still never
|
|
1199
|
+
* crosses.
|
|
1200
|
+
*/
|
|
1201
|
+
searchRestriction: z12.enum(["admins", "none"]).optional()
|
|
424
1202
|
});
|
|
425
|
-
var AuthConfigResponseSchema =
|
|
426
|
-
issuer:
|
|
427
|
-
cliClientId:
|
|
1203
|
+
var AuthConfigResponseSchema = z12.object({
|
|
1204
|
+
issuer: z12.url(),
|
|
1205
|
+
cliClientId: z12.string().min(1),
|
|
428
1206
|
/** Public client the portal SPA uses for code+PKCE in the browser. */
|
|
429
|
-
webClientId:
|
|
1207
|
+
webClientId: z12.string().min(1).optional(),
|
|
430
1208
|
/** Expected token audience — part of what the CLI binds cached tokens to. */
|
|
431
|
-
audience:
|
|
1209
|
+
audience: z12.string().min(1).optional(),
|
|
432
1210
|
/**
|
|
433
1211
|
* Whether this deployment permits `public` (anonymous) apps. Drives the SPA's
|
|
434
1212
|
* visibility UI — it hides the public option when false. Absent = forbidden
|
|
@@ -436,9 +1214,9 @@ var AuthConfigResponseSchema = z8.object({
|
|
|
436
1214
|
* opt-in). Server-side enforcement is independent of this hint (portal routes
|
|
437
1215
|
* + edge serving).
|
|
438
1216
|
*/
|
|
439
|
-
allowPublicApps:
|
|
1217
|
+
allowPublicApps: z12.boolean().optional(),
|
|
440
1218
|
/** Whether this deployment permits `password` (shared-passphrase) apps. Same shape as {@link allowPublicApps}. */
|
|
441
|
-
allowPasswordApps:
|
|
1219
|
+
allowPasswordApps: z12.boolean().optional()
|
|
442
1220
|
});
|
|
443
1221
|
function portalApiScope(audience) {
|
|
444
1222
|
if (!audience || audience.startsWith("urn:")) return null;
|
|
@@ -447,27 +1225,27 @@ function portalApiScope(audience) {
|
|
|
447
1225
|
}
|
|
448
1226
|
|
|
449
1227
|
// ../shared/src/deployment.ts
|
|
450
|
-
import { z as
|
|
451
|
-
var DeploymentConfigResponseSchema =
|
|
1228
|
+
import { z as z13 } from "zod";
|
|
1229
|
+
var DeploymentConfigResponseSchema = z13.object({
|
|
452
1230
|
/**
|
|
453
1231
|
* Scheme + host + (non-default) port where apps are served, as reachable by
|
|
454
1232
|
* this browser (architecture §4.1). The app slug is prepended as a subdomain:
|
|
455
1233
|
* `https://<slug>.<host>`.
|
|
456
1234
|
*/
|
|
457
|
-
appPublicBase:
|
|
1235
|
+
appPublicBase: z13.url(),
|
|
458
1236
|
/**
|
|
459
1237
|
* Base of the opt-in dev gateway (dev-mode design §3), absent when it is not
|
|
460
1238
|
* deployed. Unlike {@link appPublicBase} the slug goes in the *path*, not the
|
|
461
1239
|
* host. Absent ⇒ dev mode is unavailable here, so the UI says so instead of
|
|
462
1240
|
* printing an unreachable host.
|
|
463
1241
|
*/
|
|
464
|
-
devApiBase:
|
|
1242
|
+
devApiBase: z13.url().optional(),
|
|
465
1243
|
/**
|
|
466
1244
|
* Month-to-date platform spend ceiling (USD) for the admin budget watch line.
|
|
467
1245
|
* Display-only — the gateway is the choke point, so the rollup is exact, but
|
|
468
1246
|
* nothing enforces this. Absent ⇒ no ceiling shown.
|
|
469
1247
|
*/
|
|
470
|
-
platformMonthlyUsdCap:
|
|
1248
|
+
platformMonthlyUsdCap: z13.number().positive().optional(),
|
|
471
1249
|
/**
|
|
472
1250
|
* Deploy bundle size caps in megabytes (`DEPLOY_MAX_FILE_MB` /
|
|
473
1251
|
* `DEPLOY_MAX_BUNDLE_MB` on the portal) — `deployMaxFileMb` is per file,
|
|
@@ -477,8 +1255,142 @@ var DeploymentConfigResponseSchema = z9.object({
|
|
|
477
1255
|
* substitute a default, because printing the wrong cap to an agent sends it
|
|
478
1256
|
* chasing a rejection it can't see the cause of.
|
|
479
1257
|
*/
|
|
480
|
-
deployMaxFileMb:
|
|
481
|
-
deployMaxBundleMb:
|
|
1258
|
+
deployMaxFileMb: z13.number().positive().optional(),
|
|
1259
|
+
deployMaxBundleMb: z13.number().positive().optional()
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
// ../shared/src/catalogue.ts
|
|
1263
|
+
import { z as z14 } from "zod";
|
|
1264
|
+
var CapabilityCatalogueSchema = z14.object({
|
|
1265
|
+
/**
|
|
1266
|
+
* Visibility modes an app may declare here. `internal` is always present
|
|
1267
|
+
* (SSO, any directory principal — the baseline). `group` is present only when
|
|
1268
|
+
* an IdP with group claims is configured: on a dev-token-only portal it is
|
|
1269
|
+
* absent, matching `/api/v1/auth/config`'s 404. `password` / `public` are
|
|
1270
|
+
* present only when the operator policy permits them
|
|
1271
|
+
* (`PORTAL_ALLOW_*_APPS`). A mode absent from this list is not requestable.
|
|
1272
|
+
*/
|
|
1273
|
+
visibility: z14.object({
|
|
1274
|
+
modes: z14.array(VisibilityModeSchema).min(1)
|
|
1275
|
+
}),
|
|
1276
|
+
/**
|
|
1277
|
+
* LLM: the **servable** model ids, not merely the curated catalog. The set is
|
|
1278
|
+
* the operator's declaration when one exists (`PORTAL_LLM_MODEL_ALLOWLIST`,
|
|
1279
|
+
* minus `PORTAL_LLM_MODEL_BLOCKLIST` — ADR-0047; on a `deployFoundry` install
|
|
1280
|
+
* the Bicep derives it from `foundryModels`, so it reads exactly what was
|
|
1281
|
+
* deployed), and otherwise the ADR-0036 v1 heuristic: a model is servable
|
|
1282
|
+
* when its upstream family has a seeded `platform` secret, so a
|
|
1283
|
+
* curated-but-unseeded model (e.g. a `gpt-*` model whose `openai` platform
|
|
1284
|
+
* key was never added) is omitted and an agent that reads this list never
|
|
1285
|
+
* designs an app that 502s at call time. Every id in this list is also
|
|
1286
|
+
* curated, so requesting one is baseline (auto-approve).
|
|
1287
|
+
*
|
|
1288
|
+
* `baselineDollarsPerDay` is the spend cap at/under which an LLM grant
|
|
1289
|
+
* auto-approves; above it queues. From `@azx-pbc/shared/approval.ts`, so the
|
|
1290
|
+
* catalogue and the classifier cannot drift.
|
|
1291
|
+
*/
|
|
1292
|
+
llm: z14.object({
|
|
1293
|
+
models: z14.array(z14.string().min(1)),
|
|
1294
|
+
baselineDollarsPerDay: z14.number().positive()
|
|
1295
|
+
}),
|
|
1296
|
+
/**
|
|
1297
|
+
* App-data: whether the three scopes are provisioned here, and the daily
|
|
1298
|
+
* budgets at/under which a data grant auto-approves. `provisioned` is `true`
|
|
1299
|
+
* today (the storage tables exist unconditionally); the field is the seam for
|
|
1300
|
+
* a future deployment-level toggle, present before it is needed. The
|
|
1301
|
+
* baselines come from `@azx-pbc/shared/approval.ts`.
|
|
1302
|
+
*/
|
|
1303
|
+
data: z14.object({
|
|
1304
|
+
provisioned: z14.boolean(),
|
|
1305
|
+
baselineWritesPerDay: z14.number().positive(),
|
|
1306
|
+
baselineBytesPerDay: z14.number().positive()
|
|
1307
|
+
}),
|
|
1308
|
+
/**
|
|
1309
|
+
* The fetch-proxy (`/_api/fetch`) and direct-CSP surfaces.
|
|
1310
|
+
*
|
|
1311
|
+
* `externalOriginsPermitted` is `true` when an app may request a direct-CSP
|
|
1312
|
+
* origin grant at all (the surface exists; any entry still queues for
|
|
1313
|
+
* approval). `connections` names the stored `global`-scope secrets an app may
|
|
1314
|
+
* reference from `capabilities.fetch.origins[].connection` — names only, never
|
|
1315
|
+
* values, and without the origin each one fronts: a global secret has no
|
|
1316
|
+
* stored origin (the origin is declared per-app in each manifest), so listing
|
|
1317
|
+
* one here would be a guess. An agent learns "this connection exists and is
|
|
1318
|
+
* referenceable" and supplies the origin itself. `providers` is the configured
|
|
1319
|
+
* OAuth connection providers an app may bind from
|
|
1320
|
+
* `capabilities.fetch.origins[].provider` — metadata-only
|
|
1321
|
+
* {@link CatalogueProviderSchema} entries (ref, kind, display name, API
|
|
1322
|
+
* destinations, env), never a credential field, served to every
|
|
1323
|
+
* authenticated principal exactly like `connections`. An origin bound to a
|
|
1324
|
+
* provider must be one of that provider's `apiOrigins` in the applicable
|
|
1325
|
+
* environment, so an app author picks both from this list.
|
|
1326
|
+
* `baselineRequestsPerDay` is the proxied-request budget at/under which a
|
|
1327
|
+
* fetch grant auto-approves.
|
|
1328
|
+
*/
|
|
1329
|
+
fetch: z14.object({
|
|
1330
|
+
externalOriginsPermitted: z14.boolean(),
|
|
1331
|
+
connections: z14.array(z14.object({ name: z14.string().min(1) })),
|
|
1332
|
+
providers: z14.array(CatalogueProviderSchema),
|
|
1333
|
+
baselineRequestsPerDay: z14.number().positive()
|
|
1334
|
+
}),
|
|
1335
|
+
/**
|
|
1336
|
+
* MCP is carried in the manifest but **not enforced** — no transport exists
|
|
1337
|
+
* today. Surfaced as `false` so an agent does not assume a working `mcp`
|
|
1338
|
+
* grant; declaring any server queues for approval regardless.
|
|
1339
|
+
*/
|
|
1340
|
+
mcp: z14.object({
|
|
1341
|
+
enforced: z14.boolean()
|
|
1342
|
+
}),
|
|
1343
|
+
/**
|
|
1344
|
+
* Offline (ADR-0035): whether the platform service-worker grant is available,
|
|
1345
|
+
* and the rule a `scope` must satisfy (not the domain root, not a `/_…`
|
|
1346
|
+
* reserved path). Any grant or rescope queues for approval.
|
|
1347
|
+
*/
|
|
1348
|
+
offline: z14.object({
|
|
1349
|
+
available: z14.boolean(),
|
|
1350
|
+
/** Human-facing summary of the scope rule; not machine-parsed. */
|
|
1351
|
+
scopeRule: z14.string().min(1)
|
|
1352
|
+
}),
|
|
1353
|
+
/** Deploy bundle size caps in megabytes, as enforced on upload. */
|
|
1354
|
+
deploy: z14.object({
|
|
1355
|
+
maxFileMb: z14.number().positive(),
|
|
1356
|
+
maxBundleMb: z14.number().positive()
|
|
1357
|
+
}),
|
|
1358
|
+
/**
|
|
1359
|
+
* The opt-in dev-gateway base, absent when it is not deployed here (same
|
|
1360
|
+
* convention as `DeploymentConfigResponseSchema.devApiBase`).
|
|
1361
|
+
*/
|
|
1362
|
+
devApiBase: z14.url().optional(),
|
|
1363
|
+
/**
|
|
1364
|
+
* The approval half of the catalogue (decision 3). The four baselines mirror
|
|
1365
|
+
* `@azx-pbc/shared/approval.ts` exactly, and `elevationTriggers` enumerates
|
|
1366
|
+
* the requests that **queue for a human** rather than auto-applying — the
|
|
1367
|
+
* thing an agent reads to design an app that ships immediately. A trigger
|
|
1368
|
+
* absent from this list never queues.
|
|
1369
|
+
*/
|
|
1370
|
+
approval: z14.object({
|
|
1371
|
+
baselines: z14.object({
|
|
1372
|
+
dollarsPerDay: z14.number().positive(),
|
|
1373
|
+
writesPerDay: z14.number().positive(),
|
|
1374
|
+
bytesPerDay: z14.number().positive(),
|
|
1375
|
+
fetchRequestsPerDay: z14.number().positive()
|
|
1376
|
+
}),
|
|
1377
|
+
elevationTriggers: z14.array(
|
|
1378
|
+
z14.enum([
|
|
1379
|
+
/** Any `externalOrigins` entry — widens CSP for a direct browser call. */
|
|
1380
|
+
"externalOrigins",
|
|
1381
|
+
/** Any `fetch.origins` entry — a proxied outbound origin. */
|
|
1382
|
+
"fetchOrigins",
|
|
1383
|
+
/** Any `mcp` server — no transport is enforced today. */
|
|
1384
|
+
"mcp",
|
|
1385
|
+
/** `visibility: public` — crosses the tenant boundary. */
|
|
1386
|
+
"publicVisibility",
|
|
1387
|
+
/** Any budget above its baseline (LLM spend, data writes/bytes, fetch requests). */
|
|
1388
|
+
"budgetAboveBaseline",
|
|
1389
|
+
/** An LLM model not in this deployment's servable (curated) set. */
|
|
1390
|
+
"uncuratedLlmModel"
|
|
1391
|
+
])
|
|
1392
|
+
)
|
|
1393
|
+
})
|
|
482
1394
|
});
|
|
483
1395
|
|
|
484
1396
|
// ../shared/src/scrypt.ts
|
|
@@ -494,16 +1406,16 @@ var SCRYPT_PARAMS = {
|
|
|
494
1406
|
};
|
|
495
1407
|
|
|
496
1408
|
// ../shared/src/llm.ts
|
|
497
|
-
import { z as
|
|
498
|
-
var LlmMessageSchema =
|
|
499
|
-
role:
|
|
500
|
-
content:
|
|
1409
|
+
import { z as z15 } from "zod";
|
|
1410
|
+
var LlmMessageSchema = z15.object({
|
|
1411
|
+
role: z15.enum(["user", "assistant"]),
|
|
1412
|
+
content: z15.string().min(1)
|
|
501
1413
|
});
|
|
502
|
-
var LlmUsageSchema =
|
|
503
|
-
inputTokens:
|
|
504
|
-
outputTokens:
|
|
505
|
-
cacheReadInputTokens:
|
|
506
|
-
cacheCreationInputTokens:
|
|
1414
|
+
var LlmUsageSchema = z15.object({
|
|
1415
|
+
inputTokens: z15.int().nonnegative(),
|
|
1416
|
+
outputTokens: z15.int().nonnegative(),
|
|
1417
|
+
cacheReadInputTokens: z15.int().nonnegative().default(0),
|
|
1418
|
+
cacheCreationInputTokens: z15.int().nonnegative().default(0)
|
|
507
1419
|
});
|
|
508
1420
|
var MAX_SCHEMA_CHARS = 32768;
|
|
509
1421
|
var MAX_SCHEMA_DEPTH = 12;
|
|
@@ -526,11 +1438,11 @@ function withinSchemaBudget(schema) {
|
|
|
526
1438
|
}
|
|
527
1439
|
return serialized.length <= MAX_SCHEMA_CHARS && schemaDepth(schema) <= MAX_SCHEMA_DEPTH;
|
|
528
1440
|
}
|
|
529
|
-
var LlmResponseFormatSchema =
|
|
530
|
-
type:
|
|
1441
|
+
var LlmResponseFormatSchema = z15.object({
|
|
1442
|
+
type: z15.literal("json_schema"),
|
|
531
1443
|
/** Schema name. OpenAI requires one; defaulted at translation when omitted. */
|
|
532
|
-
name:
|
|
533
|
-
schema:
|
|
1444
|
+
name: z15.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1-64 chars of [A-Za-z0-9_-]").optional(),
|
|
1445
|
+
schema: z15.record(z15.string(), z15.unknown()).refine((s) => s.type === "object", 'schema root must be `{"type":"object"}`').refine(
|
|
534
1446
|
withinSchemaBudget,
|
|
535
1447
|
`schema must serialize to <= ${MAX_SCHEMA_CHARS} characters and nest <= ${MAX_SCHEMA_DEPTH} levels`
|
|
536
1448
|
)
|
|
@@ -540,18 +1452,18 @@ var LlmResponseFormatSchema = z10.object({
|
|
|
540
1452
|
// on `gpt-*` but not `claude-*`, which is exactly the provider leak this seam
|
|
541
1453
|
// exists to prevent. The platform always enforces; see ADR-0034.
|
|
542
1454
|
});
|
|
543
|
-
var LlmChatRequestSchema =
|
|
544
|
-
model:
|
|
545
|
-
messages:
|
|
1455
|
+
var LlmChatRequestSchema = z15.object({
|
|
1456
|
+
model: z15.string().min(1),
|
|
1457
|
+
messages: z15.array(LlmMessageSchema).min(1),
|
|
546
1458
|
/** Optional system prompt; maps to the vendor's system channel. */
|
|
547
|
-
system:
|
|
548
|
-
maxTokens:
|
|
1459
|
+
system: z15.string().optional(),
|
|
1460
|
+
maxTokens: z15.int().positive().max(128e3).optional(),
|
|
549
1461
|
/** Sampling temperature; forwarded as-is (the vendor validates its own range). */
|
|
550
|
-
temperature:
|
|
1462
|
+
temperature: z15.number().optional(),
|
|
551
1463
|
/** Nucleus sampling; forwarded as-is. */
|
|
552
|
-
topP:
|
|
1464
|
+
topP: z15.number().optional(),
|
|
553
1465
|
/** Stop sequences; normalized to a list at the boundary. */
|
|
554
|
-
stop:
|
|
1466
|
+
stop: z15.array(z15.string()).optional(),
|
|
555
1467
|
/**
|
|
556
1468
|
* Constrain the completion to a JSON schema (ADR-0034). Refused up front when
|
|
557
1469
|
* the requested model can't enforce it (`ModelPrice.structuredOutputs`). The
|
|
@@ -560,70 +1472,70 @@ var LlmChatRequestSchema = z10.object({
|
|
|
560
1472
|
*/
|
|
561
1473
|
responseFormat: LlmResponseFormatSchema.optional(),
|
|
562
1474
|
/** SSE streaming (default) vs a single JSON body. */
|
|
563
|
-
stream:
|
|
1475
|
+
stream: z15.boolean().default(true)
|
|
564
1476
|
});
|
|
565
|
-
var LlmChatResponseSchema =
|
|
566
|
-
model:
|
|
567
|
-
content:
|
|
568
|
-
stopReason:
|
|
1477
|
+
var LlmChatResponseSchema = z15.object({
|
|
1478
|
+
model: z15.string(),
|
|
1479
|
+
content: z15.string(),
|
|
1480
|
+
stopReason: z15.string(),
|
|
569
1481
|
usage: LlmUsageSchema
|
|
570
1482
|
});
|
|
571
|
-
var LlmStreamDeltaSchema =
|
|
572
|
-
var LlmStreamDoneSchema =
|
|
573
|
-
stopReason:
|
|
1483
|
+
var LlmStreamDeltaSchema = z15.object({ text: z15.string() });
|
|
1484
|
+
var LlmStreamDoneSchema = z15.object({
|
|
1485
|
+
stopReason: z15.string(),
|
|
574
1486
|
usage: LlmUsageSchema
|
|
575
1487
|
});
|
|
576
|
-
var LlmStreamErrorSchema =
|
|
577
|
-
code:
|
|
578
|
-
message:
|
|
1488
|
+
var LlmStreamErrorSchema = z15.object({
|
|
1489
|
+
code: z15.string(),
|
|
1490
|
+
message: z15.string()
|
|
579
1491
|
});
|
|
580
1492
|
|
|
581
1493
|
// ../shared/src/llmOpenai.ts
|
|
582
|
-
import { z as
|
|
583
|
-
var OpenAiRoleSchema =
|
|
584
|
-
var OpenAiMessageSchema =
|
|
1494
|
+
import { z as z16 } from "zod";
|
|
1495
|
+
var OpenAiRoleSchema = z16.enum(["system", "developer", "user", "assistant", "tool"]);
|
|
1496
|
+
var OpenAiMessageSchema = z16.object({
|
|
585
1497
|
role: OpenAiRoleSchema,
|
|
586
|
-
content:
|
|
1498
|
+
content: z16.union([z16.string(), z16.array(z16.unknown())]).nullish(),
|
|
587
1499
|
/** Present only on assistant tool-call turns — parsed to reject, never honored. */
|
|
588
|
-
tool_calls:
|
|
589
|
-
tool_call_id:
|
|
590
|
-
name:
|
|
591
|
-
});
|
|
592
|
-
var OpenAiStreamOptionsSchema =
|
|
593
|
-
include_usage:
|
|
594
|
-
});
|
|
595
|
-
var OpenAiResponseFormatSchema =
|
|
596
|
-
|
|
597
|
-
type:
|
|
598
|
-
json_schema:
|
|
599
|
-
name:
|
|
600
|
-
schema:
|
|
601
|
-
strict:
|
|
1500
|
+
tool_calls: z16.array(z16.unknown()).optional(),
|
|
1501
|
+
tool_call_id: z16.string().optional(),
|
|
1502
|
+
name: z16.string().optional()
|
|
1503
|
+
});
|
|
1504
|
+
var OpenAiStreamOptionsSchema = z16.object({
|
|
1505
|
+
include_usage: z16.boolean().optional()
|
|
1506
|
+
});
|
|
1507
|
+
var OpenAiResponseFormatSchema = z16.discriminatedUnion("type", [
|
|
1508
|
+
z16.object({
|
|
1509
|
+
type: z16.literal("json_schema"),
|
|
1510
|
+
json_schema: z16.object({
|
|
1511
|
+
name: z16.string().optional(),
|
|
1512
|
+
schema: z16.record(z16.string(), z16.unknown()),
|
|
1513
|
+
strict: z16.boolean().nullish(),
|
|
602
1514
|
/**
|
|
603
1515
|
* Declared so the codec can reject it rather than zod silently stripping it.
|
|
604
1516
|
* It is honorable on the OpenAI path but has no Anthropic equivalent, so
|
|
605
1517
|
* forwarding it would make behaviour depend on the backing vendor — the same
|
|
606
1518
|
* provider leak that got `json_object` rejected (ADR-0034).
|
|
607
1519
|
*/
|
|
608
|
-
description:
|
|
1520
|
+
description: z16.string().optional()
|
|
609
1521
|
})
|
|
610
1522
|
}),
|
|
611
|
-
|
|
612
|
-
|
|
1523
|
+
z16.object({ type: z16.literal("json_object") }),
|
|
1524
|
+
z16.object({ type: z16.literal("text") })
|
|
613
1525
|
]);
|
|
614
|
-
var OpenAiChatCompletionRequestSchema =
|
|
615
|
-
model:
|
|
616
|
-
messages:
|
|
617
|
-
max_tokens:
|
|
618
|
-
max_completion_tokens:
|
|
619
|
-
temperature:
|
|
620
|
-
top_p:
|
|
621
|
-
stop:
|
|
622
|
-
stream:
|
|
1526
|
+
var OpenAiChatCompletionRequestSchema = z16.object({
|
|
1527
|
+
model: z16.string().min(1),
|
|
1528
|
+
messages: z16.array(OpenAiMessageSchema).min(1),
|
|
1529
|
+
max_tokens: z16.int().positive().max(128e3).optional(),
|
|
1530
|
+
max_completion_tokens: z16.int().positive().max(128e3).optional(),
|
|
1531
|
+
temperature: z16.number().optional(),
|
|
1532
|
+
top_p: z16.number().optional(),
|
|
1533
|
+
stop: z16.union([z16.string(), z16.array(z16.string())]).optional(),
|
|
1534
|
+
stream: z16.boolean().optional(),
|
|
623
1535
|
stream_options: OpenAiStreamOptionsSchema.optional(),
|
|
624
1536
|
/** Declared so the codec can reject tool use in v1 (not supported). */
|
|
625
|
-
tools:
|
|
626
|
-
tool_choice:
|
|
1537
|
+
tools: z16.array(z16.unknown()).optional(),
|
|
1538
|
+
tool_choice: z16.unknown().optional(),
|
|
627
1539
|
/**
|
|
628
1540
|
* Structured output (ADR-0034). Parsed, not rejected — see the union above.
|
|
629
1541
|
* `.nullish()` because clients and proxies that serialize every field send
|
|
@@ -634,193 +1546,228 @@ var OpenAiChatCompletionRequestSchema = z11.object({
|
|
|
634
1546
|
// Behaviour-changing params the platform does not honor in v1. Declared (not
|
|
635
1547
|
// stripped) so the codec can reject them with a 400 rather than silently drop
|
|
636
1548
|
// them — the same "reject, never silently drop" contract as `tools`.
|
|
637
|
-
n:
|
|
638
|
-
seed:
|
|
639
|
-
logit_bias:
|
|
640
|
-
presence_penalty:
|
|
641
|
-
frequency_penalty:
|
|
642
|
-
logprobs:
|
|
643
|
-
top_logprobs:
|
|
644
|
-
});
|
|
645
|
-
var OpenAiUsageSchema =
|
|
646
|
-
prompt_tokens:
|
|
647
|
-
completion_tokens:
|
|
648
|
-
total_tokens:
|
|
649
|
-
});
|
|
650
|
-
var OpenAiChatCompletionResponseSchema =
|
|
651
|
-
id:
|
|
652
|
-
object:
|
|
653
|
-
created:
|
|
654
|
-
model:
|
|
655
|
-
choices:
|
|
656
|
-
|
|
657
|
-
index:
|
|
658
|
-
message:
|
|
659
|
-
role:
|
|
660
|
-
content:
|
|
1549
|
+
n: z16.unknown().optional(),
|
|
1550
|
+
seed: z16.unknown().optional(),
|
|
1551
|
+
logit_bias: z16.unknown().optional(),
|
|
1552
|
+
presence_penalty: z16.unknown().optional(),
|
|
1553
|
+
frequency_penalty: z16.unknown().optional(),
|
|
1554
|
+
logprobs: z16.unknown().optional(),
|
|
1555
|
+
top_logprobs: z16.unknown().optional()
|
|
1556
|
+
});
|
|
1557
|
+
var OpenAiUsageSchema = z16.object({
|
|
1558
|
+
prompt_tokens: z16.int().nonnegative(),
|
|
1559
|
+
completion_tokens: z16.int().nonnegative(),
|
|
1560
|
+
total_tokens: z16.int().nonnegative()
|
|
1561
|
+
});
|
|
1562
|
+
var OpenAiChatCompletionResponseSchema = z16.object({
|
|
1563
|
+
id: z16.string(),
|
|
1564
|
+
object: z16.literal("chat.completion"),
|
|
1565
|
+
created: z16.int(),
|
|
1566
|
+
model: z16.string(),
|
|
1567
|
+
choices: z16.array(
|
|
1568
|
+
z16.object({
|
|
1569
|
+
index: z16.int(),
|
|
1570
|
+
message: z16.object({
|
|
1571
|
+
role: z16.literal("assistant"),
|
|
1572
|
+
content: z16.string()
|
|
661
1573
|
}),
|
|
662
|
-
finish_reason:
|
|
1574
|
+
finish_reason: z16.string()
|
|
663
1575
|
})
|
|
664
1576
|
),
|
|
665
1577
|
usage: OpenAiUsageSchema
|
|
666
1578
|
});
|
|
667
|
-
var OpenAiChatCompletionChunkSchema =
|
|
668
|
-
id:
|
|
669
|
-
object:
|
|
670
|
-
created:
|
|
671
|
-
model:
|
|
672
|
-
choices:
|
|
673
|
-
|
|
674
|
-
index:
|
|
675
|
-
delta:
|
|
676
|
-
role:
|
|
677
|
-
content:
|
|
1579
|
+
var OpenAiChatCompletionChunkSchema = z16.object({
|
|
1580
|
+
id: z16.string(),
|
|
1581
|
+
object: z16.literal("chat.completion.chunk"),
|
|
1582
|
+
created: z16.int(),
|
|
1583
|
+
model: z16.string(),
|
|
1584
|
+
choices: z16.array(
|
|
1585
|
+
z16.object({
|
|
1586
|
+
index: z16.int(),
|
|
1587
|
+
delta: z16.object({
|
|
1588
|
+
role: z16.literal("assistant").optional(),
|
|
1589
|
+
content: z16.string().optional()
|
|
678
1590
|
}),
|
|
679
|
-
finish_reason:
|
|
1591
|
+
finish_reason: z16.string().nullable()
|
|
680
1592
|
})
|
|
681
1593
|
),
|
|
682
1594
|
usage: OpenAiUsageSchema.nullish()
|
|
683
1595
|
});
|
|
684
|
-
var OpenAiModelSchema =
|
|
685
|
-
id:
|
|
686
|
-
object:
|
|
687
|
-
created:
|
|
688
|
-
owned_by:
|
|
1596
|
+
var OpenAiModelSchema = z16.object({
|
|
1597
|
+
id: z16.string(),
|
|
1598
|
+
object: z16.literal("model"),
|
|
1599
|
+
created: z16.int(),
|
|
1600
|
+
owned_by: z16.string()
|
|
689
1601
|
});
|
|
690
|
-
var OpenAiModelListSchema =
|
|
691
|
-
object:
|
|
692
|
-
data:
|
|
1602
|
+
var OpenAiModelListSchema = z16.object({
|
|
1603
|
+
object: z16.literal("list"),
|
|
1604
|
+
data: z16.array(OpenAiModelSchema)
|
|
693
1605
|
});
|
|
694
1606
|
|
|
695
1607
|
// ../shared/src/usage.ts
|
|
696
|
-
import { z as
|
|
697
|
-
|
|
698
|
-
|
|
1608
|
+
import { z as z18 } from "zod";
|
|
1609
|
+
|
|
1610
|
+
// ../shared/src/principal.ts
|
|
1611
|
+
import { z as z17 } from "zod";
|
|
1612
|
+
var PRINCIPAL_KINDS = ["user", "password", "anon", "dev"];
|
|
1613
|
+
var PrincipalKindSchema = z17.enum(PRINCIPAL_KINDS);
|
|
1614
|
+
|
|
1615
|
+
// ../shared/src/usage.ts
|
|
1616
|
+
var GATEWAY_OUTCOMES = [
|
|
1617
|
+
"ok",
|
|
1618
|
+
"error",
|
|
1619
|
+
"refusal",
|
|
1620
|
+
"quota_blocked",
|
|
1621
|
+
"conflict",
|
|
1622
|
+
"forbidden",
|
|
1623
|
+
"connection_required"
|
|
1624
|
+
];
|
|
1625
|
+
var GatewayOutcomeSchema = z18.enum(GATEWAY_OUTCOMES);
|
|
699
1626
|
var USAGE_RANGES = ["24h", "7d", "30d"];
|
|
700
|
-
var UsageRangeSchema =
|
|
1627
|
+
var UsageRangeSchema = z18.enum(USAGE_RANGES);
|
|
701
1628
|
var PLATFORM_RANGES = ["7d", "30d", "90d"];
|
|
702
|
-
var PlatformRangeSchema =
|
|
703
|
-
var UsageSeriesPointSchema =
|
|
704
|
-
bucket:
|
|
705
|
-
costUsd:
|
|
706
|
-
tokens:
|
|
707
|
-
requests:
|
|
708
|
-
});
|
|
709
|
-
var UsageSummarySchema =
|
|
710
|
-
appId:
|
|
1629
|
+
var PlatformRangeSchema = z18.enum(PLATFORM_RANGES);
|
|
1630
|
+
var UsageSeriesPointSchema = z18.object({
|
|
1631
|
+
bucket: z18.iso.datetime(),
|
|
1632
|
+
costUsd: z18.number().nonnegative(),
|
|
1633
|
+
tokens: z18.int().nonnegative(),
|
|
1634
|
+
requests: z18.int().nonnegative()
|
|
1635
|
+
});
|
|
1636
|
+
var UsageSummarySchema = z18.object({
|
|
1637
|
+
appId: z18.uuid(),
|
|
711
1638
|
/** The rolling range these figures cover. */
|
|
712
1639
|
range: UsageRangeSchema,
|
|
713
|
-
requests:
|
|
714
|
-
inputTokens:
|
|
715
|
-
outputTokens:
|
|
1640
|
+
requests: z18.int().nonnegative(),
|
|
1641
|
+
inputTokens: z18.int().nonnegative(),
|
|
1642
|
+
outputTokens: z18.int().nonnegative(),
|
|
716
1643
|
/** Cache-aware input token totals (0 until prompt caching is enabled). */
|
|
717
|
-
cacheReadInputTokens:
|
|
718
|
-
cacheCreationInputTokens:
|
|
1644
|
+
cacheReadInputTokens: z18.int().nonnegative(),
|
|
1645
|
+
cacheCreationInputTokens: z18.int().nonnegative(),
|
|
719
1646
|
/** Estimated spend in USD over the window at current rates (./pricing.ts). */
|
|
720
|
-
costUsd:
|
|
1647
|
+
costUsd: z18.number().nonnegative(),
|
|
721
1648
|
/** 95th-percentile upstream latency (ms) over the window; null when no timed calls. */
|
|
722
|
-
latencyP95Ms:
|
|
1649
|
+
latencyP95Ms: z18.number().nonnegative().nullable(),
|
|
723
1650
|
/** Fraction of calls in the window whose outcome was not `ok` (0..1). */
|
|
724
|
-
errorRate:
|
|
725
|
-
/** Count of calls keyed by outcome
|
|
726
|
-
byOutcome:
|
|
727
|
-
byModel:
|
|
728
|
-
|
|
729
|
-
model:
|
|
730
|
-
tokens:
|
|
731
|
-
requests:
|
|
1651
|
+
errorRate: z18.number().min(0).max(1),
|
|
1652
|
+
/** Count of calls keyed by outcome — see {@link GATEWAY_OUTCOMES}. */
|
|
1653
|
+
byOutcome: z18.record(z18.string(), z18.int().nonnegative()),
|
|
1654
|
+
byModel: z18.array(
|
|
1655
|
+
z18.object({
|
|
1656
|
+
model: z18.string(),
|
|
1657
|
+
tokens: z18.int().nonnegative(),
|
|
1658
|
+
requests: z18.int().nonnegative(),
|
|
732
1659
|
/** Estimated spend in USD for this model over the window. */
|
|
733
|
-
costUsd:
|
|
1660
|
+
costUsd: z18.number().nonnegative()
|
|
734
1661
|
})
|
|
735
1662
|
),
|
|
736
1663
|
/** Dense, zero-filled buckets across the range, oldest-first, for the trend chart. */
|
|
737
|
-
series:
|
|
1664
|
+
series: z18.array(UsageSeriesPointSchema),
|
|
738
1665
|
/**
|
|
739
1666
|
* Today-since-midnight totals, independent of `range` — backs the daily-cap
|
|
740
1667
|
* gauge (the budget the edge enforces is per calendar day).
|
|
741
1668
|
*/
|
|
742
|
-
today:
|
|
743
|
-
tokens:
|
|
744
|
-
costUsd:
|
|
1669
|
+
today: z18.object({
|
|
1670
|
+
tokens: z18.int().nonnegative(),
|
|
1671
|
+
costUsd: z18.number().nonnegative()
|
|
745
1672
|
})
|
|
746
1673
|
});
|
|
747
|
-
var GatewayCallSchema =
|
|
748
|
-
id:
|
|
749
|
-
appId:
|
|
1674
|
+
var GatewayCallSchema = z18.object({
|
|
1675
|
+
id: z18.uuid(),
|
|
1676
|
+
appId: z18.uuid(),
|
|
750
1677
|
/** App slug at read time; null when the app row no longer exists. */
|
|
751
|
-
slug:
|
|
752
|
-
userOid:
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
1678
|
+
slug: z18.string().nullable(),
|
|
1679
|
+
userOid: z18.string(),
|
|
1680
|
+
/**
|
|
1681
|
+
* The caller's claims as captured when the call was made — the **display half**
|
|
1682
|
+
* (`App.ownerName`/`ownerEmail` applied to the ledger). Render these; never
|
|
1683
|
+
* compare them, and never join on them. Null for anonymous, shared-password and
|
|
1684
|
+
* dev-token callers, and for rows predating the columns, in which case the UI
|
|
1685
|
+
* falls back to `userOid` — an opaque principal id the platform declines
|
|
1686
|
+
* to resolve (no Graph `/users` grant by design, ADR-0040 decision 3; and
|
|
1687
|
+
* pre-re-base rows hold a pairwise `sub`), so it is a last resort rather
|
|
1688
|
+
* than a label.
|
|
1689
|
+
*/
|
|
1690
|
+
userName: z18.string().nullable(),
|
|
1691
|
+
userEmail: z18.string().nullable(),
|
|
1692
|
+
/**
|
|
1693
|
+
* Which kind of principal made the call. Read this instead of parsing
|
|
1694
|
+
* `userOid` — see {@link PrincipalKindSchema} for why the obvious shape test is
|
|
1695
|
+
* unsound. Null on rows written before the column, which render from `userOid`.
|
|
1696
|
+
*/
|
|
1697
|
+
userKind: PrincipalKindSchema.nullable(),
|
|
1698
|
+
capability: z18.string(),
|
|
1699
|
+
model: z18.string(),
|
|
1700
|
+
inputTokens: z18.int().nonnegative(),
|
|
1701
|
+
outputTokens: z18.int().nonnegative(),
|
|
1702
|
+
cacheReadInputTokens: z18.int().nonnegative(),
|
|
1703
|
+
cacheCreationInputTokens: z18.int().nonnegative(),
|
|
759
1704
|
/** Estimated spend in USD for this single call at current rates. */
|
|
760
|
-
costUsd:
|
|
1705
|
+
costUsd: z18.number().nonnegative(),
|
|
761
1706
|
/** Upstream round-trip latency in ms (0 when not measured). */
|
|
762
|
-
durationMs:
|
|
1707
|
+
durationMs: z18.int().nonnegative(),
|
|
763
1708
|
/** Upstream/egress HTTP status — set for `fetch`; null for streamed `llm`. */
|
|
764
|
-
statusCode:
|
|
1709
|
+
statusCode: z18.int().nullable(),
|
|
1710
|
+
/**
|
|
1711
|
+
* Request path of the proxied call — set for `fetch`; null otherwise. The
|
|
1712
|
+
* target's query string is excluded (see `redactFetchTarget` in `./logging.ts`)
|
|
1713
|
+
* because that is where credentials are conventionally placed — but a path can
|
|
1714
|
+
* carry one too, and nothing here detects that. See ADR-0021.
|
|
1715
|
+
*/
|
|
1716
|
+
path: z18.string().nullable(),
|
|
1717
|
+
/** HTTP method of the proxied call — set for `fetch`; null otherwise. */
|
|
1718
|
+
method: z18.string().nullable(),
|
|
765
1719
|
/** LLM stop reason; null for non-LLM calls. */
|
|
766
|
-
stopReason:
|
|
1720
|
+
stopReason: z18.string().nullable(),
|
|
767
1721
|
/** Short upstream error string; null on success. */
|
|
768
|
-
errorDetail:
|
|
1722
|
+
errorDetail: z18.string().nullable(),
|
|
769
1723
|
outcome: GatewayOutcomeSchema,
|
|
770
|
-
createdAt:
|
|
1724
|
+
createdAt: z18.iso.datetime()
|
|
771
1725
|
});
|
|
772
|
-
var GatewayAuditPageSchema =
|
|
773
|
-
rows:
|
|
1726
|
+
var GatewayAuditPageSchema = z18.object({
|
|
1727
|
+
rows: z18.array(GatewayCallSchema),
|
|
774
1728
|
/** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
|
|
775
|
-
nextBefore:
|
|
1729
|
+
nextBefore: z18.iso.datetime().optional()
|
|
776
1730
|
});
|
|
777
|
-
var PlatformUsageSchema =
|
|
1731
|
+
var PlatformUsageSchema = z18.object({
|
|
778
1732
|
/** The rolling range the series + breakdowns cover. */
|
|
779
1733
|
range: PlatformRangeSchema,
|
|
780
1734
|
/** Dense, zero-filled daily buckets across the range, oldest-first. */
|
|
781
|
-
series:
|
|
1735
|
+
series: z18.array(UsageSeriesPointSchema),
|
|
782
1736
|
/** Per-app rollup over the range, busiest-first. */
|
|
783
|
-
byApp:
|
|
784
|
-
|
|
785
|
-
slug:
|
|
786
|
-
tokens:
|
|
787
|
-
requests:
|
|
1737
|
+
byApp: z18.array(
|
|
1738
|
+
z18.object({
|
|
1739
|
+
slug: z18.string().nullable(),
|
|
1740
|
+
tokens: z18.int().nonnegative(),
|
|
1741
|
+
requests: z18.int().nonnegative(),
|
|
788
1742
|
/** Estimated spend in USD for this app over the range. */
|
|
789
|
-
costUsd:
|
|
1743
|
+
costUsd: z18.number().nonnegative()
|
|
790
1744
|
})
|
|
791
1745
|
),
|
|
792
1746
|
/** Month-to-date headline KPIs (independent of `range`). */
|
|
793
|
-
totals:
|
|
794
|
-
tokensMTD:
|
|
795
|
-
requestsMTD:
|
|
1747
|
+
totals: z18.object({
|
|
1748
|
+
tokensMTD: z18.int().nonnegative(),
|
|
1749
|
+
requestsMTD: z18.int().nonnegative(),
|
|
796
1750
|
/** Estimated month-to-date spend in USD across all apps. */
|
|
797
|
-
costMTD:
|
|
1751
|
+
costMTD: z18.number().nonnegative(),
|
|
798
1752
|
/** Distinct `userOid`s seen month-to-date. */
|
|
799
|
-
activeUsers:
|
|
1753
|
+
activeUsers: z18.int().nonnegative()
|
|
800
1754
|
}),
|
|
801
1755
|
/** Token + cost share by capability over the range (essentially all `llm` in M4). */
|
|
802
|
-
capabilityMix:
|
|
803
|
-
|
|
804
|
-
capability:
|
|
805
|
-
tokens:
|
|
1756
|
+
capabilityMix: z18.array(
|
|
1757
|
+
z18.object({
|
|
1758
|
+
capability: z18.string(),
|
|
1759
|
+
tokens: z18.int().nonnegative(),
|
|
806
1760
|
/** Estimated spend in USD for this capability over the range. */
|
|
807
|
-
costUsd:
|
|
1761
|
+
costUsd: z18.number().nonnegative()
|
|
808
1762
|
})
|
|
809
1763
|
)
|
|
810
1764
|
});
|
|
811
1765
|
|
|
812
1766
|
// ../shared/src/data.ts
|
|
813
|
-
import { z as
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
var ENVS = ["prod", "dev"];
|
|
818
|
-
var EnvSchema = z13.enum(ENVS);
|
|
819
|
-
|
|
820
|
-
// ../shared/src/data.ts
|
|
821
|
-
var CollectionItemSchema = z14.object({
|
|
822
|
-
id: z14.uuid(),
|
|
823
|
-
collection: z14.string(),
|
|
1767
|
+
import { z as z19 } from "zod";
|
|
1768
|
+
var CollectionItemSchema = z19.object({
|
|
1769
|
+
id: z19.uuid(),
|
|
1770
|
+
collection: z19.string(),
|
|
824
1771
|
/**
|
|
825
1772
|
* Which tier collected the row (dev-mode §5). The runtime roles are RLS-pinned
|
|
826
1773
|
* to one tier each, but the portal reads across both, so the discriminator has
|
|
@@ -829,148 +1776,215 @@ var CollectionItemSchema = z14.object({
|
|
|
829
1776
|
*/
|
|
830
1777
|
env: EnvSchema,
|
|
831
1778
|
/** The submitting user, if authenticated; null for anonymous/public visitors. */
|
|
832
|
-
userOid:
|
|
833
|
-
|
|
1779
|
+
userOid: z19.string().nullable(),
|
|
1780
|
+
/**
|
|
1781
|
+
* The submitter's captured claims — display half, rendered but never compared
|
|
1782
|
+
* (see `GatewayCallSchema.userName`). Null whenever `userOid` is, and for
|
|
1783
|
+
* password/dev-token principals.
|
|
1784
|
+
*/
|
|
1785
|
+
userName: z19.string().nullable(),
|
|
1786
|
+
userEmail: z19.string().nullable(),
|
|
1787
|
+
/** The submitter's kind; null whenever `userOid` is, and on pre-column rows. */
|
|
1788
|
+
userKind: PrincipalKindSchema.nullable(),
|
|
1789
|
+
item: z19.unknown(),
|
|
834
1790
|
/** Hashed IP / truncated UA for abuse triage; null if not captured. */
|
|
835
|
-
meta:
|
|
836
|
-
createdAt:
|
|
1791
|
+
meta: z19.unknown().nullable(),
|
|
1792
|
+
createdAt: z19.iso.datetime()
|
|
837
1793
|
});
|
|
838
|
-
var CollectionItemsPageSchema =
|
|
839
|
-
rows:
|
|
1794
|
+
var CollectionItemsPageSchema = z19.object({
|
|
1795
|
+
rows: z19.array(CollectionItemSchema),
|
|
840
1796
|
/** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
|
|
841
|
-
nextBefore:
|
|
1797
|
+
nextBefore: z19.iso.datetime().optional()
|
|
842
1798
|
});
|
|
843
|
-
var CollectionSummarySchema =
|
|
844
|
-
name:
|
|
1799
|
+
var CollectionSummarySchema = z19.object({
|
|
1800
|
+
name: z19.string(),
|
|
845
1801
|
env: EnvSchema,
|
|
846
|
-
count:
|
|
1802
|
+
count: z19.int().nonnegative(),
|
|
847
1803
|
/** Newest row's timestamp; null only if the group is somehow empty. */
|
|
848
|
-
lastAt:
|
|
1804
|
+
lastAt: z19.iso.datetime().nullable()
|
|
849
1805
|
});
|
|
850
1806
|
|
|
851
1807
|
// ../shared/src/collectionTable.ts
|
|
852
1808
|
var BOM = String.fromCharCode(65279);
|
|
853
1809
|
|
|
854
|
-
// ../shared/src/
|
|
855
|
-
import { z as
|
|
856
|
-
var
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1810
|
+
// ../shared/src/connections.ts
|
|
1811
|
+
import { z as z20 } from "zod";
|
|
1812
|
+
var CONNECTION_STATUSES = ["live", "reconnect-needed", "invalidated"];
|
|
1813
|
+
var ConnectionStatusSchema = z20.enum(CONNECTION_STATUSES);
|
|
1814
|
+
var ConnectionMaterialSchema = z20.strictObject({
|
|
1815
|
+
access: z20.string().min(1),
|
|
1816
|
+
refresh: z20.string().min(1)
|
|
1817
|
+
});
|
|
1818
|
+
var MAX_GRANTED_SCOPES = 64;
|
|
1819
|
+
var GrantedScopesSchema = z20.array(ScopeTokenSchema).max(MAX_GRANTED_SCOPES);
|
|
1820
|
+
var UserConnectionSchema = z20.strictObject({
|
|
1821
|
+
id: z20.uuid(),
|
|
1822
|
+
userOid: z20.string().min(1),
|
|
1823
|
+
providerId: z20.uuid(),
|
|
1824
|
+
providerRevision: z20.int().positive(),
|
|
1825
|
+
env: EnvSchema,
|
|
1826
|
+
status: ConnectionStatusSchema,
|
|
1827
|
+
material: z20.string().min(1),
|
|
1828
|
+
grantedScopes: GrantedScopesSchema,
|
|
1829
|
+
grantedAt: z20.iso.datetime(),
|
|
1830
|
+
expiresAt: z20.iso.datetime(),
|
|
1831
|
+
renewBeforeNext: z20.boolean(),
|
|
1832
|
+
pendingRetire: z20.string().min(1).nullable(),
|
|
1833
|
+
lastRenewedAt: z20.iso.datetime().nullable(),
|
|
1834
|
+
createdAt: z20.iso.datetime(),
|
|
1835
|
+
updatedAt: z20.iso.datetime()
|
|
1836
|
+
});
|
|
1837
|
+
var MyConnectionSchema = z20.strictObject({
|
|
1838
|
+
id: z20.uuid(),
|
|
1839
|
+
providerRef: ProviderRefSchema,
|
|
1840
|
+
providerDisplayName: z20.string().min(1).max(200),
|
|
1841
|
+
env: EnvSchema,
|
|
1842
|
+
status: ConnectionStatusSchema.exclude(["invalidated"]),
|
|
1843
|
+
grantedScopes: GrantedScopesSchema,
|
|
1844
|
+
grantedAt: z20.iso.datetime(),
|
|
1845
|
+
sharedApps: z20.array(BoundAppSchema)
|
|
1846
|
+
});
|
|
1847
|
+
var MyConnectionsResponseSchema = z20.strictObject({
|
|
1848
|
+
connections: z20.array(MyConnectionSchema)
|
|
1849
|
+
});
|
|
1850
|
+
var DisconnectResponseSchema = z20.strictObject({
|
|
1851
|
+
outcome: z20.enum(["disconnected", "already_removed"])
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
// ../shared/src/consent.ts
|
|
1855
|
+
import { z as z21 } from "zod";
|
|
1856
|
+
var ConsentStateSchema = z21.string().min(43).max(128);
|
|
1857
|
+
var ConsentNonceSchema = z21.string().min(16).max(128);
|
|
1858
|
+
var ConsentAttemptTagSchema = z21.string().min(1).max(128).regex(/^[A-Za-z0-9._~-]+$/, "must be URL-safe punctuation, letters, or digits");
|
|
1859
|
+
var ConsentIdentitySchema = z21.discriminatedUnion("kind", [
|
|
1860
|
+
z21.strictObject({ kind: z21.literal("user"), userOid: z21.string().min(1).max(128) }),
|
|
1861
|
+
z21.strictObject({
|
|
1862
|
+
kind: z21.literal("dev"),
|
|
1863
|
+
developerOid: z21.string().min(1).max(128),
|
|
1864
|
+
nonce: ConsentNonceSchema
|
|
1865
|
+
})
|
|
865
1866
|
]);
|
|
866
|
-
var
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
};
|
|
873
|
-
var
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}),
|
|
883
|
-
|
|
884
|
-
z15.object({ kind: z15.literal("query"), param: z15.string().min(1) }),
|
|
885
|
-
/**
|
|
886
|
-
* HMAC over a timestamp. The signed input is the timestamp string **alone** —
|
|
887
|
-
* not the method, path, query, or body — so injection is a pure function of
|
|
888
|
-
* (private key, now) and needs no request context.
|
|
889
|
-
*
|
|
890
|
-
* The canonical form lives in the KIND NAME, deliberately. A scheme that signs
|
|
891
|
-
* method+path+body is a *sibling kind* (`hmac-request`) — a code change with
|
|
892
|
-
* tests, reviewed — never an admin-editable canonical-string template.
|
|
893
|
-
* Canonicalization is where implementations of this family go wrong, and it
|
|
894
|
-
* does not belong in a text box.
|
|
895
|
-
*
|
|
896
|
-
* SHA-256, lowercase-hex, and ISO-8601-with-milliseconds are fixed rather than
|
|
897
|
-
* configurable. Each would carry a default, and `app_secrets.injection` is a
|
|
898
|
-
* schemaless JSON column, so adding a knob later is purely additive with no
|
|
899
|
-
* migration — while fixing them now removes the weak-algorithm and
|
|
900
|
-
* unencodable-digest failure classes outright.
|
|
901
|
-
*
|
|
902
|
-
* The stored value is a JSON blob carrying both halves of the key pair
|
|
903
|
-
* ({@link HmacCredentialSchema}): regenerating the pair changes both, and a
|
|
904
|
-
* blob rotates atomically through the existing rotate route.
|
|
905
|
-
*/
|
|
906
|
-
z15.object({
|
|
907
|
-
kind: z15.literal("hmac-timestamp"),
|
|
908
|
-
/** Header carrying the timestamp that is also the entire signed input. */
|
|
909
|
-
timestampHeader: headerName(from),
|
|
910
|
-
/** Header carrying the rendered credential + signature. */
|
|
911
|
-
authHeader: headerName(from).default("authorization"),
|
|
912
|
-
/**
|
|
913
|
-
* Value written to `authHeader`. `{credential}` and `{signature}` are
|
|
914
|
-
* substituted. Named rather than the `header` kind's bare `{}` because there
|
|
915
|
-
* are two substitutions: the convention is one value ⇒ `{}`, more than one ⇒
|
|
916
|
-
* named placeholders. Positional `{}` here would let a swapped template
|
|
917
|
-
* produce a well-formed header that silently fails to authenticate.
|
|
918
|
-
*
|
|
919
|
-
* Both placeholders are required. This recipe writes exactly two headers,
|
|
920
|
-
* one of them the timestamp, so there is no configuration where the public
|
|
921
|
-
* credential id travels elsewhere — omitting `{credential}` means the
|
|
922
|
-
* upstream can never identify the key and every call 401s.
|
|
923
|
-
*/
|
|
924
|
-
template: headerTemplate(from).refine((t) => t.includes("{signature}"), "template must contain {signature}").refine((t) => t.includes("{credential}"), "template must contain {credential}")
|
|
925
|
-
}).refine(
|
|
926
|
-
(r) => r.timestampHeader !== r.authHeader,
|
|
927
|
-
"timestampHeader and authHeader must differ \u2014 the second write overwrites the first"
|
|
928
|
-
)
|
|
1867
|
+
var HttpUrlNoUserinfoSchema = (() => {
|
|
1868
|
+
const userinfo = /^https?:\/\/[/\\]*[^/?#\\]*@/i;
|
|
1869
|
+
return z21.url({ protocol: /^https?$/ }).refine(
|
|
1870
|
+
(value) => !userinfo.test(value.replace(/[\t\n\r]/g, "")),
|
|
1871
|
+
"URL must not carry userinfo"
|
|
1872
|
+
);
|
|
1873
|
+
})();
|
|
1874
|
+
var ConsultRequestSchema = z21.strictObject({
|
|
1875
|
+
identity: ConsentIdentitySchema,
|
|
1876
|
+
appSlug: z21.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
1877
|
+
providerRef: ProviderRefSchema,
|
|
1878
|
+
openerOrigin: HttpUrlNoUserinfoSchema.transform((value) => new URL(value).origin),
|
|
1879
|
+
callbackUrl: HttpUrlNoUserinfoSchema
|
|
1880
|
+
});
|
|
1881
|
+
var ConsultResponseSchema = z21.discriminatedUnion("outcome", [
|
|
1882
|
+
z21.strictObject({ outcome: z21.literal("already_connected") }),
|
|
1883
|
+
z21.strictObject({ outcome: z21.literal("not_available") }),
|
|
1884
|
+
z21.strictObject({ outcome: z21.literal("started"), authorizeUrl: z21.url() })
|
|
929
1885
|
]);
|
|
930
|
-
var
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
credential: z15.string().min(1),
|
|
934
|
-
key: z15.string().min(1)
|
|
1886
|
+
var CancelRequestSchema = z21.strictObject({
|
|
1887
|
+
identity: ConsentIdentitySchema,
|
|
1888
|
+
state: ConsentStateSchema
|
|
935
1889
|
});
|
|
936
|
-
var
|
|
937
|
-
var
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
var
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1890
|
+
var CONSENT_CANCEL_OUTCOMES = ["cancelled", "not_cancellable"];
|
|
1891
|
+
var CancelResponseSchema = z21.strictObject({
|
|
1892
|
+
outcome: z21.enum(CONSENT_CANCEL_OUTCOMES)
|
|
1893
|
+
});
|
|
1894
|
+
var ConnectionStatusIdentitySchema = z21.discriminatedUnion("kind", [
|
|
1895
|
+
z21.strictObject({ kind: z21.literal("user"), userOid: z21.string().min(1).max(128) }),
|
|
1896
|
+
z21.strictObject({ kind: z21.literal("dev"), developerOid: z21.string().min(1).max(128) })
|
|
1897
|
+
]);
|
|
1898
|
+
var ConnectionStatusRequestSchema = z21.strictObject({
|
|
1899
|
+
identity: ConnectionStatusIdentitySchema,
|
|
1900
|
+
appSlug: z21.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
1901
|
+
providerRef: ProviderRefSchema
|
|
1902
|
+
});
|
|
1903
|
+
var ConnectionStatusResponseSchema = z21.strictObject({
|
|
1904
|
+
status: z21.enum(["connected", "not_connected", "not_available"])
|
|
1905
|
+
});
|
|
1906
|
+
var AttemptCancelRequestSchema = z21.strictObject({
|
|
1907
|
+
provider: ProviderRefSchema,
|
|
1908
|
+
attempt: ConsentAttemptTagSchema
|
|
1909
|
+
});
|
|
1910
|
+
var DevConsentStartResponseSchema = z21.discriminatedUnion("outcome", [
|
|
1911
|
+
z21.strictObject({ outcome: z21.literal("started"), popupUrl: z21.url() }),
|
|
1912
|
+
z21.strictObject({ outcome: z21.literal("already_connected") }),
|
|
1913
|
+
z21.strictObject({ outcome: z21.literal("not_available") })
|
|
1914
|
+
]);
|
|
1915
|
+
var HELIX_CONNECT_MESSAGE_SOURCE = "helix-connect";
|
|
1916
|
+
var CONNECT_MESSAGE_VERSION = 1;
|
|
1917
|
+
var CONSENT_MESSAGE_OUTCOMES = [
|
|
1918
|
+
"connected",
|
|
1919
|
+
"already_connected",
|
|
1920
|
+
"denied",
|
|
1921
|
+
"cancelled",
|
|
1922
|
+
"timeout",
|
|
1923
|
+
"blocked",
|
|
1924
|
+
"signin_required",
|
|
1925
|
+
"error"
|
|
1926
|
+
];
|
|
1927
|
+
var CONSENT_MESSAGE_REASONS = [
|
|
1928
|
+
"conflict",
|
|
1929
|
+
"provider_unavailable",
|
|
1930
|
+
"provider_misconfigured",
|
|
1931
|
+
"provider_incompatible",
|
|
1932
|
+
"service_unavailable"
|
|
1933
|
+
];
|
|
1934
|
+
var ConnectOutcomeMessageSchema = z21.strictObject({
|
|
1935
|
+
source: z21.literal(HELIX_CONNECT_MESSAGE_SOURCE),
|
|
1936
|
+
version: z21.literal(CONNECT_MESSAGE_VERSION),
|
|
1937
|
+
attempt: ConsentAttemptTagSchema.optional(),
|
|
1938
|
+
provider: ProviderRefSchema,
|
|
1939
|
+
outcome: z21.enum(CONSENT_MESSAGE_OUTCOMES),
|
|
1940
|
+
reason: z21.enum(CONSENT_MESSAGE_REASONS).nullable()
|
|
1941
|
+
}).refine((message) => message.outcome === "error" || message.reason === null, {
|
|
1942
|
+
error: "reason is only carried with the error outcome"
|
|
1943
|
+
});
|
|
1944
|
+
var ConnectResultSchema = z21.object({
|
|
1945
|
+
outcome: z21.enum(CONSENT_MESSAGE_OUTCOMES),
|
|
1946
|
+
provider: z21.string(),
|
|
1947
|
+
attempt: ConsentAttemptTagSchema.optional(),
|
|
1948
|
+
reason: z21.enum(CONSENT_MESSAGE_REASONS).optional()
|
|
1949
|
+
}).refine((result) => result.outcome === "error" || result.reason === void 0, {
|
|
1950
|
+
error: "reason is only carried with the error outcome"
|
|
968
1951
|
});
|
|
969
|
-
|
|
970
|
-
|
|
1952
|
+
|
|
1953
|
+
// ../shared/src/exchange.ts
|
|
1954
|
+
import { z as z22 } from "zod";
|
|
1955
|
+
var ExchangeCodeSchema = z22.string().min(1).max(1024);
|
|
1956
|
+
var CodeVerifierSchema = z22.string().min(43).max(128).regex(/^[A-Za-z0-9\-._~]+$/, "must be an RFC 7636 code_verifier (43-128 unreserved characters)");
|
|
1957
|
+
var ExchangeRequestSchema = z22.strictObject({
|
|
1958
|
+
providerId: z22.uuid(),
|
|
1959
|
+
providerRevision: z22.int().positive(),
|
|
1960
|
+
env: EnvSchema,
|
|
1961
|
+
code: ExchangeCodeSchema,
|
|
1962
|
+
codeVerifier: CodeVerifierSchema,
|
|
1963
|
+
redirectUri: HttpUrlNoUserinfoSchema
|
|
1964
|
+
});
|
|
1965
|
+
var EXCHANGE_REJECTION_REASONS = [
|
|
1966
|
+
"unusable_lifetime",
|
|
1967
|
+
"missing_refresh_token",
|
|
1968
|
+
"missing_permissions"
|
|
1969
|
+
];
|
|
1970
|
+
var ExchangeRejectionReasonSchema = z22.enum(EXCHANGE_REJECTION_REASONS);
|
|
1971
|
+
var MAX_EXCHANGED_SCOPES = 64;
|
|
1972
|
+
var ExchangedScopesSchema = z22.array(ScopeTokenSchema).max(MAX_EXCHANGED_SCOPES);
|
|
1973
|
+
var ExchangeResponseSchema = z22.discriminatedUnion("outcome", [
|
|
1974
|
+
z22.strictObject({
|
|
1975
|
+
outcome: z22.literal("exchanged"),
|
|
1976
|
+
access: z22.string().min(1),
|
|
1977
|
+
refresh: z22.string().min(1),
|
|
1978
|
+
grantedScopes: ExchangedScopesSchema,
|
|
1979
|
+
accessExpiresAt: z22.iso.datetime()
|
|
1980
|
+
}),
|
|
1981
|
+
z22.strictObject({ outcome: z22.literal("rejected"), reason: ExchangeRejectionReasonSchema }),
|
|
1982
|
+
z22.strictObject({ outcome: z22.literal("provider_unavailable") }),
|
|
1983
|
+
z22.strictObject({ outcome: z22.literal("exchange_failed") })
|
|
1984
|
+
]);
|
|
971
1985
|
|
|
972
1986
|
// ../shared/src/devTokens.ts
|
|
973
|
-
import { z as
|
|
1987
|
+
import { z as z23 } from "zod";
|
|
974
1988
|
function isValidDevOrigin(input) {
|
|
975
1989
|
if (input.includes("*")) return false;
|
|
976
1990
|
let url;
|
|
@@ -984,47 +1998,69 @@ function isValidDevOrigin(input) {
|
|
|
984
1998
|
if (url.username !== "" || url.password !== "") return false;
|
|
985
1999
|
return input === url.origin || input === `${url.origin}/`;
|
|
986
2000
|
}
|
|
987
|
-
var DevOriginSchema =
|
|
2001
|
+
var DevOriginSchema = z23.string().refine(
|
|
988
2002
|
isValidDevOrigin,
|
|
989
2003
|
"must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
|
|
990
2004
|
).transform((o) => new URL(o).origin);
|
|
991
|
-
var TtlDaysSchema =
|
|
992
|
-
var DevTokenMintRequestSchema =
|
|
993
|
-
origins:
|
|
2005
|
+
var TtlDaysSchema = z23.number().int().min(1).max(365).optional();
|
|
2006
|
+
var DevTokenMintRequestSchema = z23.object({
|
|
2007
|
+
origins: z23.array(DevOriginSchema).min(1).max(20),
|
|
994
2008
|
ttlDays: TtlDaysSchema
|
|
995
2009
|
});
|
|
996
|
-
var DevTokenRotateRequestSchema =
|
|
2010
|
+
var DevTokenRotateRequestSchema = z23.object({
|
|
997
2011
|
ttlDays: TtlDaysSchema
|
|
998
2012
|
});
|
|
999
|
-
var DevTokenMetadataSchema =
|
|
1000
|
-
id:
|
|
1001
|
-
developerOid:
|
|
1002
|
-
origins:
|
|
1003
|
-
expiresAt:
|
|
1004
|
-
revokedAt:
|
|
1005
|
-
createdAt:
|
|
1006
|
-
});
|
|
1007
|
-
var DevTokenMintResponseSchema =
|
|
1008
|
-
token:
|
|
2013
|
+
var DevTokenMetadataSchema = z23.object({
|
|
2014
|
+
id: z23.string(),
|
|
2015
|
+
developerOid: z23.string(),
|
|
2016
|
+
origins: z23.array(z23.string()),
|
|
2017
|
+
expiresAt: z23.string(),
|
|
2018
|
+
revokedAt: z23.string().nullable().optional(),
|
|
2019
|
+
createdAt: z23.string()
|
|
2020
|
+
});
|
|
2021
|
+
var DevTokenMintResponseSchema = z23.object({
|
|
2022
|
+
token: z23.string(),
|
|
1009
2023
|
metadata: DevTokenMetadataSchema
|
|
1010
2024
|
});
|
|
1011
2025
|
|
|
1012
2026
|
// ../shared/src/instruction.ts
|
|
1013
|
-
import { z as
|
|
2027
|
+
import { z as z24 } from "zod";
|
|
1014
2028
|
var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
|
|
1015
|
-
var InstructionCapabilitySchema =
|
|
1016
|
-
var AttestedInstructionSchema =
|
|
2029
|
+
var InstructionCapabilitySchema = z24.enum(INSTRUCTION_CAPABILITIES);
|
|
2030
|
+
var AttestedInstructionSchema = z24.strictObject({
|
|
1017
2031
|
/** App the call is attributed to (registry app id). */
|
|
1018
|
-
appId:
|
|
2032
|
+
appId: z24.string().min(1),
|
|
1019
2033
|
/** Authenticated user, or the anonymous sentinel on `public` apps. */
|
|
1020
|
-
userOid:
|
|
2034
|
+
userOid: z24.string().min(1),
|
|
2035
|
+
/**
|
|
2036
|
+
* Which kind of principal `userOid` is (the `PrincipalKind` vocabulary the
|
|
2037
|
+
* edge records at capture time — recorded, never inferred). The delegated
|
|
2038
|
+
* resolution refuses `anon` and `password` callers — they can
|
|
2039
|
+
* never hold a connection — and serving that refusal
|
|
2040
|
+
* requires knowing the KIND: `userOid`'s shape proves nothing (a shared-
|
|
2041
|
+
* password pseudonym and an Entra `sub` share the base64url alphabet, and
|
|
2042
|
+
* the sentinel is only exact for `anon`). Optional on the payload as a
|
|
2043
|
+
* whole so secret-backed and keyless instructions keep minting unchanged;
|
|
2044
|
+
* mandatory on a delegated one — the refine below makes a provider-bearing
|
|
2045
|
+
* instruction without it unrepresentable, so an old edge cannot mint a
|
|
2046
|
+
* delegated call egress would have to guess about.
|
|
2047
|
+
*/
|
|
2048
|
+
userKind: PrincipalKindSchema.optional(),
|
|
1021
2049
|
capability: InstructionCapabilitySchema,
|
|
1022
2050
|
/** The allowlisted origin the edge authorized (scheme + host + port). */
|
|
1023
|
-
origin:
|
|
2051
|
+
origin: z24.url(),
|
|
1024
2052
|
/** Connection (secret) name to inject, if this is a secret-backed call. */
|
|
1025
|
-
connection:
|
|
2053
|
+
connection: z24.string().min(1).optional(),
|
|
2054
|
+
/**
|
|
2055
|
+
* Provider reference for a delegated call — the user's connection to this
|
|
2056
|
+
* provider is what egress injects. Exactly
|
|
2057
|
+
* one credential source travels: this and `connection` are XOR-enforced
|
|
2058
|
+
* below — a delegated instruction carries a provider ref or a
|
|
2059
|
+
* secret name, never both.
|
|
2060
|
+
*/
|
|
2061
|
+
provider: ProviderRefSchema.optional(),
|
|
1026
2062
|
/** Correlates the edge audit row with the egress call. */
|
|
1027
|
-
requestId:
|
|
2063
|
+
requestId: z24.string().min(1),
|
|
1028
2064
|
/**
|
|
1029
2065
|
* The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
|
|
1030
2066
|
* Egress refuses a mismatched verb/resource, so a captured instruction can't be
|
|
@@ -1039,8 +2075,8 @@ var AttestedInstructionSchema = z17.object({
|
|
|
1039
2075
|
* means "old edge", not tampering — egress asserts ONLY when the claim is
|
|
1040
2076
|
* present. Make required once a fleet is reliably past deploy.
|
|
1041
2077
|
*/
|
|
1042
|
-
method:
|
|
1043
|
-
path:
|
|
2078
|
+
method: z24.string().min(1).optional(),
|
|
2079
|
+
path: z24.string().optional(),
|
|
1044
2080
|
/**
|
|
1045
2081
|
* Environment tier this call is scoped to (dev-mode design §6). Egress resolves
|
|
1046
2082
|
* the connection secret within this tier — a `dev` instruction can never reach a
|
|
@@ -1049,12 +2085,27 @@ var AttestedInstructionSchema = z17.object({
|
|
|
1049
2085
|
* minted before this field existed verifies as production.
|
|
1050
2086
|
*/
|
|
1051
2087
|
env: EnvSchema.default("prod")
|
|
2088
|
+
}).superRefine((i, ctx) => {
|
|
2089
|
+
if (i.connection !== void 0 && i.provider !== void 0) {
|
|
2090
|
+
ctx.addIssue({
|
|
2091
|
+
code: "custom",
|
|
2092
|
+
path: ["provider"],
|
|
2093
|
+
message: "an instruction carries exactly one credential source \u2014 a connection secret or a provider reference, never both."
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
if (i.provider !== void 0 && i.userKind === void 0) {
|
|
2097
|
+
ctx.addIssue({
|
|
2098
|
+
code: "custom",
|
|
2099
|
+
path: ["userKind"],
|
|
2100
|
+
message: "a delegated instruction must carry the caller's principal kind \u2014 egress refuses anonymous and shared-password callers by kind, never by inferring it from userOid."
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
1052
2103
|
});
|
|
1053
2104
|
var INSTRUCTION_TTL_SECONDS = 30;
|
|
1054
2105
|
var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
|
|
1055
2106
|
|
|
1056
2107
|
// ../shared/src/fetch.ts
|
|
1057
|
-
import { z as
|
|
2108
|
+
import { z as z25 } from "zod";
|
|
1058
2109
|
var FETCH_ERROR_CODES = [
|
|
1059
2110
|
"forbidden",
|
|
1060
2111
|
"rate_limited",
|
|
@@ -1062,12 +2113,75 @@ var FETCH_ERROR_CODES = [
|
|
|
1062
2113
|
"blocked",
|
|
1063
2114
|
"too_large",
|
|
1064
2115
|
"replay",
|
|
1065
|
-
"upstream_error"
|
|
2116
|
+
"upstream_error",
|
|
2117
|
+
"connection_required",
|
|
2118
|
+
"provider_unavailable",
|
|
2119
|
+
"provider_misconfigured"
|
|
1066
2120
|
];
|
|
1067
|
-
var FetchErrorCodeSchema =
|
|
1068
|
-
var
|
|
2121
|
+
var FetchErrorCodeSchema = z25.enum(FETCH_ERROR_CODES);
|
|
2122
|
+
var FetchErrorProviderSchema = z25.strictObject({
|
|
2123
|
+
ref: ProviderRefSchema,
|
|
2124
|
+
displayName: z25.string().min(1).max(200).optional()
|
|
2125
|
+
});
|
|
2126
|
+
var FetchProxyErrorSchema = z25.object({
|
|
1069
2127
|
code: FetchErrorCodeSchema,
|
|
1070
|
-
message:
|
|
2128
|
+
message: z25.string(),
|
|
2129
|
+
/** Present only on the delegated-call codes that carry it (`connection_required`, `provider_unavailable`). */
|
|
2130
|
+
provider: FetchErrorProviderSchema.optional()
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
// ../shared/src/sessions.ts
|
|
2134
|
+
import { z as z26 } from "zod";
|
|
2135
|
+
var SessionPrincipalKindSchema = z26.enum(["user", "password"]);
|
|
2136
|
+
var SessionSummarySchema = z26.object({
|
|
2137
|
+
id: z26.uuid(),
|
|
2138
|
+
appId: z26.uuid(),
|
|
2139
|
+
/** App slug at read time; the FK guarantees the app row exists. */
|
|
2140
|
+
slug: z26.string(),
|
|
2141
|
+
/**
|
|
2142
|
+
* The identity half — Entra's `oid` claim since ADR-0048 (a pairwise `sub`
|
|
2143
|
+
* on rows that predate the re-base; a `pw_*` pseudonym for shared-password
|
|
2144
|
+
* visitors). Compared and joined on, never rendered except as a last
|
|
2145
|
+
* resort: the platform deliberately holds no Graph `/users` grant, so no
|
|
2146
|
+
* principal id it stores resolves to a name.
|
|
2147
|
+
*/
|
|
2148
|
+
userOid: z26.string(),
|
|
2149
|
+
/**
|
|
2150
|
+
* The display half, captured at login. Render, never compare. Null for
|
|
2151
|
+
* shared-password visitors (each login is a fresh pseudonym) and for rows
|
|
2152
|
+
* predating the columns.
|
|
2153
|
+
*/
|
|
2154
|
+
userName: z26.string().nullable(),
|
|
2155
|
+
userEmail: z26.string().nullable(),
|
|
2156
|
+
userKind: SessionPrincipalKindSchema.nullable(),
|
|
2157
|
+
/** Group-id snapshot as of this session's login/refresh. */
|
|
2158
|
+
groups: z26.array(z26.string()),
|
|
2159
|
+
createdAt: z26.iso.datetime(),
|
|
2160
|
+
/** Null while the handoff is un-redeemed — impossible here (live rows only). */
|
|
2161
|
+
activatedAt: z26.iso.datetime().nullable(),
|
|
2162
|
+
/** When silent re-auth (`prompt=none`) becomes due; past = overdue. */
|
|
2163
|
+
refreshDueAt: z26.iso.datetime(),
|
|
2164
|
+
/** Hard cap — the session is dead at this instant regardless of activity. */
|
|
2165
|
+
expiresAt: z26.iso.datetime()
|
|
2166
|
+
});
|
|
2167
|
+
var SessionListResponseSchema = z26.object({
|
|
2168
|
+
rows: z26.array(SessionSummarySchema),
|
|
2169
|
+
groupNames: z26.record(z26.string(), z26.string().nullable()),
|
|
2170
|
+
groupsResolved: z26.boolean()
|
|
2171
|
+
});
|
|
2172
|
+
var SessionRevokeRequestSchema = z26.object({
|
|
2173
|
+
/**
|
|
2174
|
+
* A session's principal id as seen in the list — the `oid` claim, a `pw_*`
|
|
2175
|
+
* pseudonym, or a pre-re-base pairwise `sub`. Opaque to the admin; copied
|
|
2176
|
+
* from the row they clicked, so its shape is whatever the session rows
|
|
2177
|
+
* hold — bounded only against abuse, not parsed.
|
|
2178
|
+
*/
|
|
2179
|
+
userOid: z26.string().min(1).max(200)
|
|
2180
|
+
});
|
|
2181
|
+
var SessionRevokeResultSchema = z26.object({
|
|
2182
|
+
removed: z26.int().nonnegative(),
|
|
2183
|
+
/** Slugs of the apps the removed sessions lived on; empty when none. */
|
|
2184
|
+
apps: z26.array(z26.string())
|
|
1071
2185
|
});
|
|
1072
2186
|
|
|
1073
2187
|
// src/client.ts
|
|
@@ -1079,7 +2193,7 @@ var CliError = class extends Error {
|
|
|
1079
2193
|
this.code = code;
|
|
1080
2194
|
}
|
|
1081
2195
|
};
|
|
1082
|
-
var VersionListSchema =
|
|
2196
|
+
var VersionListSchema = z27.array(VersionSchema);
|
|
1083
2197
|
var PortalClient = class {
|
|
1084
2198
|
#baseUrl;
|
|
1085
2199
|
#tokenProvider;
|
|
@@ -1089,7 +2203,17 @@ var PortalClient = class {
|
|
|
1089
2203
|
this.#tokenProvider = typeof token === "string" ? async () => token : token;
|
|
1090
2204
|
}
|
|
1091
2205
|
createApp(input) {
|
|
1092
|
-
return this.#json(AppSchema, "POST", "/api/v1/apps", {
|
|
2206
|
+
return this.#json(AppSchema, "POST", "/api/v1/apps", {
|
|
2207
|
+
auth: true,
|
|
2208
|
+
body: {
|
|
2209
|
+
slug: input.slug,
|
|
2210
|
+
displayName: input.displayName,
|
|
2211
|
+
// Sent only when set, so a description-free create posts exactly the
|
|
2212
|
+
// body it always did — an older portal never sees a new key at all.
|
|
2213
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
2214
|
+
...input.visibility !== void 0 ? { visibility: input.visibility } : {}
|
|
2215
|
+
}
|
|
2216
|
+
});
|
|
1093
2217
|
}
|
|
1094
2218
|
/** Public IdP discovery info — how `helix login` finds the issuer. */
|
|
1095
2219
|
getAuthConfig() {
|
|
@@ -1099,6 +2223,15 @@ var PortalClient = class {
|
|
|
1099
2223
|
me() {
|
|
1100
2224
|
return this.#json(PortalMeResponseSchema, "GET", "/api/v1/me", { auth: true });
|
|
1101
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* The deployment-rendered agent skill (`GET /api/v1/skill`, ADR-0036) —
|
|
2228
|
+
* `text/markdown`, already substituted with this deployment's hosts, servable
|
|
2229
|
+
* models, and approval baselines. Authed like every other `/api/v1` read.
|
|
2230
|
+
* Powers `helix skill`.
|
|
2231
|
+
*/
|
|
2232
|
+
getSkill() {
|
|
2233
|
+
return this.#text("/api/v1/skill", { auth: true });
|
|
2234
|
+
}
|
|
1102
2235
|
listVersions(slug) {
|
|
1103
2236
|
return this.#json(VersionListSchema, "GET", `/api/v1/apps/${enc(slug)}/versions`, {
|
|
1104
2237
|
auth: true
|
|
@@ -1156,23 +2289,47 @@ var PortalClient = class {
|
|
|
1156
2289
|
}
|
|
1157
2290
|
return schema.parse(data);
|
|
1158
2291
|
}
|
|
2292
|
+
/** A non-JSON GET (the markdown skill) — errors still typed via the envelope. */
|
|
2293
|
+
async #text(path3, opts) {
|
|
2294
|
+
const headers = await this.#authHeaders(opts.auth ?? false);
|
|
2295
|
+
const res = await fetch(this.#url(path3), { method: "GET", headers });
|
|
2296
|
+
const body = await res.text();
|
|
2297
|
+
if (!res.ok) {
|
|
2298
|
+
const data = body ? safeJsonParse(body) : void 0;
|
|
2299
|
+
const parsed = ApiErrorSchema.safeParse(data);
|
|
2300
|
+
if (parsed.success) throw new CliError(parsed.data.error.message, parsed.data.error.code);
|
|
2301
|
+
throw new CliError(`request failed (HTTP ${res.status})`);
|
|
2302
|
+
}
|
|
2303
|
+
return body;
|
|
2304
|
+
}
|
|
1159
2305
|
};
|
|
1160
2306
|
function enc(segment) {
|
|
1161
2307
|
return encodeURIComponent(segment);
|
|
1162
2308
|
}
|
|
2309
|
+
function safeJsonParse(value) {
|
|
2310
|
+
try {
|
|
2311
|
+
return JSON.parse(value);
|
|
2312
|
+
} catch {
|
|
2313
|
+
return void 0;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
1163
2316
|
|
|
1164
2317
|
// src/commands.ts
|
|
1165
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
2318
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2319
|
+
import { resolve } from "node:path";
|
|
2320
|
+
|
|
2321
|
+
// ../deploy-skill/src/index.ts
|
|
2322
|
+
var SKILL_FILENAME = "SKILL.md";
|
|
1166
2323
|
|
|
1167
2324
|
// src/zip.ts
|
|
1168
2325
|
import { ZipArchive } from "archiver";
|
|
1169
2326
|
function zipDirectory(dir) {
|
|
1170
|
-
return new Promise((
|
|
2327
|
+
return new Promise((resolve2, reject) => {
|
|
1171
2328
|
const chunks = [];
|
|
1172
2329
|
const archive = new ZipArchive({ zlib: { level: 9 } });
|
|
1173
2330
|
archive.on("data", (c) => chunks.push(c));
|
|
1174
2331
|
archive.on("error", reject);
|
|
1175
|
-
archive.on("end", () =>
|
|
2332
|
+
archive.on("end", () => resolve2(Buffer.concat(chunks)));
|
|
1176
2333
|
archive.directory(dir, false);
|
|
1177
2334
|
void archive.finalize();
|
|
1178
2335
|
});
|
|
@@ -1297,9 +2454,16 @@ function requireSlug(config) {
|
|
|
1297
2454
|
function parseVisibility(input) {
|
|
1298
2455
|
if (!input) return void 0;
|
|
1299
2456
|
if (input.startsWith("group:")) {
|
|
1300
|
-
const
|
|
1301
|
-
if (
|
|
1302
|
-
|
|
2457
|
+
const groupIds = input.slice("group:".length).split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
2458
|
+
if (groupIds.length === 0) {
|
|
2459
|
+
throw new CliError("group visibility needs at least one id: group:<id>[,<id>\u2026]");
|
|
2460
|
+
}
|
|
2461
|
+
if (groupIds.length > MAX_VISIBILITY_GROUPS) {
|
|
2462
|
+
throw new CliError(
|
|
2463
|
+
`group visibility takes at most ${MAX_VISIBILITY_GROUPS} ids (got ${groupIds.length})`
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
return { mode: "group", groupIds };
|
|
1303
2467
|
}
|
|
1304
2468
|
if (input === "internal" || input === "password" || input === "public") {
|
|
1305
2469
|
return { mode: input };
|
|
@@ -1309,7 +2473,9 @@ function parseVisibility(input) {
|
|
|
1309
2473
|
'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
2474
|
);
|
|
1311
2475
|
}
|
|
1312
|
-
throw new CliError(
|
|
2476
|
+
throw new CliError(
|
|
2477
|
+
`invalid visibility "${input}" (internal | group:<id>[,<id>\u2026] | password | public)`
|
|
2478
|
+
);
|
|
1313
2479
|
}
|
|
1314
2480
|
function printVersion(v) {
|
|
1315
2481
|
console.log(` version ${v.number} (${v.status}) \u2014 ${v.id}`);
|
|
@@ -1317,12 +2483,16 @@ function printVersion(v) {
|
|
|
1317
2483
|
}
|
|
1318
2484
|
function printApp(app) {
|
|
1319
2485
|
console.log(` app ${app.slug} \u2014 live version: ${app.currentVersionId ?? "(none)"}`);
|
|
2486
|
+
if (app.description) console.log(` ${app.description}`);
|
|
1320
2487
|
}
|
|
1321
2488
|
async function createCommand(client, config, opts) {
|
|
1322
2489
|
const slug = requireSlug(config);
|
|
1323
2490
|
const app = await client.createApp({
|
|
1324
2491
|
slug,
|
|
1325
2492
|
displayName: opts.displayName ?? slug,
|
|
2493
|
+
// Flag wins over helix.json, and an unset description is simply not sent —
|
|
2494
|
+
// an older portal never sees a key it doesn't know.
|
|
2495
|
+
description: opts.description ?? config.description,
|
|
1326
2496
|
visibility: parseVisibility(opts.visibility)
|
|
1327
2497
|
});
|
|
1328
2498
|
console.log(`Created app "${app.slug}".`);
|
|
@@ -1385,7 +2555,8 @@ async function loginCommand(client, config) {
|
|
|
1385
2555
|
await writeTokens({ portalUrl: config.portalUrl, issuer }, tokens, void 0, { audience });
|
|
1386
2556
|
const authed = new PortalClient(config.portalUrl, tokens.accessToken);
|
|
1387
2557
|
const me = await authed.me();
|
|
1388
|
-
|
|
2558
|
+
const oid = me.oid ? ` \u2014 oid ${me.oid}` : "";
|
|
2559
|
+
console.log(`Logged in as ${me.name ?? me.sub} (${me.sub})${oid}.`);
|
|
1389
2560
|
}
|
|
1390
2561
|
async function logoutCommand(config) {
|
|
1391
2562
|
const forgot = await deleteTokens(config.portalUrl);
|
|
@@ -1393,7 +2564,14 @@ async function logoutCommand(config) {
|
|
|
1393
2564
|
}
|
|
1394
2565
|
async function whoamiCommand(client) {
|
|
1395
2566
|
const me = await client.me();
|
|
1396
|
-
|
|
2567
|
+
const oid = me.oid ? `${me.oid} \u2014 ` : "";
|
|
2568
|
+
console.log(`${oid}${me.sub} (via ${me.via}${me.name ? `, ${me.name}` : ""})`);
|
|
2569
|
+
}
|
|
2570
|
+
async function skillCommand(client, _config, opts) {
|
|
2571
|
+
const skill = await client.getSkill();
|
|
2572
|
+
const outPath = resolve(opts.path ?? SKILL_FILENAME);
|
|
2573
|
+
await writeFile2(outPath, skill, "utf8");
|
|
2574
|
+
console.log(`Wrote skill to ${outPath}`);
|
|
1397
2575
|
}
|
|
1398
2576
|
|
|
1399
2577
|
// src/auth/session.ts
|
|
@@ -1461,7 +2639,8 @@ async function resolveConfig(flags, env = process.env, cwd = process.cwd()) {
|
|
|
1461
2639
|
token: flags.token ?? env.HELIX_TOKEN ?? env.AZX_TOKEN,
|
|
1462
2640
|
slug: flags.slug ?? file.slug,
|
|
1463
2641
|
dir: flags.dir ?? file.dir ?? DEFAULT_DIR,
|
|
1464
|
-
bundle: flags.bundle
|
|
2642
|
+
bundle: flags.bundle,
|
|
2643
|
+
description: flags.description ?? file.description
|
|
1465
2644
|
};
|
|
1466
2645
|
}
|
|
1467
2646
|
|
|
@@ -1473,15 +2652,16 @@ Usage:
|
|
|
1473
2652
|
helix logout
|
|
1474
2653
|
helix whoami
|
|
1475
2654
|
helix deploy [--dir <dir>] [--bundle <zip>] [--promote]
|
|
1476
|
-
helix create [--display-name <name>] [--visibility <v>]
|
|
2655
|
+
helix create [--display-name <name>] [--description <text>] [--visibility <v>]
|
|
1477
2656
|
helix versions
|
|
1478
2657
|
helix promote <number>
|
|
1479
2658
|
helix rollback [number]
|
|
2659
|
+
helix skill [--path <file>] # write this deployment's agent skill to disk
|
|
1480
2660
|
|
|
1481
2661
|
Common flags: --slug <slug> --portal-url <url> --token <token>
|
|
1482
2662
|
Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; CI/scripts).
|
|
1483
|
-
Config file: helix.json { slug, portalUrl, dir }
|
|
1484
|
-
Visibility: internal | group:<id> | password | public
|
|
2663
|
+
Config file: helix.json { slug, portalUrl, dir, description }
|
|
2664
|
+
Visibility: internal | group:<id>[,<id>...] | password | public
|
|
1485
2665
|
`;
|
|
1486
2666
|
async function main() {
|
|
1487
2667
|
const { values, positionals } = parseCliArgs(process.argv.slice(2));
|
|
@@ -1511,12 +2691,16 @@ async function main() {
|
|
|
1511
2691
|
case "whoami":
|
|
1512
2692
|
await whoamiCommand(client);
|
|
1513
2693
|
break;
|
|
2694
|
+
case "skill":
|
|
2695
|
+
await skillCommand(client, config, { path: values.path });
|
|
2696
|
+
break;
|
|
1514
2697
|
case "deploy":
|
|
1515
2698
|
await deployCommand(client, config, { promote: values.promote });
|
|
1516
2699
|
break;
|
|
1517
2700
|
case "create":
|
|
1518
2701
|
await createCommand(client, config, {
|
|
1519
2702
|
displayName: values["display-name"],
|
|
2703
|
+
description: values.description ?? config.description,
|
|
1520
2704
|
visibility: values.visibility
|
|
1521
2705
|
});
|
|
1522
2706
|
break;
|