@vxil/cli 0.26.0 → 0.26.2

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 CHANGED
@@ -23,7 +23,8 @@ export default defineConfig({
23
23
  features: {
24
24
  auth: { enabled: true },
25
25
  cms: { enabled: true, draftPublish: true },
26
- notifications: { enabled: true, provider: 'resend', fromEmail: 'noreply@example.com' },
26
+ // provider 'resend' needs its key: `vxil secrets set RESEND_API_KEY` (or use provider: 'mock' to try it)
27
+ notifications: { enabled: true, provider: 'resend', resendApiKeyRef: 'secret:RESEND_API_KEY', fromEmail: 'noreply@example.com' },
27
28
  },
28
29
  // (b) CMS schema-as-code — collections + fields, reconciled by `vxil push`
29
30
  cms: {
@@ -334,6 +334,7 @@ export declare const AuthConfigSchema: import("@sinclair/typebox").TObject<{
334
334
  breachedPasswords: import("@sinclair/typebox").TBoolean;
335
335
  captchaSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
336
336
  captchaOnReset: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
337
+ emailChangeStepUp: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
337
338
  allowedRedirectOrigins: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
338
339
  inviteOnly: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
339
340
  verifyLinkEvicts: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
package/dist/vxil-main.js CHANGED
@@ -4079,6 +4079,12 @@ var AuthConfigSchema = Type.Object({
4079
4079
  // Optional, NO default (absent = false): every existing manifest folds
4080
4080
  // byte-identically and reset stays ungated. Inert without captchaSecretRef.
4081
4081
  captchaOnReset: Type.Optional(Type.Boolean()),
4082
+ // AUTH-EMAIL-CHANGE-GUARD (2026-10-13): when true, POST /email/change/request
4083
+ // answers 403 step_up_required unless THIS session passed a step-up in the
4084
+ // last 300 s (the password-change mechanism: auth.sessions.elevated_at).
4085
+ // Optional, NO default (absent = false) — existing manifests fold unchanged.
4086
+ // Requires otp.enabled (cross-field rule in validateFeatureConfig).
4087
+ emailChangeStepUp: Type.Optional(Type.Boolean()),
4082
4088
  allowedRedirectOrigins: Type.Optional(Type.Array(
4083
4089
  Type.String({ minLength: 8, maxLength: 253, pattern: REDIRECT_ORIGIN_PATTERN }),
4084
4090
  { maxItems: 32 }
@@ -5536,9 +5542,9 @@ function lowerAllTriggerBindings(def, name) {
5536
5542
  return bindings;
5537
5543
  }
5538
5544
  var CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
5539
- var VXIL_CONFIG_PKG_VERSION = "0.19.0";
5540
- var VXIL_SDK_PKG_VERSION = "0.26.0";
5541
- var VXIL_CLI_PKG_VERSION = "0.26.0";
5545
+ var VXIL_CONFIG_PKG_VERSION = "0.19.1";
5546
+ var VXIL_SDK_PKG_VERSION = "0.26.1";
5547
+ var VXIL_CLI_PKG_VERSION = "0.26.2";
5542
5548
  function ensureScaffoldPackageJson(cwd, opts = {}) {
5543
5549
  const file = resolve(cwd, "package.json");
5544
5550
  const wanted = {
@@ -10901,6 +10907,16 @@ function redirectOriginNotice(cfg) {
10901
10907
  }
10902
10908
 
10903
10909
  // src/link.ts
10910
+ function linkEnvLabel(o) {
10911
+ if (o.envFlag !== void 0) return { env: o.envFlag, fromWorkload: false };
10912
+ const wl = o.workload === "production" ? "production" : o.workload === "staging" ? "staging" : o.workload === "development" ? "dev" : void 0;
10913
+ if (wl) {
10914
+ if (o.kept !== void 0 && o.kept === "production" === (wl === "production")) return { env: o.kept, fromWorkload: false };
10915
+ const notice = o.kept !== void 0 ? `relabelled ${o.kept} \u2192 ${wl} from the project's workload; \`--env ${o.kept}\` to keep it` : void 0;
10916
+ return { env: wl, fromWorkload: true, ...notice ? { notice } : {} };
10917
+ }
10918
+ return { env: o.kept ?? envLabelFor(o.base), fromWorkload: false };
10919
+ }
10904
10920
  function data(r) {
10905
10921
  return r.json.data ?? r.json;
10906
10922
  }
@@ -10934,7 +10950,7 @@ async function linkViaSession(dash, cookie, slug, keyName) {
10934
10950
  }
10935
10951
  const apiKey = data(mint).api_key;
10936
10952
  if (!apiKey) throw new Error("mint returned no api_key");
10937
- return { result: { slug, tenant_id: t.id, api_key: apiKey } };
10953
+ return { result: { slug, tenant_id: t.id, api_key: apiKey, ...typeof t.workload === "string" ? { workload: t.workload } : {} } };
10938
10954
  }
10939
10955
  async function linkViaDeviceFlow(dash, slug, io) {
10940
10956
  const now = io.now ?? Date.now;
@@ -10960,7 +10976,7 @@ async function linkViaDeviceFlow(dash, slug, io) {
10960
10976
  if (poll.status === 200) {
10961
10977
  const p = data(poll);
10962
10978
  if (!p.api_key || !p.tenant_id) throw new Error("approval returned no api_key");
10963
- return { slug: p.slug || slug, tenant_id: p.tenant_id, api_key: p.api_key };
10979
+ return { slug: p.slug || slug, tenant_id: p.tenant_id, api_key: p.api_key, ...typeof p.workload === "string" ? { workload: p.workload } : {} };
10964
10980
  }
10965
10981
  throw new Error(`link failed (${errText(poll)})`);
10966
10982
  }
@@ -11520,6 +11536,10 @@ function isWorkload(v) {
11520
11536
  function workloadEnvLabel(w) {
11521
11537
  return w === "production" ? "production" : w === "staging" ? "staging" : "dev";
11522
11538
  }
11539
+ function projectCreateNextHint(slug, workload) {
11540
+ const env = isWorkload(workload) ? ` --env ${workloadEnvLabel(workload)}` : "";
11541
+ return `next: vxil link ${slug} --mint${env} && vxil push (this repo's binding is unchanged until then)`;
11542
+ }
11523
11543
  function quickstartWorkload(workloadFlag, envFlag, dev) {
11524
11544
  if (workloadFlag !== void 0) {
11525
11545
  if (!isWorkload(workloadFlag)) return { error: `--workload must be one of ${PROJECT_WORKLOADS.join(", ")}` };
@@ -15523,6 +15543,17 @@ function formatCompared(c) {
15523
15543
  }
15524
15544
  return parts.join(" \xB7 ");
15525
15545
  }
15546
+ function planFooter(counts, compared) {
15547
+ if (counts.drift > 0) return "(run `vxil push` to apply)";
15548
+ if (counts.ignored > 0 || counts.notCompared > 0) {
15549
+ const parts = [];
15550
+ if (counts.ignored > 0) parts.push(`${counts.ignored} ignored (\`vxil push\` would still apply them)`);
15551
+ if (counts.notCompared > 0) parts.push(`${counts.notCompared} not compared`);
15552
+ return `no unignored drift found \u2014 ${parts.join(" / ")}; not a clean match`;
15553
+ }
15554
+ const what = formatCompared(compared);
15555
+ return `\u2713 no drift \u2014 the target already matches your config${what ? ` (compared ${what})` : ""}; nothing to push`;
15556
+ }
15526
15557
 
15527
15558
  // src/doctor.ts
15528
15559
  var SECRETS_ADVISORY_NOTE = "advisory only: a config push with an unstored `secret:<name>` ref is a WARNING, never a 422 \u2014 vxil's promotion order sets production secrets AFTER the config push (`vxil secrets set <feature>/<name>`); doctor never deletes a stored secret";
@@ -16394,6 +16425,7 @@ async function reuseDevTenantBySlug(dash, cookie, slug) {
16394
16425
  }
16395
16426
 
16396
16427
  // src/try.ts
16428
+ var TRY_ENV_LABEL = "sandbox";
16397
16429
  async function mintTry(fetchImpl, base2, opts = {}) {
16398
16430
  const url = `${base2.replace(/\/$/, "")}/v1/try`;
16399
16431
  const body = opts.turnstileToken ? { turnstile_token: opts.turnstileToken } : {};
@@ -22705,16 +22737,22 @@ async function dashFetch(base2, method, path, body, cookie) {
22705
22737
  function makeDash(base2) {
22706
22738
  return (method, path, body, cookie) => dashFetch(base2, method, path, body, cookie);
22707
22739
  }
22708
- function linkRequest(slug, tenantId, base2) {
22740
+ function linkRequest(slug, tenantId, base2, workload) {
22709
22741
  const as = flag("as");
22710
- const env = flag("env") ?? keptEnvLabel(loadProject(), slug, tenantId, as) ?? envLabelFor(base2);
22711
- return { slug, tenantId, edgeUrl: base2, env, ...as !== void 0 ? { as } : {} };
22742
+ const { env, fromWorkload, notice } = linkEnvLabel({
22743
+ base: base2,
22744
+ envFlag: flag("env"),
22745
+ kept: keptEnvLabel(loadProject(), slug, tenantId, as),
22746
+ workload
22747
+ });
22748
+ if (notice) console.error(`vxil: ${notice}`);
22749
+ return { slug, tenantId, edgeUrl: base2, env, fromWorkload, ...as !== void 0 ? { as } : {} };
22712
22750
  }
22713
- function persistLink(slug, tenantId, apiKey, base2) {
22714
- const req = linkRequest(slug, tenantId, base2);
22751
+ function persistLink(slug, tenantId, apiKey, base2, workload) {
22752
+ const req = linkRequest(slug, tenantId, base2, workload);
22715
22753
  const plan = planLinkBinding(loadProject(), req);
22716
22754
  if ("error" in plan) fail(plan.error);
22717
- const envWarn = envLabelWarning(req.env, base2);
22755
+ const envWarn = req.fromWorkload ? null : envLabelWarning(req.env, base2);
22718
22756
  if (envWarn) console.error(`vxil: ${envWarn}`);
22719
22757
  const creds = loadCredentials();
22720
22758
  creds.keys = { ...creds.keys, [slug]: apiKey };
@@ -23168,7 +23206,7 @@ async function runApply(apply, sel = "prod", opts = {}) {
23168
23206
  const gate = !apply && !!opts.gate;
23169
23207
  const explain = !apply && !!opts.explain;
23170
23208
  const { api, target } = requireApi(sel, apply ? { write: "push" } : gate ? { exitCode: 2 } : {});
23171
- if (!apply) console.error(targetBanner(target, gate ? "diff" : "plan"));
23209
+ if (!apply) console.error(targetBanner(target, gate || opts.verb === "diff" ? "diff" : "plan"));
23172
23210
  const report = { drift: 0, ignored: 0, errors: 0 };
23173
23211
  const pushProblems = { hard: [], strict: [] };
23174
23212
  const strictPush = apply && hasFlag("strict");
@@ -23221,8 +23259,10 @@ async function runApply(apply, sel = "prod", opts = {}) {
23221
23259
  if (gate) report.errors++;
23222
23260
  }
23223
23261
  }
23262
+ let uncompared = 0;
23224
23263
  const notCompared = (u, hint) => {
23225
23264
  console.log(formatNotCompared(u, hint));
23265
+ uncompared++;
23226
23266
  if (gate) report.errors++;
23227
23267
  };
23228
23268
  const runSection = async (label, fn) => {
@@ -23343,6 +23383,7 @@ ${r.feature}.${r.datum} (declared in config):`);
23343
23383
  console.log(formatDeclaredApiState(r, { strict: strictApiState }));
23344
23384
  if (r.skipped) continue;
23345
23385
  if (r.refused) {
23386
+ uncompared++;
23346
23387
  if (gate) report.errors++;
23347
23388
  else console.error(`vxil: warning \u2014 ${r.feature}.${r.datum} not converged: ${r.refused}`);
23348
23389
  if (apply) pushProblems.strict.push(`${r.feature}.${r.datum} refused: ${r.refused}`);
@@ -23350,6 +23391,7 @@ ${r.feature}.${r.datum} (declared in config):`);
23350
23391
  }
23351
23392
  if (r.deferred) {
23352
23393
  console.error(`vxil: warning \u2014 ${r.feature}.${r.datum} not converged yet: ${r.deferred}`);
23394
+ uncompared++;
23353
23395
  if (gate) report.errors++;
23354
23396
  if (apply) pushProblems.strict.push(`${r.feature}.${r.datum} deferred: ${r.deferred}`);
23355
23397
  continue;
@@ -23511,6 +23553,7 @@ api state \u2014 ${self.target.tenantSlug ?? "(env key)"} vs ${other.target.tena
23511
23553
  for (const u of b2.unavailable) notCompared(u, `--against ${other.target.tenantSlug ?? ""}`.trim());
23512
23554
  for (const w of [...truncationWarnings(a, "the target"), ...truncationWarnings(b2, "--against")]) {
23513
23555
  console.log(w);
23556
+ uncompared++;
23514
23557
  if (gate) report.errors++;
23515
23558
  }
23516
23559
  if (a.unavailable.length || b2.unavailable.length) return;
@@ -23556,7 +23599,8 @@ vxil diff \u2192 ${report.drift} drift item(s), ${report.ignored} ignored, ${rep
23556
23599
  }
23557
23600
  process.exit(code);
23558
23601
  }
23559
- if (!apply) console.log("\n(run `vxil push` to apply)");
23602
+ if (!apply) console.log(`
23603
+ ${planFooter({ drift: report.drift, ignored: report.ignored, notCompared: uncompared }, compared)}`);
23560
23604
  if (apply && !hasFlag("no-gen")) await runGen(sel);
23561
23605
  if (apply) {
23562
23606
  const exitWith = pushExitCode(pushProblems, strictPush, target.slot === "primary" && target.tenantSlug ? { slug: target.tenantSlug } : {});
@@ -24346,7 +24390,7 @@ try {
24346
24390
  tenant_id: result.tenant_id,
24347
24391
  slug: result.slug,
24348
24392
  edge_url: base2,
24349
- env: flag("env") ?? envLabelFor(base2),
24393
+ env: flag("env") ?? TRY_ENV_LABEL,
24350
24394
  ephemeral: true,
24351
24395
  ...result.expires_at ? { expires_at: result.expires_at } : {}
24352
24396
  });
@@ -24422,7 +24466,7 @@ try {
24422
24466
  if (linkSession) {
24423
24467
  const r = await linkViaSession(dash, linkSession, slug, keyName);
24424
24468
  if ("result" in r) {
24425
- const bound = persistLink(r.result.slug, r.result.tenant_id, r.result.api_key, base2);
24469
+ const bound = persistLink(r.result.slug, r.result.tenant_id, r.result.api_key, base2, r.result.workload);
24426
24470
  console.log(`\u2713 linked '${slug}' ${bound} (minted a key via your dashboard session)`);
24427
24471
  break;
24428
24472
  }
@@ -24436,7 +24480,7 @@ try {
24436
24480
  log: (l) => console.log(l),
24437
24481
  sleep: (ms) => new Promise((r) => setTimeout(r, ms))
24438
24482
  });
24439
- const bound = persistLink(linked.slug, linked.tenant_id, linked.api_key, base2);
24483
+ const bound = persistLink(linked.slug, linked.tenant_id, linked.api_key, base2, linked.workload);
24440
24484
  console.log(`
24441
24485
  \u2713 linked '${linked.slug}' ${bound} (approved in the browser)`);
24442
24486
  }
@@ -24794,7 +24838,7 @@ try {
24794
24838
  const { project } = await withDashSession(dash, base2, (c) => createProjectViaSession(dash, c, p.body));
24795
24839
  if (jsonOut) console.log(JSON.stringify(project, null, 2));
24796
24840
  else console.log(`\u2713 created '${project.slug}' (${project.display_name}, ${project.tier}${project.workload ? `, ${project.workload}` : ""}) \u2014 no key and no features yet`);
24797
- console.error(`next: vxil link ${project.slug} --mint && vxil push (this repo's binding is unchanged)`);
24841
+ console.error(projectCreateNextHint(project.slug, project.workload));
24798
24842
  break;
24799
24843
  }
24800
24844
  if (sub === "workload") {
@@ -25049,6 +25093,7 @@ ${formatSummary(state)}`);
25049
25093
  await runApply(false, sel, {
25050
25094
  explain: hasFlag("explain"),
25051
25095
  gate: against !== void 0 || hasFlag("exit-code"),
25096
+ verb: "diff",
25052
25097
  ...hasFlag("api-state") ? { apiState: { self: selector(), against: againstSelector(against) } } : {}
25053
25098
  });
25054
25099
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/cli",
3
- "version": "0.26.0",
3
+ "version": "0.26.2",
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.19.0",
49
- "@vxil/feature-configs": "0.17.0",
48
+ "@vxil/config": "0.19.1",
49
+ "@vxil/feature-configs": "0.17.1",
50
50
  "@vxil/runtime": "0.0.1",
51
51
  "@vxil/types": "0.0.1",
52
52
  "miniflare": "^4.20260730.0"