@vxil/cli 0.35.2 → 0.36.1

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.
@@ -73,6 +73,24 @@ export declare const SNS_TOPIC_ARN_PATTERN = "^arn:aws(-[a-z]+)?:sns:[a-z0-9-]+:
73
73
  * a list so a newly launched SES region needs no vxil release; a typo still
74
74
  * fails at `vxil push` (TypeBox), never as a DNS error on the first send. */
75
75
  export declare const SES_REGION_PATTERN = "^[a-z]{2}(-[a-z]+)+-\\d$";
76
+ /** (w45 W44-WEBHOOK-SECRETREF-PATTERN) the ONE pattern every webhook secret
77
+ * ref shares (notifications `webhookSecretRef`, payments `stripe` / `paddle`
78
+ * / `revenuecat` `webhookSecretRef`): a secret-store name, letters, digits or
79
+ * '_', at most 64, with the optional `secret:` prefix both resolvers strip
80
+ * (payments-v1 resolveProviderSecrets, notifications-v1
81
+ * resolveTenantSecretRef). Stored secret names are lowercase-only
82
+ * (control-plane SECRET_NAME_RE), so both resolvers LOWERCASE the name after
83
+ * the strip: a capital ref reads its lowercase row. Upper case is accepted
84
+ * (founder decision D-k2-1 option (a), parked; built per the w45 brief) so
85
+ * this pattern refuses no stored config: the w45 read-only count found
86
+ * stored refs with capitals on staging, which never resolved before the
87
+ * lowercasing (so they were not live). A typo shape (a space, '-', '.', a
88
+ * path, an empty or over-long name) is refused at push instead of failing
89
+ * closed only when a webhook arrives. */
90
+ export declare const WEBHOOK_SECRET_REF_PATTERN = "^(secret:)?[A-Za-z0-9_]{1,64}$";
91
+ /** The validator's message for a ref that misses WEBHOOK_SECRET_REF_PATTERN
92
+ * (TypeBox's own reads "Expected string to match '…'"). */
93
+ export declare const WEBHOOK_SECRET_REF_MESSAGE = "a secret ref is the name of a stored secret: lowercase letters, numbers or '_' (capitals are read as lowercase), at most 64 characters, optionally prefixed 'secret:'";
76
94
  export declare const NotificationsConfigSchema: import("@sinclair/typebox").TObject<{
77
95
  enabled: import("@sinclair/typebox").TBoolean;
78
96
  fromEmail: import("@sinclair/typebox").TString;
@@ -834,6 +852,7 @@ export declare const PaymentsConfigSchema: import("@sinclair/typebox").TObject<{
834
852
  webhookSecretRef: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
835
853
  acceptSandbox: import("@sinclair/typebox").TBoolean;
836
854
  sandboxUsers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
855
+ testStore: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"accept">, import("@sinclair/typebox").TLiteral<"reject">]>>;
837
856
  }>>;
