@objectstack/core 17.0.0-rc.5 → 17.0.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.cjs CHANGED
@@ -35,13 +35,17 @@ __export(index_exports, {
35
35
  ANONYMOUS_DENY_MESSAGE: () => ANONYMOUS_DENY_MESSAGE,
36
36
  ANONYMOUS_DENY_STATUS: () => ANONYMOUS_DENY_STATUS,
37
37
  API_KEY_PREFIX: () => API_KEY_PREFIX,
38
+ AUDIENCE_BINDING_SUGGESTION_STATUSES: () => AUDIENCE_BINDING_SUGGESTION_STATUSES,
39
+ AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES: () => AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
38
40
  CORE_FALLBACK_FACTORIES: () => CORE_FALLBACK_FACTORIES,
39
41
  DependencyResolver: () => DependencyResolver,
42
+ ENTRY_EXECUTION_CONTEXT_FIELDS: () => ENTRY_EXECUTION_CONTEXT_FIELDS,
40
43
  HotReloadManager: () => HotReloadManager,
41
44
  LiteKernel: () => LiteKernel,
42
45
  MigrationJournalRefusal: () => MigrationJournalRefusal,
43
46
  MigrationPlanRegistry: () => MigrationPlanRegistry,
44
47
  NamespaceResolver: () => NamespaceResolver,
48
+ OPERATION_PRIVATE_KEY_PREFIX: () => OPERATION_PRIVATE_KEY_PREFIX,
45
49
  ObjectKernel: () => ObjectKernel,
46
50
  ObjectKernelBase: () => ObjectKernelBase,
47
51
  ObjectLogger: () => ObjectLogger,
@@ -63,12 +67,17 @@ __export(index_exports, {
63
67
  ServiceLifecycle: () => ServiceLifecycle,
64
68
  UnknownFilterTokenError: () => UnknownFilterTokenError,
65
69
  UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
70
+ assembleExecutionContext: () => assembleExecutionContext,
71
+ assembleExecutionContextOrGuest: () => assembleExecutionContextOrGuest,
66
72
  assertInitServiceRequirements: () => assertInitServiceRequirements,
73
+ assertMetadataRegisterContract: () => assertMetadataRegisterContract,
67
74
  bucketKeyToCalendarRange: () => bucketKeyToCalendarRange,
68
75
  buildPermissionsFromGrants: () => buildPermissionsFromGrants,
69
76
  bulkWrite: () => bulkWrite,
70
77
  calendarPartsInTz: () => calendarPartsInTz,
71
78
  calendarPartsInTzOrUtc: () => calendarPartsInTzOrUtc,
79
+ canonicalMetadataServiceType: () => canonicalMetadataServiceType,
80
+ collectInternalWriteResponseFields: () => collectInternalWriteResponseFields,
72
81
  counterSignPayload: () => counterSignPayload,
73
82
  createLogger: () => createLogger,
74
83
  createMemoryCache: () => createMemoryCache,
@@ -93,18 +102,22 @@ __export(index_exports, {
93
102
  getMemoryUsage: () => getMemoryUsage,
94
103
  hashApiKey: () => hashApiKey,
95
104
  hashMigrationPlan: () => hashMigrationPlan,
105
+ isAudienceBindingSuggestionStatus: () => isAudienceBindingSuggestionStatus,
96
106
  isAuthGateAllowlisted: () => isAuthGateAllowlisted,
97
107
  isExpired: () => isExpired,
98
108
  isGrantActive: () => isGrantActive,
99
109
  isGrantExpired: () => isGrantExpired,
100
110
  isNode: () => isNode,
101
111
  nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
112
+ normalizeAuthGate: () => normalizeAuthGate,
113
+ omitInternalFieldsFromWriteResponse: () => omitInternalFieldsFromWriteResponse,
102
114
  parseScopes: () => parseScopes,
103
115
  parseSignature: () => parseSignature,
104
116
  planChunks: () => planChunks,
105
117
  postureVisibleRows: () => postureVisibleRows,
106
118
  readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
107
119
  readRunJournal: () => readRunJournal,
120
+ recordNotFoundError: () => recordNotFoundError,
108
121
  resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
109
122
  resolveAuthzContext: () => resolveAuthzContext,
110
123
  resolveFilterToken: () => resolveFilterToken,
@@ -118,6 +131,7 @@ __export(index_exports, {
118
131
  safeExit: () => safeExit,
119
132
  shouldDenyAnonymous: () => shouldDenyAnonymous,
120
133
  signPayload: () => signPayload,
134
+ unknownAudienceBindingSuggestionStatusMessage: () => unknownAudienceBindingSuggestionStatusMessage,
121
135
  utcInstantMs: () => import_data.utcInstantMs,
122
136
  validateInitServiceContract: () => validateInitServiceContract,
123
137
  verifyPayload: () => verifyPayload,
@@ -126,6 +140,7 @@ __export(index_exports, {
126
140
  verifyPublisherSignature: () => verifyPublisherSignature,
127
141
  wireAuthoredTranslationSync: () => wireAuthoredTranslationSync,
128
142
  withTransientRetry: () => withTransientRetry,
143
+ withoutOperationPrivateKeys: () => withoutOperationPrivateKeys,
129
144
  zonedDateStartToUtcMs: () => zonedDateStartToUtcMs
130
145
  });
131
146
  module.exports = __toCommonJS(index_exports);
@@ -213,6 +228,30 @@ function assertInitServiceRequirements(plugin, isServiceRegistered) {
213
228
  }
214
229
  }
215
230
 
231
+ // src/hook-dispatch.ts
232
+ function traceDispatch(name, handlers, logger) {
233
+ logger.debug(`Triggering hook: ${name}`, {
234
+ hook: name,
235
+ handlerCount: handlers.length
236
+ });
237
+ }
238
+ async function dispatchHookIsolating(name, handlers, logger, args = []) {
239
+ traceDispatch(name, handlers, logger);
240
+ for (const handler of handlers) {
241
+ try {
242
+ await handler(...args);
243
+ } catch (error) {
244
+ logger.error(`Hook handler failed: ${name}`, error);
245
+ }
246
+ }
247
+ }
248
+ async function dispatchHookPropagating(name, handlers, logger, args = []) {
249
+ if (logger) traceDispatch(name, handlers, logger);
250
+ for (const handler of handlers) {
251
+ await handler(...args);
252
+ }
253
+ }
254
+
216
255
  // src/kernel-base.ts
217
256
  var ObjectKernelBase = class {
218
257
  constructor(logger) {
@@ -292,11 +331,11 @@ var ObjectKernelBase = class {
292
331
  }
293
332
  this.hooks.get(name).push(handler);
294
333
  },
334
+ // PROPAGATING dispatch, and deliberately WITHOUT the trace line the
335
+ // kernel's own dispatch sites emit — `context.trigger` has never
336
+ // logged one, so no logger is handed over (#5282).
295
337
  trigger: async (name, ...args) => {
296
- const handlers = this.hooks.get(name) || [];
297
- for (const handler of handlers) {
298
- await handler(...args);
299
- }
338
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], void 0, args);
300
339
  },
301
340
  getServices: () => {
302
341
  if (this.services instanceof Map) {
@@ -415,22 +454,16 @@ var ObjectKernelBase = class {
415
454
  * (`kernel:ready`, `kernel:bootstrapped`, `kernel:listening`) use
416
455
  * {@link triggerHookOrThrow} (#5170, #5257).
417
456
  *
457
+ * The loop itself lives in {@link dispatchHookIsolating} — one
458
+ * implementation shared with `ObjectKernel`'s own `kernel:shutdown`
459
+ * dispatch, which cannot inherit this method (`ObjectKernel` does not
460
+ * extend this class) and used to hand-mirror it (#5282).
461
+ *
418
462
  * @param name - Hook name
419
463
  * @param args - Arguments to pass to handlers
420
464
  */
421
465
  async triggerHook(name, ...args) {
422
- const handlers = this.hooks.get(name) || [];
423
- this.logger.debug(`Triggering hook: ${name}`, {
424
- hook: name,
425
- handlerCount: handlers.length
426
- });
427
- for (const handler of handlers) {
428
- try {
429
- await handler(...args);
430
- } catch (error) {
431
- this.logger.error(`Hook handler failed: ${name}`, error);
432
- }
433
- }
466
+ await dispatchHookIsolating(name, this.hooks.get(name) || [], this.logger, args);
434
467
  }
435
468
  /**
436
469
  * Trigger a hook with all registered handlers, PROPAGATING the first
@@ -467,18 +500,15 @@ var ObjectKernelBase = class {
467
500
  * default — and it is the reason this dispatcher is chosen per hook rather
468
501
  * than swapped in wholesale.
469
502
  *
503
+ * The loop itself lives in {@link dispatchHookPropagating} — the same
504
+ * function `PluginContext.trigger` runs on both kernels, so "propagating"
505
+ * means one thing repo-wide (#5282).
506
+ *
470
507
  * @param name - Hook name
471
508
  * @param args - Arguments to pass to handlers
472
509
  */
473
510
  async triggerHookOrThrow(name, ...args) {
474
- const handlers = this.hooks.get(name) || [];
475
- this.logger.debug(`Triggering hook: ${name}`, {
476
- hook: name,
477
- handlerCount: handlers.length
478
- });
479
- for (const handler of handlers) {
480
- await handler(...args);
481
- }
511
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], this.logger, args);
482
512
  }
483
513
  /**
484
514
  * Get current kernel state
@@ -614,7 +644,15 @@ var ObjectLogger = class _ObjectLogger {
614
644
  redact: config.redact ?? ["password", "token", "secret", "key"],
615
645
  sourceLocation: config.sourceLocation ?? false,
616
646
  file: config.file,
617
- rotation: config.rotation ?? { maxSize: "10m", maxFiles: 5 }
647
+ // Per-key, because `LoggerConfig` is the AUTHOR state (ADR-0122): the
648
+ // schema defaults `maxSize`/`maxFiles` *inside* `rotation`, so a caller
649
+ // may legitimately write `{ rotation: { maxSize: '5m' } }` and this
650
+ // constructor — which does not parse — has to fill the other half the
651
+ // same way `LoggerConfigSchema.parse` would.
652
+ rotation: {
653
+ maxSize: config.rotation?.maxSize ?? "10m",
654
+ maxFiles: config.rotation?.maxFiles ?? 5
655
+ }
618
656
  };
619
657
  this.bindings = bindings;
620
658
  this.redactPatterns = this.config.redact.map(tokenizeFieldName).filter((words) => words.length > 0);
@@ -808,7 +846,7 @@ function createLogger(config) {
808
846
  }
809
847
 
810
848
  // src/kernel.ts
811
- var import_system2 = require("@objectstack/spec/system");
849
+ var import_system3 = require("@objectstack/spec/system");
812
850
 
813
851
  // src/security/plugin-config-validator.ts
814
852
  var import_zod = require("zod");
@@ -1476,6 +1514,7 @@ function createMemoryJob() {
1476
1514
  }
1477
1515
 
1478
1516
  // src/fallbacks/memory-i18n.ts
1517
+ var import_system = require("@objectstack/spec/system");
1479
1518
  function deepMerge(target, source) {
1480
1519
  const result = { ...target };
1481
1520
  for (const key of Object.keys(source)) {
@@ -1509,6 +1548,7 @@ function createMemoryI18n() {
1509
1548
  const translations = /* @__PURE__ */ new Map();
1510
1549
  const authored = /* @__PURE__ */ new Map();
1511
1550
  let defaultLocale = "en";
1551
+ let supportedLocales;
1512
1552
  function resolveKey(data, key) {
1513
1553
  const parts = key.split(".");
1514
1554
  let current = data;
@@ -1574,9 +1614,29 @@ function createMemoryI18n() {
1574
1614
  authored.set(locale, { ...data });
1575
1615
  }
1576
1616
  },
1617
+ /**
1618
+ * Report the locales this stack offers.
1619
+ *
1620
+ * [#7679] When the app declared `i18n.supportedLocales`, that declaration
1621
+ * IS the answer — in declared order, and including a declared locale no
1622
+ * bundle was ever loaded for (declared-but-unserved). Reporting the
1623
+ * declaration rather than an intersection is what gives a client the
1624
+ * signal that the locale it is being offered has nothing behind it yet;
1625
+ * quietly dropping it would leave the gap invisible on both sides. It is
1626
+ * also the only answer that does not depend on how much had loaded by the
1627
+ * time this was called.
1628
+ *
1629
+ * With nothing declared, the loaded set — the behaviour every app that
1630
+ * never opted in already has.
1631
+ */
1577
1632
  getLocales() {
1633
+ if (supportedLocales) return [...supportedLocales];
1578
1634
  return [.../* @__PURE__ */ new Set([...translations.keys(), ...authored.keys()])];
1579
1635
  },
1636
+ /** @see II18nService.setSupportedLocales — [#7679] */
1637
+ setSupportedLocales(locales) {
1638
+ supportedLocales = (0, import_system.normalizeSupportedLocales)(locales);
1639
+ },
1580
1640
  getDefaultLocale() {
1581
1641
  return defaultLocale;
1582
1642
  },
@@ -1586,14 +1646,42 @@ function createMemoryI18n() {
1586
1646
  };
1587
1647
  }
1588
1648
 
1649
+ // src/metadata-service-contract.ts
1650
+ var import_shared = require("@objectstack/spec/shared");
1651
+ var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
1652
+ function canonicalMetadataServiceType(type) {
1653
+ return (0, import_shared.pluralToSingular)(type);
1654
+ }
1655
+ function registerRefusal(message) {
1656
+ const err = new Error(message);
1657
+ err.code = REGISTER_REFUSAL_CODE;
1658
+ err.status = 400;
1659
+ return err;
1660
+ }
1661
+ function assertMetadataRegisterContract(type, name, data) {
1662
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1663
+ const shape = data === null ? "null" : Array.isArray(data) ? "an array" : `a ${typeof data}`;
1664
+ throw registerRefusal(
1665
+ `IMetadataService.register('${type}', '${name}'): data is ${shape}, not a metadata document. register() stores plain-object documents only \u2014 accepting a value the service cannot key was measured as accept-then-drop on document-keyed stores (#7378 row 3: refuse loudly, never coerce into storability). Wrap the value in a document object whose shape the '${type}' type's schema accepts, or store it under a type that declares one.`
1666
+ );
1667
+ }
1668
+ const documentName = data.name;
1669
+ if (documentName !== void 0 && documentName !== name) {
1670
+ throw registerRefusal(
1671
+ `IMetadataService.register('${type}', '${name}'): data.name is '${String(documentName)}', which disagrees with the name argument '${name}'. A disagreement is almost always an authoring bug, and resolving it silently in either direction can file the item under a key the caller never wrote (#7378 row 1: refuse loudly, locate the mismatch). Register under one name: pass the intended key as the argument and make data.name match it, or omit data.name.`
1672
+ );
1673
+ }
1674
+ }
1675
+
1589
1676
  // src/fallbacks/memory-metadata.ts
1590
1677
  function createMemoryMetadata() {
1591
1678
  const store = /* @__PURE__ */ new Map();
1592
1679
  function getTypeMap(type) {
1593
- let map = store.get(type);
1680
+ const canonical = canonicalMetadataServiceType(type);
1681
+ let map = store.get(canonical);
1594
1682
  if (!map) {
1595
1683
  map = /* @__PURE__ */ new Map();
1596
- store.set(type, map);
1684
+ store.set(canonical, map);
1597
1685
  }
1598
1686
  return map;
1599
1687
  }
@@ -1608,6 +1696,7 @@ function createMemoryMetadata() {
1608
1696
  },
1609
1697
  _serviceName: "metadata",
1610
1698
  async register(type, name, data) {
1699
+ assertMetadataRegisterContract(type, name, data);
1611
1700
  getTypeMap(type).set(name, data);
1612
1701
  },
1613
1702
  // Mirror MetadataManager.registerInMemory (synchronous, no persistence).
@@ -1619,7 +1708,11 @@ function createMemoryMetadata() {
1619
1708
  // so `defineStack({ datasources })` entries silently never reached the
1620
1709
  // registry and were absent from GET /api/v1/datasources and
1621
1710
  // GET /api/v1/meta/datasource (ADR-0015 §18). This store is already
1622
- // in-memory only, so registerInMemory and register share an implementation.
1711
+ // in-memory only, so registerInMemory and register share a store — but
1712
+ // NOT the [#7378] refusals: the ruling names `register`, and this member
1713
+ // is a boot-time seeding primitive for source-control-owned artefacts
1714
+ // (see assertMetadataRegisterContract's header for the boundary). It does
1715
+ // share the row-2 canonical type fold, via getTypeMap.
1623
1716
  registerInMemory(type, name, data) {
1624
1717
  getTypeMap(type).set(name, data);
1625
1718
  },
@@ -1648,7 +1741,7 @@ function createMemoryMetadata() {
1648
1741
  }
1649
1742
 
1650
1743
  // src/fallbacks/authored-translation-sync.ts
1651
- var import_system = require("@objectstack/spec/system");
1744
+ var import_system2 = require("@objectstack/spec/system");
1652
1745
  var OWNER_PROP = "__authoredTranslationSyncOwner";
1653
1746
  var LOCALE_LIKE = /^[a-z]{2,3}([_-]([A-Za-z]{4}|[A-Za-z]{2}|[0-9]{3}))?$/;
1654
1747
  async function readAuthoredTranslationLayer(engine, logger) {
@@ -1678,7 +1771,7 @@ async function readAuthoredTranslationLayer(engine, logger) {
1678
1771
  continue;
1679
1772
  }
1680
1773
  if (!data || typeof data !== "object") continue;
1681
- const legacyKeys = import_system.LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1774
+ const legacyKeys = import_system2.LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
1682
1775
  if (legacyKeys.length > 0) {
1683
1776
  logger?.warn?.(
1684
1777
  `[i18n] authored translation '${row?.name}' uses the retired object-first shape (${legacyKeys.join(", ")}) \u2014 nothing resolves from it; re-author it under 'objects.<object_name>' with a top-level 'locale' \u2014 skipped`
@@ -1843,11 +1936,12 @@ var ObjectKernel = class {
1843
1936
  }
1844
1937
  this.hooks.get(name).push(handler);
1845
1938
  },
1939
+ // PROPAGATING dispatch — the same shared loop `LiteKernel`'s
1940
+ // context.trigger runs, and deliberately WITHOUT a trace line:
1941
+ // `context.trigger` has never emitted one on either kernel, so no
1942
+ // logger is handed over (#5282).
1846
1943
  trigger: async (name, ...args) => {
1847
- const handlers = this.hooks.get(name) || [];
1848
- for (const handler of handlers) {
1849
- await handler(...args);
1850
- }
1944
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], void 0, args);
1851
1945
  },
1852
1946
  getServices: () => {
1853
1947
  return new Map(this.services);
@@ -1915,7 +2009,7 @@ var ObjectKernel = class {
1915
2009
  */
1916
2010
  preInjectCoreFallbacks() {
1917
2011
  if (this.config.skipSystemValidation) return;
1918
- for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
2012
+ for (const [serviceName, criticality] of Object.entries(import_system3.ServiceRequirementDef)) {
1919
2013
  if (criticality !== "core") continue;
1920
2014
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1921
2015
  if (!hasService) {
@@ -1939,7 +2033,7 @@ var ObjectKernel = class {
1939
2033
  this.logger.debug("Validating system service requirements...");
1940
2034
  const missingServices = [];
1941
2035
  const missingCoreServices = [];
1942
- for (const [serviceName, criticality] of Object.entries(import_system2.ServiceRequirementDef)) {
2036
+ for (const [serviceName, criticality] of Object.entries(import_system3.ServiceRequirementDef)) {
1943
2037
  const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
1944
2038
  if (!hasService) {
1945
2039
  if (criticality === "required") {
@@ -2271,27 +2365,21 @@ var ObjectKernel = class {
2271
2365
  * one bad handler must not amplify into leaked resources and unflushed
2272
2366
  * writes. Same reasoning, same wording, same `Hook handler failed:
2273
2367
  * kernel:shutdown` log line as `LiteKernel`'s dispatch site, which reaches
2274
- * the shared isolating dispatcher `ObjectKernelBase.triggerHook` (#5257).
2368
+ * the isolating dispatcher through `ObjectKernelBase.triggerHook` (#5257).
2275
2369
  *
2276
- * `ObjectKernel` cannot call that dispatcher: it does not extend
2370
+ * Until #5282 "same wording" was literally that the loop was typed out a
2371
+ * second time here, because `ObjectKernel` does not extend
2277
2372
  * `ObjectKernelBase` (only `LiteKernel` does) and owns its own `hooks` map,
2278
- * so the semantics are mirrored here rather than shared. One hook name
2279
- * meaning two opposite things across the two kernels is exactly the bug
2280
- * #5170/#5257 closed, so the pin for this one lives on both sides too.
2373
+ * so the base's `protected triggerHook` is out of reach. The loop now lives
2374
+ * in {@link dispatchHookIsolating}, which BOTH sides call: the storage is
2375
+ * still two maps (deliberately unifying it was out of #5282's scope), but
2376
+ * "isolating" is one implementation, so it can no longer drift on one
2377
+ * kernel while the other keeps the old shape. That drift is exactly the bug
2378
+ * #5170 / #5257 / #5274 each closed one hook at a time, and the paired-pin
2379
+ * gate (`scripts/check-kernel-hook-pairs.mjs`) covers the residue.
2281
2380
  */
2282
2381
  async triggerShutdownHookIsolating() {
2283
- const handlers = this.hooks.get("kernel:shutdown") || [];
2284
- this.logger.debug("Triggering hook: kernel:shutdown", {
2285
- hook: "kernel:shutdown",
2286
- handlerCount: handlers.length
2287
- });
2288
- for (const handler of handlers) {
2289
- try {
2290
- await handler();
2291
- } catch (error) {
2292
- this.logger.error("Hook handler failed: kernel:shutdown", error);
2293
- }
2294
- }
2382
+ await dispatchHookIsolating("kernel:shutdown", this.hooks.get("kernel:shutdown") || [], this.logger);
2295
2383
  }
2296
2384
  async performShutdown() {
2297
2385
  await this.triggerShutdownHookIsolating();
@@ -2459,6 +2547,20 @@ __export(qa_exports, {
2459
2547
  });
2460
2548
 
2461
2549
  // src/qa/runner.ts
2550
+ function describeActualType(value) {
2551
+ if (value === null) return "null";
2552
+ if (Array.isArray(value)) return "array";
2553
+ return typeof value;
2554
+ }
2555
+ function containsInapplicableHint(actual) {
2556
+ if (actual === void 0) {
2557
+ return "The path resolved to nothing \u2014 the field is absent from the result, or the path is misspelled. Use 'is_null' if asserting absence is what you meant.";
2558
+ }
2559
+ if (actual === null) {
2560
+ return "The path resolved to null. Use 'is_null' if asserting absence is what you meant.";
2561
+ }
2562
+ return "'contains' tests array membership and string substrings only. Use 'equals' to compare a scalar, or point the field at the array or string you meant to look inside.";
2563
+ }
2462
2564
  var TestRunner = class {
2463
2565
  constructor(adapter) {
2464
2566
  this.adapter = adapter;
@@ -2586,6 +2688,10 @@ var TestRunner = class {
2586
2688
  if (!actual.includes(expected)) throw new Error(`Assertion failed: ${assertion.field} array does not contain ${expected}`);
2587
2689
  } else if (typeof actual === "string") {
2588
2690
  if (!actual.includes(String(expected))) throw new Error(`Assertion failed: ${assertion.field} string does not contain ${expected}`);
2691
+ } else {
2692
+ throw new Error(
2693
+ `Assertion failed: ${assertion.field} cannot be evaluated by 'contains' \u2014 expected an array or a string at that path, got ${describeActualType(actual)}. ` + containsInapplicableHint(actual)
2694
+ );
2589
2695
  }
2590
2696
  break;
2591
2697
  case "not_null":
@@ -2602,11 +2708,29 @@ var TestRunner = class {
2602
2708
  };
2603
2709
 
2604
2710
  // src/qa/http-adapter.ts
2711
+ var import_api = require("@objectstack/spec/api");
2712
+ var dataPathCache;
2713
+ function defaultDataPath() {
2714
+ if (dataPathCache === void 0) {
2715
+ const api = import_api.RestApiConfigSchema.parse({});
2716
+ const crud = import_api.CrudEndpointsConfigSchema.parse({});
2717
+ dataPathCache = `${api.apiPath ?? `${api.basePath}/${api.version}`}${crud.dataPrefix}`;
2718
+ }
2719
+ return dataPathCache;
2720
+ }
2605
2721
  var HttpTestAdapter = class {
2606
2722
  constructor(baseUrl, authToken) {
2607
2723
  this.baseUrl = baseUrl;
2608
2724
  this.authToken = authToken;
2609
2725
  }
2726
+ /** `{baseUrl}{apiBasePath}{dataPrefix}/{object}` — the collection URL. */
2727
+ collectionUrl(objectName) {
2728
+ return `${this.baseUrl}${defaultDataPath()}/${encodeURIComponent(objectName)}`;
2729
+ }
2730
+ /** `{collection}/{id}` — the single-record URL. */
2731
+ recordUrl(objectName, id) {
2732
+ return `${this.collectionUrl(objectName)}/${encodeURIComponent(String(id))}`;
2733
+ }
2610
2734
  async execute(action, _context) {
2611
2735
  const headers = {
2612
2736
  "Content-Type": "application/json"
@@ -2638,7 +2762,7 @@ var HttpTestAdapter = class {
2638
2762
  }
2639
2763
  }
2640
2764
  async createRecord(objectName, data, headers) {
2641
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}`, {
2765
+ const response = await fetch(this.collectionUrl(objectName), {
2642
2766
  method: "POST",
2643
2767
  headers,
2644
2768
  body: JSON.stringify(data)
@@ -2646,19 +2770,19 @@ var HttpTestAdapter = class {
2646
2770
  return this.handleResponse(response);
2647
2771
  }
2648
2772
  async updateRecord(objectName, data, headers) {
2649
- const id = data.id;
2773
+ const { id, ...fields } = data;
2650
2774
  if (!id) throw new Error("Update record requires id in payload");
2651
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2652
- method: "PUT",
2775
+ const response = await fetch(this.recordUrl(objectName, id), {
2776
+ method: "PATCH",
2653
2777
  headers,
2654
- body: JSON.stringify(data)
2778
+ body: JSON.stringify(fields)
2655
2779
  });
2656
2780
  return this.handleResponse(response);
2657
2781
  }
2658
2782
  async deleteRecord(objectName, data, headers) {
2659
2783
  const id = data.id;
2660
2784
  if (!id) throw new Error("Delete record requires id in payload");
2661
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2785
+ const response = await fetch(this.recordUrl(objectName, id), {
2662
2786
  method: "DELETE",
2663
2787
  headers
2664
2788
  });
@@ -2667,14 +2791,14 @@ var HttpTestAdapter = class {
2667
2791
  async readRecord(objectName, data, headers) {
2668
2792
  const id = data.id;
2669
2793
  if (!id) throw new Error("Read record requires id in payload");
2670
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2794
+ const response = await fetch(this.recordUrl(objectName, id), {
2671
2795
  method: "GET",
2672
2796
  headers
2673
2797
  });
2674
2798
  return this.handleResponse(response);
2675
2799
  }
2676
2800
  async queryRecords(objectName, data, headers) {
2677
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/query`, {
2801
+ const response = await fetch(`${this.collectionUrl(objectName)}/query`, {
2678
2802
  method: "POST",
2679
2803
  headers,
2680
2804
  body: JSON.stringify(data)
@@ -4466,7 +4590,116 @@ async function resolveLocalizationContext(input) {
4466
4590
  };
4467
4591
  }
4468
4592
 
4593
+ // src/security/assemble-execution-context.ts
4594
+ var ENTRY_EXECUTION_CONTEXT_FIELDS = [
4595
+ "positions",
4596
+ "permissions",
4597
+ "systemPermissions",
4598
+ "isSystem",
4599
+ "principalKind",
4600
+ "onBehalfOf",
4601
+ "audience",
4602
+ "userId",
4603
+ "tenantId",
4604
+ "email",
4605
+ "accessToken",
4606
+ "tabPermissions",
4607
+ "posture",
4608
+ "authGate",
4609
+ "org_user_ids",
4610
+ "accessible_org_ids",
4611
+ "oauthScopes",
4612
+ "timezone",
4613
+ "locale",
4614
+ "currency"
4615
+ ];
4616
+ function emit(fields) {
4617
+ const ctx = {};
4618
+ for (const key of ENTRY_EXECUTION_CONTEXT_FIELDS) {
4619
+ const value = fields[key];
4620
+ if (value !== void 0) ctx[key] = value;
4621
+ }
4622
+ return ctx;
4623
+ }
4624
+ function entryFields(input, anonymous) {
4625
+ const { authz, oauth, localization, requestLocale, accessToken, authGate } = input;
4626
+ const agent = !anonymous && oauth?.clientId ? oauth : void 0;
4627
+ return {
4628
+ // [ADR-0090 D9/D10] Principal taxonomy at the HTTP entry: a session-backed
4629
+ // request is a human principal; a sessionless one is a guest, holding the
4630
+ // built-in `guest` position implicitly and exclusively. Internal engine
4631
+ // calls that construct bare contexts never pass through here, so the
4632
+ // security plugin's empty-context skip path keeps its meaning.
4633
+ positions: agent ? [] : anonymous ? ["guest"] : authz.positions,
4634
+ permissions: agent ? agent.scopePermissions : authz.permissions,
4635
+ // [ADR-0090 D10] System capabilities on the agent principal gate business
4636
+ // ACTION invocation (`actionPermissionError` reads `ctx.systemPermissions`)
4637
+ // — a door SEPARATE from the object CRUD/FLS/RLS intersection, which is
4638
+ // driven by the resolved ceiling SETS (they carry no caps, so cap-gated
4639
+ // OBJECT access stays denied to the agent regardless of this line). The
4640
+ // `actions:execute` scope IS the user's consent to let this agent invoke
4641
+ // actions on their behalf; without it the agent holds none.
4642
+ systemPermissions: agent ? agent.delegatesActions ? authz.systemPermissions ?? [] : [] : authz.systemPermissions,
4643
+ isSystem: false,
4644
+ principalKind: agent ? "agent" : anonymous ? "guest" : "human",
4645
+ onBehalfOf: agent ? { userId: authz.userId, principalKind: "human" } : void 0,
4646
+ // [ADR-0090 D10/D11 — P1 shape] No transport resolves an external
4647
+ // (portal/partner) audience yet; `undefined` reads as 'internal'. Named
4648
+ // here rather than excluded so the gap is visible in the closed set instead
4649
+ // of being invisible outside it — when an external principal type lands,
4650
+ // this is the line that must change, on every face at once.
4651
+ audience: void 0,
4652
+ userId: authz.userId,
4653
+ tenantId: authz.tenantId,
4654
+ email: authz.email,
4655
+ accessToken,
4656
+ tabPermissions: authz.tabPermissions,
4657
+ // [ADR-0095 D2 / #2947] The derived posture rung, carried so every
4658
+ // transport presents enforcement the SAME value. Present only for an
4659
+ // authenticated principal (guest → absent).
4660
+ posture: authz.posture,
4661
+ // [ADR-0069 / #7280] The AUTHENTICATION-policy gate, carried for the seam
4662
+ // that reads it off the envelope (REST's `enforceAuth`). Anonymous → never:
4663
+ // a guest has no authenticated session for a policy gate to attach to, so
4664
+ // "gated guest" is not a state this entry can emit even if a face passed
4665
+ // one.
4666
+ authGate: anonymous ? void 0 : authGate,
4667
+ /** Fellow-org user IDs for RLS scoping of identity tables. */
4668
+ org_user_ids: authz.org_user_ids,
4669
+ // [ADR-0105 D2] The caller's org access set — the `group` posture's Layer 0
4670
+ // wall reads it directly, so every transport must carry it (#6206).
4671
+ accessible_org_ids: authz.accessible_org_ids,
4672
+ // OAuth provenance: surface the token's granted scopes so the MCP
4673
+ // dispatcher can narrow the exposed tool families (undefined for every
4674
+ // other provenance = not scope-limited).
4675
+ oauthScopes: oauth && authz.userId === oauth.userId ? oauth.scopes : void 0,
4676
+ // Anonymous → no localization (no scope to resolve against); the engine
4677
+ // default stands. [#3957] The request's OWN language preference wins over
4678
+ // the workspace default, so a rejection message is not rendered in English
4679
+ // beside the Chinese label of the very field it names.
4680
+ timezone: anonymous ? void 0 : localization?.timezone,
4681
+ locale: anonymous ? void 0 : requestLocale ?? localization?.locale,
4682
+ currency: anonymous ? void 0 : localization?.currency
4683
+ };
4684
+ }
4685
+ function assembleExecutionContext(input) {
4686
+ if (!input.authz.userId) return void 0;
4687
+ return emit(entryFields(input, false));
4688
+ }
4689
+ function assembleExecutionContextOrGuest(input) {
4690
+ return emit(entryFields(input, !input.authz.userId));
4691
+ }
4692
+
4469
4693
  // src/security/auth-gate.ts
4694
+ var DEFAULT_AUTH_GATE_MESSAGE = "Access is blocked by an authentication policy.";
4695
+ function normalizeAuthGate(sessionUser) {
4696
+ const gate = sessionUser?.authGate;
4697
+ if (!gate || typeof gate.code !== "string") return null;
4698
+ return {
4699
+ code: gate.code,
4700
+ message: typeof gate.message === "string" && gate.message ? gate.message : DEFAULT_AUTH_GATE_MESSAGE
4701
+ };
4702
+ }
4470
4703
  var ALLOW_PREFIXES = ["/api/v1/auth/", "/api/auth/", "/auth/"];
4471
4704
  var ALLOW_SUFFIXES = ["/health", "/ready", "/discovery", "/me/apps", "/me/localization"];
4472
4705
  function isAuthGateAllowlisted(rawPath) {
@@ -4485,13 +4718,10 @@ function isAuthGateAllowlisted(rawPath) {
4485
4718
  return false;
4486
4719
  }
4487
4720
  function evaluateAuthGate(sessionUser, path) {
4488
- const gate = sessionUser?.authGate;
4489
- if (!gate || typeof gate.code !== "string") return null;
4721
+ const gate = normalizeAuthGate(sessionUser);
4722
+ if (!gate) return null;
4490
4723
  if (isAuthGateAllowlisted(path)) return null;
4491
- return {
4492
- code: gate.code,
4493
- message: typeof gate.message === "string" && gate.message ? gate.message : "Access is blocked by an authentication policy."
4494
- };
4724
+ return gate;
4495
4725
  }
4496
4726
 
4497
4727
  // src/security/anonymous-deny.ts
@@ -4513,6 +4743,29 @@ function shouldDenyAnonymous(input) {
4513
4743
  return true;
4514
4744
  }
4515
4745
 
4746
+ // src/security/audience-binding-suggestion-status.ts
4747
+ var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
4748
+ pending: true,
4749
+ confirmed: true,
4750
+ dismissed: true
4751
+ };
4752
+ var AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES = Object.keys(
4753
+ AUDIENCE_BINDING_SUGGESTION_STATUSES
4754
+ );
4755
+ var isAudienceBindingSuggestionStatus = (value) => Object.prototype.hasOwnProperty.call(AUDIENCE_BINDING_SUGGESTION_STATUSES, value);
4756
+ var unknownAudienceBindingSuggestionStatusMessage = (value) => `Unknown status filter '${value}' \u2014 expected one of: ${AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES.join(", ")}`;
4757
+
4758
+ // src/security/operation-private-keys.ts
4759
+ var OPERATION_PRIVATE_KEY_PREFIX = "__";
4760
+ function withoutOperationPrivateKeys(exec) {
4761
+ const out = {};
4762
+ for (const [key, value] of Object.entries(exec)) {
4763
+ if (key.startsWith(OPERATION_PRIVATE_KEY_PREFIX)) continue;
4764
+ out[key] = value;
4765
+ }
4766
+ return out;
4767
+ }
4768
+
4516
4769
  // src/utils/datetime.ts
4517
4770
  var import_data = require("@objectstack/spec/data");
4518
4771
  function calendarPartsInTz(d, tz) {
@@ -4755,9 +5008,30 @@ async function bulkWrite(rows, opts) {
4755
5008
  return results;
4756
5009
  }
4757
5010
 
5011
+ // src/utils/internal-write-response.ts
5012
+ function collectInternalWriteResponseFields(schema) {
5013
+ const fields = schema?.fields;
5014
+ if (!fields || typeof fields !== "object") return [];
5015
+ const out = [];
5016
+ for (const [name, def] of Object.entries(fields)) {
5017
+ if (def && def.internal === true) out.push(name);
5018
+ }
5019
+ return out;
5020
+ }
5021
+ function omitInternalFieldsFromWriteResponse(schema, records) {
5022
+ if (!records) return;
5023
+ const internalFields = collectInternalWriteResponseFields(schema);
5024
+ if (internalFields.length === 0) return;
5025
+ const list = Array.isArray(records) ? records : [records];
5026
+ for (const row of list) {
5027
+ if (!row || typeof row !== "object") continue;
5028
+ for (const field of internalFields) delete row[field];
5029
+ }
5030
+ }
5031
+
4758
5032
  // src/utils/migration-journal.ts
4759
5033
  var import_node_crypto3 = require("crypto");
4760
- var import_system3 = require("@objectstack/spec/system");
5034
+ var import_system4 = require("@objectstack/spec/system");
4761
5035
  var SYSTEM_CTX = { isSystem: true };
4762
5036
  var DEFAULT_CHUNK_SIZE = 200;
4763
5037
  function engineCanRollBack(engine) {
@@ -4815,14 +5089,14 @@ function hashMigrationPlan(plan, chunks) {
4815
5089
  }
4816
5090
  async function appendEvent(engine, event, execContext) {
4817
5091
  await engine.insert(
4818
- import_system3.MIGRATION_JOURNAL_OBJECT,
5092
+ import_system4.MIGRATION_JOURNAL_OBJECT,
4819
5093
  { ...event, created_at: event.created_at ?? (/* @__PURE__ */ new Date()).toISOString() },
4820
5094
  { context: execContext ?? { ...SYSTEM_CTX } }
4821
5095
  );
4822
5096
  }
4823
5097
  async function readRunJournal(engine, runId) {
4824
5098
  const rows = await engine.find(
4825
- import_system3.MIGRATION_JOURNAL_OBJECT,
5099
+ import_system4.MIGRATION_JOURNAL_OBJECT,
4826
5100
  { where: { run_id: runId } },
4827
5101
  { context: { ...SYSTEM_CTX } }
4828
5102
  );
@@ -4837,7 +5111,7 @@ function chunkSetOf(events, kind) {
4837
5111
  }
4838
5112
  async function findInterruptedRuns(engine) {
4839
5113
  const started = await engine.find(
4840
- import_system3.MIGRATION_JOURNAL_OBJECT,
5114
+ import_system4.MIGRATION_JOURNAL_OBJECT,
4841
5115
  { where: { kind: "run_started" } },
4842
5116
  { context: { ...SYSTEM_CTX } }
4843
5117
  );
@@ -5345,6 +5619,15 @@ function filterTokenContextFrom(execCtx, now) {
5345
5619
  };
5346
5620
  }
5347
5621
 
5622
+ // src/utils/record-not-found.ts
5623
+ function recordNotFoundError(object, id) {
5624
+ const err = new Error(`Record ${id} not found in ${object}`);
5625
+ err.code = "RECORD_NOT_FOUND";
5626
+ err.status = 404;
5627
+ err.object = object;
5628
+ return err;
5629
+ }
5630
+
5348
5631
  // src/health-monitor.ts
5349
5632
  var PluginHealthMonitor = class {
5350
5633
  constructor(logger) {
@@ -6328,13 +6611,17 @@ var NamespaceResolver = class {
6328
6611
  ANONYMOUS_DENY_MESSAGE,
6329
6612
  ANONYMOUS_DENY_STATUS,
6330
6613
  API_KEY_PREFIX,
6614
+ AUDIENCE_BINDING_SUGGESTION_STATUSES,
6615
+ AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
6331
6616
  CORE_FALLBACK_FACTORIES,
6332
6617
  DependencyResolver,
6618
+ ENTRY_EXECUTION_CONTEXT_FIELDS,
6333
6619
  HotReloadManager,
6334
6620
  LiteKernel,
6335
6621
  MigrationJournalRefusal,
6336
6622
  MigrationPlanRegistry,
6337
6623
  NamespaceResolver,
6624
+ OPERATION_PRIVATE_KEY_PREFIX,
6338
6625
  ObjectKernel,
6339
6626
  ObjectKernelBase,
6340
6627
  ObjectLogger,
@@ -6356,12 +6643,17 @@ var NamespaceResolver = class {
6356
6643
  ServiceLifecycle,
6357
6644
  UnknownFilterTokenError,
6358
6645
  UnresolvedFilterTokenError,
6646
+ assembleExecutionContext,
6647
+ assembleExecutionContextOrGuest,
6359
6648
  assertInitServiceRequirements,
6649
+ assertMetadataRegisterContract,
6360
6650
  bucketKeyToCalendarRange,
6361
6651
  buildPermissionsFromGrants,
6362
6652
  bulkWrite,
6363
6653
  calendarPartsInTz,
6364
6654
  calendarPartsInTzOrUtc,
6655
+ canonicalMetadataServiceType,
6656
+ collectInternalWriteResponseFields,
6365
6657
  counterSignPayload,
6366
6658
  createLogger,
6367
6659
  createMemoryCache,
@@ -6386,18 +6678,22 @@ var NamespaceResolver = class {
6386
6678
  getMemoryUsage,
6387
6679
  hashApiKey,
6388
6680
  hashMigrationPlan,
6681
+ isAudienceBindingSuggestionStatus,
6389
6682
  isAuthGateAllowlisted,
6390
6683
  isExpired,
6391
6684
  isGrantActive,
6392
6685
  isGrantExpired,
6393
6686
  isNode,
6394
6687
  nextUtcCalendarDay,
6688
+ normalizeAuthGate,
6689
+ omitInternalFieldsFromWriteResponse,
6395
6690
  parseScopes,
6396
6691
  parseSignature,
6397
6692
  planChunks,
6398
6693
  postureVisibleRows,
6399
6694
  readAuthoredTranslationLayer,
6400
6695
  readRunJournal,
6696
+ recordNotFoundError,
6401
6697
  resolveApiKeyPrincipal,
6402
6698
  resolveAuthzContext,
6403
6699
  resolveFilterToken,
@@ -6411,6 +6707,7 @@ var NamespaceResolver = class {
6411
6707
  safeExit,
6412
6708
  shouldDenyAnonymous,
6413
6709
  signPayload,
6710
+ unknownAudienceBindingSuggestionStatusMessage,
6414
6711
  utcInstantMs,
6415
6712
  validateInitServiceContract,
6416
6713
  verifyPayload,
@@ -6419,6 +6716,7 @@ var NamespaceResolver = class {
6419
6716
  verifyPublisherSignature,
6420
6717
  wireAuthoredTranslationSync,
6421
6718
  withTransientRetry,
6719
+ withoutOperationPrivateKeys,
6422
6720
  zonedDateStartToUtcMs
6423
6721
  });
6424
6722
  //# sourceMappingURL=index.cjs.map