@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.js CHANGED
@@ -93,6 +93,30 @@ function assertInitServiceRequirements(plugin, isServiceRegistered) {
93
93
  }
94
94
  }
95
95
 
96
+ // src/hook-dispatch.ts
97
+ function traceDispatch(name, handlers, logger) {
98
+ logger.debug(`Triggering hook: ${name}`, {
99
+ hook: name,
100
+ handlerCount: handlers.length
101
+ });
102
+ }
103
+ async function dispatchHookIsolating(name, handlers, logger, args = []) {
104
+ traceDispatch(name, handlers, logger);
105
+ for (const handler of handlers) {
106
+ try {
107
+ await handler(...args);
108
+ } catch (error) {
109
+ logger.error(`Hook handler failed: ${name}`, error);
110
+ }
111
+ }
112
+ }
113
+ async function dispatchHookPropagating(name, handlers, logger, args = []) {
114
+ if (logger) traceDispatch(name, handlers, logger);
115
+ for (const handler of handlers) {
116
+ await handler(...args);
117
+ }
118
+ }
119
+
96
120
  // src/kernel-base.ts
97
121
  var ObjectKernelBase = class {
98
122
  constructor(logger) {
@@ -172,11 +196,11 @@ var ObjectKernelBase = class {
172
196
  }
173
197
  this.hooks.get(name).push(handler);
174
198
  },
199
+ // PROPAGATING dispatch, and deliberately WITHOUT the trace line the
200
+ // kernel's own dispatch sites emit — `context.trigger` has never
201
+ // logged one, so no logger is handed over (#5282).
175
202
  trigger: async (name, ...args) => {
176
- const handlers = this.hooks.get(name) || [];
177
- for (const handler of handlers) {
178
- await handler(...args);
179
- }
203
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], void 0, args);
180
204
  },
