@camstack/system 1.2.442 → 1.2.444

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.
Files changed (107) hide show
  1. package/assets/record-entries/icon.svg +8 -0
  2. package/dist/addon-runner.js +41 -11
  3. package/dist/addon-runner.mjs +42 -12
  4. package/dist/addon-utils.js +1 -1
  5. package/dist/addon-utils.mjs +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.d.ts +10 -2
  11. package/dist/builtins/alerts/alerts.addon.js +43 -10
  12. package/dist/builtins/alerts/alerts.addon.mjs +43 -10
  13. package/dist/builtins/autotrack/index.js +1 -1
  14. package/dist/builtins/autotrack/index.mjs +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  16. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  17. package/dist/builtins/calendars/calendars.addon.js +1 -1
  18. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  19. package/dist/builtins/camera-grid/index.js +1 -1
  20. package/dist/builtins/camera-grid/index.mjs +1 -1
  21. package/dist/builtins/composer/composer.addon.js +2 -2
  22. package/dist/builtins/composer/composer.addon.mjs +2 -2
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/block-package.d.ts +4 -2
  26. package/dist/builtins/core-blocks/block-settings-store.d.ts +65 -19
  27. package/dist/builtins/core-blocks/core-blocks.addon.js +365 -38
  28. package/dist/builtins/core-blocks/core-blocks.addon.mjs +365 -38
  29. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  30. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/keychain/index.js +3 -3
  34. package/dist/builtins/keychain/index.mjs +3 -3
  35. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  36. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  37. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  38. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  39. package/dist/builtins/local-network/local-network.addon.js +2 -2
  40. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  41. package/dist/builtins/loki-logging/index.js +1 -1
  42. package/dist/builtins/loki-logging/index.mjs +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.js +2 -2
  44. package/dist/builtins/native-metrics/native-metrics.addon.mjs +2 -2
  45. package/dist/builtins/platform-probe/index.js +2 -2
  46. package/dist/builtins/platform-probe/index.mjs +2 -2
  47. package/dist/builtins/record-entries/entries-service.d.ts +42 -0
  48. package/dist/builtins/record-entries/index.d.ts +1 -0
  49. package/dist/builtins/record-entries/index.js +142 -0
  50. package/dist/builtins/record-entries/index.mjs +136 -0
  51. package/dist/builtins/record-entries/record-entries.addon.d.ts +13 -0
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +1 -1
  55. package/dist/builtins/snapshot/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/file-host-client.d.ts +2 -0
  57. package/dist/builtins/sqlite-storage/file-host-handlers.d.ts +4 -0
  58. package/dist/builtins/sqlite-storage/file-host-protocol.d.ts +7 -1
  59. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  60. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  61. package/dist/builtins/sqlite-storage/index.js +1 -1
  62. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  63. package/dist/builtins/sqlite-storage/owned-sqlite-file.d.ts +7 -0
  64. package/dist/builtins/sqlite-storage/placement-router.d.ts +5 -0
  65. package/dist/builtins/sqlite-storage/records/records-catalog.d.ts +29 -0
  66. package/dist/builtins/sqlite-storage/records/records-engine-types.d.ts +46 -0
  67. package/dist/builtins/sqlite-storage/records/records-engine.d.ts +90 -0
  68. package/dist/builtins/sqlite-storage/records/records-explain.d.ts +18 -0
  69. package/dist/builtins/sqlite-storage/records/records-plan.d.ts +90 -0
  70. package/dist/builtins/sqlite-storage/records/records-probe.d.ts +33 -0
  71. package/dist/builtins/sqlite-storage/records/records-provider.d.ts +59 -0
  72. package/dist/builtins/sqlite-storage/records/records-rows.d.ts +46 -0
  73. package/dist/builtins/sqlite-storage/records/records-sql.d.ts +41 -0
  74. package/dist/builtins/sqlite-storage/sqlite-settings-backend.d.ts +0 -10
  75. package/dist/builtins/sqlite-storage/sqlite-settings.addon.d.ts +8 -0
  76. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +337 -4
  77. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +337 -4
  78. package/dist/builtins/sqlite-storage/storage-admin-provider.d.ts +7 -1
  79. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  80. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  81. package/dist/builtins/system-config/system-config.addon.js +2 -2
  82. package/dist/builtins/system-config/system-config.addon.mjs +2 -2
  83. package/dist/builtins/winston-logging/index.js +1 -1
  84. package/dist/builtins/winston-logging/index.mjs +1 -1
  85. package/dist/{composition-sources-CftPpY4O.mjs → composition-sources-C3rKncIF.mjs} +1 -1
  86. package/dist/{composition-sources-DmSEMtUD.js → composition-sources-NAPZddic.js} +1 -1
  87. package/dist/{constants-2iWQ-Nx-.mjs → constants-BKX2QnIg.mjs} +1 -1
  88. package/dist/{constants-b7kpjuO4.js → constants-BXiNEIeV.js} +1 -1
  89. package/dist/{device-ca-tKY_ZS9H.js → device-ca-DT9X2S6d.js} +1 -1
  90. package/dist/{device-ca-BJpYTAPg.mjs → device-ca-jxjSL8Ov.mjs} +1 -1
  91. package/dist/{dist-CyIHDLCg.js → dist-D1vECb_6.js} +1247 -120
  92. package/dist/{dist-DsrQYcLm.mjs → dist-DERK-gP5.mjs} +1068 -121
  93. package/dist/file-host-worker.js +11 -1
  94. package/dist/file-host-worker.mjs +11 -1
  95. package/dist/{hub-mdns-ciao-BS2akjWx.mjs → hub-mdns-ciao-CRaC97V0.mjs} +1 -1
  96. package/dist/{hub-mdns-ciao-Da-0NEyr.js → hub-mdns-ciao-Cah9GCIg.js} +1 -1
  97. package/dist/index.js +53 -21
  98. package/dist/index.mjs +54 -22
  99. package/dist/kernel/moleculer/records-api-transport.d.ts +9 -0
  100. package/dist/kernel/settings-view-types.d.ts +0 -7
  101. package/dist/{restore-archive-Cdf09dHm.js → restore-archive-Dy-qKTdF.js} +1 -1
  102. package/dist/{restore-archive-RaA3uZ7H.mjs → restore-archive-daKyE24F.mjs} +1 -1
  103. package/dist/{wal-maintenance-C-bmkDhm.js → wal-maintenance-dAv4IdzC.js} +0 -0
  104. package/dist/{wal-maintenance-DE8QrreK.mjs → wal-maintenance-zRc24mOC.mjs} +0 -0
  105. package/package.json +22 -1
  106. package/dist/{custom-action-registry-BKmoaqG0.js → custom-action-registry-COyb49zm.js} +1 -1
  107. package/dist/{custom-action-registry-CyUuhATc.mjs → custom-action-registry-stpC3Zy-.mjs} +1 -1
