@noormdev/sdk 1.0.0-alpha.33 → 1.0.0-alpha.35

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/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createRequire } from 'module';
2
2
  import { processFile, DtReader, isTemplate, FORMAT_VERSION, DT_EXTENSIONS, encryptWithPassphrase, ENCODED_TYPES } from './chunk-5AYPF5D5.js';
3
3
  import './chunk-UOFJT5PY.js';
4
- import { initializeVault, getVaultStatus, setVaultSecret, getVaultSecret, getAllVaultSecrets, listVaultSecretKeys, deleteVaultSecret, vaultSecretExists, getVaultKey, noormDb, getNoormTables, encryptVaultKey } from './chunk-DE3NJ22A.js';
5
- import { makeNestedConfig, gigabytes, observer, attempt, retry, clone, merge, attemptSync, ObserverRelay } from './chunk-DLQES4PM.js';
4
+ import { initializeVault, getVaultStatus, setVaultSecret, getVaultSecret, getAllVaultSecrets, listVaultSecretKeys, deleteVaultSecret, vaultSecretExists, getVaultKey, noormDb, getNoormTables, encryptVaultKey } from './chunk-WFVWNXTV.js';
5
+ import { makeNestedConfig, gigabytes, observer, attempt, attemptSync, retry, clone, merge, ObserverRelay } from './chunk-DLQES4PM.js';
6
6
  import { dist_default } from './chunk-TBRUDUBQ.js';
7
7
  import { require_dist } from './chunk-YJBKNUTP.js';
8
8
  import { __commonJS, __export, __toESM } from './chunk-D2XW2UOP.js';