181
205
  getServices: () => {
182
206
  if (this.services instanceof Map) {
@@ -295,22 +319,16 @@ var ObjectKernelBase = class {
295
319
  * (`kernel:ready`, `kernel:bootstrapped`, `kernel:listening`) use
296
320
  * {@link triggerHookOrThrow} (#5170, #5257).
297
321
  *
322
+ * The loop itself lives in {@link dispatchHookIsolating} — one
323
+ * implementation shared with `ObjectKernel`'s own `kernel:shutdown`
324
+ * dispatch, which cannot inherit this method (`ObjectKernel` does not
325
+ * extend this class) and used to hand-mirror it (#5282).
326
+ *
298
327
  * @param name - Hook name
299
328
  * @param args - Arguments to pass to handlers
300
329
  */
301
330
  async triggerHook(name, ...args) {
302
- const handlers = this.hooks.get(name) || [];
303
- this.logger.debug(`Triggering hook: ${name}`, {
304
- hook: name,
305
- handlerCount: handlers.length
306
- });
307
- for (const handler of handlers) {
308
- try {
309
- await handler(...args);
310
- } catch (error) {
311
- this.logger.error(`Hook handler failed: ${name}`, error);
312
- }
313
- }
331
+ await dispatchHookIsolating(name, this.hooks.get(name) || [], this.logger, args);
314
332
  }
315
333
  /**
316
334
  * Trigger a hook with all registered handlers, PROPAGATING the first
@@ -347,18 +365,15 @@ var ObjectKernelBase = class {
347
365
  * default — and it is the reason this dispatcher is chosen per hook rather
348
366
  * than swapped in wholesale.
349
367
  *
368
+ * The loop itself lives in {@link dispatchHookPropagating} — the same
369
+ * function `PluginContext.trigger` runs on both kernels, so "propagating"
370
+ * means one thing repo-wide (#5282).
371
+ *
350
372
  * @param name - Hook name
351
373
  * @param args - Arguments to pass to handlers
352
374
  */
353
375
  async triggerHookOrThrow(name, ...args) {
354
- const handlers = this.hooks.get(name) || [];
355
- this.logger.debug(`Triggering hook: ${name}`, {
356
- hook: name,
357
- handlerCount: handlers.length
358
- });
359
- for (const handler of handlers) {
360
- await handler(...args);
361
- }
376
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], this.logger, args);
362
377
  }
363
378
  /**
364
379
  * Get current kernel state
@@ -494,7 +509,15 @@ var ObjectLogger = class _ObjectLogger {
494
509
  redact: config.redact ?? ["password", "token", "secret", "key"],
495
510
  sourceLocation: config.sourceLocation ?? false,
496
511
  file: config.file,
497
- rotation: config.rotation ?? { maxSize: "10m", maxFiles: 5 }
512
+ // Per-key, because `LoggerConfig` is the AUTHOR state (ADR-0122): the
513
+ // schema defaults `maxSize`/`maxFiles` *inside* `rotation`, so a caller
514
+ // may legitimately write `{ rotation: { maxSize: '5m' } }` and this
515
+ // constructor — which does not parse — has to fill the other half the
516
+ // same way `LoggerConfigSchema.parse` would.
517
+ rotation: {
518
+ maxSize: config.rotation?.maxSize ?? "10m",
519
+ maxFiles: config.rotation?.maxFiles ?? 5
520
+ }
498
521
  };
499
522
  this.bindings = bindings;
500
523
  this.redactPatterns = this.config.redact.map(tokenizeFieldName).filter((words) => words.length > 0);
@@ -1362,6 +1385,7 @@ function createMemoryJob() {
1362
1385
  }
1363
1386
 
1364
1387
  // src/fallbacks/memory-i18n.ts
1388
+ import { normalizeSupportedLocales } from "@objectstack/spec/system";
1365
1389
  function deepMerge(target, source) {
1366
1390
  const result = { ...target };
1367
1391
  for (const key of Object.keys(source)) {
@@ -1395,6 +1419,7 @@ function createMemoryI18n() {
1395
1419
  const translations = /* @__PURE__ */ new Map();
1396
1420
  const authored = /* @__PURE__ */ new Map();
1397
1421
  let defaultLocale = "en";
1422
+ let supportedLocales;
1398
1423
  function resolveKey(data, key) {
1399
1424
  const parts = key.split(".");
1400
1425
  let current = data;
@@ -1460,9 +1485,29 @@ function createMemoryI18n() {
1460
1485
  authored.set(locale, { ...data });
1461
1486
  }
1462
1487
  },
1488
+ /**
1489
+ * Report the locales this stack offers.
1490
+ *
1491
+ * [#7679] When the app declared `i18n.supportedLocales`, that declaration
1492
+ * IS the answer — in declared order, and including a declared locale no
1493
+ * bundle was ever loaded for (declared-but-unserved). Reporting the
1494
+ * declaration rather than an intersection is what gives a client the
1495
+ * signal that the locale it is being offered has nothing behind it yet;
1496
+ * quietly dropping it would leave the gap invisible on both sides. It is
1497
+ * also the only answer that does not depend on how much had loaded by the
1498
+ * time this was called.
1499
+ *
1500
+ * With nothing declared, the loaded set — the behaviour every app that
1501
+ * never opted in already has.
1502
+ */
1463
1503
  getLocales() {
1504
+ if (supportedLocales) return [...supportedLocales];
1464
1505
  return [.../* @__PURE__ */ new Set([...translations.keys(), ...authored.keys()])];
1465
1506
  },
1507
+ /** @see II18nService.setSupportedLocales — [#7679] */
1508
+ setSupportedLocales(locales) {
1509
+ supportedLocales = normalizeSupportedLocales(locales);
1510
+ },
1466
1511
  getDefaultLocale() {
1467
1512
  return defaultLocale;
1468
1513
  },
@@ -1472,14 +1517,42 @@ function createMemoryI18n() {
1472
1517
  };
1473
1518
  }
1474
1519
 
1520
+ // src/metadata-service-contract.ts
1521
+ import { pluralToSingular } from "@objectstack/spec/shared";
1522
+ var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
1523
+ function canonicalMetadataServiceType(type) {
1524
+ return pluralToSingular(type);
1525
+ }
1526
+ function registerRefusal(message) {
1527
+ const err = new Error(message);
1528
+ err.code = REGISTER_REFUSAL_CODE;
1529
+ err.status = 400;
1530
+ return err;
1531
+ }
1532
+ function assertMetadataRegisterContract(type, name, data) {
1533
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
1534
+ const shape = data === null ? "null" : Array.isArray(data) ? "an array" : `a ${typeof data}`;
1535
+ throw registerRefusal(
1536
+ `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.`
1537
+ );
1538
+ }
1539
+ const documentName = data.name;
1540
+ if (documentName !== void 0 && documentName !== name) {
1541
+ throw registerRefusal(
1542
+ `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.`
1543
+ );
1544
+ }
1545
+ }
1546
+
1475
1547
  // src/fallbacks/memory-metadata.ts
1476
1548
  function createMemoryMetadata() {
1477
1549
  const store = /* @__PURE__ */ new Map();
1478
1550
  function getTypeMap(type) {
1479
- let map = store.get(type);
1551
+ const canonical = canonicalMetadataServiceType(type);
1552
+ let map = store.get(canonical);
1480
1553
  if (!map) {
1481
1554
  map = /* @__PURE__ */ new Map();
1482
- store.set(type, map);
1555
+ store.set(canonical, map);
1483
1556
  }
1484
1557
  return map;
1485
1558
  }
@@ -1494,6 +1567,7 @@ function createMemoryMetadata() {
1494
1567
  },
1495
1568
  _serviceName: "metadata",
1496
1569
  async register(type, name, data) {
1570
+ assertMetadataRegisterContract(type, name, data);
1497
1571
  getTypeMap(type).set(name, data);
1498
1572
  },
1499
1573
  // Mirror MetadataManager.registerInMemory (synchronous, no persistence).
@@ -1505,7 +1579,11 @@ function createMemoryMetadata() {
1505
1579
  // so `defineStack({ datasources })` entries silently never reached the
1506
1580
  // registry and were absent from GET /api/v1/datasources and
1507
1581
  // GET /api/v1/meta/datasource (ADR-0015 §18). This store is already
1508
- // in-memory only, so registerInMemory and register share an implementation.
1582
+ // in-memory only, so registerInMemory and register share a store — but
1583
+ // NOT the [#7378] refusals: the ruling names `register`, and this member
1584
+ // is a boot-time seeding primitive for source-control-owned artefacts
1585
+ // (see assertMetadataRegisterContract's header for the boundary). It does
1586
+ // share the row-2 canonical type fold, via getTypeMap.
1509
1587
  registerInMemory(type, name, data) {
1510
1588
  getTypeMap(type).set(name, data);
1511
1589
  },
@@ -1729,11 +1807,12 @@ var ObjectKernel = class {
1729
1807
  }
1730
1808
  this.hooks.get(name).push(handler);
1731
1809
  },
1810
+ // PROPAGATING dispatch — the same shared loop `LiteKernel`'s
1811
+ // context.trigger runs, and deliberately WITHOUT a trace line:
1812
+ // `context.trigger` has never emitted one on either kernel, so no
1813
+ // logger is handed over (#5282).
1732
1814
  trigger: async (name, ...args) => {
1733
- const handlers = this.hooks.get(name) || [];
1734
- for (const handler of handlers) {
1735
- await handler(...args);
1736
- }
1815
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], void 0, args);
1737
1816
  },