@@ -1,21 +1,8 @@
1
- /**
2
- * Preference values for core blocks.
3
- *
4
- * Schema lives on the block record (`settingsSchema`, extracted from the
5
- * compiled entry's named export or set by the editor). Values live HERE —
6
- * one JSON blob per blockId — so a settings patch never rewrites the
7
- * operator's source, and so a block that has not yet declared a schema still
8
- * keeps any values the UI already wrote.
9
- *
10
- * On every patch the same values are mirrored into the block addon's
11
- * `addon-settings` rows so a running script reads them the same way any
12
- * addon does: `ctx.settings.readAddonStore()`.
13
- */
14
- import type { SettingsStoreClient } from "@camstack/types";
1
+ import { type ConfigUISchema, type RawSecretMigrationReport, type SecretsAccess, type SettingsStoreClient } from "@camstack/types";
15
2
  /**
16
3
  * @durable class=config owner=core-blocks
17
- * write="`setValues` / `mergeValues` persist the operator's prefs for one block"
18
- * retention="`delete` when the block is deleted. Nothing ages out."
4
+ * write="NOTHING since D880 - the values live in the block addon's `addon-settings` rows. Only the one-shot `migrateRawSecrets` reads it, then deletes each row."
5
+ * retention="legacy: every row is deleted by the verified operator migration; the declaration goes with it once every hub ran it."
19
6
  */
20
7
  export declare const CORE_BLOCK_SETTINGS_COLLECTION = "core-blocks:settings";