838
857
  paypal: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TObject<{
839
858
  clientIdRef: import("@sinclair/typebox").TString;
package/dist/vxil-main.js CHANGED
@@ -5322,7 +5322,7 @@ function isDeclaredOnOwnerErase(v) {
5322
5322
  function onOwnerEraseWidens(declared, remote) {
5323
5323
  return declared === "delete" && remote !== "delete";
5324
5324
  }
5325
- var FN_RETRY_MAX_ATTEMPTS, FN_FORWARD_HEADERS_MAX, FN_SERVER_SCOPES_MAX, NOTIF_OVERRIDE_SUBJECT_MAX, NOTIF_OVERRIDE_BODY_MAX, NOTIF_OVERRIDE_MAX_BYTES, SNS_TOPIC_ARN_PATTERN, SES_REGION_PATTERN, NotificationsConfigSchema, GENERATION_DEFAULTS, GENERATION_BOUNDS, JobsConfigSchema, OAuthRefsSchema, AppleRefsSchema, OidcProviderSchema, REDIRECT_ORIGIN_PATTERN, AuthConfigSchema, RL_POLICY_NAME_PATTERN, RL_MAX_DECLARED_POLICIES, DeclaredRlPolicySchema, RateLimitsConfigSchema, FilesConfigSchema, DeclaredWebhookSubscriptionSchema, WebhooksConfigSchema, CommentsConfigSchema, CMS_MAX_COLLECTIONS_CEILING, CMS_QUERY_DEFAULTS, CmsConfigSchema, McpConfigSchema, RealtimeConfigSchema, PresenceConfigSchema, DeclaredOrgRoleSchema, OrgsConfigSchema, ActivityFeedConfigSchema, SEARCH_COLLECTION_DECLARED_CAP, SEARCH_COLLECTION_DIMS, DeclaredSearchCollectionSchema, HybridLaneWeights, VectorSearchConfigSchema, AI_TEMPLATE_NAME_PATTERN, AI_MAX_DECLARED_TEMPLATES, DeclaredAiTemplateSchema, AiConfigSchema, RagConfigSchema, PaymentsConfigSchema, FunctionsConfigSchema, CopilotConfigSchema, DENY_FUNCTION_SCOPES, FEATURE_KEYS, CMS_RETAIN_DAYS_MAX, CMS_DELETED_RETAIN_DAYS_MAX, CMS_DELETED_RETAIN_DAYS_DEFAULT;
5325
+ var FN_RETRY_MAX_ATTEMPTS, FN_FORWARD_HEADERS_MAX, FN_SERVER_SCOPES_MAX, NOTIF_OVERRIDE_SUBJECT_MAX, NOTIF_OVERRIDE_BODY_MAX, NOTIF_OVERRIDE_MAX_BYTES, SNS_TOPIC_ARN_PATTERN, SES_REGION_PATTERN, WEBHOOK_SECRET_REF_PATTERN, webhookSecretRef, NotificationsConfigSchema, GENERATION_DEFAULTS, GENERATION_BOUNDS, JobsConfigSchema, OAuthRefsSchema, AppleRefsSchema, OidcProviderSchema, REDIRECT_ORIGIN_PATTERN, AuthConfigSchema, RL_POLICY_NAME_PATTERN, RL_MAX_DECLARED_POLICIES, DeclaredRlPolicySchema, RateLimitsConfigSchema, FilesConfigSchema, DeclaredWebhookSubscriptionSchema, WebhooksConfigSchema, CommentsConfigSchema, CMS_MAX_COLLECTIONS_CEILING, CMS_QUERY_DEFAULTS, CmsConfigSchema, McpConfigSchema, RealtimeConfigSchema, PresenceConfigSchema, DeclaredOrgRoleSchema, OrgsConfigSchema, ActivityFeedConfigSchema, SEARCH_COLLECTION_DECLARED_CAP, SEARCH_COLLECTION_DIMS, DeclaredSearchCollectionSchema, HybridLaneWeights, VectorSearchConfigSchema, AI_TEMPLATE_NAME_PATTERN, AI_MAX_DECLARED_TEMPLATES, DeclaredAiTemplateSchema, AiConfigSchema, RagConfigSchema, PaymentsConfigSchema, FunctionsConfigSchema, CopilotConfigSchema, DENY_FUNCTION_SCOPES, FEATURE_KEYS, CMS_RETAIN_DAYS_MAX, CMS_DELETED_RETAIN_DAYS_MAX, CMS_DELETED_RETAIN_DAYS_DEFAULT;
5326
5326
  var init_src = __esm({
5327
5327
  "../feature-configs/src/index.ts"() {
5328
5328
  "use strict";
@@ -5354,6 +5354,8 @@ var init_src = __esm({
5354
5354
  NOTIF_OVERRIDE_MAX_BYTES = 64 * 1024;
5355
5355
  SNS_TOPIC_ARN_PATTERN = "^arn:aws(-[a-z]+)?:sns:[a-z0-9-]+:\\d{12}:[A-Za-z0-9_-]{1,256}$";
5356
5356
  SES_REGION_PATTERN = "^[a-z]{2}(-[a-z]+)+-\\d$";
5357
+ WEBHOOK_SECRET_REF_PATTERN = "^(secret:)?[A-Za-z0-9_]{1,64}$";
5358
+ webhookSecretRef = () => Type.String({ pattern: WEBHOOK_SECRET_REF_PATTERN });
5357
5359
  NotificationsConfigSchema = Type.Object({
5358
5360
  enabled: Type.Boolean({ default: true }),
5359
5361
  fromEmail: Type.String({ format: "email" }),
@@ -5369,7 +5371,7 @@ var init_src = __esm({
5369
5371
  // instead of the platform-wide PROVIDER_WEBHOOK_SECRET, binding the signature
5370
5372
  // to the tenant so a signed event for tenant A can never validate at tenant
5371
5373
  // B's webhook URL (mirrors payments revenuecat.webhookSecretRef).
5372
- webhookSecretRef: Type.Optional(Type.String()),
5374
+ webhookSecretRef: Type.Optional(webhookSecretRef()),
5373
5375
  // 'mock' exists for staging/e2e (deterministic provider); 'resend' is MVP;
5374
5376
  // 'ses' (2026-09-19) is the second real provider — Amazon SES v2, BYO IAM
5375
5377
  // keys, same delivery/suppression/preference pipeline as resend.
@@ -6722,12 +6724,12 @@ var init_src = __esm({
6722
6724
  // "configure it or not", not each inner ref — guide ch. 6, auth).
6723
6725
  stripe: Type.Optional(Type.Object({
6724
6726
  secretKeyRef: Type.String(),
6725
- webhookSecretRef: Type.String(),
6727
+ webhookSecretRef: webhookSecretRef(),
6726
6728
  taxEnabled: Type.Boolean({ default: false })
6727
6729
  })),
6728
6730
  paddle: Type.Optional(Type.Object({
6729
6731
  apiKeyRef: Type.String(),
6730
- webhookSecretRef: Type.String(),
6732
+ webhookSecretRef: webhookSecretRef(),
6731
6733
  sandbox: Type.Boolean({ default: false })
6732
6734
  })),
6733
6735
  revenuecat: Type.Optional(Type.Object({
@@ -6743,7 +6745,7 @@ var init_src = __esm({
6743
6745
  // verification, it fails CLOSED: every delivery is 401 bad_signature with a
6744
6746
  // `sig_failed` row that can never be reprocessed. Mirrors stripe/paddle
6745
6747
  // webhookSecretRef.
6746
- webhookSecretRef: Type.Optional(Type.String()),
6748
+ webhookSecretRef: Type.Optional(webhookSecretRef()),
6747
6749
  // Environment integrity. RevenueCat posts SANDBOX
6748
6750
  // and PRODUCTION events to the SAME webhook with the same auth header, so a
6749
6751
  // sandbox purchase would otherwise fold into production entitlements. A
@@ -6761,7 +6763,19 @@ var init_src = __esm({
6761
6763
  // TestFlight purchase and is refused by the CLI's production promotion
6762
6764
  // gate). No default on purpose (Value.Default would materialize it into
6763
6765
  // every RC manifest); absent = an empty list.
6764
- sandboxUsers: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 20 }))
6766
+ sandboxUsers: Type.Optional(Type.Array(Type.String({ minLength: 1, maxLength: 256 }), { maxItems: 20 })),
6767
+ // (2026-11-03) RevenueCat Test Store purchases.
6768
+ // A Test Store purchase is SANDBOX data that needs no store account,
6769
+ // invite or license tester (anyone with the app's public `test_` key can
6770
+ // make one), so `acceptSandbox: true` on a production project would fold
6771
+ // it. 'reject' refuses every delivery whose RAW RevenueCat event says
6772
+ // `store: TEST_STORE` (and every sync/restore snapshot from the Test
6773
+ // Store) BEFORE any other environment rule, `sandboxUsers` included: the
6774
+ // delivery is kept as outcome 'rejected_test_store' (200, never folded).
6775
+ // Absent = 'accept' (today's behaviour). No schema default on purpose
6776
+ // (the sandboxUsers rule: Value.Default would materialize it into every RC
6777
+ // manifest); the reader (payments-v1 environmentAccepted) owns the default.
6778
+ testStore: Type.Optional(Type.Union([Type.Literal("accept"), Type.Literal("reject")]))
6765
6779
  })),
6766
6780
  paypal: Type.Optional(Type.Object({
6767
6781
  clientIdRef: Type.String(),
@@ -9031,9 +9045,9 @@ var init_lib = __esm({
9031
9045
  timeoutMs: { min: 1e3, max: 12e4 }
9032
9046
  };
9033
9047
  CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
9034
- VXIL_CONFIG_PKG_VERSION = "0.26.1";
9035
- VXIL_SDK_PKG_VERSION = "0.35.1";
9036
- VXIL_CLI_PKG_VERSION = "0.35.2";
9048
+ VXIL_CONFIG_PKG_VERSION = "0.27.0";
9049
+ VXIL_SDK_PKG_VERSION = "0.35.3";
9050
+ VXIL_CLI_PKG_VERSION = "0.36.1";
9037
9051
  SCAFFOLD_NODE_TYPES_RANGE = "^24.0.0";
9038
9052
  SCAFFOLD_TSCONFIG = {
9039
9053
  compilerOptions: {
@@ -13341,7 +13355,7 @@ function promotionRefusals(cfg, allowed) {
13341
13355
  out.push({
13342
13356
  kind: "sandbox",
13343
13357
  flag: PROMOTION_OVERRIDE_FLAGS.sandbox,
13344
- message: `sandbox provider flags [${sandboxes.join(", ")}] would ship to a live money path (a sandbox key signs with the wrong secret; revenuecat.acceptSandbox folds EVERY TestFlight / test-track purchase into live entitlements \u2014 for store review, list the reviewer app_user_ids in revenuecat.sandboxUsers instead, which a production push allows). Set them false, or override with --${PROMOTION_OVERRIDE_FLAGS.sandbox}.`
13358
+ message: `sandbox provider flags [${sandboxes.join(", ")}] would ship to a live money path (a sandbox key signs with the wrong secret; revenuecat.acceptSandbox folds EVERY TestFlight / test-track purchase into live entitlements \u2014 for store review, list the reviewer app_user_ids in revenuecat.sandboxUsers instead, which a production push allows; if you override it for testers, also set revenuecat.testStore: 'reject', or anyone with the Test Store key folds a purchase). Set them false, or override with --${PROMOTION_OVERRIDE_FLAGS.sandbox}.`
13345
13359
  });
13346
13360
  }
13347
13361
  const devOrigins = devRedirectOrigins(redirectOrigins(cfg));
@@ -17505,13 +17519,13 @@ var TOOLS = [
17505
17519
  {
17506
17520
  name: "payments_list_webhook_events",
17507
17521
  feature: "payments",
17508
- description: "List provider webhook deliveries from the event log, newest-first: outcome (received|processed|error|sig_failed|parse_failed|reprocessed|ignored|unowned|rejected_environment), the provider-reported environment (production|sandbox), signature status, error, and timestamps per delivery. Use it to diagnose billing sync issues (e.g. an unmapped price or product, or a RevenueCat entitlement id that is not a ledger.tierMap key (with no ledger.priceMap hit) \u2192 outcome error, a misconfigured webhook secret producing sig_failed rows, sandbox deliveries landing as rejected_environment). An unmapped one-off product lands `error` unless you set ledger.unmappedProduct:'ignore', which lands it `ignored` instead. Read-only \u2014 needs payments:read. Server-side key only (403 server_only on a thin-client/end-user key) \u2014 the log is tenant-wide and carries provider payloads. Payloads are detail-only (console surface).",
17522
+ description: "List provider webhook deliveries from the event log, newest-first: outcome (received|processed|error|sig_failed|parse_failed|reprocessed|ignored|unowned|rejected_environment|rejected_test_store), the provider-reported environment (production|sandbox), signature status, error, and timestamps per delivery. Use it to diagnose billing sync issues (e.g. an unmapped price or product, or a RevenueCat entitlement id that is not a ledger.tierMap key (with no ledger.priceMap hit) \u2192 outcome error, a misconfigured webhook secret producing sig_failed rows, sandbox deliveries landing as rejected_environment, RevenueCat Test Store purchases refused under revenuecat.testStore 'reject' landing as rejected_test_store). An unmapped one-off product lands `error` unless you set ledger.unmappedProduct:'ignore', which lands it `ignored` instead. Read-only \u2014 needs payments:read. Server-side key only (403 server_only on a thin-client/end-user key) \u2014 the log is tenant-wide and carries provider payloads. Payloads are detail-only (console surface).",
17509
17523
  inputSchema: {
17510
17524
  type: "object",
17511
17525
  properties: {
17512
17526
  provider: { type: "string", enum: ["mock", "stripe", "paddle", "revenuecat", "paypal"] },
17513
17527
  event_type: { type: "string", description: "Exact event type filter, e.g. 'subscription.updated'." },
17514
- outcome: { type: "string", enum: ["received", "processed", "error", "sig_failed", "parse_failed", "reprocessed", "ignored", "unowned", "rejected_environment"] },
17528
+ outcome: { type: "string", enum: ["received", "processed", "error", "sig_failed", "parse_failed", "reprocessed", "ignored", "unowned", "rejected_environment", "rejected_test_store"] },
17515
17529
  environment: { type: "string", enum: ["production", "sandbox"], description: "The provider-reported environment of the delivery (a separate axis from the API key label)." },
17516
17530
  provider_charge_id: { type: "string", maxLength: 200, description: "Exact match on the normalised charge id (the same provider_charge_id a payments.charge.* event carries): the delivery that recorded that charge plus any refund/dispute against it \u2014 usually one row, so ONE detail read (GET /v1/payments/webhook-events/{eventId}) fetches the verified payload. Rows received before 2026-09-19 carry no charge id." },
17517
17531
  provider_evt_id: { type: "string", maxLength: 200, description: "Exact match on the provider's own event id (the provider_evt_id every row shows)." },
@@ -19377,6 +19391,7 @@ var EXTRA_KNOWN_FLAGS = [
19377
19391
  "include-empty",
19378
19392
  "include-password-hashes",
19379
19393
  "with-password-hashes",
19394
+ "password-hash-column",
19380
19395
  "with-oauth-identities",
19381
19396
  "keep-source-id",
19382
19397
  "keep-timestamps",
@@ -20471,7 +20486,7 @@ function inferMapping(source, opts = {}) {
20471
20486
  const cmsNames = [...cmsByName.keys()];
20472
20487
  const loadOrder = topoOrder(cmsNames, source.foreignKeys, profile?.table, warnings);
20473
20488
  if (source.authUsers?.present) {
20474
- residuals.push(source.authUsers.hasPasswordHash ? "passwords: hashes are NOT imported by default \u2014 `vxil migrate data --with-password-hashes` carries them (bcrypt, verified and rehashed at each user's first sign-in); without it users re-auth via magic-link/OTP/social/reset on first sign-in" : "auth: credentials do not transfer \u2014 users re-auth via their existing OTP/magic-link/social flow post-cutover");
20489
+ residuals.push(source.authUsers.hasPasswordHash ? "passwords: hashes are NOT imported by default \u2014 `vxil migrate data --with-password-hashes` carries them (bcrypt, verified and rehashed at each user's first sign-in; a generic-pg / csv-json source also names the column with `--password-hash-column <column>`); without it users re-auth via magic-link/OTP/social/reset on first sign-in" : "auth: credentials do not transfer \u2014 users re-auth via their existing OTP/magic-link/social flow post-cutover");
20475
20490
  }
20476
20491
  for (const chk of source.checks ?? []) {
20477
20492
  if (!cmsByName.has(lastSeg(chk.table))) continue;
@@ -21113,7 +21128,7 @@ function credentialEntry(row2, withHashes) {
21113
21128
  async function loadCredentials2({ deps, adapter, state, save, opts, delta = false }) {
21114
21129
  const result = { table: KEY, read: 0, written: 0, skipped: 0, errors: [], cursor: null };
21115
21130
  if (!adapter.readCredentials) {
21116
- result.errors.push("credentials: this source has no credential reader (supabase only today) \u2014 post the hashes yourself with the SDK `auth.users.import()`, or drop --with-password-hashes / --oidc-issuer");
21131
+ result.errors.push("credentials: this source has no credential reader \u2014 post the hashes yourself with the SDK `auth.users.import()`, or drop --with-password-hashes / --oidc-issuer");
21117
21132
  return result;
21118
21133
  }
21119
21134
  const cursors = cursorsFor(state, delta);
@@ -21128,14 +21143,16 @@ async function loadCredentials2({ deps, adapter, state, save, opts, delta = fals
21128
21143
  const reasons = /* @__PURE__ */ new Map();
21129
21144
  let would = deps.dryRun ? {} : null;
21130
21145
  let withPassword = 0;
21146
+ let refused = 0;
21131
21147
  let withLinks = 0;
21132
21148
  let linked = 0;
21133
- const readOpts = opts.withIdentities ? { identities: true } : void 0;
21149
+ const readOpts = opts.withIdentities || opts.withHashes ? { ...opts.withIdentities ? { identities: true } : {}, ...opts.withHashes ? { hashes: true } : {} } : void 0;
21134
21150
  for await (const batch of adapter.readCredentials(cursors[KEY] ?? void 0, readOpts)) {
21135
21151
  const entries = [];
21136
21152
  const digests = [];
21137
21153
  for (const row2 of batch.rows) {
21138
21154
  result.read++;
21155
+ if (opts.withHashes && row2.hash_refused) refused++;
21139
21156
  const entry = credentialEntry(row2, opts.withHashes);
21140
21157
  const h = rowHash({ ...entry, issuer: opts.oidcIssuer ?? null });
21141
21158
  if (sent[row2.id] === h) {
@@ -21217,6 +21234,9 @@ async function loadCredentials2({ deps, adapter, state, save, opts, delta = fals
21217
21234
  const list = [...reasons].map(([r, n]) => `${r}=${n}`).join(" ");
21218
21235
  deps.log(`credentials: ${[...reasons.values()].reduce((a, b2) => a + b2, 0)} user(s) not imported (${list}) \u2014 they sign in once another way (magic link, OTP, a reset); \`not_registered\` means the users step did not load them`);
21219
21236
  }
21237
+ if (refused > 0) {
21238
+ deps.log(`credentials: ${refused} password hash(es) are not bcrypt (cost 4\u201310), the only format the import verifies \u2014 ${deps.dryRun ? "they would NOT be sent" : "NOT sent"}; those users are imported without a password and sign in once another way (magic link, OTP, a reset)`);
21239
+ }
21220
21240
  result.cursor = deps.dryRun ? cursors[KEY] ?? null : null;
21221
21241
  return result;
21222
21242
  }
@@ -22525,7 +22545,7 @@ ${notes.length ? `${notes.join("\n")}
22525
22545
  `;
22526
22546
  }
22527
22547
  var STANDING_CAVEATS = [
22528
- "Passwords: imported only with `vxil migrate data --with-password-hashes` (supabase sources: bcrypt, verified and replaced at each user's first sign-in \u2014 no forced re-sign-in); without it, or for a hash it cannot accept, users re-auth once via magic-link/OTP/social/reset.",
22548
+ "Passwords: imported only with `vxil migrate data --with-password-hashes` (bcrypt, verified and replaced at each user's first sign-in \u2014 no forced re-sign-in; generic-pg / csv-json sources also name the hash column with `--password-hash-column <column>`); without it, or for a hash it cannot accept, users re-auth once via magic-link/OTP/social/reset.",
22529
22549
  "Push device tokens: vxil has no push provider \u2014 token rows migrate as ordinary owner-scoped cms app data, and sending is a function over egress (the push-notifications blueprint); or skip those tables and let clients re-enroll post-cutover.",
22530
22550
  "Subscriptions / entitlements / tier: NEVER seeded (provider-derived truth) \u2014 rebuild from your live provider webhooks after cutover.",
22531
22551
  "DB triggers / RPCs / pg_cron / row-level access rules: not auto-migrated \u2014 hand-wire as vxil functions (cms-hook/cron/http triggers; cms-hooks are async post-commit, never in-transaction) + per-collection ownerField.",
@@ -24905,6 +24925,36 @@ export default defineConfig({
24905
24925
  import { readFileSync as readFileSync8, readdirSync, existsSync as existsSync8, statSync } from "node:fs";
24906
24926
  import { basename as basename3, extname, join as join4, resolve as resolve6 } from "node:path";
24907
24927
 
24928
+ // src/migrate/adapters/hashImport.ts
24929
+ var BCRYPT_IMPORT_RE = /^\$2[aby]\$(\d{2})\$[./A-Za-z0-9]{53}$/;
24930
+ var BCRYPT_IMPORT_MIN_COST = 4;
24931
+ var BCRYPT_IMPORT_MAX_COST = 10;
24932
+ function importableBcrypt(v) {
24933
+ if (typeof v !== "string") return null;
24934
+ const h = v.trim();
24935
+ const m = BCRYPT_IMPORT_RE.exec(h);
24936
+ if (!m) return null;
24937
+ const cost = Number(m[1]);
24938
+ return cost >= BCRYPT_IMPORT_MIN_COST && cost <= BCRYPT_IMPORT_MAX_COST ? h : null;
24939
+ }
24940
+ function genericCredentialRow(id, raw) {
24941
+ const empty = raw === null || raw === void 0 || typeof raw === "string" && raw.trim() === "";
24942
+ const hash = empty ? null : importableBcrypt(raw);
24943
+ return {
24944
+ id,
24945
+ password_hash: hash,
24946
+ // a generic source has no confirmed-address column vxil can trust
24947
+ email_verified: false,
24948
+ ...!empty && hash === null ? { hash_refused: true } : {}
24949
+ };
24950
+ }
24951
+ function missingHashColumnError(from) {
24952
+ return new Error(`--from ${from} --with-password-hashes needs --password-hash-column <column>: the auth table column that holds each user's bcrypt hash (only bcrypt is imported; other formats are reported and not sent)`);
24953
+ }
24954
+ function noIdentitiesError(from) {
24955
+ return new Error(`--with-oauth-identities reads Supabase auth.identities \u2014 a ${from} source has none; drop the flag`);
24956
+ }
24957
+
24908
24958
  // src/migrate/adapters/identity.ts
24909
24959
  var IDENTITY_COLUMNS = ["id", "email", "display_name", "avatar_url", "raw_user_meta_data"];
24910
24960
  function splitIdentityColumns(columns) {
@@ -25026,11 +25076,13 @@ var CsvJsonAdapter = class {
25026
25076
  dir;
25027
25077
  batchSize;
25028
25078
  sampleSize;
25079
+ passwordHashColumn;
25029
25080
  cache;
25030
25081
  constructor(opts) {
25031
25082
  this.dir = resolve6(opts.dir);
25032
25083
  this.batchSize = opts.batchSize ?? 500;
25033
25084
  this.sampleSize = opts.sampleSize ?? 200;
25085
+ this.passwordHashColumn = opts.passwordHashColumn;
25034
25086
  }
25035
25087
  load() {
25036
25088
  if (this.cache) return this.cache;
@@ -25277,6 +25329,28 @@ var CsvJsonAdapter = class {
25277
25329
  };
25278
25330
  }
25279
25331
  }
25332
+ /** The opt-in credentials read — the authTable's `id` plus the
25333
+ * ONE column the operator named (--password-hash-column), bcrypt only
25334
+ * (hashImport.ts); without --with-password-hashes no hash is read. */
25335
+ async *readCredentials(cursor, opts = {}) {
25336
+ if (opts.identities) throw noIdentitiesError("csv-json");
25337
+ const { schema, tables } = this.load();
25338
+ if (!schema.authUsers?.present || !schema.authUsers.table) {
25339
+ throw new Error("csv-json: no authTable declared in _schema.json \u2014 credentials step unavailable");
25340
+ }
25341
+ const t = tables.get(schema.authUsers.table);
25342
+ const hashCol = opts.hashes ? this.passwordHashColumn : void 0;
25343
+ if (opts.hashes && hashCol === void 0) throw missingHashColumnError("csv-json");
25344
+ if (hashCol !== void 0 && !t.columns.some((c) => c.name === hashCol)) {
25345
+ throw new Error(`--password-hash-column '${hashCol}' is not a column of the authTable '${t.name}'`);
25346
+ }
25347
+ for await (const batch of this.paged(t.rows, cursor)) {
25348
+ yield {
25349
+ rows: batch.rows.filter((r) => r.id !== null && r.id !== void 0 && r.id !== "").map((r) => genericCredentialRow(String(r.id), hashCol !== void 0 ? r[hashCol] : null)),
25350
+ nextCursor: batch.nextCursor
25351
+ };
25352
+ }
25353
+ }
25280
25354
  async *paged(all, cursor) {
25281
25355
  let offset = 0;
25282
25356
  if (cursor !== void 0 && cursor !== "") {
@@ -25562,6 +25636,7 @@ var GenericPgAdapter = class {
25562
25636
  schemas;
25563
25637
  batchSize;
25564
25638
  authTable;
25639
+ passwordHashColumn;
25565
25640
  redactedUrl;
25566
25641
  /** subclass override point (supabase sets 'supabase'). */
25567
25642
  providerName = "generic-pg";
@@ -25574,6 +25649,7 @@ var GenericPgAdapter = class {
25574
25649
  this.batchSize = opts.batchSize ?? 500;
25575
25650
  this.statementTimeoutMs = Math.trunc(opts.statementTimeoutMs ?? 12e4);
25576
25651
  this.authTable = opts.authTable;
25652
+ this.passwordHashColumn = opts.passwordHashColumn;
25577
25653
  this.redactedUrl = redactSourceUrl(opts.url);
25578
25654
  this.sql = src_default(opts.url, {
25579
25655
  max: 1,
@@ -25871,6 +25947,33 @@ var GenericPgAdapter = class {
25871
25947
  const t = await this.resolveTable(source.authUsers.table);
25872
25948
  yield* this.pagedRead(t, this.identityColumns(t).keep, cursor);
25873
25949
  }
25950
+ /** The opt-in credentials read of a generic source — the auth
25951
+ * table's `id` plus the ONE column the operator named
25952
+ * (--password-hash-column), bcrypt only (hashImport.ts). The hashes are
25953
+ * held in memory and sent only in the import body, never logged. Without
25954
+ * --with-password-hashes (an --oidc-issuer-only run) no hash is read. */
25955
+ async *readCredentials(cursor, opts = {}) {
25956
+ if (opts.identities) throw noIdentitiesError(this.providerName);
25957
+ const source = await this.introspect();
25958
+ if (!source.authUsers?.present || !source.authUsers.table) {
25959
+ throw new Error("no auth table configured \u2014 pass --auth-table for generic-pg sources");
25960
+ }
25961
+ const t = await this.resolveTable(source.authUsers.table);
25962
+ const hashCol = opts.hashes ? this.passwordHashColumn : void 0;
25963
+ if (opts.hashes && hashCol === void 0) throw missingHashColumnError(this.providerName);
25964
+ if (hashCol !== void 0 && !t.columns.some((c) => c.name === hashCol)) {
25965
+ throw new Error(`--password-hash-column '${hashCol}' is not a column of the auth table '${t.name}'`);
25966
+ }
25967
+ if (!t.columns.some((c) => c.name === "id")) throw new Error(`the auth table '${t.name}' has no id column`);
25968
+ const pk = t.columns.filter((c) => c.isPk).map((c) => c.name);
25969
+ const cols = [.../* @__PURE__ */ new Set(["id", ...hashCol !== void 0 ? [hashCol] : [], ...pk.length === 1 ? pk : []])];
25970
+ for await (const batch of this.pagedRead(t, cols, cursor)) {
25971
+ yield {
25972
+ rows: batch.rows.filter((r) => r.id !== null && r.id !== void 0 && r.id !== "").map((r) => genericCredentialRow(String(r.id), hashCol !== void 0 ? r[hashCol] : null)),
25973
+ nextCursor: batch.nextCursor
25974
+ };
25975
+ }
25976
+ }
25874
25977
  async close() {
25875
25978
  await this.sql.end({ timeout: 5 });
25876
25979
  }
@@ -26141,6 +26244,7 @@ function pgOpts(opts) {
26141
26244
  url: requireUrl(opts),
26142
26245
  ...opts.schemas !== void 0 ? { schemas: opts.schemas } : {},
26143
26246
  ...opts.authTable !== void 0 ? { authTable: opts.authTable } : {},
26247
+ ...opts.passwordHashColumn !== void 0 ? { passwordHashColumn: opts.passwordHashColumn } : {},
26144
26248
  ...opts.batchSize !== void 0 ? { batchSize: opts.batchSize } : {},
26145
26249
  ...opts.statementTimeoutMs !== void 0 ? { statementTimeoutMs: opts.statementTimeoutMs } : {}
26146
26250
  };
@@ -26155,6 +26259,7 @@ var registry = /* @__PURE__ */ new Map([
26155
26259
  if (!opts.dir) throw new Error("--from csv-json needs --dir <export-directory>");
26156
26260
  return new CsvJsonAdapter({
26157
26261
  dir: opts.dir,
26262
+ ...opts.passwordHashColumn !== void 0 ? { passwordHashColumn: opts.passwordHashColumn } : {},
26158
26263
  ...opts.batchSize !== void 0 ? { batchSize: opts.batchSize } : {}
26159
26264
  });
26160
26265
  }]
@@ -30270,7 +30375,8 @@ function migrateUsage() {
30270
30375
  step on the server: created/kept/skipped by reason, nothing written) \xB7 [--file-links shared-link] stored
30271
30376
  public file URLs become stable shared links instead of object ids
30272
30377
  \xB7 [--with-oauth-identities] carry the Google/Apple/GitHub/Facebook sign-in links (needs
30273
- --with-password-hashes in the same run) \xB7 [--keep-timestamps] keep each row's source
30378
+ --with-password-hashes in the same run) \xB7 generic-pg / csv-json: --with-password-hashes
30379
+ --password-hash-column <column> (bcrypt only; other formats are reported, not sent) \xB7 [--keep-timestamps] keep each row's source
30274
30380
  created_at / updated_at (cms creates; server key only)
30275
30381
  \xB7 [--files-owner <user id>] the owner of every migrated file (default: the path's first
30276
30382
  segment) \xB7 [--files-org-from-prefix] the path's first segment is the WORKSPACE (org_id):
@@ -30293,6 +30399,7 @@ ${migrateUsage()}`);
30293
30399
  const url = flag("url");
30294
30400
  const dir = flag("dir");
30295
30401
  const authTable = flag("auth-table");
30402
+ const passwordHashColumn = flag("password-hash-column");
30296
30403
  const schemas = flag("schemas");
30297
30404
  const batch = flag("batch");
30298
30405
  const sbUrl = process.env.SUPABASE_URL;
@@ -30302,6 +30409,7 @@ ${migrateUsage()}`);
30302
30409
  ...url !== void 0 ? { url } : {},
30303
30410
  ...dir !== void 0 ? { dir } : {},
30304
30411
  ...authTable !== void 0 ? { authTable } : {},
30412
+ ...passwordHashColumn !== void 0 ? { passwordHashColumn } : {},
30305
30413
  ...schemas !== void 0 ? { schemas: schemas.split(",").map((s) => s.trim()).filter(Boolean) } : {},
30306
30414
  ...batch !== void 0 ? { batchSize: Number(batch) } : {},
30307
30415
  ...from === "supabase" && sbUrl && sbKey ? { storage: { url: sbUrl, serviceKey: sbKey } } : {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vxil/cli",
3
- "version": "0.35.2",
3
+ "version": "0.36.1",
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.26.1",
49
- "@vxil/feature-configs": "0.23.2",
48
+ "@vxil/config": "0.27.0",
49
+ "@vxil/feature-configs": "0.24.0",
50
50
  "@vxil/runtime": "0.0.1",
51
51
  "@vxil/types": "0.0.1",
52
52
  "miniflare": "^4.20260730.0"