@azx-pbc/helix-cli 0.3.0 → 0.5.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 +51 -11
- package/dist/helix.js +1872 -592
- package/package.json +3 -3
package/dist/helix.js
CHANGED
|
@@ -10,8 +10,13 @@ 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" },
|
|
14
15
|
path: { type: "string" },
|
|
16
|
+
repo: { type: "string" },
|
|
17
|
+
branch: { type: "string" },
|
|
18
|
+
reusable: { type: "boolean" },
|
|
19
|
+
force: { type: "boolean" },
|
|
15
20
|
help: { type: "boolean" }
|
|
16
21
|
};
|
|
17
22
|
function parseCliArgs(argv) {
|
|
@@ -20,7 +25,7 @@ function parseCliArgs(argv) {
|
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
// src/client.ts
|
|
23
|
-
import { z as
|
|
28
|
+
import { z as z29 } from "zod";
|
|
24
29
|
|
|
25
30
|
// ../shared/src/visibility.ts
|
|
26
31
|
import { z } from "zod";
|
|
@@ -88,10 +93,19 @@ var DirectoryGroupsResponseSchema = z2.discriminatedUnion("available", [
|
|
|
88
93
|
// ../shared/src/app.ts
|
|
89
94
|
import { z as z3 } from "zod";
|
|
90
95
|
var SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
96
|
+
var APP_DESCRIPTION_MAX = 500;
|
|
91
97
|
var AppSchema = z3.object({
|
|
92
98
|
id: z3.uuid(),
|
|
93
99
|
slug: z3.string().min(1).max(63).regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
94
100
|
displayName: z3.string().min(1).max(200),
|
|
101
|
+
/**
|
|
102
|
+
* What the app is for, in one sentence. Plain text (≤ {@link APP_DESCRIPTION_MAX}
|
|
103
|
+
* chars) — it renders as a subtitle in list rows and the detail header, so
|
|
104
|
+
* markdown would invite a wall of text in a table cell. Optional on the wire
|
|
105
|
+
* like `url`/`ownerId`: the CLI parses this schema, and every row predating
|
|
106
|
+
* the field carries none. Never `null` — an unset description omits the key.
|
|
107
|
+
*/
|
|
108
|
+
description: z3.string().max(APP_DESCRIPTION_MAX).optional(),
|
|
95
109
|
visibility: VisibilitySchema,
|
|
96
110
|
/** The version currently served; null before the first deploy. */
|
|
97
111
|
currentVersionId: z3.uuid().nullable(),
|
|
@@ -109,9 +123,9 @@ var AppSchema = z3.object({
|
|
|
109
123
|
*/
|
|
110
124
|
url: z3.url().optional(),
|
|
111
125
|
/**
|
|
112
|
-
* Who owns this app — the
|
|
113
|
-
*
|
|
114
|
-
*
|
|
126
|
+
* Who owns this app — the creator's Entra `oid` (ADR-0048). The **identity**
|
|
127
|
+
* half: this is what `scope=mine` and `ownsApp` compare against, and it is
|
|
128
|
+
* never what a screen renders.
|
|
115
129
|
*
|
|
116
130
|
* Optional for the same reason as `url`: the CLI parses this schema, and rows
|
|
117
131
|
* predating the approvals work carry no owner at all.
|
|
@@ -119,11 +133,12 @@ var AppSchema = z3.object({
|
|
|
119
133
|
ownerId: z3.string().optional(),
|
|
120
134
|
/**
|
|
121
135
|
* The owner's claims as captured at create time. The **display** half: render
|
|
122
|
-
* these, never compare them. `ownerId`
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
136
|
+
* these, never compare them. `ownerId` is the Entra `oid` (ADR-0048) — an
|
|
137
|
+
* opaque directory object id since the re-base, opaque-by-accident (an
|
|
138
|
+
* email) on rows created before it — so display travels separately. Absent
|
|
139
|
+
* for rows created before the columns existed; fall back to `ownerId` (the
|
|
140
|
+
* pre-re-base rows where it is still an email are the one case where the
|
|
141
|
+
* fallback renders something readable).
|
|
127
142
|
*/
|
|
128
143
|
ownerName: z3.string().optional(),
|
|
129
144
|
ownerEmail: z3.string().optional()
|
|
@@ -165,6 +180,16 @@ var DeployReportSchema = z4.object({
|
|
|
165
180
|
/** Candidate roots the planner considered. */
|
|
166
181
|
candidates: z4.array(z4.string().max(1024)).max(20).default([])
|
|
167
182
|
});
|
|
183
|
+
var VersionSourceSchema = z4.object({
|
|
184
|
+
provider: z4.literal("github"),
|
|
185
|
+
repoFullName: z4.string(),
|
|
186
|
+
repositoryId: z4.string(),
|
|
187
|
+
ref: z4.string(),
|
|
188
|
+
commitSha: z4.string(),
|
|
189
|
+
workflowRef: z4.string(),
|
|
190
|
+
runId: z4.string(),
|
|
191
|
+
runAttempt: z4.int().positive()
|
|
192
|
+
});
|
|
168
193
|
var VersionSchema = z4.object({
|
|
169
194
|
id: z4.uuid(),
|
|
170
195
|
appId: z4.uuid(),
|
|
@@ -175,20 +200,385 @@ var VersionSchema = z4.object({
|
|
|
175
200
|
status: VersionStatusSchema,
|
|
176
201
|
createdAt: z4.iso.datetime(),
|
|
177
202
|
/** Client-asserted salvage provenance (ADR-0038); absent on CLI uploads. */
|
|
178
|
-
deployReport: DeployReportSchema.optional()
|
|
203
|
+
deployReport: DeployReportSchema.optional(),
|
|
204
|
+
/** Verified CI provenance (ADR-0051); absent on non-CI uploads. */
|
|
205
|
+
source: VersionSourceSchema.optional()
|
|
179
206
|
});
|
|
180
207
|
|
|
181
208
|
// ../shared/src/manifest.ts
|
|
209
|
+
import { z as z8 } from "zod";
|
|
210
|
+
|
|
211
|
+
// ../shared/src/providers.ts
|
|
212
|
+
import { z as z7 } from "zod";
|
|
213
|
+
|
|
214
|
+
// ../shared/src/env.ts
|
|
182
215
|
import { z as z5 } from "zod";
|
|
183
|
-
var
|
|
184
|
-
|
|
216
|
+
var ENVS = ["prod", "dev"];
|
|
217
|
+
var EnvSchema = z5.enum(ENVS);
|
|
218
|
+
|
|
219
|
+
// ../shared/src/secrets.ts
|
|
220
|
+
import { z as z6 } from "zod";
|
|
221
|
+
var FORBIDDEN_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
222
|
+
"host",
|
|
223
|
+
"content-length",
|
|
224
|
+
"transfer-encoding",
|
|
225
|
+
"connection",
|
|
226
|
+
"te",
|
|
227
|
+
"upgrade",
|
|
228
|
+
"expect",
|
|
229
|
+
"trailer"
|
|
230
|
+
]);
|
|
231
|
+
var HEADER_TOKEN = /^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/;
|
|
232
|
+
var HEADER_VALUE_SAFE = /^[\t\x20-\x7e]*$/;
|
|
233
|
+
var HEADER_VALUE_WIRE = /^[\t\x20-\x7e\x80-\xff]*$/;
|
|
234
|
+
var headerName = (from) => {
|
|
235
|
+
const base = z6.string().min(1);
|
|
236
|
+
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");
|
|
237
|
+
};
|
|
238
|
+
var headerTemplate = (from) => from === "request" ? z6.string().max(512).regex(HEADER_VALUE_SAFE) : z6.string().regex(HEADER_VALUE_WIRE);
|
|
239
|
+
var injectionRecipe = (from) => z6.discriminatedUnion("kind", [
|
|
240
|
+
/** `Authorization: Bearer <secret>` — the common case. */
|
|
241
|
+
z6.object({ kind: z6.literal("header-bearer") }),
|
|
242
|
+
/** Arbitrary header; `{}` in `template` is replaced with the secret. */
|
|
243
|
+
z6.object({
|
|
244
|
+
kind: z6.literal("header"),
|
|
245
|
+
name: headerName(from),
|
|
246
|
+
template: headerTemplate(from).default("{}")
|
|
247
|
+
}),
|
|
248
|
+
/** Query parameter `?<param>=<secret>`. */
|
|
249
|
+
z6.object({ kind: z6.literal("query"), param: z6.string().min(1) }),
|
|
250
|
+
/**
|
|
251
|
+
* HMAC over a timestamp. The signed input is the timestamp string **alone** —
|
|
252
|
+
* not the method, path, query, or body — so injection is a pure function of
|
|
253
|
+
* (private key, now) and needs no request context.
|
|
254
|
+
*
|
|
255
|
+
* The canonical form lives in the KIND NAME, deliberately. A scheme that signs
|
|
256
|
+
* method+path+body is a *sibling kind* (`hmac-request`) — a code change with
|
|
257
|
+
* tests, reviewed — never an admin-editable canonical-string template.
|
|
258
|
+
* Canonicalization is where implementations of this family go wrong, and it
|
|
259
|
+
* does not belong in a text box.
|
|
260
|
+
*
|
|
261
|
+
* SHA-256, lowercase-hex, and ISO-8601-with-milliseconds are fixed rather than
|
|
262
|
+
* configurable. Each would carry a default, and `app_secrets.injection` is a
|
|
263
|
+
* schemaless JSON column, so adding a knob later is purely additive with no
|
|
264
|
+
* migration — while fixing them now removes the weak-algorithm and
|
|
265
|
+
* unencodable-digest failure classes outright.
|
|
266
|
+
*
|
|
267
|
+
* The stored value is a JSON blob carrying both halves of the key pair
|
|
268
|
+
* ({@link HmacCredentialSchema}): regenerating the pair changes both, and a
|
|
269
|
+
* blob rotates atomically through the existing rotate route.
|
|
270
|
+
*/
|
|
271
|
+
z6.object({
|
|
272
|
+
kind: z6.literal("hmac-timestamp"),
|
|
273
|
+
/** Header carrying the timestamp that is also the entire signed input. */
|
|
274
|
+
timestampHeader: headerName(from),
|
|
275
|
+
/** Header carrying the rendered credential + signature. */
|
|
276
|
+
authHeader: headerName(from).default("authorization"),
|
|
277
|
+
/**
|
|
278
|
+
* Value written to `authHeader`. `{credential}` and `{signature}` are
|
|
279
|
+
* substituted. Named rather than the `header` kind's bare `{}` because there
|
|
280
|
+
* are two substitutions: the convention is one value ⇒ `{}`, more than one ⇒
|
|
281
|
+
* named placeholders. Positional `{}` here would let a swapped template
|
|
282
|
+
* produce a well-formed header that silently fails to authenticate.
|
|
283
|
+
*
|
|
284
|
+
* Both placeholders are required. This recipe writes exactly two headers,
|
|
285
|
+
* one of them the timestamp, so there is no configuration where the public
|
|
286
|
+
* credential id travels elsewhere — omitting `{credential}` means the
|
|
287
|
+
* upstream can never identify the key and every call 401s.
|
|
288
|
+
*/
|
|
289
|
+
template: headerTemplate(from).refine((t) => t.includes("{signature}"), "template must contain {signature}").refine((t) => t.includes("{credential}"), "template must contain {credential}")
|
|
290
|
+
}).refine(
|
|
291
|
+
(r) => r.timestampHeader !== r.authHeader,
|
|
292
|
+
"timestampHeader and authHeader must differ \u2014 the second write overwrites the first"
|
|
293
|
+
)
|
|
294
|
+
]);
|
|
295
|
+
var InjectionRecipeSchema = injectionRecipe("request");
|
|
296
|
+
var StoredInjectionRecipeSchema = injectionRecipe("stored");
|
|
297
|
+
var HeaderNameSchema = headerName("request");
|
|
298
|
+
var HmacCredentialSchema = z6.object({
|
|
299
|
+
credential: z6.string().min(1),
|
|
300
|
+
key: z6.string().min(1)
|
|
301
|
+
});
|
|
302
|
+
var SECRET_SCOPES = ["app", "global", "platform"];
|
|
303
|
+
var SecretScopeSchema = z6.enum(SECRET_SCOPES);
|
|
304
|
+
var SecretMetadataSchema = z6.object({
|
|
305
|
+
id: z6.string(),
|
|
306
|
+
name: z6.string().min(1),
|
|
307
|
+
scope: SecretScopeSchema,
|
|
308
|
+
/** Partition tier (dev-mode §6): a dev fetch injects only `dev` connection secrets. */
|
|
309
|
+
env: EnvSchema,
|
|
310
|
+
/**
|
|
311
|
+
* The **stored** parser, not the strict one — this field describes a row, and
|
|
312
|
+
* the SPA re-parses this schema on every response. Using the strict parser here
|
|
313
|
+
* would let the portal return a name the browser then refuses, moving the read
|
|
314
|
+
* failure from the server to the client, where it presents as a dead page.
|
|
315
|
+
*
|
|
316
|
+
* `null` = the stored recipe is unreadable (it names a reserved header, or is
|
|
317
|
+
* not a recipe at all). The credential still exists and is still deletable; it
|
|
318
|
+
* cannot be rotated, and egress fails its hop closed. Recipes are immutable by
|
|
319
|
+
* design, so recovery is delete-and-recreate.
|
|
320
|
+
*/
|
|
321
|
+
injection: StoredInjectionRecipeSchema.nullable(),
|
|
322
|
+
createdBy: z6.string(),
|
|
323
|
+
createdAt: z6.string(),
|
|
324
|
+
rotatedAt: z6.string().nullable().optional(),
|
|
325
|
+
lastUsedAt: z6.string().nullable().optional(),
|
|
326
|
+
boundApps: z6.array(z6.string()).default([])
|
|
327
|
+
});
|
|
328
|
+
var SecretCreateRequestSchema = z6.object({
|
|
329
|
+
name: z6.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and hyphens"),
|
|
330
|
+
value: z6.string().min(1),
|
|
331
|
+
/** Target tier (dev-mode §6). Defaults `prod`; `dev` configures a dev-tier credential. */
|
|
332
|
+
env: EnvSchema.default("prod"),
|
|
333
|
+
injection: InjectionRecipeSchema.default({ kind: "header-bearer" })
|
|
334
|
+
});
|
|
335
|
+
var SecretRotateRequestSchema = z6.object({ value: z6.string().min(1) });
|
|
336
|
+
var SecretGrantRequestSchema = z6.object({ appSlug: z6.string().min(1) });
|
|
337
|
+
|
|
338
|
+
// ../shared/src/providers.ts
|
|
339
|
+
var PROVIDER_KINDS = ["rest-delegated"];
|
|
340
|
+
var ProviderKindSchema = z7.enum(PROVIDER_KINDS);
|
|
341
|
+
var PROVIDER_REF_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
342
|
+
var PROVIDER_REF_MAX = 64;
|
|
343
|
+
var ProviderRefSchema = z7.string().min(1).max(PROVIDER_REF_MAX).regex(PROVIDER_REF_PATTERN, "lowercase letters, digits, and hyphens");
|
|
344
|
+
var TokenPlacementSchema = z7.discriminatedUnion("kind", [
|
|
345
|
+
/** `Authorization: Bearer <access token>` — the default placement. */
|
|
346
|
+
z7.strictObject({ kind: z7.literal("header-bearer") }),
|
|
347
|
+
/** The access token verbatim in the named header (no template machinery). */
|
|
348
|
+
z7.strictObject({ kind: z7.literal("header"), name: HeaderNameSchema })
|
|
349
|
+
]);
|
|
350
|
+
var ScopeTokenSchema = z7.string().min(1).max(256).regex(/^[\x21\x23-\x5b\x5d-\x7e]+$/, "must be an RFC 6749 scope token");
|
|
351
|
+
var API_ORIGIN_RULE = "an API destination is an origin \u2014 scheme://host[:port], no path, query, credentials, or fragment";
|
|
352
|
+
var BARE_ORIGIN = /^https?:\/\/[^/?#@\\]*(?:\/)?$/i;
|
|
353
|
+
var ApiOriginSchema = z7.url({ protocol: /^https?$/ }).superRefine((value, ctx) => {
|
|
354
|
+
if (!BARE_ORIGIN.test(value)) {
|
|
355
|
+
ctx.addIssue({ code: "custom", message: API_ORIGIN_RULE });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
let url;
|
|
359
|
+
try {
|
|
360
|
+
url = new URL(value);
|
|
361
|
+
} catch {
|
|
362
|
+
ctx.addIssue({ code: "custom", message: API_ORIGIN_RULE });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
366
|
+
ctx.addIssue({ code: "custom", message: API_ORIGIN_RULE });
|
|
367
|
+
}
|
|
368
|
+
}).transform((value) => new URL(value).origin);
|
|
369
|
+
var URL_PARSER_CONTROLS = /[\t\n\r]/g;
|
|
370
|
+
var ENDPOINT_USERINFO = /^https?:\/\/[/\\]*[^/?#\\]*@/i;
|
|
371
|
+
var OAuthEndpointSchema = z7.url({ protocol: /^https?$/ }).refine(
|
|
372
|
+
(value) => !ENDPOINT_USERINFO.test(value.replace(URL_PARSER_CONTROLS, "")),
|
|
373
|
+
"a vendor endpoint URL must not carry userinfo \u2014 client auth is never configured in the URL"
|
|
374
|
+
);
|
|
375
|
+
var MAX_API_ORIGINS = 16;
|
|
376
|
+
var MAX_REQUESTED_SCOPES = 32;
|
|
377
|
+
var noDuplicates = (values) => new Set(values).size === values.length;
|
|
378
|
+
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");
|
|
379
|
+
var RequestedScopesSchema = z7.array(ScopeTokenSchema).max(MAX_REQUESTED_SCOPES).refine(noDuplicates, "duplicate requested scope");
|
|
380
|
+
var ProviderDisplayNameSchema = z7.string().min(1).max(200);
|
|
381
|
+
var ProviderEditableFieldsSchema = z7.strictObject({
|
|
382
|
+
displayName: ProviderDisplayNameSchema,
|
|
383
|
+
authorizeEndpoint: OAuthEndpointSchema,
|
|
384
|
+
tokenEndpoint: OAuthEndpointSchema,
|
|
385
|
+
requestedScopes: RequestedScopesSchema.default([]),
|
|
386
|
+
apiOrigins: ApiOriginsSchema,
|
|
387
|
+
tokenPlacement: TokenPlacementSchema.default({ kind: "header-bearer" })
|
|
388
|
+
});
|
|
389
|
+
var ProviderConfigSchema = ProviderEditableFieldsSchema.extend({
|
|
390
|
+
ref: ProviderRefSchema,
|
|
391
|
+
kind: ProviderKindSchema.default("rest-delegated")
|
|
392
|
+
});
|
|
393
|
+
var ProviderCreateRequestSchema = ProviderConfigSchema.extend({
|
|
394
|
+
env: EnvSchema,
|
|
395
|
+
clientId: z7.string().min(1),
|
|
396
|
+
clientSecret: z7.string().min(1)
|
|
397
|
+
});
|
|
398
|
+
var undefinedKeysMeanAbsence = (body) => {
|
|
399
|
+
if (typeof body !== "object" || body === null) return body;
|
|
400
|
+
const withoutUndefined = { ...body };
|
|
401
|
+
for (const key of Object.keys(withoutUndefined)) {
|
|
402
|
+
if (withoutUndefined[key] === void 0) delete withoutUndefined[key];
|
|
403
|
+
}
|
|
404
|
+
return withoutUndefined;
|
|
405
|
+
};
|
|
406
|
+
var OWN_PROTO_KEY_RULE = "an own __proto__ key is not a provider field \u2014 the body is refused whole";
|
|
407
|
+
var ProviderUpdateRequestSchema = z7.preprocess(
|
|
408
|
+
(body, ctx) => {
|
|
409
|
+
const normalized = undefinedKeysMeanAbsence(body);
|
|
410
|
+
if (typeof normalized === "object" && normalized !== null && Object.hasOwn(normalized, "__proto__")) {
|
|
411
|
+
ctx.addIssue({ code: "custom", message: OWN_PROTO_KEY_RULE });
|
|
412
|
+
}
|
|
413
|
+
return normalized;
|
|
414
|
+
},
|
|
415
|
+
ProviderEditableFieldsSchema.extend({
|
|
416
|
+
requestedScopes: RequestedScopesSchema,
|
|
417
|
+
tokenPlacement: TokenPlacementSchema,
|
|
418
|
+
revision: z7.int().positive(),
|
|
419
|
+
clientId: z7.string().min(1).optional(),
|
|
420
|
+
clientSecret: z7.string().min(1).optional(),
|
|
421
|
+
/**
|
|
422
|
+
* The invalidation acknowledgement ({@link CONFIRM_INVALIDATION_FIELD}):
|
|
423
|
+
* the client sets it to `true` when submitting a sensitive delta through
|
|
424
|
+
* the review panel. Absent or
|
|
425
|
+
* `false` against a sensitive delta is the 409
|
|
426
|
+
* `confirmation_required` rejection — nothing is applied; against a
|
|
427
|
+
* non-sensitive edit it is irrelevant and ignored. Not stored: it is
|
|
428
|
+
* consent for the mutation, never provider state.
|
|
429
|
+
*/
|
|
430
|
+
confirmInvalidation: z7.boolean().optional()
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
var ConnectionProviderSchema = ProviderEditableFieldsSchema.extend({
|
|
434
|
+
requestedScopes: RequestedScopesSchema,
|
|
435
|
+
tokenPlacement: TokenPlacementSchema,
|
|
436
|
+
id: z7.uuid(),
|
|
437
|
+
revision: z7.int().positive(),
|
|
438
|
+
ref: ProviderRefSchema,
|
|
439
|
+
kind: ProviderKindSchema,
|
|
440
|
+
env: EnvSchema,
|
|
441
|
+
clientIdMaterial: z7.string().min(1),
|
|
442
|
+
clientSecretMaterial: z7.string().min(1),
|
|
443
|
+
createdAt: z7.iso.datetime(),
|
|
444
|
+
updatedAt: z7.iso.datetime()
|
|
445
|
+
});
|
|
446
|
+
var ProviderMetadataSchema = ConnectionProviderSchema.omit({
|
|
447
|
+
clientIdMaterial: true,
|
|
448
|
+
clientSecretMaterial: true
|
|
449
|
+
});
|
|
450
|
+
var ProviderListResponseSchema = z7.strictObject({
|
|
451
|
+
callbackUrl: z7.url(),
|
|
452
|
+
providers: z7.array(ProviderMetadataSchema)
|
|
453
|
+
});
|
|
454
|
+
var ProviderExportDocumentSchema = z7.strictObject({
|
|
455
|
+
version: z7.literal(1),
|
|
456
|
+
provider: ProviderConfigSchema
|
|
457
|
+
});
|
|
458
|
+
var CatalogueProviderSchema = z7.strictObject({
|
|
459
|
+
ref: ProviderRefSchema,
|
|
460
|
+
kind: ProviderKindSchema,
|
|
461
|
+
displayName: z7.string().min(1).max(200),
|
|
462
|
+
apiOrigins: ApiOriginsSchema,
|
|
463
|
+
env: EnvSchema
|
|
464
|
+
});
|
|
465
|
+
var SENSITIVE_PROVIDER_FIELDS = [
|
|
466
|
+
"clientId",
|
|
467
|
+
"authorizeEndpoint",
|
|
468
|
+
"tokenEndpoint",
|
|
469
|
+
"requestedScopes",
|
|
470
|
+
"apiOrigins",
|
|
471
|
+
"tokenPlacement"
|
|
472
|
+
];
|
|
473
|
+
var CONFIRM_INVALIDATION_FIELD = "confirmInvalidation";
|
|
474
|
+
var SensitiveProviderFieldSchema = z7.enum(SENSITIVE_PROVIDER_FIELDS);
|
|
475
|
+
var PROVIDER_DIFF_FIELDS = [
|
|
476
|
+
"displayName",
|
|
477
|
+
"authorizeEndpoint",
|
|
478
|
+
"tokenEndpoint",
|
|
479
|
+
"requestedScopes",
|
|
480
|
+
"apiOrigins",
|
|
481
|
+
"tokenPlacement"
|
|
482
|
+
];
|
|
483
|
+
var ProviderDiffFieldSchema = z7.enum(PROVIDER_DIFF_FIELDS);
|
|
484
|
+
var ProviderImportPreviewRequestSchema = z7.strictObject({
|
|
485
|
+
document: ProviderExportDocumentSchema,
|
|
486
|
+
mode: z7.enum(["create", "update"]).optional(),
|
|
487
|
+
env: EnvSchema.optional(),
|
|
488
|
+
targetId: z7.uuid().optional()
|
|
489
|
+
});
|
|
490
|
+
var ProviderImportCollisionSchema = z7.strictObject({
|
|
491
|
+
providerId: z7.uuid(),
|
|
492
|
+
ref: ProviderRefSchema,
|
|
493
|
+
env: EnvSchema
|
|
494
|
+
});
|
|
495
|
+
var ProviderImportPreviewTargetSchema = z7.strictObject({
|
|
496
|
+
providerId: z7.uuid(),
|
|
497
|
+
ref: ProviderRefSchema,
|
|
498
|
+
env: EnvSchema,
|
|
499
|
+
displayName: z7.string().min(1).max(200),
|
|
500
|
+
revision: z7.int().positive()
|
|
501
|
+
});
|
|
502
|
+
var ProviderImportPreviewDiffEntrySchema = z7.strictObject({
|
|
503
|
+
field: ProviderDiffFieldSchema,
|
|
504
|
+
current: z7.union([z7.string(), z7.array(z7.string()), TokenPlacementSchema]),
|
|
505
|
+
imported: z7.union([z7.string(), z7.array(z7.string()), TokenPlacementSchema])
|
|
506
|
+
});
|
|
507
|
+
var ProviderImportPreviewResponseSchema = z7.discriminatedUnion("mode", [
|
|
508
|
+
z7.strictObject({ mode: z7.literal(null), provider: ProviderConfigSchema }),
|
|
509
|
+
z7.strictObject({
|
|
510
|
+
mode: z7.literal("create"),
|
|
511
|
+
env: EnvSchema,
|
|
512
|
+
provider: ProviderConfigSchema,
|
|
513
|
+
collision: ProviderImportCollisionSchema.nullable()
|
|
514
|
+
}),
|
|
515
|
+
z7.strictObject({
|
|
516
|
+
mode: z7.literal("update"),
|
|
517
|
+
target: ProviderImportPreviewTargetSchema,
|
|
518
|
+
diff: z7.array(ProviderImportPreviewDiffEntrySchema),
|
|
519
|
+
sensitiveFields: z7.array(SensitiveProviderFieldSchema)
|
|
520
|
+
})
|
|
521
|
+
]);
|
|
522
|
+
var ProviderImportCreateRequestSchema = z7.strictObject({
|
|
523
|
+
mode: z7.literal("create"),
|
|
524
|
+
document: ProviderExportDocumentSchema,
|
|
525
|
+
env: EnvSchema,
|
|
526
|
+
clientId: z7.string().min(1),
|
|
527
|
+
clientSecret: z7.string().min(1)
|
|
528
|
+
});
|
|
529
|
+
var ProviderImportUpdateRequestSchema = z7.strictObject({
|
|
530
|
+
mode: z7.literal("update"),
|
|
531
|
+
document: ProviderExportDocumentSchema,
|
|
532
|
+
targetId: z7.uuid(),
|
|
533
|
+
revision: z7.int().positive(),
|
|
534
|
+
clientId: z7.string().min(1).optional(),
|
|
535
|
+
clientSecret: z7.string().min(1).optional(),
|
|
536
|
+
[CONFIRM_INVALIDATION_FIELD]: z7.boolean().optional()
|
|
537
|
+
});
|
|
538
|
+
var ProviderImportRequestSchema = z7.discriminatedUnion("mode", [
|
|
539
|
+
ProviderImportCreateRequestSchema,
|
|
540
|
+
ProviderImportUpdateRequestSchema
|
|
541
|
+
]);
|
|
542
|
+
var ProviderImportResponseSchema = z7.strictObject({
|
|
543
|
+
outcome: z7.enum(["created", "updated"]),
|
|
544
|
+
provider: ProviderMetadataSchema
|
|
545
|
+
});
|
|
546
|
+
var BoundAppSchema = z7.strictObject({
|
|
547
|
+
id: z7.uuid(),
|
|
548
|
+
slug: z7.string().max(63).regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
549
|
+
displayName: z7.string().min(1).max(200)
|
|
550
|
+
});
|
|
551
|
+
var ProviderImpactSchema = z7.strictObject({
|
|
552
|
+
providerId: z7.uuid(),
|
|
553
|
+
ref: ProviderRefSchema,
|
|
554
|
+
env: EnvSchema,
|
|
555
|
+
displayName: z7.string().min(1).max(200),
|
|
556
|
+
revision: z7.int().positive(),
|
|
557
|
+
boundApps: z7.array(BoundAppSchema),
|
|
558
|
+
connections: z7.number().int().nonnegative(),
|
|
559
|
+
pendingAttempts: z7.number().int().nonnegative()
|
|
560
|
+
});
|
|
561
|
+
var ConfirmationRequiredDetailsSchema = z7.strictObject({
|
|
562
|
+
impact: ProviderImpactSchema,
|
|
563
|
+
sensitiveFields: z7.array(SensitiveProviderFieldSchema).optional()
|
|
564
|
+
});
|
|
565
|
+
var ProviderDeleteRequestSchema = z7.strictObject({
|
|
566
|
+
[CONFIRM_INVALIDATION_FIELD]: z7.boolean().optional()
|
|
567
|
+
});
|
|
568
|
+
var ProviderDeleteResponseSchema = z7.strictObject({
|
|
569
|
+
outcome: z7.enum(["deleted", "already_removed"])
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// ../shared/src/manifest.ts
|
|
573
|
+
var LlmCapabilitySchema = z8.object({
|
|
574
|
+
models: z8.array(z8.string()).default([]),
|
|
185
575
|
/**
|
|
186
576
|
* Per-app daily LLM spend cap in USD; unset ⇒ unbounded. Denominated in
|
|
187
577
|
* dollars (not tokens) so the cap means the same thing across models — the
|
|
188
578
|
* edge prices each call via `@azx-pbc/shared` pricing and enforces a daily +
|
|
189
579
|
* rolling-hour burst window off the frozen `costMicroUsd` ledger column.
|
|
190
580
|
*/
|
|
191
|
-
dollarsPerDay:
|
|
581
|
+
dollarsPerDay: z8.number().positive().optional()
|
|
192
582
|
});
|
|
193
583
|
var KEY_MAX_CHARS = 256;
|
|
194
584
|
var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
@@ -199,18 +589,18 @@ function isValidDataKey(key) {
|
|
|
199
589
|
key === key.trim();
|
|
200
590
|
}
|
|
201
591
|
var DATA_KEY_RULE = "must be 1\u2013256 printable ASCII characters (0x20\u20130x7E) with no leading or trailing space";
|
|
202
|
-
var DataKeySchema =
|
|
203
|
-
var DataCapabilitySchema =
|
|
204
|
-
user:
|
|
205
|
-
collections:
|
|
206
|
-
sharedRead:
|
|
207
|
-
sharedWrite:
|
|
592
|
+
var DataKeySchema = z8.string().refine(isValidDataKey, { message: DATA_KEY_RULE });
|
|
593
|
+
var DataCapabilitySchema = z8.object({
|
|
594
|
+
user: z8.boolean().default(false),
|
|
595
|
+
collections: z8.array(DataKeySchema).default([]),
|
|
596
|
+
sharedRead: z8.array(DataKeySchema).default([]),
|
|
597
|
+
sharedWrite: z8.array(DataKeySchema).default([]),
|
|
208
598
|
/** ADR-0042: authorize every key starting with one of these prefixes (read). */
|
|
209
|
-
sharedReadPrefixes:
|
|
599
|
+
sharedReadPrefixes: z8.array(DataKeySchema).default([]),
|
|
210
600
|
/** ADR-0042: authorize every key starting with one of these prefixes (write). */
|
|
211
|
-
sharedWritePrefixes:
|
|
212
|
-
writesPerDay:
|
|
213
|
-
bytesPerDay:
|
|
601
|
+
sharedWritePrefixes: z8.array(DataKeySchema).default([]),
|
|
602
|
+
writesPerDay: z8.int().positive().optional(),
|
|
603
|
+
bytesPerDay: z8.int().positive().optional()
|
|
214
604
|
}).superRefine((d, ctx) => {
|
|
215
605
|
if (d.sharedWritePrefixes.length > 0 && d.writesPerDay === void 0) {
|
|
216
606
|
ctx.addIssue({
|
|
@@ -220,17 +610,37 @@ var DataCapabilitySchema = z5.object({
|
|
|
220
610
|
});
|
|
221
611
|
}
|
|
222
612
|
});
|
|
223
|
-
var FetchConnectionSchema =
|
|
224
|
-
origin:
|
|
225
|
-
connection:
|
|
613
|
+
var FetchConnectionSchema = z8.strictObject({
|
|
614
|
+
origin: z8.url(),
|
|
615
|
+
connection: z8.string().min(1).optional(),
|
|
616
|
+
provider: ProviderRefSchema.optional(),
|
|
617
|
+
required: z8.boolean().optional()
|
|
618
|
+
}).superRefine((c, ctx) => {
|
|
619
|
+
if (c.connection !== void 0 && c.provider !== void 0) {
|
|
620
|
+
ctx.addIssue({
|
|
621
|
+
code: "custom",
|
|
622
|
+
path: ["provider"],
|
|
623
|
+
message: "an origin binds at most one credential source \u2014 declare a stored secret (connection) or an OAuth provider (provider), not both"
|
|
624
|
+
});
|
|
625
|
+
}
|
|
226
626
|
});
|
|
227
|
-
var FetchCapabilitySchema =
|
|
228
|
-
/**
|
|
229
|
-
|
|
627
|
+
var FetchCapabilitySchema = z8.object({
|
|
628
|
+
/**
|
|
629
|
+
* Legacy view of the fetch-shim grant. The grant is declared first-class as
|
|
630
|
+
* {@link ShimCapabilitySchema} (`capabilities.shim.fetch`); this boolean is
|
|
631
|
+
* the pre-`capabilities.shim` spelling, still accepted on input and kept synchronized
|
|
632
|
+
* with the canonical block on every parse (see {@link CapabilitiesSchema}) —
|
|
633
|
+
* new manifests should declare `capabilities.shim`, not this field.
|
|
634
|
+
*/
|
|
635
|
+
shim: z8.boolean().default(false),
|
|
230
636
|
/** Origins reached through the proxy (mode `proxy`); direct stays in `externalOrigins`. */
|
|
231
|
-
origins:
|
|
637
|
+
origins: z8.array(FetchConnectionSchema).default([]),
|
|
232
638
|
/** Per-app daily proxied-request budget; unset ⇒ unbounded (fetch-proxy §7). */
|
|
233
|
-
requestsPerDay:
|
|
639
|
+
requestsPerDay: z8.int().positive().optional()
|
|
640
|
+
});
|
|
641
|
+
var ShimCapabilitySchema = z8.object({
|
|
642
|
+
fetch: z8.boolean().default(false),
|
|
643
|
+
connect: z8.boolean().default(false)
|
|
234
644
|
});
|
|
235
645
|
var SCOPE_SEGMENT = /^[A-Za-z0-9\-._~]+$/;
|
|
236
646
|
function isValidServiceWorkerScope(scope) {
|
|
@@ -243,36 +653,67 @@ function isValidServiceWorkerScope(scope) {
|
|
|
243
653
|
}
|
|
244
654
|
return !first.startsWith("_");
|
|
245
655
|
}
|
|
246
|
-
var OfflineCapabilitySchema =
|
|
656
|
+
var OfflineCapabilitySchema = z8.object({
|
|
247
657
|
/**
|
|
248
658
|
* URL path prefix the worker controls, e.g. `/app/`. Leading and trailing
|
|
249
659
|
* slash required; never root, never a `_`-prefixed platform namespace.
|
|
250
660
|
*/
|
|
251
|
-
scope:
|
|
661
|
+
scope: z8.string().refine(isValidServiceWorkerScope, {
|
|
252
662
|
message: "scope must be a non-root path prefix with a leading and trailing slash, and must not start with a reserved `_` segment (e.g. `/app/`)"
|
|
253
663
|
})
|
|
254
664
|
});
|
|
255
|
-
var CapabilitiesSchema =
|
|
665
|
+
var CapabilitiesSchema = z8.object({
|
|
256
666
|
llm: LlmCapabilitySchema.optional(),
|
|
257
667
|
data: DataCapabilitySchema.optional(),
|
|
258
668
|
/** Platform-registered MCP servers this app may reach (§6.1, exposed as REST). */
|
|
259
|
-
mcp:
|
|
669
|
+
mcp: z8.array(z8.string()).default([]),
|
|
260
670
|
/** Extra CSP `connect-src` origins for **direct** browser calls (§4.4). */
|
|
261
|
-
externalOrigins:
|
|
671
|
+
externalOrigins: z8.array(z8.url()).default([]),
|
|
262
672
|
/** Governed outbound HTTP via the fetch-proxy / egress plane (in build, M4.5). */
|
|
263
673
|
fetch: FetchCapabilitySchema.optional(),
|
|
264
674
|
/** Platform-owned, scope-confined service worker for offline cold boot (ADR-0035). */
|
|
265
|
-
offline: OfflineCapabilitySchema.optional()
|
|
675
|
+
offline: OfflineCapabilitySchema.optional(),
|
|
676
|
+
/** Injected platform helpers — the first-class shim grant. */
|
|
677
|
+
shim: ShimCapabilitySchema.optional()
|
|
678
|
+
}).transform((caps) => {
|
|
679
|
+
const legacyShim = caps.fetch?.shim ?? false;
|
|
680
|
+
const fetchOn = legacyShim || (caps.shim?.fetch ?? false);
|
|
681
|
+
const connectOn = caps.shim?.connect ?? false;
|
|
682
|
+
const fetch2 = caps.fetch ?? (fetchOn ? { shim: fetchOn, origins: [] } : void 0);
|
|
683
|
+
return {
|
|
684
|
+
...caps,
|
|
685
|
+
fetch: fetch2 === void 0 ? void 0 : { ...fetch2, shim: fetchOn },
|
|
686
|
+
...fetchOn || connectOn ? { shim: { fetch: fetchOn, connect: connectOn } } : {}
|
|
687
|
+
};
|
|
688
|
+
});
|
|
689
|
+
var ProviderBindingStatusSchema = z8.strictObject({
|
|
690
|
+
/** The manifest origin as declared (matches `FetchConnection.origin`). */
|
|
691
|
+
origin: z8.string(),
|
|
692
|
+
/** The bound provider reference. */
|
|
693
|
+
ref: ProviderRefSchema,
|
|
694
|
+
/**
|
|
695
|
+
* Whether an approval that granted this binding still matches the provider
|
|
696
|
+
* row's current identity — the {@link isProviderBindingEffective} rule
|
|
697
|
+
* (the one definition; the portal never re-derives the comparison). `false`
|
|
698
|
+
* means the provider was deleted or a sensitive edit stale-dated the stamp.
|
|
699
|
+
*/
|
|
700
|
+
effective: z8.boolean()
|
|
266
701
|
});
|
|
267
|
-
var AppManifestSchema =
|
|
702
|
+
var AppManifestSchema = z8.object({
|
|
268
703
|
/** App slug; matches `App.slug`. */
|
|
269
|
-
app:
|
|
704
|
+
app: z8.string().min(1),
|
|
270
705
|
visibility: VisibilitySchema,
|
|
271
|
-
capabilities: CapabilitiesSchema.default({ mcp: [], externalOrigins: [] })
|
|
706
|
+
capabilities: CapabilitiesSchema.default({ mcp: [], externalOrigins: [] }),
|
|
707
|
+
/**
|
|
708
|
+
* Effectiveness of every provider-bound origin in `capabilities`, computed
|
|
709
|
+
* server-side on the read (never stored). Absent when the manifest declares
|
|
710
|
+
* no provider binding — every payload without one keeps its exact shape.
|
|
711
|
+
*/
|
|
712
|
+
providerBindings: z8.array(ProviderBindingStatusSchema).optional()
|
|
272
713
|
});
|
|
273
714
|
|
|
274
715
|
// ../shared/src/approval.ts
|
|
275
|
-
import { z as
|
|
716
|
+
import { z as z9 } from "zod";
|
|
276
717
|
|
|
277
718
|
// ../shared/src/pricing.ts
|
|
278
719
|
var MODEL_PRICING = {
|
|
@@ -320,6 +761,9 @@ var MODEL_PRICING = {
|
|
|
320
761
|
"claude-opus-4-7": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
|
|
321
762
|
"claude-opus-4-6": { inputPerMTok: 5, outputPerMTok: 25, provider: "anthropic" },
|
|
322
763
|
"claude-sonnet-4-6": { inputPerMTok: 3, outputPerMTok: 15, provider: "anthropic" },
|
|
764
|
+
// Near-term churn: haiku-4-5's only two Foundry versions both stop serving
|
|
765
|
+
// 2026-10-19 (measured eastus2 2026-09-16). Not a cut — first-party serving
|
|
766
|
+
// is unaffected — but expect the Foundry deployments to need a successor.
|
|
323
767
|
"claude-haiku-4-5": {
|
|
324
768
|
inputPerMTok: 1,
|
|
325
769
|
outputPerMTok: 5,
|
|
@@ -393,13 +837,14 @@ var MODEL_PRICING = {
|
|
|
393
837
|
},
|
|
394
838
|
// Previous generations, kept curated so a deployed app's manifest keeps working.
|
|
395
839
|
// Rates re-verified 2026-09-14 and unchanged.
|
|
840
|
+
//
|
|
841
|
+
// `gpt-4o-mini`, `o3` and `o4-mini` were deliberately cut (2026-09-16) rather
|
|
842
|
+
// than retained on that principle: Azure AI Foundry's RP refuses any model
|
|
843
|
+
// whose *default* version is `Deprecating` (`ServiceModelDeprecating`), so
|
|
844
|
+
// they could never ride the one-flag Foundry path, and the current generation
|
|
845
|
+
// covers their price points first-party. A manifest still naming one gets the
|
|
846
|
+
// edge's `model_not_allowed` (no price configured) — the cut is the break.
|
|
396
847
|
"gpt-4o": { inputPerMTok: 2.5, outputPerMTok: 10, provider: "openai", structuredOutputs: true },
|
|
397
|
-
"gpt-4o-mini": {
|
|
398
|
-
inputPerMTok: 0.15,
|
|
399
|
-
outputPerMTok: 0.6,
|
|
400
|
-
provider: "openai",
|
|
401
|
-
structuredOutputs: true
|
|
402
|
-
},
|
|
403
848
|
"gpt-4.1": { inputPerMTok: 2, outputPerMTok: 8, provider: "openai", structuredOutputs: true },
|
|
404
849
|
"gpt-4.1-mini": {
|
|
405
850
|
inputPerMTok: 0.4,
|
|
@@ -412,28 +857,12 @@ var MODEL_PRICING = {
|
|
|
412
857
|
outputPerMTok: 0.4,
|
|
413
858
|
provider: "openai",
|
|
414
859
|
structuredOutputs: true
|
|
415
|
-
},
|
|
416
|
-
o3: {
|
|
417
|
-
inputPerMTok: 2,
|
|
418
|
-
outputPerMTok: 8,
|
|
419
|
-
provider: "openai",
|
|
420
|
-
reasoning: true,
|
|
421
|
-
minCompletionTokens: 25e3,
|
|
422
|
-
structuredOutputs: true
|
|
423
|
-
},
|
|
424
|
-
"o4-mini": {
|
|
425
|
-
inputPerMTok: 1.1,
|
|
426
|
-
outputPerMTok: 4.4,
|
|
427
|
-
provider: "openai",
|
|
428
|
-
reasoning: true,
|
|
429
|
-
minCompletionTokens: 25e3,
|
|
430
|
-
structuredOutputs: true
|
|
431
860
|
}
|
|
432
861
|
};
|
|
433
862
|
|
|
434
863
|
// ../shared/src/approval.ts
|
|
435
864
|
var RISK_LEVELS = ["low", "med", "high"];
|
|
436
|
-
var RiskSchema =
|
|
865
|
+
var RiskSchema = z9.enum(RISK_LEVELS);
|
|
437
866
|
var APPROVAL_STATUSES = [
|
|
438
867
|
"pending",
|
|
439
868
|
"approved",
|
|
@@ -441,151 +870,195 @@ var APPROVAL_STATUSES = [
|
|
|
441
870
|
"withdrawn",
|
|
442
871
|
"needs_changes"
|
|
443
872
|
];
|
|
444
|
-
var ApprovalStatusSchema =
|
|
445
|
-
var
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
873
|
+
var ApprovalStatusSchema = z9.enum(APPROVAL_STATUSES);
|
|
874
|
+
var ProviderStampSchema = z9.strictObject({
|
|
875
|
+
/** The env-unique reference the manifest binds (the catalogue/manifest key). */
|
|
876
|
+
ref: ProviderRefSchema,
|
|
877
|
+
/** Which partition the stamped row lives in — a ref may exist in both. */
|
|
878
|
+
env: EnvSchema,
|
|
879
|
+
/** The surrogate row id — delete+recreate mints a new one, dangling old stamps. */
|
|
880
|
+
providerId: z9.uuid(),
|
|
881
|
+
/** The row's revision at filing; a sensitive edit advances it. */
|
|
882
|
+
revision: z9.int().positive()
|
|
883
|
+
});
|
|
884
|
+
var DeltaSchema = z9.object({
|
|
885
|
+
path: z9.string(),
|
|
886
|
+
from: z9.union([z9.string(), z9.number(), z9.boolean()]).optional(),
|
|
887
|
+
to: z9.union([z9.string(), z9.number(), z9.boolean()]).optional(),
|
|
888
|
+
/**
|
|
889
|
+
* The provider stamps recorded **at filing** for a provider-bound origin
|
|
890
|
+
* delta — one per env-partitioned `connection_providers` row that
|
|
891
|
+
* existed under the bound ref when the request was filed (an
|
|
892
|
+
* approval records the provider's `ref` + `providerId` + `revision`, and
|
|
893
|
+
* env partitioning makes a ref's full identity the set of its rows).
|
|
894
|
+
*
|
|
895
|
+
* This is the payload half of the apply-time conflict: approving a request
|
|
896
|
+
* whose stamps no longer match the provider's current identity/revision
|
|
897
|
+
* approves nothing — an old pending approval cannot approve access to a
|
|
898
|
+
* newer configuration. Consumers read the rule itself from
|
|
899
|
+
* {@link isProviderBindingEffective}; none re-derives the comparison.
|
|
900
|
+
*/
|
|
901
|
+
providerStamps: z9.array(ProviderStampSchema).optional(),
|
|
902
|
+
/**
|
|
903
|
+
* The requesting app's visibility **at filing**: `true` when the app was
|
|
904
|
+
* `public`, `false` (stamped as not-public) otherwise. The
|
|
905
|
+
* warning — a public app's anonymous visitors can never connect a vendor
|
|
906
|
+
* account — is data on the request, so the queue card needs no extra fetch.
|
|
907
|
+
* The flag never auto-rejects.
|
|
908
|
+
*/
|
|
909
|
+
publicApp: z9.boolean().optional(),
|
|
910
|
+
/**
|
|
911
|
+
* This delta's own severity as classified at filing — what the request-level
|
|
912
|
+
* `risk` is the max of. Absent on requests filed before the field existed
|
|
913
|
+
* (deltas are stored as JSON, so nothing backfills them); renderers fall
|
|
914
|
+
* back to the aggregate rather than guessing.
|
|
915
|
+
*/
|
|
916
|
+
risk: RiskSchema.optional()
|
|
449
917
|
});
|
|
450
|
-
var PriorDecisionsSchema =
|
|
918
|
+
var PriorDecisionsSchema = z9.object({
|
|
451
919
|
/** Prior *decided* (non-pending) requests on this app — the history count. */
|
|
452
|
-
total:
|
|
920
|
+
total: z9.number().int().nonnegative(),
|
|
453
921
|
/** Prior denied requests touching an overlapping policy area (quiet signal). */
|
|
454
|
-
deniedSameArea:
|
|
922
|
+
deniedSameArea: z9.number().int().nonnegative(),
|
|
455
923
|
/** Prior denied requests sharing an exact delta path (loud signal; ⊆ area). */
|
|
456
|
-
deniedSameGrant:
|
|
924
|
+
deniedSameGrant: z9.number().int().nonnegative(),
|
|
457
925
|
/** The most recent decided request (any status), for the "last decision" line. */
|
|
458
|
-
last:
|
|
926
|
+
last: z9.object({
|
|
459
927
|
status: ApprovalStatusSchema,
|
|
460
|
-
note:
|
|
461
|
-
decidedBy:
|
|
462
|
-
decidedAt:
|
|
928
|
+
note: z9.string().nullable(),
|
|
929
|
+
decidedBy: z9.string().nullable(),
|
|
930
|
+
decidedAt: z9.string()
|
|
463
931
|
}).optional()
|
|
464
932
|
});
|
|
465
|
-
var ApprovalRequestSchema =
|
|
466
|
-
id:
|
|
467
|
-
appId:
|
|
933
|
+
var ApprovalRequestSchema = z9.object({
|
|
934
|
+
id: z9.string(),
|
|
935
|
+
appId: z9.string(),
|
|
468
936
|
/** Joined for the admin queue / banner; not a column. */
|
|
469
|
-
appSlug:
|
|
470
|
-
appDisplayName:
|
|
937
|
+
appSlug: z9.string().optional(),
|
|
938
|
+
appDisplayName: z9.string().optional(),
|
|
471
939
|
status: ApprovalStatusSchema,
|
|
472
940
|
risk: RiskSchema,
|
|
473
|
-
deltas:
|
|
474
|
-
baseSnapshot:
|
|
475
|
-
requestedBy:
|
|
476
|
-
reason:
|
|
477
|
-
decidedBy:
|
|
478
|
-
decisionNote:
|
|
479
|
-
createdAt:
|
|
480
|
-
decidedAt:
|
|
941
|
+
deltas: z9.array(DeltaSchema),
|
|
942
|
+
baseSnapshot: z9.unknown(),
|
|
943
|
+
requestedBy: z9.string(),
|
|
944
|
+
reason: z9.string().nullable().optional(),
|
|
945
|
+
decidedBy: z9.string().nullable().optional(),
|
|
946
|
+
decisionNote: z9.string().nullable().optional(),
|
|
947
|
+
createdAt: z9.string(),
|
|
948
|
+
decidedAt: z9.string().nullable().optional(),
|
|
481
949
|
/** Sibling-decision context for the admin queue (issue #26); read-side only. */
|
|
482
950
|
priorDecisions: PriorDecisionsSchema.optional()
|
|
483
951
|
});
|
|
484
|
-
var ManifestUpdateResultSchema =
|
|
952
|
+
var ManifestUpdateResultSchema = z9.object({
|
|
485
953
|
manifest: AppManifestSchema,
|
|
486
|
-
applied:
|
|
487
|
-
pending:
|
|
954
|
+
applied: z9.array(DeltaSchema),
|
|
955
|
+
pending: z9.string().nullable()
|
|
488
956
|
});
|
|
489
|
-
var VisibilityUpdateResultSchema =
|
|
957
|
+
var VisibilityUpdateResultSchema = z9.object({
|
|
490
958
|
app: AppSchema,
|
|
491
|
-
applied:
|
|
492
|
-
pending:
|
|
959
|
+
applied: z9.array(DeltaSchema),
|
|
960
|
+
pending: z9.string().nullable()
|
|
493
961
|
});
|
|
494
|
-
var ApprovalDecisionRequestSchema =
|
|
495
|
-
note:
|
|
962
|
+
var ApprovalDecisionRequestSchema = z9.object({
|
|
963
|
+
note: z9.string().max(2e3).optional()
|
|
496
964
|
});
|
|
497
965
|
var CURATED_LLM_MODELS = Object.keys(MODEL_PRICING);
|
|
498
966
|
|
|
499
967
|
// ../shared/src/health.ts
|
|
500
|
-
import { z as
|
|
501
|
-
var HealthStateSchema =
|
|
502
|
-
var HealthCheckSchema =
|
|
968
|
+
import { z as z10 } from "zod";
|
|
969
|
+
var HealthStateSchema = z10.enum(["ok", "degraded", "error"]);
|
|
970
|
+
var HealthCheckSchema = z10.object({
|
|
503
971
|
/** Stable identifier an alert rule can key on, e.g. `registry-projection`. */
|
|
504
|
-
name:
|
|
972
|
+
name: z10.string(),
|
|
505
973
|
status: HealthStateSchema,
|
|
506
974
|
/** Operator-facing one-liner: what is wrong, and for how long. */
|
|
507
|
-
detail:
|
|
975
|
+
detail: z10.string().optional(),
|
|
508
976
|
/** Wall-clock instant this check last succeeded. Report-only — never the
|
|
509
977
|
* basis of a staleness decision (see the monotonic note in the edge's
|
|
510
978
|
* registry projection). */
|
|
511
|
-
lastSuccessAt:
|
|
979
|
+
lastSuccessAt: z10.iso.datetime().optional(),
|
|
512
980
|
/** Numeric facts a dashboard or a log-based metric can key on. */
|
|
513
|
-
metrics:
|
|
981
|
+
metrics: z10.record(z10.string(), z10.number()).optional()
|
|
514
982
|
});
|
|
515
|
-
var HealthStatusSchema =
|
|
983
|
+
var HealthStatusSchema = z10.object({
|
|
516
984
|
/** The roll-up: the worst state across `checks` (see `worstHealthState`). */
|
|
517
985
|
status: HealthStateSchema,
|
|
518
|
-
service:
|
|
986
|
+
service: z10.string(),
|
|
519
987
|
/** Process uptime in seconds. */
|
|
520
|
-
uptime:
|
|
988
|
+
uptime: z10.number().nonnegative(),
|
|
521
989
|
/** Absent means the service reports liveness only (portal and egress today). */
|
|
522
|
-
checks:
|
|
990
|
+
checks: z10.array(HealthCheckSchema).optional()
|
|
523
991
|
});
|
|
524
992
|
|
|
525
993
|
// ../shared/src/api.ts
|
|
526
|
-
import { z as
|
|
527
|
-
var CreateAppRequestSchema =
|
|
994
|
+
import { z as z11 } from "zod";
|
|
995
|
+
var CreateAppRequestSchema = z11.object({
|
|
528
996
|
slug: AppSchema.shape.slug,
|
|
529
997
|
displayName: AppSchema.shape.displayName,
|
|
998
|
+
/** Optional one-line plain-text summary; rendered as a subtitle in the portal. */
|
|
999
|
+
description: AppSchema.shape.description,
|
|
530
1000
|
visibility: WritableVisibilitySchema.default({ mode: "internal" }),
|
|
531
1001
|
/** Optional per-app capability grant set at create time (architecture §6.3). */
|
|
532
1002
|
capabilities: CapabilitiesSchema.optional()
|
|
533
1003
|
});
|
|
534
|
-
var
|
|
1004
|
+
var UpdateAppRequestSchema = z11.object({
|
|
1005
|
+
description: z11.string().max(APP_DESCRIPTION_MAX).nullable().optional()
|
|
1006
|
+
});
|
|
1007
|
+
var SetManifestRequestSchema = z11.object({
|
|
535
1008
|
capabilities: CapabilitiesSchema,
|
|
536
1009
|
/** Justification carried onto an approval request for any elevated deltas. */
|
|
537
|
-
reason:
|
|
1010
|
+
reason: z11.string().max(2e3).optional()
|
|
538
1011
|
});
|
|
539
|
-
var SetVisibilityRequestSchema =
|
|
1012
|
+
var SetVisibilityRequestSchema = z11.object({
|
|
540
1013
|
visibility: WritableVisibilitySchema,
|
|
541
1014
|
/** Justification carried onto the approval request when going public. */
|
|
542
|
-
reason:
|
|
1015
|
+
reason: z11.string().max(2e3).optional()
|
|
543
1016
|
});
|
|
544
1017
|
var MIN_PASSWORD_LENGTH = 12;
|
|
545
|
-
var SetPasswordRequestSchema =
|
|
546
|
-
password:
|
|
1018
|
+
var SetPasswordRequestSchema = z11.object({
|
|
1019
|
+
password: z11.string().min(MIN_PASSWORD_LENGTH).optional()
|
|
547
1020
|
});
|
|
548
|
-
var PasswordCredentialResponseSchema =
|
|
1021
|
+
var PasswordCredentialResponseSchema = z11.object({
|
|
549
1022
|
/** The shared passphrase, in cleartext, for the owner to copy/share. */
|
|
550
|
-
password:
|
|
1023
|
+
password: z11.string().min(1),
|
|
551
1024
|
/** The app's public URL, prebuilt so the UI can offer a one-click copy. */
|
|
552
|
-
url:
|
|
1025
|
+
url: z11.url(),
|
|
553
1026
|
/** When the current password was set (ISO 8601). */
|
|
554
|
-
setAt:
|
|
555
|
-
});
|
|
556
|
-
var CspWarningSchema =
|
|
557
|
-
file:
|
|
558
|
-
origin:
|
|
559
|
-
hint:
|
|
560
|
-
});
|
|
561
|
-
var OriginGrantRequestSchema =
|
|
562
|
-
origin:
|
|
563
|
-
reason:
|
|
564
|
-
});
|
|
565
|
-
var CspViolationSchema =
|
|
566
|
-
appId:
|
|
567
|
-
appSlug:
|
|
568
|
-
directive:
|
|
569
|
-
blockedUri:
|
|
570
|
-
count:
|
|
571
|
-
lastSeen:
|
|
1027
|
+
setAt: z11.string()
|
|
1028
|
+
});
|
|
1029
|
+
var CspWarningSchema = z11.object({
|
|
1030
|
+
file: z11.string(),
|
|
1031
|
+
origin: z11.string(),
|
|
1032
|
+
hint: z11.string()
|
|
1033
|
+
});
|
|
1034
|
+
var OriginGrantRequestSchema = z11.object({
|
|
1035
|
+
origin: z11.url(),
|
|
1036
|
+
reason: z11.string().max(2e3).optional()
|
|
1037
|
+
});
|
|
1038
|
+
var CspViolationSchema = z11.object({
|
|
1039
|
+
appId: z11.string(),
|
|
1040
|
+
appSlug: z11.string().nullable(),
|
|
1041
|
+
directive: z11.string(),
|
|
1042
|
+
blockedUri: z11.string(),
|
|
1043
|
+
count: z11.number().int(),
|
|
1044
|
+
lastSeen: z11.string(),
|
|
572
1045
|
/**
|
|
573
1046
|
* The blocked origin is already permitted by the app's *current* manifest, so
|
|
574
1047
|
* this historical report is no longer actionable. Derived at read time
|
|
575
1048
|
* (directive-aware — only `connect-src`/`img-src` are widened by an
|
|
576
1049
|
* `externalOrigins` grant); the row itself is never deleted.
|
|
577
1050
|
*/
|
|
578
|
-
resolved:
|
|
1051
|
+
resolved: z11.boolean()
|
|
579
1052
|
});
|
|
580
|
-
var CspViolationsPageSchema =
|
|
581
|
-
violations:
|
|
1053
|
+
var CspViolationsPageSchema = z11.object({
|
|
1054
|
+
violations: z11.array(CspViolationSchema)
|
|
582
1055
|
});
|
|
583
|
-
var UploadVersionResponseSchema =
|
|
1056
|
+
var UploadVersionResponseSchema = z11.object({
|
|
584
1057
|
version: VersionSchema,
|
|
585
|
-
warnings:
|
|
1058
|
+
warnings: z11.array(CspWarningSchema)
|
|
586
1059
|
});
|
|
587
|
-
var RollbackRequestSchema =
|
|
588
|
-
toNumber:
|
|
1060
|
+
var RollbackRequestSchema = z11.object({
|
|
1061
|
+
toNumber: z11.int().positive().optional()
|
|
589
1062
|
});
|
|
590
1063
|
var API_ERROR_CODES = [
|
|
591
1064
|
"validation_failed",
|
|
@@ -596,6 +1069,14 @@ var API_ERROR_CODES = [
|
|
|
596
1069
|
/** Authenticated but not allowed — reserved for v1 RBAC. */
|
|
597
1070
|
"forbidden",
|
|
598
1071
|
"conflict",
|
|
1072
|
+
/**
|
|
1073
|
+
* Portal: a sensitive provider edit (or a deletion) arrived without its
|
|
1074
|
+
* invalidation acknowledgement. HTTP 409 like `conflict`, but
|
|
1075
|
+
* distinguishable: the SPA's
|
|
1076
|
+
* review panel opens on this code, and `details` carries the impact payload
|
|
1077
|
+
* (`ConfirmationRequiredDetailsSchema` in providers.ts) it renders.
|
|
1078
|
+
*/
|
|
1079
|
+
"confirmation_required",
|
|
599
1080
|
/** Gateway: requested model is not in the app's manifest allowlist (§6.3). */
|
|
600
1081
|
"model_not_allowed",
|
|
601
1082
|
/** Gateway: the app's daily token budget is exhausted (§6.1). */
|
|
@@ -619,22 +1100,28 @@ var API_ERROR_CODES = [
|
|
|
619
1100
|
"capability_unavailable",
|
|
620
1101
|
"internal"
|
|
621
1102
|
];
|
|
622
|
-
var ApiErrorCodeSchema =
|
|
623
|
-
var ApiErrorSchema =
|
|
624
|
-
error:
|
|
1103
|
+
var ApiErrorCodeSchema = z11.enum(API_ERROR_CODES);
|
|
1104
|
+
var ApiErrorSchema = z11.object({
|
|
1105
|
+
error: z11.object({
|
|
625
1106
|
code: ApiErrorCodeSchema,
|
|
626
|
-
message:
|
|
627
|
-
details:
|
|
1107
|
+
message: z11.string(),
|
|
1108
|
+
details: z11.unknown().optional()
|
|
628
1109
|
})
|
|
629
1110
|
});
|
|
630
1111
|
|
|
631
1112
|
// ../shared/src/auth.ts
|
|
632
|
-
import { z as
|
|
633
|
-
var MeResponseSchema =
|
|
634
|
-
user:
|
|
635
|
-
/**
|
|
636
|
-
|
|
637
|
-
|
|
1113
|
+
import { z as z12 } from "zod";
|
|
1114
|
+
var MeResponseSchema = z12.object({
|
|
1115
|
+
user: z12.object({
|
|
1116
|
+
/**
|
|
1117
|
+
* The principal id — Entra's `oid` claim (the directory object id), stable
|
|
1118
|
+
* for the life of the user object and identical across every app
|
|
1119
|
+
* registration in the tenant (ADR-0048): safe to key app data on, and the
|
|
1120
|
+
* user scope partitions on exactly this value. Opaque by design — never
|
|
1121
|
+
* render it; `displayName` and `email` are the half fit to display.
|
|
1122
|
+
*/
|
|
1123
|
+
id: z12.string(),
|
|
1124
|
+
displayName: z12.string(),
|
|
638
1125
|
/**
|
|
639
1126
|
* The captured address claim, or `null` when there isn't one — **nullable,
|
|
640
1127
|
* not optional, and the difference is the contract**. A missing key would
|
|
@@ -649,15 +1136,29 @@ var MeResponseSchema = z9.object({
|
|
|
649
1136
|
* to join on. Entra addresses are reassignable and B2B guests carry their
|
|
650
1137
|
* home-tenant address.
|
|
651
1138
|
*/
|
|
652
|
-
email:
|
|
1139
|
+
email: z12.string().nullable()
|
|
653
1140
|
})
|
|
654
1141
|
});
|
|
655
|
-
var PortalMeResponseSchema =
|
|
656
|
-
|
|
1142
|
+
var PortalMeResponseSchema = z12.object({
|
|
1143
|
+
/**
|
|
1144
|
+
* The canonical principal id — Entra's `oid` claim from the access token
|
|
1145
|
+
* (ADR-0048). This is what `App.ownerId` stores and what ownership compares;
|
|
1146
|
+
* `sub` below is the display/audit half, never a join key.
|
|
1147
|
+
*
|
|
1148
|
+
* **Optional, not required**, for the same cross-version reason as the
|
|
1149
|
+
* defaulted fields below: this schema parses `PortalClient.me()` in the
|
|
1150
|
+
* *published* CLI, which routinely talks to portals older than itself, and
|
|
1151
|
+
* portals are customer-deployed and version independently (ADR-0028). An
|
|
1152
|
+
* older portal has no `oid` to echo — pre-ADR-0048 it has no such concept —
|
|
1153
|
+
* and nothing in the CLI keys on it. A current portal always sends it (the
|
|
1154
|
+
* dev-token actor gets a fixed synthetic value).
|
|
1155
|
+
*/
|
|
1156
|
+
oid: z12.string().optional(),
|
|
1157
|
+
sub: z12.string(),
|
|
657
1158
|
/** How the actor was established: `oidc` or `dev-token`. */
|
|
658
|
-
via:
|
|
659
|
-
name:
|
|
660
|
-
email:
|
|
1159
|
+
via: z12.string(),
|
|
1160
|
+
name: z12.string().optional(),
|
|
1161
|
+
email: z12.string().optional(),
|
|
661
1162
|
/**
|
|
662
1163
|
* Whether the actor holds the `platform-admin` role. Computed server-side from
|
|
663
1164
|
* the actor's group/role claim — the raw ids never cross to the browser. Drives
|
|
@@ -669,7 +1170,7 @@ var PortalMeResponseSchema = z9.object({
|
|
|
669
1170
|
* polarity is the cautious one — a portal that never sent the field is not
|
|
670
1171
|
* telling us the caller is an admin.
|
|
671
1172
|
*/
|
|
672
|
-
isAdmin:
|
|
1173
|
+
isAdmin: z12.boolean().default(false),
|
|
673
1174
|
/**
|
|
674
1175
|
* Whether this caller may run a tenant-wide group search (ADR-0040 decision
|
|
675
1176
|
* 11). Computed server-side from the deployment's `PORTAL_DIRECTORY_SEARCH`
|
|
@@ -695,7 +1196,7 @@ var PortalMeResponseSchema = z9.object({
|
|
|
695
1196
|
* describes the portal actually being talked to. Defaulting to `false` would
|
|
696
1197
|
* instead tell a client that search is unavailable where it works.
|
|
697
1198
|
*/
|
|
698
|
-
canSearchDirectory:
|
|
1199
|
+
canSearchDirectory: z12.boolean().default(true),
|
|
699
1200
|
/**
|
|
700
1201
|
* Why search was refused, when it was — **present only when
|
|
701
1202
|
* {@link canSearchDirectory} is false**, and never `everyone`, which by
|
|
@@ -713,15 +1214,15 @@ var PortalMeResponseSchema = z9.object({
|
|
|
713
1214
|
* infer from the 403 they would get by asking. The admin group id still never
|
|
714
1215
|
* crosses.
|
|
715
1216
|
*/
|
|
716
|
-
searchRestriction:
|
|
1217
|
+
searchRestriction: z12.enum(["admins", "none"]).optional()
|
|
717
1218
|
});
|
|
718
|
-
var AuthConfigResponseSchema =
|
|
719
|
-
issuer:
|
|
720
|
-
cliClientId:
|
|
1219
|
+
var AuthConfigResponseSchema = z12.object({
|
|
1220
|
+
issuer: z12.url(),
|
|
1221
|
+
cliClientId: z12.string().min(1),
|
|
721
1222
|
/** Public client the portal SPA uses for code+PKCE in the browser. */
|
|
722
|
-
webClientId:
|
|
1223
|
+
webClientId: z12.string().min(1).optional(),
|
|
723
1224
|
/** Expected token audience — part of what the CLI binds cached tokens to. */
|
|
724
|
-
audience:
|
|
1225
|
+
audience: z12.string().min(1).optional(),
|
|
725
1226
|
/**
|
|
726
1227
|
* Whether this deployment permits `public` (anonymous) apps. Drives the SPA's
|
|
727
1228
|
* visibility UI — it hides the public option when false. Absent = forbidden
|
|
@@ -729,9 +1230,9 @@ var AuthConfigResponseSchema = z9.object({
|
|
|
729
1230
|
* opt-in). Server-side enforcement is independent of this hint (portal routes
|
|
730
1231
|
* + edge serving).
|
|
731
1232
|
*/
|
|
732
|
-
allowPublicApps:
|
|
1233
|
+
allowPublicApps: z12.boolean().optional(),
|
|
733
1234
|
/** Whether this deployment permits `password` (shared-passphrase) apps. Same shape as {@link allowPublicApps}. */
|
|
734
|
-
allowPasswordApps:
|
|
1235
|
+
allowPasswordApps: z12.boolean().optional()
|
|
735
1236
|
});
|
|
736
1237
|
function portalApiScope(audience) {
|
|
737
1238
|
if (!audience || audience.startsWith("urn:")) return null;
|
|
@@ -740,27 +1241,27 @@ function portalApiScope(audience) {
|
|
|
740
1241
|
}
|
|
741
1242
|
|
|
742
1243
|
// ../shared/src/deployment.ts
|
|
743
|
-
import { z as
|
|
744
|
-
var DeploymentConfigResponseSchema =
|
|
1244
|
+
import { z as z13 } from "zod";
|
|
1245
|
+
var DeploymentConfigResponseSchema = z13.object({
|
|
745
1246
|
/**
|
|
746
1247
|
* Scheme + host + (non-default) port where apps are served, as reachable by
|
|
747
1248
|
* this browser (architecture §4.1). The app slug is prepended as a subdomain:
|
|
748
1249
|
* `https://<slug>.<host>`.
|
|
749
1250
|
*/
|
|
750
|
-
appPublicBase:
|
|
1251
|
+
appPublicBase: z13.url(),
|
|
751
1252
|
/**
|
|
752
1253
|
* Base of the opt-in dev gateway (dev-mode design §3), absent when it is not
|
|
753
1254
|
* deployed. Unlike {@link appPublicBase} the slug goes in the *path*, not the
|
|
754
1255
|
* host. Absent ⇒ dev mode is unavailable here, so the UI says so instead of
|
|
755
1256
|
* printing an unreachable host.
|
|
756
1257
|
*/
|
|
757
|
-
devApiBase:
|
|
1258
|
+
devApiBase: z13.url().optional(),
|
|
758
1259
|
/**
|
|
759
1260
|
* Month-to-date platform spend ceiling (USD) for the admin budget watch line.
|
|
760
1261
|
* Display-only — the gateway is the choke point, so the rollup is exact, but
|
|
761
1262
|
* nothing enforces this. Absent ⇒ no ceiling shown.
|
|
762
1263
|
*/
|
|
763
|
-
platformMonthlyUsdCap:
|
|
1264
|
+
platformMonthlyUsdCap: z13.number().positive().optional(),
|
|
764
1265
|
/**
|
|
765
1266
|
* Deploy bundle size caps in megabytes (`DEPLOY_MAX_FILE_MB` /
|
|
766
1267
|
* `DEPLOY_MAX_BUNDLE_MB` on the portal) — `deployMaxFileMb` is per file,
|
|
@@ -770,13 +1271,26 @@ var DeploymentConfigResponseSchema = z10.object({
|
|
|
770
1271
|
* substitute a default, because printing the wrong cap to an agent sends it
|
|
771
1272
|
* chasing a rejection it can't see the cause of.
|
|
772
1273
|
*/
|
|
773
|
-
deployMaxFileMb:
|
|
774
|
-
deployMaxBundleMb:
|
|
1274
|
+
deployMaxFileMb: z13.number().positive().optional(),
|
|
1275
|
+
deployMaxBundleMb: z13.number().positive().optional(),
|
|
1276
|
+
/**
|
|
1277
|
+
* Repo-backed deploys (ADR-0051). `audience` is the exact `aud` the CI route
|
|
1278
|
+
* accepts, so `helix deploy` in GitHub Actions requests its token for this
|
|
1279
|
+
* value rather than for whatever URL it was pointed at. `serverUrl` is the
|
|
1280
|
+
* GitHub web origin for commit and run links. Optional only to tolerate a
|
|
1281
|
+
* portal that predates the feature; when `enabled` is false the CI route and
|
|
1282
|
+
* the binding routes answer 404 and clients hide the surface.
|
|
1283
|
+
*/
|
|
1284
|
+
repoDeploy: z13.object({
|
|
1285
|
+
enabled: z13.boolean(),
|
|
1286
|
+
audience: z13.url(),
|
|
1287
|
+
serverUrl: z13.url()
|
|
1288
|
+
}).optional()
|
|
775
1289
|
});
|
|
776
1290
|
|
|
777
1291
|
// ../shared/src/catalogue.ts
|
|
778
|
-
import { z as
|
|
779
|
-
var CapabilityCatalogueSchema =
|
|
1292
|
+
import { z as z14 } from "zod";
|
|
1293
|
+
var CapabilityCatalogueSchema = z14.object({
|
|
780
1294
|
/**
|
|
781
1295
|
* Visibility modes an app may declare here. `internal` is always present
|
|
782
1296
|
* (SSO, any directory principal — the baseline). `group` is present only when
|
|
@@ -785,24 +1299,28 @@ var CapabilityCatalogueSchema = z11.object({
|
|
|
785
1299
|
* present only when the operator policy permits them
|
|
786
1300
|
* (`PORTAL_ALLOW_*_APPS`). A mode absent from this list is not requestable.
|
|
787
1301
|
*/
|
|
788
|
-
visibility:
|
|
789
|
-
modes:
|
|
1302
|
+
visibility: z14.object({
|
|
1303
|
+
modes: z14.array(VisibilityModeSchema).min(1)
|
|
790
1304
|
}),
|
|
791
1305
|
/**
|
|
792
|
-
* LLM: the **servable** model ids, not merely the curated catalog.
|
|
793
|
-
*
|
|
794
|
-
* `
|
|
795
|
-
*
|
|
796
|
-
*
|
|
797
|
-
*
|
|
1306
|
+
* LLM: the **servable** model ids, not merely the curated catalog. The set is
|
|
1307
|
+
* the operator's declaration when one exists (`PORTAL_LLM_MODEL_ALLOWLIST`,
|
|
1308
|
+
* minus `PORTAL_LLM_MODEL_BLOCKLIST` — ADR-0047; on a `deployFoundry` install
|
|
1309
|
+
* the Bicep derives it from `foundryModels`, so it reads exactly what was
|
|
1310
|
+
* deployed), and otherwise the ADR-0036 v1 heuristic: a model is servable
|
|
1311
|
+
* when its upstream family has a seeded `platform` secret, so a
|
|
1312
|
+
* curated-but-unseeded model (e.g. a `gpt-*` model whose `openai` platform
|
|
1313
|
+
* key was never added) is omitted and an agent that reads this list never
|
|
1314
|
+
* designs an app that 502s at call time. Every id in this list is also
|
|
1315
|
+
* curated, so requesting one is baseline (auto-approve).
|
|
798
1316
|
*
|
|
799
1317
|
* `baselineDollarsPerDay` is the spend cap at/under which an LLM grant
|
|
800
1318
|
* auto-approves; above it queues. From `@azx-pbc/shared/approval.ts`, so the
|
|
801
1319
|
* catalogue and the classifier cannot drift.
|
|
802
1320
|
*/
|
|
803
|
-
llm:
|
|
804
|
-
models:
|
|
805
|
-
baselineDollarsPerDay:
|
|
1321
|
+
llm: z14.object({
|
|
1322
|
+
models: z14.array(z14.string().min(1)),
|
|
1323
|
+
baselineDollarsPerDay: z14.number().positive()
|
|
806
1324
|
}),
|
|
807
1325
|
/**
|
|
808
1326
|
* App-data: whether the three scopes are provisioned here, and the daily
|
|
@@ -811,10 +1329,10 @@ var CapabilityCatalogueSchema = z11.object({
|
|
|
811
1329
|
* a future deployment-level toggle, present before it is needed. The
|
|
812
1330
|
* baselines come from `@azx-pbc/shared/approval.ts`.
|
|
813
1331
|
*/
|
|
814
|
-
data:
|
|
815
|
-
provisioned:
|
|
816
|
-
baselineWritesPerDay:
|
|
817
|
-
baselineBytesPerDay:
|
|
1332
|
+
data: z14.object({
|
|
1333
|
+
provisioned: z14.boolean(),
|
|
1334
|
+
baselineWritesPerDay: z14.number().positive(),
|
|
1335
|
+
baselineBytesPerDay: z14.number().positive()
|
|
818
1336
|
}),
|
|
819
1337
|
/**
|
|
820
1338
|
* The fetch-proxy (`/_api/fetch`) and direct-CSP surfaces.
|
|
@@ -826,42 +1344,51 @@ var CapabilityCatalogueSchema = z11.object({
|
|
|
826
1344
|
* values, and without the origin each one fronts: a global secret has no
|
|
827
1345
|
* stored origin (the origin is declared per-app in each manifest), so listing
|
|
828
1346
|
* one here would be a guess. An agent learns "this connection exists and is
|
|
829
|
-
* referenceable" and supplies the origin itself. `
|
|
830
|
-
*
|
|
1347
|
+
* referenceable" and supplies the origin itself. `providers` is the configured
|
|
1348
|
+
* OAuth connection providers an app may bind from
|
|
1349
|
+
* `capabilities.fetch.origins[].provider` — metadata-only
|
|
1350
|
+
* {@link CatalogueProviderSchema} entries (ref, kind, display name, API
|
|
1351
|
+
* destinations, env), never a credential field, served to every
|
|
1352
|
+
* authenticated principal exactly like `connections`. An origin bound to a
|
|
1353
|
+
* provider must be one of that provider's `apiOrigins` in the applicable
|
|
1354
|
+
* environment, so an app author picks both from this list.
|
|
1355
|
+
* `baselineRequestsPerDay` is the proxied-request budget at/under which a
|
|
1356
|
+
* fetch grant auto-approves.
|
|
831
1357
|
*/
|
|
832
|
-
fetch:
|
|
833
|
-
externalOriginsPermitted:
|
|
834
|
-
connections:
|
|
835
|
-
|
|
1358
|
+
fetch: z14.object({
|
|
1359
|
+
externalOriginsPermitted: z14.boolean(),
|
|
1360
|
+
connections: z14.array(z14.object({ name: z14.string().min(1) })),
|
|
1361
|
+
providers: z14.array(CatalogueProviderSchema),
|
|
1362
|
+
baselineRequestsPerDay: z14.number().positive()
|
|
836
1363
|
}),
|
|
837
1364
|
/**
|
|
838
1365
|
* MCP is carried in the manifest but **not enforced** — no transport exists
|
|
839
1366
|
* today. Surfaced as `false` so an agent does not assume a working `mcp`
|
|
840
1367
|
* grant; declaring any server queues for approval regardless.
|
|
841
1368
|
*/
|
|
842
|
-
mcp:
|
|
843
|
-
enforced:
|
|
1369
|
+
mcp: z14.object({
|
|
1370
|
+
enforced: z14.boolean()
|
|
844
1371
|
}),
|
|
845
1372
|
/**
|
|
846
1373
|
* Offline (ADR-0035): whether the platform service-worker grant is available,
|
|
847
1374
|
* and the rule a `scope` must satisfy (not the domain root, not a `/_…`
|
|
848
1375
|
* reserved path). Any grant or rescope queues for approval.
|
|
849
1376
|
*/
|
|
850
|
-
offline:
|
|
851
|
-
available:
|
|
1377
|
+
offline: z14.object({
|
|
1378
|
+
available: z14.boolean(),
|
|
852
1379
|
/** Human-facing summary of the scope rule; not machine-parsed. */
|
|
853
|
-
scopeRule:
|
|
1380
|
+
scopeRule: z14.string().min(1)
|
|
854
1381
|
}),
|
|
855
1382
|
/** Deploy bundle size caps in megabytes, as enforced on upload. */
|
|
856
|
-
deploy:
|
|
857
|
-
maxFileMb:
|
|
858
|
-
maxBundleMb:
|
|
1383
|
+
deploy: z14.object({
|
|
1384
|
+
maxFileMb: z14.number().positive(),
|
|
1385
|
+
maxBundleMb: z14.number().positive()
|
|
859
1386
|
}),
|
|
860
1387
|
/**
|
|
861
1388
|
* The opt-in dev-gateway base, absent when it is not deployed here (same
|
|
862
1389
|
* convention as `DeploymentConfigResponseSchema.devApiBase`).
|
|
863
1390
|
*/
|
|
864
|
-
devApiBase:
|
|
1391
|
+
devApiBase: z14.url().optional(),
|
|
865
1392
|
/**
|
|
866
1393
|
* The approval half of the catalogue (decision 3). The four baselines mirror
|
|
867
1394
|
* `@azx-pbc/shared/approval.ts` exactly, and `elevationTriggers` enumerates
|
|
@@ -869,15 +1396,15 @@ var CapabilityCatalogueSchema = z11.object({
|
|
|
869
1396
|
* thing an agent reads to design an app that ships immediately. A trigger
|
|
870
1397
|
* absent from this list never queues.
|
|
871
1398
|
*/
|
|
872
|
-
approval:
|
|
873
|
-
baselines:
|
|
874
|
-
dollarsPerDay:
|
|
875
|
-
writesPerDay:
|
|
876
|
-
bytesPerDay:
|
|
877
|
-
fetchRequestsPerDay:
|
|
1399
|
+
approval: z14.object({
|
|
1400
|
+
baselines: z14.object({
|
|
1401
|
+
dollarsPerDay: z14.number().positive(),
|
|
1402
|
+
writesPerDay: z14.number().positive(),
|
|
1403
|
+
bytesPerDay: z14.number().positive(),
|
|
1404
|
+
fetchRequestsPerDay: z14.number().positive()
|
|
878
1405
|
}),
|
|
879
|
-
elevationTriggers:
|
|
880
|
-
|
|
1406
|
+
elevationTriggers: z14.array(
|
|
1407
|
+
z14.enum([
|
|
881
1408
|
/** Any `externalOrigins` entry — widens CSP for a direct browser call. */
|
|
882
1409
|
"externalOrigins",
|
|
883
1410
|
/** Any `fetch.origins` entry — a proxied outbound origin. */
|
|
@@ -908,16 +1435,16 @@ var SCRYPT_PARAMS = {
|
|
|
908
1435
|
};
|
|
909
1436
|
|
|
910
1437
|
// ../shared/src/llm.ts
|
|
911
|
-
import { z as
|
|
912
|
-
var LlmMessageSchema =
|
|
913
|
-
role:
|
|
914
|
-
content:
|
|
1438
|
+
import { z as z15 } from "zod";
|
|
1439
|
+
var LlmMessageSchema = z15.object({
|
|
1440
|
+
role: z15.enum(["user", "assistant"]),
|
|
1441
|
+
content: z15.string().min(1)
|
|
915
1442
|
});
|
|
916
|
-
var LlmUsageSchema =
|
|
917
|
-
inputTokens:
|
|
918
|
-
outputTokens:
|
|
919
|
-
cacheReadInputTokens:
|
|
920
|
-
cacheCreationInputTokens:
|
|
1443
|
+
var LlmUsageSchema = z15.object({
|
|
1444
|
+
inputTokens: z15.int().nonnegative(),
|
|
1445
|
+
outputTokens: z15.int().nonnegative(),
|
|
1446
|
+
cacheReadInputTokens: z15.int().nonnegative().default(0),
|
|
1447
|
+
cacheCreationInputTokens: z15.int().nonnegative().default(0)
|
|
921
1448
|
});
|
|
922
1449
|
var MAX_SCHEMA_CHARS = 32768;
|
|
923
1450
|
var MAX_SCHEMA_DEPTH = 12;
|
|
@@ -940,11 +1467,11 @@ function withinSchemaBudget(schema) {
|
|
|
940
1467
|
}
|
|
941
1468
|
return serialized.length <= MAX_SCHEMA_CHARS && schemaDepth(schema) <= MAX_SCHEMA_DEPTH;
|
|
942
1469
|
}
|
|
943
|
-
var LlmResponseFormatSchema =
|
|
944
|
-
type:
|
|
1470
|
+
var LlmResponseFormatSchema = z15.object({
|
|
1471
|
+
type: z15.literal("json_schema"),
|
|
945
1472
|
/** Schema name. OpenAI requires one; defaulted at translation when omitted. */
|
|
946
|
-
name:
|
|
947
|
-
schema:
|
|
1473
|
+
name: z15.string().regex(/^[A-Za-z0-9_-]{1,64}$/, "must be 1-64 chars of [A-Za-z0-9_-]").optional(),
|
|
1474
|
+
schema: z15.record(z15.string(), z15.unknown()).refine((s) => s.type === "object", 'schema root must be `{"type":"object"}`').refine(
|
|
948
1475
|
withinSchemaBudget,
|
|
949
1476
|
`schema must serialize to <= ${MAX_SCHEMA_CHARS} characters and nest <= ${MAX_SCHEMA_DEPTH} levels`
|
|
950
1477
|
)
|
|
@@ -954,18 +1481,18 @@ var LlmResponseFormatSchema = z12.object({
|
|
|
954
1481
|
// on `gpt-*` but not `claude-*`, which is exactly the provider leak this seam
|
|
955
1482
|
// exists to prevent. The platform always enforces; see ADR-0034.
|
|
956
1483
|
});
|
|
957
|
-
var LlmChatRequestSchema =
|
|
958
|
-
model:
|
|
959
|
-
messages:
|
|
1484
|
+
var LlmChatRequestSchema = z15.object({
|
|
1485
|
+
model: z15.string().min(1),
|
|
1486
|
+
messages: z15.array(LlmMessageSchema).min(1),
|
|
960
1487
|
/** Optional system prompt; maps to the vendor's system channel. */
|
|
961
|
-
system:
|
|
962
|
-
maxTokens:
|
|
1488
|
+
system: z15.string().optional(),
|
|
1489
|
+
maxTokens: z15.int().positive().max(128e3).optional(),
|
|
963
1490
|
/** Sampling temperature; forwarded as-is (the vendor validates its own range). */
|
|
964
|
-
temperature:
|
|
1491
|
+
temperature: z15.number().optional(),
|
|
965
1492
|
/** Nucleus sampling; forwarded as-is. */
|
|
966
|
-
topP:
|
|
1493
|
+
topP: z15.number().optional(),
|
|
967
1494
|
/** Stop sequences; normalized to a list at the boundary. */
|
|
968
|
-
stop:
|
|
1495
|
+
stop: z15.array(z15.string()).optional(),
|
|
969
1496
|
/**
|
|
970
1497
|
* Constrain the completion to a JSON schema (ADR-0034). Refused up front when
|
|
971
1498
|
* the requested model can't enforce it (`ModelPrice.structuredOutputs`). The
|
|
@@ -974,70 +1501,70 @@ var LlmChatRequestSchema = z12.object({
|
|
|
974
1501
|
*/
|
|
975
1502
|
responseFormat: LlmResponseFormatSchema.optional(),
|
|
976
1503
|
/** SSE streaming (default) vs a single JSON body. */
|
|
977
|
-
stream:
|
|
1504
|
+
stream: z15.boolean().default(true)
|
|
978
1505
|
});
|
|
979
|
-
var LlmChatResponseSchema =
|
|
980
|
-
model:
|
|
981
|
-
content:
|
|
982
|
-
stopReason:
|
|
1506
|
+
var LlmChatResponseSchema = z15.object({
|
|
1507
|
+
model: z15.string(),
|
|
1508
|
+
content: z15.string(),
|
|
1509
|
+
stopReason: z15.string(),
|
|
983
1510
|
usage: LlmUsageSchema
|
|
984
1511
|
});
|
|
985
|
-
var LlmStreamDeltaSchema =
|
|
986
|
-
var LlmStreamDoneSchema =
|
|
987
|
-
stopReason:
|
|
1512
|
+
var LlmStreamDeltaSchema = z15.object({ text: z15.string() });
|
|
1513
|
+
var LlmStreamDoneSchema = z15.object({
|
|
1514
|
+
stopReason: z15.string(),
|
|
988
1515
|
usage: LlmUsageSchema
|
|
989
1516
|
});
|
|
990
|
-
var LlmStreamErrorSchema =
|
|
991
|
-
code:
|
|
992
|
-
message:
|
|
1517
|
+
var LlmStreamErrorSchema = z15.object({
|
|
1518
|
+
code: z15.string(),
|
|
1519
|
+
message: z15.string()
|
|
993
1520
|
});
|
|
994
1521
|
|
|
995
1522
|
// ../shared/src/llmOpenai.ts
|
|
996
|
-
import { z as
|
|
997
|
-
var OpenAiRoleSchema =
|
|
998
|
-
var OpenAiMessageSchema =
|
|
1523
|
+
import { z as z16 } from "zod";
|
|
1524
|
+
var OpenAiRoleSchema = z16.enum(["system", "developer", "user", "assistant", "tool"]);
|
|
1525
|
+
var OpenAiMessageSchema = z16.object({
|
|
999
1526
|
role: OpenAiRoleSchema,
|
|
1000
|
-
content:
|
|
1527
|
+
content: z16.union([z16.string(), z16.array(z16.unknown())]).nullish(),
|
|
1001
1528
|
/** Present only on assistant tool-call turns — parsed to reject, never honored. */
|
|
1002
|
-
tool_calls:
|
|
1003
|
-
tool_call_id:
|
|
1004
|
-
name:
|
|
1005
|
-
});
|
|
1006
|
-
var OpenAiStreamOptionsSchema =
|
|
1007
|
-
include_usage:
|
|
1008
|
-
});
|
|
1009
|
-
var OpenAiResponseFormatSchema =
|
|
1010
|
-
|
|
1011
|
-
type:
|
|
1012
|
-
json_schema:
|
|
1013
|
-
name:
|
|
1014
|
-
schema:
|
|
1015
|
-
strict:
|
|
1529
|
+
tool_calls: z16.array(z16.unknown()).optional(),
|
|
1530
|
+
tool_call_id: z16.string().optional(),
|
|
1531
|
+
name: z16.string().optional()
|
|
1532
|
+
});
|
|
1533
|
+
var OpenAiStreamOptionsSchema = z16.object({
|
|
1534
|
+
include_usage: z16.boolean().optional()
|
|
1535
|
+
});
|
|
1536
|
+
var OpenAiResponseFormatSchema = z16.discriminatedUnion("type", [
|
|
1537
|
+
z16.object({
|
|
1538
|
+
type: z16.literal("json_schema"),
|
|
1539
|
+
json_schema: z16.object({
|
|
1540
|
+
name: z16.string().optional(),
|
|
1541
|
+
schema: z16.record(z16.string(), z16.unknown()),
|
|
1542
|
+
strict: z16.boolean().nullish(),
|
|
1016
1543
|
/**
|
|
1017
1544
|
* Declared so the codec can reject it rather than zod silently stripping it.
|
|
1018
1545
|
* It is honorable on the OpenAI path but has no Anthropic equivalent, so
|
|
1019
1546
|
* forwarding it would make behaviour depend on the backing vendor — the same
|
|
1020
1547
|
* provider leak that got `json_object` rejected (ADR-0034).
|
|
1021
1548
|
*/
|
|
1022
|
-
description:
|
|
1549
|
+
description: z16.string().optional()
|
|
1023
1550
|
})
|
|
1024
1551
|
}),
|
|
1025
|
-
|
|
1026
|
-
|
|
1552
|
+
z16.object({ type: z16.literal("json_object") }),
|
|
1553
|
+
z16.object({ type: z16.literal("text") })
|
|
1027
1554
|
]);
|
|
1028
|
-
var OpenAiChatCompletionRequestSchema =
|
|
1029
|
-
model:
|
|
1030
|
-
messages:
|
|
1031
|
-
max_tokens:
|
|
1032
|
-
max_completion_tokens:
|
|
1033
|
-
temperature:
|
|
1034
|
-
top_p:
|
|
1035
|
-
stop:
|
|
1036
|
-
stream:
|
|
1555
|
+
var OpenAiChatCompletionRequestSchema = z16.object({
|
|
1556
|
+
model: z16.string().min(1),
|
|
1557
|
+
messages: z16.array(OpenAiMessageSchema).min(1),
|
|
1558
|
+
max_tokens: z16.int().positive().max(128e3).optional(),
|
|
1559
|
+
max_completion_tokens: z16.int().positive().max(128e3).optional(),
|
|
1560
|
+
temperature: z16.number().optional(),
|
|
1561
|
+
top_p: z16.number().optional(),
|
|
1562
|
+
stop: z16.union([z16.string(), z16.array(z16.string())]).optional(),
|
|
1563
|
+
stream: z16.boolean().optional(),
|
|
1037
1564
|
stream_options: OpenAiStreamOptionsSchema.optional(),
|
|
1038
1565
|
/** Declared so the codec can reject tool use in v1 (not supported). */
|
|
1039
|
-
tools:
|
|
1040
|
-
tool_choice:
|
|
1566
|
+
tools: z16.array(z16.unknown()).optional(),
|
|
1567
|
+
tool_choice: z16.unknown().optional(),
|
|
1041
1568
|
/**
|
|
1042
1569
|
* Structured output (ADR-0034). Parsed, not rejected — see the union above.
|
|
1043
1570
|
* `.nullish()` because clients and proxies that serialize every field send
|
|
@@ -1048,71 +1575,71 @@ var OpenAiChatCompletionRequestSchema = z13.object({
|
|
|
1048
1575
|
// Behaviour-changing params the platform does not honor in v1. Declared (not
|
|
1049
1576
|
// stripped) so the codec can reject them with a 400 rather than silently drop
|
|
1050
1577
|
// them — the same "reject, never silently drop" contract as `tools`.
|
|
1051
|
-
n:
|
|
1052
|
-
seed:
|
|
1053
|
-
logit_bias:
|
|
1054
|
-
presence_penalty:
|
|
1055
|
-
frequency_penalty:
|
|
1056
|
-
logprobs:
|
|
1057
|
-
top_logprobs:
|
|
1058
|
-
});
|
|
1059
|
-
var OpenAiUsageSchema =
|
|
1060
|
-
prompt_tokens:
|
|
1061
|
-
completion_tokens:
|
|
1062
|
-
total_tokens:
|
|
1063
|
-
});
|
|
1064
|
-
var OpenAiChatCompletionResponseSchema =
|
|
1065
|
-
id:
|
|
1066
|
-
object:
|
|
1067
|
-
created:
|
|
1068
|
-
model:
|
|
1069
|
-
choices:
|
|
1070
|
-
|
|
1071
|
-
index:
|
|
1072
|
-
message:
|
|
1073
|
-
role:
|
|
1074
|
-
content:
|
|
1578
|
+
n: z16.unknown().optional(),
|
|
1579
|
+
seed: z16.unknown().optional(),
|
|
1580
|
+
logit_bias: z16.unknown().optional(),
|
|
1581
|
+
presence_penalty: z16.unknown().optional(),
|
|
1582
|
+
frequency_penalty: z16.unknown().optional(),
|
|
1583
|
+
logprobs: z16.unknown().optional(),
|
|
1584
|
+
top_logprobs: z16.unknown().optional()
|
|
1585
|
+
});
|
|
1586
|
+
var OpenAiUsageSchema = z16.object({
|
|
1587
|
+
prompt_tokens: z16.int().nonnegative(),
|
|
1588
|
+
completion_tokens: z16.int().nonnegative(),
|
|
1589
|
+
total_tokens: z16.int().nonnegative()
|
|
1590
|
+
});
|
|
1591
|
+
var OpenAiChatCompletionResponseSchema = z16.object({
|
|
1592
|
+
id: z16.string(),
|
|
1593
|
+
object: z16.literal("chat.completion"),
|
|
1594
|
+
created: z16.int(),
|
|
1595
|
+
model: z16.string(),
|
|
1596
|
+
choices: z16.array(
|
|
1597
|
+
z16.object({
|
|
1598
|
+
index: z16.int(),
|
|
1599
|
+
message: z16.object({
|
|
1600
|
+
role: z16.literal("assistant"),
|
|
1601
|
+
content: z16.string()
|
|
1075
1602
|
}),
|
|
1076
|
-
finish_reason:
|
|
1603
|
+
finish_reason: z16.string()
|
|
1077
1604
|
})
|
|
1078
1605
|
),
|
|
1079
1606
|
usage: OpenAiUsageSchema
|
|
1080
1607
|
});
|
|
1081
|
-
var OpenAiChatCompletionChunkSchema =
|
|
1082
|
-
id:
|
|
1083
|
-
object:
|
|
1084
|
-
created:
|
|
1085
|
-
model:
|
|
1086
|
-
choices:
|
|
1087
|
-
|
|
1088
|
-
index:
|
|
1089
|
-
delta:
|
|
1090
|
-
role:
|
|
1091
|
-
content:
|
|
1608
|
+
var OpenAiChatCompletionChunkSchema = z16.object({
|
|
1609
|
+
id: z16.string(),
|
|
1610
|
+
object: z16.literal("chat.completion.chunk"),
|
|
1611
|
+
created: z16.int(),
|
|
1612
|
+
model: z16.string(),
|
|
1613
|
+
choices: z16.array(
|
|
1614
|
+
z16.object({
|
|
1615
|
+
index: z16.int(),
|
|
1616
|
+
delta: z16.object({
|
|
1617
|
+
role: z16.literal("assistant").optional(),
|
|
1618
|
+
content: z16.string().optional()
|
|
1092
1619
|
}),
|
|
1093
|
-
finish_reason:
|
|
1620
|
+
finish_reason: z16.string().nullable()
|
|
1094
1621
|
})
|
|
1095
1622
|
),
|
|
1096
1623
|
usage: OpenAiUsageSchema.nullish()
|
|
1097
1624
|
});
|
|
1098
|
-
var OpenAiModelSchema =
|
|
1099
|
-
id:
|
|
1100
|
-
object:
|
|
1101
|
-
created:
|
|
1102
|
-
owned_by:
|
|
1625
|
+
var OpenAiModelSchema = z16.object({
|
|
1626
|
+
id: z16.string(),
|
|
1627
|
+
object: z16.literal("model"),
|
|
1628
|
+
created: z16.int(),
|
|
1629
|
+
owned_by: z16.string()
|
|
1103
1630
|
});
|
|
1104
|
-
var OpenAiModelListSchema =
|
|
1105
|
-
object:
|
|
1106
|
-
data:
|
|
1631
|
+
var OpenAiModelListSchema = z16.object({
|
|
1632
|
+
object: z16.literal("list"),
|
|
1633
|
+
data: z16.array(OpenAiModelSchema)
|
|
1107
1634
|
});
|
|
1108
1635
|
|
|
1109
1636
|
// ../shared/src/usage.ts
|
|
1110
|
-
import { z as
|
|
1637
|
+
import { z as z18 } from "zod";
|
|
1111
1638
|
|
|
1112
1639
|
// ../shared/src/principal.ts
|
|
1113
|
-
import { z as
|
|
1640
|
+
import { z as z17 } from "zod";
|
|
1114
1641
|
var PRINCIPAL_KINDS = ["user", "password", "anon", "dev"];
|
|
1115
|
-
var PrincipalKindSchema =
|
|
1642
|
+
var PrincipalKindSchema = z17.enum(PRINCIPAL_KINDS);
|
|
1116
1643
|
|
|
1117
1644
|
// ../shared/src/usage.ts
|
|
1118
1645
|
var GATEWAY_OUTCOMES = [
|
|
@@ -1121,159 +1648,208 @@ var GATEWAY_OUTCOMES = [
|
|
|
1121
1648
|
"refusal",
|
|
1122
1649
|
"quota_blocked",
|
|
1123
1650
|
"conflict",
|
|
1124
|
-
"forbidden"
|
|
1651
|
+
"forbidden",
|
|
1652
|
+
"connection_required"
|
|
1125
1653
|
];
|
|
1126
|
-
var GatewayOutcomeSchema =
|
|
1654
|
+
var GatewayOutcomeSchema = z18.enum(GATEWAY_OUTCOMES);
|
|
1127
1655
|
var USAGE_RANGES = ["24h", "7d", "30d"];
|
|
1128
|
-
var UsageRangeSchema =
|
|
1656
|
+
var UsageRangeSchema = z18.enum(USAGE_RANGES);
|
|
1129
1657
|
var PLATFORM_RANGES = ["7d", "30d", "90d"];
|
|
1130
|
-
var PlatformRangeSchema =
|
|
1131
|
-
var UsageSeriesPointSchema =
|
|
1132
|
-
bucket:
|
|
1133
|
-
costUsd:
|
|
1134
|
-
tokens:
|
|
1135
|
-
requests:
|
|
1136
|
-
});
|
|
1137
|
-
var UsageSummarySchema =
|
|
1138
|
-
appId:
|
|
1658
|
+
var PlatformRangeSchema = z18.enum(PLATFORM_RANGES);
|
|
1659
|
+
var UsageSeriesPointSchema = z18.object({
|
|
1660
|
+
bucket: z18.iso.datetime(),
|
|
1661
|
+
costUsd: z18.number().nonnegative(),
|
|
1662
|
+
tokens: z18.int().nonnegative(),
|
|
1663
|
+
requests: z18.int().nonnegative()
|
|
1664
|
+
});
|
|
1665
|
+
var UsageSummarySchema = z18.object({
|
|
1666
|
+
appId: z18.uuid(),
|
|
1139
1667
|
/** The rolling range these figures cover. */
|
|
1140
1668
|
range: UsageRangeSchema,
|
|
1141
|
-
requests:
|
|
1142
|
-
inputTokens:
|
|
1143
|
-
outputTokens:
|
|
1669
|
+
requests: z18.int().nonnegative(),
|
|
1670
|
+
inputTokens: z18.int().nonnegative(),
|
|
1671
|
+
outputTokens: z18.int().nonnegative(),
|
|
1144
1672
|
/** Cache-aware input token totals (0 until prompt caching is enabled). */
|
|
1145
|
-
cacheReadInputTokens:
|
|
1146
|
-
cacheCreationInputTokens:
|
|
1673
|
+
cacheReadInputTokens: z18.int().nonnegative(),
|
|
1674
|
+
cacheCreationInputTokens: z18.int().nonnegative(),
|
|
1147
1675
|
/** Estimated spend in USD over the window at current rates (./pricing.ts). */
|
|
1148
|
-
costUsd:
|
|
1676
|
+
costUsd: z18.number().nonnegative(),
|
|
1149
1677
|
/** 95th-percentile upstream latency (ms) over the window; null when no timed calls. */
|
|
1150
|
-
latencyP95Ms:
|
|
1678
|
+
latencyP95Ms: z18.number().nonnegative().nullable(),
|
|
1151
1679
|
/** Fraction of calls in the window whose outcome was not `ok` (0..1). */
|
|
1152
|
-
errorRate:
|
|
1680
|
+
errorRate: z18.number().min(0).max(1),
|
|
1153
1681
|
/** Count of calls keyed by outcome — see {@link GATEWAY_OUTCOMES}. */
|
|
1154
|
-
byOutcome:
|
|
1155
|
-
byModel:
|
|
1156
|
-
|
|
1157
|
-
model:
|
|
1158
|
-
tokens:
|
|
1159
|
-
requests:
|
|
1682
|
+
byOutcome: z18.record(z18.string(), z18.int().nonnegative()),
|
|
1683
|
+
byModel: z18.array(
|
|
1684
|
+
z18.object({
|
|
1685
|
+
model: z18.string(),
|
|
1686
|
+
tokens: z18.int().nonnegative(),
|
|
1687
|
+
requests: z18.int().nonnegative(),
|
|
1160
1688
|
/** Estimated spend in USD for this model over the window. */
|
|
1161
|
-
costUsd:
|
|
1689
|
+
costUsd: z18.number().nonnegative()
|
|
1162
1690
|
})
|
|
1163
1691
|
),
|
|
1164
1692
|
/** Dense, zero-filled buckets across the range, oldest-first, for the trend chart. */
|
|
1165
|
-
series:
|
|
1693
|
+
series: z18.array(UsageSeriesPointSchema),
|
|
1166
1694
|
/**
|
|
1167
1695
|
* Today-since-midnight totals, independent of `range` — backs the daily-cap
|
|
1168
1696
|
* gauge (the budget the edge enforces is per calendar day).
|
|
1169
1697
|
*/
|
|
1170
|
-
today:
|
|
1171
|
-
tokens:
|
|
1172
|
-
costUsd:
|
|
1698
|
+
today: z18.object({
|
|
1699
|
+
tokens: z18.int().nonnegative(),
|
|
1700
|
+
costUsd: z18.number().nonnegative()
|
|
1173
1701
|
})
|
|
1174
1702
|
});
|
|
1175
|
-
var GatewayCallSchema =
|
|
1176
|
-
id:
|
|
1177
|
-
appId:
|
|
1703
|
+
var GatewayCallSchema = z18.object({
|
|
1704
|
+
id: z18.uuid(),
|
|
1705
|
+
appId: z18.uuid(),
|
|
1178
1706
|
/** App slug at read time; null when the app row no longer exists. */
|
|
1179
|
-
slug:
|
|
1180
|
-
userOid:
|
|
1707
|
+
slug: z18.string().nullable(),
|
|
1708
|
+
userOid: z18.string(),
|
|
1181
1709
|
/**
|
|
1182
1710
|
* The caller's claims as captured when the call was made — the **display half**
|
|
1183
1711
|
* (`App.ownerName`/`ownerEmail` applied to the ledger). Render these; never
|
|
1184
1712
|
* compare them, and never join on them. Null for anonymous, shared-password and
|
|
1185
1713
|
* dev-token callers, and for rows predating the columns, in which case the UI
|
|
1186
|
-
* falls back to `userOid` —
|
|
1187
|
-
*
|
|
1714
|
+
* falls back to `userOid` — an opaque principal id the platform declines
|
|
1715
|
+
* to resolve (no Graph `/users` grant by design, ADR-0040 decision 3; and
|
|
1716
|
+
* pre-re-base rows hold a pairwise `sub`), so it is a last resort rather
|
|
1717
|
+
* than a label.
|
|
1188
1718
|
*/
|
|
1189
|
-
userName:
|
|
1190
|
-
userEmail:
|
|
1719
|
+
userName: z18.string().nullable(),
|
|
1720
|
+
userEmail: z18.string().nullable(),
|
|
1191
1721
|
/**
|
|
1192
1722
|
* Which kind of principal made the call. Read this instead of parsing
|
|
1193
1723
|
* `userOid` — see {@link PrincipalKindSchema} for why the obvious shape test is
|
|
1194
1724
|
* unsound. Null on rows written before the column, which render from `userOid`.
|
|
1195
1725
|
*/
|
|
1196
1726
|
userKind: PrincipalKindSchema.nullable(),
|
|
1197
|
-
capability:
|
|
1198
|
-
model:
|
|
1199
|
-
inputTokens:
|
|
1200
|
-
outputTokens:
|
|
1201
|
-
cacheReadInputTokens:
|
|
1202
|
-
cacheCreationInputTokens:
|
|
1727
|
+
capability: z18.string(),
|
|
1728
|
+
model: z18.string(),
|
|
1729
|
+
inputTokens: z18.int().nonnegative(),
|
|
1730
|
+
outputTokens: z18.int().nonnegative(),
|
|
1731
|
+
cacheReadInputTokens: z18.int().nonnegative(),
|
|
1732
|
+
cacheCreationInputTokens: z18.int().nonnegative(),
|
|
1203
1733
|
/** Estimated spend in USD for this single call at current rates. */
|
|
1204
|
-
costUsd:
|
|
1734
|
+
costUsd: z18.number().nonnegative(),
|
|
1205
1735
|
/** Upstream round-trip latency in ms (0 when not measured). */
|
|
1206
|
-
durationMs:
|
|
1736
|
+
durationMs: z18.int().nonnegative(),
|
|
1207
1737
|
/** Upstream/egress HTTP status — set for `fetch`; null for streamed `llm`. */
|
|
1208
|
-
statusCode:
|
|
1738
|
+
statusCode: z18.int().nullable(),
|
|
1209
1739
|
/**
|
|
1210
1740
|
* Request path of the proxied call — set for `fetch`; null otherwise. The
|
|
1211
1741
|
* target's query string is excluded (see `redactFetchTarget` in `./logging.ts`)
|
|
1212
1742
|
* because that is where credentials are conventionally placed — but a path can
|
|
1213
1743
|
* carry one too, and nothing here detects that. See ADR-0021.
|
|
1214
1744
|
*/
|
|
1215
|
-
path:
|
|
1745
|
+
path: z18.string().nullable(),
|
|
1216
1746
|
/** HTTP method of the proxied call — set for `fetch`; null otherwise. */
|
|
1217
|
-
method:
|
|
1747
|
+
method: z18.string().nullable(),
|
|
1218
1748
|
/** LLM stop reason; null for non-LLM calls. */
|
|
1219
|
-
stopReason:
|
|
1220
|
-
/**
|
|
1221
|
-
|
|
1749
|
+
stopReason: z18.string().nullable(),
|
|
1750
|
+
/**
|
|
1751
|
+
* Short upstream error string; null on success. Admin-only in practice — it
|
|
1752
|
+
* can quote request content and, on an auth failure, the key, so it serves
|
|
1753
|
+
* the admin audit log's audience while the app-scoped feed
|
|
1754
|
+
* (`/api/v1/apps/:slug/audit`) nulls it for non-admin callers.
|
|
1755
|
+
*/
|
|
1756
|
+
errorDetail: z18.string().nullable(),
|
|
1222
1757
|
outcome: GatewayOutcomeSchema,
|
|
1223
|
-
createdAt:
|
|
1758
|
+
createdAt: z18.iso.datetime()
|
|
1224
1759
|
});
|
|
1225
|
-
var GatewayAuditPageSchema =
|
|
1226
|
-
rows:
|
|
1760
|
+
var GatewayAuditPageSchema = z18.object({
|
|
1761
|
+
rows: z18.array(GatewayCallSchema),
|
|
1227
1762
|
/** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
|
|
1228
|
-
nextBefore:
|
|
1763
|
+
nextBefore: z18.iso.datetime().optional()
|
|
1229
1764
|
});
|
|
1230
|
-
var PlatformUsageSchema =
|
|
1765
|
+
var PlatformUsageSchema = z18.object({
|
|
1231
1766
|
/** The rolling range the series + breakdowns cover. */
|
|
1232
1767
|
range: PlatformRangeSchema,
|
|
1233
1768
|
/** Dense, zero-filled daily buckets across the range, oldest-first. */
|
|
1234
|
-
series:
|
|
1769
|
+
series: z18.array(UsageSeriesPointSchema),
|
|
1235
1770
|
/** Per-app rollup over the range, busiest-first. */
|
|
1236
|
-
byApp:
|
|
1237
|
-
|
|
1238
|
-
slug:
|
|
1239
|
-
tokens:
|
|
1240
|
-
requests:
|
|
1771
|
+
byApp: z18.array(
|
|
1772
|
+
z18.object({
|
|
1773
|
+
slug: z18.string().nullable(),
|
|
1774
|
+
tokens: z18.int().nonnegative(),
|
|
1775
|
+
requests: z18.int().nonnegative(),
|
|
1241
1776
|
/** Estimated spend in USD for this app over the range. */
|
|
1242
|
-
costUsd:
|
|
1777
|
+
costUsd: z18.number().nonnegative()
|
|
1243
1778
|
})
|
|
1244
1779
|
),
|
|
1245
1780
|
/** Month-to-date headline KPIs (independent of `range`). */
|
|
1246
|
-
totals:
|
|
1247
|
-
tokensMTD:
|
|
1248
|
-
requestsMTD:
|
|
1781
|
+
totals: z18.object({
|
|
1782
|
+
tokensMTD: z18.int().nonnegative(),
|
|
1783
|
+
requestsMTD: z18.int().nonnegative(),
|
|
1249
1784
|
/** Estimated month-to-date spend in USD across all apps. */
|
|
1250
|
-
costMTD:
|
|
1785
|
+
costMTD: z18.number().nonnegative(),
|
|
1251
1786
|
/** Distinct `userOid`s seen month-to-date. */
|
|
1252
|
-
activeUsers:
|
|
1787
|
+
activeUsers: z18.int().nonnegative()
|
|
1253
1788
|
}),
|
|
1254
1789
|
/** Token + cost share by capability over the range (essentially all `llm` in M4). */
|
|
1255
|
-
capabilityMix:
|
|
1256
|
-
|
|
1257
|
-
capability:
|
|
1258
|
-
tokens:
|
|
1790
|
+
capabilityMix: z18.array(
|
|
1791
|
+
z18.object({
|
|
1792
|
+
capability: z18.string(),
|
|
1793
|
+
tokens: z18.int().nonnegative(),
|
|
1259
1794
|
/** Estimated spend in USD for this capability over the range. */
|
|
1260
|
-
costUsd:
|
|
1795
|
+
costUsd: z18.number().nonnegative()
|
|
1261
1796
|
})
|
|
1262
1797
|
)
|
|
1263
1798
|
});
|
|
1264
1799
|
|
|
1265
|
-
// ../shared/src/
|
|
1266
|
-
import { z as
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
var
|
|
1271
|
-
|
|
1800
|
+
// ../shared/src/visitors.ts
|
|
1801
|
+
import { z as z19 } from "zod";
|
|
1802
|
+
var VISITOR_RANGES = ["7d", "30d", "90d"];
|
|
1803
|
+
var VisitorRangeSchema = z19.enum(VISITOR_RANGES);
|
|
1804
|
+
var count = z19.int().nonnegative();
|
|
1805
|
+
var VisitorTotalsSchema = z19.object({
|
|
1806
|
+
visits: count,
|
|
1807
|
+
uniqueVisitors: count
|
|
1808
|
+
});
|
|
1809
|
+
var GeoStatusSchema = z19.object({
|
|
1810
|
+
available: z19.boolean(),
|
|
1811
|
+
reason: z19.string().nullable(),
|
|
1812
|
+
attribution: z19.object({ text: z19.string(), url: z19.url() }).nullable()
|
|
1813
|
+
});
|
|
1814
|
+
var VisitorLocationSchema = z19.object({
|
|
1815
|
+
/** ISO 3166-1 alpha-2. */
|
|
1816
|
+
country: z19.string(),
|
|
1817
|
+
countryName: z19.string().nullable(),
|
|
1818
|
+
/** First-level subdivision (state, province); null when the database has none. */
|
|
1819
|
+
region: z19.string().nullable(),
|
|
1820
|
+
visitors: count,
|
|
1821
|
+
visits: count
|
|
1822
|
+
});
|
|
1823
|
+
var VisitorSeriesPointSchema = z19.object({
|
|
1824
|
+
/** Bucket-start ISO timestamp (one calendar day). */
|
|
1825
|
+
bucket: z19.iso.datetime(),
|
|
1826
|
+
visits: count,
|
|
1827
|
+
/** Distinct visitors that day — a visitor active on two days counts on each. */
|
|
1828
|
+
uniqueVisitors: count
|
|
1829
|
+
});
|
|
1830
|
+
var VisitorSummarySchema = z19.object({
|
|
1831
|
+
appId: z19.uuid(),
|
|
1832
|
+
range: VisitorRangeSchema,
|
|
1833
|
+
/** The selected window. */
|
|
1834
|
+
current: VisitorTotalsSchema,
|
|
1835
|
+
/** The window of the same length immediately before it, for the deltas. */
|
|
1836
|
+
prior: VisitorTotalsSchema,
|
|
1837
|
+
/** Dense, zero-filled daily buckets, oldest first. */
|
|
1838
|
+
series: z19.array(VisitorSeriesPointSchema),
|
|
1839
|
+
/** The top {@link VISITOR_LOCATION_LIMIT} locations by visitors. */
|
|
1840
|
+
locations: z19.array(VisitorLocationSchema),
|
|
1841
|
+
/** Every resolved location past the top ones. */
|
|
1842
|
+
otherLocations: VisitorTotalsSchema,
|
|
1843
|
+
/** Private, proxied or unknown networks, and every row when geo is unavailable. */
|
|
1844
|
+
unresolved: VisitorTotalsSchema,
|
|
1845
|
+
geo: GeoStatusSchema
|
|
1846
|
+
});
|
|
1272
1847
|
|
|
1273
1848
|
// ../shared/src/data.ts
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1849
|
+
import { z as z20 } from "zod";
|
|
1850
|
+
var CollectionItemSchema = z20.object({
|
|
1851
|
+
id: z20.uuid(),
|
|
1852
|
+
collection: z20.string(),
|
|
1277
1853
|
/**
|
|
1278
1854
|
* Which tier collected the row (dev-mode §5). The runtime roles are RLS-pinned
|
|
1279
1855
|
* to one tier each, but the portal reads across both, so the discriminator has
|
|
@@ -1282,157 +1858,215 @@ var CollectionItemSchema = z17.object({
|
|
|
1282
1858
|
*/
|
|
1283
1859
|
env: EnvSchema,
|
|
1284
1860
|
/** The submitting user, if authenticated; null for anonymous/public visitors. */
|
|
1285
|
-
userOid:
|
|
1861
|
+
userOid: z20.string().nullable(),
|
|
1286
1862
|
/**
|
|
1287
1863
|
* The submitter's captured claims — display half, rendered but never compared
|
|
1288
1864
|
* (see `GatewayCallSchema.userName`). Null whenever `userOid` is, and for
|
|
1289
1865
|
* password/dev-token principals.
|
|
1290
1866
|
*/
|
|
1291
|
-
userName:
|
|
1292
|
-
userEmail:
|
|
1867
|
+
userName: z20.string().nullable(),
|
|
1868
|
+
userEmail: z20.string().nullable(),
|
|
1293
1869
|
/** The submitter's kind; null whenever `userOid` is, and on pre-column rows. */
|
|
1294
1870
|
userKind: PrincipalKindSchema.nullable(),
|
|
1295
|
-
item:
|
|
1871
|
+
item: z20.unknown(),
|
|
1296
1872
|
/** Hashed IP / truncated UA for abuse triage; null if not captured. */
|
|
1297
|
-
meta:
|
|
1298
|
-
createdAt:
|
|
1873
|
+
meta: z20.unknown().nullable(),
|
|
1874
|
+
createdAt: z20.iso.datetime()
|
|
1299
1875
|
});
|
|
1300
|
-
var CollectionItemsPageSchema =
|
|
1301
|
-
rows:
|
|
1876
|
+
var CollectionItemsPageSchema = z20.object({
|
|
1877
|
+
rows: z20.array(CollectionItemSchema),
|
|
1302
1878
|
/** Pass as `?before=` to fetch the next (older) page; absent when exhausted. */
|
|
1303
|
-
nextBefore:
|
|
1879
|
+
nextBefore: z20.iso.datetime().optional()
|
|
1304
1880
|
});
|
|
1305
|
-
var CollectionSummarySchema =
|
|
1306
|
-
name:
|
|
1881
|
+
var CollectionSummarySchema = z20.object({
|
|
1882
|
+
name: z20.string(),
|
|
1307
1883
|
env: EnvSchema,
|
|
1308
|
-
count:
|
|
1884
|
+
count: z20.int().nonnegative(),
|
|
1309
1885
|
/** Newest row's timestamp; null only if the group is somehow empty. */
|
|
1310
|
-
lastAt:
|
|
1886
|
+
lastAt: z20.iso.datetime().nullable()
|
|
1311
1887
|
});
|
|
1312
1888
|
|
|
1313
1889
|
// ../shared/src/collectionTable.ts
|
|
1314
1890
|
var BOM = String.fromCharCode(65279);
|
|
1315
1891
|
|
|
1316
|
-
// ../shared/src/
|
|
1317
|
-
import { z as
|
|
1318
|
-
var
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1892
|
+
// ../shared/src/connections.ts
|
|
1893
|
+
import { z as z21 } from "zod";
|
|
1894
|
+
var CONNECTION_STATUSES = ["live", "reconnect-needed", "invalidated"];
|
|
1895
|
+
var ConnectionStatusSchema = z21.enum(CONNECTION_STATUSES);
|
|
1896
|
+
var ConnectionMaterialSchema = z21.strictObject({
|
|
1897
|
+
access: z21.string().min(1),
|
|
1898
|
+
refresh: z21.string().min(1)
|
|
1899
|
+
});
|
|
1900
|
+
var MAX_GRANTED_SCOPES = 64;
|
|
1901
|
+
var GrantedScopesSchema = z21.array(ScopeTokenSchema).max(MAX_GRANTED_SCOPES);
|
|
1902
|
+
var UserConnectionSchema = z21.strictObject({
|
|
1903
|
+
id: z21.uuid(),
|
|
1904
|
+
userOid: z21.string().min(1),
|
|
1905
|
+
providerId: z21.uuid(),
|
|
1906
|
+
providerRevision: z21.int().positive(),
|
|
1907
|
+
env: EnvSchema,
|
|
1908
|
+
status: ConnectionStatusSchema,
|
|
1909
|
+
material: z21.string().min(1),
|
|
1910
|
+
grantedScopes: GrantedScopesSchema,
|
|
1911
|
+
grantedAt: z21.iso.datetime(),
|
|
1912
|
+
expiresAt: z21.iso.datetime(),
|
|
1913
|
+
renewBeforeNext: z21.boolean(),
|
|
1914
|
+
pendingRetire: z21.string().min(1).nullable(),
|
|
1915
|
+
lastRenewedAt: z21.iso.datetime().nullable(),
|
|
1916
|
+
createdAt: z21.iso.datetime(),
|
|
1917
|
+
updatedAt: z21.iso.datetime()
|
|
1918
|
+
});
|
|
1919
|
+
var MyConnectionSchema = z21.strictObject({
|
|
1920
|
+
id: z21.uuid(),
|
|
1921
|
+
providerRef: ProviderRefSchema,
|
|
1922
|
+
providerDisplayName: z21.string().min(1).max(200),
|
|
1923
|
+
env: EnvSchema,
|
|
1924
|
+
status: ConnectionStatusSchema.exclude(["invalidated"]),
|
|
1925
|
+
grantedScopes: GrantedScopesSchema,
|
|
1926
|
+
grantedAt: z21.iso.datetime(),
|
|
1927
|
+
sharedApps: z21.array(BoundAppSchema)
|
|
1928
|
+
});
|
|
1929
|
+
var MyConnectionsResponseSchema = z21.strictObject({
|
|
1930
|
+
connections: z21.array(MyConnectionSchema)
|
|
1931
|
+
});
|
|
1932
|
+
var DisconnectResponseSchema = z21.strictObject({
|
|
1933
|
+
outcome: z21.enum(["disconnected", "already_removed"])
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
// ../shared/src/consent.ts
|
|
1937
|
+
import { z as z22 } from "zod";
|
|
1938
|
+
var ConsentStateSchema = z22.string().min(43).max(128);
|
|
1939
|
+
var ConsentNonceSchema = z22.string().min(16).max(128);
|
|
1940
|
+
var ConsentAttemptTagSchema = z22.string().min(1).max(128).regex(/^[A-Za-z0-9._~-]+$/, "must be URL-safe punctuation, letters, or digits");
|
|
1941
|
+
var ConsentIdentitySchema = z22.discriminatedUnion("kind", [
|
|
1942
|
+
z22.strictObject({ kind: z22.literal("user"), userOid: z22.string().min(1).max(128) }),
|
|
1943
|
+
z22.strictObject({
|
|
1944
|
+
kind: z22.literal("dev"),
|
|
1945
|
+
developerOid: z22.string().min(1).max(128),
|
|
1946
|
+
nonce: ConsentNonceSchema
|
|
1947
|
+
})
|
|
1327
1948
|
]);
|
|
1328
|
-
var
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
};
|
|
1335
|
-
var
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
}),
|
|
1345
|
-
|
|
1346
|
-
z18.object({ kind: z18.literal("query"), param: z18.string().min(1) }),
|
|
1347
|
-
/**
|
|
1348
|
-
* HMAC over a timestamp. The signed input is the timestamp string **alone** —
|
|
1349
|
-
* not the method, path, query, or body — so injection is a pure function of
|
|
1350
|
-
* (private key, now) and needs no request context.
|
|
1351
|
-
*
|
|
1352
|
-
* The canonical form lives in the KIND NAME, deliberately. A scheme that signs
|
|
1353
|
-
* method+path+body is a *sibling kind* (`hmac-request`) — a code change with
|
|
1354
|
-
* tests, reviewed — never an admin-editable canonical-string template.
|
|
1355
|
-
* Canonicalization is where implementations of this family go wrong, and it
|
|
1356
|
-
* does not belong in a text box.
|
|
1357
|
-
*
|
|
1358
|
-
* SHA-256, lowercase-hex, and ISO-8601-with-milliseconds are fixed rather than
|
|
1359
|
-
* configurable. Each would carry a default, and `app_secrets.injection` is a
|
|
1360
|
-
* schemaless JSON column, so adding a knob later is purely additive with no
|
|
1361
|
-
* migration — while fixing them now removes the weak-algorithm and
|
|
1362
|
-
* unencodable-digest failure classes outright.
|
|
1363
|
-
*
|
|
1364
|
-
* The stored value is a JSON blob carrying both halves of the key pair
|
|
1365
|
-
* ({@link HmacCredentialSchema}): regenerating the pair changes both, and a
|
|
1366
|
-
* blob rotates atomically through the existing rotate route.
|
|
1367
|
-
*/
|
|
1368
|
-
z18.object({
|
|
1369
|
-
kind: z18.literal("hmac-timestamp"),
|
|
1370
|
-
/** Header carrying the timestamp that is also the entire signed input. */
|
|
1371
|
-
timestampHeader: headerName(from),
|
|
1372
|
-
/** Header carrying the rendered credential + signature. */
|
|
1373
|
-
authHeader: headerName(from).default("authorization"),
|
|
1374
|
-
/**
|
|
1375
|
-
* Value written to `authHeader`. `{credential}` and `{signature}` are
|
|
1376
|
-
* substituted. Named rather than the `header` kind's bare `{}` because there
|
|
1377
|
-
* are two substitutions: the convention is one value ⇒ `{}`, more than one ⇒
|
|
1378
|
-
* named placeholders. Positional `{}` here would let a swapped template
|
|
1379
|
-
* produce a well-formed header that silently fails to authenticate.
|
|
1380
|
-
*
|
|
1381
|
-
* Both placeholders are required. This recipe writes exactly two headers,
|
|
1382
|
-
* one of them the timestamp, so there is no configuration where the public
|
|
1383
|
-
* credential id travels elsewhere — omitting `{credential}` means the
|
|
1384
|
-
* upstream can never identify the key and every call 401s.
|
|
1385
|
-
*/
|
|
1386
|
-
template: headerTemplate(from).refine((t) => t.includes("{signature}"), "template must contain {signature}").refine((t) => t.includes("{credential}"), "template must contain {credential}")
|
|
1387
|
-
}).refine(
|
|
1388
|
-
(r) => r.timestampHeader !== r.authHeader,
|
|
1389
|
-
"timestampHeader and authHeader must differ \u2014 the second write overwrites the first"
|
|
1390
|
-
)
|
|
1949
|
+
var HttpUrlNoUserinfoSchema = (() => {
|
|
1950
|
+
const userinfo = /^https?:\/\/[/\\]*[^/?#\\]*@/i;
|
|
1951
|
+
return z22.url({ protocol: /^https?$/ }).refine(
|
|
1952
|
+
(value) => !userinfo.test(value.replace(/[\t\n\r]/g, "")),
|
|
1953
|
+
"URL must not carry userinfo"
|
|
1954
|
+
);
|
|
1955
|
+
})();
|
|
1956
|
+
var ConsultRequestSchema = z22.strictObject({
|
|
1957
|
+
identity: ConsentIdentitySchema,
|
|
1958
|
+
appSlug: z22.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
1959
|
+
providerRef: ProviderRefSchema,
|
|
1960
|
+
openerOrigin: HttpUrlNoUserinfoSchema.transform((value) => new URL(value).origin),
|
|
1961
|
+
callbackUrl: HttpUrlNoUserinfoSchema
|
|
1962
|
+
});
|
|
1963
|
+
var ConsultResponseSchema = z22.discriminatedUnion("outcome", [
|
|
1964
|
+
z22.strictObject({ outcome: z22.literal("already_connected") }),
|
|
1965
|
+
z22.strictObject({ outcome: z22.literal("not_available") }),
|
|
1966
|
+
z22.strictObject({ outcome: z22.literal("started"), authorizeUrl: z22.url() })
|
|
1391
1967
|
]);
|
|
1392
|
-
var
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
credential: z18.string().min(1),
|
|
1396
|
-
key: z18.string().min(1)
|
|
1968
|
+
var CancelRequestSchema = z22.strictObject({
|
|
1969
|
+
identity: ConsentIdentitySchema,
|
|
1970
|
+
state: ConsentStateSchema
|
|
1397
1971
|
});
|
|
1398
|
-
var
|
|
1399
|
-
var
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
var
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1972
|
+
var CONSENT_CANCEL_OUTCOMES = ["cancelled", "not_cancellable"];
|
|
1973
|
+
var CancelResponseSchema = z22.strictObject({
|
|
1974
|
+
outcome: z22.enum(CONSENT_CANCEL_OUTCOMES)
|
|
1975
|
+
});
|
|
1976
|
+
var ConnectionStatusIdentitySchema = z22.discriminatedUnion("kind", [
|
|
1977
|
+
z22.strictObject({ kind: z22.literal("user"), userOid: z22.string().min(1).max(128) }),
|
|
1978
|
+
z22.strictObject({ kind: z22.literal("dev"), developerOid: z22.string().min(1).max(128) })
|
|
1979
|
+
]);
|
|
1980
|
+
var ConnectionStatusRequestSchema = z22.strictObject({
|
|
1981
|
+
identity: ConnectionStatusIdentitySchema,
|
|
1982
|
+
appSlug: z22.string().regex(SLUG_PATTERN, "must be a lowercase DNS label (a-z, 0-9, hyphen)"),
|
|
1983
|
+
providerRef: ProviderRefSchema
|
|
1984
|
+
});
|
|
1985
|
+
var ConnectionStatusResponseSchema = z22.strictObject({
|
|
1986
|
+
status: z22.enum(["connected", "not_connected", "not_available"])
|
|
1987
|
+
});
|
|
1988
|
+
var AttemptCancelRequestSchema = z22.strictObject({
|
|
1989
|
+
provider: ProviderRefSchema,
|
|
1990
|
+
attempt: ConsentAttemptTagSchema
|
|
1991
|
+
});
|
|
1992
|
+
var DevConsentStartResponseSchema = z22.discriminatedUnion("outcome", [
|
|
1993
|
+
z22.strictObject({ outcome: z22.literal("started"), popupUrl: z22.url() }),
|
|
1994
|
+
z22.strictObject({ outcome: z22.literal("already_connected") }),
|
|
1995
|
+
z22.strictObject({ outcome: z22.literal("not_available") })
|
|
1996
|
+
]);
|
|
1997
|
+
var HELIX_CONNECT_MESSAGE_SOURCE = "helix-connect";
|
|
1998
|
+
var CONNECT_MESSAGE_VERSION = 1;
|
|
1999
|
+
var CONSENT_MESSAGE_OUTCOMES = [
|
|
2000
|
+
"connected",
|
|
2001
|
+
"already_connected",
|
|
2002
|
+
"denied",
|
|
2003
|
+
"cancelled",
|
|
2004
|
+
"timeout",
|
|
2005
|
+
"blocked",
|
|
2006
|
+
"signin_required",
|
|
2007
|
+
"error"
|
|
2008
|
+
];
|
|
2009
|
+
var CONSENT_MESSAGE_REASONS = [
|
|
2010
|
+
"conflict",
|
|
2011
|
+
"provider_unavailable",
|
|
2012
|
+
"provider_misconfigured",
|
|
2013
|
+
"provider_incompatible",
|
|
2014
|
+
"service_unavailable"
|
|
2015
|
+
];
|
|
2016
|
+
var ConnectOutcomeMessageSchema = z22.strictObject({
|
|
2017
|
+
source: z22.literal(HELIX_CONNECT_MESSAGE_SOURCE),
|
|
2018
|
+
version: z22.literal(CONNECT_MESSAGE_VERSION),
|
|
2019
|
+
attempt: ConsentAttemptTagSchema.optional(),
|
|
2020
|
+
provider: ProviderRefSchema,
|
|
2021
|
+
outcome: z22.enum(CONSENT_MESSAGE_OUTCOMES),
|
|
2022
|
+
reason: z22.enum(CONSENT_MESSAGE_REASONS).nullable()
|
|
2023
|
+
}).refine((message) => message.outcome === "error" || message.reason === null, {
|
|
2024
|
+
error: "reason is only carried with the error outcome"
|
|
2025
|
+
});
|
|
2026
|
+
var ConnectResultSchema = z22.object({
|
|
2027
|
+
outcome: z22.enum(CONSENT_MESSAGE_OUTCOMES),
|
|
2028
|
+
provider: z22.string(),
|
|
2029
|
+
attempt: ConsentAttemptTagSchema.optional(),
|
|
2030
|
+
reason: z22.enum(CONSENT_MESSAGE_REASONS).optional()
|
|
2031
|
+
}).refine((result) => result.outcome === "error" || result.reason === void 0, {
|
|
2032
|
+
error: "reason is only carried with the error outcome"
|
|
1430
2033
|
});
|
|
1431
|
-
|
|
1432
|
-
|
|
2034
|
+
|
|
2035
|
+
// ../shared/src/exchange.ts
|
|
2036
|
+
import { z as z23 } from "zod";
|
|
2037
|
+
var ExchangeCodeSchema = z23.string().min(1).max(1024);
|
|
2038
|
+
var CodeVerifierSchema = z23.string().min(43).max(128).regex(/^[A-Za-z0-9\-._~]+$/, "must be an RFC 7636 code_verifier (43-128 unreserved characters)");
|
|
2039
|
+
var ExchangeRequestSchema = z23.strictObject({
|
|
2040
|
+
providerId: z23.uuid(),
|
|
2041
|
+
providerRevision: z23.int().positive(),
|
|
2042
|
+
env: EnvSchema,
|
|
2043
|
+
code: ExchangeCodeSchema,
|
|
2044
|
+
codeVerifier: CodeVerifierSchema,
|
|
2045
|
+
redirectUri: HttpUrlNoUserinfoSchema
|
|
2046
|
+
});
|
|
2047
|
+
var EXCHANGE_REJECTION_REASONS = [
|
|
2048
|
+
"unusable_lifetime",
|
|
2049
|
+
"missing_refresh_token",
|
|
2050
|
+
"missing_permissions"
|
|
2051
|
+
];
|
|
2052
|
+
var ExchangeRejectionReasonSchema = z23.enum(EXCHANGE_REJECTION_REASONS);
|
|
2053
|
+
var MAX_EXCHANGED_SCOPES = 64;
|
|
2054
|
+
var ExchangedScopesSchema = z23.array(ScopeTokenSchema).max(MAX_EXCHANGED_SCOPES);
|
|
2055
|
+
var ExchangeResponseSchema = z23.discriminatedUnion("outcome", [
|
|
2056
|
+
z23.strictObject({
|
|
2057
|
+
outcome: z23.literal("exchanged"),
|
|
2058
|
+
access: z23.string().min(1),
|
|
2059
|
+
refresh: z23.string().min(1),
|
|
2060
|
+
grantedScopes: ExchangedScopesSchema,
|
|
2061
|
+
accessExpiresAt: z23.iso.datetime()
|
|
2062
|
+
}),
|
|
2063
|
+
z23.strictObject({ outcome: z23.literal("rejected"), reason: ExchangeRejectionReasonSchema }),
|
|
2064
|
+
z23.strictObject({ outcome: z23.literal("provider_unavailable") }),
|
|
2065
|
+
z23.strictObject({ outcome: z23.literal("exchange_failed") })
|
|
2066
|
+
]);
|
|
1433
2067
|
|
|
1434
2068
|
// ../shared/src/devTokens.ts
|
|
1435
|
-
import { z as
|
|
2069
|
+
import { z as z24 } from "zod";
|
|
1436
2070
|
function isValidDevOrigin(input) {
|
|
1437
2071
|
if (input.includes("*")) return false;
|
|
1438
2072
|
let url;
|
|
@@ -1446,47 +2080,112 @@ function isValidDevOrigin(input) {
|
|
|
1446
2080
|
if (url.username !== "" || url.password !== "") return false;
|
|
1447
2081
|
return input === url.origin || input === `${url.origin}/`;
|
|
1448
2082
|
}
|
|
1449
|
-
var DevOriginSchema =
|
|
2083
|
+
var DevOriginSchema = z24.string().refine(
|
|
1450
2084
|
isValidDevOrigin,
|
|
1451
2085
|
"must be an exact origin (scheme://host[:port]) \u2014 no path, query, or wildcard"
|
|
1452
2086
|
).transform((o) => new URL(o).origin);
|
|
1453
|
-
var TtlDaysSchema =
|
|
1454
|
-
var DevTokenMintRequestSchema =
|
|
1455
|
-
origins:
|
|
2087
|
+
var TtlDaysSchema = z24.number().int().min(1).max(365).optional();
|
|
2088
|
+
var DevTokenMintRequestSchema = z24.object({
|
|
2089
|
+
origins: z24.array(DevOriginSchema).min(1).max(20),
|
|
1456
2090
|
ttlDays: TtlDaysSchema
|
|
1457
2091
|
});
|
|
1458
|
-
var DevTokenRotateRequestSchema =
|
|
2092
|
+
var DevTokenRotateRequestSchema = z24.object({
|
|
1459
2093
|
ttlDays: TtlDaysSchema
|
|
1460
2094
|
});
|
|
1461
|
-
var DevTokenMetadataSchema =
|
|
1462
|
-
id:
|
|
1463
|
-
developerOid:
|
|
1464
|
-
origins:
|
|
1465
|
-
expiresAt:
|
|
1466
|
-
revokedAt:
|
|
1467
|
-
createdAt:
|
|
2095
|
+
var DevTokenMetadataSchema = z24.object({
|
|
2096
|
+
id: z24.string(),
|
|
2097
|
+
developerOid: z24.string(),
|
|
2098
|
+
origins: z24.array(z24.string()),
|
|
2099
|
+
expiresAt: z24.string(),
|
|
2100
|
+
revokedAt: z24.string().nullable().optional(),
|
|
2101
|
+
createdAt: z24.string()
|
|
1468
2102
|
});
|
|
1469
|
-
var DevTokenMintResponseSchema =
|
|
1470
|
-
token:
|
|
2103
|
+
var DevTokenMintResponseSchema = z24.object({
|
|
2104
|
+
token: z24.string(),
|
|
1471
2105
|
metadata: DevTokenMetadataSchema
|
|
1472
2106
|
});
|
|
1473
2107
|
|
|
2108
|
+
// ../shared/src/repoBinding.ts
|
|
2109
|
+
import { z as z25 } from "zod";
|
|
2110
|
+
function hasControlChar(v) {
|
|
2111
|
+
for (let i = 0; i < v.length; i++) {
|
|
2112
|
+
const c = v.charCodeAt(i);
|
|
2113
|
+
if (c < 32 || c === 127) return true;
|
|
2114
|
+
}
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
var RepoFullNameSchema = z25.string().max(140).regex(
|
|
2118
|
+
/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/,
|
|
2119
|
+
"must be owner/name, as in the repository's GitHub URL"
|
|
2120
|
+
).refine((v) => !/\/\.{1,2}$/.test(v), "must be owner/name, as in the repository's GitHub URL");
|
|
2121
|
+
var BranchNameSchema = z25.string().min(1).max(255).refine(
|
|
2122
|
+
(v) => !v.startsWith("refs/") && !hasControlChar(v) && !/[\s~^:?*[\\]/.test(v) && !v.includes("..") && !v.includes("@{") && !v.includes("//") && !/^[-/]/.test(v) && !/(\/|\.lock|\.)$/.test(v),
|
|
2123
|
+
"must be a branch name such as main, without refs/heads/"
|
|
2124
|
+
);
|
|
2125
|
+
var WorkflowRefSchema = z25.string().max(512).regex(
|
|
2126
|
+
/^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}\/\.github\/workflows\/[^@\s]+\.ya?ml@\S+$/,
|
|
2127
|
+
"must look like owner/repo/.github/workflows/deploy.yml@refs/heads/main"
|
|
2128
|
+
);
|
|
2129
|
+
var EnvironmentNameSchema = z25.string().min(1).max(255).refine((v) => !hasControlChar(v) && v.trim() === v, "must be an environment name");
|
|
2130
|
+
var SetRepoBindingRequestSchema = z25.object({
|
|
2131
|
+
repoFullName: RepoFullNameSchema,
|
|
2132
|
+
branch: BranchNameSchema,
|
|
2133
|
+
workflowRef: WorkflowRefSchema.optional(),
|
|
2134
|
+
environment: EnvironmentNameSchema.optional()
|
|
2135
|
+
});
|
|
2136
|
+
var REPO_PROVIDERS = ["github"];
|
|
2137
|
+
var RepoBindingSchema = z25.object({
|
|
2138
|
+
provider: z25.enum(REPO_PROVIDERS),
|
|
2139
|
+
repoFullName: z25.string(),
|
|
2140
|
+
branch: z25.string(),
|
|
2141
|
+
workflowRef: z25.string().optional(),
|
|
2142
|
+
environment: z25.string().optional(),
|
|
2143
|
+
/** Pinned from the first accepted token; absent until then. */
|
|
2144
|
+
repositoryId: z25.string().optional(),
|
|
2145
|
+
repositoryOwnerId: z25.string().optional(),
|
|
2146
|
+
pinnedAt: z25.iso.datetime().optional(),
|
|
2147
|
+
createdAt: z25.iso.datetime(),
|
|
2148
|
+
updatedAt: z25.iso.datetime()
|
|
2149
|
+
});
|
|
2150
|
+
|
|
1474
2151
|
// ../shared/src/instruction.ts
|
|
1475
|
-
import { z as
|
|
2152
|
+
import { z as z26 } from "zod";
|
|
1476
2153
|
var INSTRUCTION_CAPABILITIES = ["fetch", "llm"];
|
|
1477
|
-
var InstructionCapabilitySchema =
|
|
1478
|
-
var AttestedInstructionSchema =
|
|
2154
|
+
var InstructionCapabilitySchema = z26.enum(INSTRUCTION_CAPABILITIES);
|
|
2155
|
+
var AttestedInstructionSchema = z26.strictObject({
|
|
1479
2156
|
/** App the call is attributed to (registry app id). */
|
|
1480
|
-
appId:
|
|
2157
|
+
appId: z26.string().min(1),
|
|
1481
2158
|
/** Authenticated user, or the anonymous sentinel on `public` apps. */
|
|
1482
|
-
userOid:
|
|
2159
|
+
userOid: z26.string().min(1),
|
|
2160
|
+
/**
|
|
2161
|
+
* Which kind of principal `userOid` is (the `PrincipalKind` vocabulary the
|
|
2162
|
+
* edge records at capture time — recorded, never inferred). The delegated
|
|
2163
|
+
* resolution refuses `anon` and `password` callers — they can
|
|
2164
|
+
* never hold a connection — and serving that refusal
|
|
2165
|
+
* requires knowing the KIND: `userOid`'s shape proves nothing (a shared-
|
|
2166
|
+
* password pseudonym and an Entra `sub` share the base64url alphabet, and
|
|
2167
|
+
* the sentinel is only exact for `anon`). Optional on the payload as a
|
|
2168
|
+
* whole so secret-backed and keyless instructions keep minting unchanged;
|
|
2169
|
+
* mandatory on a delegated one — the refine below makes a provider-bearing
|
|
2170
|
+
* instruction without it unrepresentable, so an old edge cannot mint a
|
|
2171
|
+
* delegated call egress would have to guess about.
|
|
2172
|
+
*/
|
|
2173
|
+
userKind: PrincipalKindSchema.optional(),
|
|
1483
2174
|
capability: InstructionCapabilitySchema,
|
|
1484
2175
|
/** The allowlisted origin the edge authorized (scheme + host + port). */
|
|
1485
|
-
origin:
|
|
2176
|
+
origin: z26.url(),
|
|
1486
2177
|
/** Connection (secret) name to inject, if this is a secret-backed call. */
|
|
1487
|
-
connection:
|
|
2178
|
+
connection: z26.string().min(1).optional(),
|
|
2179
|
+
/**
|
|
2180
|
+
* Provider reference for a delegated call — the user's connection to this
|
|
2181
|
+
* provider is what egress injects. Exactly
|
|
2182
|
+
* one credential source travels: this and `connection` are XOR-enforced
|
|
2183
|
+
* below — a delegated instruction carries a provider ref or a
|
|
2184
|
+
* secret name, never both.
|
|
2185
|
+
*/
|
|
2186
|
+
provider: ProviderRefSchema.optional(),
|
|
1488
2187
|
/** Correlates the edge audit row with the egress call. */
|
|
1489
|
-
requestId:
|
|
2188
|
+
requestId: z26.string().min(1),
|
|
1490
2189
|
/**
|
|
1491
2190
|
* The HTTP method + URL pathname the edge authorized (ADR-0013 step 2, issue #6).
|
|
1492
2191
|
* Egress refuses a mismatched verb/resource, so a captured instruction can't be
|
|
@@ -1501,8 +2200,8 @@ var AttestedInstructionSchema = z20.object({
|
|
|
1501
2200
|
* means "old edge", not tampering — egress asserts ONLY when the claim is
|
|
1502
2201
|
* present. Make required once a fleet is reliably past deploy.
|
|
1503
2202
|
*/
|
|
1504
|
-
method:
|
|
1505
|
-
path:
|
|
2203
|
+
method: z26.string().min(1).optional(),
|
|
2204
|
+
path: z26.string().optional(),
|
|
1506
2205
|
/**
|
|
1507
2206
|
* Environment tier this call is scoped to (dev-mode design §6). Egress resolves
|
|
1508
2207
|
* the connection secret within this tier — a `dev` instruction can never reach a
|
|
@@ -1511,12 +2210,27 @@ var AttestedInstructionSchema = z20.object({
|
|
|
1511
2210
|
* minted before this field existed verifies as production.
|
|
1512
2211
|
*/
|
|
1513
2212
|
env: EnvSchema.default("prod")
|
|
2213
|
+
}).superRefine((i, ctx) => {
|
|
2214
|
+
if (i.connection !== void 0 && i.provider !== void 0) {
|
|
2215
|
+
ctx.addIssue({
|
|
2216
|
+
code: "custom",
|
|
2217
|
+
path: ["provider"],
|
|
2218
|
+
message: "an instruction carries exactly one credential source \u2014 a connection secret or a provider reference, never both."
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
if (i.provider !== void 0 && i.userKind === void 0) {
|
|
2222
|
+
ctx.addIssue({
|
|
2223
|
+
code: "custom",
|
|
2224
|
+
path: ["userKind"],
|
|
2225
|
+
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."
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
1514
2228
|
});
|
|
1515
2229
|
var INSTRUCTION_TTL_SECONDS = 30;
|
|
1516
2230
|
var INSTRUCTION_BURN_RETENTION_SECONDS = INSTRUCTION_TTL_SECONDS + 15;
|
|
1517
2231
|
|
|
1518
2232
|
// ../shared/src/fetch.ts
|
|
1519
|
-
import { z as
|
|
2233
|
+
import { z as z27 } from "zod";
|
|
1520
2234
|
var FETCH_ERROR_CODES = [
|
|
1521
2235
|
"forbidden",
|
|
1522
2236
|
"rate_limited",
|
|
@@ -1524,24 +2238,95 @@ var FETCH_ERROR_CODES = [
|
|
|
1524
2238
|
"blocked",
|
|
1525
2239
|
"too_large",
|
|
1526
2240
|
"replay",
|
|
1527
|
-
"upstream_error"
|
|
2241
|
+
"upstream_error",
|
|
2242
|
+
"connection_required",
|
|
2243
|
+
"provider_unavailable",
|
|
2244
|
+
"provider_misconfigured"
|
|
1528
2245
|
];
|
|
1529
|
-
var FetchErrorCodeSchema =
|
|
1530
|
-
var
|
|
2246
|
+
var FetchErrorCodeSchema = z27.enum(FETCH_ERROR_CODES);
|
|
2247
|
+
var FetchErrorProviderSchema = z27.strictObject({
|
|
2248
|
+
ref: ProviderRefSchema,
|
|
2249
|
+
displayName: z27.string().min(1).max(200).optional()
|
|
2250
|
+
});
|
|
2251
|
+
var FetchProxyErrorSchema = z27.object({
|
|
1531
2252
|
code: FetchErrorCodeSchema,
|
|
1532
|
-
message:
|
|
2253
|
+
message: z27.string(),
|
|
2254
|
+
/** Present only on the delegated-call codes that carry it (`connection_required`, `provider_unavailable`). */
|
|
2255
|
+
provider: FetchErrorProviderSchema.optional()
|
|
2256
|
+
});
|
|
2257
|
+
|
|
2258
|
+
// ../shared/src/sessions.ts
|
|
2259
|
+
import { z as z28 } from "zod";
|
|
2260
|
+
var SessionPrincipalKindSchema = z28.enum(["user", "password"]);
|
|
2261
|
+
var SessionSummarySchema = z28.object({
|
|
2262
|
+
id: z28.uuid(),
|
|
2263
|
+
appId: z28.uuid(),
|
|
2264
|
+
/** App slug at read time; the FK guarantees the app row exists. */
|
|
2265
|
+
slug: z28.string(),
|
|
2266
|
+
/**
|
|
2267
|
+
* The identity half — Entra's `oid` claim since ADR-0048 (a pairwise `sub`
|
|
2268
|
+
* on rows that predate the re-base; a `pw_*` pseudonym for shared-password
|
|
2269
|
+
* visitors). Compared and joined on, never rendered except as a last
|
|
2270
|
+
* resort: the platform deliberately holds no Graph `/users` grant, so no
|
|
2271
|
+
* principal id it stores resolves to a name.
|
|
2272
|
+
*/
|
|
2273
|
+
userOid: z28.string(),
|
|
2274
|
+
/**
|
|
2275
|
+
* The display half, captured at login. Render, never compare. Null for
|
|
2276
|
+
* shared-password visitors (each login is a fresh pseudonym) and for rows
|
|
2277
|
+
* predating the columns.
|
|
2278
|
+
*/
|
|
2279
|
+
userName: z28.string().nullable(),
|
|
2280
|
+
userEmail: z28.string().nullable(),
|
|
2281
|
+
userKind: SessionPrincipalKindSchema.nullable(),
|
|
2282
|
+
/** Group-id snapshot as of this session's login/refresh. */
|
|
2283
|
+
groups: z28.array(z28.string()),
|
|
2284
|
+
createdAt: z28.iso.datetime(),
|
|
2285
|
+
/** Null while the handoff is un-redeemed — impossible here (live rows only). */
|
|
2286
|
+
activatedAt: z28.iso.datetime().nullable(),
|
|
2287
|
+
/** When silent re-auth (`prompt=none`) becomes due; past = overdue. */
|
|
2288
|
+
refreshDueAt: z28.iso.datetime(),
|
|
2289
|
+
/** Hard cap — the session is dead at this instant regardless of activity. */
|
|
2290
|
+
expiresAt: z28.iso.datetime()
|
|
2291
|
+
});
|
|
2292
|
+
var SessionListResponseSchema = z28.object({
|
|
2293
|
+
rows: z28.array(SessionSummarySchema),
|
|
2294
|
+
groupNames: z28.record(z28.string(), z28.string().nullable()),
|
|
2295
|
+
groupsResolved: z28.boolean()
|
|
2296
|
+
});
|
|
2297
|
+
var SessionRevokeRequestSchema = z28.object({
|
|
2298
|
+
/**
|
|
2299
|
+
* A session's principal id as seen in the list — the `oid` claim, a `pw_*`
|
|
2300
|
+
* pseudonym, or a pre-re-base pairwise `sub`. Opaque to the admin; copied
|
|
2301
|
+
* from the row they clicked, so its shape is whatever the session rows
|
|
2302
|
+
* hold — bounded only against abuse, not parsed.
|
|
2303
|
+
*/
|
|
2304
|
+
userOid: z28.string().min(1).max(200)
|
|
2305
|
+
});
|
|
2306
|
+
var SessionRevokeResultSchema = z28.object({
|
|
2307
|
+
removed: z28.int().nonnegative(),
|
|
2308
|
+
/** Slugs of the apps the removed sessions lived on; empty when none. */
|
|
2309
|
+
apps: z28.array(z28.string())
|
|
1533
2310
|
});
|
|
1534
2311
|
|
|
1535
2312
|
// src/client.ts
|
|
1536
2313
|
var CliError = class extends Error {
|
|
1537
2314
|
code;
|
|
1538
|
-
|
|
2315
|
+
/** The portal envelope's `details`, when it sent one. */
|
|
2316
|
+
details;
|
|
2317
|
+
constructor(message, code, details) {
|
|
1539
2318
|
super(message);
|
|
1540
2319
|
this.name = "CliError";
|
|
1541
2320
|
this.code = code;
|
|
2321
|
+
this.details = details;
|
|
1542
2322
|
}
|
|
1543
2323
|
};
|
|
1544
|
-
|
|
2324
|
+
function errorReason(err) {
|
|
2325
|
+
if (!(err instanceof CliError)) return void 0;
|
|
2326
|
+
const reason = err.details?.reason;
|
|
2327
|
+
return typeof reason === "string" ? reason : void 0;
|
|
2328
|
+
}
|
|
2329
|
+
var VersionListSchema = z29.array(VersionSchema);
|
|
1545
2330
|
var PortalClient = class {
|
|
1546
2331
|
#baseUrl;
|
|
1547
2332
|
#tokenProvider;
|
|
@@ -1551,12 +2336,50 @@ var PortalClient = class {
|
|
|
1551
2336
|
this.#tokenProvider = typeof token === "string" ? async () => token : token;
|
|
1552
2337
|
}
|
|
1553
2338
|
createApp(input) {
|
|
1554
|
-
return this.#json(AppSchema, "POST", "/api/v1/apps", {
|
|
2339
|
+
return this.#json(AppSchema, "POST", "/api/v1/apps", {
|
|
2340
|
+
auth: true,
|
|
2341
|
+
body: {
|
|
2342
|
+
slug: input.slug,
|
|
2343
|
+
displayName: input.displayName,
|
|
2344
|
+
// Sent only when set, so a description-free create posts exactly the
|
|
2345
|
+
// body it always did — an older portal never sees a new key at all.
|
|
2346
|
+
...input.description !== void 0 ? { description: input.description } : {},
|
|
2347
|
+
...input.visibility !== void 0 ? { visibility: input.visibility } : {}
|
|
2348
|
+
}
|
|
2349
|
+
});
|
|
1555
2350
|
}
|
|
1556
2351
|
/** Public IdP discovery info — how `helix login` finds the issuer. */
|
|
1557
2352
|
getAuthConfig() {
|
|
1558
2353
|
return this.#json(AuthConfigResponseSchema, "GET", "/api/v1/auth/config", {});
|
|
1559
2354
|
}
|
|
2355
|
+
/** Public deployment topology (`GET /api/v1/config`), incl. the CI token audience. */
|
|
2356
|
+
getDeploymentConfig() {
|
|
2357
|
+
return this.#json(DeploymentConfigResponseSchema, "GET", "/api/v1/config", {});
|
|
2358
|
+
}
|
|
2359
|
+
/** The app's GitHub binding (ADR-0051), or null when it has none. */
|
|
2360
|
+
async getRepoBinding(slug) {
|
|
2361
|
+
try {
|
|
2362
|
+
return await this.#json(RepoBindingSchema, "GET", `/api/v1/apps/${enc(slug)}/repo-binding`, {
|
|
2363
|
+
auth: true
|
|
2364
|
+
});
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
if (errorReason(err) === "no_binding") return null;
|
|
2367
|
+
throw err;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
putRepoBinding(slug, body) {
|
|
2371
|
+
return this.#json(RepoBindingSchema, "PUT", `/api/v1/apps/${enc(slug)}/repo-binding`, {
|
|
2372
|
+
auth: true,
|
|
2373
|
+
body
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
async deleteRepoBinding(slug) {
|
|
2377
|
+
const res = await fetch(this.#url(`/api/v1/apps/${enc(slug)}/repo-binding`), {
|
|
2378
|
+
method: "DELETE",
|
|
2379
|
+
headers: await this.#authHeaders(true)
|
|
2380
|
+
});
|
|
2381
|
+
if (!res.ok) await this.#parse(z29.unknown(), res);
|
|
2382
|
+
}
|
|
1560
2383
|
/** The authenticated actor, per the portal — powers `helix whoami`. */
|
|
1561
2384
|
me() {
|
|
1562
2385
|
return this.#json(PortalMeResponseSchema, "GET", "/api/v1/me", { auth: true });
|
|
@@ -1596,8 +2419,22 @@ var PortalClient = class {
|
|
|
1596
2419
|
});
|
|
1597
2420
|
return this.#parse(UploadVersionResponseSchema, res);
|
|
1598
2421
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
2422
|
+
/**
|
|
2423
|
+
* Upload from GitHub Actions to the CI route (ADR-0051), authenticated by the
|
|
2424
|
+
* job's GitHub OIDC token rather than a Helix sign-in.
|
|
2425
|
+
*/
|
|
2426
|
+
async uploadCiVersion(slug, zip, githubToken) {
|
|
2427
|
+
const form = new FormData();
|
|
2428
|
+
form.append("bundle", new Blob([new Uint8Array(zip)]), "bundle.zip");
|
|
2429
|
+
const res = await fetch(this.#url(`/api/v1/apps/${enc(slug)}/ci/versions`), {
|
|
2430
|
+
method: "POST",
|
|
2431
|
+
headers: { authorization: `Bearer ${githubToken}` },
|
|
2432
|
+
body: form
|
|
2433
|
+
});
|
|
2434
|
+
return this.#parse(UploadVersionResponseSchema, res);
|
|
2435
|
+
}
|
|
2436
|
+
#url(path5) {
|
|
2437
|
+
return `${this.#baseUrl}${path5}`;
|
|
1601
2438
|
}
|
|
1602
2439
|
async #authHeaders(auth) {
|
|
1603
2440
|
if (!auth) return {};
|
|
@@ -1607,10 +2444,10 @@ var PortalClient = class {
|
|
|
1607
2444
|
}
|
|
1608
2445
|
return { authorization: `Bearer ${token}` };
|
|
1609
2446
|
}
|
|
1610
|
-
async #json(schema, method,
|
|
2447
|
+
async #json(schema, method, path5, opts) {
|
|
1611
2448
|
const headers = await this.#authHeaders(opts.auth ?? false);
|
|
1612
2449
|
if (opts.body !== void 0) headers["content-type"] = "application/json";
|
|
1613
|
-
const res = await fetch(this.#url(
|
|
2450
|
+
const res = await fetch(this.#url(path5), {
|
|
1614
2451
|
method,
|
|
1615
2452
|
headers,
|
|
1616
2453
|
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
|
|
@@ -1622,20 +2459,26 @@ var PortalClient = class {
|
|
|
1622
2459
|
const data = text ? JSON.parse(text) : void 0;
|
|
1623
2460
|
if (!res.ok) {
|
|
1624
2461
|
const parsed = ApiErrorSchema.safeParse(data);
|
|
1625
|
-
if (parsed.success)
|
|
2462
|
+
if (parsed.success) {
|
|
2463
|
+
const { message, code, details } = parsed.data.error;
|
|
2464
|
+
throw new CliError(message, code, details);
|
|
2465
|
+
}
|
|
1626
2466
|
throw new CliError(`request failed (HTTP ${res.status})`);
|
|
1627
2467
|
}
|
|
1628
2468
|
return schema.parse(data);
|
|
1629
2469
|
}
|
|
1630
2470
|
/** A non-JSON GET (the markdown skill) — errors still typed via the envelope. */
|
|
1631
|
-
async #text(
|
|
2471
|
+
async #text(path5, opts) {
|
|
1632
2472
|
const headers = await this.#authHeaders(opts.auth ?? false);
|
|
1633
|
-
const res = await fetch(this.#url(
|
|
2473
|
+
const res = await fetch(this.#url(path5), { method: "GET", headers });
|
|
1634
2474
|
const body = await res.text();
|
|
1635
2475
|
if (!res.ok) {
|
|
1636
2476
|
const data = body ? safeJsonParse(body) : void 0;
|
|
1637
2477
|
const parsed = ApiErrorSchema.safeParse(data);
|
|
1638
|
-
if (parsed.success)
|
|
2478
|
+
if (parsed.success) {
|
|
2479
|
+
const { message, code, details } = parsed.data.error;
|
|
2480
|
+
throw new CliError(message, code, details);
|
|
2481
|
+
}
|
|
1639
2482
|
throw new CliError(`request failed (HTTP ${res.status})`);
|
|
1640
2483
|
}
|
|
1641
2484
|
return body;
|
|
@@ -1782,6 +2625,28 @@ async function deleteTokens(portalUrl, file = defaultTokenPath()) {
|
|
|
1782
2625
|
return true;
|
|
1783
2626
|
}
|
|
1784
2627
|
|
|
2628
|
+
// src/auth/githubActions.ts
|
|
2629
|
+
import { z as z30 } from "zod";
|
|
2630
|
+
function githubTokenSource(env) {
|
|
2631
|
+
const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
2632
|
+
const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
2633
|
+
return requestUrl && requestToken ? { requestUrl, requestToken } : null;
|
|
2634
|
+
}
|
|
2635
|
+
var IdTokenResponseSchema = z30.object({ value: z30.string().min(1) });
|
|
2636
|
+
async function requestGithubIdToken(source, audience, fetchImpl = fetch) {
|
|
2637
|
+
const url = new URL(source.requestUrl);
|
|
2638
|
+
url.searchParams.set("audience", audience);
|
|
2639
|
+
const res = await fetchImpl(url, {
|
|
2640
|
+
headers: { authorization: `Bearer ${source.requestToken}`, accept: "application/json" }
|
|
2641
|
+
});
|
|
2642
|
+
if (!res.ok) {
|
|
2643
|
+
throw new CliError(`GitHub refused the ID token request (HTTP ${res.status})`);
|
|
2644
|
+
}
|
|
2645
|
+
const parsed = IdTokenResponseSchema.safeParse(await res.json());
|
|
2646
|
+
if (!parsed.success) throw new CliError("GitHub's ID token response had no token");
|
|
2647
|
+
return parsed.data.value;
|
|
2648
|
+
}
|
|
2649
|
+
|
|
1785
2650
|
// src/commands.ts
|
|
1786
2651
|
function requireSlug(config) {
|
|
1787
2652
|
if (!config.slug) {
|
|
@@ -1821,24 +2686,68 @@ function printVersion(v) {
|
|
|
1821
2686
|
}
|
|
1822
2687
|
function printApp(app) {
|
|
1823
2688
|
console.log(` app ${app.slug} \u2014 live version: ${app.currentVersionId ?? "(none)"}`);
|
|
2689
|
+
if (app.description) console.log(` ${app.description}`);
|
|
1824
2690
|
}
|
|
1825
2691
|
async function createCommand(client, config, opts) {
|
|
1826
2692
|
const slug = requireSlug(config);
|
|
1827
2693
|
const app = await client.createApp({
|
|
1828
2694
|
slug,
|
|
1829
2695
|
displayName: opts.displayName ?? slug,
|
|
2696
|
+
// Flag wins over helix.json, and an unset description is simply not sent —
|
|
2697
|
+
// an older portal never sees a key it doesn't know.
|
|
2698
|
+
description: opts.description ?? config.description,
|
|
1830
2699
|
visibility: parseVisibility(opts.visibility)
|
|
1831
2700
|
});
|
|
1832
2701
|
console.log(`Created app "${app.slug}".`);
|
|
1833
2702
|
printApp(app);
|
|
1834
2703
|
}
|
|
1835
|
-
async function deployCommand(client, config, opts) {
|
|
2704
|
+
async function deployCommand(client, config, opts, env = process.env) {
|
|
1836
2705
|
const slug = requireSlug(config);
|
|
2706
|
+
const github = config.token ? null : githubTokenSource(env);
|
|
2707
|
+
if (!config.token && !github && env.GITHUB_ACTIONS === "true") {
|
|
2708
|
+
throw new CliError(
|
|
2709
|
+
"running in GitHub Actions without an ID token; add `permissions: id-token: write` to the workflow (see `helix ci init`)"
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
if (github && !isSafePortalUrl(config.portalUrl)) {
|
|
2713
|
+
throw new CliError(
|
|
2714
|
+
`won't send a GitHub token over plain http (${config.portalUrl}); use the portal's https URL`
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
if (github && opts.promote) {
|
|
2718
|
+
throw new CliError(
|
|
2719
|
+
"CI deploys land as a preview; promote it in the portal or with `helix promote <n>`"
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
1837
2722
|
const zip = config.bundle ? await readFile2(config.bundle) : await zipDirectory(config.dir);
|
|
1838
2723
|
console.log(`Uploading bundle to "${slug}"\u2026`);
|
|
1839
|
-
|
|
2724
|
+
let result;
|
|
2725
|
+
if (github) {
|
|
2726
|
+
const deployment = await client.getDeploymentConfig();
|
|
2727
|
+
if (!deployment.repoDeploy?.enabled) {
|
|
2728
|
+
throw new CliError("this portal does not accept repo-backed deploys (ask its operator)");
|
|
2729
|
+
}
|
|
2730
|
+
const idToken = await requestGithubIdToken(github, deployment.repoDeploy.audience);
|
|
2731
|
+
try {
|
|
2732
|
+
result = await client.uploadCiVersion(slug, zip, idToken);
|
|
2733
|
+
} catch (err) {
|
|
2734
|
+
if (err instanceof CliError && err.code === "forbidden" && !errorReason(err)) {
|
|
2735
|
+
throw new CliError(
|
|
2736
|
+
`${err.message}; check that "${slug}" is connected to this repo (helix ci show)`,
|
|
2737
|
+
err.code
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
throw err;
|
|
2741
|
+
}
|
|
2742
|
+
} else {
|
|
2743
|
+
result = await client.uploadVersion(slug, zip);
|
|
2744
|
+
}
|
|
2745
|
+
const { version, warnings } = result;
|
|
1840
2746
|
console.log(`Uploaded as preview:`);
|
|
1841
2747
|
printVersion(version);
|
|
2748
|
+
if (version.source) {
|
|
2749
|
+
console.log(` from: ${version.source.repoFullName}@${version.source.commitSha.slice(0, 7)}`);
|
|
2750
|
+
}
|
|
1842
2751
|
if (warnings.length > 0) {
|
|
1843
2752
|
console.log(`
|
|
1844
2753
|
${warnings.length} CSP warning(s):`);
|
|
@@ -1854,6 +2763,10 @@ Promoted version ${version.number} to live.`);
|
|
|
1854
2763
|
Not live yet \u2014 promote with: helix promote ${version.number}`);
|
|
1855
2764
|
}
|
|
1856
2765
|
}
|
|
2766
|
+
function isSafePortalUrl(url) {
|
|
2767
|
+
const { protocol, hostname } = new URL(url);
|
|
2768
|
+
return protocol === "https:" || protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(hostname);
|
|
2769
|
+
}
|
|
1857
2770
|
async function versionsCommand(client, config) {
|
|
1858
2771
|
const slug = requireSlug(config);
|
|
1859
2772
|
const versions = await client.listVersions(slug);
|
|
@@ -1889,7 +2802,8 @@ async function loginCommand(client, config) {
|
|
|
1889
2802
|
await writeTokens({ portalUrl: config.portalUrl, issuer }, tokens, void 0, { audience });
|
|
1890
2803
|
const authed = new PortalClient(config.portalUrl, tokens.accessToken);
|
|
1891
2804
|
const me = await authed.me();
|
|
1892
|
-
|
|
2805
|
+
const oid = me.oid ? ` \u2014 oid ${me.oid}` : "";
|
|
2806
|
+
console.log(`Logged in as ${me.name ?? me.sub} (${me.sub})${oid}.`);
|
|
1893
2807
|
}
|
|
1894
2808
|
async function logoutCommand(config) {
|
|
1895
2809
|
const forgot = await deleteTokens(config.portalUrl);
|
|
@@ -1897,7 +2811,8 @@ async function logoutCommand(config) {
|
|
|
1897
2811
|
}
|
|
1898
2812
|
async function whoamiCommand(client) {
|
|
1899
2813
|
const me = await client.me();
|
|
1900
|
-
|
|
2814
|
+
const oid = me.oid ? `${me.oid} \u2014 ` : "";
|
|
2815
|
+
console.log(`${oid}${me.sub} (via ${me.via}${me.name ? `, ${me.name}` : ""})`);
|
|
1901
2816
|
}
|
|
1902
2817
|
async function skillCommand(client, _config, opts) {
|
|
1903
2818
|
const skill = await client.getSkill();
|
|
@@ -1947,6 +2862,12 @@ function makeTokenProvider(opts, deps = defaultDeps) {
|
|
|
1947
2862
|
};
|
|
1948
2863
|
}
|
|
1949
2864
|
|
|
2865
|
+
// src/ci/commands.ts
|
|
2866
|
+
import { execFile } from "node:child_process";
|
|
2867
|
+
import { mkdir as mkdir2, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
2868
|
+
import path4 from "node:path";
|
|
2869
|
+
import { promisify } from "node:util";
|
|
2870
|
+
|
|
1950
2871
|
// src/config.ts
|
|
1951
2872
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
1952
2873
|
import path2 from "node:path";
|
|
@@ -1971,10 +2892,346 @@ async function resolveConfig(flags, env = process.env, cwd = process.cwd()) {
|
|
|
1971
2892
|
token: flags.token ?? env.HELIX_TOKEN ?? env.AZX_TOKEN,
|
|
1972
2893
|
slug: flags.slug ?? file.slug,
|
|
1973
2894
|
dir: flags.dir ?? file.dir ?? DEFAULT_DIR,
|
|
1974
|
-
bundle: flags.bundle
|
|
2895
|
+
bundle: flags.bundle,
|
|
2896
|
+
description: flags.description ?? file.description
|
|
1975
2897
|
};
|
|
1976
2898
|
}
|
|
1977
2899
|
|
|
2900
|
+
// src/version.ts
|
|
2901
|
+
var CLI_VERSION = true ? "0.5.0" : "latest";
|
|
2902
|
+
|
|
2903
|
+
// src/ci/remote.ts
|
|
2904
|
+
function parseGithubRemote(remote, githubHost = "github.com") {
|
|
2905
|
+
const url = remote.trim();
|
|
2906
|
+
const scp = /^[\w.-]+@([^:/]+):(.+)$/.exec(url);
|
|
2907
|
+
let host;
|
|
2908
|
+
let path5;
|
|
2909
|
+
if (scp && !url.includes("://")) {
|
|
2910
|
+
host = scp[1];
|
|
2911
|
+
path5 = scp[2];
|
|
2912
|
+
} else {
|
|
2913
|
+
let parsed;
|
|
2914
|
+
try {
|
|
2915
|
+
parsed = new URL(url);
|
|
2916
|
+
} catch {
|
|
2917
|
+
return null;
|
|
2918
|
+
}
|
|
2919
|
+
if (!["https:", "http:", "ssh:", "git:"].includes(parsed.protocol)) return null;
|
|
2920
|
+
host = parsed.hostname;
|
|
2921
|
+
path5 = parsed.pathname.replace(/^\/+/, "");
|
|
2922
|
+
}
|
|
2923
|
+
if (host.toLowerCase() !== githubHost.toLowerCase()) return null;
|
|
2924
|
+
const match = /^([A-Za-z0-9][A-Za-z0-9-]{0,38})\/([A-Za-z0-9._-]+?)(\.git)?\/?$/.exec(path5);
|
|
2925
|
+
return match ? `${match[1]}/${match[2]}` : null;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
// src/ci/workflow.ts
|
|
2929
|
+
import { access, readFile as readFile4 } from "node:fs/promises";
|
|
2930
|
+
import path3 from "node:path";
|
|
2931
|
+
var REUSABLE_WORKFLOW_PATH = "AZX-PBC-OSS/helix-actions/.github/workflows/deploy.yml";
|
|
2932
|
+
var REUSABLE_WORKFLOW_TAG = "v1";
|
|
2933
|
+
var REUSABLE_WORKFLOW_REF = `${REUSABLE_WORKFLOW_PATH}@refs/tags/${REUSABLE_WORKFLOW_TAG}`;
|
|
2934
|
+
var WORKFLOW_FILE = ".github/workflows/helix-deploy.yml";
|
|
2935
|
+
var DEFAULT_PNPM_VERSION = "10";
|
|
2936
|
+
async function exists(file) {
|
|
2937
|
+
try {
|
|
2938
|
+
await access(file);
|
|
2939
|
+
return true;
|
|
2940
|
+
} catch {
|
|
2941
|
+
return false;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
var LOCKFILES = [
|
|
2945
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
2946
|
+
["yarn.lock", "yarn"],
|
|
2947
|
+
["package-lock.json", "npm"]
|
|
2948
|
+
];
|
|
2949
|
+
var posix = (p) => p.split(path3.sep).join("/");
|
|
2950
|
+
async function readPackageJson(file) {
|
|
2951
|
+
try {
|
|
2952
|
+
return JSON.parse(await readFile4(file, "utf8"));
|
|
2953
|
+
} catch {
|
|
2954
|
+
return null;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
async function detectBuildPlan(appDir, repoRoot) {
|
|
2958
|
+
let packageManager = "npm";
|
|
2959
|
+
let lockfilePath = null;
|
|
2960
|
+
search: for (const dir of [appDir, repoRoot]) {
|
|
2961
|
+
for (const [name, pm] of LOCKFILES) {
|
|
2962
|
+
if (await exists(path3.join(dir, name))) {
|
|
2963
|
+
packageManager = pm;
|
|
2964
|
+
lockfilePath = posix(path3.relative(repoRoot, path3.join(dir, name)));
|
|
2965
|
+
break search;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
const appPkg = await readPackageJson(path3.join(appDir, "package.json"));
|
|
2970
|
+
let pnpmPackageJson = null;
|
|
2971
|
+
if (packageManager === "pnpm") {
|
|
2972
|
+
for (const dir of [appDir, repoRoot]) {
|
|
2973
|
+
const pkg = dir === appDir ? appPkg : await readPackageJson(path3.join(dir, "package.json"));
|
|
2974
|
+
if (typeof pkg?.packageManager === "string" && pkg.packageManager.startsWith("pnpm@")) {
|
|
2975
|
+
pnpmPackageJson = posix(path3.relative(repoRoot, path3.join(dir, "package.json")));
|
|
2976
|
+
break;
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const buildScript = typeof appPkg?.scripts?.build === "string" ? "build" : null;
|
|
2981
|
+
return {
|
|
2982
|
+
packageManager,
|
|
2983
|
+
hasLockfile: lockfilePath !== null,
|
|
2984
|
+
lockfilePath,
|
|
2985
|
+
pnpmPackageJson,
|
|
2986
|
+
buildScript
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
var INSTALL = {
|
|
2990
|
+
npm: "npm ci",
|
|
2991
|
+
pnpm: "pnpm install --frozen-lockfile",
|
|
2992
|
+
yarn: "yarn install --frozen-lockfile"
|
|
2993
|
+
};
|
|
2994
|
+
var HEADER = (branch) => `# Deploys this app to Helix as a preview on every push to ${branch}.
|
|
2995
|
+
# Written by \`helix ci init\`. The repo needs no secret: the deploy job
|
|
2996
|
+
# authenticates with its own GitHub OIDC token. The build job runs the repo's
|
|
2997
|
+
# code without that permission. A preview goes live only when someone promotes
|
|
2998
|
+
# it, in the portal or with \`helix promote <n>\`.
|
|
2999
|
+
name: Deploy to Helix
|
|
3000
|
+
on:
|
|
3001
|
+
push:
|
|
3002
|
+
branches: [${JSON.stringify(branch)}]
|
|
3003
|
+
workflow_dispatch:
|
|
3004
|
+
# Each job asks for only what it needs.
|
|
3005
|
+
permissions: {}
|
|
3006
|
+
`;
|
|
3007
|
+
var inDir = (workingDirectory, rel) => workingDirectory === "." ? rel : `${workingDirectory}/${rel}`;
|
|
3008
|
+
var FIND_OUTPUT = `if [ -f helix.json ]; then cfg=helix.json; elif [ -f azx.json ]; then cfg=azx.json; else echo "::error::no helix.json in this directory"; exit 1; fi
|
|
3009
|
+
echo "config=$cfg" >> "$GITHUB_OUTPUT"
|
|
3010
|
+
echo "dir=$(node -p "require('./' + process.argv[1]).dir || 'dist'" "$cfg")" >> "$GITHUB_OUTPUT"`;
|
|
3011
|
+
function renderStarterWorkflow(opts) {
|
|
3012
|
+
const { plan, workingDirectory: wd } = opts;
|
|
3013
|
+
const sub = wd !== ".";
|
|
3014
|
+
const install = plan.hasLockfile ? INSTALL[plan.packageManager] : "npm install";
|
|
3015
|
+
const pnpmSetup = plan.packageManager !== "pnpm" ? [] : [
|
|
3016
|
+
" - uses: pnpm/action-setup@v4",
|
|
3017
|
+
" with:",
|
|
3018
|
+
plan.pnpmPackageJson ? ` package_json_file: ${plan.pnpmPackageJson}` : ` version: ${DEFAULT_PNPM_VERSION}`
|
|
3019
|
+
];
|
|
3020
|
+
const cache = plan.lockfilePath ? [
|
|
3021
|
+
` cache: ${plan.packageManager}`,
|
|
3022
|
+
` cache-dependency-path: ${plan.lockfilePath}`
|
|
3023
|
+
] : [];
|
|
3024
|
+
const lines = [
|
|
3025
|
+
HEADER(opts.branch),
|
|
3026
|
+
"jobs:",
|
|
3027
|
+
" build:",
|
|
3028
|
+
" runs-on: ubuntu-latest",
|
|
3029
|
+
" permissions:",
|
|
3030
|
+
" contents: read",
|
|
3031
|
+
...sub ? [" defaults:", " run:", ` working-directory: ${JSON.stringify(wd)}`] : [],
|
|
3032
|
+
" steps:",
|
|
3033
|
+
" - uses: actions/checkout@v4",
|
|
3034
|
+
...pnpmSetup,
|
|
3035
|
+
" - uses: actions/setup-node@v4",
|
|
3036
|
+
" with:",
|
|
3037
|
+
" node-version: 24",
|
|
3038
|
+
...cache,
|
|
3039
|
+
` - run: ${install}`,
|
|
3040
|
+
...plan.buildScript ? [` - run: ${plan.packageManager} run ${plan.buildScript}`] : [],
|
|
3041
|
+
" - id: output",
|
|
3042
|
+
" run: |",
|
|
3043
|
+
` ${FIND_OUTPUT}`,
|
|
3044
|
+
" - uses: actions/upload-artifact@v4",
|
|
3045
|
+
" with:",
|
|
3046
|
+
" name: helix-bundle",
|
|
3047
|
+
" path: |",
|
|
3048
|
+
` ${inDir(wd, "${{ steps.output.outputs.config }}")}`,
|
|
3049
|
+
` ${inDir(wd, "${{ steps.output.outputs.dir }}")}`,
|
|
3050
|
+
" include-hidden-files: true",
|
|
3051
|
+
" if-no-files-found: error",
|
|
3052
|
+
" deploy:",
|
|
3053
|
+
" needs: build",
|
|
3054
|
+
" runs-on: ubuntu-latest",
|
|
3055
|
+
" permissions:",
|
|
3056
|
+
" id-token: write",
|
|
3057
|
+
" steps:",
|
|
3058
|
+
" - uses: actions/download-artifact@v4",
|
|
3059
|
+
" with:",
|
|
3060
|
+
" name: helix-bundle",
|
|
3061
|
+
` path: ${JSON.stringify(wd)}`,
|
|
3062
|
+
" - uses: actions/setup-node@v4",
|
|
3063
|
+
" with:",
|
|
3064
|
+
" node-version: 24",
|
|
3065
|
+
` - run: npx --yes @azx-pbc/helix-cli@${opts.cliVersion} deploy`,
|
|
3066
|
+
...sub ? [` working-directory: ${JSON.stringify(wd)}`] : [],
|
|
3067
|
+
""
|
|
3068
|
+
];
|
|
3069
|
+
return lines.join("\n");
|
|
3070
|
+
}
|
|
3071
|
+
function renderReusableWorkflow(opts) {
|
|
3072
|
+
const { plan } = opts;
|
|
3073
|
+
const lines = [
|
|
3074
|
+
HEADER(opts.branch),
|
|
3075
|
+
"jobs:",
|
|
3076
|
+
" deploy:",
|
|
3077
|
+
` uses: ${REUSABLE_WORKFLOW_PATH}@${REUSABLE_WORKFLOW_TAG}`,
|
|
3078
|
+
// A caller grants the reusable workflow at most these; its own jobs narrow
|
|
3079
|
+
// them per job.
|
|
3080
|
+
" permissions:",
|
|
3081
|
+
" contents: read",
|
|
3082
|
+
" id-token: write",
|
|
3083
|
+
" with:",
|
|
3084
|
+
` package-manager: ${plan.packageManager}`,
|
|
3085
|
+
` working-directory: ${JSON.stringify(opts.workingDirectory)}`,
|
|
3086
|
+
` build-script: ${JSON.stringify(plan.buildScript ?? "")}`,
|
|
3087
|
+
...plan.packageManager === "pnpm" && !plan.pnpmPackageJson ? [` pnpm-version: ${JSON.stringify(DEFAULT_PNPM_VERSION)}`] : [],
|
|
3088
|
+
` cli-version: ${JSON.stringify(opts.cliVersion)}`,
|
|
3089
|
+
""
|
|
3090
|
+
];
|
|
3091
|
+
return lines.join("\n");
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
// src/ci/commands.ts
|
|
3095
|
+
var run = promisify(execFile);
|
|
3096
|
+
var defaultGit = async (args, cwd) => (await run("git", args, { cwd })).stdout.trim();
|
|
3097
|
+
function requireSlug2(config) {
|
|
3098
|
+
if (!config.slug) throw new CliError("no app slug; set it in helix.json or pass --slug");
|
|
3099
|
+
return config.slug;
|
|
3100
|
+
}
|
|
3101
|
+
async function ciInitCommand(client, config, opts, deps = {}) {
|
|
3102
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
3103
|
+
const git = deps.git ?? defaultGit;
|
|
3104
|
+
const log = deps.log ?? console.log;
|
|
3105
|
+
const slug = requireSlug2(config);
|
|
3106
|
+
const deployment = await client.getDeploymentConfig();
|
|
3107
|
+
if (!deployment.repoDeploy?.enabled) {
|
|
3108
|
+
throw new CliError("this portal does not accept repo-backed deploys (ask its operator)");
|
|
3109
|
+
}
|
|
3110
|
+
const githubHost = new URL(deployment.repoDeploy.serverUrl).hostname;
|
|
3111
|
+
let repoRoot;
|
|
3112
|
+
try {
|
|
3113
|
+
repoRoot = await git(["rev-parse", "--show-toplevel"], cwd);
|
|
3114
|
+
} catch {
|
|
3115
|
+
throw new CliError("not inside a git checkout; run this from the app's directory in its repo");
|
|
3116
|
+
}
|
|
3117
|
+
const repo = opts.repo ?? parseGithubRemote(await originUrl(git, cwd), githubHost);
|
|
3118
|
+
if (!repo) {
|
|
3119
|
+
throw new CliError(
|
|
3120
|
+
`couldn't read a ${githubHost} repo from the origin remote; pass --repo owner/name`
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
const branch = opts.branch ?? await defaultBranch(git, cwd);
|
|
3124
|
+
if (!branch || branch === "HEAD") {
|
|
3125
|
+
throw new CliError("couldn't tell which branch to deploy from; pass --branch <name>");
|
|
3126
|
+
}
|
|
3127
|
+
const workflowPath = path4.join(repoRoot, WORKFLOW_FILE);
|
|
3128
|
+
if (!opts.force && await fileExists(workflowPath)) {
|
|
3129
|
+
throw new CliError(`${WORKFLOW_FILE} already exists; pass --force to replace it`);
|
|
3130
|
+
}
|
|
3131
|
+
const workingDirectory = path4.relative(repoRoot, cwd).split(path4.sep).join("/") || ".";
|
|
3132
|
+
const plan = await detectBuildPlan(cwd, repoRoot);
|
|
3133
|
+
const render = opts.reusable ? renderReusableWorkflow : renderStarterWorkflow;
|
|
3134
|
+
const workflow = render({ branch, workingDirectory, plan, cliVersion: CLI_VERSION });
|
|
3135
|
+
const existing = await client.getRepoBinding(slug);
|
|
3136
|
+
const workflowRef = opts.reusable ? REUSABLE_WORKFLOW_REF : existing?.workflowRef;
|
|
3137
|
+
const environment = existing?.environment;
|
|
3138
|
+
if (existing?.workflowRef && workflowRef !== existing.workflowRef && !opts.force) {
|
|
3139
|
+
throw new CliError(
|
|
3140
|
+
`the binding pins deploys to workflow ${existing.workflowRef}; --reusable would replace that pin. Pass --force to replace it.`
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
const binding = await client.putRepoBinding(slug, {
|
|
3144
|
+
repoFullName: repo,
|
|
3145
|
+
branch,
|
|
3146
|
+
...workflowRef ? { workflowRef } : {},
|
|
3147
|
+
...environment ? { environment } : {}
|
|
3148
|
+
});
|
|
3149
|
+
const configFile = await writeHelixJson(cwd, {
|
|
3150
|
+
slug,
|
|
3151
|
+
portalUrl: config.portalUrl,
|
|
3152
|
+
dir: config.dir
|
|
3153
|
+
});
|
|
3154
|
+
await mkdir2(path4.dirname(workflowPath), { recursive: true });
|
|
3155
|
+
await writeFile3(workflowPath, workflow, "utf8");
|
|
3156
|
+
log(`Connected "${slug}" to ${repo}.`);
|
|
3157
|
+
log(`Branch: ${branch}${opts.branch ? "" : " (use --branch to change)"}`);
|
|
3158
|
+
printBinding(binding, log);
|
|
3159
|
+
log(`Wrote ${path4.relative(cwd, workflowPath) || WORKFLOW_FILE} and ${configFile}.`);
|
|
3160
|
+
if (isLocalPortal(config.portalUrl)) {
|
|
3161
|
+
log(`warning: helix.json points CI at ${config.portalUrl}, which a GitHub runner can't reach.`);
|
|
3162
|
+
}
|
|
3163
|
+
log("Commit both files and push; each push lands a preview to promote.");
|
|
3164
|
+
}
|
|
3165
|
+
async function ciShowCommand(client, config, log = console.log) {
|
|
3166
|
+
const slug = requireSlug2(config);
|
|
3167
|
+
const binding = await client.getRepoBinding(slug);
|
|
3168
|
+
if (!binding) {
|
|
3169
|
+
log(`"${slug}" is not connected to a repo. Run: helix ci init`);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
printBinding(binding, log);
|
|
3173
|
+
}
|
|
3174
|
+
async function ciRemoveCommand(client, config, log = console.log) {
|
|
3175
|
+
const slug = requireSlug2(config);
|
|
3176
|
+
await client.deleteRepoBinding(slug);
|
|
3177
|
+
log(`Disconnected "${slug}" from its repo. CI deploys to it are now refused.`);
|
|
3178
|
+
}
|
|
3179
|
+
function printBinding(b, log) {
|
|
3180
|
+
log(` repo: ${b.repoFullName}`);
|
|
3181
|
+
log(` branch: ${b.branch}`);
|
|
3182
|
+
if (b.workflowRef) log(` workflow: ${b.workflowRef}`);
|
|
3183
|
+
if (b.environment) log(` environment: ${b.environment}`);
|
|
3184
|
+
log(
|
|
3185
|
+
b.repositoryId ? ` pinned: repo id ${b.repositoryId}, owner id ${b.repositoryOwnerId ?? "?"}` : " pinned: not yet (the first deploy pins the repo's ids)"
|
|
3186
|
+
);
|
|
3187
|
+
}
|
|
3188
|
+
async function originUrl(git, cwd) {
|
|
3189
|
+
try {
|
|
3190
|
+
return await git(["remote", "get-url", "origin"], cwd);
|
|
3191
|
+
} catch {
|
|
3192
|
+
return "";
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
async function defaultBranch(git, cwd) {
|
|
3196
|
+
try {
|
|
3197
|
+
const remoteHead = await git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], cwd);
|
|
3198
|
+
if (remoteHead.startsWith("origin/")) return remoteHead.slice("origin/".length);
|
|
3199
|
+
} catch {
|
|
3200
|
+
}
|
|
3201
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
3202
|
+
}
|
|
3203
|
+
async function fileExists(file) {
|
|
3204
|
+
try {
|
|
3205
|
+
await readFile5(file);
|
|
3206
|
+
return true;
|
|
3207
|
+
} catch {
|
|
3208
|
+
return false;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
async function writeHelixJson(cwd, values) {
|
|
3212
|
+
for (const name2 of CONFIG_FILENAMES) {
|
|
3213
|
+
const file = path4.join(cwd, name2);
|
|
3214
|
+
let existing;
|
|
3215
|
+
try {
|
|
3216
|
+
existing = JSON.parse(await readFile5(file, "utf8"));
|
|
3217
|
+
} catch (err) {
|
|
3218
|
+
if (err.code === "ENOENT") continue;
|
|
3219
|
+
throw err;
|
|
3220
|
+
}
|
|
3221
|
+
await writeFile3(file, `${JSON.stringify({ ...existing, ...values }, null, 2)}
|
|
3222
|
+
`, "utf8");
|
|
3223
|
+
return name2;
|
|
3224
|
+
}
|
|
3225
|
+
const name = CONFIG_FILENAMES[0];
|
|
3226
|
+
await writeFile3(path4.join(cwd, name), `${JSON.stringify(values, null, 2)}
|
|
3227
|
+
`, "utf8");
|
|
3228
|
+
return name;
|
|
3229
|
+
}
|
|
3230
|
+
function isLocalPortal(url) {
|
|
3231
|
+
const host = new URL(url).hostname;
|
|
3232
|
+
return host === "localhost" || host === "127.0.0.1" || host.endsWith(".local") || host === "::1";
|
|
3233
|
+
}
|
|
3234
|
+
|
|
1978
3235
|
// src/bin.ts
|
|
1979
3236
|
var USAGE = `helix \u2014 Helix deploy CLI
|
|
1980
3237
|
|
|
@@ -1983,15 +3240,19 @@ Usage:
|
|
|
1983
3240
|
helix logout
|
|
1984
3241
|
helix whoami
|
|
1985
3242
|
helix deploy [--dir <dir>] [--bundle <zip>] [--promote]
|
|
1986
|
-
helix create [--display-name <name>] [--visibility <v>]
|
|
3243
|
+
helix create [--display-name <name>] [--description <text>] [--visibility <v>]
|
|
1987
3244
|
helix versions
|
|
1988
3245
|
helix promote <number>
|
|
1989
3246
|
helix rollback [number]
|
|
1990
3247
|
helix skill [--path <file>] # write this deployment's agent skill to disk
|
|
3248
|
+
helix ci init [--repo <owner/name>] [--branch <b>] [--reusable] [--force]
|
|
3249
|
+
# connect the app to its GitHub repo and write the workflow
|
|
3250
|
+
helix ci show | helix ci remove
|
|
1991
3251
|
|
|
1992
3252
|
Common flags: --slug <slug> --portal-url <url> --token <token>
|
|
1993
|
-
Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login;
|
|
1994
|
-
|
|
3253
|
+
Env: HELIX_PORTAL_URL, HELIX_TOKEN (static token \u2014 skips login; local scripts).
|
|
3254
|
+
In GitHub Actions, deploy uses the job's OIDC token (permissions: id-token: write).
|
|
3255
|
+
Config file: helix.json { slug, portalUrl, dir, description }
|
|
1995
3256
|
Visibility: internal | group:<id>[,<id>...] | password | public
|
|
1996
3257
|
`;
|
|
1997
3258
|
async function main() {
|
|
@@ -2031,9 +3292,28 @@ async function main() {
|
|
|
2031
3292
|
case "create":
|
|
2032
3293
|
await createCommand(client, config, {
|
|
2033
3294
|
displayName: values["display-name"],
|
|
3295
|
+
description: values.description ?? config.description,
|
|
2034
3296
|
visibility: values.visibility
|
|
2035
3297
|
});
|
|
2036
3298
|
break;
|
|
3299
|
+
case "ci": {
|
|
3300
|
+
const sub = positionals[1];
|
|
3301
|
+
if (sub === "init") {
|
|
3302
|
+
await ciInitCommand(client, config, {
|
|
3303
|
+
repo: values.repo,
|
|
3304
|
+
branch: values.branch,
|
|
3305
|
+
reusable: values.reusable,
|
|
3306
|
+
force: values.force
|
|
3307
|
+
});
|
|
3308
|
+
} else if (sub === "show") {
|
|
3309
|
+
await ciShowCommand(client, config);
|
|
3310
|
+
} else if (sub === "remove") {
|
|
3311
|
+
await ciRemoveCommand(client, config);
|
|
3312
|
+
} else {
|
|
3313
|
+
throw new CliError("usage: helix ci init | show | remove");
|
|
3314
|
+
}
|
|
3315
|
+
break;
|
|
3316
|
+
}
|
|
2037
3317
|
case "versions":
|
|
2038
3318
|
await versionsCommand(client, config);
|
|
2039
3319
|
break;
|