@objectstack/service-settings 6.7.1 → 6.8.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/index.js CHANGED
@@ -86,6 +86,15 @@ var SettingsService = class {
86
86
  if (extras?.secretStore) this.secretStore = extras.secretStore;
87
87
  if (extras?.auditWriter) this.auditWriter = extras.auditWriter;
88
88
  if (extras?.cryptoProvider) this.cryptoProvider = extras.cryptoProvider;
89
+ for (const ns of this.registry.keys()) {
90
+ this.emitChange({
91
+ namespace: ns,
92
+ key: "*",
93
+ scope: "global",
94
+ action: "set",
95
+ at: (/* @__PURE__ */ new Date()).toISOString()
96
+ });
97
+ }
89
98
  }
90
99
  /**
91
100
  * Cascade priority ranks for lock comparisons (lower = higher
@@ -505,24 +514,31 @@ var SettingsService = class {
505
514
  if (row.encrypted) {
506
515
  if (!row.value_enc) return null;
507
516
  let plain;
508
- if (this.cryptoProvider && this.secretStore && typeof row.value_enc === "string" && row.value_enc.startsWith("sec_")) {
509
- const secret = await this.secretStore.get(row.value_enc);
510
- if (!secret) return null;
511
- plain = await this.cryptoProvider.decrypt(
512
- {
513
- id: secret.id,
514
- kmsKeyId: secret.kms_key_id,
515
- alg: secret.alg,
516
- version: secret.version,
517
- ciphertext: secret.ciphertext
518
- },
519
- { namespace: row.namespace, key: row.key }
517
+ try {
518
+ if (this.cryptoProvider && this.secretStore && typeof row.value_enc === "string" && row.value_enc.startsWith("sec_")) {
519
+ const secret = await this.secretStore.get(row.value_enc);
520
+ if (!secret) return null;
521
+ plain = await this.cryptoProvider.decrypt(
522
+ {
523
+ id: secret.id,
524
+ kmsKeyId: secret.kms_key_id,
525
+ alg: secret.alg,
526
+ version: secret.version,
527
+ ciphertext: secret.ciphertext
528
+ },
529
+ { namespace: row.namespace, key: row.key }
530
+ );
531
+ } else {
532
+ plain = await this.crypto.decrypt(row.value_enc, {
533
+ namespace: row.namespace,
534
+ key: row.key
535
+ });
536
+ }
537
+ } catch (err) {
538
+ console.warn(
539
+ `[SettingsService] failed to decrypt ${row.namespace}.${row.key}: ${err?.message ?? err}. Returning null so the namespace remains readable; re-save the field to repair.`
520
540
  );
521
- } else {
522
- plain = await this.crypto.decrypt(row.value_enc, {
523
- namespace: row.namespace,
524
- key: row.key
525
- });
541
+ return null;
526
542
  }
527
543
  try {
528
544
  return JSON.parse(plain);
@@ -558,6 +574,47 @@ function coerceEnvValue(raw, hint) {
558
574
 
559
575
  // src/in-memory-crypto-provider.ts
560
576
  import { createHash, randomBytes, createCipheriv, createDecipheriv } from "crypto";
577
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
578
+ import { homedir } from "os";
579
+ import { dirname, join } from "path";
580
+ var DEV_KEY_ENV = "OBJECTSTACK_DEV_CRYPTO_KEY";
581
+ var devKeyFallbackPath = () => {
582
+ const proc = globalThis?.process;
583
+ const home = proc?.env?.OBJECTSTACK_HOME || (proc?.env?.HOME ? join(proc.env.HOME, ".objectstack") : void 0) || join(homedir(), ".objectstack");
584
+ return join(home, "dev-crypto-key");
585
+ };
586
+ var loadOrCreateDevKey = () => {
587
+ try {
588
+ const path = devKeyFallbackPath();
589
+ if (existsSync(path)) {
590
+ const raw = readFileSync(path, "utf8").trim();
591
+ const parsed = parseDevKey(raw);
592
+ if (parsed) return { key: parsed, path, generated: false };
593
+ }
594
+ const key = randomBytes(32);
595
+ mkdirSync(dirname(path), { recursive: true });
596
+ writeFileSync(path, key.toString("base64"), { mode: 384 });
597
+ return { key, path, generated: true };
598
+ } catch {
599
+ return void 0;
600
+ }
601
+ };
602
+ var parseDevKey = (raw) => {
603
+ if (!raw) return void 0;
604
+ const trimmed = raw.trim();
605
+ if (!trimmed) return void 0;
606
+ if (/^[0-9a-fA-F]{64}$/.test(trimmed)) return Buffer.from(trimmed, "hex");
607
+ try {
608
+ const normalised = trimmed.replace(/-/g, "+").replace(/_/g, "/");
609
+ const buf = Buffer.from(normalised, "base64");
610
+ if (buf.length === 32) return buf;
611
+ } catch {
612
+ }
613
+ console.warn(
614
+ `[InMemoryCryptoProvider] ${DEV_KEY_ENV} is set but is not 32 bytes (hex or base64). Ignoring and generating an ephemeral key.`
615
+ );
616
+ return void 0;
617
+ };
561
618
  var isWebContainerRuntime = () => {
562
619
  const g = globalThis;
563
620
  return typeof g !== "undefined" && (Boolean(g.process?.versions?.webcontainer) || Boolean(g.process?.env?.SHELL?.includes?.("jsh")) || Boolean(g.process?.env?.STACKBLITZ));
@@ -581,7 +638,37 @@ var loadNobleGcm = () => {
581
638
  };
582
639
  var InMemoryCryptoProvider = class {
583
640
  constructor(opts = {}) {
584
- this.key = opts.key ?? randomBytes(32);
641
+ if (opts.key) {
642
+ this.key = opts.key;
643
+ } else {
644
+ const fromEnv = parseDevKey(
645
+ globalThis?.process?.env?.[DEV_KEY_ENV]
646
+ );
647
+ if (fromEnv) {
648
+ this.key = fromEnv;
649
+ } else {
650
+ const isTest = Boolean(
651
+ globalThis?.process?.env?.VITEST || globalThis?.process?.env?.NODE_ENV === "test"
652
+ );
653
+ const persisted = isTest ? void 0 : loadOrCreateDevKey();
654
+ if (persisted) {
655
+ this.key = persisted.key;
656
+ if (persisted.generated) {
657
+ console.warn(
658
+ `[InMemoryCryptoProvider] No ${DEV_KEY_ENV} set \u2014 generated a new AES-256-GCM key and persisted it to ${persisted.path} (mode 0600). Future restarts will reuse it automatically. For shared/CI environments, set ${DEV_KEY_ENV} explicitly in your environment.`
659
+ );
660
+ }
661
+ } else {
662
+ this.key = randomBytes(32);
663
+ if (!isTest) {
664
+ console.warn(
665
+ `[InMemoryCryptoProvider] No ${DEV_KEY_ENV} set and could not persist a fallback key \u2014 generated an ephemeral AES-256-GCM key. Existing encrypted settings (e.g. AI API keys) will fail to decrypt on next restart. To make the key survive restarts, add this to your .env:
666
+ ${DEV_KEY_ENV}=${this.key.toString("base64")}`
667
+ );
668
+ }
669
+ }
670
+ }
671
+ }
585
672
  this.useNoble = isWebContainerRuntime();
586
673
  }
587
674
  async encrypt(plain, ctx) {
@@ -1449,7 +1536,7 @@ var manifest3 = {
1449
1536
  };
1450
1537
  var aiSettingsManifest = manifest3;
1451
1538
  var aiTestActionHandler = async ({ values, payload }) => {
1452
- const overrides = payload && typeof payload === "object" && payload !== null && "values" in payload ? payload.values ?? {} : {};
1539
+ const overrides = extractOverrides(payload);
1453
1540
  const merged = { ...values, ...overrides };
1454
1541
  const provider = String(merged.provider ?? "memory");
1455
1542
  values = merged;
@@ -1482,6 +1569,14 @@ var aiTestActionHandler = async ({ values, payload }) => {
1482
1569
  message: `${provider} configured (model=${model}). Mount @objectstack/service-ai to exercise live calls.`
1483
1570
  };
1484
1571
  };
1572
+ function extractOverrides(payload) {
1573
+ if (!payload || typeof payload !== "object") return {};
1574
+ const p = payload;
1575
+ if (p.values && typeof p.values === "object" && p.values !== null) {
1576
+ return p.values;
1577
+ }
1578
+ return p;
1579
+ }
1485
1580
 
1486
1581
  // src/manifests/knowledge.manifest.ts
1487
1582
  var manifest4 = {
@@ -2394,7 +2489,7 @@ var SettingsServicePlugin = class {
2394
2489
  }
2395
2490
  if (engine) {
2396
2491
  this.service.bindEngine(
2397
- engine,
2492
+ wrapEngineAsSettingsEngine(engine),
2398
2493
  this.buildAuditSink(ctx, engine),
2399
2494
  {
2400
2495
  secretStore: this.buildSecretStore(engine),
@@ -2470,11 +2565,11 @@ var SettingsServicePlugin = class {
2470
2565
  return row ?? null;
2471
2566
  },
2472
2567
  async update(id, patch) {
2473
- await eng.update("sys_secret", {
2474
- where: { id },
2475
- data: patch,
2476
- bypassTenantAudit: true
2477
- });
2568
+ await eng.update(
2569
+ "sys_secret",
2570
+ { id, ...patch },
2571
+ { bypassTenantAudit: true }
2572
+ );
2478
2573
  }
2479
2574
  };
2480
2575
  }
@@ -2509,6 +2604,30 @@ var SettingsServicePlugin = class {
2509
2604
  };
2510
2605
  }
2511
2606
  };
2607
+ function wrapEngineAsSettingsEngine(engine) {
2608
+ const eng = engine;
2609
+ return {
2610
+ async find(objectName, opts) {
2611
+ return eng.find(objectName, opts);
2612
+ },
2613
+ async insert(objectName, data, opts) {
2614
+ return eng.insert(objectName, data, opts);
2615
+ },
2616
+ async update(objectName, opts) {
2617
+ const { where, data, bypassTenantAudit } = opts;
2618
+ const driverOpts = bypassTenantAudit ? { bypassTenantAudit: true } : void 0;
2619
+ const id = where?.id;
2620
+ if (id !== void 0 && id !== null) {
2621
+ return eng.update(objectName, { id, ...data }, driverOpts);
2622
+ }
2623
+ return eng.update(objectName, data, {
2624
+ where,
2625
+ multi: true,
2626
+ ...driverOpts ?? {}
2627
+ });
2628
+ }
2629
+ };
2630
+ }
2512
2631
  export {
2513
2632
  NoopCryptoAdapter,
2514
2633
  SETTINGS_PLUGIN_ID,