@vxil/cli 0.35.2 → 0.36.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-main.js +101 -7
- package/package.json +1 -1
package/dist/vxil-main.js
CHANGED
|
@@ -9032,8 +9032,8 @@ var init_lib = __esm({
|
|
|
9032
9032
|
};
|
|
9033
9033
|
CONFIG_FILENAMES = ["vxil.config.ts", "vxil.config.mjs", "vxil.config.js"];
|
|
9034
9034
|
VXIL_CONFIG_PKG_VERSION = "0.26.1";
|
|
9035
|
-
VXIL_SDK_PKG_VERSION = "0.35.
|
|
9036
|
-
VXIL_CLI_PKG_VERSION = "0.
|
|
9035
|
+
VXIL_SDK_PKG_VERSION = "0.35.2";
|
|
9036
|
+
VXIL_CLI_PKG_VERSION = "0.36.0";
|
|
9037
9037
|
SCAFFOLD_NODE_TYPES_RANGE = "^24.0.0";
|
|
9038
9038
|
SCAFFOLD_TSCONFIG = {
|
|
9039
9039
|
compilerOptions: {
|
|
@@ -19377,6 +19377,7 @@ var EXTRA_KNOWN_FLAGS = [
|
|
|
19377
19377
|
"include-empty",
|
|
19378
19378
|
"include-password-hashes",
|
|
19379
19379
|
"with-password-hashes",
|
|
19380
|
+
"password-hash-column",
|
|
19380
19381
|
"with-oauth-identities",
|
|
19381
19382
|
"keep-source-id",
|
|
19382
19383
|
"keep-timestamps",
|
|
@@ -20471,7 +20472,7 @@ function inferMapping(source, opts = {}) {
|
|
|
20471
20472
|
const cmsNames = [...cmsByName.keys()];
|
|
20472
20473
|
const loadOrder = topoOrder(cmsNames, source.foreignKeys, profile?.table, warnings);
|
|
20473
20474
|
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");
|
|
20475
|
+
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
20476
|
}
|
|
20476
20477
|
for (const chk of source.checks ?? []) {
|
|
20477
20478
|
if (!cmsByName.has(lastSeg(chk.table))) continue;
|
|
@@ -21113,7 +21114,7 @@ function credentialEntry(row2, withHashes) {
|
|
|
21113
21114
|
async function loadCredentials2({ deps, adapter, state, save, opts, delta = false }) {
|
|
21114
21115
|
const result = { table: KEY, read: 0, written: 0, skipped: 0, errors: [], cursor: null };
|
|
21115
21116
|
if (!adapter.readCredentials) {
|
|
21116
|
-
result.errors.push("credentials: this source has no credential reader
|
|
21117
|
+
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
21118
|
return result;
|
|
21118
21119
|
}
|
|
21119
21120
|
const cursors = cursorsFor(state, delta);
|
|
@@ -21128,14 +21129,16 @@ async function loadCredentials2({ deps, adapter, state, save, opts, delta = fals
|
|
|
21128
21129
|
const reasons = /* @__PURE__ */ new Map();
|
|
21129
21130
|
let would = deps.dryRun ? {} : null;
|
|
21130
21131
|
let withPassword = 0;
|
|
21132
|
+
let refused = 0;
|
|
21131
21133
|
let withLinks = 0;
|
|
21132
21134
|
let linked = 0;
|
|
21133
|
-
const readOpts = opts.withIdentities ? { identities: true } : void 0;
|
|
21135
|
+
const readOpts = opts.withIdentities || opts.withHashes ? { ...opts.withIdentities ? { identities: true } : {}, ...opts.withHashes ? { hashes: true } : {} } : void 0;
|
|
21134
21136
|
for await (const batch of adapter.readCredentials(cursors[KEY] ?? void 0, readOpts)) {
|
|
21135
21137
|
const entries = [];
|
|
21136
21138
|
const digests = [];
|
|
21137
21139
|
for (const row2 of batch.rows) {
|
|
21138
21140
|
result.read++;
|
|
21141
|
+
if (opts.withHashes && row2.hash_refused) refused++;
|
|
21139
21142
|
const entry = credentialEntry(row2, opts.withHashes);
|
|
21140
21143
|
const h = rowHash({ ...entry, issuer: opts.oidcIssuer ?? null });
|
|
21141
21144
|
if (sent[row2.id] === h) {
|
|
@@ -21217,6 +21220,9 @@ async function loadCredentials2({ deps, adapter, state, save, opts, delta = fals
|
|
|
21217
21220
|
const list = [...reasons].map(([r, n]) => `${r}=${n}`).join(" ");
|
|
21218
21221
|
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
21222
|
}
|
|
21223
|
+
if (refused > 0) {
|
|
21224
|
+
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)`);
|
|
21225
|
+
}
|
|
21220
21226
|
result.cursor = deps.dryRun ? cursors[KEY] ?? null : null;
|
|
21221
21227
|
return result;
|
|
21222
21228
|
}
|
|
@@ -22525,7 +22531,7 @@ ${notes.length ? `${notes.join("\n")}
|
|
|
22525
22531
|
`;
|
|
22526
22532
|
}
|
|
22527
22533
|
var STANDING_CAVEATS = [
|
|
22528
|
-
"Passwords: imported only with `vxil migrate data --with-password-hashes` (
|
|
22534
|
+
"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
22535
|
"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
22536
|
"Subscriptions / entitlements / tier: NEVER seeded (provider-derived truth) \u2014 rebuild from your live provider webhooks after cutover.",
|
|
22531
22537
|
"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 +24911,36 @@ export default defineConfig({
|
|
|
24905
24911
|
import { readFileSync as readFileSync8, readdirSync, existsSync as existsSync8, statSync } from "node:fs";
|
|
24906
24912
|
import { basename as basename3, extname, join as join4, resolve as resolve6 } from "node:path";
|
|
24907
24913
|
|
|
24914
|
+
// src/migrate/adapters/hashImport.ts
|
|
24915
|
+
var BCRYPT_IMPORT_RE = /^\$2[aby]\$(\d{2})\$[./A-Za-z0-9]{53}$/;
|
|
24916
|
+
var BCRYPT_IMPORT_MIN_COST = 4;
|
|
24917
|
+
var BCRYPT_IMPORT_MAX_COST = 10;
|
|
24918
|
+
function importableBcrypt(v) {
|
|
24919
|
+
if (typeof v !== "string") return null;
|
|
24920
|
+
const h = v.trim();
|
|
24921
|
+
const m = BCRYPT_IMPORT_RE.exec(h);
|
|
24922
|
+
if (!m) return null;
|
|
24923
|
+
const cost = Number(m[1]);
|
|
24924
|
+
return cost >= BCRYPT_IMPORT_MIN_COST && cost <= BCRYPT_IMPORT_MAX_COST ? h : null;
|
|
24925
|
+
}
|
|
24926
|
+
function genericCredentialRow(id, raw) {
|
|
24927
|
+
const empty = raw === null || raw === void 0 || typeof raw === "string" && raw.trim() === "";
|
|
24928
|
+
const hash = empty ? null : importableBcrypt(raw);
|
|
24929
|
+
return {
|
|
24930
|
+
id,
|
|
24931
|
+
password_hash: hash,
|
|
24932
|
+
// a generic source has no confirmed-address column vxil can trust
|
|
24933
|
+
email_verified: false,
|
|
24934
|
+
...!empty && hash === null ? { hash_refused: true } : {}
|
|
24935
|
+
};
|
|
24936
|
+
}
|
|
24937
|
+
function missingHashColumnError(from) {
|
|
24938
|
+
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)`);
|
|
24939
|
+
}
|
|
24940
|
+
function noIdentitiesError(from) {
|
|
24941
|
+
return new Error(`--with-oauth-identities reads Supabase auth.identities \u2014 a ${from} source has none; drop the flag`);
|
|
24942
|
+
}
|
|
24943
|
+
|
|
24908
24944
|
// src/migrate/adapters/identity.ts
|
|
24909
24945
|
var IDENTITY_COLUMNS = ["id", "email", "display_name", "avatar_url", "raw_user_meta_data"];
|
|
24910
24946
|
function splitIdentityColumns(columns) {
|
|
@@ -25026,11 +25062,13 @@ var CsvJsonAdapter = class {
|
|
|
25026
25062
|
dir;
|
|
25027
25063
|
batchSize;
|
|
25028
25064
|
sampleSize;
|
|
25065
|
+
passwordHashColumn;
|
|
25029
25066
|
cache;
|
|
25030
25067
|
constructor(opts) {
|
|
25031
25068
|
this.dir = resolve6(opts.dir);
|
|
25032
25069
|
this.batchSize = opts.batchSize ?? 500;
|
|
25033
25070
|
this.sampleSize = opts.sampleSize ?? 200;
|
|
25071
|
+
this.passwordHashColumn = opts.passwordHashColumn;
|
|
25034
25072
|
}
|
|
25035
25073
|
load() {
|
|
25036
25074
|
if (this.cache) return this.cache;
|
|
@@ -25277,6 +25315,28 @@ var CsvJsonAdapter = class {
|
|
|
25277
25315
|
};
|
|
25278
25316
|
}
|
|
25279
25317
|
}
|
|
25318
|
+
/** The opt-in credentials read — the authTable's `id` plus the
|
|
25319
|
+
* ONE column the operator named (--password-hash-column), bcrypt only
|
|
25320
|
+
* (hashImport.ts); without --with-password-hashes no hash is read. */
|
|
25321
|
+
async *readCredentials(cursor, opts = {}) {
|
|
25322
|
+
if (opts.identities) throw noIdentitiesError("csv-json");
|
|
25323
|
+
const { schema, tables } = this.load();
|
|
25324
|
+
if (!schema.authUsers?.present || !schema.authUsers.table) {
|
|
25325
|
+
throw new Error("csv-json: no authTable declared in _schema.json \u2014 credentials step unavailable");
|
|
25326
|
+
}
|
|
25327
|
+
const t = tables.get(schema.authUsers.table);
|
|
25328
|
+
const hashCol = opts.hashes ? this.passwordHashColumn : void 0;
|
|
25329
|
+
if (opts.hashes && hashCol === void 0) throw missingHashColumnError("csv-json");
|
|
25330
|
+
if (hashCol !== void 0 && !t.columns.some((c) => c.name === hashCol)) {
|
|
25331
|
+
throw new Error(`--password-hash-column '${hashCol}' is not a column of the authTable '${t.name}'`);
|
|
25332
|
+
}
|
|
25333
|
+
for await (const batch of this.paged(t.rows, cursor)) {
|
|
25334
|
+
yield {
|
|
25335
|
+
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)),
|
|
25336
|
+
nextCursor: batch.nextCursor
|
|
25337
|
+
};
|
|
25338
|
+
}
|
|
25339
|
+
}
|
|
25280
25340
|
async *paged(all, cursor) {
|
|
25281
25341
|
let offset = 0;
|
|
25282
25342
|
if (cursor !== void 0 && cursor !== "") {
|
|
@@ -25562,6 +25622,7 @@ var GenericPgAdapter = class {
|
|
|
25562
25622
|
schemas;
|
|
25563
25623
|
batchSize;
|
|
25564
25624
|
authTable;
|
|
25625
|
+
passwordHashColumn;
|
|
25565
25626
|
redactedUrl;
|
|
25566
25627
|
/** subclass override point (supabase sets 'supabase'). */
|
|
25567
25628
|
providerName = "generic-pg";
|
|
@@ -25574,6 +25635,7 @@ var GenericPgAdapter = class {
|
|
|
25574
25635
|
this.batchSize = opts.batchSize ?? 500;
|
|
25575
25636
|
this.statementTimeoutMs = Math.trunc(opts.statementTimeoutMs ?? 12e4);
|
|
25576
25637
|
this.authTable = opts.authTable;
|
|
25638
|
+
this.passwordHashColumn = opts.passwordHashColumn;
|
|
25577
25639
|
this.redactedUrl = redactSourceUrl(opts.url);
|
|
25578
25640
|
this.sql = src_default(opts.url, {
|
|
25579
25641
|
max: 1,
|
|
@@ -25871,6 +25933,33 @@ var GenericPgAdapter = class {
|
|
|
25871
25933
|
const t = await this.resolveTable(source.authUsers.table);
|
|
25872
25934
|
yield* this.pagedRead(t, this.identityColumns(t).keep, cursor);
|
|
25873
25935
|
}
|
|
25936
|
+
/** The opt-in credentials read of a generic source — the auth
|
|
25937
|
+
* table's `id` plus the ONE column the operator named
|
|
25938
|
+
* (--password-hash-column), bcrypt only (hashImport.ts). The hashes are
|
|
25939
|
+
* held in memory and sent only in the import body, never logged. Without
|
|
25940
|
+
* --with-password-hashes (an --oidc-issuer-only run) no hash is read. */
|
|
25941
|
+
async *readCredentials(cursor, opts = {}) {
|
|
25942
|
+
if (opts.identities) throw noIdentitiesError(this.providerName);
|
|
25943
|
+
const source = await this.introspect();
|
|
25944
|
+
if (!source.authUsers?.present || !source.authUsers.table) {
|
|
25945
|
+
throw new Error("no auth table configured \u2014 pass --auth-table for generic-pg sources");
|
|
25946
|
+
}
|
|
25947
|
+
const t = await this.resolveTable(source.authUsers.table);
|
|
25948
|
+
const hashCol = opts.hashes ? this.passwordHashColumn : void 0;
|
|
25949
|
+
if (opts.hashes && hashCol === void 0) throw missingHashColumnError(this.providerName);
|
|
25950
|
+
if (hashCol !== void 0 && !t.columns.some((c) => c.name === hashCol)) {
|
|
25951
|
+
throw new Error(`--password-hash-column '${hashCol}' is not a column of the auth table '${t.name}'`);
|
|
25952
|
+
}
|
|
25953
|
+
if (!t.columns.some((c) => c.name === "id")) throw new Error(`the auth table '${t.name}' has no id column`);
|
|
25954
|
+
const pk = t.columns.filter((c) => c.isPk).map((c) => c.name);
|
|
25955
|
+
const cols = [.../* @__PURE__ */ new Set(["id", ...hashCol !== void 0 ? [hashCol] : [], ...pk.length === 1 ? pk : []])];
|
|
25956
|
+
for await (const batch of this.pagedRead(t, cols, cursor)) {
|
|
25957
|
+
yield {
|
|
25958
|
+
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)),
|
|
25959
|
+
nextCursor: batch.nextCursor
|
|
25960
|
+
};
|
|
25961
|
+
}
|
|
25962
|
+
}
|
|
25874
25963
|
async close() {
|
|
25875
25964
|
await this.sql.end({ timeout: 5 });
|
|
25876
25965
|
}
|
|
@@ -26141,6 +26230,7 @@ function pgOpts(opts) {
|
|
|
26141
26230
|
url: requireUrl(opts),
|
|
26142
26231
|
...opts.schemas !== void 0 ? { schemas: opts.schemas } : {},
|
|
26143
26232
|
...opts.authTable !== void 0 ? { authTable: opts.authTable } : {},
|
|
26233
|
+
...opts.passwordHashColumn !== void 0 ? { passwordHashColumn: opts.passwordHashColumn } : {},
|
|
26144
26234
|
...opts.batchSize !== void 0 ? { batchSize: opts.batchSize } : {},
|
|
26145
26235
|
...opts.statementTimeoutMs !== void 0 ? { statementTimeoutMs: opts.statementTimeoutMs } : {}
|
|
26146
26236
|
};
|
|
@@ -26155,6 +26245,7 @@ var registry = /* @__PURE__ */ new Map([
|
|
|
26155
26245
|
if (!opts.dir) throw new Error("--from csv-json needs --dir <export-directory>");
|
|
26156
26246
|
return new CsvJsonAdapter({
|
|
26157
26247
|
dir: opts.dir,
|
|
26248
|
+
...opts.passwordHashColumn !== void 0 ? { passwordHashColumn: opts.passwordHashColumn } : {},
|
|
26158
26249
|
...opts.batchSize !== void 0 ? { batchSize: opts.batchSize } : {}
|
|
26159
26250
|
});
|
|
26160
26251
|
}]
|
|
@@ -30270,7 +30361,8 @@ function migrateUsage() {
|
|
|
30270
30361
|
step on the server: created/kept/skipped by reason, nothing written) \xB7 [--file-links shared-link] stored
|
|
30271
30362
|
public file URLs become stable shared links instead of object ids
|
|
30272
30363
|
\xB7 [--with-oauth-identities] carry the Google/Apple/GitHub/Facebook sign-in links (needs
|
|
30273
|
-
--with-password-hashes in the same run) \xB7
|
|
30364
|
+
--with-password-hashes in the same run) \xB7 generic-pg / csv-json: --with-password-hashes
|
|
30365
|
+
--password-hash-column <column> (bcrypt only; other formats are reported, not sent) \xB7 [--keep-timestamps] keep each row's source
|
|
30274
30366
|
created_at / updated_at (cms creates; server key only)
|
|
30275
30367
|
\xB7 [--files-owner <user id>] the owner of every migrated file (default: the path's first
|
|
30276
30368
|
segment) \xB7 [--files-org-from-prefix] the path's first segment is the WORKSPACE (org_id):
|
|
@@ -30293,6 +30385,7 @@ ${migrateUsage()}`);
|
|
|
30293
30385
|
const url = flag("url");
|
|
30294
30386
|
const dir = flag("dir");
|
|
30295
30387
|
const authTable = flag("auth-table");
|
|
30388
|
+
const passwordHashColumn = flag("password-hash-column");
|
|
30296
30389
|
const schemas = flag("schemas");
|
|
30297
30390
|
const batch = flag("batch");
|
|
30298
30391
|
const sbUrl = process.env.SUPABASE_URL;
|
|
@@ -30302,6 +30395,7 @@ ${migrateUsage()}`);
|
|
|
30302
30395
|
...url !== void 0 ? { url } : {},
|
|
30303
30396
|
...dir !== void 0 ? { dir } : {},
|
|
30304
30397
|
...authTable !== void 0 ? { authTable } : {},
|
|
30398
|
+
...passwordHashColumn !== void 0 ? { passwordHashColumn } : {},
|
|
30305
30399
|
...schemas !== void 0 ? { schemas: schemas.split(",").map((s) => s.trim()).filter(Boolean) } : {},
|
|
30306
30400
|
...batch !== void 0 ? { batchSize: Number(batch) } : {},
|
|
30307
30401
|
...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.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",
|