@@ -10,7 +10,7 @@ import { constants, existsSync, readFileSync, mkdirSync, writeFileSync, createWr
10
10
  import path, { join, dirname, resolve } from 'path';
11
11
  import { homedir, availableParallelism, userInfo } from 'os';
12
12
  import { stat, mkdir, writeFile, unlink, rename, rm, readdir, readFile, access } from 'fs/promises';
13
- import { createHash, createDecipheriv, randomBytes, createCipheriv, randomUUID, hkdfSync } from 'crypto';
13
+ import { createHash, createPrivateKey, createPublicKey, createDecipheriv, randomBytes, createCipheriv, randomUUID, hkdfSync } from 'crypto';
14
14
  import { execSync } from 'child_process';
15
15
  import { sql } from 'kysely';
16
16
  import { Worker, parentPort, workerData } from 'worker_threads';
@@ -304,6 +304,9 @@ var PRIVATE_KEY_PATH = join(NOORM_HOME, "identity.key");
304
304
  var PUBLIC_KEY_PATH = join(NOORM_HOME, "identity.pub");
305
305
  var IDENTITY_METADATA_PATH = join(NOORM_HOME, "identity.json");
306
306
  async function loadIdentityMetadata() {
307
+ if (identityOverride) {
308
+ return identityOverride;
309
+ }
307
310
  const [content, err] = await attempt(
308
311
  () => readFile(IDENTITY_METADATA_PATH, { encoding: "utf8" })
309
312
  );
@@ -320,6 +323,9 @@ async function loadIdentityMetadata() {
320
323
  return parsed;
321
324
  }
322
325
  async function loadPrivateKey() {
326
+ if (keyOverride) {
327
+ return keyOverride;
328
+ }
323
329
  const [content, err] = await attempt(() => readFile(PRIVATE_KEY_PATH, { encoding: "utf8" }));
324
330
  if (err) {
325
331
  if (err.code === "ENOENT") {
@@ -339,6 +345,34 @@ async function loadPublicKey() {
339
345
  }
340
346
  return content.trim();
341
347
  }
348
+ function isValidKeyHex(hex4) {
349
+ if (!/^[0-9a-f]+$/i.test(hex4)) {
350
+ return false;
351
+ }
352
+ const validLengths = [88, 96];
353
+ return validLengths.includes(hex4.length);
354
+ }
355
+ var keyOverride = null;
356
+ function setKeyOverride(key) {
357
+ keyOverride = key;
358
+ }
359
+ var identityOverride = null;
360
+ function setIdentityOverride(identity) {
361
+ identityOverride = identity;
362
+ }
363
+ function derivePublicKeyFromPrivate(privateKeyHex) {
364
+ const privateKeyObj = createPrivateKey({
365
+ key: Buffer.from(privateKeyHex, "hex"),
366
+ format: "der",
367
+ type: "pkcs8"
368
+ });
369
+ const publicKeyObj = createPublicKey(privateKeyObj);
370
+ const publicKeyDer = publicKeyObj.export({
371
+ type: "spki",
372
+ format: "der"
373
+ });
374
+ return publicKeyDer.toString("hex");
375
+ }
342
376
  function deriveStateKey(privateKey) {
343
377
  const privateKeyBuffer = Buffer.from(privateKey, "hex");
344
378
  return Buffer.from(
@@ -14866,199 +14900,26 @@ function getEffectiveBuildPaths(buildInclude, buildExclude, rules, config2) {
14866
14900
 
14867
14901
  // src/core/settings/manager.ts
14868
14902
  var import_yaml = __toESM(require_dist(), 1);
14869
-
14870
- // src/core/config/schema.ts
14871
- var DialectSchema2 = external_exports.enum(["postgres", "mysql", "sqlite", "mssql"]);
14872
- var ConfigNameSchema = external_exports.string().min(1, "Config name is required").regex(
14873
- /^[a-z0-9_-]+$/i,
14874
- "Config name must contain only letters, numbers, hyphens, and underscores"
14875
- );
14876
- var PortSchema2 = external_exports.number().int().min(1, "Port must be at least 1").max(65535, "Port must be at most 65535");
14877
- var PoolSchema = external_exports.object({
14878
- min: external_exports.number().int().min(0).optional(),
14879
- max: external_exports.number().int().min(1).optional()
14880
- });
14881
- var SSLSchema = external_exports.union([
14882
- external_exports.boolean(),
14883
- external_exports.object({
14884
- rejectUnauthorized: external_exports.boolean().optional(),
14885
- ca: external_exports.string().optional(),
14886
- cert: external_exports.string().optional(),
14887
- key: external_exports.string().optional()
14888
- })
14889
- ]);
14890
- var ConnectionSchema = external_exports.object({
14891
- dialect: DialectSchema2,
14892
- host: external_exports.string().optional(),
14893
- port: PortSchema2.optional(),
14894
- database: external_exports.string().min(1, "Database name is required"),
14895
- filename: external_exports.string().optional(),
14896
- user: external_exports.string().optional(),
14897
- password: external_exports.string().optional(),
14898
- ssl: SSLSchema.optional(),
14899
- pool: PoolSchema.optional()
14900
- }).refine((conn) => conn.dialect === "sqlite" || conn.host, {
14901
- message: "Host is required for non-SQLite databases",
14902
- path: ["host"]
14903
- });
14904
- var ConfigSchema = external_exports.object({
14905
- name: ConfigNameSchema,
14906
- type: external_exports.enum(["local", "remote"]).default("local"),
14907
- isTest: external_exports.boolean().default(false),
14908
- protected: external_exports.boolean().default(false),
14909
- connection: ConnectionSchema,
14910
- identity: external_exports.string().optional()
14911
- });
14912
- var PartialConnectionSchema = external_exports.object({
14913
- dialect: DialectSchema2.optional(),
14914
- host: external_exports.string().optional(),
14915
- port: PortSchema2.optional(),
14916
- database: external_exports.string().optional(),
14917
- filename: external_exports.string().optional(),
14918
- user: external_exports.string().optional(),
14919
- password: external_exports.string().optional(),
14920
- ssl: SSLSchema.optional(),
14921
- pool: PoolSchema.optional()
14922
- });
14923
- external_exports.object({
14924
- name: external_exports.string().regex(/^[a-z0-9_-]+$/i).optional(),
14925
- type: external_exports.enum(["local", "remote"]).optional(),
14926
- isTest: external_exports.boolean().optional(),
14927
- protected: external_exports.boolean().optional(),
14928
- connection: PartialConnectionSchema.optional(),
14929
- identity: external_exports.string().optional()
14930
- });
14931
- ConfigSchema.extend({
14932
- name: ConfigNameSchema.optional()
14933
- });
14934
- var ConfigValidationError = class extends Error {
14935
- constructor(message, field, issues) {
14936
- super(message);
14937
- this.field = field;
14938
- this.issues = issues;
14939
- this.name = "ConfigValidationError";
14940
- }
14941
- };
14942
- function parseConfig(config2) {
14943
- const result = ConfigSchema.safeParse(config2);
14944
- if (!result.success) {
14945
- const firstIssue = result.error.issues[0];
14946
- throw new ConfigValidationError(
14947
- firstIssue?.message ?? "Validation failed",
14948
- firstIssue?.path.join(".") || "unknown",
14949
- result.error.issues
14950
- );
14951
- }
14952
- return result.data;
14953
- }
14954
-
14955
- // src/core/environment.ts
14956
- function getEnvConfigName() {
14957
- return process.env["NOORM_CONFIG"];
14958
- }
14959
-
14960
- // src/core/config/resolver.ts
14961
- var SettingsProvider = class {
14962
- #manager;
14963
- constructor(manager) {
14964
- this.#manager = manager;
14965
- }
14966
- /** Get a stage by name */
14967
- getStage(name) {
14968
- return this.#manager.getStage(name) ?? null;
14969
- }
14970
- /** Get stage that matches a config name (for auto-linking) */
14971
- findStageForConfig(configName) {
14972
- return this.#manager.getStage(configName) ?? null;
14973
- }
14974
- };
14975
- var DEFAULTS = {
14976
- type: "local",
14977
- isTest: false,
14978
- protected: false,
14979
- connection: {
14980
- host: "localhost",
14981
- pool: { min: 0, max: 10 }
14982
- },
14983
- log: {
14984
- level: "info"
14985
- }
14986
- };
14987
- function resolveConfig(state, options = {}) {
14988
- const configName = options.name ?? getEnvConfigName() ?? state.getActiveConfigName();
14989
- if (!configName) {
14990
- const envConfig2 = getEnvConfig();
14991
- if (envConfig2.connection?.dialect && envConfig2.connection?.database) {
14992
- return resolveFromEnvOnly(envConfig2, options.flags, options.stage, options.settings);
14993
- }
14994
- return null;
14995
- }
14996
- const stored = state.getConfig(configName);
14997
- if (!stored) {
14998
- throw new Error(`Config "${configName}" not found`);
14999
- }
15000
- const stageDefaults = getStageDefaults(configName, options.stage, options.settings);
15001
- const envConfig = getEnvConfig();
15002
- let merged = clone(DEFAULTS);
15003
- if (stageDefaults) {
15004
- merged = merge(merged, stageDefaults);
15005
- }
15006
- merged = merge(merged, stored);
15007
- merged = merge(merged, envConfig);
15008
- merged = merge(merged, options.flags ?? {});
15009
- return parseConfig(merged);
15010
- }
15011
- function getStageDefaults(configName, stageName, settings) {
15012
- if (!settings) return null;
15013
- if (stageName) return settings.getStage(stageName)?.defaults ?? null;
15014
- return settings.findStageForConfig(configName)?.defaults ?? null;
15015
- }
15016
- function resolveFromEnvOnly(envConfig, flags, stageName, settings) {
15017
- const name = envConfig.name ?? flags?.name ?? "__env__";
15018
- const stageDefaults = getStageDefaults(name, stageName, settings);
15019
- let merged = clone(DEFAULTS);
15020
- if (stageDefaults) {
15021
- merged = merge(merged, stageDefaults);
15022
- }
15023
- merged = merge(merged, envConfig);
15024
- merged = merge(merged, flags ?? {});
15025
- if (!merged.name) {
15026
- merged.name = "__env__";
15027
- }
15028
- return parseConfig(merged);
15029
- }
15030
-
15031
- // src/core/config/index.ts
15032
- var META_ENV_VARS = /* @__PURE__ */ new Set([
14903
+ var META_SETTINGS_ENV_VARS = /* @__PURE__ */ new Set([
15033
14904
  "NOORM_CONFIG",
15034
- // Config selection
15035
14905
  "NOORM_YES",
15036
- // Skip confirmations
15037
- "NOORM_JSON"
15038
- // JSON output mode
14906
+ "NOORM_JSON",
14907
+ "NOORM_HEADLESS",
14908
+ "NOORM_DEBUG",
14909
+ "NOORM_DEV",
14910
+ "NOORM_CI_CONFIG_NAME",
14911
+ "NOORM_LOGGER_DEBUG",
14912
+ "NOORM_IDENTITY"
15039
14913
  ]);
15040
- var VALID_DIALECTS = ["postgres", "mysql", "sqlite", "mssql"];
15041
- var { allConfigs, getConfig } = makeNestedConfig(process.env, {
15042
- filter: (key) => key.startsWith("NOORM_") && !META_ENV_VARS.has(key),
15043
- stripPrefix: "NOORM_",
15044
- forceAllCapToLower: true,
15045
- memoizeOpts: false,
15046
- skipConversion: (key) => key.toLowerCase().includes("password")
15047
- });
15048
- function getEnvConfig() {
15049
- const config2 = allConfigs();
15050
- if (config2.connection?.dialect) {
15051
- const result = DialectSchema2.safeParse(config2.connection.dialect);
15052
- if (!result.success) {
15053
- throw new Error(
15054
- `Invalid NOORM_CONNECTION_DIALECT: must be one of ${VALID_DIALECTS.join(", ")}`
15055
- );
15056
- }
15057
- }
15058
- return config2;
14914
+ function allSettingsEnv() {
14915
+ const { allConfigs: allConfigs2 } = makeNestedConfig(process.env, {
14916
+ filter: (key) => key.startsWith("NOORM_") && !META_SETTINGS_ENV_VARS.has(key) && !key.startsWith("NOORM_IDENTITY_") && !key.startsWith("NOORM_CONNECTION_"),
14917
+ stripPrefix: "NOORM_",
14918
+ forceAllCapToLower: true,
14919
+ memoizeOpts: false
14920
+ });
14921
+ return allConfigs2();
15059
14922
  }
15060
-
15061
- // src/core/settings/manager.ts
15062
14923
  var SettingsManager = class {
15063
14924
  #projectRoot;
15064
14925
  #settingsDir;
@@ -15134,7 +14995,7 @@ var SettingsManager = class {
15134
14995
  }
15135
14996
  fromFile = true;
15136
14997
  }
15137
- this.#settings = merge(this.#settings, allConfigs());
14998
+ this.#settings = merge(this.#settings, allSettingsEnv());
15138
14999
  this.#loaded = true;
15139
15000
  observer.emit("settings:loaded", {
15140
15001
  path: this.settingsFilePath,
@@ -15718,6 +15579,13 @@ function formatIdentity(identity) {
15718
15579
  }
15719
15580
  return identity.name;
15720
15581
  }
15582
+ function computeIdentityHash(input) {
15583
+ if (!input.email || !input.name || !input.machine || false) {
15584
+ throw new Error("All fields required for identity hash: email, name, machine, os");
15585
+ }
15586
+ const hashInput = [input.email, input.name, input.machine, input.os].join("\0");
15587
+ return createHash("sha256").update(hashInput, "utf8").digest("hex");
15588
+ }
15721
15589
 
15722
15590
  // src/core/identity/factory.ts
15723
15591
  async function loadExistingIdentity() {
@@ -16638,6 +16506,11 @@ function createDialectQuoting(opts) {
16638
16506
  return { quote: quote5, qualifiedName: qualifiedName4 };
16639
16507
  }
16640
16508
 
16509
+ // src/core/environment.ts
16510
+ function getEnvConfigName() {
16511
+ return process.env["NOORM_CONFIG"];
16512
+ }
16513
+
16641
16514
  // src/core/logger/logger.ts
16642
16515
  __toESM(require_dayjs_min(), 1);
16643
16516
 
@@ -16765,6 +16638,50 @@ async function registerIdentity(db, identity, dialect) {
16765
16638
  return { ok: true, registered: true };
16766
16639
  }
16767
16640
 
16641
+ // src/core/identity/env.ts
16642
+ var CI_ENV_VARS = {
16643
+ privateKey: "NOORM_IDENTITY_PRIVATE_KEY",
16644
+ name: "NOORM_IDENTITY_NAME",
16645
+ email: "NOORM_IDENTITY_EMAIL"
16646
+ };
16647
+ function loadIdentityFromEnv() {
16648
+ const rawKey = process.env[CI_ENV_VARS.privateKey];
16649
+ const rawName = process.env[CI_ENV_VARS.name];
16650
+ const rawEmail = process.env[CI_ENV_VARS.email];
16651
+ if (!rawKey || !rawName || !rawEmail) {
16652
+ return null;
16653
+ }
16654
+ const privateKey = rawKey.trim();
16655
+ const name = rawName.trim();
16656
+ const email3 = rawEmail.trim();
16657
+ if (!name || !email3) {
16658
+ return null;
16659
+ }
16660
+ if (!isValidKeyHex(privateKey)) {
16661
+ return null;
16662
+ }
16663
+ const [publicKey, deriveErr] = attemptSync(() => derivePublicKeyFromPrivate(privateKey));
16664
+ if (deriveErr || !publicKey) {
16665
+ return null;
16666
+ }
16667
+ const identityHash = computeIdentityHash({
16668
+ email: email3,
16669
+ name,
16670
+ machine: publicKey,
16671
+ os: "env"
16672
+ });
16673
+ const identity = {
16674
+ identityHash,
16675
+ name,
16676
+ email: email3,
16677
+ publicKey,
16678
+ machine: "ci",
16679
+ os: "env",
16680
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
16681
+ };
16682
+ return { identity, privateKey, source: "env" };
16683
+ }
16684
+
16768
16685
  // src/core/identity/index.ts
16769
16686
  var cachedIdentity = null;
16770
16687
  function resolveIdentity2(options = {}) {
@@ -16791,6 +16708,205 @@ async function waitForIdentityToLoad(db, dialect) {
16791
16708
  });
16792
16709
  }
16793
16710
 
16711
+ // src/core/config/schema.ts
16712
+ var DialectSchema2 = external_exports.enum(["postgres", "mysql", "sqlite", "mssql"]);
16713
+ var ConfigNameSchema = external_exports.string().min(1, "Config name is required").regex(
16714
+ /^[a-z0-9_-]+$/i,
16715
+ "Config name must contain only letters, numbers, hyphens, and underscores"
16716
+ );
16717
+ var PortSchema2 = external_exports.number().int().min(1, "Port must be at least 1").max(65535, "Port must be at most 65535");
16718
+ var PoolSchema = external_exports.object({
16719
+ min: external_exports.number().int().min(0).optional(),
16720
+ max: external_exports.number().int().min(1).optional()
16721
+ });
16722
+ var SSLSchema = external_exports.union([
16723
+ external_exports.boolean(),
16724
+ external_exports.object({
16725
+ rejectUnauthorized: external_exports.boolean().optional(),
16726
+ ca: external_exports.string().optional(),
16727
+ cert: external_exports.string().optional(),
16728
+ key: external_exports.string().optional()
16729
+ })
16730
+ ]);
16731
+ var ConnectionSchema = external_exports.object({
16732
+ dialect: DialectSchema2,
16733
+ host: external_exports.string().optional(),
16734
+ port: PortSchema2.optional(),
16735
+ database: external_exports.string().min(1, "Database name is required"),
16736
+ filename: external_exports.string().optional(),
16737
+ user: external_exports.string().optional(),
16738
+ password: external_exports.string().optional(),
16739
+ ssl: SSLSchema.optional(),
16740
+ pool: PoolSchema.optional()
16741
+ }).refine((conn) => conn.dialect === "sqlite" || conn.host, {
16742
+ message: "Host is required for non-SQLite databases",
16743
+ path: ["host"]
16744
+ });
16745
+ var ConfigSchema = external_exports.object({
16746
+ name: ConfigNameSchema,
16747
+ type: external_exports.enum(["local", "remote"]).default("local"),
16748
+ isTest: external_exports.boolean().default(false),
16749
+ protected: external_exports.boolean().default(false),
16750
+ connection: ConnectionSchema,
16751
+ identity: external_exports.string().optional()
16752
+ });
16753
+ var PartialConnectionSchema = external_exports.object({
16754
+ dialect: DialectSchema2.optional(),
16755
+ host: external_exports.string().optional(),
16756
+ port: PortSchema2.optional(),
16757
+ database: external_exports.string().optional(),
16758
+ filename: external_exports.string().optional(),
16759
+ user: external_exports.string().optional(),
16760
+ password: external_exports.string().optional(),
16761
+ ssl: SSLSchema.optional(),
16762
+ pool: PoolSchema.optional()
16763
+ });
16764
+ external_exports.object({
16765
+ name: external_exports.string().regex(/^[a-z0-9_-]+$/i).optional(),
16766
+ type: external_exports.enum(["local", "remote"]).optional(),
16767
+ isTest: external_exports.boolean().optional(),
16768
+ protected: external_exports.boolean().optional(),
16769
+ connection: PartialConnectionSchema.optional(),
16770
+ identity: external_exports.string().optional()
16771
+ });
16772
+ ConfigSchema.extend({
16773
+ name: ConfigNameSchema.optional()
16774
+ });
16775
+ var ConfigValidationError = class extends Error {
16776
+ constructor(message, field, issues) {
16777
+ super(message);
16778
+ this.field = field;
16779
+ this.issues = issues;
16780
+ this.name = "ConfigValidationError";
16781
+ }
16782
+ };
16783
+ function parseConfig(config2) {
16784
+ const result = ConfigSchema.safeParse(config2);
16785
+ if (!result.success) {
16786
+ const firstIssue = result.error.issues[0];
16787
+ throw new ConfigValidationError(
16788
+ firstIssue?.message ?? "Validation failed",
16789
+ firstIssue?.path.join(".") || "unknown",
16790
+ result.error.issues
16791
+ );
16792
+ }
16793
+ return result.data;
16794
+ }
16795
+
16796
+ // src/core/config/index.ts
16797
+ var META_ENV_VARS = /* @__PURE__ */ new Set([
16798
+ "NOORM_CONFIG",
16799
+ // Config selection
16800
+ "NOORM_YES",
16801
+ // Skip confirmations
16802
+ "NOORM_JSON",
16803
+ // JSON output mode
16804
+ "NOORM_HEADLESS",
16805
+ // Headless mode detection
16806
+ "NOORM_DEBUG",
16807
+ // Debug logging
16808
+ "NOORM_DEV",
16809
+ // Dev mode detection
16810
+ "NOORM_CI_CONFIG_NAME",
16811
+ // ci init config name override
16812
+ "NOORM_LOGGER_DEBUG"
16813
+ // Logger-internal debug
16814
+ ]);
16815
+ var NON_CONFIG_ENV_PREFIXES = [
16816
+ "NOORM_IDENTITY_"
16817
+ ];
16818
+ var VALID_DIALECTS = ["postgres", "mysql", "sqlite", "mssql"];
16819
+ var { allConfigs, getConfig } = makeNestedConfig(process.env, {
16820
+ filter: (key) => key.startsWith("NOORM_") && !META_ENV_VARS.has(key) && !NON_CONFIG_ENV_PREFIXES.some((prefix) => key.startsWith(prefix)),
16821
+ stripPrefix: "NOORM_",
16822
+ forceAllCapToLower: true,
16823
+ memoizeOpts: false,
16824
+ skipConversion: (key) => key.toLowerCase().includes("password")
16825
+ });
16826
+ function getEnvConfig() {
16827
+ const config2 = allConfigs();
16828
+ if (config2.connection?.dialect) {
16829
+ const result = DialectSchema2.safeParse(config2.connection.dialect);
16830
+ if (!result.success) {
16831
+ throw new Error(
16832
+ `Invalid NOORM_CONNECTION_DIALECT: must be one of ${VALID_DIALECTS.join(", ")}`
16833
+ );
16834
+ }
16835
+ }
16836
+ return config2;
16837
+ }
16838
+
16839
+ // src/core/config/resolver.ts
16840
+ var SettingsProvider = class {
16841
+ #manager;
16842
+ constructor(manager) {
16843
+ this.#manager = manager;
16844
+ }
16845
+ /** Get a stage by name */
16846
+ getStage(name) {
16847
+ return this.#manager.getStage(name) ?? null;
16848
+ }
16849
+ /** Get stage that matches a config name (for auto-linking) */
16850
+ findStageForConfig(configName) {
16851
+ return this.#manager.getStage(configName) ?? null;
16852
+ }
16853
+ };
16854
+ var DEFAULTS = {
16855
+ type: "local",
16856
+ isTest: false,
16857
+ protected: false,
16858
+ connection: {
16859
+ host: "localhost",
16860
+ pool: { min: 0, max: 10 }
16861
+ },
16862
+ log: {
16863
+ level: "info"
16864
+ }
16865
+ };
16866
+ function resolveConfig(state, options = {}) {
16867
+ const configName = options.name ?? getEnvConfigName() ?? state.getActiveConfigName();
16868
+ if (!configName) {
16869
+ const envConfig2 = getEnvConfig();
16870
+ if (envConfig2.connection?.dialect && envConfig2.connection?.database) {
16871
+ return resolveFromEnvOnly(envConfig2, options.flags, options.stage, options.settings);
16872
+ }
16873
+ return null;
16874
+ }
16875
+ const stored = state.getConfig(configName);
16876
+ if (!stored) {
16877
+ throw new Error(`Config "${configName}" not found`);
16878
+ }
16879
+ const stageDefaults = getStageDefaults(configName, options.stage, options.settings);
16880
+ const envConfig = getEnvConfig();
16881
+ let merged = clone(DEFAULTS);
16882
+ if (stageDefaults) {
16883
+ merged = merge(merged, stageDefaults);
16884
+ }
16885
+ merged = merge(merged, stored);
16886
+ merged = merge(merged, envConfig);
16887
+ merged = merge(merged, options.flags ?? {});
16888
+ return parseConfig(merged);
16889
+ }
16890
+ function getStageDefaults(configName, stageName, settings) {
16891
+ if (!settings) return null;
16892
+ if (stageName) return settings.getStage(stageName)?.defaults ?? null;
16893
+ return settings.findStageForConfig(configName)?.defaults ?? null;
16894
+ }
16895
+ function resolveFromEnvOnly(envConfig, flags, stageName, settings) {
16896
+ const name = envConfig.name ?? flags?.name ?? "__env__";
16897
+ const stageDefaults = getStageDefaults(name, stageName, settings);
16898
+ let merged = clone(DEFAULTS);
16899
+ if (stageDefaults) {
16900
+ merged = merge(merged, stageDefaults);
16901
+ }
16902
+ merged = merge(merged, envConfig);
16903
+ merged = merge(merged, flags ?? {});
16904
+ if (!merged.name) {
16905
+ merged.name = "__env__";
16906
+ }
16907
+ return parseConfig(merged);
16908
+ }
16909
+
16794
16910
  // src/sdk/tvp.ts
16795
16911
  var MSSQL_PARAM_LIMIT = 2100;
16796
16912
  function tvp(typeName, rows) {
@@ -16822,6 +16938,16 @@ function isTvp(value) {
16822
16938
  }
16823
16939
 
16824
16940
  // src/sdk/sql.ts
16941
+ function quoteIdent(dialect, name) {
16942
+ const parts = name.includes(".") ? name.split(".", 2) : [name];
16943
+ if (dialect === "postgres") {
16944
+ return parts.map((p) => '"' + p.replace(/"/g, '""') + '"').join(".");
16945
+ }
16946
+ if (dialect === "mssql") {
16947
+ return parts.map((p) => "[" + p.replace(/]/g, "]]") + "]").join(".");
16948
+ }
16949
+ return parts.map((p) => "`" + p.replace(/`/g, "``") + "`").join(".");
16950
+ }
16825
16951
  function buildProcCall(dialect, name, params) {
16826
16952
  if (dialect === "sqlite") {
16827
16953
  throw new Error("SQLite does not support stored procedures.");
@@ -16829,7 +16955,7 @@ function buildProcCall(dialect, name, params) {
16829
16955
  if (params && hasTvpParam(params) && dialect !== "mssql") {
16830
16956
  throw new Error("Table-valued parameters (TVP) are only supported on MSSQL.");
16831
16957
  }
16832
- const rawName = sql.raw(name);
16958
+ const rawName = sql.raw(quoteIdent(dialect, name));
16833
16959
  if (!params || Array.isArray(params) && params.length === 0 || !Array.isArray(params) && Object.keys(params).length === 0) {
16834
16960
  if (dialect === "mssql") {
16835
16961
  return sql`EXEC ${rawName}`;
@@ -16851,8 +16977,8 @@ function buildFuncCall(dialect, name, column, params) {
16851
16977
  if (params && hasTvpParam(params) && dialect !== "mssql") {
16852
16978
  throw new Error("Table-valued parameters (TVP) are only supported on MSSQL.");
16853
16979
  }
16854
- const rawName = sql.raw(name);
16855
- const rawCol = sql.raw(column);
16980
+ const rawName = sql.raw(quoteIdent(dialect, name));
16981
+ const rawCol = sql.raw(quoteIdent(dialect, column));
16856
16982
  if (!params || Array.isArray(params) && params.length === 0 || !Array.isArray(params) && Object.keys(params).length === 0) {
16857
16983
  return sql`SELECT ${rawName}() AS ${rawCol}`;
16858
16984
  }
@@ -16861,7 +16987,7 @@ function buildFuncCall(dialect, name, column, params) {
16861
16987
  }
16862
16988
  if (dialect === "postgres" && !Array.isArray(params)) {
16863
16989
  const parts = Object.entries(params).map(
16864
- ([key, val]) => sql`${sql.raw(key)} => ${val}`
16990
+ ([key, val]) => sql`${sql.raw(quoteIdent("postgres", key))} => ${val}`
16865
16991
  );
16866
16992
  return sql`SELECT ${rawName}(${sql.join(parts)}) AS ${rawCol}`;
16867
16993
  }
@@ -16888,7 +17014,7 @@ function buildTvfCall(dialect, name, params) {
16888
17014
  if (params && hasTvpParam(params) && dialect !== "mssql") {
16889
17015
  throw new Error("Table-valued parameters (TVP) are only supported on MSSQL.");
16890
17016
  }
16891
- const rawName = sql.raw(name);
17017
+ const rawName = sql.raw(quoteIdent(dialect, name));
16892
17018
  if (!params || Array.isArray(params) && params.length === 0 || !Array.isArray(params) && Object.keys(params).length === 0) {
16893
17019
  return sql`SELECT * FROM ${rawName}()`;
16894
17020
  }
@@ -16897,7 +17023,7 @@ function buildTvfCall(dialect, name, params) {
16897
17023
  }
16898
17024
  if (dialect === "postgres" && !Array.isArray(params)) {
16899
17025
  const parts = Object.entries(params).map(
16900
- ([key, val]) => sql`${sql.raw(key)} => ${val}`
17026
+ ([key, val]) => sql`${sql.raw(quoteIdent("postgres", key))} => ${val}`
16901
17027
  );
16902
17028
  return sql`SELECT * FROM ${rawName}(${sql.join(parts)})`;
16903
17029
  }
@@ -16924,7 +17050,7 @@ function buildPostgresProc(rawName, params) {
16924
17050
  return sql`CALL ${rawName}(${joined})`;
16925
17051
  }
16926
17052
  const parts = Object.entries(params).map(
16927
- ([key, val]) => sql`${sql.raw(key)} => ${val}`
17053
+ ([key, val]) => sql`${sql.raw(quoteIdent("postgres", key))} => ${val}`
16928
17054
  );
16929
17055
  return sql`CALL ${rawName}(${sql.join(parts)})`;
16930
17056
  }
@@ -19119,7 +19245,7 @@ async function executeChange(context, change, options = {}) {
19119
19245
  const [, lockErr] = await attempt(
19120
19246
  () => lockManager.acquire(context.db, context.configName, identity, {
19121
19247
  reason: `Change: ${change.name}`,
19122
- dialect: context.dialect
19248
+ dialect: context.dialect ?? "postgres"
19123
19249
  })
19124
19250
  );
19125
19251
  if (lockErr) {
@@ -19191,7 +19317,7 @@ async function revertChange(context, change, options = {}) {
19191
19317
  const [, lockErr] = await attempt(
19192
19318
  () => lockManager.acquire(context.db, context.configName, identity, {
19193
19319
  reason: `Revert: ${change.name}`,
19194
- dialect: context.dialect
19320
+ dialect: context.dialect ?? "postgres"
19195
19321
  })
19196
19322
  );
19197
19323
  if (lockErr) {
@@ -20117,9 +20243,16 @@ var ChangesNamespace = class {
20117
20243
  /**
20118
20244
  * Apply a specific change.
20119
20245
  *
20246
+ * Pass `dryRun: true` to render the change to `tmp/` without
20247
+ * touching the database, or `preview: true` to emit rendered SQL.
20248
+ *
20120
20249
  * @example
20121
20250
  * ```typescript
20122
20251
  * const result = await ctx.noorm.changes.apply('2024-01-15-add-users')
20252
+ * const dry = await ctx.noorm.changes.apply(
20253
+ * '2024-01-15-add-users',
20254
+ * { dryRun: true },
20255
+ * )
20123
20256
  * ```
20124
20257
  */
20125
20258
  async apply(name, options) {
@@ -20128,9 +20261,16 @@ var ChangesNamespace = class {
20128
20261
  /**
20129
20262
  * Revert a specific change.
20130
20263
  *
20264
+ * Pass `dryRun: true` to render the revert files to `tmp/` without
20265
+ * touching the database, or `preview: true` to emit rendered SQL.
20266
+ *
20131
20267
  * @example
20132
20268
  * ```typescript
20133
20269
  * const result = await ctx.noorm.changes.revert('2024-01-15-add-users')
20270
+ * const dry = await ctx.noorm.changes.revert(
20271
+ * '2024-01-15-add-users',
20272
+ * { dryRun: true },
20273
+ * )
20134
20274
  * ```
20135
20275
  */
20136
20276
  async revert(name, options) {
@@ -20140,25 +20280,37 @@ var ChangesNamespace = class {
20140
20280
  /**
20141
20281
  * Apply all pending changes.
20142
20282
  *
20283
+ * Pass `dryRun: true` to render each change to `tmp/` without
20284
+ * touching the database. Pass `preview: true` to emit rendered
20285
+ * SQL (also without DB writes). When omitted, both default to
20286
+ * `false` and changes are applied normally.
20287
+ *
20143
20288
  * @example
20144
20289
  * ```typescript
20145
20290
  * const result = await ctx.noorm.changes.ff()
20291
+ * const dry = await ctx.noorm.changes.ff({ dryRun: true })
20292
+ * const forced = await ctx.noorm.changes.ff({ force: true })
20146
20293
  * ```
20147
20294
  */
20148
- async ff() {
20149
- return this.#getManager().ff();
20295
+ async ff(options) {
20296
+ return this.#getManager().ff(options);
20150
20297
  }
20151
20298
  /**
20152
20299
  * Apply the next N pending changes (default 1).
20153
20300
  *
20301
+ * Pass `dryRun: true` to render the changes to `tmp/` without
20302
+ * touching the database. Pass `preview: true` to emit rendered
20303
+ * SQL (also without DB writes).
20304
+ *
20154
20305
  * @example
20155
20306
  * ```typescript
20156
20307
  * const result = await ctx.noorm.changes.next()
20157
20308
  * const result = await ctx.noorm.changes.next(3)
20309
+ * const dry = await ctx.noorm.changes.next(2, { dryRun: true })
20158
20310
  * ```
20159
20311
  */
20160
- async next(count = 1) {
20161
- return this.#getManager().next(count);
20312
+ async next(count = 1, options) {
20313
+ return this.#getManager().next(count, options);
20162
20314
  }
20163
20315
  // ─────────────────────────────────────────────────────
20164
20316
  // Status & history (connected)
@@ -20266,8 +20418,12 @@ var ChangesNamespace = class {
20266
20418
  }
20267
20419
  #createChangeContext() {
20268
20420
  const state = getStateManager(this.#state.projectRoot);
20421
+ if (!this.#state.connection) {
20422
+ throw new Error("Not connected. Call connect() first.");
20423
+ }
20269
20424
  return {
20270
20425
  db: this.#kysely,
20426
+ dialect: this.#state.connection.dialect,
20271
20427
  configName: this.#state.config.name,
20272
20428
  identity: this.#state.identity,
20273
20429
  projectRoot: this.#state.projectRoot,
@@ -20285,6 +20441,48 @@ var ChangesNamespace = class {
20285
20441
  return this.#manager;
20286
20442
  }
20287
20443
  };
20444
+ function splitMssqlBatches(content) {
20445
+ const batches = content.split(/^\s*GO\s*$/mi);
20446
+ return batches.map((b) => b.trim()).filter((b) => b.length > 0 && !isCommentOnly(b));
20447
+ }
20448
+ function isCommentOnly(batch) {
20449
+ for (const line of batch.split("\n")) {
20450
+ const trimmed = line.trim();
20451
+ if (trimmed.length > 0 && !trimmed.startsWith("--")) {
20452
+ return false;
20453
+ }
20454
+ }
20455
+ return true;
20456
+ }
20457
+ async function executeSqlBody(context, sqlContent) {
20458
+ if (context.dialect !== "mssql") {
20459
+ const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(context.db));
20460
+ if (execErr) {
20461
+ return getSqlErrorMessage(execErr);
20462
+ }
20463
+ return null;
20464
+ }
20465
+ const batches = splitMssqlBatches(sqlContent);
20466
+ if (batches.length === 0) {
20467
+ return null;
20468
+ }
20469
+ if (batches.length === 1) {
20470
+ const [, execErr] = await attempt(() => sql.raw(batches[0]).execute(context.db));
20471
+ if (execErr) {
20472
+ return getSqlErrorMessage(execErr);
20473
+ }
20474
+ return null;
20475
+ }
20476
+ for (let i = 0; i < batches.length; i++) {
20477
+ const [, execErr] = await attempt(() => sql.raw(batches[i]).execute(context.db));
20478
+ if (execErr) {
20479
+ return `[batch ${i + 1} of ${batches.length}] ${getSqlErrorMessage(execErr)}`;
20480
+ }
20481
+ }
20482
+ return null;
20483
+ }
20484
+
20485
+ // src/core/runner/runner.ts
20288
20486
  var SQL_EXTENSIONS2 = [".sql", ".sql.tmpl"];
20289
20487
  var FILE_HEADER_TEMPLATE = `-- ============================================================
20290
20488
  -- File: %FILE%
@@ -20650,14 +20848,14 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
20650
20848
  return result2;
20651
20849
  }
20652
20850
  }
20653
- const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(context.db));
20851
+ const execErrMsg = await executeSqlBody(context, sqlContent);
20654
20852
  const durationMs = performance.now() - start;
20655
- if (execErr) {
20853
+ if (execErrMsg) {
20656
20854
  const result2 = {
20657
20855
  filepath,
20658
20856
  checksum: finalChecksum,
20659
20857
  status: "failed",
20660
- error: getSqlErrorMessage(execErr),
20858
+ error: execErrMsg,
20661
20859
  durationMs
20662
20860
  };
20663
20861
  await tracker.updateFileExecution(
@@ -20665,13 +20863,13 @@ async function executeSingleFileWithUpdate(context, filepath, checksum, options,
20665
20863
  relFilepath,
20666
20864
  "failed",
20667
20865
  Math.round(durationMs),
20668
- getSqlErrorMessage(execErr)
20866
+ execErrMsg
20669
20867
  );
20670
20868
  observer.emit("file:after", {
20671
20869
  filepath,
20672
20870
  status: "failed",
20673
20871
  durationMs,
20674
- error: getSqlErrorMessage(execErr)
20872
+ error: execErrMsg
20675
20873
  });
20676
20874
  return result2;
20677
20875
  }
@@ -20774,14 +20972,14 @@ async function executeSingleFile(context, filepath, options, tracker, operationI
20774
20972
  });
20775
20973
  return result2;
20776
20974
  }
20777
- const [, execErr] = await attempt(() => sql.raw(sqlContent).execute(context.db));
20975
+ const execErrMsg = await executeSqlBody(context, sqlContent);
20778
20976
  const durationMs = performance.now() - start;
20779
- if (execErr) {
20977
+ if (execErrMsg) {
20780
20978
  const result2 = {
20781
20979
  filepath,
20782
20980
  checksum,
20783
20981
  status: "failed",
20784
- error: getSqlErrorMessage(execErr),
20982
+ error: execErrMsg,
20785
20983
  durationMs
20786
20984
  };
20787
20985
  await tracker.recordExecution({
@@ -20789,14 +20987,14 @@ async function executeSingleFile(context, filepath, options, tracker, operationI
20789
20987
  filepath: relFilepath,
20790
20988
  checksum,
20791
20989
  status: "failed",
20792
- errorMessage: getSqlErrorMessage(execErr),
20990
+ errorMessage: execErrMsg,
20793
20991
  durationMs: Math.round(durationMs)
20794
20992
  });
20795
20993
  observer.emit("file:after", {
20796
20994
  filepath,
20797
20995
  status: "failed",
20798
20996
  durationMs,
20799
- error: getSqlErrorMessage(execErr)
20997
+ error: execErrMsg
20800
20998
  });
20801
20999
  return result2;
20802
21000
  }
@@ -23322,10 +23520,10 @@ var { quote, qualifiedName } = createDialectQuoting({
23322
23520
  escape: '""'
23323
23521
  });
23324
23522
  var postgresTeardownOperations = {
23325
- disableForeignKeyChecks() {
23523
+ disableForeignKeyChecks(_tables) {
23326
23524
  return "SET session_replication_role = 'replica'";
23327
23525
  },
23328
- enableForeignKeyChecks() {
23526
+ enableForeignKeyChecks(_tables) {
23329
23527
  return "SET session_replication_role = 'origin'";
23330
23528
  },
23331
23529
  truncateTable(tableName, schema, restartIdentity = true) {
@@ -23360,10 +23558,10 @@ var { quote: quote2, qualifiedName: qualifiedName2 } = createDialectQuoting({
23360
23558
  escape: "``"
23361
23559
  });
23362
23560
  var mysqlTeardownOperations = {
23363
- disableForeignKeyChecks() {
23561
+ disableForeignKeyChecks(_tables) {
23364
23562
  return "SET FOREIGN_KEY_CHECKS = 0";
23365
23563
  },
23366
- enableForeignKeyChecks() {
23564
+ enableForeignKeyChecks(_tables) {
23367
23565
  return "SET FOREIGN_KEY_CHECKS = 1";
23368
23566
  },
23369
23567
  truncateTable(tableName, schema, _restartIdentity = true) {
@@ -23396,10 +23594,16 @@ var { quote: quote3, qualifiedName: qualifiedName3 } = createDialectQuoting({
23396
23594
  escape: "]]"
23397
23595
  });
23398
23596
  var mssqlTeardownOperations = {
23399
- disableForeignKeyChecks() {
23597
+ disableForeignKeyChecks(tables) {
23598
+ if (tables && tables.length > 0) {
23599
+ return tables.map((t) => `ALTER TABLE ${qualifiedName3(t)} NOCHECK CONSTRAINT ALL`);
23600
+ }
23400
23601
  return "EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'";
23401
23602
  },
23402
- enableForeignKeyChecks() {
23603
+ enableForeignKeyChecks(tables) {
23604
+ if (tables && tables.length > 0) {
23605
+ return tables.map((t) => `ALTER TABLE ${qualifiedName3(t)} CHECK CONSTRAINT ALL`);
23606
+ }
23403
23607
  return "EXEC sp_MSforeachtable 'ALTER TABLE ? CHECK CONSTRAINT ALL'";
23404
23608
  },
23405
23609
  truncateTable(tableName, schema, restartIdentity = true) {
@@ -23438,10 +23642,10 @@ var { quote: quote4 } = createDialectQuoting({
23438
23642
  escape: '""'
23439
23643
  });
23440
23644
  var sqliteTeardownOperations = {
23441
- disableForeignKeyChecks() {
23645
+ disableForeignKeyChecks(_tables) {
23442
23646
  return "PRAGMA foreign_keys = OFF";
23443
23647
  },
23444
- enableForeignKeyChecks() {
23648
+ enableForeignKeyChecks(_tables) {
23445
23649
  return "PRAGMA foreign_keys = ON";
23446
23650
  },
23447
23651
  truncateTable(tableName, _schema, _restartIdentity = false) {
@@ -23491,6 +23695,13 @@ function isNoormTable2(name) {
23491
23695
  if (name.startsWith("__noorm_")) return true;
23492
23696
  return false;
23493
23697
  }
23698
+ function pushFlat(out, value) {
23699
+ if (Array.isArray(value)) {
23700
+ out.push(...value);
23701
+ return;
23702
+ }
23703
+ out.push(value);
23704
+ }
23494
23705
  async function truncateData(db, dialect, options = {}) {
23495
23706
  const startTime = performance.now();
23496
23707
  const ops = getTeardownOperations(dialect);
@@ -23520,11 +23731,13 @@ async function truncateData(db, dialect, options = {}) {
23520
23731
  }
23521
23732
  truncated.push(tableName);
23522
23733
  }
23523
- statements.push(ops.disableForeignKeyChecks());
23524
- for (const tableName of truncated) {
23525
- statements.push(ops.truncateTable(tableName, void 0, options.restartIdentity ?? true));
23734
+ if (truncated.length > 0) {
23735
+ pushFlat(statements, ops.disableForeignKeyChecks(truncated));
23736
+ for (const tableName of truncated) {
23737
+ statements.push(ops.truncateTable(tableName, void 0, options.restartIdentity ?? true));
23738
+ }
23739
+ pushFlat(statements, ops.enableForeignKeyChecks(truncated));
23526
23740
  }
23527
- statements.push(ops.enableForeignKeyChecks());
23528
23741
  if (!options.dryRun) {
23529
23742
  for (const stmt of statements) {
23530
23743
  if (stmt.startsWith("--")) continue;
@@ -23596,6 +23809,18 @@ async function teardownSchema(db, dialect, options = {}) {
23596
23809
  dropped.foreignKeys.push(fk.name);
23597
23810
  statements.push(ops.dropForeignKey(fk.name, tableName, fk.schema));
23598
23811
  }
23812
+ if (!options.keepProcedures) {
23813
+ for (const proc of procedures) {
23814
+ dropped.procedures.push(proc.name);
23815
+ statements.push(ops.dropProcedure(proc.name, proc.schema));
23816
+ }
23817
+ }
23818
+ if (!options.keepFunctions) {
23819
+ for (const fn of functions) {
23820
+ dropped.functions.push(fn.name);
23821
+ statements.push(ops.dropFunction(fn.name, fn.schema));
23822
+ }
23823
+ }
23599
23824
  if (!options.keepViews) {
23600
23825
  for (const view of views) {
23601
23826
  dropped.views.push(view.name);
@@ -23615,18 +23840,6 @@ async function teardownSchema(db, dialect, options = {}) {
23615
23840
  dropped.tables.push(tableName);
23616
23841
  statements.push(ops.dropTable(tableName, table.schema));
23617
23842
  }
23618
- if (!options.keepFunctions) {
23619
- for (const fn of functions) {
23620
- dropped.functions.push(fn.name);
23621
- statements.push(ops.dropFunction(fn.name, fn.schema));
23622
- }
23623
- }
23624
- if (!options.keepProcedures) {
23625
- for (const proc of procedures) {
23626
- dropped.procedures.push(proc.name);
23627
- statements.push(ops.dropProcedure(proc.name, proc.schema));
23628
- }
23629
- }
23630
23843
  const skipTypesDueToKeptRoutines = dialect === "mssql" && (options.keepFunctions || options.keepProcedures);
23631
23844
  if (!options.keepTypes && !skipTypesDueToKeptRoutines) {
23632
23845
  const sortedTypes = [...types].sort((a, b) => {
@@ -24286,25 +24499,60 @@ async function copyVaultSecrets(sourceConfig, destConfig, keys, identityHash, pr
24286
24499
  // src/sdk/namespaces/vault.ts
24287
24500
  var VaultNamespace = class {
24288
24501
  #state;
24502
+ #cryptoIdentity = null;
24289
24503
  constructor(state) {
24290
24504
  this.#state = state;
24291
24505
  }
24506
+ /**
24507
+ * Load the cryptographic identity (with publicKey + identityHash) on
24508
+ * demand. The Context's state.identity is the audit identity (name +
24509
+ * email only); the crypto fields needed for vault encryption live in
24510
+ * `~/.noorm/identity.json` (or a NOORM_IDENTITY_* env override).
24511
+ *
24512
+ * Cached after the first call so repeated vault ops don't re-read disk.
24513
+ */
24514
+ async #getCryptoIdentity() {
24515
+ if (this.#cryptoIdentity) return this.#cryptoIdentity;
24516
+ const loaded = await loadIdentityMetadata();
24517
+ if (!loaded) {
24518
+ throw new Error(
24519
+ "Vault operations require a cryptographic identity at ~/.noorm/identity.json. Run `noorm identity init` or set NOORM_IDENTITY_* env vars in CI."
24520
+ );
24521
+ }
24522
+ this.#cryptoIdentity = loaded;
24523
+ return loaded;
24524
+ }
24292
24525
  // ─────────────────────────────────────────────────────
24293
24526
  // Lifecycle
24294
24527
  // ─────────────────────────────────────────────────────
24295
24528
  /**
24296
24529
  * Initialize the vault for this database.
24297
24530
  *
24531
+ * Idempotent. Returns the vault key on first init; returns [null, null]
24532
+ * on subsequent calls when the vault already exists. Returns [null, Error]
24533
+ * only on real failures (DB errors, encryption errors).
24534
+ *
24535
+ * The `vault:initialized` observer event fires only on first init, never
24536
+ * on repeat calls.
24537
+ *
24298
24538
  * @example
24299
24539
  * ```typescript
24300
- * const [vaultKey, err] = await ctx.noorm.vault.init()
24540
+ * const [vaultKey, err] = await ctx.noorm.vault.init();
24541
+ * if (err) throw err;
24542
+ * if (vaultKey) {
24543
+ * // first-time init — set initial secrets, etc.
24544
+ * }
24545
+ * else {
24546
+ * // already initialized — vault.get / vault.set with private key
24547
+ * }
24301
24548
  * ```
24302
24549
  */
24303
24550
  async init() {
24551
+ const crypto = await this.#getCryptoIdentity();
24304
24552
  return initializeVault(
24305
24553
  this.#kysely,
24306
- this.#identityHash,
24307
- this.#publicKey,
24554
+ crypto.identityHash,
24555
+ crypto.publicKey,
24308
24556
  this.#dialect
24309
24557
  );
24310
24558
  }
@@ -24317,9 +24565,10 @@ var VaultNamespace = class {
24317
24565
  * ```
24318
24566
  */
24319
24567
  async status() {
24568
+ const crypto = await this.#getCryptoIdentity();
24320
24569
  return getVaultStatus(
24321
24570
  this.#kysely,
24322
- this.#identityHash,
24571
+ crypto.identityHash,
24323
24572
  this.#dialect
24324
24573
  );
24325
24574
  }
@@ -24456,13 +24705,14 @@ var VaultNamespace = class {
24456
24705
  * ```
24457
24706
  */
24458
24707
  async copy(destConfig, keys, privateKey, options) {
24708
+ const crypto = await this.#getCryptoIdentity();
24459
24709
  return copyVaultSecrets(
24460
24710
  this.#state.config,
24461
24711
  destConfig,
24462
24712
  keys,
24463
- this.#identityHash,
24713
+ crypto.identityHash,
24464
24714
  privateKey,
24465
- this.#publicKey,
24715
+ crypto.publicKey,
24466
24716
  options
24467
24717
  );
24468
24718
  }
@@ -24475,21 +24725,14 @@ var VaultNamespace = class {
24475
24725
  }
24476
24726
  return this.#state.connection.db;
24477
24727
  }
24478
- get #identityHash() {
24479
- const identity = this.#state.identity;
24480
- return identity["identityHash"] ?? "";
24481
- }
24482
- get #publicKey() {
24483
- const identity = this.#state.identity;
24484
- return identity["publicKey"] ?? "";
24485
- }
24486
24728
  get #dialect() {
24487
24729
  return this.#state.config.connection.dialect;
24488
24730
  }
24489
24731
  async #getVaultKey(privateKey) {
24732
+ const crypto = await this.#getCryptoIdentity();
24490
24733
  return getVaultKey(
24491
24734
  this.#kysely,
24492
- this.#identityHash,
24735
+ crypto.identityHash,
24493
24736
  privateKey,
24494
24737
  this.#dialect
24495
24738
  );
@@ -24575,7 +24818,7 @@ function isSameServer(source, dest) {
24575
24818
  }
24576
24819
  return true;
24577
24820
  }
24578
- function quoteIdent(name) {
24821
+ function quoteIdent2(name) {
24579
24822
  return `"${name.replace(/"/g, '""')}"`;
24580
24823
  }
24581
24824
  var postgresTransferOperations = {
@@ -24592,20 +24835,20 @@ var postgresTransferOperations = {
24592
24835
  return null;
24593
24836
  },
24594
24837
  getResetSequenceSql(table, column, schema) {
24595
- const fullTable = schema ? `${quoteIdent(schema)}.${quoteIdent(table)}` : quoteIdent(table);
24838
+ const fullTable = schema ? `${quoteIdent2(schema)}.${quoteIdent2(table)}` : quoteIdent2(table);
24596
24839
  return `
24597
24840
  SELECT setval(
24598
24841
  pg_get_serial_sequence('${schema ? schema + "." : ""}${table}', '${column}'),
24599
- COALESCE((SELECT MAX(${quoteIdent(column)}) FROM ${fullTable}), 0) + 1,
24842
+ COALESCE((SELECT MAX(${quoteIdent2(column)}) FROM ${fullTable}), 0) + 1,
24600
24843
  false
24601
24844
  )
24602
24845
  `.trim();
24603
24846
  },
24604
24847
  buildConflictInsert(table, columns, primaryKey, strategy) {
24605
- const quotedCols = columns.map(quoteIdent).join(", ");
24848
+ const quotedCols = columns.map(quoteIdent2).join(", ");
24606
24849
  const placeholders = columns.map((_, i) => `$${i + 1}`).join(", ");
24607
- const pkCols = primaryKey.map(quoteIdent).join(", ");
24608
- let insertSql = `INSERT INTO ${quoteIdent(table)} (${quotedCols}) OVERRIDING SYSTEM VALUE VALUES (${placeholders})`;
24850
+ const pkCols = primaryKey.map(quoteIdent2).join(", ");
24851
+ let insertSql = `INSERT INTO ${quoteIdent2(table)} (${quotedCols}) OVERRIDING SYSTEM VALUE VALUES (${placeholders})`;
24609
24852
  switch (strategy) {
24610
24853
  case "fail":
24611
24854
  break;
@@ -24615,7 +24858,7 @@ var postgresTransferOperations = {
24615
24858
  case "update": {
24616
24859
  const updateCols = columns.filter((c) => !primaryKey.includes(c));
24617
24860
  if (updateCols.length > 0) {
24618
- const setClauses = updateCols.map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`).join(", ");
24861
+ const setClauses = updateCols.map((c) => `${quoteIdent2(c)} = EXCLUDED.${quoteIdent2(c)}`).join(", ");
24619
24862
  insertSql += ` ON CONFLICT (${pkCols}) DO UPDATE SET ${setClauses}`;
24620
24863
  } else {
24621
24864
  insertSql += ` ON CONFLICT (${pkCols}) DO NOTHING`;
@@ -24624,7 +24867,7 @@ var postgresTransferOperations = {
24624
24867
  }
24625
24868
  case "replace":
24626
24869
  {
24627
- const setClauses = columns.map((c) => `${quoteIdent(c)} = EXCLUDED.${quoteIdent(c)}`).join(", ");
24870
+ const setClauses = columns.map((c) => `${quoteIdent2(c)} = EXCLUDED.${quoteIdent2(c)}`).join(", ");
24628
24871
  insertSql += ` ON CONFLICT (${pkCols}) DO UPDATE SET ${setClauses}`;
24629
24872
  }
24630
24873
  break;
@@ -24632,9 +24875,9 @@ var postgresTransferOperations = {
24632
24875
  return insertSql;
24633
24876
  },
24634
24877
  buildDirectTransfer(srcDb, srcTable, dstTable, columns, srcSchema = "public", dstSchema = "public") {
24635
- const quotedCols = columns.map(quoteIdent).join(", ");
24636
- const srcFull = `${quoteIdent(srcSchema)}.${quoteIdent(srcTable)}`;
24637
- const dstFull = `${quoteIdent(dstSchema)}.${quoteIdent(dstTable)}`;
24878
+ const quotedCols = columns.map(quoteIdent2).join(", ");
24879
+ const srcFull = `${quoteIdent2(srcSchema)}.${quoteIdent2(srcTable)}`;
24880
+ const dstFull = `${quoteIdent2(dstSchema)}.${quoteIdent2(dstTable)}`;
24638
24881
  return `INSERT INTO ${dstFull} (${quotedCols}) OVERRIDING SYSTEM VALUE SELECT ${quotedCols} FROM ${srcFull}`;
24639
24882
  },
24640
24883
  async executeDisableFK(db) {
@@ -24644,7 +24887,7 @@ var postgresTransferOperations = {
24644
24887
  await sql.raw(this.getEnableFKSql()).execute(db);
24645
24888
  }
24646
24889
  };
24647
- function quoteIdent2(name) {
24890
+ function quoteIdent3(name) {
24648
24891
  return `\`${name.replace(/`/g, "``")}\``;
24649
24892
  }
24650
24893
  var mysqlTransferOperations = {
@@ -24661,32 +24904,32 @@ var mysqlTransferOperations = {
24661
24904
  return null;
24662
24905
  },
24663
24906
  getResetSequenceSql(table, _column, _schema) {
24664
- return `ALTER TABLE ${quoteIdent2(table)} AUTO_INCREMENT = 1`;
24907
+ return `ALTER TABLE ${quoteIdent3(table)} AUTO_INCREMENT = 1`;
24665
24908
  },
24666
24909
  buildConflictInsert(table, columns, primaryKey, strategy) {
24667
- const quotedCols = columns.map(quoteIdent2).join(", ");
24910
+ const quotedCols = columns.map(quoteIdent3).join(", ");
24668
24911
  const placeholders = columns.map(() => "?").join(", ");
24669
24912
  switch (strategy) {
24670
24913
  case "fail":
24671
- return `INSERT INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
24914
+ return `INSERT INTO ${quoteIdent3(table)} (${quotedCols}) VALUES (${placeholders})`;
24672
24915
  case "skip":
24673
- return `INSERT IGNORE INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
24916
+ return `INSERT IGNORE INTO ${quoteIdent3(table)} (${quotedCols}) VALUES (${placeholders})`;
24674
24917
  case "update": {
24675
24918
  const updateCols = columns.filter((c) => !primaryKey.includes(c));
24676
24919
  if (updateCols.length > 0) {
24677
- const setClauses = updateCols.map((c) => `${quoteIdent2(c)} = VALUES(${quoteIdent2(c)})`).join(", ");
24678
- return `INSERT INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${setClauses}`;
24920
+ const setClauses = updateCols.map((c) => `${quoteIdent3(c)} = VALUES(${quoteIdent3(c)})`).join(", ");
24921
+ return `INSERT INTO ${quoteIdent3(table)} (${quotedCols}) VALUES (${placeholders}) ON DUPLICATE KEY UPDATE ${setClauses}`;
24679
24922
  }
24680
- return `INSERT IGNORE INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
24923
+ return `INSERT IGNORE INTO ${quoteIdent3(table)} (${quotedCols}) VALUES (${placeholders})`;
24681
24924
  }
24682
24925
  case "replace":
24683
- return `REPLACE INTO ${quoteIdent2(table)} (${quotedCols}) VALUES (${placeholders})`;
24926
+ return `REPLACE INTO ${quoteIdent3(table)} (${quotedCols}) VALUES (${placeholders})`;
24684
24927
  }
24685
24928
  },
24686
24929
  buildDirectTransfer(srcDb, srcTable, dstTable, columns, _srcSchema, _dstSchema) {
24687
- const quotedCols = columns.map(quoteIdent2).join(", ");
24688
- const srcFull = `${quoteIdent2(srcDb)}.${quoteIdent2(srcTable)}`;
24689
- const dstFull = quoteIdent2(dstTable);
24930
+ const quotedCols = columns.map(quoteIdent3).join(", ");
24931
+ const srcFull = `${quoteIdent3(srcDb)}.${quoteIdent3(srcTable)}`;
24932
+ const dstFull = quoteIdent3(dstTable);
24690
24933
  return `INSERT INTO ${dstFull} (${quotedCols}) SELECT ${quotedCols} FROM ${srcFull}`;
24691
24934
  },
24692
24935
  async executeDisableFK(db) {
@@ -24696,7 +24939,7 @@ var mysqlTransferOperations = {
24696
24939
  await sql.raw(this.getEnableFKSql()).execute(db);
24697
24940
  }
24698
24941
  };
24699
- function quoteIdent3(name) {
24942
+ function quoteIdent4(name) {
24700
24943
  return `[${name.replace(/\]/g, "]]")}]`;
24701
24944
  }
24702
24945
  var mssqlTransferOperations = {
@@ -24707,27 +24950,27 @@ var mssqlTransferOperations = {
24707
24950
  return "-- FK enable per-table";
24708
24951
  },
24709
24952
  getEnableIdentityInsertSql(table) {
24710
- return `SET IDENTITY_INSERT ${quoteIdent3(table)} ON`;
24953
+ return `SET IDENTITY_INSERT ${quoteIdent4(table)} ON`;
24711
24954
  },
24712
24955
  getDisableIdentityInsertSql(table) {
24713
- return `SET IDENTITY_INSERT ${quoteIdent3(table)} OFF`;
24956
+ return `SET IDENTITY_INSERT ${quoteIdent4(table)} OFF`;
24714
24957
  },
24715
24958
  getResetSequenceSql(table, _column, _schema) {
24716
24959
  return `DBCC CHECKIDENT ('${table}', RESEED)`;
24717
24960
  },
24718
24961
  buildConflictInsert(table, columns, primaryKey, strategy) {
24719
- const quotedCols = columns.map(quoteIdent3).join(", ");
24962
+ const quotedCols = columns.map(quoteIdent4).join(", ");
24720
24963
  const placeholders = columns.map((_, i) => `@p${i}`).join(", ");
24721
24964
  switch (strategy) {
24722
24965
  case "fail":
24723
- return `INSERT INTO ${quoteIdent3(table)} (${quotedCols}) VALUES (${placeholders})`;
24966
+ return `INSERT INTO ${quoteIdent4(table)} (${quotedCols}) VALUES (${placeholders})`;
24724
24967
  case "skip":
24725
24968
  case "update":
24726
24969
  case "replace": {
24727
- const pkConditions = primaryKey.map((pk) => `target.${quoteIdent3(pk)} = source.${quoteIdent3(pk)}`).join(" AND ");
24728
- const sourceValues = columns.map((c, i) => `@p${i} AS ${quoteIdent3(c)}`).join(", ");
24970
+ const pkConditions = primaryKey.map((pk) => `target.${quoteIdent4(pk)} = source.${quoteIdent4(pk)}`).join(" AND ");
24971
+ const sourceValues = columns.map((c, i) => `@p${i} AS ${quoteIdent4(c)}`).join(", ");
24729
24972
  let mergeSql = `
24730
- MERGE INTO ${quoteIdent3(table)} AS target
24973
+ MERGE INTO ${quoteIdent4(table)} AS target
24731
24974
  USING (SELECT ${sourceValues}) AS source
24732
24975
  ON (${pkConditions})
24733
24976
  `.trim();
@@ -24735,12 +24978,12 @@ var mssqlTransferOperations = {
24735
24978
  mergeSql += `
24736
24979
  WHEN NOT MATCHED THEN
24737
24980
  INSERT (${quotedCols})
24738
- VALUES (${columns.map((c) => `source.${quoteIdent3(c)}`).join(", ")})
24981
+ VALUES (${columns.map((c) => `source.${quoteIdent4(c)}`).join(", ")})
24739
24982
  `;
24740
24983
  } else if (strategy === "update") {
24741
24984
  const updateCols = columns.filter((c) => !primaryKey.includes(c));
24742
24985
  if (updateCols.length > 0) {
24743
- const setClauses = updateCols.map((c) => `target.${quoteIdent3(c)} = source.${quoteIdent3(c)}`).join(", ");
24986
+ const setClauses = updateCols.map((c) => `target.${quoteIdent4(c)} = source.${quoteIdent4(c)}`).join(", ");
24744
24987
  mergeSql += `
24745
24988
  WHEN MATCHED THEN
24746
24989
  UPDATE SET ${setClauses}
@@ -24749,16 +24992,16 @@ var mssqlTransferOperations = {
24749
24992
  mergeSql += `
24750
24993
  WHEN NOT MATCHED THEN
24751
24994
  INSERT (${quotedCols})
24752
- VALUES (${columns.map((c) => `source.${quoteIdent3(c)}`).join(", ")})
24995
+ VALUES (${columns.map((c) => `source.${quoteIdent4(c)}`).join(", ")})
24753
24996
  `;
24754
24997
  } else {
24755
- const setClauses = columns.map((c) => `target.${quoteIdent3(c)} = source.${quoteIdent3(c)}`).join(", ");
24998
+ const setClauses = columns.map((c) => `target.${quoteIdent4(c)} = source.${quoteIdent4(c)}`).join(", ");
24756
24999
  mergeSql += `
24757
25000
  WHEN MATCHED THEN
24758
25001
  UPDATE SET ${setClauses}
24759
25002
  WHEN NOT MATCHED THEN
24760
25003
  INSERT (${quotedCols})
24761
- VALUES (${columns.map((c) => `source.${quoteIdent3(c)}`).join(", ")})
25004
+ VALUES (${columns.map((c) => `source.${quoteIdent4(c)}`).join(", ")})
24762
25005
  `;
24763
25006
  }
24764
25007
  mergeSql += ";";
@@ -24767,19 +25010,19 @@ var mssqlTransferOperations = {
24767
25010
  }
24768
25011
  },
24769
25012
  buildDirectTransfer(srcDb, srcTable, dstTable, columns, srcSchema = "dbo", dstSchema = "dbo") {
24770
- const quotedCols = columns.map(quoteIdent3).join(", ");
24771
- const srcFull = `${quoteIdent3(srcDb)}.${quoteIdent3(srcSchema)}.${quoteIdent3(srcTable)}`;
24772
- const dstFull = `${quoteIdent3(dstSchema)}.${quoteIdent3(dstTable)}`;
25013
+ const quotedCols = columns.map(quoteIdent4).join(", ");
25014
+ const srcFull = `${quoteIdent4(srcDb)}.${quoteIdent4(srcSchema)}.${quoteIdent4(srcTable)}`;
25015
+ const dstFull = `${quoteIdent4(dstSchema)}.${quoteIdent4(dstTable)}`;
24773
25016
  return `INSERT INTO ${dstFull} (${quotedCols}) SELECT ${quotedCols} FROM ${srcFull}`;
24774
25017
  },
24775
25018
  async executeDisableFK(db, tables) {
24776
25019
  for (const table of tables) {
24777
- await sql.raw(`ALTER TABLE ${quoteIdent3(table)} NOCHECK CONSTRAINT ALL`).execute(db);
25020
+ await sql.raw(`ALTER TABLE ${quoteIdent4(table)} NOCHECK CONSTRAINT ALL`).execute(db);
24778
25021
  }
24779
25022
  },
24780
25023
  async executeEnableFK(db, tables) {
24781
25024
  for (const table of tables) {
24782
- await sql.raw(`ALTER TABLE ${quoteIdent3(table)} WITH CHECK CHECK CONSTRAINT ALL`).execute(db);
25025
+ await sql.raw(`ALTER TABLE ${quoteIdent4(table)} WITH CHECK CHECK CONSTRAINT ALL`).execute(db);
24783
25026
  }
24784
25027
  }
24785
25028
  };
@@ -26319,10 +26562,10 @@ async function transferTableCrossDialect(ctx, plan, transferPlan, options) {
26319
26562
  }, null];
26320
26563
  }
26321
26564
  async function fetchBatch(db, dialect, table, columns, limit, offset, _schema) {
26322
- const quoteIdent4 = dialect === "mssql" ? (c) => `[${c}]` : dialect === "mysql" ? (c) => `\`${c}\`` : (c) => `"${c}"`;
26323
- const columnList = columns.map(quoteIdent4).join(", ");
26565
+ const quoteIdent5 = dialect === "mssql" ? (c) => `[${c}]` : dialect === "mysql" ? (c) => `\`${c}\`` : (c) => `"${c}"`;
26566
+ const columnList = columns.map(quoteIdent5).join(", ");
26324
26567
  if (dialect === "mssql") {
26325
- const orderCol = quoteIdent4(columns[0]);
26568
+ const orderCol = quoteIdent5(columns[0]);
26326
26569
  const result2 = await sql`
26327
26570
  SELECT ${sql.raw(columnList)}
26328
26571
  FROM ${sql.table(table)}
@@ -26988,9 +27231,9 @@ async function exportTableWithWorkers(ctx) {
26988
27231
  totalRows = Number(firstRow?.["cnt"] ?? 0);
26989
27232
  }
26990
27233
  const columns = dtSchema.columns.map((c) => c.name);
26991
- const quoteIdent4 = getQuoteIdent(dialect);
26992
- const columnList = columns.map(quoteIdent4).join(", ");
26993
- const orderCol = quoteIdent4(columns[0]);
27234
+ const quoteIdent5 = getQuoteIdent(dialect);
27235
+ const columnList = columns.map(quoteIdent5).join(", ");
27236
+ const orderCol = quoteIdent5(columns[0]);
26994
27237
  let globalIndex = 0;
26995
27238
  let loaded = 0;
26996
27239
  let processed = 0;
@@ -27748,10 +27991,40 @@ var Context = class {
27748
27991
  throw new Error("SQLite does not support stored procedures.");
27749
27992
  }
27750
27993
  const params = args[0];
27994
+ if (this.dialect === "postgres") {
27995
+ return this.#executeProcPostgres(name, params);
27996
+ }
27751
27997
  const query = buildProcCall(this.dialect, name, params);
27752
27998
  const result = await query.execute(this.kysely);
27753
27999
  return result.rows ?? [];
27754
28000
  }
28001
+ /**
28002
+ * Execute a stored procedure on PostgreSQL with FUNCTION fallback.
28003
+ *
28004
+ * PostgreSQL distinguishes between PROCEDURE and FUNCTION objects:
28005
+ * `CALL` only works against true procedures and raises SQLSTATE 42809
28006
+ * (`<name>(...) is not a procedure`) when the target is a function.
28007
+ *
28008
+ * The noorm playbook tells schema authors to use `CREATE PROCEDURE`
28009
+ * for void-returning ops and `CREATE FUNCTION` for row-returning ops,
28010
+ * and `ctx.proc()` is expected to handle both. So if `CALL` fails
28011
+ * with the wrong-object-type error, we retry with
28012
+ * `SELECT * FROM <name>(...)`, which works for both scalar and
28013
+ * set-returning PG functions.
28014
+ */
28015
+ async #executeProcPostgres(name, params) {
28016
+ const callQuery = buildProcCall("postgres", name, params);
28017
+ const [callResult, callErr] = await attempt(() => callQuery.execute(this.kysely));
28018
+ if (!callErr) {
28019
+ return callResult.rows ?? [];
28020
+ }
28021
+ if (!isFunctionNotProcedureError(callErr)) {
28022
+ throw callErr;
28023
+ }
28024
+ const tvfQuery = buildTvfCall("postgres", name, params);
28025
+ const tvfResult = await tvfQuery.execute(this.kysely);
28026
+ return tvfResult.rows ?? [];
28027
+ }
27755
28028
  /**
27756
28029
  * Call a database function and return the scalar result.
27757
28030
  *
@@ -27867,6 +28140,14 @@ var Context = class {
27867
28140
  return ready;
27868
28141
  }
27869
28142
  };
28143
+ function isFunctionNotProcedureError(err) {
28144
+ if (!err || typeof err !== "object") return false;
28145
+ const obj = err;
28146
+ if (obj.code !== "42809" && obj.code !== "42883") return false;
28147
+ const msg = String(obj.message ?? "");
28148
+ const hint = String(obj.hint ?? "");
28149
+ return msg.includes("is not a procedure") || msg.includes("procedure") || hint.includes("use SELECT");
28150
+ }
27870
28151
 
27871
28152
  // src/sdk/index.ts
27872
28153
  function toSettingsProvider(manager) {
@@ -27874,6 +28155,11 @@ function toSettingsProvider(manager) {
27874
28155
  }
27875
28156
  async function createContext(options = {}) {
27876
28157
  const projectRoot = options.projectRoot ?? process.cwd();
28158
+ const envIdentity = loadIdentityFromEnv();
28159
+ if (envIdentity) {
28160
+ setKeyOverride(envIdentity.privateKey);
28161
+ setIdentityOverride(envIdentity.identity);
28162
+ }
27877
28163
  await initState(projectRoot);
27878
28164
  const state = getStateManager(projectRoot);
27879
28165
  const settingsManager = getSettingsManager(projectRoot);