@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.
- package/dist/_vxil-feature-configs.d.ts +19 -0
- package/dist/vxil-main.js +125 -17
- package/package.json +3 -3
|
@@ -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(
|
|
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:
|
|
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:
|
|
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(
|
|
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.
|
|
9035
|
-
VXIL_SDK_PKG_VERSION = "0.35.
|
|
9036
|
-
VXIL_CLI_PKG_VERSION = "0.
|
|
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
|
|
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` (
|
|
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
|
|
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.
|
|
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.
|
|
49
|
-
"@vxil/feature-configs": "0.
|
|
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"
|