1738
1817
  getServices: () => {
1739
1818
  return new Map(this.services);
@@ -2157,27 +2236,21 @@ var ObjectKernel = class {
2157
2236
  * one bad handler must not amplify into leaked resources and unflushed
2158
2237
  * writes. Same reasoning, same wording, same `Hook handler failed:
2159
2238
  * kernel:shutdown` log line as `LiteKernel`'s dispatch site, which reaches
2160
- * the shared isolating dispatcher `ObjectKernelBase.triggerHook` (#5257).
2239
+ * the isolating dispatcher through `ObjectKernelBase.triggerHook` (#5257).
2161
2240
  *
2162
- * `ObjectKernel` cannot call that dispatcher: it does not extend
2241
+ * Until #5282 "same wording" was literally that the loop was typed out a
2242
+ * second time here, because `ObjectKernel` does not extend
2163
2243
  * `ObjectKernelBase` (only `LiteKernel` does) and owns its own `hooks` map,
2164
- * so the semantics are mirrored here rather than shared. One hook name
2165
- * meaning two opposite things across the two kernels is exactly the bug
2166
- * #5170/#5257 closed, so the pin for this one lives on both sides too.
2244
+ * so the base's `protected triggerHook` is out of reach. The loop now lives
2245
+ * in {@link dispatchHookIsolating}, which BOTH sides call: the storage is
2246
+ * still two maps (deliberately unifying it was out of #5282's scope), but
2247
+ * "isolating" is one implementation, so it can no longer drift on one
2248
+ * kernel while the other keeps the old shape. That drift is exactly the bug
2249
+ * #5170 / #5257 / #5274 each closed one hook at a time, and the paired-pin
2250
+ * gate (`scripts/check-kernel-hook-pairs.mjs`) covers the residue.
2167
2251
  */
2168
2252
  async triggerShutdownHookIsolating() {
2169
- const handlers = this.hooks.get("kernel:shutdown") || [];
2170
- this.logger.debug("Triggering hook: kernel:shutdown", {
2171
- hook: "kernel:shutdown",
2172
- handlerCount: handlers.length
2173
- });
2174
- for (const handler of handlers) {
2175
- try {
2176
- await handler();
2177
- } catch (error) {
2178
- this.logger.error("Hook handler failed: kernel:shutdown", error);
2179
- }
2180
- }
2253
+ await dispatchHookIsolating("kernel:shutdown", this.hooks.get("kernel:shutdown") || [], this.logger);
2181
2254
  }
2182
2255
  async performShutdown() {
2183
2256
  await this.triggerShutdownHookIsolating();
@@ -2345,6 +2418,20 @@ __export(qa_exports, {
2345
2418
  });
2346
2419
 
2347
2420
  // src/qa/runner.ts
2421
+ function describeActualType(value) {
2422
+ if (value === null) return "null";
2423
+ if (Array.isArray(value)) return "array";
2424
+ return typeof value;
2425
+ }
2426
+ function containsInapplicableHint(actual) {
2427
+ if (actual === void 0) {
2428
+ 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.";
2429
+ }
2430
+ if (actual === null) {
2431
+ return "The path resolved to null. Use 'is_null' if asserting absence is what you meant.";
2432
+ }
2433
+ 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.";
2434
+ }
2348
2435
  var TestRunner = class {
2349
2436
  constructor(adapter) {
2350
2437
  this.adapter = adapter;
@@ -2472,6 +2559,10 @@ var TestRunner = class {
2472
2559
  if (!actual.includes(expected)) throw new Error(`Assertion failed: ${assertion.field} array does not contain ${expected}`);
2473
2560
  } else if (typeof actual === "string") {
2474
2561
  if (!actual.includes(String(expected))) throw new Error(`Assertion failed: ${assertion.field} string does not contain ${expected}`);
2562
+ } else {
2563
+ throw new Error(
2564
+ `Assertion failed: ${assertion.field} cannot be evaluated by 'contains' \u2014 expected an array or a string at that path, got ${describeActualType(actual)}. ` + containsInapplicableHint(actual)
2565
+ );
2475
2566
  }
2476
2567
  break;
2477
2568
  case "not_null":
@@ -2488,11 +2579,29 @@ var TestRunner = class {
2488
2579
  };
2489
2580
 
2490
2581
  // src/qa/http-adapter.ts
2582
+ import { RestApiConfigSchema, CrudEndpointsConfigSchema } from "@objectstack/spec/api";
2583
+ var dataPathCache;
2584
+ function defaultDataPath() {
2585
+ if (dataPathCache === void 0) {
2586
+ const api = RestApiConfigSchema.parse({});
2587
+ const crud = CrudEndpointsConfigSchema.parse({});
2588
+ dataPathCache = `${api.apiPath ?? `${api.basePath}/${api.version}`}${crud.dataPrefix}`;
2589
+ }
2590
+ return dataPathCache;
2591
+ }
2491
2592
  var HttpTestAdapter = class {
2492
2593
  constructor(baseUrl, authToken) {
2493
2594
  this.baseUrl = baseUrl;
2494
2595
  this.authToken = authToken;
2495
2596
  }
2597
+ /** `{baseUrl}{apiBasePath}{dataPrefix}/{object}` — the collection URL. */
2598
+ collectionUrl(objectName) {
2599
+ return `${this.baseUrl}${defaultDataPath()}/${encodeURIComponent(objectName)}`;
2600
+ }
2601
+ /** `{collection}/{id}` — the single-record URL. */
2602
+ recordUrl(objectName, id) {
2603
+ return `${this.collectionUrl(objectName)}/${encodeURIComponent(String(id))}`;
2604
+ }
2496
2605
  async execute(action, _context) {
2497
2606
  const headers = {
2498
2607
  "Content-Type": "application/json"
@@ -2524,7 +2633,7 @@ var HttpTestAdapter = class {
2524
2633
  }
2525
2634
  }
2526
2635
  async createRecord(objectName, data, headers) {
2527
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}`, {
2636
+ const response = await fetch(this.collectionUrl(objectName), {
2528
2637
  method: "POST",
2529
2638
  headers,
2530
2639
  body: JSON.stringify(data)
@@ -2532,19 +2641,19 @@ var HttpTestAdapter = class {
2532
2641
  return this.handleResponse(response);
2533
2642
  }
2534
2643
  async updateRecord(objectName, data, headers) {
2535
- const id = data.id;
2644
+ const { id, ...fields } = data;
2536
2645
  if (!id) throw new Error("Update record requires id in payload");
2537
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2538
- method: "PUT",
2646
+ const response = await fetch(this.recordUrl(objectName, id), {
2647
+ method: "PATCH",
2539
2648
  headers,
2540
- body: JSON.stringify(data)
2649
+ body: JSON.stringify(fields)
2541
2650
  });
2542
2651
  return this.handleResponse(response);
2543
2652
  }
2544
2653
  async deleteRecord(objectName, data, headers) {
2545
2654
  const id = data.id;
2546
2655
  if (!id) throw new Error("Delete record requires id in payload");
2547
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2656
+ const response = await fetch(this.recordUrl(objectName, id), {
2548
2657
  method: "DELETE",
2549
2658
  headers
2550
2659
  });
@@ -2553,14 +2662,14 @@ var HttpTestAdapter = class {
2553
2662
  async readRecord(objectName, data, headers) {
2554
2663
  const id = data.id;
2555
2664
  if (!id) throw new Error("Read record requires id in payload");
2556
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/${id}`, {
2665
+ const response = await fetch(this.recordUrl(objectName, id), {
2557
2666
  method: "GET",
2558
2667
  headers
2559
2668
  });
2560
2669
  return this.handleResponse(response);
2561
2670
  }
2562
2671
  async queryRecords(objectName, data, headers) {
2563
- const response = await fetch(`${this.baseUrl}/api/data/${objectName}/query`, {
2672
+ const response = await fetch(`${this.collectionUrl(objectName)}/query`, {
2564
2673
  method: "POST",
2565
2674
  headers,
2566
2675
  body: JSON.stringify(data)
@@ -4357,7 +4466,116 @@ async function resolveLocalizationContext(input) {
4357
4466
  };
4358
4467
  }
4359
4468
 
4469
+ // src/security/assemble-execution-context.ts
4470
+ var ENTRY_EXECUTION_CONTEXT_FIELDS = [
4471
+ "positions",
4472
+ "permissions",
4473
+ "systemPermissions",
4474
+ "isSystem",
4475
+ "principalKind",
4476
+ "onBehalfOf",
4477
+ "audience",
4478
+ "userId",
4479
+ "tenantId",
4480
+ "email",
4481
+ "accessToken",
4482
+ "tabPermissions",
4483
+ "posture",
4484
+ "authGate",
4485
+ "org_user_ids",
4486
+ "accessible_org_ids",
4487
+ "oauthScopes",
4488
+ "timezone",
4489
+ "locale",
4490
+ "currency"
4491
+ ];
4492
+ function emit(fields) {
4493
+ const ctx = {};
4494
+ for (const key of ENTRY_EXECUTION_CONTEXT_FIELDS) {
4495
+ const value = fields[key];
4496
+ if (value !== void 0) ctx[key] = value;
4497
+ }
4498
+ return ctx;
4499
+ }
4500
+ function entryFields(input, anonymous) {
4501
+ const { authz, oauth, localization, requestLocale, accessToken, authGate } = input;
4502
+ const agent = !anonymous && oauth?.clientId ? oauth : void 0;
4503
+ return {
4504
+ // [ADR-0090 D9/D10] Principal taxonomy at the HTTP entry: a session-backed
4505
+ // request is a human principal; a sessionless one is a guest, holding the
4506
+ // built-in `guest` position implicitly and exclusively. Internal engine
4507
+ // calls that construct bare contexts never pass through here, so the
4508
+ // security plugin's empty-context skip path keeps its meaning.
4509
+ positions: agent ? [] : anonymous ? ["guest"] : authz.positions,
4510
+ permissions: agent ? agent.scopePermissions : authz.permissions,
4511
+ // [ADR-0090 D10] System capabilities on the agent principal gate business
4512
+ // ACTION invocation (`actionPermissionError` reads `ctx.systemPermissions`)
4513
+ // — a door SEPARATE from the object CRUD/FLS/RLS intersection, which is
4514
+ // driven by the resolved ceiling SETS (they carry no caps, so cap-gated
4515
+ // OBJECT access stays denied to the agent regardless of this line). The
4516
+ // `actions:execute` scope IS the user's consent to let this agent invoke
4517
+ // actions on their behalf; without it the agent holds none.
4518
+ systemPermissions: agent ? agent.delegatesActions ? authz.systemPermissions ?? [] : [] : authz.systemPermissions,
4519
+ isSystem: false,
4520
+ principalKind: agent ? "agent" : anonymous ? "guest" : "human",
4521
+ onBehalfOf: agent ? { userId: authz.userId, principalKind: "human" } : void 0,
4522
+ // [ADR-0090 D10/D11 — P1 shape] No transport resolves an external
4523
+ // (portal/partner) audience yet; `undefined` reads as 'internal'. Named
4524
+ // here rather than excluded so the gap is visible in the closed set instead
4525
+ // of being invisible outside it — when an external principal type lands,
4526
+ // this is the line that must change, on every face at once.
4527
+ audience: void 0,
4528
+ userId: authz.userId,
4529
+ tenantId: authz.tenantId,
4530
+ email: authz.email,
4531
+ accessToken,
4532
+ tabPermissions: authz.tabPermissions,
4533
+ // [ADR-0095 D2 / #2947] The derived posture rung, carried so every
4534
+ // transport presents enforcement the SAME value. Present only for an
4535
+ // authenticated principal (guest → absent).
4536
+ posture: authz.posture,
4537
+ // [ADR-0069 / #7280] The AUTHENTICATION-policy gate, carried for the seam
4538
+ // that reads it off the envelope (REST's `enforceAuth`). Anonymous → never:
4539
+ // a guest has no authenticated session for a policy gate to attach to, so
4540
+ // "gated guest" is not a state this entry can emit even if a face passed
4541
+ // one.
4542
+ authGate: anonymous ? void 0 : authGate,
4543
+ /** Fellow-org user IDs for RLS scoping of identity tables. */
4544
+ org_user_ids: authz.org_user_ids,
4545
+ // [ADR-0105 D2] The caller's org access set — the `group` posture's Layer 0
4546
+ // wall reads it directly, so every transport must carry it (#6206).
4547
+ accessible_org_ids: authz.accessible_org_ids,
4548
+ // OAuth provenance: surface the token's granted scopes so the MCP
4549
+ // dispatcher can narrow the exposed tool families (undefined for every
4550
+ // other provenance = not scope-limited).
4551
+ oauthScopes: oauth && authz.userId === oauth.userId ? oauth.scopes : void 0,
4552
+ // Anonymous → no localization (no scope to resolve against); the engine
4553
+ // default stands. [#3957] The request's OWN language preference wins over
4554
+ // the workspace default, so a rejection message is not rendered in English
4555
+ // beside the Chinese label of the very field it names.
4556
+ timezone: anonymous ? void 0 : localization?.timezone,
4557
+ locale: anonymous ? void 0 : requestLocale ?? localization?.locale,
4558
+ currency: anonymous ? void 0 : localization?.currency
4559
+ };
4560
+ }
4561
+ function assembleExecutionContext(input) {
4562
+ if (!input.authz.userId) return void 0;
4563
+ return emit(entryFields(input, false));
4564
+ }
4565
+ function assembleExecutionContextOrGuest(input) {
4566
+ return emit(entryFields(input, !input.authz.userId));
4567
+ }
4568
+
4360
4569
  // src/security/auth-gate.ts
4570
+ var DEFAULT_AUTH_GATE_MESSAGE = "Access is blocked by an authentication policy.";
4571
+ function normalizeAuthGate(sessionUser) {
4572
+ const gate = sessionUser?.authGate;
4573
+ if (!gate || typeof gate.code !== "string") return null;
4574
+ return {
4575
+ code: gate.code,
4576
+ message: typeof gate.message === "string" && gate.message ? gate.message : DEFAULT_AUTH_GATE_MESSAGE
4577
+ };
4578
+ }
4361
4579
  var ALLOW_PREFIXES = ["/api/v1/auth/", "/api/auth/", "/auth/"];
4362
4580
  var ALLOW_SUFFIXES = ["/health", "/ready", "/discovery", "/me/apps", "/me/localization"];
4363
4581
  function isAuthGateAllowlisted(rawPath) {
@@ -4376,13 +4594,10 @@ function isAuthGateAllowlisted(rawPath) {
4376
4594
  return false;
4377
4595
  }
4378
4596
  function evaluateAuthGate(sessionUser, path) {
4379
- const gate = sessionUser?.authGate;
4380
- if (!gate || typeof gate.code !== "string") return null;
4597
+ const gate = normalizeAuthGate(sessionUser);
4598
+ if (!gate) return null;
4381
4599
  if (isAuthGateAllowlisted(path)) return null;
4382
- return {
4383
- code: gate.code,
4384
- message: typeof gate.message === "string" && gate.message ? gate.message : "Access is blocked by an authentication policy."
4385
- };
4600
+ return gate;
4386
4601
  }
4387
4602
 
4388
4603
  // src/security/anonymous-deny.ts
@@ -4404,6 +4619,29 @@ function shouldDenyAnonymous(input) {
4404
4619
  return true;
4405
4620
  }
4406
4621
 
4622
+ // src/security/audience-binding-suggestion-status.ts
4623
+ var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
4624
+ pending: true,
4625
+ confirmed: true,
4626
+ dismissed: true
4627
+ };
4628
+ var AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES = Object.keys(
4629
+ AUDIENCE_BINDING_SUGGESTION_STATUSES
4630
+ );
4631
+ var isAudienceBindingSuggestionStatus = (value) => Object.prototype.hasOwnProperty.call(AUDIENCE_BINDING_SUGGESTION_STATUSES, value);
4632
+ var unknownAudienceBindingSuggestionStatusMessage = (value) => `Unknown status filter '${value}' \u2014 expected one of: ${AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES.join(", ")}`;
4633
+
4634
+ // src/security/operation-private-keys.ts
4635
+ var OPERATION_PRIVATE_KEY_PREFIX = "__";
4636
+ function withoutOperationPrivateKeys(exec) {
4637
+ const out = {};
4638
+ for (const [key, value] of Object.entries(exec)) {
4639
+ if (key.startsWith(OPERATION_PRIVATE_KEY_PREFIX)) continue;
4640
+ out[key] = value;
4641
+ }
4642
+ return out;
4643
+ }
4644
+
4407
4645
  // src/utils/datetime.ts
4408
4646
  import { nextUtcCalendarDay, utcInstantMs } from "@objectstack/spec/data";
4409
4647
  function calendarPartsInTz(d, tz) {
@@ -4646,6 +4884,27 @@ async function bulkWrite(rows, opts) {
4646
4884
  return results;
4647
4885
  }
4648
4886
 
4887
+ // src/utils/internal-write-response.ts
4888
+ function collectInternalWriteResponseFields(schema) {
4889
+ const fields = schema?.fields;
4890
+ if (!fields || typeof fields !== "object") return [];
4891
+ const out = [];
4892
+ for (const [name, def] of Object.entries(fields)) {
4893
+ if (def && def.internal === true) out.push(name);
4894
+ }
4895
+ return out;
4896
+ }
4897
+ function omitInternalFieldsFromWriteResponse(schema, records) {
4898
+ if (!records) return;
4899
+ const internalFields = collectInternalWriteResponseFields(schema);
4900
+ if (internalFields.length === 0) return;
4901
+ const list = Array.isArray(records) ? records : [records];
4902
+ for (const row of list) {
4903
+ if (!row || typeof row !== "object") continue;
4904
+ for (const field of internalFields) delete row[field];
4905
+ }
4906
+ }
4907
+
4649
4908
  // src/utils/migration-journal.ts
4650
4909
  import { createHash as createHash2, randomUUID } from "crypto";
4651
4910
  import {
@@ -5241,6 +5500,15 @@ function filterTokenContextFrom(execCtx, now) {
5241
5500
  };
5242
5501
  }
5243
5502
 
5503
+ // src/utils/record-not-found.ts
5504
+ function recordNotFoundError(object, id) {
5505
+ const err = new Error(`Record ${id} not found in ${object}`);
5506
+ err.code = "RECORD_NOT_FOUND";
5507
+ err.status = 404;
5508
+ err.object = object;
5509
+ return err;
5510
+ }
5511
+
5244
5512
  // src/health-monitor.ts
5245
5513
  var PluginHealthMonitor = class {
5246
5514
  constructor(logger) {
@@ -6223,13 +6491,17 @@ export {
6223
6491
  ANONYMOUS_DENY_MESSAGE,
6224
6492
  ANONYMOUS_DENY_STATUS,
6225
6493
  API_KEY_PREFIX,
6494
+ AUDIENCE_BINDING_SUGGESTION_STATUSES,
6495
+ AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
6226
6496
  CORE_FALLBACK_FACTORIES,
6227
6497
  DependencyResolver,
6498
+ ENTRY_EXECUTION_CONTEXT_FIELDS,
6228
6499
  HotReloadManager,
6229
6500
  LiteKernel,
6230
6501
  MigrationJournalRefusal,
6231
6502
  MigrationPlanRegistry,
6232
6503
  NamespaceResolver,
6504
+ OPERATION_PRIVATE_KEY_PREFIX,
6233
6505
  ObjectKernel,
6234
6506
  ObjectKernelBase,
6235
6507
  ObjectLogger,
@@ -6251,12 +6523,17 @@ export {
6251
6523
  ServiceLifecycle,
6252
6524
  UnknownFilterTokenError,
6253
6525
  UnresolvedFilterTokenError,
6526
+ assembleExecutionContext,
6527
+ assembleExecutionContextOrGuest,
6254
6528
  assertInitServiceRequirements,
6529
+ assertMetadataRegisterContract,
6255
6530
  bucketKeyToCalendarRange,
6256
6531
  buildPermissionsFromGrants,
6257
6532
  bulkWrite,
6258
6533
  calendarPartsInTz,
6259
6534
  calendarPartsInTzOrUtc,
6535
+ canonicalMetadataServiceType,
6536
+ collectInternalWriteResponseFields,
6260
6537
  counterSignPayload,
6261
6538
  createLogger,
6262
6539
  createMemoryCache,
@@ -6281,18 +6558,22 @@ export {
6281
6558
  getMemoryUsage,
6282
6559
  hashApiKey,
6283
6560
  hashMigrationPlan,
6561
+ isAudienceBindingSuggestionStatus,
6284
6562
  isAuthGateAllowlisted,
6285
6563
  isExpired,
6286
6564
  isGrantActive,
6287
6565
  isGrantExpired,
6288
6566
  isNode,
6289
6567
  nextUtcCalendarDay,
6568
+ normalizeAuthGate,
6569
+ omitInternalFieldsFromWriteResponse,
6290
6570
  parseScopes,
6291
6571
  parseSignature,
6292
6572
  planChunks,
6293
6573
  postureVisibleRows,
6294
6574
  readAuthoredTranslationLayer,
6295
6575
  readRunJournal,
6576
+ recordNotFoundError,
6296
6577
  resolveApiKeyPrincipal,
6297
6578
  resolveAuthzContext,
6298
6579
  resolveFilterToken,
@@ -6306,6 +6587,7 @@ export {
6306
6587
  safeExit,
6307
6588
  shouldDenyAnonymous,
6308
6589
  signPayload,
6590
+ unknownAudienceBindingSuggestionStatusMessage,
6309
6591
  utcInstantMs,
6310
6592
  validateInitServiceContract,
6311
6593
  verifyPayload,
@@ -6314,6 +6596,7 @@ export {
6314
6596
  verifyPublisherSignature,
6315
6597
  wireAuthoredTranslationSync,
6316
6598
  withTransientRetry,
6599
+ withoutOperationPrivateKeys,
6317
6600
  zonedDateStartToUtcMs
6318
6601
  };
6319
6602
  //# sourceMappingURL=index.js.map