@vxil/cli 0.33.0 → 0.35.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/dist/_vxil-feature-configs-chatWebhook.d.ts +16 -0
- package/dist/_vxil-feature-configs-cmsRules.d.ts +1 -1
- package/dist/_vxil-feature-configs-hooks.d.ts +20 -0
- package/dist/_vxil-feature-configs.d.ts +5 -1
- package/dist/config.d.ts +9 -0
- package/dist/vxil-main.js +223 -45
- package/package.json +3 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** ALERT-CHAT-DEST: the secret-name grammar of `webhooks.alerts.chatWebhook.secret`
|
|
2
|
+
* (the secret store's own SECRET_NAME_RE, optionally `secret:`-prefixed like
|
|
3
|
+
* every other *Ref). */
|
|
4
|
+
export declare const CHAT_WEBHOOK_SECRET_PATTERN = "^(secret:)?[a-z0-9_]{1,64}$";
|
|
5
|
+
/** ALERT-CHAT-DEST: where a failure-alert chat webhook may point. EXACT hosts,
|
|
6
|
+
* plus two Microsoft Teams Workflows suffixes (each tenant gets its own
|
|
7
|
+
* sub-domain there). Nothing else: the digest cron POSTs from vxil's own
|
|
8
|
+
* network, so an open URL would be an SSRF / exfiltration door. */
|
|
9
|
+
export declare const CHAT_WEBHOOK_HOSTS: readonly string[];
|
|
10
|
+
export declare const CHAT_WEBHOOK_HOST_SUFFIXES: readonly string[];
|
|
11
|
+
/** ALERT-CHAT-DEST: null when `raw` is a chat webhook URL the digest may POST
|
|
12
|
+
* to, else a short reason. https only, default port, no credentials in the
|
|
13
|
+
* authority, host on the allowlist. PURE — shared by the digest cron (the
|
|
14
|
+
* enforcing check) and the dashboard (an early hint before the value is
|
|
15
|
+
* stored write-only). */
|
|
16
|
+
export declare function chatWebhookUrlError(raw: string): string | null;
|
|
@@ -30,7 +30,7 @@ export declare const CmsRuleParamsSchema: import("@sinclair/typebox").TObject<{
|
|
|
30
30
|
}>;
|
|
31
31
|
export declare const CmsRuleEntrySchema: import("@sinclair/typebox").TObject<{
|
|
32
32
|
app: import("@sinclair/typebox").TString;
|
|
33
|
-
kind: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"
|
|
33
|
+
kind: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"owner_only_edit" | "archive_by_role" | "state_flow" | "stamp_actor" | "create_by_role" | "role_gate" | "no_self_decision" | "capacity" | "no_overlap">[]>;
|
|
34
34
|
collection: import("@sinclair/typebox").TString;
|
|
35
35
|
params: import("@sinclair/typebox").TObject<{
|
|
36
36
|
field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
@@ -224,4 +224,24 @@ export declare function highlightTokens(src: string): Array<{
|
|
|
224
224
|
value: string;
|
|
225
225
|
kind: HlKind;
|
|
226
226
|
}>;
|
|
227
|
+
/** = control-plane `appHookOwnership.APP_HOOK_RE` (the App builder ownership
|
|
228
|
+
* marker `app_` + an app id + `_`; parity pinned by the control-plane
|
|
229
|
+
* appgenDoors test). Only a name carrying a real App builder app id. */
|
|
230
|
+
export declare const APP_OWNED_HOOK_RE: RegExp;
|
|
231
|
+
/** The App builder rule kinds whose DELETE is not free for an end user
|
|
232
|
+
* (= the non-'free' keys of control-plane `appgenDoors.DELETE_SEMANTICS`;
|
|
233
|
+
* parity pinned by the control-plane appgenDoors test). appgenRules names a
|
|
234
|
+
* rule's hooks `<stem><collection>_<kind>[…]`. */
|
|
235
|
+
export declare const APP_DELETE_GUARDED_RULE_KINDS: readonly ["owner_only_edit", "archive_by_role", "state_flow"];
|
|
236
|
+
/** The collections that carry an enabled App builder hook of a
|
|
237
|
+
* delete-guarded rule kind. An END-USER cascade (cascade / set_null) into one
|
|
238
|
+
* of them is refused by cms runCascade — the platform twin of the app proxy's
|
|
239
|
+
* D5 parent-delete refusal (a guard on the child list cannot be bypassed by
|
|
240
|
+
* deleting its parent). Structural: the hook NAME the App builder compiler
|
|
241
|
+
* writes, never user text. A rule that compiles no write check (archive_by_role
|
|
242
|
+
* on the proxy lane) carries the always-true `<list>_archive_by_role_delete_guard`
|
|
243
|
+
* marker (control-plane appgenRules.withDeleteGuardMarkers), so this set equals
|
|
244
|
+
* the lists the proxy's appgenDoors.doorsOf guards (parity pinned in the
|
|
245
|
+
* control-plane appgenDoors test). Pure. */
|
|
246
|
+
export declare function appDeleteGuardedCollections(hooks: Record<string, HookDef> | undefined): ReadonlySet<string>;
|
|
227
247
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Static, type TSchema } from '@sinclair/typebox';
|
|
2
2
|
import { ACTIVITY_FEED_BUILTIN_GROUPS } from './_vxil-feature-configs-feedGroups.js';
|
|
3
|
+
export { CHAT_WEBHOOK_HOSTS, CHAT_WEBHOOK_HOST_SUFFIXES, CHAT_WEBHOOK_SECRET_PATTERN, chatWebhookUrlError } from './_vxil-feature-configs-chatWebhook.js';
|
|
3
4
|
export { AI_THINKING_LEVELS, geminiThinkingLevelError, minimalThinkingLevel, type GeminiThinkingLevel } from './_vxil-feature-configs-geminiThinking.js';
|
|
4
5
|
export * from './_vxil-feature-configs-hooks.js';
|
|
5
6
|
export * from './_vxil-feature-configs-readmodels.js';
|
|
@@ -440,6 +441,9 @@ export declare const WebhooksConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
440
441
|
enabled: import("@sinclair/typebox").TBoolean;
|
|
441
442
|
minLevel: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"warn">, import("@sinclair/typebox").TLiteral<"error">]>;
|
|
442
443
|
digestMinutes: import("@sinclair/typebox").TInteger;
|
|
444
|
+
chatWebhook: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
|
|
445
|
+
secret: import("@sinclair/typebox").TString;
|
|
446
|
+
}>>;
|
|
443
447
|
}>>;
|
|
444
448
|
subscriptions: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
445
449
|
target_url: import("@sinclair/typebox").TString;
|
|
@@ -511,7 +515,7 @@ export declare const CmsConfigSchema: import("@sinclair/typebox").TObject<{
|
|
|
511
515
|
}>>>;
|
|
512
516
|
rules: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
|
|
513
517
|
app: import("@sinclair/typebox").TString;
|
|
514
|
-
kind: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"
|
|
518
|
+
kind: import("@sinclair/typebox").TUnion<import("@sinclair/typebox").TLiteral<"owner_only_edit" | "archive_by_role" | "state_flow" | "stamp_actor" | "create_by_role" | "role_gate" | "no_self_decision" | "capacity" | "no_overlap">[]>;
|
|
515
519
|
collection: import("@sinclair/typebox").TString;
|
|
516
520
|
params: import("@sinclair/typebox").TObject<{
|
|
517
521
|
field: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
package/dist/config.d.ts
CHANGED
|
@@ -136,6 +136,15 @@ export interface CollectionDef {
|
|
|
136
136
|
* a config leaf) and carried by BOTH push paths; widening (clearing, or
|
|
137
137
|
* adding a role) needs `vxil push --allow-destructive`. */
|
|
138
138
|
writeRoles?: string[];
|
|
139
|
+
/** W39-3: role slugs whose VERIFIED holders MANAGE this owned collection —
|
|
140
|
+
* they read every row (get / list / count) and may update another member's
|
|
141
|
+
* row through the single-item PATCH; the collection's hooks still decide
|
|
142
|
+
* which fields (every other door stays owner-scoped). Needs `ownerField`
|
|
143
|
+
* and an end-user-writable `endUserAccess`. Absent / `[]` = none. Up to 16
|
|
144
|
+
* slugs. Carried by BOTH push paths; adding a role widens reads and needs
|
|
145
|
+
* `vxil push --allow-destructive`. The App builder sets `['app_manager']`
|
|
146
|
+
* on its members apps' own-only lists while the app names managers. */
|
|
147
|
+
managerRoles?: string[];
|
|
139
148
|
/** Retention in days (guide ch. 4): items created more than this many days
|
|
140
149
|
* ago are soft-deleted by the nightly sweep (recoverable for 30 days). An
|
|
141
150
|
* integer 1..3650, or `null` to keep items until deleted. ABSENT = not
|
package/dist/vxil-main.js
CHANGED
|
@@ -5199,6 +5199,15 @@ var init_feedGroups = __esm({
|
|
|
5199
5199
|
}
|
|
5200
5200
|
});
|
|
5201
5201
|
|
|
5202
|
+
// ../feature-configs/src/chatWebhook.ts
|
|
5203
|
+
var CHAT_WEBHOOK_SECRET_PATTERN;
|
|
5204
|
+
var init_chatWebhook = __esm({
|
|
5205
|
+
"../feature-configs/src/chatWebhook.ts"() {
|
|
5206
|
+
"use strict";
|
|
5207
|
+
CHAT_WEBHOOK_SECRET_PATTERN = "^(secret:)?[a-z0-9_]{1,64}$";
|
|
5208
|
+
}
|
|
5209
|
+
});
|
|
5210
|
+
|
|
5202
5211
|
// ../feature-configs/src/canonicalJson.ts
|
|
5203
5212
|
function canonicalJson(v) {
|
|
5204
5213
|
if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x) ?? "null").join(",")}]`;
|
|
@@ -5324,6 +5333,8 @@ var init_src = __esm({
|
|
|
5324
5333
|
init_publicAssets();
|
|
5325
5334
|
init_geminiThinking();
|
|
5326
5335
|
init_feedGroups();
|
|
5336
|
+
init_chatWebhook();
|
|
5337
|
+
init_chatWebhook();
|
|
5327
5338
|
init_cmsRules();
|
|
5328
5339
|
init_geminiThinking();
|
|
5329
5340
|
init_hooks();
|
|
@@ -6074,7 +6085,20 @@ var init_src = __esm({
|
|
|
6074
6085
|
alerts: Type.Optional(Type.Object({
|
|
6075
6086
|
enabled: Type.Boolean({ default: false }),
|
|
6076
6087
|
minLevel: Type.Union([Type.Literal("warn"), Type.Literal("error")], { default: "error" }),
|
|
6077
|
-
digestMinutes: Type.Integer({ default: 15, minimum: 0, maximum: 1440 })
|
|
6088
|
+
digestMinutes: Type.Integer({ default: 15, minimum: 0, maximum: 1440 }),
|
|
6089
|
+
// ALERT-CHAT-DEST (w41, founder decision D 2026-10-30): a chat webhook
|
|
6090
|
+
// (Google Chat / Slack / Teams Workflows) that receives the SAME digest as
|
|
6091
|
+
// a `{ text }` POST, beside the owner e-mail. The URL embeds the channel's
|
|
6092
|
+
// key/token, so it is NEVER a config value: `secret` NAMES a tenant secret
|
|
6093
|
+
// stored under feature 'webhooks' (PUT /v1/secrets/webhooks/<name>,
|
|
6094
|
+
// envelope-encrypted under KEK_WEBHOOKS), resolved only by the digest cron.
|
|
6095
|
+
// The host is checked against CHAT_WEBHOOK_HOSTS at send time (the value
|
|
6096
|
+
// is write-only, so it cannot be checked here). Optional, NO default —
|
|
6097
|
+
// existing manifests fold byte-identically. Inside the alerts bag, so the
|
|
6098
|
+
// leaf count is unchanged.
|
|
6099
|
+
chatWebhook: Type.Optional(Type.Object({
|
|
6100
|
+
secret: Type.String({ pattern: CHAT_WEBHOOK_SECRET_PATTERN })
|
|
6101
|
+
}))
|
|
6078
6102
|
})),
|
|
6079
6103
|
// DECLARED API STATE (2026-09-23): the tenant's OUTBOUND
|
|
6080
6104
|
// subscriptions as config. Keyed by `target_url` — the only stable identity a
|
|
@@ -7246,7 +7270,7 @@ function formatRulesSummary(s, needsAck = false) {
|
|
|
7246
7270
|
if (!s.added.length && !s.removed.length) return " rules (server summary): no rule added or removed";
|
|
7247
7271
|
return [
|
|
7248
7272
|
" rules (server summary):",
|
|
7249
|
-
...s.added.
|
|
7273
|
+
...s.added.flatMap((l) => [` + ${oneLine(l.sentence)} ${enforcedTag(l)}`, ...unwrittenLine(l)]),
|
|
7250
7274
|
...s.removed.map((l) => ` - ${oneLine(l.sentence)} ${enforcedTag(l)}${needsAck ? NEEDS_ACK : ""}`)
|
|
7251
7275
|
].join("\n");
|
|
7252
7276
|
}
|
|
@@ -7308,7 +7332,7 @@ ${tail}`;
|
|
|
7308
7332
|
return `${head}export default defineConfig(${marked.slice(0, at)}${block}${marked.slice(at + needle.length)});
|
|
7309
7333
|
${tail}`;
|
|
7310
7334
|
}
|
|
7311
|
-
var isObj, isLine, oneLine, enforcedTag, NEEDS_ACK, RULES_MARK;
|
|
7335
|
+
var isObj, isLine, oneLine, enforcedTag, NEEDS_ACK, unwrittenLine, RULES_MARK;
|
|
7312
7336
|
var init_cmsRulesView = __esm({
|
|
7313
7337
|
"src/cmsRulesView.ts"() {
|
|
7314
7338
|
"use strict";
|
|
@@ -7319,6 +7343,10 @@ var init_cmsRulesView = __esm({
|
|
|
7319
7343
|
oneLine = (s) => s.replace(/[\r\n\u2028\u2029]+/g, " ").trim();
|
|
7320
7344
|
enforcedTag = (l) => l.enforced_by === "server" ? "[enforced_by: server]" : `[enforced_by: app \u2014 only writes through ${oneLine(l.app)}]`;
|
|
7321
7345
|
NEEDS_ACK = " (needs --allow-rule-removal)";
|
|
7346
|
+
unwrittenLine = (l) => {
|
|
7347
|
+
const keys = Array.isArray(l.unwritten_keys) ? l.unwritten_keys.filter((k) => typeof k === "string").map(oneLine) : [];
|
|
7348
|
+
return keys.length ? [` ! ${oneLine(l.app)}'s page does not send ${keys.join(", ")}: the app refuses those creates until it does (fix the page in App builder)`] : [];
|
|
7349
|
+
};
|
|
7322
7350
|
RULES_MARK = "\0vxil-pull-rules\0";
|
|
7323
7351
|
}
|
|
7324
7352
|
});
|
|
@@ -7601,7 +7629,7 @@ var init_src3 = __esm({
|
|
|
7601
7629
|
// Dedicated write scope for the BYO-secret path. PUT /v1/secrets/:feature/:name
|
|
7602
7630
|
// requires THIS (or 'admin') as of 2026-07-17 ('features:write' is no
|
|
7603
7631
|
// longer secret-write-equivalent). GET/DELETE /v1/secrets still also accept
|
|
7604
|
-
// 'features:write'.
|
|
7632
|
+
// 'features:write'; GET also accepts 'features:read' (names + ref only, 2026-10-30).
|
|
7605
7633
|
"secrets:write",
|
|
7606
7634
|
"users:read",
|
|
7607
7635
|
"users:write",
|
|
@@ -7776,6 +7804,7 @@ var init_src3 = __esm({
|
|
|
7776
7804
|
var functions_exports = {};
|
|
7777
7805
|
__export(functions_exports, {
|
|
7778
7806
|
BUNDLE_HEADROOM_WARN_RATIO: () => BUNDLE_HEADROOM_WARN_RATIO,
|
|
7807
|
+
DEPLOY_ENTITLEMENT_NOT_PROBED: () => DEPLOY_ENTITLEMENT_NOT_PROBED,
|
|
7779
7808
|
FN_LIMIT_BOUNDS: () => FN_LIMIT_BOUNDS,
|
|
7780
7809
|
MAX_FN_BINDINGS: () => MAX_FN_BINDINGS,
|
|
7781
7810
|
assertBundleContained: () => assertBundleContained,
|
|
@@ -7787,6 +7816,7 @@ __export(functions_exports, {
|
|
|
7787
7816
|
describeBinding: () => describeBinding,
|
|
7788
7817
|
describeRuntime: () => describeRuntime,
|
|
7789
7818
|
diffBindings: () => diffBindings,
|
|
7819
|
+
entitlementNotProbed: () => entitlementNotProbed,
|
|
7790
7820
|
formatBundleTooLarge: () => formatBundleTooLarge,
|
|
7791
7821
|
formatFnChanges: () => formatFnChanges,
|
|
7792
7822
|
formatFunctionsPreflight: () => formatFunctionsPreflight,
|
|
@@ -8292,6 +8322,9 @@ async function probeFunctionsEntitlement(api, name) {
|
|
|
8292
8322
|
}
|
|
8293
8323
|
return { entitled: true };
|
|
8294
8324
|
}
|
|
8325
|
+
function entitlementNotProbed(e) {
|
|
8326
|
+
return !e.entitled && e.status === 403 && e.code === "insufficient_scope";
|
|
8327
|
+
}
|
|
8295
8328
|
function billingPageFor(tenantId, baseUrl) {
|
|
8296
8329
|
const tid = tenantId && tenantId !== "(unknown)" ? tenantId : "<tenant>";
|
|
8297
8330
|
const path = `/dashboard/${tid}/billing`;
|
|
@@ -8303,6 +8336,10 @@ function billingPageFor(tenantId, baseUrl) {
|
|
|
8303
8336
|
}
|
|
8304
8337
|
function formatFunctionsPreflight(e, opts) {
|
|
8305
8338
|
const n = `${opts.declared} declared function(s)`;
|
|
8339
|
+
if (entitlementNotProbed(e)) {
|
|
8340
|
+
if (opts.verb === "plan") return DEPLOY_ENTITLEMENT_NOT_PROBED.trim();
|
|
8341
|
+
return `functions: the pushing key lacks features:write (insufficient_scope${e.message ? ` \u2014 ${e.message}` : ""}); ${n} would be refused. Push with a key that holds features:write, or run \`vxil push --skip-functions\` to push everything else.`;
|
|
8342
|
+
}
|
|
8306
8343
|
if (e.code === "sandbox_restricted" || e.status === 403) {
|
|
8307
8344
|
return `functions: this project cannot deploy functions (${e.code ?? e.status}${e.message ? ` \u2014 ${e.message}` : ""}); ${n} would be refused.${e.hint ? ` ${e.hint}` : ""}` + (opts.verb === "push" ? " Run `vxil push --skip-functions` to push everything else." : "");
|
|
8308
8345
|
}
|
|
@@ -8330,7 +8367,7 @@ function formatLogLines(lines) {
|
|
|
8330
8367
|
}
|
|
8331
8368
|
return out.length ? out.join("\n") : " (no log lines)";
|
|
8332
8369
|
}
|
|
8333
|
-
var WIRING_WARNING, WORKSPACE_ROOTS, BUNDLE_HEADROOM_WARN_RATIO;
|
|
8370
|
+
var WIRING_WARNING, WORKSPACE_ROOTS, BUNDLE_HEADROOM_WARN_RATIO, DEPLOY_ENTITLEMENT_NOT_PROBED;
|
|
8334
8371
|
var init_functions = __esm({
|
|
8335
8372
|
"src/functions.ts"() {
|
|
8336
8373
|
"use strict";
|
|
@@ -8341,6 +8378,7 @@ var init_functions = __esm({
|
|
|
8341
8378
|
WIRING_WARNING = /trigger subscription|cron schedule/i;
|
|
8342
8379
|
WORKSPACE_ROOTS = /* @__PURE__ */ new Map();
|
|
8343
8380
|
BUNDLE_HEADROOM_WARN_RATIO = 0.8;
|
|
8381
|
+
DEPLOY_ENTITLEMENT_NOT_PROBED = " ! not probed: deploy entitlement (needs features:write)";
|
|
8344
8382
|
}
|
|
8345
8383
|
});
|
|
8346
8384
|
|
|
@@ -8731,7 +8769,7 @@ function applyEnvelope(r, what) {
|
|
|
8731
8769
|
}
|
|
8732
8770
|
return r.body.data;
|
|
8733
8771
|
}
|
|
8734
|
-
async function serverApply({ api, bundle, resume, allowDestructive, allowRuleRemoval, onPlan }) {
|
|
8772
|
+
async function serverApply({ api, bundle, resume, allowDestructive, allowRuleRemoval, onPlan, poll }) {
|
|
8735
8773
|
if (resume) {
|
|
8736
8774
|
return applyEnvelope(await api("POST", "/v1/apply", { apply_id: resume, mode: "apply" }), "apply resume");
|
|
8737
8775
|
}
|
|
@@ -8746,10 +8784,44 @@ async function serverApply({ api, bundle, resume, allowDestructive, allowRuleRem
|
|
|
8746
8784
|
"apply plan"
|
|
8747
8785
|
);
|
|
8748
8786
|
onPlan?.(plan);
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
)
|
|
8787
|
+
let executed;
|
|
8788
|
+
try {
|
|
8789
|
+
executed = await api("POST", "/v1/apply", { apply_id: plan.apply_id, mode: "apply" });
|
|
8790
|
+
} catch (e) {
|
|
8791
|
+
return pollApplyAfterTransportError({ api, applyId: plan.apply_id, cause: e, ...poll ?? {} });
|
|
8792
|
+
}
|
|
8793
|
+
return applyEnvelope(executed, `apply execute (resume with: vxil push --server --resume ${plan.apply_id})`);
|
|
8794
|
+
}
|
|
8795
|
+
async function pollApplyAfterTransportError({ api, applyId, cause, intervalMs = APPLY_POLL_INTERVAL_MS, maxMs = APPLY_POLL_MAX_MS, sleep, onWait }) {
|
|
8796
|
+
const wait = sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
8797
|
+
const why = cause?.message ?? String(cause);
|
|
8798
|
+
const resumeHint = `resume with: vxil push --server --resume ${applyId}`;
|
|
8799
|
+
onWait?.(` \u2026 the apply answer was lost in transport (${why}) \u2014 reading apply ${applyId} every ${intervalMs / 1e3} s (up to ${maxMs / 1e3} s)`);
|
|
8800
|
+
let last = null;
|
|
8801
|
+
const maxReads = Math.max(1, Math.floor(maxMs / intervalMs));
|
|
8802
|
+
for (let n = 1; ; n++) {
|
|
8803
|
+
let res = null;
|
|
8804
|
+
try {
|
|
8805
|
+
res = await api("GET", `/v1/apply/${encodeURIComponent(applyId)}`);
|
|
8806
|
+
} catch {
|
|
8807
|
+
}
|
|
8808
|
+
if (res && res.status >= 400 && res.status < 500 && res.status !== 429) {
|
|
8809
|
+
try {
|
|
8810
|
+
applyEnvelope(res, `apply ${applyId} status read (${resumeHint})`);
|
|
8811
|
+
} catch (e) {
|
|
8812
|
+
throw new ApplyUnreadError(applyId, e.message);
|
|
8813
|
+
}
|
|
8814
|
+
}
|
|
8815
|
+
const readable = res && res.status < 400 && !res.body?.error && typeof res.body?.data?.status === "string";
|
|
8816
|
+
if (res && readable) {
|
|
8817
|
+
last = applyEnvelope(res, `apply ${applyId} status read (${resumeHint})`);
|
|
8818
|
+
if (last.status !== "running" && last.status !== "planned") return { ...last, polled: true };
|
|
8819
|
+
}
|
|
8820
|
+
if (n >= maxReads) break;
|
|
8821
|
+
await wait(intervalMs);
|
|
8822
|
+
}
|
|
8823
|
+
if (!last) throw new ApplyUnreadError(applyId, `apply execute: ${why}; apply ${applyId} could not be read for ${maxMs / 1e3} s \u2014 check it with \`vxil api GET /v1/apply/${applyId}\`, then ${resumeHint} if it is still running`);
|
|
8824
|
+
return { ...last, polled: true };
|
|
8753
8825
|
}
|
|
8754
8826
|
function cmsEnableManifest(declared) {
|
|
8755
8827
|
return declared.limits !== void 0 ? { ...CMS_ENABLE_MANIFEST, limits: declared.limits } : { ...CMS_ENABLE_MANIFEST };
|
|
@@ -8933,7 +9005,7 @@ function readUnavailable(route, res) {
|
|
|
8933
9005
|
function formatNotCompared(u, hint) {
|
|
8934
9006
|
return ` ! not compared: ${u.route} \u2192 ${u.code ?? u.status}${u.message ? ` \u2014 ${u.message}` : ""}${hint ? ` (${hint})` : ""}`;
|
|
8935
9007
|
}
|
|
8936
|
-
var MAX_FN_BINDINGS, FN_LIMIT_BOUNDS, CONFIG_FILENAMES, VXIL_CONFIG_PKG_VERSION, VXIL_SDK_PKG_VERSION, VXIL_CLI_PKG_VERSION, SCAFFOLD_NODE_TYPES_RANGE, SCAFFOLD_TSCONFIG, CONFIG_IMPORT_SPECIFIERS, QUICKSTART_DEFAULT_FEATURES, APP_HOOK_RE, ACTIVE_ORG_MIN_SDK, loadedConfigApiVersion, STEP_GLYPH, PUSH_READ_CONCURRENCY, CMS_ENABLE_MANIFEST, CMS_PROPAGATION_ATTEMPTS, CMS_PROPAGATION_DELAY_MS;
|
|
9008
|
+
var MAX_FN_BINDINGS, FN_LIMIT_BOUNDS, CONFIG_FILENAMES, VXIL_CONFIG_PKG_VERSION, VXIL_SDK_PKG_VERSION, VXIL_CLI_PKG_VERSION, SCAFFOLD_NODE_TYPES_RANGE, SCAFFOLD_TSCONFIG, CONFIG_IMPORT_SPECIFIERS, QUICKSTART_DEFAULT_FEATURES, APP_HOOK_RE, ACTIVE_ORG_MIN_SDK, loadedConfigApiVersion, STEP_GLYPH, ApplyUnreadError, APPLY_POLL_INTERVAL_MS, APPLY_POLL_MAX_MS, PUSH_READ_CONCURRENCY, CMS_ENABLE_MANIFEST, CMS_PROPAGATION_ATTEMPTS, CMS_PROPAGATION_DELAY_MS;
|
|
8937
9009
|
var init_lib = __esm({
|
|
8938
9010
|
"src/lib.ts"() {
|
|
8939
9011
|
"use strict";
|
|
@@ -8946,9 +9018,9 @@ var init_lib = __esm({
|
|
|
8946
9018
|
timeoutMs: { min: 1e3, max: 12e4 }
|
|
8947
9019
|
};
|
|
8948
9020
|
CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
|
|
8949
|
-
VXIL_CONFIG_PKG_VERSION = "0.
|
|
8950
|
-
VXIL_SDK_PKG_VERSION = "0.
|
|
8951
|
-
VXIL_CLI_PKG_VERSION = "0.
|
|
9021
|
+
VXIL_CONFIG_PKG_VERSION = "0.26.0";
|
|
9022
|
+
VXIL_SDK_PKG_VERSION = "0.34.0";
|
|
9023
|
+
VXIL_CLI_PKG_VERSION = "0.35.0";
|
|
8952
9024
|
SCAFFOLD_NODE_TYPES_RANGE = "^24.0.0";
|
|
8953
9025
|
SCAFFOLD_TSCONFIG = {
|
|
8954
9026
|
compilerOptions: {
|
|
@@ -8973,6 +9045,16 @@ var init_lib = __esm({
|
|
|
8973
9045
|
rolled_back: "\u21A9",
|
|
8974
9046
|
kept: "\u2248"
|
|
8975
9047
|
};
|
|
9048
|
+
ApplyUnreadError = class extends Error {
|
|
9049
|
+
constructor(applyId, message) {
|
|
9050
|
+
super(message);
|
|
9051
|
+
this.applyId = applyId;
|
|
9052
|
+
this.name = "ApplyUnreadError";
|
|
9053
|
+
}
|
|
9054
|
+
applyId;
|
|
9055
|
+
};
|
|
9056
|
+
APPLY_POLL_INTERVAL_MS = 5e3;
|
|
9057
|
+
APPLY_POLL_MAX_MS = 12e4;
|
|
8976
9058
|
PUSH_READ_CONCURRENCY = 5;
|
|
8977
9059
|
CMS_ENABLE_MANIFEST = { enabled: true };
|
|
8978
9060
|
CMS_PROPAGATION_ATTEMPTS = 4;
|
|
@@ -11160,6 +11242,10 @@ async function aiTemplateContentSha256(t) {
|
|
|
11160
11242
|
}));
|
|
11161
11243
|
}
|
|
11162
11244
|
|
|
11245
|
+
// ../runtime/src/innerJwt.ts
|
|
11246
|
+
var APP_MANAGER_ROLE = "app_manager";
|
|
11247
|
+
var enc = new TextEncoder();
|
|
11248
|
+
|
|
11163
11249
|
// ../runtime/src/egress.ts
|
|
11164
11250
|
function isPrivateIPv4(host) {
|
|
11165
11251
|
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
@@ -11748,6 +11834,12 @@ function writeRolesWidens(declared, remote) {
|
|
|
11748
11834
|
if (declared.length === 0) return true;
|
|
11749
11835
|
return declared.some((r) => !remote.includes(r));
|
|
11750
11836
|
}
|
|
11837
|
+
function appBuilderManagersKept(declared, remote) {
|
|
11838
|
+
return !declared && remote.length === 1 && remote[0] === APP_MANAGER_ROLE;
|
|
11839
|
+
}
|
|
11840
|
+
function managerRolesWidens(declared, remote) {
|
|
11841
|
+
return declared.some((r) => !remote.includes(r));
|
|
11842
|
+
}
|
|
11751
11843
|
function orgFieldWidens(declared, remote) {
|
|
11752
11844
|
return remote !== null && declared !== remote;
|
|
11753
11845
|
}
|
|
@@ -11852,6 +11944,7 @@ function remoteCollectionOf(c) {
|
|
|
11852
11944
|
writeRoles: writeRolesOf(c.write_roles),
|
|
11853
11945
|
orgField: typeof c.org_field === "string" && c.org_field.length > 0 ? c.org_field : null,
|
|
11854
11946
|
orgReadRoles: writeRolesOf(c.org_read_roles),
|
|
11947
|
+
managerRoles: writeRolesOf(c.manager_roles),
|
|
11855
11948
|
retainDays: typeof c.retain_days === "number" ? c.retain_days : null,
|
|
11856
11949
|
deletedRetainDays: typeof c.deleted_retain_days === "number" ? c.deleted_retain_days : null,
|
|
11857
11950
|
onOwnerErase: c.on_owner_erase === "delete" ? "delete" : "keep",
|
|
@@ -11871,6 +11964,7 @@ function pulledCollectionBlock(c) {
|
|
|
11871
11964
|
...rc.writeRoles.length ? { writeRoles: rc.writeRoles } : {},
|
|
11872
11965
|
...rc.orgField ? { orgField: rc.orgField } : {},
|
|
11873
11966
|
...rc.orgReadRoles.length ? { orgReadRoles: rc.orgReadRoles } : {},
|
|
11967
|
+
...rc.managerRoles.length ? { managerRoles: rc.managerRoles } : {},
|
|
11874
11968
|
...rc.retainDays !== null ? { retainDays: rc.retainDays } : {},
|
|
11875
11969
|
...rc.deletedRetainDays !== null ? { deletedRetainDays: rc.deletedRetainDays } : {},
|
|
11876
11970
|
...rc.onOwnerErase === "delete" ? { onOwnerErase: "delete" } : {},
|
|
@@ -11938,6 +12032,10 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
11938
12032
|
if (wr !== void 0 && (!Array.isArray(wr) || wr.some((r) => typeof r !== "string"))) {
|
|
11939
12033
|
throw new Error(`cms.collections.${name}.writeRoles must be an array of role slugs (got ${JSON.stringify(wr)})`);
|
|
11940
12034
|
}
|
|
12035
|
+
const mr = def.managerRoles;
|
|
12036
|
+
if (mr !== void 0 && (!Array.isArray(mr) || mr.some((r) => typeof r !== "string"))) {
|
|
12037
|
+
throw new Error(`cms.collections.${name}.managerRoles must be an array of role slugs (got ${JSON.stringify(mr)})`);
|
|
12038
|
+
}
|
|
11941
12039
|
const wsErrs = workspaceCollectionErrors(name, def);
|
|
11942
12040
|
if (wsErrs.length) throw new Error(wsErrs[0]);
|
|
11943
12041
|
const rd = def.retainDays;
|
|
@@ -12132,6 +12230,24 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
12132
12230
|
});
|
|
12133
12231
|
}
|
|
12134
12232
|
}
|
|
12233
|
+
const declaredMgr = writeRolesOf(def.managerRoles);
|
|
12234
|
+
if (!appBuilderManagersKept(def.managerRoles !== void 0, rc.managerRoles) && stableStringify(declaredMgr) !== stableStringify(rc.managerRoles)) {
|
|
12235
|
+
const widens = managerRolesWidens(declaredMgr, rc.managerRoles);
|
|
12236
|
+
const fmt = (r) => r.length ? `[${r.join(", ")}]` : "none";
|
|
12237
|
+
if (!widens || allowDestructive) {
|
|
12238
|
+
collChanges.push({
|
|
12239
|
+
collection: name,
|
|
12240
|
+
kind: "set-manager-roles",
|
|
12241
|
+
detail: `${name} managerRoles \u2192 ${fmt(declaredMgr)} (was ${fmt(rc.managerRoles)})` + (widens ? " \u2014 WIDENS who reads every row (full-config reconciliation)" : " (narrows end-user reads)")
|
|
12242
|
+
});
|
|
12243
|
+
} else {
|
|
12244
|
+
collChanges.push({
|
|
12245
|
+
collection: name,
|
|
12246
|
+
kind: "warn",
|
|
12247
|
+
detail: `${name}: managerRoles are ${fmt(rc.managerRoles)} remotely but ${fmt(declaredMgr)} in config \u2014 that WIDENS which end users read every row, so it is NOT applied. Declare the remote roles to keep them, or re-run with --allow-destructive to widen.`
|
|
12248
|
+
});
|
|
12249
|
+
}
|
|
12250
|
+
}
|
|
12135
12251
|
const declaredOrg = def.orgField ?? null;
|
|
12136
12252
|
const declaredOrgRoles = writeRolesOf(def.orgReadRoles);
|
|
12137
12253
|
const orgSteps = [];
|
|
@@ -12291,6 +12407,8 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
12291
12407
|
if (access !== "readwrite") body.end_user_access = access;
|
|
12292
12408
|
const roles = writeRolesOf(def.writeRoles);
|
|
12293
12409
|
if (roles.length > 0) body.write_roles = roles;
|
|
12410
|
+
const mgrRoles = writeRolesOf(def.managerRoles);
|
|
12411
|
+
if (mgrRoles.length > 0) body.manager_roles = mgrRoles;
|
|
12294
12412
|
if (def.orgField) body.org_field = def.orgField;
|
|
12295
12413
|
const orgRoles = writeRolesOf(def.orgReadRoles);
|
|
12296
12414
|
if (orgRoles.length > 0) body.org_read_roles = orgRoles;
|
|
@@ -12357,6 +12475,14 @@ async function planCmsSchema({ api, collections, apply, allowDestructive = false
|
|
|
12357
12475
|
throw new Error(`cms: set writeRoles on ${c.collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
|
|
12358
12476
|
}
|
|
12359
12477
|
applied++;
|
|
12478
|
+
} else if (c.kind === "set-manager-roles") {
|
|
12479
|
+
const target = writeRolesOf(collections[c.collection].managerRoles);
|
|
12480
|
+
const res = await api("POST", `/v1/cms/collections/${encodeURIComponent(c.collection)}/fields`, { manager_roles: target });
|
|
12481
|
+
if (res.status !== 200 && res.status !== 201) {
|
|
12482
|
+
const e = res.body.error ?? {};
|
|
12483
|
+
throw new Error(`cms: set managerRoles on ${c.collection} failed: ${e.code ?? res.status} ${e.message ?? ""}`);
|
|
12484
|
+
}
|
|
12485
|
+
applied++;
|
|
12360
12486
|
} else if (c.kind === "set-org-field" || c.kind === "set-org-read-roles") {
|
|
12361
12487
|
const def = collections[c.collection];
|
|
12362
12488
|
const remoteRc = remote.get(c.collection);
|
|
@@ -12453,7 +12579,7 @@ function formatCmsChanges(changes) {
|
|
|
12453
12579
|
if (c.kind === "destructive") return ` ! ${c.detail}`;
|
|
12454
12580
|
if (c.kind === "warn") return ` \u26A0 ${c.detail}`;
|
|
12455
12581
|
if (c.kind === "reindex") return ` \u21BB ${c.detail}`;
|
|
12456
|
-
if (c.kind === "alter-flags" || c.kind === "set-owner-field" || c.kind === "set-end-user-access" || c.kind === "set-write-roles" || c.kind === "set-retain-days" || c.kind === "set-deleted-retain-days" || c.kind === "set-on-owner-erase") return ` ~ ${c.detail}`;
|
|
12582
|
+
if (c.kind === "alter-flags" || c.kind === "set-owner-field" || c.kind === "set-end-user-access" || c.kind === "set-write-roles" || c.kind === "set-manager-roles" || c.kind === "set-retain-days" || c.kind === "set-deleted-retain-days" || c.kind === "set-on-owner-erase") return ` ~ ${c.detail}`;
|
|
12457
12583
|
return ` + ${c.detail}`;
|
|
12458
12584
|
}).join("\n");
|
|
12459
12585
|
}
|
|
@@ -12805,6 +12931,10 @@ function storedSessionFor(creds, dashBase) {
|
|
|
12805
12931
|
if (host !== dashboardOrigin(dashBase)) return { session: null, reason: "other-host", host };
|
|
12806
12932
|
return { session: creds.session, host };
|
|
12807
12933
|
}
|
|
12934
|
+
function sessionExpiryFrom(setCookie, now) {
|
|
12935
|
+
const m = /;\s*max-age=(\d+)/i.exec(setCookie ?? "");
|
|
12936
|
+
return m ? new Date(now.getTime() + Number(m[1]) * 1e3).toISOString() : void 0;
|
|
12937
|
+
}
|
|
12808
12938
|
function sessionNotForTarget(s, target) {
|
|
12809
12939
|
if (s.reason === "other-host") return `the stored dashboard session belongs to ${s.host}, not this target (${dashboardOrigin(target)}) \u2014 run \`vxil login\` against this target to use it here`;
|
|
12810
12940
|
if (s.reason === "unbound") return `the stored dashboard session has no recorded host (written by an older CLI) and is not sent anywhere \u2014 run \`vxil login\` again to bind it`;
|
|
@@ -16328,7 +16458,7 @@ var TOOLS = [
|
|
|
16328
16458
|
{
|
|
16329
16459
|
name: "cms_list_collections",
|
|
16330
16460
|
feature: "cms",
|
|
16331
|
-
description: "List the tenant's content model: collections with their typed field definitions (the schema items are validated against). Each field row carries its attributes \u2014 required, validation, index_slot, relation_to, computed/compute, is_unique, on_delete, read_roles (the end-user org roles allowed to READ that field; null = ungated), and on an equality-indexed field indexed: true + index_ready (equality filters on it are index-served once ready; until then, and on unindexed unslotted fields, they are a bounded scan \u2014 same results). A field with read_roles is omitted from end-user reads without a matching role and is never served on the public lane, so a missing key in an item's data may be a read gate rather than an unset value. Each collection row also carries end_user_access: 'readwrite' (default), 'read' (a verified end user \u2014 a thin-client key with a session \u2014 may read but every write answers 403 server_only), 'none' (end-user reads and writes are 403 server_only) or 'read_all' (a verified end user reads every row, past owner_field, and writes only their own), and write_roles: the roles a verified end user must hold to WRITE the collection (null = ungated; without one every end-user write answers 403 role_required, reads are unaffected); server keys are never affected. retain_days (null = keep) is the collection's retention: the nightly platform sweep soft-deletes live items created more than that many days ago. deleted_retain_days (null = 30) is how many days a deleted item stays restorable before the nightly cleanup purges it. on_owner_erase 'delete' (null = keep) means a GDPR erase of a user hard-deletes the items they own here. org_field (null = none) makes the collection workspace-scoped: a verified end user reads and writes only rows of the session's active org (no active org \u2192 403 org_required) and write_roles match the active-org role; org_read_roles (null = any member) gates reads by that role.",
|
|
16461
|
+
description: "List the tenant's content model: collections with their typed field definitions (the schema items are validated against). Each field row carries its attributes \u2014 required, validation, index_slot, relation_to, computed/compute, is_unique, on_delete, read_roles (the end-user org roles allowed to READ that field; null = ungated), and on an equality-indexed field indexed: true + index_ready (equality filters on it are index-served once ready; until then, and on unindexed unslotted fields, they are a bounded scan \u2014 same results). A field with read_roles is omitted from end-user reads without a matching role and is never served on the public lane, so a missing key in an item's data may be a read gate rather than an unset value. Each collection row also carries end_user_access: 'readwrite' (default), 'read' (a verified end user \u2014 a thin-client key with a session \u2014 may read but every write answers 403 server_only), 'none' (end-user reads and writes are 403 server_only) or 'read_all' (a verified end user reads every row, past owner_field, and writes only their own), and write_roles: the roles a verified end user must hold to WRITE the collection (null = ungated; without one every end-user write answers 403 role_required, reads are unaffected); manager_roles: the roles whose verified holders manage an owned collection (get / list / count read every row, the single-item update may change another member's row under the collection's hooks; null = none); server keys are never affected. retain_days (null = keep) is the collection's retention: the nightly platform sweep soft-deletes live items created more than that many days ago. deleted_retain_days (null = 30) is how many days a deleted item stays restorable before the nightly cleanup purges it. on_owner_erase 'delete' (null = keep) means a GDPR erase of a user hard-deletes the items they own here. org_field (null = none) makes the collection workspace-scoped: a verified end user reads and writes only rows of the session's active org (no active org \u2192 403 org_required) and write_roles match the active-org role; org_read_roles (null = any member) gates reads by that role.",
|
|
16332
16462
|
inputSchema: { type: "object", properties: {} },
|
|
16333
16463
|
method: "GET",
|
|
16334
16464
|
path: "/v1/cms/collections"
|
|
@@ -17428,7 +17558,7 @@ var TOOLS = [
|
|
|
17428
17558
|
// Substrate tool (no `feature` — always-on like users_upsert): usage is a
|
|
17429
17559
|
// platform property, not a feature you enable.
|
|
17430
17560
|
name: "usage_current",
|
|
17431
|
-
description: "Current-month usage for this project: requests used, the plan's included quota, remaining (null when the plan has no fixed cap), month-to-date per-feature metered detail (may lag \u2014 it is aggregated periodically), and `routes` \u2014 the same request count split by the surface that served it (the resolved upstream; /v1/fn/* reports as 'fn-invoke', busiest first, observational only), `files` (live private objects and bytes vs the storage `limit` uploads are held to \u2014 422 quota_exceeded at it) and `functions` (today's UTC call count vs the plan's `limit_per_day`, and on a Free staging
|
|
17561
|
+
description: "Current-month usage for this project: requests used, the plan's included quota, remaining (null when the plan has no fixed cap), month-to-date per-feature metered detail (may lag \u2014 it is aggregated periodically), and `routes` \u2014 the same request count split by the surface that served it (the resolved upstream; /v1/fn/* reports as 'fn-invoke', busiest first, observational only), `files` (live private objects and bytes vs the storage `limit` uploads are held to \u2014 422 quota_exceeded at it) and `functions` (today's UTC call count vs the plan's `limit_per_day`, and `cpu_ms_today` (CPU-ms charged today, settled to measured CPU plus a small per-call dispatch overhead) vs `cpu_ms_limit_per_day`, the enforced daily CPU budget on every plan (Free production 150,000, a Free staging or development project 500,000, a paid project its plan row; null on Enterprise) \u2014 429 cpu_ms_per_day past it; a snapshot that can trail by a minute or two, null when there is no snapshot for today), and `daily_caps` \u2014 each daily lane of the plan (api_calls, App builder units, functions CPU, realtime, bandwidth, image transforms, and db_bytes \u2014 the database size measured nightly, over which cms creates and updates answer 413 db_size_limit) with `used` (null where not readable yet), `cap`, `resets_at` (null for db_bytes) and `state` ('warning' at 80 %, 'reached' at the cap), for the pool the caps count in (a Free account's projects share one). Read-only. Call this before a bulk run to self-check remaining quota: the Free tier is hard-capped (429 quota_exceeded when exhausted); paid tiers have no monthly cap and are never billed for overage. Needs usage:read or features:read.",
|
|
17432
17562
|
inputSchema: { type: "object", properties: {} },
|
|
17433
17563
|
method: "GET",
|
|
17434
17564
|
path: "/v1/usage"
|
|
@@ -17486,7 +17616,7 @@ var TOOLS = [
|
|
|
17486
17616
|
},
|
|
17487
17617
|
{
|
|
17488
17618
|
name: "config_put",
|
|
17489
|
-
description: "Replace ONE feature's configuration with `body` (the whole manifest, as config_get returns it). DRY RUN BY DEFAULT: without `dry_run: false` nothing is written and the answer is the validated manifest and summary the write would produce, so preview first, then repeat with dry_run false. A real write needs `if_match` (the version from config_get) once the feature has a version; a stale one answers 409 config_version_conflict, so read again and retry. The answer is the server's own summary (new version, and for rate-limits, webhooks and ai the declared rows planned or applied) \u2014 nothing undeclared is deleted. Hooks named app_* belong to App builder apps: they are kept as stored, and a write that declares or changes one is refused (422 reserved_hook_name). cms `rules` (App builder rules): a body without it keeps them; adding one needs no ack, but removing or changing one is refused (422 rules_widening, naming the rules) unless allow_rule_removal is true \u2014 when you did not mean to remove them, read config_get again (your body is older than the app's rules). functions accepts only a logDrain change, with dry_run false.
|
|
17619
|
+
description: "Replace ONE feature's configuration with `body` (the whole manifest, as config_get returns it). DRY RUN BY DEFAULT: without `dry_run: false` nothing is written and the answer is the validated manifest and summary the write would produce, so preview first, then repeat with dry_run false. A real write needs `if_match` (the version from config_get) once the feature has a version; a stale one answers 409 config_version_conflict, so read again and retry. The answer is the server's own summary (new version, and for rate-limits, webhooks and ai the declared rows planned or applied) \u2014 nothing undeclared is deleted. Hooks named app_* belong to App builder apps: they are kept as stored, and a write that declares or changes one is refused (422 reserved_hook_name). cms `rules` (App builder rules): a body without it keeps them; adding one needs no ack, but removing or changing one is refused (422 rules_widening, naming the rules) unless allow_rule_removal is true \u2014 when you did not mean to remove them, read config_get again (your body is older than the app's rules). functions accepts only a logDrain change, with dry_run false. webhooks `alerts` is the failure-alert digest ({ enabled, minLevel, digestMinutes }); `alerts.chatWebhook: { secret: \"<name>\" }` also posts each digest as { text } to a Google Chat, Slack or Teams Workflows webhook whose URL is stored as the webhooks secret <name> (PUT /v1/secrets/webhooks/<name>; the URL is never a config value, and only chat.googleapis.com, hooks.slack.com, *.webhook.office.com and *.logic.azure.com are posted to). A dry run (the default) needs features:read; a write (dry_run false) needs features:write.",
|
|
17490
17620
|
inputSchema: {
|
|
17491
17621
|
type: "object",
|
|
17492
17622
|
properties: {
|
|
@@ -17907,6 +18037,10 @@ function collectSecretRefs(cfg) {
|
|
|
17907
18037
|
if (typeof drainRef === "string" && drainRef.replace(/^secret:/, "")) {
|
|
17908
18038
|
add({ name: drainRef.replace(/^secret:/, ""), feature: "functions", via: "features.functions.logDrain.secretRef" });
|
|
17909
18039
|
}
|
|
18040
|
+
const chatRef = cfg.features?.webhooks?.alerts?.chatWebhook?.secret;
|
|
18041
|
+
if (typeof chatRef === "string" && chatRef.replace(/^secret:/, "")) {
|
|
18042
|
+
add({ name: chatRef.replace(/^secret:/, ""), feature: "webhooks", via: "features.webhooks.alerts.chatWebhook.secret" });
|
|
18043
|
+
}
|
|
17910
18044
|
for (const [fn, def] of Object.entries(cfg.functions ?? {})) {
|
|
17911
18045
|
for (const ref of normalizeSecretRefs(def.secrets)) {
|
|
17912
18046
|
const bare = ref.slice("secret:".length);
|
|
@@ -17965,6 +18099,10 @@ function unstoredSecretVerdict(opts) {
|
|
|
17965
18099
|
if (opts.ignored) return "ignored";
|
|
17966
18100
|
return opts.strict ? "drift" : "informational";
|
|
17967
18101
|
}
|
|
18102
|
+
var SECRETS_NOT_COMPARED_LINE = " ! not compared: secrets (needs features:read or secrets:write)";
|
|
18103
|
+
function secretsListRefusal(opts) {
|
|
18104
|
+
return opts.status === 403 && opts.code === "insufficient_scope" && !opts.strict ? "skip" : "error";
|
|
18105
|
+
}
|
|
17968
18106
|
function platformOnlyFnVerdict(opts) {
|
|
17969
18107
|
if (opts.ignored) return "ignored";
|
|
17970
18108
|
return opts.gate && !opts.strict ? "informational" : "drift";
|
|
@@ -18265,6 +18403,18 @@ function devSlotExpiryCheck(dev, now) {
|
|
|
18265
18403
|
detail: `'${dev.slug}' expires ${dev.expires_at} (in ${hours}h)` + (hours < 72 ? " \u2014 run `vxil dev up` to extend" : "")
|
|
18266
18404
|
};
|
|
18267
18405
|
}
|
|
18406
|
+
function dashboardSessionCheck(creds, now) {
|
|
18407
|
+
const name = "dashboard session";
|
|
18408
|
+
if (!creds.session) return { name, ok: true, detail: "no session \u2014 `vxil login` signs in (only session verbs such as `vxil keys` need one)" };
|
|
18409
|
+
const host = creds.session_host ? ` \xB7 bound to ${creds.session_host}` : " \xB7 no recorded host (run `vxil login` again to bind it)";
|
|
18410
|
+
const who = creds.session_account ?? "(account not recorded by the CLI that signed in \u2014 `vxil account show` names it)";
|
|
18411
|
+
const exp = creds.session_expires_at ? new Date(creds.session_expires_at) : null;
|
|
18412
|
+
if (exp && !Number.isNaN(exp.getTime()) && exp.getTime() <= now.getTime()) {
|
|
18413
|
+
return { name, ok: true, warn: true, detail: `${who} \xB7 expired ${exp.toISOString().slice(0, 16).replace("T", " ")} UTC${host} \u2014 \`vxil login\` again` };
|
|
18414
|
+
}
|
|
18415
|
+
const when2 = exp && !Number.isNaN(exp.getTime()) ? `expires ${exp.toISOString().slice(0, 16).replace("T", " ")} UTC` : "expiry not recorded";
|
|
18416
|
+
return { name, ok: true, detail: `${who} \xB7 ${when2}${host}` };
|
|
18417
|
+
}
|
|
18268
18418
|
|
|
18269
18419
|
// src/keys.ts
|
|
18270
18420
|
init_src3();
|
|
@@ -18561,9 +18711,9 @@ function renderInvites(l) {
|
|
|
18561
18711
|
|
|
18562
18712
|
// src/configDrafts.ts
|
|
18563
18713
|
init_lib();
|
|
18564
|
-
var
|
|
18714
|
+
var enc2 = encodeURIComponent;
|
|
18565
18715
|
async function showDraft(api, feature) {
|
|
18566
|
-
const r = await api("GET", `/v1/config/${
|
|
18716
|
+
const r = await api("GET", `/v1/config/${enc2(feature)}/draft`);
|
|
18567
18717
|
if (r.status === 404) return { none: true };
|
|
18568
18718
|
if (r.status !== 200) throw new Error(`could not read the ${feature} draft (${apiErrText(r)})`);
|
|
18569
18719
|
const d = r.body.data ?? {};
|
|
@@ -18574,7 +18724,7 @@ async function showDraft(api, feature) {
|
|
|
18574
18724
|
...d.applied_by ? { applied_by: d.applied_by } : {},
|
|
18575
18725
|
...d.created_at ? { created_at: d.created_at } : {}
|
|
18576
18726
|
};
|
|
18577
|
-
const cur = await api("GET", `/v1/config/${
|
|
18727
|
+
const cur = await api("GET", `/v1/config/${enc2(feature)}`);
|
|
18578
18728
|
if (cur.status !== 200 && cur.status !== 404) {
|
|
18579
18729
|
throw new Error(`could not read the active ${feature} config to compare (${apiErrText(cur)})`);
|
|
18580
18730
|
}
|
|
@@ -18583,12 +18733,12 @@ async function showDraft(api, feature) {
|
|
|
18583
18733
|
return { draft, activeVersion, changes: diffManifest(active, draft.manifest) };
|
|
18584
18734
|
}
|
|
18585
18735
|
async function saveDraft(api, feature, manifest) {
|
|
18586
|
-
const r = await api("PUT", `/v1/config/${
|
|
18736
|
+
const r = await api("PUT", `/v1/config/${enc2(feature)}/draft`, manifest);
|
|
18587
18737
|
if (r.status !== 200) throw new Error(`draft not saved (${apiErrText(r)})`);
|
|
18588
18738
|
return { feature, manifest: r.body.data?.manifest ?? manifest };
|
|
18589
18739
|
}
|
|
18590
18740
|
async function publishDraft(api, feature) {
|
|
18591
|
-
const r = await api("POST", `/v1/config/${
|
|
18741
|
+
const r = await api("POST", `/v1/config/${enc2(feature)}/draft/publish`);
|
|
18592
18742
|
if (r.status === 404) throw new Error(`no ${feature} draft to publish \u2014 \`vxil config draft save ${feature}\` stages one`);
|
|
18593
18743
|
if (r.status !== 200) throw new Error(`draft not published (${apiErrText(r)})`);
|
|
18594
18744
|
const d = r.body.data ?? {};
|
|
@@ -18599,7 +18749,7 @@ async function publishDraft(api, feature) {
|
|
|
18599
18749
|
};
|
|
18600
18750
|
}
|
|
18601
18751
|
async function discardDraft(api, feature) {
|
|
18602
|
-
const r = await api("DELETE", `/v1/config/${
|
|
18752
|
+
const r = await api("DELETE", `/v1/config/${enc2(feature)}/draft`);
|
|
18603
18753
|
if (r.status === 404) return { discarded: false };
|
|
18604
18754
|
if (r.status !== 200) throw new Error(`draft not discarded (${apiErrText(r)})`);
|
|
18605
18755
|
return { discarded: true };
|
|
@@ -18611,7 +18761,7 @@ function parseVersion(raw) {
|
|
|
18611
18761
|
return { version: Number(raw) };
|
|
18612
18762
|
}
|
|
18613
18763
|
async function duplicateToDraft(api, feature, version) {
|
|
18614
|
-
const r = await api("POST", `/v1/config/${
|
|
18764
|
+
const r = await api("POST", `/v1/config/${enc2(feature)}/versions/${version}/duplicate`);
|
|
18615
18765
|
if (r.status === 404) throw new Error(`${feature} has no published version ${version} \u2014 \`vxil versions ${feature}\` lists them`);
|
|
18616
18766
|
if (r.status !== 200) throw new Error(`could not copy version ${version} into a draft (${apiErrText(r)})`);
|
|
18617
18767
|
return {
|
|
@@ -19404,7 +19554,9 @@ function serverApplyErrorJson(input) {
|
|
|
19404
19554
|
return {
|
|
19405
19555
|
ok: false,
|
|
19406
19556
|
exit_code: input.exitCode,
|
|
19407
|
-
|
|
19557
|
+
// a run that planned but lost the execute answer
|
|
19558
|
+
// (and could not read the journal back) still names its apply
|
|
19559
|
+
apply_id: input.applyId ?? null,
|
|
19408
19560
|
status: null,
|
|
19409
19561
|
steps: [],
|
|
19410
19562
|
trigger_wiring: null,
|
|
@@ -20653,7 +20805,7 @@ async function loadCmsTable({
|
|
|
20653
20805
|
let patched = 0;
|
|
20654
20806
|
let unhashed = 0;
|
|
20655
20807
|
let unhashedSame = 0;
|
|
20656
|
-
const
|
|
20808
|
+
const enc3 = encodeURIComponent(collection);
|
|
20657
20809
|
const start = cursors[key] ?? void 0;
|
|
20658
20810
|
const tsCols = keepTimestamps ? sourceTimestampColumns(mapping) : {};
|
|
20659
20811
|
if (keepTimestamps && tsCols.created === void 0) {
|
|
@@ -20707,7 +20859,7 @@ async function loadCmsTable({
|
|
|
20707
20859
|
patched++;
|
|
20708
20860
|
continue;
|
|
20709
20861
|
}
|
|
20710
|
-
const cur = await deps.api("GET", `/v1/cms/items/${
|
|
20862
|
+
const cur = await deps.api("GET", `/v1/cms/items/${enc3}/${encodeURIComponent(existing)}`);
|
|
20711
20863
|
const stored = cur.status === 200 ? cur.body.data?.data : void 0;
|
|
20712
20864
|
if (holdsContent(stored, item, storageTargets)) {
|
|
20713
20865
|
hashes[sourceId] = h;
|
|
@@ -20723,7 +20875,7 @@ async function loadCmsTable({
|
|
|
20723
20875
|
patched++;
|
|
20724
20876
|
continue;
|
|
20725
20877
|
}
|
|
20726
|
-
const res2 = await deps.api("PATCH", `/v1/cms/items/${
|
|
20878
|
+
const res2 = await deps.api("PATCH", `/v1/cms/items/${enc3}/${encodeURIComponent(existing)}`, { data: item.full });
|
|
20727
20879
|
if (res2.status === 200) {
|
|
20728
20880
|
hashes[sourceId] = h;
|
|
20729
20881
|
noteStorage(sourceId, row2);
|
|
@@ -20744,7 +20896,7 @@ async function loadCmsTable({
|
|
|
20744
20896
|
}
|
|
20745
20897
|
if (deps.dryRun) continue;
|
|
20746
20898
|
const srcTs = keepTimestamps ? sourceTimestampsOf(row2, tsCols) : void 0;
|
|
20747
|
-
const res = await deps.api("POST", `/v1/cms/items/${
|
|
20899
|
+
const res = await deps.api("POST", `/v1/cms/items/${enc3}`, {
|
|
20748
20900
|
data: item.create,
|
|
20749
20901
|
status: itemStatus,
|
|
20750
20902
|
...srcTs ? { source_timestamps: srcTs } : {}
|
|
@@ -20766,7 +20918,7 @@ async function loadCmsTable({
|
|
|
20766
20918
|
const createdVal = dedupeField ? item.create[dedupeField.target] : void 0;
|
|
20767
20919
|
if (res.status === 409 && res.body.error?.code === "unique_violation" && dedupeField && createdVal !== void 0) {
|
|
20768
20920
|
const filter = encodeURIComponent(JSON.stringify({ [dedupeField.target]: createdVal }));
|
|
20769
|
-
const q = await deps.api("GET", `/v1/cms/items/${
|
|
20921
|
+
const q = await deps.api("GET", `/v1/cms/items/${enc3}?filter=${filter}&limit=1`);
|
|
20770
20922
|
const items = q.body.data?.items ?? [];
|
|
20771
20923
|
if (q.status === 200 && items.length > 0 && typeof items[0].item_id === "string") {
|
|
20772
20924
|
idMap[sourceId] = items[0].item_id;
|
|
@@ -21308,7 +21460,7 @@ async function loadFiles({ deps, plan, adapter, state, save, filesOwner, filesOr
|
|
|
21308
21460
|
const itmMap = state.idMap[table] ?? {};
|
|
21309
21461
|
const linked = hashesFor(state, rk);
|
|
21310
21462
|
const pk = sourcePkColumn(mapping);
|
|
21311
|
-
const
|
|
21463
|
+
const enc3 = encodeURIComponent(mapping.collection);
|
|
21312
21464
|
let misses = 0;
|
|
21313
21465
|
let privateUrls = 0;
|
|
21314
21466
|
const columns = [pk, ...storageFields.map((f) => f.source)];
|
|
@@ -21354,7 +21506,7 @@ async function loadFiles({ deps, plan, adapter, state, save, filesOwner, filesOr
|
|
|
21354
21506
|
}
|
|
21355
21507
|
continue;
|
|
21356
21508
|
}
|
|
21357
|
-
const res = await deps.api("PATCH", `/v1/cms/items/${
|
|
21509
|
+
const res = await deps.api("PATCH", `/v1/cms/items/${enc3}/${encodeURIComponent(itm)}`, { data: patch });
|
|
21358
21510
|
if (res.status !== 200) {
|
|
21359
21511
|
result.errors.push(apiErrorLine(`files relink ${table} row '${sourceId}' (item ${itm})`, res));
|
|
21360
21512
|
if (result.errors.length >= ROW_FAILURE_CAP) {
|
|
@@ -21617,7 +21769,7 @@ async function loadVectors({ deps, plan, target, adapter, state, save, delta })
|
|
|
21617
21769
|
const pk = sourcePkColumn(mapping);
|
|
21618
21770
|
const idMap = state.idMap[target.table] ?? {};
|
|
21619
21771
|
const ingested = hashesFor(state, key);
|
|
21620
|
-
const
|
|
21772
|
+
const enc3 = encodeURIComponent(target.collection);
|
|
21621
21773
|
let noText = 0;
|
|
21622
21774
|
let noItem = 0;
|
|
21623
21775
|
const opts = {
|
|
@@ -21648,7 +21800,7 @@ async function loadVectors({ deps, plan, target, adapter, state, save, delta })
|
|
|
21648
21800
|
} else if (doc.why === "no-text") noText++;
|
|
21649
21801
|
result.skipped++;
|
|
21650
21802
|
if (ingested[sourceId] !== void 0 && !deps.dryRun) {
|
|
21651
|
-
const del = await deps.api("DELETE", `/v1/search/${
|
|
21803
|
+
const del = await deps.api("DELETE", `/v1/search/${enc3}/documents/${encodeURIComponent(docId)}`);
|
|
21652
21804
|
if (del.status === 200 || del.status === 404) delete ingested[sourceId];
|
|
21653
21805
|
}
|
|
21654
21806
|
if (result.errors.length >= ROW_FAILURE_CAP) break;
|
|
@@ -21660,7 +21812,7 @@ async function loadVectors({ deps, plan, target, adapter, state, save, delta })
|
|
|
21660
21812
|
continue;
|
|
21661
21813
|
}
|
|
21662
21814
|
if (deps.dryRun) continue;
|
|
21663
|
-
const res = await deps.api("POST", `/v1/search/${
|
|
21815
|
+
const res = await deps.api("POST", `/v1/search/${enc3}/documents`, {
|
|
21664
21816
|
doc_id: docId,
|
|
21665
21817
|
chunks: [{ content: text, vector }],
|
|
21666
21818
|
metadata: {}
|
|
@@ -26143,10 +26295,15 @@ function noKeyMessage(t) {
|
|
|
26143
26295
|
if (t.slot === "named") return `no API key stored for target '${t.slotName}' (tenant '${t.tenantSlug}') \u2014 re-run \`vxil link ${t.tenantSlug} --as ${t.slotName}\``;
|
|
26144
26296
|
return "no API key \u2014 run `vxil quickstart` or `vxil link <slug>`, or set VXIL_API_KEY";
|
|
26145
26297
|
}
|
|
26146
|
-
function storeSession(cookie, dashBase) {
|
|
26298
|
+
function storeSession(cookie, dashBase, account, setCookie) {
|
|
26147
26299
|
const creds = loadCredentials();
|
|
26148
26300
|
creds.session = cookie;
|
|
26149
26301
|
creds.session_host = dashboardOrigin(dashBase);
|
|
26302
|
+
const expires = sessionExpiryFrom(setCookie, /* @__PURE__ */ new Date());
|
|
26303
|
+
if (account) creds.session_account = account;
|
|
26304
|
+
else delete creds.session_account;
|
|
26305
|
+
if (expires) creds.session_expires_at = expires;
|
|
26306
|
+
else delete creds.session_expires_at;
|
|
26150
26307
|
saveCredentials(creds);
|
|
26151
26308
|
}
|
|
26152
26309
|
function sessionForTarget(dashBase) {
|
|
@@ -26183,7 +26340,7 @@ async function interactiveLogin(dash, dashBase) {
|
|
|
26183
26340
|
const cookie = r.cookie?.split(";")[0];
|
|
26184
26341
|
if (!cookie) fail("login succeeded but returned no session cookie");
|
|
26185
26342
|
if (mustPrompt) {
|
|
26186
|
-
storeSession(cookie, dashBase);
|
|
26343
|
+
storeSession(cookie, dashBase, e, r.cookie);
|
|
26187
26344
|
console.error(`\u2713 signed in \u2014 session stored in ~/.vxil/credentials.json (for ${dashboardOrigin(dashBase)})`);
|
|
26188
26345
|
}
|
|
26189
26346
|
return cookie;
|
|
@@ -26594,7 +26751,9 @@ async function runApply(apply, sel = "prod", opts = {}) {
|
|
|
26594
26751
|
const skipFunctions = hasFlag("skip-functions");
|
|
26595
26752
|
if (declaredFns.length && !skipFunctions && !gate) {
|
|
26596
26753
|
const ent = await probeFunctionsEntitlement(api, declaredFns[0]);
|
|
26597
|
-
if (!ent
|
|
26754
|
+
if (!apply && entitlementNotProbed(ent)) {
|
|
26755
|
+
console.error(DEPLOY_ENTITLEMENT_NOT_PROBED);
|
|
26756
|
+
} else if (!ent.entitled) {
|
|
26598
26757
|
const msg = formatFunctionsPreflight(ent, {
|
|
26599
26758
|
declared: declaredFns.length,
|
|
26600
26759
|
baseUrl: target.baseUrl,
|
|
@@ -26877,9 +27036,13 @@ functions: skipped (--skip-functions) \u2014 ${declaredFns.length} declared func
|
|
|
26877
27036
|
const res = await api("GET", "/v1/secrets");
|
|
26878
27037
|
console.log("\nsecrets (names only \u2014 values are never read or compared):");
|
|
26879
27038
|
if (res.status !== 200) {
|
|
27039
|
+
if (secretsListRefusal({ status: res.status, ...res.body.error?.code ? { code: res.body.error.code } : {}, strict: strictSecrets }) === "skip") {
|
|
27040
|
+
console.log(SECRETS_NOT_COMPARED_LINE);
|
|
27041
|
+
return;
|
|
27042
|
+
}
|
|
26880
27043
|
notCompared(
|
|
26881
27044
|
{ route: "GET /v1/secrets", status: res.status, ...res.body.error?.code ? { code: res.body.error.code } : {}, ...res.body.error?.message ? { message: res.body.error.message } : {} },
|
|
26882
|
-
"needs features:
|
|
27045
|
+
"needs features:read, secrets:write or admin"
|
|
26883
27046
|
);
|
|
26884
27047
|
return;
|
|
26885
27048
|
}
|
|
@@ -27122,9 +27285,21 @@ ${formatTriggerWiring(w).join("\n")}`);
|
|
|
27122
27285
|
say(formatApplySteps(plan.steps));
|
|
27123
27286
|
for (const w of plan.warnings ?? []) say(` ! ${w}`);
|
|
27124
27287
|
say("\napplying server-side\u2026");
|
|
27288
|
+
},
|
|
27289
|
+
// a transport error on execute reads the journal back
|
|
27290
|
+
poll: { onWait: say }
|
|
27291
|
+
}).catch((e) => {
|
|
27292
|
+
if (e instanceof ApplyUnreadError && jsonOut) {
|
|
27293
|
+
jsonFailDoc = (error, exitCode) => JSON.stringify(serverApplyErrorJson({ error, exitCode, applyId: e.applyId }));
|
|
27125
27294
|
}
|
|
27295
|
+
throw e;
|
|
27126
27296
|
});
|
|
27127
|
-
|
|
27297
|
+
if (done.polled && (done.status === "running" || done.status === "planned")) {
|
|
27298
|
+
report(done, false);
|
|
27299
|
+
const problems2 = { hard: [], strict: [] };
|
|
27300
|
+
return finish(done, problems2, 1, `apply ${done.apply_id} is still ${done.status} after the execute answer was lost in transport \u2014 it was NOT resumed automatically; check it with \`vxil api GET /v1/apply/${done.apply_id}\`, then resume with: vxil push --server --resume ${done.apply_id}`);
|
|
27301
|
+
}
|
|
27302
|
+
for (let attempt = 1; !done.polled && done.status === "running" && onlyPropagationDeferred(done.steps) && attempt <= CMS_PROPAGATION_ATTEMPTS; attempt++) {
|
|
27128
27303
|
if (attempt === 1) report(done, false);
|
|
27129
27304
|
say(` \u2026 deferred: the feature config is still propagating \u2014 waiting ${CMS_PROPAGATION_DELAY_MS / 1e3} s, then resuming the same apply (${attempt}/${CMS_PROPAGATION_ATTEMPTS})`);
|
|
27130
27305
|
await new Promise((r) => setTimeout(r, CMS_PROPAGATION_DELAY_MS));
|
|
@@ -27384,6 +27559,7 @@ async function runDoctor() {
|
|
|
27384
27559
|
detail: t.apiKey ? `${t.slot === "named" ? `target '${t.slotName}'` : t.slot === "dev" ? "dev slot" : t.slot === "env" ? "VXIL_API_KEY (no project binding)" : "primary"} \u2192 tenant '${t.tenantSlug ?? "(unknown slug)"}' \xB7 ${t.envLabel} \xB7 ${t.baseUrl} \xB7 key from ${t.keySource}` : noKeyMessage(t)
|
|
27385
27560
|
});
|
|
27386
27561
|
}
|
|
27562
|
+
checks.push(dashboardSessionCheck(loadCredentials(), /* @__PURE__ */ new Date()));
|
|
27387
27563
|
const proj = loadProject();
|
|
27388
27564
|
if (proj) {
|
|
27389
27565
|
const slots = bindingSlots(proj).map((s) => `${s.label}=${s.slug}`);
|
|
@@ -27443,7 +27619,7 @@ async function runDoctor() {
|
|
|
27443
27619
|
checks.push(...secretsChecks(collectSecretRefs(cfg), sres.body.data?.secrets ?? []));
|
|
27444
27620
|
} else {
|
|
27445
27621
|
const e = sres.body.error ?? {};
|
|
27446
|
-
checks.push({ name: "secrets", ok: true, warn: true, detail: `not compared: GET /v1/secrets \u2192 ${e.code ?? sres.status} (names-only read; needs features:
|
|
27622
|
+
checks.push({ name: "secrets", ok: true, warn: true, detail: `not compared: GET /v1/secrets \u2192 ${e.code ?? sres.status} (names-only read; needs features:read, secrets:write or admin)` });
|
|
27447
27623
|
}
|
|
27448
27624
|
}
|
|
27449
27625
|
if (cfg?.functions && Object.keys(cfg.functions).length) {
|
|
@@ -27810,7 +27986,7 @@ try {
|
|
|
27810
27986
|
if (r.status !== 200) fail(`login failed: ${r.status} ${JSON.stringify(r.json).slice(0, 160)}`);
|
|
27811
27987
|
const cookie = r.cookie?.split(";")[0];
|
|
27812
27988
|
if (!cookie) fail("login succeeded but returned no session cookie");
|
|
27813
|
-
storeSession(cookie, dash);
|
|
27989
|
+
storeSession(cookie, dash, email, r.cookie);
|
|
27814
27990
|
console.log(`\u2713 logged in \u2014 session stored in ~/.vxil/credentials.json (bound to ${dashboardOrigin(dash)}; it is only ever sent there)`);
|
|
27815
27991
|
break;
|
|
27816
27992
|
}
|
|
@@ -28087,6 +28263,8 @@ try {
|
|
|
28087
28263
|
}
|
|
28088
28264
|
delete creds.session;
|
|
28089
28265
|
delete creds.session_host;
|
|
28266
|
+
delete creds.session_account;
|
|
28267
|
+
delete creds.session_expires_at;
|
|
28090
28268
|
saveCredentials(creds);
|
|
28091
28269
|
if (!host) {
|
|
28092
28270
|
console.error("vxil: the stored session had no recorded dashboard host (written by an older CLI), so it was not sent anywhere to be revoked \u2014 it was removed from this machine and expires on its own");
|
|
@@ -28721,7 +28899,7 @@ ${plan.rationale}`);
|
|
|
28721
28899
|
const rows = res.body.data?.secrets ?? [];
|
|
28722
28900
|
if (!rows.length) console.log("(no secrets stored)");
|
|
28723
28901
|
for (const r of rows) {
|
|
28724
|
-
console.log(` ${r.feature}/${r.secret_name} (ref secret:${r.secret_name} \xB7 rotated ${r.rotated_at})`);
|
|
28902
|
+
console.log(` ${r.feature}/${r.secret_name} (ref secret:${r.secret_name}${r.rotated_at ? ` \xB7 rotated ${r.rotated_at}` : ""})`);
|
|
28725
28903
|
}
|
|
28726
28904
|
}
|
|
28727
28905
|
} else if (sub === "rm") {
|
|
@@ -29593,7 +29771,7 @@ async function runSecretsMatrix() {
|
|
|
29593
29771
|
}
|
|
29594
29772
|
const secrets = res.body.data?.secrets ?? [];
|
|
29595
29773
|
for (const r of secrets) {
|
|
29596
|
-
rows.push({ slug: s.slug, slot: s.label, feature: r.feature, secret_name: r.secret_name, rotated_at: r.rotated_at, ...r.kek_version !== void 0 ? { kek_version: r.kek_version } : {} });
|
|
29774
|
+
rows.push({ slug: s.slug, slot: s.label, feature: r.feature, secret_name: r.secret_name, rotated_at: r.rotated_at ?? "-", ...r.kek_version !== void 0 ? { kek_version: r.kek_version } : {} });
|
|
29597
29775
|
}
|
|
29598
29776
|
tenants.push({ slug: s.slug, slot: s.label, edge, status: "ok", count: secrets.length });
|
|
29599
29777
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vxil/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "The vxil CLI — init, quickstart, push, gen, secrets, keys, migrate, doctor, diff, functions, payments; installs the `vxil` command (npm i -g @vxil/cli).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@vxil/config": "0.
|
|
49
|
-
"@vxil/feature-configs": "0.23.
|
|
48
|
+
"@vxil/config": "0.26.0",
|
|
49
|
+
"@vxil/feature-configs": "0.23.1",
|
|
50
50
|
"@vxil/runtime": "0.0.1",
|
|
51
51
|
"@vxil/types": "0.0.1",
|
|
52
52
|
"miniflare": "^4.20260730.0"
|