@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.mjs CHANGED
@@ -128,6 +128,23 @@ var SchemaRegistry = class {
128
128
  // ==========================================
129
129
  /** Type → Name/ID → MetadataItem */
130
130
  this.metadata = /* @__PURE__ */ new Map();
131
+ /**
132
+ * App name → navigation contributions (ADR-0029 D7).
133
+ *
134
+ * Lets packages inject nav items into apps they do not own (the UI analog
135
+ * of object extenders). Merged into the owning app's `navigation` tree on
136
+ * read in {@link getApp} / {@link getAllApps} by group id + priority.
137
+ */
138
+ this.appNavContributions = /* @__PURE__ */ new Map();
139
+ /**
140
+ * Package ids that must be installed in a DISABLED state. Seeded once at
141
+ * boot (from persisted state) BEFORE any package registration so that every
142
+ * registration path — boot artifact, marketplace rehydrate, local import —
143
+ * honors persisted disable state uniformly without a fragile post-boot
144
+ * re-application hook. See {@link setInitialDisabledPackageIds} and
145
+ * {@link installPackage}.
146
+ */
147
+ this.initialDisabledPackageIds = /* @__PURE__ */ new Set();
131
148
  if (options.multiTenant !== void 0) {
132
149
  this.multiTenant = options.multiTenant;
133
150
  } else {
@@ -144,6 +161,14 @@ var SchemaRegistry = class {
144
161
  if (this._logLevel === "silent" || this._logLevel === "error" || this._logLevel === "warn") return;
145
162
  console.log(msg);
146
163
  }
164
+ /**
165
+ * Seed the set of package ids that should be installed disabled. Call this
166
+ * before package registration begins; later `installPackage` calls for these
167
+ * ids land in the `disabled` state. Replaces any previously seeded set.
168
+ */
169
+ setInitialDisabledPackageIds(ids) {
170
+ this.initialDisabledPackageIds = new Set(ids);
171
+ }
147
172
  // ==========================================
148
173
  // Namespace Management
149
174
  // ==========================================
@@ -338,6 +363,47 @@ var SchemaRegistry = class {
338
363
  const contributors = this.objectContributors.get(fqn);
339
364
  return contributors?.find((c) => c.ownership === "own");
340
365
  }
366
+ /**
367
+ * ADR-0029 K0 — assert every registered object resolves to exactly one
368
+ * owner.
369
+ *
370
+ * A second `own` from a different package is already rejected eagerly in
371
+ * {@link registerObject} (it throws). This is the install-time backstop
372
+ * called once all packages are registered (kernel bootstrap complete),
373
+ * and it additionally catches the case `registerObject` cannot: an object
374
+ * that has only `extend` contributions and **no owner** — which would
375
+ * otherwise resolve to nothing. Surfacing it here turns a silent
376
+ * "extend a non-existent object" into a clear bootstrap error.
377
+ *
378
+ * This is the invariant the kernel-decomposition (ADR-0029) relies on:
379
+ * the `sys` namespace is shared across many first-party plugins, but each
380
+ * object name has exactly one owner.
381
+ *
382
+ * @throws Error listing every object whose owner count is not exactly 1.
383
+ */
384
+ assertSingleOwnerPerObject() {
385
+ const violations = [];
386
+ for (const [fqn, contributors] of this.objectContributors.entries()) {
387
+ const owners = contributors.filter((c) => c.ownership === "own");
388
+ if (owners.length === 0) {
389
+ const extenders = contributors.map((c) => c.packageId).join(", ") || "(none)";
390
+ violations.push(
391
+ `Object "${fqn}" has no owner \u2014 only extend contributions from [${extenders}]. Exactly one package must register it with ownership 'own'.`
392
+ );
393
+ } else if (owners.length > 1) {
394
+ const names = owners.map((c) => c.packageId).join(", ");
395
+ violations.push(
396
+ `Object "${fqn}" has ${owners.length} owners [${names}] \u2014 exactly one is allowed.`
397
+ );
398
+ }
399
+ }
400
+ if (violations.length > 0) {
401
+ throw new Error(
402
+ `[Registry] single-owner-per-object check failed (ADR-0029):
403
+ ` + violations.join("\n ")
404
+ );
405
+ }
406
+ }
341
407
  /**
342
408
  * Unregister all objects contributed by a package.
343
409
  *
@@ -469,10 +535,23 @@ var SchemaRegistry = class {
469
535
  return this.getAllObjects(packageId);
470
536
  }
471
537
  const items = Array.from(this.metadata.get(type)?.values() || []);
538
+ let result = items;
472
539
  if (packageId) {
473
- return items.filter((item) => item._packageId === packageId);
540
+ result = result.filter((item) => item._packageId === packageId);
474
541
  }
475
- return items;
542
+ if (type !== "package") {
543
+ result = result.filter((item) => !this.isPackageDisabled(item?._packageId));
544
+ }
545
+ return result;
546
+ }
547
+ /**
548
+ * Whether a package has been explicitly disabled. Unknown packages and
549
+ * items with no owning package are treated as enabled.
550
+ */
551
+ isPackageDisabled(packageId) {
552
+ if (!packageId) return false;
553
+ const pkg = this.getPackage(packageId);
554
+ return pkg?.enabled === false || pkg?.status === "disabled";
476
555
  }
477
556
  /**
478
557
  * Get all registered metadata types (Kinds)
@@ -489,12 +568,14 @@ var SchemaRegistry = class {
489
568
  // ==========================================
490
569
  installPackage(manifest, settings) {
491
570
  const now = (/* @__PURE__ */ new Date()).toISOString();
571
+ const disabled = this.initialDisabledPackageIds.has(manifest.id);
492
572
  const pkg = {
493
573
  manifest,
494
- status: "installed",
495
- enabled: true,
574
+ status: disabled ? "disabled" : "installed",
575
+ enabled: !disabled,
496
576
  installedAt: now,
497
577
  updatedAt: now,
578
+ ...disabled ? { statusChangedAt: now } : {},
498
579
  settings
499
580
  };
500
581
  if (manifest.namespace) {
@@ -564,10 +645,88 @@ var SchemaRegistry = class {
564
645
  this.registerItem("app", app, "name", packageId);
565
646
  }
566
647
  getApp(name) {
567
- return this.getItem("app", name);
648
+ const app = this.getItem("app", name);
649
+ if (!app) return app;
650
+ return this.applyNavContributions(app);
568
651
  }
569
652
  getAllApps() {
570
- return this.listItems("app");
653
+ return this.listItems("app").map((app) => this.applyNavContributions(app));
654
+ }
655
+ // ==========================================
656
+ // App navigation contributions (ADR-0029 D7)
657
+ // ==========================================
658
+ /**
659
+ * Register a navigation contribution — a package injecting nav items into
660
+ * an app it does not own (the UI-layer analog of object `extend`).
661
+ *
662
+ * Contributions are merged into the target app's `navigation` tree lazily
663
+ * on read ({@link getApp} / {@link getAllApps}) by group id + priority, so
664
+ * registration order does not matter and the owning app can be registered
665
+ * before or after its contributors.
666
+ */
667
+ registerAppNavContribution(contribution, packageId) {
668
+ if (!contribution || !contribution.app) return;
669
+ const list = this.appNavContributions.get(contribution.app) ?? [];
670
+ list.push({
671
+ packageId,
672
+ group: contribution.group,
673
+ priority: contribution.priority ?? 200,
674
+ items: Array.isArray(contribution.items) ? contribution.items : []
675
+ });
676
+ this.appNavContributions.set(contribution.app, list);
677
+ this.log(
678
+ `[Registry] Navigation contribution: ${packageId ?? "(unknown)"} -> ${contribution.app}` + (contribution.group ? `/${contribution.group}` : "") + ` (${list[list.length - 1].items.length} items)`
679
+ );
680
+ }
681
+ /** Contributions registered for an app (empty array when none). */
682
+ getAppNavContributions(appName) {
683
+ return this.appNavContributions.get(appName) ?? [];
684
+ }
685
+ /**
686
+ * Return a copy of `app` with all registered navigation contributions
687
+ * merged into its `navigation` tree. The stored app is never mutated, so
688
+ * repeated reads stay idempotent.
689
+ *
690
+ * Public so the protocol serving path (`getMetaItems` / `getMetaItem` for
691
+ * `app`) can merge contributions the same way `getApp` / `getAllApps` do —
692
+ * the REST app endpoints read through the protocol, not these helpers, so
693
+ * the merge must be reachable from there too (ADR-0029 D7).
694
+ */
695
+ applyNavContributions(app) {
696
+ const contributions = this.appNavContributions.get(app?.name);
697
+ if (!contributions || contributions.length === 0) return app;
698
+ const cloned = structuredClone(app);
699
+ const nav = Array.isArray(cloned.navigation) ? cloned.navigation : cloned.navigation = [];
700
+ const sorted = [...contributions].sort((a, b) => a.priority - b.priority);
701
+ for (const c of sorted) {
702
+ if (!c.items.length) continue;
703
+ if (c.group) {
704
+ const group = this.findNavGroup(nav, c.group);
705
+ if (group) {
706
+ if (!Array.isArray(group.children)) group.children = [];
707
+ group.children.push(...c.items);
708
+ } else {
709
+ this.log(
710
+ `[Registry] Navigation contribution from "${c.packageId ?? "(unknown)"}" targets missing group "${c.group}" in app "${app.name}" \u2014 appending at top level.`
711
+ );
712
+ nav.push(...c.items);
713
+ }
714
+ } else {
715
+ nav.push(...c.items);
716
+ }
717
+ }
718
+ return cloned;
719
+ }
720
+ /** Depth-first search for a `type: 'group'` nav item by id. */
721
+ findNavGroup(items, groupId) {
722
+ for (const item of items) {
723
+ if (item && item.id === groupId && item.type === "group") return item;
724
+ if (item && Array.isArray(item.children)) {
725
+ const found = this.findNavGroup(item.children, groupId);
726
+ if (found) return found;
727
+ }
728
+ }
729
+ return void 0;
571
730
  }
572
731
  // ==========================================
573
732
  // Plugin Helpers
@@ -626,6 +785,7 @@ var SchemaRegistry = class {
626
785
  this.mergedObjectCache.clear();
627
786
  this.namespaceRegistry.clear();
628
787
  this.metadata.clear();
788
+ this.appNavContributions.clear();
629
789
  this.log("[Registry] Reset complete");
630
790
  }
631
791
  };
@@ -780,6 +940,12 @@ var SysMetadataRepository = class {
780
940
  version,
781
941
  updated_at: now
782
942
  };
943
+ if (existing) {
944
+ const existingPkg = existing.package_id ?? null;
945
+ parentRowData.package_id = existingPkg ?? opts.packageId ?? null;
946
+ } else {
947
+ parentRowData.package_id = opts.packageId ?? null;
948
+ }
783
949
  if (existing) {
784
950
  const existingId = existing.id;
785
951
  if (existingId === void 0) {
@@ -1311,8 +1477,9 @@ var SysMetadataRepository = class {
1311
1477
  import { ConflictError as ConflictError2 } from "@objectstack/metadata-core";
1312
1478
  import { parseFilterAST, isFilterAST } from "@objectstack/spec/data";
1313
1479
  import { PLURAL_TO_SINGULAR as PLURAL_TO_SINGULAR3, SINGULAR_TO_PLURAL as SINGULAR_TO_PLURAL2 } from "@objectstack/spec/shared";
1480
+ import { isAggregatedViewContainer } from "@objectstack/spec/ui";
1314
1481
  import { METADATA_FORM_REGISTRY } from "@objectstack/spec/system";
1315
- import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2 } from "@objectstack/spec/kernel";
1482
+ import { DEFAULT_METADATA_TYPE_REGISTRY as DEFAULT_METADATA_TYPE_REGISTRY2, getMetadataTypeSchema as getMetadataTypeSchema2, getMetadataTypeActions } from "@objectstack/spec/kernel";
1316
1483
  import {
1317
1484
  extractProtection,
1318
1485
  evaluateLockForWrite,
@@ -2010,6 +2177,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2010
2177
  const zodSchema = getMetadataTypeSchema2(singular);
2011
2178
  const schema = (zodSchema ? toJsonSchemaSafe(zodSchema) : void 0) ?? HAND_CRAFTED_SCHEMAS[singular];
2012
2179
  const form = TYPE_TO_FORM[singular];
2180
+ const typeActions = getMetadataTypeActions(singular);
2013
2181
  const base = registryByType.get(singular);
2014
2182
  if (base) {
2015
2183
  const isEnvOverridden = writableOverrides.has(singular);
@@ -2021,7 +2189,11 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2021
2189
  allowOrgOverride: base.allowOrgOverride || isEnvOverridden,
2022
2190
  overrideSource: isEnvOverridden && !base.allowOrgOverride ? "env" : "registry",
2023
2191
  schema,
2024
- form
2192
+ form,
2193
+ // Override the spread `base.actions` with the merged view
2194
+ // (declarative + plugin-registered). Omit when empty to
2195
+ // preserve the prior "no actions key" response shape.
2196
+ ...typeActions.length ? { actions: typeActions } : {}
2025
2197
  };
2026
2198
  }
2027
2199
  return {
@@ -2040,7 +2212,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2040
2212
  domain: "system",
2041
2213
  overrideSource: writableOverrides.has(singular) ? "env" : "registry",
2042
2214
  schema,
2043
- form
2215
+ form,
2216
+ // Plugin-registered actions on a type with no registry entry.
2217
+ ...typeActions.length ? { actions: typeActions } : {}
2044
2218
  };
2045
2219
  }).sort((a, b) => {
2046
2220
  if (a.domain !== b.domain) return a.domain.localeCompare(b.domain);
@@ -2138,13 +2312,13 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2138
2312
  state: "active",
2139
2313
  organization_id: oid
2140
2314
  };
2141
- if (packageId) whereClause._packageId = packageId;
2315
+ if (packageId) whereClause.package_id = packageId;
2142
2316
  let rs = await this.engine.find("sys_metadata", { where: whereClause });
2143
2317
  if (!rs || rs.length === 0) {
2144
2318
  const alt = PLURAL_TO_SINGULAR3[request.type] ?? SINGULAR_TO_PLURAL2[request.type];
2145
2319
  if (alt) {
2146
2320
  const altWhere = { type: alt, state: "active", organization_id: oid };
2147
- if (packageId) altWhere._packageId = packageId;
2321
+ if (packageId) altWhere.package_id = packageId;
2148
2322
  rs = await this.engine.find("sys_metadata", { where: altWhere });
2149
2323
  }
2150
2324
  }
@@ -2167,6 +2341,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2167
2341
  for (const record of records) {
2168
2342
  const data = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2169
2343
  if (data && typeof data === "object" && "name" in data) {
2344
+ const recPkg = record.package_id ?? void 0;
2345
+ if (recPkg && data._packageId === void 0) {
2346
+ data._packageId = recPkg;
2347
+ }
2170
2348
  byName.set(data.name, data);
2171
2349
  }
2172
2350
  if (this.environmentId === void 0) {
@@ -2206,6 +2384,17 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2206
2384
  }
2207
2385
  } catch {
2208
2386
  }
2387
+ if (request.type !== "package" && request.type !== "object" && request.type !== "objects") {
2388
+ items = items.filter(
2389
+ (it) => !this.engine.registry.isPackageDisabled(it?._packageId)
2390
+ );
2391
+ }
2392
+ if (request.type === "view" || request.type === "views") {
2393
+ items = items.filter((it) => !isAggregatedViewContainer(it));
2394
+ }
2395
+ if (request.type === "app" || request.type === "apps") {
2396
+ items = items.map((app) => this.engine.registry.applyNavContributions(app));
2397
+ }
2209
2398
  return {
2210
2399
  type: request.type,
2211
2400
  items: decorateMetadataItems(
@@ -2249,6 +2438,10 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2249
2438
  const record = (orgId ? await findOverlay(orgId) : void 0) ?? await findOverlay(null);
2250
2439
  if (record) {
2251
2440
  item = typeof record.metadata === "string" ? JSON.parse(record.metadata) : record.metadata;
2441
+ const recPkg = record.package_id ?? void 0;
2442
+ if (recPkg && item && typeof item === "object" && item._packageId === void 0) {
2443
+ item._packageId = recPkg;
2444
+ }
2252
2445
  }
2253
2446
  } catch {
2254
2447
  }
@@ -2291,6 +2484,9 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
2291
2484
  if (alt) item = this.engine.registry.getItem(alt, request.name);
2292
2485
  }
2293
2486
  }
2487
+ if ((request.type === "app" || request.type === "apps") && item) {
2488
+ item = this.engine.registry.applyNavContributions(item);
2489
+ }
2294
2490
  const artifactItem = this.lookupArtifactItem(request.type, request.name);
2295
2491
  const decorated = decorateMetadataItem(
2296
2492
  request.type,
@@ -3049,7 +3245,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3049
3245
  throw new Error(`Metadata item ${request.type}/${request.name} not found`);
3050
3246
  }
3051
3247
  const content = JSON.stringify(item);
3052
- const hash = simpleHash(content);
3248
+ const hash = simpleHash(request.locale ? `${request.locale}\0${content}` : content);
3053
3249
  const etag = { value: hash, weak: false };
3054
3250
  if (request.cacheRequest?.ifNoneMatch) {
3055
3251
  const clientEtag = request.cacheRequest.ifNoneMatch.replace(/^"(.*)"$/, "$1").replace(/^W\/"(.*)"$/, "$1");
@@ -3695,7 +3891,8 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3695
3891
  actor: request.actor ?? "system",
3696
3892
  source: "protocol.saveMetaItem",
3697
3893
  intent,
3698
- state: mode === "draft" ? "draft" : "active"
3894
+ state: mode === "draft" ? "draft" : "active",
3895
+ ...request.packageId !== void 0 ? { packageId: request.packageId } : {}
3699
3896
  });
