@objectstack/objectql 7.2.1 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,6 +29,8 @@ __export(index_exports, {
29
29
  ObjectRepository: () => ObjectRepository,
30
30
  ObjectStackProtocolImplementation: () => ObjectStackProtocolImplementation,
31
31
  RESERVED_NAMESPACES: () => RESERVED_NAMESPACES,
32
+ SECRET_MASK: () => SECRET_MASK,
33
+ SECRET_REF_PREFIX: () => SECRET_REF_PREFIX,
32
34
  SchemaRegistry: () => SchemaRegistry,
33
35
  ScopedContext: () => ScopedContext,
34
36
  SysMetadataRepository: () => SysMetadataRepository,
@@ -37,11 +39,18 @@ __export(index_exports, {
37
39
  applySystemFields: () => applySystemFields,
38
40
  bindHooksToEngine: () => bindHooksToEngine,
39
41
  bucketDateValue: () => bucketDateValue,
42
+ collectSecretFields: () => collectSecretFields,
40
43
  computeFQN: () => computeFQN,
41
44
  convertIntrospectedSchemaToObjects: () => convertIntrospectedSchemaToObjects,
42
45
  createObjectQLKernel: () => createObjectQLKernel,
46
+ evaluateValidationRules: () => evaluateValidationRules,
47
+ isSecretRef: () => isSecretRef,
48
+ legalNextStates: () => legalNextStates,
49
+ makeSecretRef: () => makeSecretRef,
50
+ needsPriorRecord: () => needsPriorRecord,
43
51
  noopHookMetricsRecorder: () => noopHookMetricsRecorder,
44
52
  parseFQN: () => parseFQN,
53
+ parseSecretRef: () => parseSecretRef,
45
54
  toTitleCase: () => toTitleCase,
46
55
  validateRecord: () => validateRecord,
47
56
  wrapDeclarativeHook: () => wrapDeclarativeHook
@@ -178,6 +187,23 @@ var SchemaRegistry = class {
178
187
  // ==========================================
179
188
  /** Type → Name/ID → MetadataItem */
180
189
  this.metadata = /* @__PURE__ */ new Map();
190
+ /**
191
+ * App name → navigation contributions (ADR-0029 D7).
192
+ *
193
+ * Lets packages inject nav items into apps they do not own (the UI analog
194
+ * of object extenders). Merged into the owning app's `navigation` tree on
195
+ * read in {@link getApp} / {@link getAllApps} by group id + priority.
196
+ */
197
+ this.appNavContributions = /* @__PURE__ */ new Map();
198
+ /**
199
+ * Package ids that must be installed in a DISABLED state. Seeded once at
200
+ * boot (from persisted state) BEFORE any package registration so that every
201
+ * registration path — boot artifact, marketplace rehydrate, local import —
202
+ * honors persisted disable state uniformly without a fragile post-boot
203
+ * re-application hook. See {@link setInitialDisabledPackageIds} and
204
+ * {@link installPackage}.
205
+ */
206
+ this.initialDisabledPackageIds = /* @__PURE__ */ new Set();
181
207
  if (options.multiTenant !== void 0) {
182
208
  this.multiTenant = options.multiTenant;
183
209
  } else {
@@ -194,6 +220,14 @@ var SchemaRegistry = class {
194
220
  if (this._logLevel === "silent" || this._logLevel === "error" || this._logLevel === "warn") return;
195
221
  console.log(msg);
196
222
  }
223
+ /**
224
+ * Seed the set of package ids that should be installed disabled. Call this
225
+ * before package registration begins; later `installPackage` calls for these
226
+ * ids land in the `disabled` state. Replaces any previously seeded set.
227
+ */
228
+ setInitialDisabledPackageIds(ids) {
229
+ this.initialDisabledPackageIds = new Set(ids);
230
+ }
197
231
  // ==========================================
198
232
  // Namespace Management
199
233
  // ==========================================
@@ -388,6 +422,47 @@ var SchemaRegistry = class {
388
422
  const contributors = this.objectContributors.get(fqn);
389
423
  return contributors?.find((c) => c.ownership === "own");
390
424
  }
425
+ /**
426
+ * ADR-0029 K0 — assert every registered object resolves to exactly one
427
+ * owner.
428
+ *
429
+ * A second `own` from a different package is already rejected eagerly in
430
+ * {@link registerObject} (it throws). This is the install-time backstop
431
+ * called once all packages are registered (kernel bootstrap complete),
432
+ * and it additionally catches the case `registerObject` cannot: an object
433
+ * that has only `extend` contributions and **no owner** — which would
434
+ * otherwise resolve to nothing. Surfacing it here turns a silent
435
+ * "extend a non-existent object" into a clear bootstrap error.
436
+ *
437
+ * This is the invariant the kernel-decomposition (ADR-0029) relies on:
438
+ * the `sys` namespace is shared across many first-party plugins, but each
439
+ * object name has exactly one owner.
440
+ *
441
+ * @throws Error listing every object whose owner count is not exactly 1.
442
+ */
443
+ assertSingleOwnerPerObject() {
444
+ const violations = [];
445
+ for (const [fqn, contributors] of this.objectContributors.entries()) {
446
+ const owners = contributors.filter((c) => c.ownership === "own");
447
+ if (owners.length === 0) {
448
+ const extenders = contributors.map((c) => c.packageId).join(", ") || "(none)";
449
+ violations.push(
450
+ `Object "${fqn}" has no owner \u2014 only extend contributions from [${extenders}]. Exactly one package must register it with ownership 'own'.`
451
+ );
452
+ } else if (owners.length > 1) {
453
+ const names = owners.map((c) => c.packageId).join(", ");
454
+ violations.push(
455
+ `Object "${fqn}" has ${owners.length} owners [${names}] \u2014 exactly one is allowed.`
456
+ );
457
+ }
458
+ }
459
+ if (violations.length > 0) {
460
+ throw new Error(
461
+ `[Registry] single-owner-per-object check failed (ADR-0029):
462
+ ` + violations.join("\n ")
463
+ );
464
+ }
465
+ }
391
466
  /**
392
467
  * Unregister all objects contributed by a package.
393
468
  *
@@ -519,10 +594,23 @@ var SchemaRegistry = class {
519
594
  return this.getAllObjects(packageId);
520
595
  }
521
596
  const items = Array.from(this.metadata.get(type)?.values() || []);
597
+ let result = items;
522
598
  if (packageId) {
523
- return items.filter((item) => item._packageId === packageId);
599
+ result = result.filter((item) => item._packageId === packageId);
524
600
  }
525
- return items;
601
+ if (type !== "package") {
602
+ result = result.filter((item) => !this.isPackageDisabled(item?._packageId));
603
+ }
604
+ return result;
605
+ }
606
+ /**
607
+ * Whether a package has been explicitly disabled. Unknown packages and
608
+ * items with no owning package are treated as enabled.
609
+ */
610
+ isPackageDisabled(packageId) {
611
+ if (!packageId) return false;
612
+ const pkg = this.getPackage(packageId);
613
+ return pkg?.enabled === false || pkg?.status === "disabled";
526
614
  }
527
615
  /**
528
616
  * Get all registered metadata types (Kinds)
@@ -539,12 +627,14 @@ var SchemaRegistry = class {
539
627
  // ==========================================
540
628
  installPackage(manifest, settings) {
541
629
  const now = (/* @__PURE__ */ new Date()).toISOString();
630
+ const disabled = this.initialDisabledPackageIds.has(manifest.id);
542
631
  const pkg = {
543
632
  manifest,
544
- status: "installed",
545
- enabled: true,
633
+ status: disabled ? "disabled" : "installed",
634
+ enabled: !disabled,
546
635
  installedAt: now,
547
636
  updatedAt: now,
637
+ ...disabled ? { statusChangedAt: now } : {},
548
638
  settings
549
639
  };
550
640
  if (manifest.namespace) {
@@ -614,10 +704,88 @@ var SchemaRegistry = class {
614
704
  this.registerItem("app", app, "name", packageId);
615
705
  }
616
706
  getApp(name) {
617
- return this.getItem("app", name);
707
+ const app = this.getItem("app", name);
708
+ if (!app) return app;
709
+ return this.applyNavContributions(app);
618
710
  }
619
711
  getAllApps() {
620
- return this.listItems("app");
712
+ return this.listItems("app").map((app) => this.applyNavContributions(app));
713
+ }
714
+ // ==========================================
715
+ // App navigation contributions (ADR-0029 D7)
716
+ // ==========================================
717
+ /**
718
+ * Register a navigation contribution — a package injecting nav items into
719
+ * an app it does not own (the UI-layer analog of object `extend`).
720
+ *
721
+ * Contributions are merged into the target app's `navigation` tree lazily
722
+ * on read ({@link getApp} / {@link getAllApps}) by group id + priority, so
723
+ * registration order does not matter and the owning app can be registered
724
+ * before or after its contributors.
725
+ */
726
+ registerAppNavContribution(contribution, packageId) {
727
+ if (!contribution || !contribution.app) return;
728
+ const list = this.appNavContributions.get(contribution.app) ?? [];
729
+ list.push({
730
+ packageId,
731
+ group: contribution.group,
732
+ priority: contribution.priority ?? 200,
733
+ items: Array.isArray(contribution.items) ? contribution.items : []
734
+ });
735
+ this.appNavContributions.set(contribution.app, list);
736
+ this.log(
737
+ `[Registry] Navigation contribution: ${packageId ?? "(unknown)"} -> ${contribution.app}` + (contribution.group ? `/${contribution.group}` : "") + ` (${list[list.length - 1].items.length} items)`
738
+ );
739
+ }
740
+ /** Contributions registered for an app (empty array when none). */
741
+ getAppNavContributions(appName) {
742
+ return this.appNavContributions.get(appName) ?? [];
743
+ }
744
+ /**
745
+ * Return a copy of `app` with all registered navigation contributions
746
+ * merged into its `navigation` tree. The stored app is never mutated, so
747
+ * repeated reads stay idempotent.
748
+ *
749
+ * Public so the protocol serving path (`getMetaItems` / `getMetaItem` for
750
+ * `app`) can merge contributions the same way `getApp` / `getAllApps` do —
751
+ * the REST app endpoints read through the protocol, not these helpers, so
752
+ * the merge must be reachable from there too (ADR-0029 D7).
753
+ */
754
+ applyNavContributions(app) {
755
+ const contributions = this.appNavContributions.get(app?.name);
756
+ if (!contributions || contributions.length === 0) return app;
757
+ const cloned = structuredClone(app);
758
+ const nav = Array.isArray(cloned.navigation) ? cloned.navigation : cloned.navigation = [];
759
+ const sorted = [...contributions].sort((a, b) => a.priority - b.priority);
760
+ for (const c of sorted) {
761
+ if (!c.items.length) continue;
762
+ if (c.group) {
763
+ const group = this.findNavGroup(nav, c.group);
764
+ if (group) {
765
+ if (!Array.isArray(group.children)) group.children = [];
766
+ group.children.push(...c.items);
767
+ } else {
768
+ this.log(
769
+ `[Registry] Navigation contribution from "${c.packageId ?? "(unknown)"}" targets missing group "${c.group}" in app "${app.name}" \u2014 appending at top level.`
770
+ );
771
+ nav.push(...c.items);
772
+ }
773
+ } else {
774
+ nav.push(...c.items);
775
+ }
776
+ }
777
+ return cloned;
778
+ }
779
+ /** Depth-first search for a `type: 'group'` nav item by id. */
780
+ findNavGroup(items, groupId) {
781
+ for (const item of items) {
782
+ if (item && item.id === groupId && item.type === "group") return item;
783
+ if (item && Array.isArray(item.children)) {
784
+ const found = this.findNavGroup(item.children, groupId);
785
+ if (found) return found;
786
+ }
787
+ }
788
+ return void 0;
621
789
  }
622
790
  // ==========================================
623
791
  // Plugin Helpers
@@ -676,6 +844,7 @@ var SchemaRegistry = class {
676
844
  this.mergedObjectCache.clear();
677
845
  this.namespaceRegistry.clear();
678
846
  this.metadata.clear();
847
+ this.appNavContributions.clear();
679
848
  this.log("[Registry] Reset complete");
680
849
  }
681
850
  };
@@ -830,6 +999,12 @@ var SysMetadataRepository = class {
830
999
  version,
831
1000
  updated_at: now
832
1001
  };
1002
+ if (existing) {
1003
+ const existingPkg = existing.package_id ?? null;
1004
+ parentRowData.package_id = existingPkg ?? opts.packageId ?? null;
1005
+ } else {
1006
+ parentRowData.package_id = opts.packageId ?? null;
1007
+ }
833
1008
  if (existing) {
834
1009
  const existingId = existing.id;
835
1010
  if (existingId === void 0) {
@@ -1361,6 +1536,7 @@ var SysMetadataRepository = class {
1361
1536
  var import_metadata_core2 = require("@objectstack/metadata-core");
1362
1537
  var import_data2 = require("@objectstack/spec/data");
1363
1538
  var import_shared4 = require("@objectstack/spec/shared");
1539
+ var import_ui2 = require("@objectstack/spec/ui");
1364
1540
  var import_system = require("@objectstack/spec/system");
1365
1541
  var import_kernel4 = require("@objectstack/spec/kernel");
1366
1542
  var import_kernel5 = require("@objectstack/spec/kernel");
@@ -2055,6 +2231,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2055
2231
  const zodSchema = (0, import_kernel4.getMetadataTypeSchema)(singular);
2056
2232
  const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
2057
2233
  const form = TYPE_TO_FORM[singular];
2234
+ const typeActions = (0, import_kernel4.getMetadataTypeActions)(singular);
2058
2235
  const base = registryByType.get(singular);
2059
2236
  if (base) {
2060
2237
  const isEnvOverridden = writableOverrides.has(singular);
@@ -2066,7 +2243,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2066
2243
  allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
2067
2244
  overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
2068
2245
  schema,
2069
- form
2246
+ form,
2247
+ // Override the spread `base.actions` with the merged view
2248
+ // (declarative + plugin-registered). Omit when empty to
2249
+ // preserve the prior "no actions key" response shape.
2250
+ ...typeActions.length ? { actions: typeActions } : {}
2070
2251
  };
2071
2252
  }
2072
2253
  return {
@@ -2085,7 +2266,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2085
2266
  domain: "system",
2086
2267
  overrideSource: writableOverrides.has(singular) ? "env" : "registry",
2087
2268
  schema,
2088
- form
2269
+ form,
2270
+ // Plugin-registered actions on a type with no registry entry.
2271
+ ...typeActions.length ? { actions: typeActions } : {}
2089
2272
  };
2090
2273
  }).sort((a, b) => {
2091
2274
  if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
@@ -2183,13 +2366,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2183
2366
  state: "active",
2184
2367
  organization_id: oid
2185
2368
  };
2186
- if (packageId) whereClause._packageId = packageId;
2369
+ if (packageId) whereClause.package_id = packageId;
2187
2370
  let rs = await this.engine.find("sys_metadata", { where: whereClause });
2188
2371
  if (!rs || rs.length === 0) {
2189
2372
  const alt = import_shared4.PLURAL_TO_SINGULAR[request.type] ?? import_shared4.SINGULAR_TO_PLURAL[request.type];
2190
2373
  if (alt) {
2191
2374
  const altWhere = { type: alt, state: "active", organization_id: oid };
2192
- if (packageId) altWhere._packageId = packageId;
2375
+ if (packageId) altWhere.package_id = packageId;
2193
2376
  rs = await this.engine.find("sys_metadata", { where: altWhere });
2194
2377
  }
2195
2378
  }
@@ -2212,6 +2395,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2212
2395
  for (const record of records) {
2213
2396
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2214
2397
  if (data && typeof data === "object" && "name" in data) {
2398
+ const recPkg = record.package_id ?? void 0;
2399
+ if (recPkg && data._packageId === void 0) {
2400
+ data._packageId = recPkg;
2401
+ }
2215
2402
  byName.set(data.name, data);
2216
2403
  }
2217
2404
  if (this.environmentId === void 0) {
@@ -2251,6 +2438,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2251
2438
  }
2252
2439
  } catch {
2253
2440
  }
2441
+ if (request.type !== "package" && request.type !== "object" && request.type !== "objects") {
2442
+ items = items.filter(
2443
+ (it) => !this.engine.registry.isPackageDisabled(it?._packageId)
2444
+ );
2445
+ }
2446
+ if (request.type === "view" || request.type === "views") {
2447
+ items = items.filter((it) => !(0, import_ui2.isAggregatedViewContainer)(it));
2448
+ }
2449
+ if (request.type === "app" || request.type === "apps") {
2450
+ items = items.map((app) => this.engine.registry.applyNavContributions(app));
2451
+ }
2254
2452
  return {
2255
2453
  type: request.type,
2256
2454
  items: decorateMetadataItems(
@@ -2294,6 +2492,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2294
2492
  const record = (orgId ? await findOverlay(orgId) : void 0) ?? await findOverlay(null);
2295
2493
  if (record) {
2296
2494
  item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2495
+ const recPkg = record.package_id ?? void 0;
2496
+ if (recPkg && item && typeof item === "object" && item._packageId === void 0) {
2497
+ item._packageId = recPkg;
2498
+ }
2297
2499
  }
2298
2500
  } catch {
2299
2501
  }
@@ -2336,6 +2538,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2336
2538
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2337
2539
  }
2338
2540
  }
2541
+ if ((request.type === "app" || request.type === "apps") && item) {
2542
+ item = this.engine.registry.applyNavContributions(item);
2543
+ }
2339
2544
  const artifactItem = this.lookupArtifactItem(request.type, request.name);
2340
2545
  const decorated = decorateMetadataItem(
2341
2546
  request.type,
@@ -3094,7 +3299,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3094
3299
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
3095
3300
  }
3096
3301
  const content = JSON.stringify(item);
3097
- const hash = simpleHash(content);
3302
+ const hash = simpleHash(request.locale ? `${request.locale}\0${content}` : content);
3098
3303
  const etag = { value: hash, weak: false };
3099
3304
  if (request.cacheRequest?.ifNoneMatch) {
3100
3305
  const clientEtag = request.cacheRequest.ifNoneMatch.replace(/^"(.*)"$/, "$1").replace(/^W\/"(.*)"$/, "$1");
@@ -3740,7 +3945,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3740
3945
  actor: request.actor ?? "system",
3741
3946
  source: "protocol.saveMetaItem",
3742
3947
  intent,
3743
- state: mode === "draft" ? "draft" : "active"
3948
+ state: mode === "draft" ? "draft" : "active",
3949
+ ...request.packageId !== void 0 ? { packageId: request.packageId } : {}
3744
3950
  });
3745
3951
  if (mode === "publish") {
3746
3952
  this.applyObjectRegistryMutation(request);
@@ -3791,12 +3997,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3791
3997
  where: scopedWhere
3792
3998
  });
3793
3999
  if (existing) {
3794
- await this.engine.update("sys_metadata", {
4000
+ const updateRow = {
3795
4001
  metadata: JSON.stringify(request.item),
3796
4002
  updated_at: now,
3797
4003
  version: (existing.version || 0) + 1,
3798
4004
  state: "active"
3799
- }, {
4005
+ };
4006
+ const existingPkg = existing.package_id ?? null;
4007
+ const nextPkg = existingPkg ?? request.packageId ?? null;
4008
+ if (nextPkg !== null) updateRow.package_id = nextPkg;
4009
+ await this.engine.update("sys_metadata", updateRow, {
3800
4010
  where: { id: existing.id }
3801
4011
  });
3802
4012
  } else {
@@ -3816,6 +4026,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3816
4026
  updated_at: now,
3817
4027
  organization_id: orgId
3818
4028
  };
4029
+ if (request.packageId) row.package_id = request.packageId;
3819
4030
  await this.engine.insert("sys_metadata", row);
3820
4031
  }
3821
4032
  return {
@@ -4540,8 +4751,33 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
4540
4751
  var import_kernel6 = require("@objectstack/spec/kernel");
4541
4752
  var import_core = require("@objectstack/core");
4542
4753
  var import_system2 = require("@objectstack/spec/system");
4754
+
4755
+ // src/secret-fields.ts
4756
+ var SECRET_REF_PREFIX = "secret:";
4757
+ var SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
4758
+ function makeSecretRef(handleId) {
4759
+ return `${SECRET_REF_PREFIX}${handleId}`;
4760
+ }
4761
+ function isSecretRef(value) {
4762
+ return typeof value === "string" && value.startsWith(SECRET_REF_PREFIX);
4763
+ }
4764
+ function parseSecretRef(value) {
4765
+ return isSecretRef(value) ? value.slice(SECRET_REF_PREFIX.length) : null;
4766
+ }
4767
+ function collectSecretFields(schema) {
4768
+ const fields = schema?.fields;
4769
+ if (!fields) return [];
4770
+ const out = [];
4771
+ for (const [name, def] of Object.entries(fields)) {
4772
+ if (def && def.type === "secret") out.push(name);
4773
+ }
4774
+ return out;
4775
+ }
4776
+
4777
+ // src/engine.ts
4543
4778
  var import_shared5 = require("@objectstack/spec/shared");
4544
- var import_formula2 = require("@objectstack/formula");
4779
+ var import_formula3 = require("@objectstack/formula");
4780
+ var import_spec = require("@objectstack/spec");
4545
4781
 
4546
4782
  // src/hook-wrappers.ts
4547
4783
  var import_formula = require("@objectstack/formula");
@@ -5095,6 +5331,101 @@ function validateRecord(objectSchema, data, mode) {
5095
5331
  if (errors.length > 0) throw new ValidationError(errors);
5096
5332
  }
5097
5333
 
5334
+ // src/validation/rule-validator.ts
5335
+ var import_formula2 = require("@objectstack/formula");
5336
+ function needsPriorRecord(objectSchema) {
5337
+ const rules = objectSchema?.validations;
5338
+ if (!Array.isArray(rules)) return false;
5339
+ return rules.some(
5340
+ (r) => r != null && typeof r === "object" && (r.type === "state_machine" || r.type === "cross_field" || r.type === "script")
5341
+ );
5342
+ }
5343
+ function toExpression(cond) {
5344
+ return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
5345
+ }
5346
+ function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
5347
+ const rules = objectSchema?.validations;
5348
+ if (!Array.isArray(rules) || rules.length === 0 || !data) return;
5349
+ const previous = opts.previous ?? void 0;
5350
+ const merged = { ...previous ?? {}, ...data };
5351
+ const errors = [];
5352
+ const ordered = rules.filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
5353
+ const events = r.events ?? ["insert", "update"];
5354
+ return events.includes(mode);
5355
+ }).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
5356
+ for (const rule of ordered) {
5357
+ let violation = null;
5358
+ try {
5359
+ if (rule.type === "state_machine") {
5360
+ violation = checkStateMachine(rule, mode, data, previous);
5361
+ } else if (rule.type === "script" || rule.type === "cross_field") {
5362
+ violation = checkPredicate(rule, merged, previous, opts.logger);
5363
+ }
5364
+ } catch (err) {
5365
+ opts.logger?.warn?.(`Validation rule '${rule.name}' threw \u2014 skipped`, err);
5366
+ continue;
5367
+ }
5368
+ if (!violation) continue;
5369
+ const severity = rule.severity ?? "error";
5370
+ if (severity === "error") {
5371
+ errors.push(violation);
5372
+ } else {
5373
+ opts.logger?.warn?.(
5374
+ `Validation rule '${rule.name}' (${severity}): ${violation.message}`
5375
+ );
5376
+ }
5377
+ }
5378
+ if (errors.length > 0) throw new ValidationError(errors);
5379
+ }
5380
+ function checkStateMachine(rule, mode, data, previous) {
5381
+ if (mode === "insert" || !previous) return null;
5382
+ if (!(rule.field in data)) return null;
5383
+ const from = previous[rule.field];
5384
+ const to = data[rule.field];
5385
+ if (from === to || to === void 0 || to === null) return null;
5386
+ const fromKey = String(from);
5387
+ const allowed = rule.transitions[fromKey];
5388
+ if (!Array.isArray(allowed)) return null;
5389
+ if (!allowed.includes(String(to))) {
5390
+ return {
5391
+ field: rule.field,
5392
+ code: "invalid_transition",
5393
+ message: rule.message || `Invalid transition for ${rule.field}: ${fromKey} \u2192 ${String(to)}`
5394
+ };
5395
+ }
5396
+ return null;
5397
+ }
5398
+ function checkPredicate(rule, record, previous, logger) {
5399
+ const expr = toExpression(rule.condition);
5400
+ const result = import_formula2.ExpressionEngine.evaluate(expr, {
5401
+ record,
5402
+ previous: previous ?? void 0
5403
+ });
5404
+ if (!result.ok) {
5405
+ logger?.warn?.(
5406
+ `Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) \u2014 skipped`
5407
+ );
5408
+ return null;
5409
+ }
5410
+ if (result.value === true) {
5411
+ return {
5412
+ field: rule.fields?.[0] ?? "_record",
5413
+ code: "rule_violation",
5414
+ message: rule.message
5415
+ };
5416
+ }
5417
+ return null;
5418
+ }
5419
+ function legalNextStates(objectSchema, field, currentState) {
5420
+ const rules = objectSchema?.validations;
5421
+ if (!Array.isArray(rules)) return null;
5422
+ const rule = rules.find(
5423
+ (r) => r != null && typeof r === "object" && r.type === "state_machine" && r.field === field
5424
+ );
5425
+ if (!rule) return null;
5426
+ return rule.transitions[currentState] ?? [];
5427
+ }
5428
+
5098
5429
  // src/in-memory-aggregation.ts
5099
5430
  function applyInMemoryAggregation(rows, ast) {
5100
5431
  const groupBy = ast.groupBy ?? [];
@@ -5252,7 +5583,7 @@ function planFormulaProjection(schema, requestedFields) {
5252
5583
  if (def?.type === "formula" && def.expression) {
5253
5584
  const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
5254
5585
  plan.push({ name: f, expression: expr });
5255
- import_formula2.ExpressionEngine.compile(expr);
5586
+ import_formula3.ExpressionEngine.compile(expr);
5256
5587
  } else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
5257
5588
  projected.add(f);
5258
5589
  }
@@ -5274,7 +5605,7 @@ function applyFormulaPlan(plan, records) {
5274
5605
  for (const rec of records) {
5275
5606
  if (rec == null) continue;
5276
5607
  for (const fp of plan) {
5277
- const r = import_formula2.ExpressionEngine.evaluate(fp.expression, { record: rec });
5608
+ const r = import_formula3.ExpressionEngine.evaluate(fp.expression, { record: rec });
5278
5609
  rec[fp.name] = r.ok ? r.value : null;
5279
5610
  }
5280
5611
  }
@@ -5284,7 +5615,7 @@ function resolveMetadataItemName(key, item) {
5284
5615
  if (item.name) return item.name;
5285
5616
  if (item.id) return item.id;
5286
5617
  if (key === "views") {
5287
- return item?.list?.data?.object || item?.form?.data?.object || void 0;
5618
+ return item?.object || item?.list?.data?.object || item?.form?.data?.object || void 0;
5288
5619
  }
5289
5620
  return void 0;
5290
5621
  }
@@ -5296,6 +5627,11 @@ var _ObjectQL = class _ObjectQL {
5296
5627
  this.datasourceMapping = [];
5297
5628
  // Package manifests registry (for defaultDatasource lookup)
5298
5629
  this.manifests = /* @__PURE__ */ new Map();
5630
+ // Datasource definitions by name (ADR-0015): carries schemaMode +
5631
+ // external.allowWrites so the write gate (Gate 3) can enforce federation
5632
+ // ownership. Populated from manifests in registerApp and via
5633
+ // registerDatasourceDef. Absent entry ⇒ treated as managed (default DB).
5634
+ this.datasourceDefs = /* @__PURE__ */ new Map();
5299
5635
  // Per-object hooks with priority support
5300
5636
  this.hooks = /* @__PURE__ */ new Map([
5301
5637
  ["beforeFind", []],
@@ -5678,7 +6014,7 @@ var _ObjectQL = class _ObjectQL {
5678
6014
  if (f.defaultValue == null) continue;
5679
6015
  const dv = f.defaultValue;
5680
6016
  if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
5681
- const result = import_formula2.ExpressionEngine.evaluate(dv, {
6017
+ const result = import_formula3.ExpressionEngine.evaluate(dv, {
5682
6018
  now,
5683
6019
  user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
5684
6020
  org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
@@ -5718,6 +6054,12 @@ var _ObjectQL = class _ObjectQL {
5718
6054
  if (id) {
5719
6055
  this.manifests.set(id, manifest);
5720
6056
  }
6057
+ if (manifest.datasources) {
6058
+ const dsList = Array.isArray(manifest.datasources) ? manifest.datasources : Object.entries(manifest.datasources).map(([name, def]) => ({ name, ...def }));
6059
+ for (const ds of dsList) {
6060
+ if (ds?.name) this.registerDatasourceDef(ds);
6061
+ }
6062
+ }
5721
6063
  this._registry.installPackage(manifest);
5722
6064
  this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
5723
6065
  if (manifest.objects) {
@@ -5771,6 +6113,15 @@ var _ObjectQL = class _ObjectQL {
5771
6113
  this._registry.registerApp(resolved, id);
5772
6114
  this.logger.debug("Registered manifest-as-app", { app: manifest.name, from: id });
5773
6115
  }
6116
+ if (Array.isArray(manifest.navigationContributions) && manifest.navigationContributions.length > 0) {
6117
+ for (const contribution of manifest.navigationContributions) {
6118
+ this._registry.registerAppNavContribution(contribution, id);
6119
+ }
6120
+ this.logger.debug("Registered navigation contributions", {
6121
+ from: id,
6122
+ count: manifest.navigationContributions.length
6123
+ });
6124
+ }
5774
6125
  const metadataArrayKeys = [
5775
6126
  // UI Protocol
5776
6127
  "actions",
@@ -5793,6 +6144,8 @@ var _ObjectQL = class _ObjectQL {
5793
6144
  "policies",
5794
6145
  // AI Protocol
5795
6146
  "agents",
6147
+ "tools",
6148
+ "skills",
5796
6149
  "ragPipelines",
5797
6150
  // API Protocol
5798
6151
  "apis",
@@ -5812,6 +6165,11 @@ var _ObjectQL = class _ObjectQL {
5812
6165
  if (itemName) {
5813
6166
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
5814
6167
  this._registry.registerItem((0, import_shared5.pluralToSingular)(key), toRegister, "name", id);
6168
+ if (key === "views" && (0, import_spec.isAggregatedViewContainer)(toRegister)) {
6169
+ for (const vi of (0, import_spec.expandViewContainer)(itemName, toRegister)) {
6170
+ this._registry.registerItem("view", vi, "name", id);
6171
+ }
6172
+ }
5815
6173
  } else {
5816
6174
  this.logger.warn(`Skipping ${(0, import_shared5.pluralToSingular)(key)} without a derivable name`, { id });
5817
6175
  }
@@ -5964,6 +6322,41 @@ var _ObjectQL = class _ObjectQL {
5964
6322
  this.logger.info("Set default driver", { driverName: driver.name });
5965
6323
  }
5966
6324
  }
6325
+ /**
6326
+ * Register a Datasource *definition* (ADR-0015).
6327
+ *
6328
+ * Distinct from {@link registerDriver}, which registers a live connection.
6329
+ * This captures the declarative `schemaMode` + `external.allowWrites` so the
6330
+ * write gate ({@link assertWriteAllowed}) can enforce external-datasource
6331
+ * ownership. Safe to call repeatedly; last write wins.
6332
+ */
6333
+ registerDatasourceDef(def) {
6334
+ if (!def?.name) return;
6335
+ this.datasourceDefs.set(def.name, { schemaMode: def.schemaMode, external: def.external });
6336
+ }
6337
+ /**
6338
+ * Write gate — Gate 3 of ADR-0015 §5.3.
6339
+ *
6340
+ * Blocks insert/update/delete against a federated datasource
6341
+ * (`schemaMode !== 'managed'`) unless BOTH the datasource opts in
6342
+ * (`external.allowWrites`) AND the object opts in (`external.writable`).
6343
+ * Managed datasources (the common case, including the absence of any
6344
+ * definition) are unaffected.
6345
+ */
6346
+ assertWriteAllowed(objectName, operation) {
6347
+ const object = this._registry.getObject(objectName);
6348
+ const dsName = object?.datasource;
6349
+ if (!dsName || dsName === "default") return;
6350
+ const ds = this.datasourceDefs.get(dsName);
6351
+ if (!ds || !ds.schemaMode || ds.schemaMode === "managed") return;
6352
+ const dsAllows = ds.external?.allowWrites ?? false;
6353
+ const objAllows = object?.external?.writable ?? false;
6354
+ if (!(dsAllows && objAllows)) {
6355
+ throw new import_shared5.ExternalWriteForbiddenError(
6356
+ `Write '${operation}' blocked on object '${objectName}': datasource '${dsName}' is external (schemaMode=${ds.schemaMode}). Requires datasource.external.allowWrites=true (got ${dsAllows}) AND object.external.writable=true (got ${objAllows}).`
6357
+ );
6358
+ }
6359
+ }
5967
6360
  /**
5968
6361
  * Set the realtime service for publishing data change events.
5969
6362
  * Should be called after kernel resolves the realtime service.
@@ -5974,6 +6367,141 @@ var _ObjectQL = class _ObjectQL {
5974
6367
  this.realtimeService = service;
5975
6368
  this.logger.info("RealtimeService configured for data events");
5976
6369
  }
6370
+ /**
6371
+ * Register the crypto provider that backs `secret`-typed fields.
6372
+ *
6373
+ * When set, the engine encrypts secret fields on write (storing ciphertext in
6374
+ * `sys_secret` and only an opaque ref on the business row) and masks them on
6375
+ * read. When NOT set, writing to an object that declares a secret field is
6376
+ * **fail-closed** — the write throws rather than persist cleartext.
6377
+ *
6378
+ * Mirrors the Settings subsystem's ICryptoProvider wiring; the host (e.g.
6379
+ * `serve`) injects `InMemoryCryptoProvider` in dev and a KMS/Vault-backed
6380
+ * provider in production.
6381
+ */
6382
+ setCryptoProvider(provider) {
6383
+ this.cryptoProvider = provider;
6384
+ this.logger.info("CryptoProvider configured for secret fields");
6385
+ }
6386
+ /**
6387
+ * Encrypt any `secret`-typed fields on `row` in place before it reaches the
6388
+ * driver. Each plaintext is wrapped by the ICryptoProvider, persisted as a
6389
+ * `sys_secret` row, and replaced on `row` by an opaque ref. Cleartext never
6390
+ * reaches the business table.
6391
+ *
6392
+ * Rules:
6393
+ * - No secret fields on the object ⇒ no-op (fast path, no crypto cost).
6394
+ * - `null`/`undefined` value ⇒ left as-is (clears the secret).
6395
+ * - Value already a ref (re-save of an unchanged ref) ⇒ left as-is.
6396
+ * - Value equal to the read mask ⇒ dropped, so a form round-trip that
6397
+ * echoes the mask does not overwrite the stored secret.
6398
+ * - **Fail-closed:** any other value with no CryptoProvider registered, or
6399
+ * no reachable `sys_secret` store, THROWS — never persists cleartext.
6400
+ */
6401
+ async encryptSecretFields(object, row, context, driverOptions) {
6402
+ if (!row || typeof row !== "object") return;
6403
+ const schema = this._registry.getObject(object);
6404
+ const secretFields = collectSecretFields(schema);
6405
+ if (secretFields.length === 0) return;
6406
+ for (const field of secretFields) {
6407
+ if (!(field in row)) continue;
6408
+ const value = row[field];
6409
+ if (value === null || typeof value === "undefined") continue;
6410
+ if (isSecretRef(value)) continue;
6411
+ if (value === SECRET_MASK) {
6412
+ delete row[field];
6413
+ continue;
6414
+ }
6415
+ if (!this.cryptoProvider) {
6416
+ throw new Error(
6417
+ `Cannot persist secret field "${object}.${field}": no CryptoProvider is registered. Wire one via engine.setCryptoProvider(...) (e.g. InMemoryCryptoProvider in dev, a KMS/Vault provider in production). Refusing to store cleartext (fail-closed).`
6418
+ );
6419
+ }
6420
+ const plain = typeof value === "string" ? value : JSON.stringify(value);
6421
+ const handle = await this.cryptoProvider.encrypt(plain, {
6422
+ namespace: object,
6423
+ key: field,
6424
+ tenantId: context?.tenantId
6425
+ });
6426
+ let secretDriver;
6427
+ try {
6428
+ secretDriver = this.getDriver("sys_secret");
6429
+ } catch {
6430
+ throw new Error(
6431
+ `Cannot persist secret field "${object}.${field}": the sys_secret store is not available. Ensure the platform-objects (sys_secret) are registered before writing secret fields (fail-closed).`
6432
+ );
6433
+ }
6434
+ await secretDriver.create(
6435
+ "sys_secret",
6436
+ {
6437
+ id: handle.id,
6438
+ namespace: object,
6439
+ key: field,
6440
+ kms_key_id: handle.kmsKeyId,
6441
+ alg: handle.alg,
6442
+ version: handle.version,
6443
+ ciphertext: handle.ciphertext,
6444
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
6445
+ },
6446
+ driverOptions
6447
+ );
6448
+ row[field] = makeSecretRef(handle.id);
6449
+ }
6450
+ }
6451
+ /**
6452
+ * Mask `secret`-typed fields on read so plaintext never leaves the engine
6453
+ * through the normal query path. A set secret becomes {@link SECRET_MASK};
6454
+ * an unset one stays `null`. Privileged callers that genuinely need the
6455
+ * plaintext use {@link resolveSecret} against the stored ref.
6456
+ */
6457
+ maskSecretFields(object, rows) {
6458
+ if (!rows) return;
6459
+ const schema = this._registry.getObject(object);
6460
+ const secretFields = collectSecretFields(schema);
6461
+ if (secretFields.length === 0) return;
6462
+ const list = Array.isArray(rows) ? rows : [rows];
6463
+ for (const row of list) {
6464
+ if (!row || typeof row !== "object") continue;
6465
+ for (const field of secretFields) {
6466
+ if (!(field in row)) continue;
6467
+ row[field] = row[field] == null ? null : SECRET_MASK;
6468
+ }
6469
+ }
6470
+ }
6471
+ /**
6472
+ * Dereference a stored secret ref back to its plaintext. Intended for
6473
+ * privileged, server-side consumers (e.g. a datasource connection-pool
6474
+ * binder) — NOT exposed through the generic read path, which only ever
6475
+ * returns the mask.
6476
+ *
6477
+ * Fail-closed: throws when no CryptoProvider is registered or the
6478
+ * `sys_secret` row is missing. Returns `null` when `ref` is not a secret ref.
6479
+ */
6480
+ async resolveSecret(ref, opts) {
6481
+ const id = parseSecretRef(ref);
6482
+ if (!id) return null;
6483
+ if (!this.cryptoProvider) {
6484
+ throw new Error("Cannot resolve secret: no CryptoProvider is registered (fail-closed).");
6485
+ }
6486
+ const secretDriver = this.getDriver("sys_secret");
6487
+ const found = await secretDriver.find("sys_secret", { object: "sys_secret", where: { id } });
6488
+ const secret = Array.isArray(found) ? found[0] : found;
6489
+ if (!secret) {
6490
+ throw new Error(`Cannot resolve secret: sys_secret row "${id}" not found (fail-closed).`);
6491
+ }
6492
+ const handle = {
6493
+ id: secret.id,
6494
+ kmsKeyId: secret.kms_key_id,
6495
+ alg: secret.alg,
6496
+ version: secret.version,
6497
+ ciphertext: secret.ciphertext
6498
+ };
6499
+ return this.cryptoProvider.decrypt(handle, {
6500
+ namespace: secret.namespace,
6501
+ key: secret.key,
6502
+ tenantId: opts?.tenantId
6503
+ });
6504
+ }
5977
6505
  /**
5978
6506
  * Helper to get object definition
5979
6507
  */
@@ -6266,6 +6794,7 @@ var _ObjectQL = class _ObjectQL {
6266
6794
  hookContext.event = "afterFind";
6267
6795
  hookContext.result = result;
6268
6796
  await this.triggerHooks("afterFind", hookContext);
6797
+ this.maskSecretFields(object, hookContext.result);
6269
6798
  return hookContext.result;
6270
6799
  } catch (e) {
6271
6800
  this.logger.error("Find operation failed", e, { object });
@@ -6307,6 +6836,7 @@ var _ObjectQL = class _ObjectQL {
6307
6836
  const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
6308
6837
  result = expanded[0];
6309
6838
  }
6839
+ this.maskSecretFields(objectName, result);
6310
6840
  return result;
6311
6841
  });
6312
6842
  return opCtx.result;
@@ -6314,6 +6844,7 @@ var _ObjectQL = class _ObjectQL {
6314
6844
  async insert(object, data, options) {
6315
6845
  object = this.resolveObjectName(object);
6316
6846
  this.logger.debug("Insert operation starting", { object, isBatch: Array.isArray(data) });
6847
+ this.assertWriteAllowed(object, "insert");
6317
6848
  const driver = this.getDriver(object);
6318
6849
  const opCtx = {
6319
6850
  object,
@@ -6342,7 +6873,13 @@ var _ObjectQL = class _ObjectQL {
6342
6873
  const rows = hookContext.input.data.map(
6343
6874
  (row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
6344
6875
  );
6345
- for (const r of rows) validateRecord(schemaForValidation, r, "insert");
6876
+ for (const r of rows) {
6877
+ await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
6878
+ }
6879
+ for (const r of rows) {
6880
+ validateRecord(schemaForValidation, r, "insert");
6881
+ evaluateValidationRules(schemaForValidation, r, "insert", { logger: this.logger });
6882
+ }
6346
6883
  if (driver.bulkCreate) {
6347
6884
  result = await driver.bulkCreate(object, rows, hookContext.input.options);
6348
6885
  } else {
@@ -6355,7 +6892,9 @@ var _ObjectQL = class _ObjectQL {
6355
6892
  opCtx.context,
6356
6893
  nowSnap
6357
6894
  );
6895
+ await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
6358
6896
  validateRecord(schemaForValidation, row, "insert");
6897
+ evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
6359
6898
  result = await driver.create(object, row, hookContext.input.options);
6360
6899
  }
6361
6900
  hookContext.event = "afterInsert";
@@ -6405,6 +6944,7 @@ var _ObjectQL = class _ObjectQL {
6405
6944
  async update(object, data, options) {
6406
6945
  object = this.resolveObjectName(object);
6407
6946
  this.logger.debug("Update operation starting", { object });
6947
+ this.assertWriteAllowed(object, "update");
6408
6948
  const driver = this.getDriver(object);
6409
6949
  let id = data.id;
6410
6950
  if (!id && options?.where && typeof options.where === "object" && "id" in options.where) {
@@ -6431,11 +6971,23 @@ var _ObjectQL = class _ObjectQL {
6431
6971
  hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
6432
6972
  try {
6433
6973
  let result;
6974
+ let priorRecord = null;
6975
+ const updateSchema = this._registry.getObject(object);
6434
6976
  if (hookContext.input.id) {
6435
- validateRecord(this._registry.getObject(object), hookContext.input.data, "update");
6977
+ await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
6978
+ validateRecord(updateSchema, hookContext.input.data, "update");
6979
+ if (needsPriorRecord(updateSchema) || (this.hooks.get("afterUpdate")?.length ?? 0) > 0) {
6980
+ const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
6981
+ priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
6982
+ }
6983
+ evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
6436
6984
  result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
6437
6985
  } else if (options?.multi && driver.updateMany) {
6438
- validateRecord(this._registry.getObject(object), hookContext.input.data, "update");
6986
+ await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
6987
+ validateRecord(updateSchema, hookContext.input.data, "update");
6988
+ if (needsPriorRecord(updateSchema)) {
6989
+ this.logger.warn("Object-level validation rules (state_machine/cross_field/script) are not enforced on multi-row updates", { object });
6990
+ }
6439
6991
  const ast = { object, where: options.where };
6440
6992
  result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options);
6441
6993
  } else {
@@ -6443,6 +6995,7 @@ var _ObjectQL = class _ObjectQL {
6443
6995
  }
6444
6996
  hookContext.event = "afterUpdate";
6445
6997
  hookContext.result = result;
6998
+ if (priorRecord) hookContext.previous = priorRecord;
6446
6999
  await this.triggerHooks("afterUpdate", hookContext);
6447
7000
  if (this.realtimeService) {
6448
7001
  try {
@@ -6475,6 +7028,7 @@ var _ObjectQL = class _ObjectQL {
6475
7028
  async delete(object, options) {
6476
7029
  object = this.resolveObjectName(object);
6477
7030
  this.logger.debug("Delete operation starting", { object });
7031
+ this.assertWriteAllowed(object, "delete");
6478
7032
  const driver = this.getDriver(object);
6479
7033
  let id = void 0;
6480
7034
  if (options?.where && typeof options.where === "object" && "id" in options.where) {
@@ -6734,6 +7288,26 @@ var _ObjectQL = class _ObjectQL {
6734
7288
  getDriverByName(name) {
6735
7289
  return this.drivers.get(name);
6736
7290
  }
7291
+ /**
7292
+ * Introspect a datasource's live remote schema (ADR-0015).
7293
+ *
7294
+ * Resolves the driver registered under `datasource` and delegates to its
7295
+ * `introspectSchema()` capability. Used by the external-datasource service
7296
+ * (and CLI/REST) to list remote tables and validate federated objects.
7297
+ *
7298
+ * @throws if the datasource has no registered driver, or the driver does
7299
+ * not support introspection.
7300
+ */
7301
+ async introspectDatasource(datasource) {
7302
+ const driver = this.drivers.get(datasource);
7303
+ if (!driver) {
7304
+ throw new Error(`[ObjectQL] Datasource '${datasource}' has no registered driver to introspect.`);
7305
+ }
7306
+ if (typeof driver.introspectSchema !== "function") {
7307
+ throw new Error(`[ObjectQL] Driver for datasource '${datasource}' does not support introspectSchema().`);
7308
+ }
7309
+ return driver.introspectSchema();
7310
+ }
6737
7311
  /**
6738
7312
  * Get the driver responsible for the given object.
6739
7313
  *
@@ -7667,7 +8241,7 @@ var ObjectQLPlugin = class {
7667
8241
  */
7668
8242
  async loadMetadataFromService(metadataService, ctx) {
7669
8243
  ctx.logger.info("Syncing metadata from external service into ObjectQL registry...");
7670
- const metadataTypes = ["object", "view", "app", "flow", "workflow", "function", "hook"];
8244
+ const metadataTypes = ["object", "view", "app", "flow", "function", "hook"];
7671
8245
  let totalLoaded = 0;
7672
8246
  for (const type of metadataTypes) {
7673
8247
  try {
@@ -7818,6 +8392,8 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
7818
8392
  ObjectRepository,
7819
8393
  ObjectStackProtocolImplementation,
7820
8394
  RESERVED_NAMESPACES,
8395
+ SECRET_MASK,
8396
+ SECRET_REF_PREFIX,
7821
8397
  SchemaRegistry,
7822
8398
  ScopedContext,
7823
8399
  SysMetadataRepository,
@@ -7826,11 +8402,18 @@ function convertIntrospectedSchemaToObjects(introspectedSchema, options) {
7826
8402
  applySystemFields,
7827
8403
  bindHooksToEngine,
7828
8404
  bucketDateValue,
8405
+ collectSecretFields,
7829
8406
  computeFQN,
7830
8407
  convertIntrospectedSchemaToObjects,
7831
8408
  createObjectQLKernel,
8409
+ evaluateValidationRules,
8410
+ isSecretRef,
8411
+ legalNextStates,
8412
+ makeSecretRef,
8413
+ needsPriorRecord,
7832
8414
  noopHookMetricsRecorder,
7833
8415
  parseFQN,
8416
+ parseSecretRef,
7834
8417
  toTitleCase,
7835
8418
  validateRecord,
7836
8419
  wrapDeclarativeHook