21
8
  export declare const CORE_BLOCK_SETTINGS_COLUMNS: readonly [{
@@ -29,15 +16,74 @@ export declare const CORE_BLOCK_SETTINGS_COLUMNS: readonly [{
29
16
  readonly notNull: true;
30
17
  readonly defaultValue: "{}";
31
18
  }];
19
+ /** The keychain door for a given owner, or null when this process has none. */
20
+ export type SecretsForOwner = (owner: string) => SecretsAccess | null;
32
21
  export interface BlockSettingsStoreDeps {
33
22
  readonly store: SettingsStoreClient;
23
+ readonly keychainFor: SecretsForOwner;
24
+ /** Told about keychain refs a write dropped but could not delete (they are now orphans). */
25
+ readonly onOrphaned?: (blockId: string, refs: readonly string[]) => void;
26
+ }
27
+ /** The block declares a secret, but this process has no keychain door to put it in. */
28
+ export declare class BlockSecretsUnavailableError extends Error {
29
+ readonly blockId: string;
30
+ constructor(blockId: string);
31
+ }
32
+ /** The block addon's settings rows could not be read: never answered as "empty". */
33
+ export declare class BlockSettingsUnreadableError extends Error {
34
+ readonly blockId: string;
35
+ constructor(blockId: string, reason: string);
36
+ }
37
+ /** One block as the migration sees it. */
38
+ export interface BlockSecretsSubject {
39
+ readonly id: string;
40
+ readonly settingsSchema?: ConfigUISchema | undefined;
34
41
  }
35
42
  export declare class BlockSettingsStore {
36
43
  private readonly store;
44
+ private readonly keychainFor;
45
+ private readonly onOrphaned;
37
46
  constructor(deps: BlockSettingsStoreDeps);
47
+ /** Declares the LEGACY collection, so the migration can read it. */
38
48
  static declare(store: SettingsStoreClient): Promise<void>;
39
- getValues(blockId: string): Promise<Record<string, unknown>>;
40
- mergeValues(blockId: string, patch: Record<string, unknown>): Promise<Record<string, unknown>>;
49
+ private rows;
50
+ /**
51
+ * The block's stored values - markers, NOT resolved. For the admin form
52
+ * (`hydrateSchema` turns a marker into a "set" view) and as the `stored`
53
+ * side of a write. Absent answers `{}`; an unreadable store THROWS.
54
+ */
55
+ readStored(blockId: string): Promise<Record<string, unknown>>;
56
+ /**
57
+ * Patch the block's values. Secret fields (by the schema TYPE) go to the
58
+ * keychain first and the row gets the marker; a keychain that cannot take
59
+ * them refuses the whole patch.
60
+ */
61
+ mergeValues(blockId: string, schema: ConfigUISchema | undefined, patch: Record<string, unknown>): Promise<void>;
62
+ /** The block is gone: its rows, its keychain values and the legacy blob go with it. */
41
63
  delete(blockId: string): Promise<void>;
42
- private mirrorToBlockAddon;
64
+ /**
65
+ * D880: move every plaintext secret a block's `settingsSchema` declares into
66
+ * the keychain, and delete the legacy `core-blocks:settings` blob.
67
+ *
68
+ * Per block: gather the values (the addon-settings rows win; a key only the
69
+ * blob holds is carried over), put each secret, READ IT BACK and compare,
70
+ * and only then write the marker. The legacy row is deleted only after the
71
+ * block's rows hold every key it had and every secret resolves. A block that
72
+ * fails stays exactly as it was. Dry run unless `apply`; idempotent; counts
73
+ * only - never a value, a ref or a block id.
74
+ */
75
+ migrateRawSecrets(blocks: readonly BlockSecretsSubject[], apply: boolean): Promise<RawSecretMigrationReport>;
76
+ /**
77
+ * The keys of the legacy blob that may be copied into the block's rows: not in
78
+ * the rows already, not a secret the tree moves, not a credential-named value no
79
+ * schema declares (flagged instead), and not a marker that points at another
80
+ * owner's secret (counted as `foreign`, never copied).
81
+ */
82
+ private carryable;
83
+ private dropLegacy;
84
+ /** put -> read back -> compare -> only then the marker rows. */
85
+ private moveBlock;
86
+ /** Remove the values a failed move put, never one the row already pointed at. */
87
+ private discard;
88
+ private report;
43
89
  }
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-CyIHDLCg.js");
4
- const require_composition_sources = require("../../composition-sources-DmSEMtUD.js");
3
+ const require_dist = require("../../dist-D1vECb_6.js");
4
+ const require_composition_sources = require("../../composition-sources-NAPZddic.js");
5
5
  const require_prefix_range = require("../../prefix-range-D_KueXbQ.js");
6
6
  const require_prefix_walk = require("../../prefix-walk-Dv6MLZlM.js");
7
7
  let node_crypto = require("node:crypto");
@@ -10,6 +10,7 @@ let node_fs_promises = require("node:fs/promises");
10
10
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
11
11
  let node_path = require("node:path");
12
12
  node_path = require_chunk.__toESM(node_path);
13
+ let node_util = require("node:util");
13
14
  let node_module = require("node:module");
14
15
  let node_os = require("node:os");
15
16
  node_os = require_chunk.__toESM(node_os);
@@ -75,6 +76,10 @@ import * as blockModule from './block.js'
75
76
 
76
77
  const BLOCK_ID = __BLOCK_ID__
77
78
  const BLOCK_NAME = __BLOCK_NAME__
79
+ // The block's settings form, embedded so the SDK derives the keychain secret
80
+ // tree from it (a password field is stored as a marker and read back resolved,
81
+ // D880). null = the block declares none.
82
+ const BLOCK_SETTINGS_SCHEMA = __BLOCK_SETTINGS_SCHEMA__
78
83
  const blockMain = blockModule.default
79
84
 
80
85
  export default class CoreBlockAddon extends BaseAddon {
@@ -86,6 +91,10 @@ export default class CoreBlockAddon extends BaseAddon {
86
91
  this.disposer = null
87
92
  }
88
93
 
94
+ globalSettingsSchema() {
95
+ return BLOCK_SETTINGS_SCHEMA
96
+ }
97
+
89
98
  async onInitialize() {
90
99
  if (typeof blockMain !== 'function') {
91
100
  throw new Error(
@@ -113,8 +122,8 @@ export default class CoreBlockAddon extends BaseAddon {
113
122
  `;
114
123
  /** Build the wrapper for one block. Exported for the test that asserts the
115
124
  * generated source actually carries the block's identity. */
116
- function renderBlockWrapper(blockId, blockName) {
117
- return WRAPPER_SOURCE.replace("__BLOCK_ID__", JSON.stringify(blockId)).replace("__BLOCK_NAME__", JSON.stringify(blockName));
125
+ function renderBlockWrapper(blockId, blockName, settingsSchema = void 0) {
126
+ return WRAPPER_SOURCE.replace("__BLOCK_ID__", () => JSON.stringify(blockId)).replace("__BLOCK_NAME__", () => JSON.stringify(blockName)).replace("__BLOCK_SETTINGS_SCHEMA__", () => JSON.stringify(settingsSchema ?? null));
118
127
  }
119
128
  /** The package manifest the addon runner reads to find the entry point. */
120
129
  function renderBlockManifest(blockId, blockName) {
@@ -163,7 +172,7 @@ async function materializeBlock(input) {
163
172
  await Promise.all([
164
173
  node_fs_promises.writeFile(node_path.join(addonDir, "package.json"), renderBlockManifest(input.blockId, input.blockName), "utf-8"),
165
174
  node_fs_promises.writeFile(node_path.join(distDir, "block.js"), input.js, "utf-8"),
166
- node_fs_promises.writeFile(node_path.join(distDir, "index.js"), renderBlockWrapper(input.blockId, input.blockName), "utf-8")
175
+ node_fs_promises.writeFile(node_path.join(distDir, "index.js"), renderBlockWrapper(input.blockId, input.blockName, input.settingsSchema), "utf-8")
167
176
  ]);
168
177
  return {
169
178
  addonDir,
@@ -426,7 +435,8 @@ var BlockSupervisor = class {
426
435
  blockId: block.id,
427
436
  blockName: block.name,
428
437
  js: compiled.js,
429
- files
438
+ files,
439
+ settingsSchema: block.settingsSchema
430
440
  });
431
441
  await this.deps.host.spawn({
432
442
  runnerId: pkg.addonId,
@@ -560,9 +570,33 @@ function createAddonSettingsStore(door, addonId) {
560
570
  //#endregion
561
571
  //#region src/builtins/core-blocks/block-settings-store.ts
562
572
  /**
573
+ * Preference values for core blocks.
574
+ *
575
+ * Schema lives on the block record (`settingsSchema`, extracted from the
576
+ * compiled entry's named export or set by the editor). The VALUES live where a
577
+ * running script reads them and nowhere else: the block addon's own
578
+ * `addon-settings` rows (`core-block-<id>.<field>`, D870), so
579
+ * `ctx.settings.readAddonStore()` answers them like any addon's settings and a
580
+ * settings patch never rewrites the operator's source.
581
+ *
582
+ * A field the block's `settingsSchema` declares `password` / `secret: true` is
583
+ * keychain-backed (D783 / D787), owner = the block's addon id, scope `global`:
584
+ * the row holds the marker, the value lives in the keychain. The block's
585
+ * generated wrapper declares the same schema, so the running script reads the
586
+ * resolved VALUE. A marker is honoured only for its own ref (an incoming marker
587
+ * is never stored - the write path treats it as "keep"), a REDACTED / view
588
+ * value keeps what is stored, and a keychain that cannot take a write refuses
589
+ * it rather than storing plaintext.
590
+ *
591
+ * Before D880 the values were ALSO kept as one blob per block in
592
+ * `core-blocks:settings`: two raw copies of every password. That collection is
593
+ * now LEGACY - read only by {@link BlockSettingsStore.migrateRawSecrets}, which
594
+ * moves the secrets, copies any key the rows lack, and deletes the blob.
595
+ */
596
+ /**
563
597
  * @durable class=config owner=core-blocks
564
- * write="`setValues` / `mergeValues` persist the operator's prefs for one block"
565
- * retention="`delete` when the block is deleted. Nothing ages out."
598
+ * write="NOTHING since D880 - the values live in the block addon's `addon-settings` rows. Only the one-shot `migrateRawSecrets` reads it, then deletes each row."
599
+ * retention="legacy: every row is deleted by the verified operator migration; the declaration goes with it once every hub ran it."
566
600
  */
567
601
  var CORE_BLOCK_SETTINGS_COLLECTION = "core-blocks:settings";
568
602
  var CORE_BLOCK_SETTINGS_COLUMNS = [{
@@ -576,54 +610,335 @@ var CORE_BLOCK_SETTINGS_COLUMNS = [{
576
610
  notNull: true,
577
611
  defaultValue: "{}"
578
612
  }];
613
+ /** The block declares a secret, but this process has no keychain door to put it in. */
614
+ var BlockSecretsUnavailableError = class extends Error {
615
+ blockId;
616
+ constructor(blockId) {
617
+ super(`core block "${blockId}" declares a password field, but this process has no keychain door - nothing was saved. Unlock the keychain (System -> Secrets) and save again.`);
618
+ this.blockId = blockId;
619
+ this.name = "BlockSecretsUnavailableError";
620
+ }
621
+ };
622
+ /** The block addon's settings rows could not be read: never answered as "empty". */
623
+ var BlockSettingsUnreadableError = class extends Error {
624
+ blockId;
625
+ constructor(blockId, reason) {
626
+ super(`core block "${blockId}": its settings rows cannot be read (${reason})`);
627
+ this.blockId = blockId;
628
+ this.name = "BlockSettingsUnreadableError";
629
+ }
630
+ };
631
+ function isRecord(value) {
632
+ return typeof value === "object" && value !== null && !Array.isArray(value);
633
+ }
634
+ /** Walk `blob` along the secret `tree`: how many declared secret values are raw / markers. */
635
+ function countSecretLeaves(tree, blob) {
636
+ const counts = {
637
+ raw: 0,
638
+ markers: 0
639
+ };
640
+ if (!isRecord(blob)) return counts;
641
+ const visit = (node, value) => {
642
+ if (node.kind === "leaf") {
643
+ if (require_dist.isSecretMarker(value)) counts.markers += 1;
644
+ else if (typeof value === "string" && value.length > 0) counts.raw += 1;
645
+ return;
646
+ }
647
+ if (node.kind === "object") {
648
+ if (isRecord(value)) for (const [key, child] of node.children) visit(child, value[key]);
649
+ return;
650
+ }
651
+ if (Array.isArray(value)) for (const row of value) {
652
+ if (!isRecord(row)) continue;
653
+ for (const [key, child] of node.item) visit(child, row[key]);
654
+ }
655
+ };
656
+ for (const [key, node] of tree) visit(node, blob[key]);
657
+ return counts;
658
+ }
659
+ /** Only the top-level keys the secret tree names (and, when given, `only` holds). */
660
+ function secretKeysOf(tree, blob, only) {
661
+ const out = {};
662
+ for (const key of tree.keys()) if (key in blob && (only === void 0 || key in only)) out[key] = blob[key];
663
+ return out;
664
+ }
665
+ /**
666
+ * Names that LOOK like a credential. Only used on a block whose schema does not
667
+ * declare the field (none, or a partial one): such a value cannot be moved by type,
668
+ * so it is FLAGGED, never copied and never silently dropped.
669
+ */
670
+ var CREDENTIAL_NAME = /(password|passwd|passphrase|secret|token|api_?key|private_?key|access_?code|credential|cookie|pincode|pin$)/i;
671
+ var NOT_A_VALUE_SUFFIX = /(hash|hashed|fingerprint|count|length|expires|expiry|type|kind|version|status|label|ref|path|id|name)$/i;
672
+ function holdsRawCredential(value, depth = 0) {
673
+ if (depth > 8) return false;
674
+ if (Array.isArray(value)) return value.some((v) => holdsRawCredential(v, depth + 1));
675
+ if (!isRecord(value) || require_dist.isSecretMarker(value)) return false;
676
+ return Object.entries(value).some(([key, child]) => CREDENTIAL_NAME.test(key) && !NOT_A_VALUE_SUFFIX.test(key) ? typeof child === "string" && child.length > 0 || holdsRawCredential(child, depth + 1) : holdsRawCredential(child, depth + 1));
677
+ }
678
+ /** Top-level keys the schema does not declare secret that nevertheless hold a raw credential-named value. */
679
+ function untrackedCredentialKeys(tree, blob) {
680
+ const out = /* @__PURE__ */ new Set();
681
+ for (const [key, value] of Object.entries(blob)) {
682
+ if (tree.has(key)) continue;
683
+ if (CREDENTIAL_NAME.test(key) && !NOT_A_VALUE_SUFFIX.test(key) && typeof value === "string" && value.length > 0 || !require_dist.isSecretMarker(value) && holdsRawCredential(value)) out.add(key);
684
+ }
685
+ return out;
686
+ }
687
+ /** The legacy row's values, tolerating the old bare-blob shape. */
688
+ function legacyValuesOf(row) {
689
+ if (!isRecord(row)) return {};
690
+ const values = row["values"];
691
+ if (isRecord(values)) return { ...values };
692
+ return "values" in row ? {} : { ...row };
693
+ }
579
694
  var BlockSettingsStore = class {
580
695
  store;
696
+ keychainFor;
697
+ onOrphaned;
581
698
  constructor(deps) {
582
699
  this.store = deps.store;
700
+ this.keychainFor = deps.keychainFor;
701
+ this.onOrphaned = deps.onOrphaned ?? (() => void 0);
583
702
  }
703
+ /** Declares the LEGACY collection, so the migration can read it. */
584
704
  static async declare(store) {
585
705
  await store.declareCollection.mutate({
586
706
  collection: CORE_BLOCK_SETTINGS_COLLECTION,
587
707
  columns: [...CORE_BLOCK_SETTINGS_COLUMNS]
588
708
  });
589
709
  }
590
- async getValues(blockId) {
591
- const row = await this.store.get.query({
710
+ rows(blockId) {
711
+ const store = this.store;
712
+ return createAddonSettingsStore({
713
+ query: (input) => store.query.query(input),
714
+ get: (input) => store.get.query(input),
715
+ transact: (input) => store.transact.mutate(input)
716
+ }, require_dist.coreBlockAddonId(blockId));
717
+ }
718
+ /**
719
+ * The block's stored values - markers, NOT resolved. For the admin form
720
+ * (`hydrateSchema` turns a marker into a "set" view) and as the `stored`
721
+ * side of a write. Absent answers `{}`; an unreadable store THROWS.
722
+ */
723
+ async readStored(blockId) {
724
+ const read = await this.rows(blockId).read();
725
+ if (read.kind === "unreadable") throw new BlockSettingsUnreadableError(blockId, read.error);
726
+ return read.kind === "present" ? { ...read.value } : {};
727
+ }
728
+ /**
729
+ * Patch the block's values. Secret fields (by the schema TYPE) go to the
730
+ * keychain first and the row gets the marker; a keychain that cannot take
731
+ * them refuses the whole patch.
732
+ */
733
+ async mergeValues(blockId, schema, patch) {
734
+ const tree = require_dist.secretTreeFromSchema(schema ?? null);
735
+ if (tree.size === 0) {
736
+ await this.rows(blockId).merge(patch);
737
+ return;
738
+ }
739
+ const secrets = this.keychainFor(require_dist.coreBlockAddonId(blockId));
740
+ if (secrets === null) throw new BlockSecretsUnavailableError(blockId);
741
+ const result = await require_dist.transformSecretWrite({
742
+ tree,
743
+ secrets,
744
+ patch,
745
+ stored: await this.readStored(blockId),
746
+ operatorWrite: true
747
+ });
748
+ await this.rows(blockId).merge(result.patch);
749
+ const { orphaned } = await result.afterWrite();
750
+ if (orphaned.length > 0) this.onOrphaned(blockId, orphaned);
751
+ }
752
+ /** The block is gone: its rows, its keychain values and the legacy blob go with it. */
753
+ async delete(blockId) {
754
+ const stored = await this.readStored(blockId);
755
+ const owner = require_dist.coreBlockAddonId(blockId);
756
+ const refs = require_dist.collectSecretMarkerRefs(stored);
757
+ const removal = {};
758
+ for (const key of Object.keys(stored)) removal[key] = void 0;
759
+ await this.rows(blockId).merge(removal);
760
+ await this.store.delete.mutate({
592
761
  collection: CORE_BLOCK_SETTINGS_COLLECTION,
593
762
  key: blockId
594
763
  });
595
- if (row === null || typeof row !== "object") return {};
596
- const values = row.values;
597
- if (values !== null && typeof values === "object" && !Array.isArray(values)) return { ...values };
598
- if (!("values" in row)) return { ...row };
599
- return {};
764
+ if (refs.length === 0) return;
765
+ const secrets = this.keychainFor(owner);
766
+ try {
767
+ await secrets?.delete(refs);
768
+ if (secrets === null) this.onOrphaned(blockId, refs);
769
+ } catch {
770
+ this.onOrphaned(blockId, refs);
771
+ }
600
772
  }
601
- async mergeValues(blockId, patch) {
602
- const next = {
603
- ...await this.getValues(blockId),
604
- ...patch
773
+ /**
774
+ * D880: move every plaintext secret a block's `settingsSchema` declares into
775
+ * the keychain, and delete the legacy `core-blocks:settings` blob.
776
+ *
777
+ * Per block: gather the values (the addon-settings rows win; a key only the
778
+ * blob holds is carried over), put each secret, READ IT BACK and compare,
779
+ * and only then write the marker. The legacy row is deleted only after the
780
+ * block's rows hold every key it had and every secret resolves. A block that
781
+ * fails stays exactly as it was. Dry run unless `apply`; idempotent; counts
782
+ * only - never a value, a ref or a block id.
783
+ */
784
+ async migrateRawSecrets(blocks, apply) {
785
+ const legacyRows = await this.store.query.query({ collection: CORE_BLOCK_SETTINGS_COLLECTION });
786
+ const legacyById = /* @__PURE__ */ new Map();
787
+ for (const row of legacyRows) legacyById.set(row.id, legacyValuesOf(row.data));
788
+ const tally = {
789
+ rows: 0,
790
+ raw: 0,
791
+ markers: 0,
792
+ migrated: 0,
793
+ failed: 0,
794
+ redundant: 0
605
795
  };
606
- await this.store.set.mutate({
607
- collection: CORE_BLOCK_SETTINGS_COLLECTION,
608
- key: blockId,
609
- value: { values: next }
610
- });
611
- await this.mirrorToBlockAddon(blockId, next);
612
- return next;
796
+ const verifyAfter = [];
797
+ for (const block of blocks) {
798
+ const tree = require_dist.secretTreeFromSchema(block.settingsSchema ?? null);
799
+ const legacy = legacyById.get(block.id);
800
+ const stored = await this.readStored(block.id);
801
+ const effective = {
802
+ ...legacy,
803
+ ...stored
804
+ };
805
+ const counts = countSecretLeaves(tree, effective);
806
+ const untracked = untrackedCredentialKeys(tree, effective);
807
+ if (tree.size > 0 || untracked.size > 0) {
808
+ tally.rows += 1;
809
+ if (counts.raw > 0 || untracked.size > 0) tally.raw += 1;
810
+ else if (counts.markers > 0) tally.markers += 1;
811
+ verifyAfter.push(block);
812
+ }
813
+ const hasLegacy = legacy !== void 0;
814
+ if (hasLegacy) tally.redundant += 1;
815
+ if (!apply) continue;
816
+ const carry = this.carryable(block.id, effective, stored, tree, untracked);
817
+ if (carry.foreign > 0) tally.failed += 1;
818
+ if (counts.raw > 0) if (await this.moveBlock(block, tree, carry.rest, effective, stored)) tally.migrated += 1;
819
+ else {
820
+ tally.failed += 1;
821
+ continue;
822
+ }
823
+ else if (hasLegacy && Object.keys(carry.rest).length > 0) await this.rows(block.id).merge(carry.rest);
824
+ const legacyUntracked = untrackedCredentialKeys(tree, legacy ?? {});
825
+ if (hasLegacy && legacyUntracked.size === 0) await this.dropLegacy(block.id);
826
+ }
827
+ const known = new Set(blocks.map((b) => b.id));
828
+ for (const id of legacyById.keys()) {
829
+ if (known.has(id)) continue;
830
+ tally.redundant += 1;
831
+ if (apply) await this.dropLegacy(id);
832
+ }
833
+ return this.report(apply, tally, verifyAfter);
613
834
  }
614
- async delete(blockId) {
835
+ /**
836
+ * The keys of the legacy blob that may be copied into the block's rows: not in
837
+ * the rows already, not a secret the tree moves, not a credential-named value no
838
+ * schema declares (flagged instead), and not a marker that points at another
839
+ * owner's secret (counted as `foreign`, never copied).
840
+ */
841
+ carryable(blockId, effective, stored, tree, untracked) {
842
+ const owner = require_dist.coreBlockAddonId(blockId);
843
+ const rest = {};
844
+ let foreign = 0;
845
+ for (const [key, value] of Object.entries(effective)) {
846
+ if (key in stored || tree.has(key) || untracked.has(key)) continue;
847
+ if (require_dist.collectSecretMarkerRefs(value).some((ref) => !require_dist.isSecretRefOwnedBy(ref, owner))) {
848
+ foreign += 1;
849
+ continue;
850
+ }
851
+ rest[key] = value;
852
+ }
853
+ return {
854
+ rest,
855
+ foreign
856
+ };
857
+ }
858
+ async dropLegacy(blockId) {
615
859
  await this.store.delete.mutate({
616
860
  collection: CORE_BLOCK_SETTINGS_COLLECTION,
617
861
  key: blockId
618
862
  });
619
863
  }
620
- async mirrorToBlockAddon(blockId, values) {
621
- const store = this.store;
622
- await createAddonSettingsStore({
623
- query: (input) => store.query.query(input),
624
- get: (input) => store.get.query(input),
625
- transact: (input) => store.transact.mutate(input)
626
- }, require_dist.coreBlockAddonId(blockId)).merge(values);
864
+ /** put -> read back -> compare -> only then the marker rows. */
865
+ async moveBlock(block, tree, rest, effective, stored) {
866
+ const secrets = this.keychainFor(require_dist.coreBlockAddonId(block.id));
867
+ if (secrets === null) return false;
868
+ const original = secretKeysOf(tree, effective);
869
+ let written = null;
870
+ try {
871
+ written = await require_dist.transformSecretWrite({
872
+ tree,
873
+ secrets,
874
+ patch: original,
875
+ stored: secretKeysOf(tree, stored),
876
+ operatorWrite: true
877
+ });
878
+ const back = await require_dist.resolveSecretMarkers(written.patch, secrets, { unresolved: "omit" });
879
+ const expected = await require_dist.resolveSecretMarkers(secretKeysOf(tree, original, written.patch), secrets, { unresolved: "omit" });
880
+ if (back.unresolved.length > 0 || expected.unresolved.length > 0 || !(0, node_util.isDeepStrictEqual)(back.value, expected.value)) {
881
+ await this.discard(secrets, written.patch, stored);
882
+ return false;
883
+ }
884
+ await this.rows(block.id).merge({
885
+ ...rest,
886
+ ...written.patch
887
+ });
888
+ await written.afterWrite();
889
+ return true;
890
+ } catch {
891
+ if (written !== null) await this.discard(secrets, written.patch, stored);
892
+ return false;
893
+ }
894
+ }
895
+ /** Remove the values a failed move put, never one the row already pointed at. */
896
+ async discard(secrets, patch, stored) {
897
+ const had = new Set(require_dist.collectSecretMarkerRefs(stored));
898
+ const fresh = require_dist.collectSecretMarkerRefs(patch).filter((ref) => !had.has(ref));
899
+ if (fresh.length === 0) return;
900
+ try {
901
+ await secrets.delete(fresh);
902
+ } catch {}
903
+ }
904
+ async report(apply, tally, subjects) {
905
+ let remainingRaw = 0;
906
+ let unresolvedMarkers = 0;
907
+ const legacyById = /* @__PURE__ */ new Map();
908
+ for (const row of await this.store.query.query({ collection: CORE_BLOCK_SETTINGS_COLLECTION })) legacyById.set(row.id, legacyValuesOf(row.data));
909
+ for (const block of subjects) {
910
+ const tree = require_dist.secretTreeFromSchema(block.settingsSchema ?? null);
911
+ const after = await this.readStored(block.id);
912
+ const counts = countSecretLeaves(tree, after);
913
+ remainingRaw += counts.raw;
914
+ const legacyKeys = legacyById.get(block.id) ?? {};
915
+ for (const key of untrackedCredentialKeys(tree, after)) if (!(key in legacyKeys)) remainingRaw += 1;
916
+ if (counts.markers === 0) continue;
917
+ const secrets = this.keychainFor(require_dist.coreBlockAddonId(block.id));
918
+ if (secrets === null) {
919
+ unresolvedMarkers += counts.markers;
920
+ continue;
921
+ }
922
+ const resolved = await require_dist.resolveSecretMarkers(secretKeysOf(tree, after), secrets, { unresolved: "omit" });
923
+ unresolvedMarkers += resolved.unresolved.length;
924
+ }
925
+ const bySchema = new Map(subjects.map((b) => [b.id, b.settingsSchema]));
926
+ for (const [id, values] of legacyById) {
927
+ const tree = require_dist.secretTreeFromSchema(bySchema.get(id) ?? null);
928
+ remainingRaw += countSecretLeaves(tree, values).raw + untrackedCredentialKeys(tree, values).size;
929
+ }
930
+ return {
931
+ apply,
932
+ rows: tally.rows,
933
+ raw: tally.raw,
934
+ markers: tally.markers,
935
+ migrated: tally.migrated,
936
+ failed: tally.failed,
937
+ remainingRaw,
938
+ unresolvedMarkers,
939
+ redundantCopies: tally.redundant,
940
+ verified: remainingRaw === 0 && unresolvedMarkers === 0
941
+ };
627
942
  }
628
943
  };
629
944
  //#endregion
@@ -1953,7 +2268,14 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
1953
2268
  store: settingsStore,
1954
2269
  logger: this.ctx.logger.child("store")
1955
2270
  });
1956
- this.prefs = new BlockSettingsStore({ store: settingsStore });
2271
+ this.prefs = new BlockSettingsStore({
2272
+ store: settingsStore,
2273
+ keychainFor: (owner) => this.ctx.secrets?.forOwner?.(owner) ?? null,
2274
+ onOrphaned: (blockId, refs) => this.ctx.logger.warn("core block settings: dropped keychain refs could not be deleted", { meta: {
2275
+ blockId,
2276
+ refs: [...refs]
2277
+ } })
2278
+ });
1957
2279
  await this.store.load();
1958
2280
  this.transpiler = await loadTranspiler(this.ctx.logger);
1959
2281
  armBlocksIntegration({
@@ -2213,13 +2535,18 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
2213
2535
  if (block === null) return null;
2214
2536
  const schema = block.settingsSchema;
2215
2537
  if (schema === void 0) return null;
2216
- return require_dist.hydrateSchema(schema, await this.requirePrefs().getValues(blockId));
2538
+ return require_dist.hydrateSchema(schema, await this.requirePrefs().readStored(blockId), { inlineSecretsAsViews: true });
2217
2539
  },
2218
2540
  updateBlockSettings: async ({ blockId, patch }) => {
2219
- if (this.requireStore().get(blockId) === null) throw new Error(`core-blocks: no block "${blockId}"`);
2220
- await this.requirePrefs().mergeValues(blockId, patch);
2541
+ const block = this.requireStore().get(blockId);
2542
+ if (block === null) throw new Error(`core-blocks: no block "${blockId}"`);
2543
+ await this.requirePrefs().mergeValues(blockId, block.settingsSchema, patch);
2221
2544
  return { success: true };
2222
2545
  },
2546
+ migrateRawSecrets: async ({ apply }) => this.requirePrefs().migrateRawSecrets(this.requireStore().list().map((b) => ({
2547
+ id: b.id,
2548
+ settingsSchema: b.settingsSchema
2549
+ })), apply),
2223
2550
  getTypeDefs: async () => {
2224
2551
  this.typeDefs ??= await loadBlockTypeDefs(this.ctx.logger);
2225
2552
  return { libs: this.typeDefs.map((l) => ({