3700
3897
  if (mode === "publish") {
3701
3898
  this.applyObjectRegistryMutation(request);
@@ -3746,12 +3943,16 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3746
3943
  where: scopedWhere
3747
3944
  });
3748
3945
  if (existing) {
3749
- await this.engine.update("sys_metadata", {
3946
+ const updateRow = {
3750
3947
  metadata: JSON.stringify(request.item),
3751
3948
  updated_at: now,
3752
3949
  version: (existing.version || 0) + 1,
3753
3950
  state: "active"
3754
- }, {
3951
+ };
3952
+ const existingPkg = existing.package_id ?? null;
3953
+ const nextPkg = existingPkg ?? request.packageId ?? null;
3954
+ if (nextPkg !== null) updateRow.package_id = nextPkg;
3955
+ await this.engine.update("sys_metadata", updateRow, {
3755
3956
  where: { id: existing.id }
3756
3957
  });
3757
3958
  } else {
@@ -3771,6 +3972,7 @@ var _ObjectStackProtocolImplementation = class _ObjectStackProtocolImplementatio
3771
3972
  updated_at: now,
3772
3973
  organization_id: orgId
3773
3974
  };
3975
+ if (request.packageId) row.package_id = request.packageId;
3774
3976
  await this.engine.insert("sys_metadata", row);
3775
3977
  }
3776
3978
  return {
@@ -4495,8 +4697,33 @@ var ObjectStackProtocolImplementation = _ObjectStackProtocolImplementation;
4495
4697
  import { ExecutionContextSchema } from "@objectstack/spec/kernel";
4496
4698
  import { createLogger } from "@objectstack/core";
4497
4699
  import { CoreServiceName, StorageNameMapping } from "@objectstack/spec/system";
4498
- import { pluralToSingular } from "@objectstack/spec/shared";
4499
- import { ExpressionEngine as ExpressionEngine2 } from "@objectstack/formula";
4700
+
4701
+ // src/secret-fields.ts
4702
+ var SECRET_REF_PREFIX = "secret:";
4703
+ var SECRET_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
4704
+ function makeSecretRef(handleId) {
4705
+ return `${SECRET_REF_PREFIX}${handleId}`;
4706
+ }
4707
+ function isSecretRef(value) {
4708
+ return typeof value === "string" && value.startsWith(SECRET_REF_PREFIX);
4709
+ }
4710
+ function parseSecretRef(value) {
4711
+ return isSecretRef(value) ? value.slice(SECRET_REF_PREFIX.length) : null;
4712
+ }
4713
+ function collectSecretFields(schema) {
4714
+ const fields = schema?.fields;
4715
+ if (!fields) return [];
4716
+ const out = [];
4717
+ for (const [name, def] of Object.entries(fields)) {
4718
+ if (def && def.type === "secret") out.push(name);
4719
+ }
4720
+ return out;
4721
+ }
4722
+
4723
+ // src/engine.ts
4724
+ import { pluralToSingular, ExternalWriteForbiddenError } from "@objectstack/spec/shared";
4725
+ import { ExpressionEngine as ExpressionEngine3 } from "@objectstack/formula";
4726
+ import { isAggregatedViewContainer as isAggregatedViewContainer2, expandViewContainer } from "@objectstack/spec";
4500
4727
 
4501
4728
  // src/hook-wrappers.ts
4502
4729
  import { ExpressionEngine } from "@objectstack/formula";
@@ -5050,6 +5277,101 @@ function validateRecord(objectSchema, data, mode) {
5050
5277
  if (errors.length > 0) throw new ValidationError(errors);
5051
5278
  }
5052
5279
 
5280
+ // src/validation/rule-validator.ts
5281
+ import { ExpressionEngine as ExpressionEngine2 } from "@objectstack/formula";
5282
+ function needsPriorRecord(objectSchema) {
5283
+ const rules = objectSchema?.validations;
5284
+ if (!Array.isArray(rules)) return false;
5285
+ return rules.some(
5286
+ (r) => r != null && typeof r === "object" && (r.type === "state_machine" || r.type === "cross_field" || r.type === "script")
5287
+ );
5288
+ }
5289
+ function toExpression(cond) {
5290
+ return typeof cond === "string" ? { dialect: "cel", source: cond } : cond;
5291
+ }
5292
+ function evaluateValidationRules(objectSchema, data, mode, opts = {}) {
5293
+ const rules = objectSchema?.validations;
5294
+ if (!Array.isArray(rules) || rules.length === 0 || !data) return;
5295
+ const previous = opts.previous ?? void 0;
5296
+ const merged = { ...previous ?? {}, ...data };
5297
+ const errors = [];
5298
+ const ordered = rules.filter((r) => r != null && typeof r === "object").filter((r) => r.active !== false).filter((r) => {
5299
+ const events = r.events ?? ["insert", "update"];
5300
+ return events.includes(mode);
5301
+ }).sort((a, b) => (a.priority ?? 100) - (b.priority ?? 100));
5302
+ for (const rule of ordered) {
5303
+ let violation = null;
5304
+ try {
5305
+ if (rule.type === "state_machine") {
5306
+ violation = checkStateMachine(rule, mode, data, previous);
5307
+ } else if (rule.type === "script" || rule.type === "cross_field") {
5308
+ violation = checkPredicate(rule, merged, previous, opts.logger);
5309
+ }
5310
+ } catch (err) {
5311
+ opts.logger?.warn?.(`Validation rule '${rule.name}' threw \u2014 skipped`, err);
5312
+ continue;
5313
+ }
5314
+ if (!violation) continue;
5315
+ const severity = rule.severity ?? "error";
5316
+ if (severity === "error") {
5317
+ errors.push(violation);
5318
+ } else {
5319
+ opts.logger?.warn?.(
5320
+ `Validation rule '${rule.name}' (${severity}): ${violation.message}`
5321
+ );
5322
+ }
5323
+ }
5324
+ if (errors.length > 0) throw new ValidationError(errors);
5325
+ }
5326
+ function checkStateMachine(rule, mode, data, previous) {
5327
+ if (mode === "insert" || !previous) return null;
5328
+ if (!(rule.field in data)) return null;
5329
+ const from = previous[rule.field];
5330
+ const to = data[rule.field];
5331
+ if (from === to || to === void 0 || to === null) return null;
5332
+ const fromKey = String(from);
5333
+ const allowed = rule.transitions[fromKey];
5334
+ if (!Array.isArray(allowed)) return null;
5335
+ if (!allowed.includes(String(to))) {
5336
+ return {
5337
+ field: rule.field,
5338
+ code: "invalid_transition",
5339
+ message: rule.message || `Invalid transition for ${rule.field}: ${fromKey} \u2192 ${String(to)}`
5340
+ };
5341
+ }
5342
+ return null;
5343
+ }
5344
+ function checkPredicate(rule, record, previous, logger) {
5345
+ const expr = toExpression(rule.condition);
5346
+ const result = ExpressionEngine2.evaluate(expr, {
5347
+ record,
5348
+ previous: previous ?? void 0
5349
+ });
5350
+ if (!result.ok) {
5351
+ logger?.warn?.(
5352
+ `Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) \u2014 skipped`
5353
+ );
5354
+ return null;
5355
+ }
5356
+ if (result.value === true) {
5357
+ return {
5358
+ field: rule.fields?.[0] ?? "_record",
5359
+ code: "rule_violation",
5360
+ message: rule.message
5361
+ };
5362
+ }
5363
+ return null;
5364
+ }
5365
+ function legalNextStates(objectSchema, field, currentState) {
5366
+ const rules = objectSchema?.validations;
5367
+ if (!Array.isArray(rules)) return null;
5368
+ const rule = rules.find(
5369
+ (r) => r != null && typeof r === "object" && r.type === "state_machine" && r.field === field
5370
+ );
5371
+ if (!rule) return null;
5372
+ return rule.transitions[currentState] ?? [];
5373
+ }
5374
+
5053
5375
  // src/in-memory-aggregation.ts
5054
5376
  function applyInMemoryAggregation(rows, ast) {
5055
5377
  const groupBy = ast.groupBy ?? [];
@@ -5207,7 +5529,7 @@ function planFormulaProjection(schema, requestedFields) {
5207
5529
  if (def?.type === "formula" && def.expression) {
5208
5530
  const expr = typeof def.expression === "string" ? { dialect: "cel", source: def.expression } : def.expression;
5209
5531
  plan.push({ name: f, expression: expr });
5210
- ExpressionEngine2.compile(expr);
5532
+ ExpressionEngine3.compile(expr);
5211
5533
  } else if (Array.isArray(requestedFields) && requestedFields.length > 0) {
5212
5534
  projected.add(f);
5213
5535
  }
@@ -5229,7 +5551,7 @@ function applyFormulaPlan(plan, records) {
5229
5551
  for (const rec of records) {
5230
5552
  if (rec == null) continue;
5231
5553
  for (const fp of plan) {
5232
- const r = ExpressionEngine2.evaluate(fp.expression, { record: rec });
5554
+ const r = ExpressionEngine3.evaluate(fp.expression, { record: rec });
5233
5555
  rec[fp.name] = r.ok ? r.value : null;
5234
5556
  }
5235
5557
  }
@@ -5239,7 +5561,7 @@ function resolveMetadataItemName(key, item) {
5239
5561
  if (item.name) return item.name;
5240
5562
  if (item.id) return item.id;
5241
5563
  if (key === "views") {
5242
- return item?.list?.data?.object || item?.form?.data?.object || void 0;
5564
+ return item?.object || item?.list?.data?.object || item?.form?.data?.object || void 0;
5243
5565
  }
5244
5566
  return void 0;
5245
5567
  }
@@ -5251,6 +5573,11 @@ var _ObjectQL = class _ObjectQL {
5251
5573
  this.datasourceMapping = [];
5252
5574
  // Package manifests registry (for defaultDatasource lookup)
5253
5575
  this.manifests = /* @__PURE__ */ new Map();
5576
+ // Datasource definitions by name (ADR-0015): carries schemaMode +
5577
+ // external.allowWrites so the write gate (Gate 3) can enforce federation
5578
+ // ownership. Populated from manifests in registerApp and via
5579
+ // registerDatasourceDef. Absent entry ⇒ treated as managed (default DB).
5580
+ this.datasourceDefs = /* @__PURE__ */ new Map();
5254
5581
  // Per-object hooks with priority support
5255
5582
  this.hooks = /* @__PURE__ */ new Map([
5256
5583
  ["beforeFind", []],
@@ -5633,7 +5960,7 @@ var _ObjectQL = class _ObjectQL {
5633
5960
  if (f.defaultValue == null) continue;
5634
5961
  const dv = f.defaultValue;
5635
5962
  if (typeof dv === "object" && dv !== null && dv.dialect && typeof dv.source === "string") {
5636
- const result = ExpressionEngine2.evaluate(dv, {
5963
+ const result = ExpressionEngine3.evaluate(dv, {
5637
5964
  now,
5638
5965
  user: execCtx?.userId ? { id: String(execCtx.userId), role: execCtx?.roles?.[0] } : void 0,
5639
5966
  org: execCtx?.tenantId ? { id: String(execCtx.tenantId) } : void 0,
@@ -5673,6 +6000,12 @@ var _ObjectQL = class _ObjectQL {
5673
6000
  if (id) {
5674
6001
  this.manifests.set(id, manifest);
5675
6002
  }
6003
+ if (manifest.datasources) {
6004
+ const dsList = Array.isArray(manifest.datasources) ? manifest.datasources : Object.entries(manifest.datasources).map(([name, def]) => ({ name, ...def }));
6005
+ for (const ds of dsList) {
6006
+ if (ds?.name) this.registerDatasourceDef(ds);
6007
+ }
6008
+ }
5676
6009
  this._registry.installPackage(manifest);
5677
6010
  this.logger.debug("Installed Package", { id: manifest.id, name: manifest.name, namespace });
5678
6011
  if (manifest.objects) {
@@ -5726,6 +6059,15 @@ var _ObjectQL = class _ObjectQL {
5726
6059
  this._registry.registerApp(resolved, id);
5727
6060
  this.logger.debug("Registered manifest-as-app", { app: manifest.name, from: id });
5728
6061
  }
6062
+ if (Array.isArray(manifest.navigationContributions) && manifest.navigationContributions.length > 0) {
6063
+ for (const contribution of manifest.navigationContributions) {
6064
+ this._registry.registerAppNavContribution(contribution, id);
6065
+ }
6066
+ this.logger.debug("Registered navigation contributions", {
6067
+ from: id,
6068
+ count: manifest.navigationContributions.length
6069
+ });
6070
+ }
5729
6071
  const metadataArrayKeys = [
5730
6072
  // UI Protocol
5731
6073
  "actions",
@@ -5748,6 +6090,8 @@ var _ObjectQL = class _ObjectQL {
5748
6090
  "policies",
5749
6091
  // AI Protocol
5750
6092
  "agents",
6093
+ "tools",
6094
+ "skills",
5751
6095
  "ragPipelines",
5752
6096
  // API Protocol
5753
6097
  "apis",
@@ -5767,6 +6111,11 @@ var _ObjectQL = class _ObjectQL {
5767
6111
  if (itemName) {
5768
6112
  const toRegister = item.name === itemName ? item : { ...item, name: itemName };
5769
6113
  this._registry.registerItem(pluralToSingular(key), toRegister, "name", id);
6114
+ if (key === "views" && isAggregatedViewContainer2(toRegister)) {
6115
+ for (const vi of expandViewContainer(itemName, toRegister)) {
6116
+ this._registry.registerItem("view", vi, "name", id);
6117
+ }
6118
+ }
5770
6119
  } else {
5771
6120
  this.logger.warn(`Skipping ${pluralToSingular(key)} without a derivable name`, { id });
5772
6121
  }
@@ -5919,6 +6268,41 @@ var _ObjectQL = class _ObjectQL {
5919
6268
  this.logger.info("Set default driver", { driverName: driver.name });
5920
6269
  }
5921
6270
  }
6271
+ /**
6272
+ * Register a Datasource *definition* (ADR-0015).
6273
+ *
6274
+ * Distinct from {@link registerDriver}, which registers a live connection.
6275
+ * This captures the declarative `schemaMode` + `external.allowWrites` so the
6276
+ * write gate ({@link assertWriteAllowed}) can enforce external-datasource
6277
+ * ownership. Safe to call repeatedly; last write wins.
6278
+ */
6279
+ registerDatasourceDef(def) {
6280
+ if (!def?.name) return;
6281
+ this.datasourceDefs.set(def.name, { schemaMode: def.schemaMode, external: def.external });
6282
+ }
6283
+ /**
6284
+ * Write gate — Gate 3 of ADR-0015 §5.3.
6285
+ *
6286
+ * Blocks insert/update/delete against a federated datasource
6287
+ * (`schemaMode !== 'managed'`) unless BOTH the datasource opts in
6288
+ * (`external.allowWrites`) AND the object opts in (`external.writable`).
6289
+ * Managed datasources (the common case, including the absence of any
6290
+ * definition) are unaffected.
6291
+ */
6292
+ assertWriteAllowed(objectName, operation) {
6293
+ const object = this._registry.getObject(objectName);
6294
+ const dsName = object?.datasource;
6295
+ if (!dsName || dsName === "default") return;
6296
+ const ds = this.datasourceDefs.get(dsName);
6297
+ if (!ds || !ds.schemaMode || ds.schemaMode === "managed") return;
6298
+ const dsAllows = ds.external?.allowWrites ?? false;
6299
+ const objAllows = object?.external?.writable ?? false;
6300
+ if (!(dsAllows && objAllows)) {
6301
+ throw new ExternalWriteForbiddenError(
6302
+ `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}).`
6303
+ );
6304
+ }
6305
+ }
5922
6306
  /**
5923
6307
  * Set the realtime service for publishing data change events.
5924
6308
  * Should be called after kernel resolves the realtime service.
@@ -5929,6 +6313,141 @@ var _ObjectQL = class _ObjectQL {
5929
6313
  this.realtimeService = service;
5930
6314
  this.logger.info("RealtimeService configured for data events");
5931
6315
  }
6316
+ /**
6317
+ * Register the crypto provider that backs `secret`-typed fields.
6318
+ *
6319
+ * When set, the engine encrypts secret fields on write (storing ciphertext in
6320
+ * `sys_secret` and only an opaque ref on the business row) and masks them on
6321
+ * read. When NOT set, writing to an object that declares a secret field is
6322
+ * **fail-closed** — the write throws rather than persist cleartext.
6323
+ *
6324
+ * Mirrors the Settings subsystem's ICryptoProvider wiring; the host (e.g.
6325
+ * `serve`) injects `InMemoryCryptoProvider` in dev and a KMS/Vault-backed
6326
+ * provider in production.
6327
+ */
6328
+ setCryptoProvider(provider) {
6329
+ this.cryptoProvider = provider;
6330
+ this.logger.info("CryptoProvider configured for secret fields");
6331
+ }
6332
+ /**
6333
+ * Encrypt any `secret`-typed fields on `row` in place before it reaches the
6334
+ * driver. Each plaintext is wrapped by the ICryptoProvider, persisted as a
6335
+ * `sys_secret` row, and replaced on `row` by an opaque ref. Cleartext never
6336
+ * reaches the business table.
6337
+ *
6338
+ * Rules:
6339
+ * - No secret fields on the object ⇒ no-op (fast path, no crypto cost).
6340
+ * - `null`/`undefined` value ⇒ left as-is (clears the secret).
6341
+ * - Value already a ref (re-save of an unchanged ref) ⇒ left as-is.
6342
+ * - Value equal to the read mask ⇒ dropped, so a form round-trip that
6343
+ * echoes the mask does not overwrite the stored secret.
6344
+ * - **Fail-closed:** any other value with no CryptoProvider registered, or
6345
+ * no reachable `sys_secret` store, THROWS — never persists cleartext.
6346
+ */
6347
+ async encryptSecretFields(object, row, context, driverOptions) {
6348
+ if (!row || typeof row !== "object") return;
6349
+ const schema = this._registry.getObject(object);
6350
+ const secretFields = collectSecretFields(schema);
6351
+ if (secretFields.length === 0) return;
6352
+ for (const field of secretFields) {
6353
+ if (!(field in row)) continue;
6354
+ const value = row[field];
6355
+ if (value === null || typeof value === "undefined") continue;
6356
+ if (isSecretRef(value)) continue;
6357
+ if (value === SECRET_MASK) {
6358
+ delete row[field];
6359
+ continue;
6360
+ }
6361
+ if (!this.cryptoProvider) {
6362
+ throw new Error(
6363
+ `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).`
6364
+ );
6365
+ }
6366
+ const plain = typeof value === "string" ? value : JSON.stringify(value);
6367
+ const handle = await this.cryptoProvider.encrypt(plain, {
6368
+ namespace: object,
6369
+ key: field,
6370
+ tenantId: context?.tenantId
6371
+ });
6372
+ let secretDriver;
6373
+ try {
6374
+ secretDriver = this.getDriver("sys_secret");
6375
+ } catch {
6376
+ throw new Error(
6377
+ `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).`
6378
+ );
6379
+ }
6380
+ await secretDriver.create(
6381
+ "sys_secret",
6382
+ {
6383
+ id: handle.id,
6384
+ namespace: object,
6385
+ key: field,
6386
+ kms_key_id: handle.kmsKeyId,
6387
+ alg: handle.alg,
6388
+ version: handle.version,
6389
+ ciphertext: handle.ciphertext,
6390
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
6391
+ },
6392
+ driverOptions
6393
+ );
6394
+ row[field] = makeSecretRef(handle.id);
6395
+ }
6396
+ }
6397
+ /**
6398
+ * Mask `secret`-typed fields on read so plaintext never leaves the engine
6399
+ * through the normal query path. A set secret becomes {@link SECRET_MASK};
6400
+ * an unset one stays `null`. Privileged callers that genuinely need the
6401
+ * plaintext use {@link resolveSecret} against the stored ref.
6402
+ */
6403
+ maskSecretFields(object, rows) {
6404
+ if (!rows) return;
6405
+ const schema = this._registry.getObject(object);
6406
+ const secretFields = collectSecretFields(schema);
6407
+ if (secretFields.length === 0) return;
6408
+ const list = Array.isArray(rows) ? rows : [rows];
6409
+ for (const row of list) {
6410
+ if (!row || typeof row !== "object") continue;
6411
+ for (const field of secretFields) {
6412
+ if (!(field in row)) continue;
6413
+ row[field] = row[field] == null ? null : SECRET_MASK;
6414
+ }
6415
+ }
6416
+ }
6417
+ /**
6418
+ * Dereference a stored secret ref back to its plaintext. Intended for
6419
+ * privileged, server-side consumers (e.g. a datasource connection-pool
6420
+ * binder) — NOT exposed through the generic read path, which only ever
6421
+ * returns the mask.
6422
+ *
6423
+ * Fail-closed: throws when no CryptoProvider is registered or the
6424
+ * `sys_secret` row is missing. Returns `null` when `ref` is not a secret ref.
6425
+ */
6426
+ async resolveSecret(ref, opts) {
6427
+ const id = parseSecretRef(ref);
6428
+ if (!id) return null;
6429
+ if (!this.cryptoProvider) {
6430
+ throw new Error("Cannot resolve secret: no CryptoProvider is registered (fail-closed).");
6431
+ }
6432
+ const secretDriver = this.getDriver("sys_secret");
6433
+ const found = await secretDriver.find("sys_secret", { object: "sys_secret", where: { id } });
6434
+ const secret = Array.isArray(found) ? found[0] : found;
6435
+ if (!secret) {
6436
+ throw new Error(`Cannot resolve secret: sys_secret row "${id}" not found (fail-closed).`);
6437
+ }
6438
+ const handle = {
6439
+ id: secret.id,
6440
+ kmsKeyId: secret.kms_key_id,
6441
+ alg: secret.alg,
6442
+ version: secret.version,
6443
+ ciphertext: secret.ciphertext
6444
+ };
6445
+ return this.cryptoProvider.decrypt(handle, {
6446
+ namespace: secret.namespace,
6447
+ key: secret.key,
6448
+ tenantId: opts?.tenantId
6449
+ });
6450
+ }
5932
6451
  /**
5933
6452
  * Helper to get object definition
5934
6453
  */
@@ -6221,6 +6740,7 @@ var _ObjectQL = class _ObjectQL {
6221
6740
  hookContext.event = "afterFind";
6222
6741
  hookContext.result = result;
6223
6742
  await this.triggerHooks("afterFind", hookContext);
6743
+ this.maskSecretFields(object, hookContext.result);
6224
6744
  return hookContext.result;
6225
6745
  } catch (e) {
6226
6746
  this.logger.error("Find operation failed", e, { object });
@@ -6262,6 +6782,7 @@ var _ObjectQL = class _ObjectQL {
6262
6782
  const expanded = await this.expandRelatedRecords(objectName, [result], ast.expand, 0, opCtx.context);
6263
6783
  result = expanded[0];
6264
6784
  }
6785
+ this.maskSecretFields(objectName, result);
6265
6786
  return result;
6266
6787
  });
6267
6788
  return opCtx.result;
@@ -6269,6 +6790,7 @@ var _ObjectQL = class _ObjectQL {
6269
6790
  async insert(object, data, options) {
6270
6791
  object = this.resolveObjectName(object);
6271
6792
  this.logger.debug("Insert operation starting", { object, isBatch: Array.isArray(data) });
6793
+ this.assertWriteAllowed(object, "insert");
6272
6794
  const driver = this.getDriver(object);
6273
6795
  const opCtx = {
6274
6796
  object,
@@ -6297,7 +6819,13 @@ var _ObjectQL = class _ObjectQL {
6297
6819
  const rows = hookContext.input.data.map(
6298
6820
  (row) => this.applyFieldDefaults(object, row, opCtx.context, nowSnap)
6299
6821
  );
6300
- for (const r of rows) validateRecord(schemaForValidation, r, "insert");
6822
+ for (const r of rows) {
6823
+ await this.encryptSecretFields(object, r, opCtx.context, hookContext.input.options);
6824
+ }
6825
+ for (const r of rows) {
6826
+ validateRecord(schemaForValidation, r, "insert");
6827
+ evaluateValidationRules(schemaForValidation, r, "insert", { logger: this.logger });
6828
+ }
6301
6829
  if (driver.bulkCreate) {
6302
6830
  result = await driver.bulkCreate(object, rows, hookContext.input.options);
6303
6831
  } else {
@@ -6310,7 +6838,9 @@ var _ObjectQL = class _ObjectQL {
6310
6838
  opCtx.context,
6311
6839
  nowSnap
6312
6840
  );
6841
+ await this.encryptSecretFields(object, row, opCtx.context, hookContext.input.options);
6313
6842
  validateRecord(schemaForValidation, row, "insert");
6843
+ evaluateValidationRules(schemaForValidation, row, "insert", { logger: this.logger });
6314
6844
  result = await driver.create(object, row, hookContext.input.options);
6315
6845
  }
6316
6846
  hookContext.event = "afterInsert";
@@ -6360,6 +6890,7 @@ var _ObjectQL = class _ObjectQL {
6360
6890
  async update(object, data, options) {
6361
6891
  object = this.resolveObjectName(object);
6362
6892
  this.logger.debug("Update operation starting", { object });
6893
+ this.assertWriteAllowed(object, "update");
6363
6894
  const driver = this.getDriver(object);
6364
6895
  let id = data.id;
6365
6896
  if (!id && options?.where && typeof options.where === "object" && "id" in options.where) {
@@ -6386,11 +6917,23 @@ var _ObjectQL = class _ObjectQL {
6386
6917
  hookContext.input.options = this.buildDriverOptions(opCtx.context, hookContext.input.options);
6387
6918
  try {
6388
6919
  let result;
6920
+ let priorRecord = null;
6921
+ const updateSchema = this._registry.getObject(object);
6389
6922
  if (hookContext.input.id) {
6390
- validateRecord(this._registry.getObject(object), hookContext.input.data, "update");
6923
+ await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
6924
+ validateRecord(updateSchema, hookContext.input.data, "update");
6925
+ if (needsPriorRecord(updateSchema) || (this.hooks.get("afterUpdate")?.length ?? 0) > 0) {
6926
+ const priorAst = { object, where: { id: hookContext.input.id }, limit: 1 };
6927
+ priorRecord = await driver.findOne(object, priorAst, hookContext.input.options);
6928
+ }
6929
+ evaluateValidationRules(updateSchema, hookContext.input.data, "update", { previous: priorRecord, logger: this.logger });
6391
6930
  result = await driver.update(object, hookContext.input.id, hookContext.input.data, hookContext.input.options);
6392
6931
  } else if (options?.multi && driver.updateMany) {
6393
- validateRecord(this._registry.getObject(object), hookContext.input.data, "update");
6932
+ await this.encryptSecretFields(object, hookContext.input.data, opCtx.context, hookContext.input.options);
6933
+ validateRecord(updateSchema, hookContext.input.data, "update");
6934
+ if (needsPriorRecord(updateSchema)) {
6935
+ this.logger.warn("Object-level validation rules (state_machine/cross_field/script) are not enforced on multi-row updates", { object });
6936
+ }
6394
6937
  const ast = { object, where: options.where };
6395
6938
  result = await driver.updateMany(object, ast, hookContext.input.data, hookContext.input.options);
6396
6939
  } else {
@@ -6398,6 +6941,7 @@ var _ObjectQL = class _ObjectQL {
6398
6941
  }
6399
6942
  hookContext.event = "afterUpdate";
6400
6943
  hookContext.result = result;
6944
+ if (priorRecord) hookContext.previous = priorRecord;
6401
6945
  await this.triggerHooks("afterUpdate", hookContext);
6402
6946
  if (this.realtimeService) {
6403
6947
  try {
@@ -6430,6 +6974,7 @@ var _ObjectQL = class _ObjectQL {
6430
6974
  async delete(object, options) {
6431
6975
  object = this.resolveObjectName(object);
6432
6976
  this.logger.debug("Delete operation starting", { object });
6977
+ this.assertWriteAllowed(object, "delete");
6433
6978
  const driver = this.getDriver(object);
6434
6979
  let id = void 0;
6435
6980
  if (options?.where && typeof options.where === "object" && "id" in options.where) {
@@ -6689,6 +7234,26 @@ var _ObjectQL = class _ObjectQL {
6689
7234
  getDriverByName(name) {
6690
7235
  return this.drivers.get(name);
6691
7236
  }
7237
+ /**
7238
+ * Introspect a datasource's live remote schema (ADR-0015).
7239
+ *
7240
+ * Resolves the driver registered under `datasource` and delegates to its
7241
+ * `introspectSchema()` capability. Used by the external-datasource service
7242
+ * (and CLI/REST) to list remote tables and validate federated objects.
7243
+ *
7244
+ * @throws if the datasource has no registered driver, or the driver does
7245
+ * not support introspection.
7246
+ */
7247
+ async introspectDatasource(datasource) {
7248
+ const driver = this.drivers.get(datasource);
7249
+ if (!driver) {
7250
+ throw new Error(`[ObjectQL] Datasource '${datasource}' has no registered driver to introspect.`);
7251
+ }
7252
+ if (typeof driver.introspectSchema !== "function") {
7253
+ throw new Error(`[ObjectQL] Driver for datasource '${datasource}' does not support introspectSchema().`);
7254
+ }
7255
+ return driver.introspectSchema();
7256
+ }
6692
7257
  /**
6693
7258
  * Get the driver responsible for the given object.
6694
7259
  *
@@ -7622,7 +8187,7 @@ var ObjectQLPlugin = class {
7622
8187
  */
7623
8188
  async loadMetadataFromService(metadataService, ctx) {
7624
8189
  ctx.logger.info("Syncing metadata from external service into ObjectQL registry...");
7625
- const metadataTypes = ["object", "view", "app", "flow", "workflow", "function", "hook"];
8190
+ const metadataTypes = ["object", "view", "app", "flow", "function", "hook"];
7626
8191
  let totalLoaded = 0;
7627
8192
  for (const type of metadataTypes) {
7628
8193
  try {
@@ -7772,6 +8337,8 @@ export {
7772
8337
  ObjectRepository,
7773
8338
  ObjectStackProtocolImplementation,
7774
8339
  RESERVED_NAMESPACES,
8340
+ SECRET_MASK,
8341
+ SECRET_REF_PREFIX,
7775
8342
  SchemaRegistry,
7776
8343
  ScopedContext,
7777
8344
  SysMetadataRepository,
@@ -7780,11 +8347,18 @@ export {
7780
8347
  applySystemFields,
7781
8348
  bindHooksToEngine,
7782
8349
  bucketDateValue,
8350
+ collectSecretFields,
7783
8351
  computeFQN,
7784
8352
  convertIntrospectedSchemaToObjects,
7785
8353
  createObjectQLKernel,
8354
+ evaluateValidationRules,
8355
+ isSecretRef,
8356
+ legalNextStates,
8357
+ makeSecretRef,
8358
+ needsPriorRecord,
7786
8359
  noopHookMetricsRecorder,
7787
8360
  parseFQN,
8361
+ parseSecretRef,
7788
8362
  toTitleCase,
7789
8363
  validateRecord,
7790
8364
  wrapDeclarativeHook