@objectstack/core 17.0.0-rc.5 → 17.0.0-rc.6

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
@@ -37,11 +37,13 @@ __export(index_exports, {
37
37
  API_KEY_PREFIX: () => API_KEY_PREFIX,
38
38
  CORE_FALLBACK_FACTORIES: () => CORE_FALLBACK_FACTORIES,
39
39
  DependencyResolver: () => DependencyResolver,
40
+ ENTRY_EXECUTION_CONTEXT_FIELDS: () => ENTRY_EXECUTION_CONTEXT_FIELDS,
40
41
  HotReloadManager: () => HotReloadManager,
41
42
  LiteKernel: () => LiteKernel,
42
43
  MigrationJournalRefusal: () => MigrationJournalRefusal,
43
44
  MigrationPlanRegistry: () => MigrationPlanRegistry,
44
45
  NamespaceResolver: () => NamespaceResolver,
46
+ OPERATION_PRIVATE_KEY_PREFIX: () => OPERATION_PRIVATE_KEY_PREFIX,
45
47
  ObjectKernel: () => ObjectKernel,
46
48
  ObjectKernelBase: () => ObjectKernelBase,
47
49
  ObjectLogger: () => ObjectLogger,
@@ -63,6 +65,8 @@ __export(index_exports, {
63
65
  ServiceLifecycle: () => ServiceLifecycle,
64
66
  UnknownFilterTokenError: () => UnknownFilterTokenError,
65
67
  UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
68
+ assembleExecutionContext: () => assembleExecutionContext,
69
+ assembleExecutionContextOrGuest: () => assembleExecutionContextOrGuest,
66
70
  assertInitServiceRequirements: () => assertInitServiceRequirements,
67
71
  bucketKeyToCalendarRange: () => bucketKeyToCalendarRange,
68
72
  buildPermissionsFromGrants: () => buildPermissionsFromGrants,
@@ -99,6 +103,7 @@ __export(index_exports, {
99
103
  isGrantExpired: () => isGrantExpired,
100
104
  isNode: () => isNode,
101
105
  nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
106
+ normalizeAuthGate: () => normalizeAuthGate,
102
107
  parseScopes: () => parseScopes,
103
108
  parseSignature: () => parseSignature,
104
109
  planChunks: () => planChunks,
@@ -126,6 +131,7 @@ __export(index_exports, {
126
131
  verifyPublisherSignature: () => verifyPublisherSignature,
127
132
  wireAuthoredTranslationSync: () => wireAuthoredTranslationSync,
128
133
  withTransientRetry: () => withTransientRetry,
134
+ withoutOperationPrivateKeys: () => withoutOperationPrivateKeys,
129
135
  zonedDateStartToUtcMs: () => zonedDateStartToUtcMs
130
136
  });
131
137
  module.exports = __toCommonJS(index_exports);
@@ -213,6 +219,30 @@ function assertInitServiceRequirements(plugin, isServiceRegistered) {
213
219
  }
214
220
  }
215
221
 
222
+ // src/hook-dispatch.ts
223
+ function traceDispatch(name, handlers, logger) {
224
+ logger.debug(`Triggering hook: ${name}`, {
225
+ hook: name,
226
+ handlerCount: handlers.length
227
+ });
228
+ }
229
+ async function dispatchHookIsolating(name, handlers, logger, args = []) {
230
+ traceDispatch(name, handlers, logger);
231
+ for (const handler of handlers) {
232
+ try {
233
+ await handler(...args);
234
+ } catch (error) {
235
+ logger.error(`Hook handler failed: ${name}`, error);
236
+ }
237
+ }
238
+ }
239
+ async function dispatchHookPropagating(name, handlers, logger, args = []) {
240
+ if (logger) traceDispatch(name, handlers, logger);
241
+ for (const handler of handlers) {
242
+ await handler(...args);
243
+ }
244
+ }
245
+
216
246
  // src/kernel-base.ts
217
247
  var ObjectKernelBase = class {
218
248
  constructor(logger) {
@@ -292,11 +322,11 @@ var ObjectKernelBase = class {
292
322
  }
293
323
  this.hooks.get(name).push(handler);
294
324
  },
325
+ // PROPAGATING dispatch, and deliberately WITHOUT the trace line the
326
+ // kernel's own dispatch sites emit — `context.trigger` has never
327
+ // logged one, so no logger is handed over (#5282).
295
328
  trigger: async (name, ...args) => {
296
- const handlers = this.hooks.get(name) || [];
297
- for (const handler of handlers) {
298
- await handler(...args);
299
- }
329
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], void 0, args);
300
330
  },
301
331
  getServices: () => {
302
332
  if (this.services instanceof Map) {
@@ -415,22 +445,16 @@ var ObjectKernelBase = class {
415
445
  * (`kernel:ready`, `kernel:bootstrapped`, `kernel:listening`) use
416
446
  * {@link triggerHookOrThrow} (#5170, #5257).
417
447
  *
448
+ * The loop itself lives in {@link dispatchHookIsolating} — one
449
+ * implementation shared with `ObjectKernel`'s own `kernel:shutdown`
450
+ * dispatch, which cannot inherit this method (`ObjectKernel` does not
451
+ * extend this class) and used to hand-mirror it (#5282).
452
+ *
418
453
  * @param name - Hook name
419
454
  * @param args - Arguments to pass to handlers
420
455
  */
421
456
  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
- }
457
+ await dispatchHookIsolating(name, this.hooks.get(name) || [], this.logger, args);
434
458
  }
435
459
  /**
436
460
  * Trigger a hook with all registered handlers, PROPAGATING the first
@@ -467,18 +491,15 @@ var ObjectKernelBase = class {
467
491
  * default — and it is the reason this dispatcher is chosen per hook rather
468
492
  * than swapped in wholesale.
469
493
  *
494
+ * The loop itself lives in {@link dispatchHookPropagating} — the same
495
+ * function `PluginContext.trigger` runs on both kernels, so "propagating"
496
+ * means one thing repo-wide (#5282).
497
+ *
470
498
  * @param name - Hook name
471
499
  * @param args - Arguments to pass to handlers
472
500
  */
473
501
  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
- }
502
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], this.logger, args);
482
503
  }
483
504
  /**
484
505
  * Get current kernel state
@@ -614,7 +635,15 @@ var ObjectLogger = class _ObjectLogger {
614
635
  redact: config.redact ?? ["password", "token", "secret", "key"],
615
636
  sourceLocation: config.sourceLocation ?? false,
616
637
  file: config.file,
617
- rotation: config.rotation ?? { maxSize: "10m", maxFiles: 5 }
638
+ // Per-key, because `LoggerConfig` is the AUTHOR state (ADR-0122): the
639
+ // schema defaults `maxSize`/`maxFiles` *inside* `rotation`, so a caller
640
+ // may legitimately write `{ rotation: { maxSize: '5m' } }` and this
641
+ // constructor — which does not parse — has to fill the other half the
642
+ // same way `LoggerConfigSchema.parse` would.
643
+ rotation: {
644
+ maxSize: config.rotation?.maxSize ?? "10m",
645
+ maxFiles: config.rotation?.maxFiles ?? 5
646
+ }
618
647
  };
619
648
  this.bindings = bindings;
620
649
  this.redactPatterns = this.config.redact.map(tokenizeFieldName).filter((words) => words.length > 0);
@@ -1843,11 +1872,12 @@ var ObjectKernel = class {
1843
1872
  }
1844
1873
  this.hooks.get(name).push(handler);
1845
1874
  },
1875
+ // PROPAGATING dispatch — the same shared loop `LiteKernel`'s
1876
+ // context.trigger runs, and deliberately WITHOUT a trace line:
1877
+ // `context.trigger` has never emitted one on either kernel, so no
1878
+ // logger is handed over (#5282).
1846
1879
  trigger: async (name, ...args) => {
1847
- const handlers = this.hooks.get(name) || [];
1848
- for (const handler of handlers) {
1849
- await handler(...args);
1850
- }
1880
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], void 0, args);
1851
1881
  },
1852
1882
  getServices: () => {
1853
1883
  return new Map(this.services);
@@ -2271,27 +2301,21 @@ var ObjectKernel = class {
2271
2301
  * one bad handler must not amplify into leaked resources and unflushed
2272
2302
  * writes. Same reasoning, same wording, same `Hook handler failed:
2273
2303
  * kernel:shutdown` log line as `LiteKernel`'s dispatch site, which reaches
2274
- * the shared isolating dispatcher `ObjectKernelBase.triggerHook` (#5257).
2304
+ * the isolating dispatcher through `ObjectKernelBase.triggerHook` (#5257).
2275
2305
  *
2276
- * `ObjectKernel` cannot call that dispatcher: it does not extend
2306
+ * Until #5282 "same wording" was literally that the loop was typed out a
2307
+ * second time here, because `ObjectKernel` does not extend
2277
2308
  * `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.
2309
+ * so the base's `protected triggerHook` is out of reach. The loop now lives
2310
+ * in {@link dispatchHookIsolating}, which BOTH sides call: the storage is
2311
+ * still two maps (deliberately unifying it was out of #5282's scope), but
2312
+ * "isolating" is one implementation, so it can no longer drift on one
2313
+ * kernel while the other keeps the old shape. That drift is exactly the bug
2314
+ * #5170 / #5257 / #5274 each closed one hook at a time, and the paired-pin
2315
+ * gate (`scripts/check-kernel-hook-pairs.mjs`) covers the residue.
2281
2316
  */
2282
2317
  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
- }
2318
+ await dispatchHookIsolating("kernel:shutdown", this.hooks.get("kernel:shutdown") || [], this.logger);
2295
2319
  }
2296
2320
  async performShutdown() {
2297
2321
  await this.triggerShutdownHookIsolating();
@@ -2459,6 +2483,20 @@ __export(qa_exports, {
2459
2483
  });
2460
2484
 
2461
2485
  // src/qa/runner.ts
2486
+ function describeActualType(value) {
2487
+ if (value === null) return "null";
2488
+ if (Array.isArray(value)) return "array";
2489
+ return typeof value;
2490
+ }
2491
+ function containsInapplicableHint(actual) {
2492
+ if (actual === void 0) {
2493
+ 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.";
2494
+ }
2495
+ if (actual === null) {
2496
+ return "The path resolved to null. Use 'is_null' if asserting absence is what you meant.";
2497
+ }
2498
+ 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.";
2499
+ }
2462
2500
  var TestRunner = class {
2463
2501
  constructor(adapter) {
2464
2502
  this.adapter = adapter;
@@ -2586,6 +2624,10 @@ var TestRunner = class {
2586
2624
  if (!actual.includes(expected)) throw new Error(`Assertion failed: ${assertion.field} array does not contain ${expected}`);
2587
2625
  } else if (typeof actual === "string") {
2588
2626
  if (!actual.includes(String(expected))) throw new Error(`Assertion failed: ${assertion.field} string does not contain ${expected}`);
2627
+ } else {
2628
+ throw new Error(
2629
+ `Assertion failed: ${assertion.field} cannot be evaluated by 'contains' \u2014 expected an array or a string at that path, got ${describeActualType(actual)}. ` + containsInapplicableHint(actual)
2630
+ );
2589
2631
  }
2590
2632
  break;
2591
2633
  case "not_null":
@@ -4466,7 +4508,116 @@ async function resolveLocalizationContext(input) {
4466
4508
  };
4467
4509
  }
4468
4510
 
4511
+ // src/security/assemble-execution-context.ts
4512
+ var ENTRY_EXECUTION_CONTEXT_FIELDS = [
4513
+ "positions",
4514
+ "permissions",
4515
+ "systemPermissions",
4516
+ "isSystem",
4517
+ "principalKind",
4518
+ "onBehalfOf",
4519
+ "audience",
4520
+ "userId",
4521
+ "tenantId",
4522
+ "email",
4523
+ "accessToken",
4524
+ "tabPermissions",
4525
+ "posture",
4526
+ "authGate",
4527
+ "org_user_ids",
4528
+ "accessible_org_ids",
4529
+ "oauthScopes",
4530
+ "timezone",
4531
+ "locale",
4532
+ "currency"
4533
+ ];
4534
+ function emit(fields) {
4535
+ const ctx = {};
4536
+ for (const key of ENTRY_EXECUTION_CONTEXT_FIELDS) {
4537
+ const value = fields[key];
4538
+ if (value !== void 0) ctx[key] = value;
4539
+ }
4540
+ return ctx;
4541
+ }
4542
+ function entryFields(input, anonymous) {
4543
+ const { authz, oauth, localization, requestLocale, accessToken, authGate } = input;
4544
+ const agent = !anonymous && oauth?.clientId ? oauth : void 0;
4545
+ return {
4546
+ // [ADR-0090 D9/D10] Principal taxonomy at the HTTP entry: a session-backed
4547
+ // request is a human principal; a sessionless one is a guest, holding the
4548
+ // built-in `guest` position implicitly and exclusively. Internal engine
4549
+ // calls that construct bare contexts never pass through here, so the
4550
+ // security plugin's empty-context skip path keeps its meaning.
4551
+ positions: agent ? [] : anonymous ? ["guest"] : authz.positions,
4552
+ permissions: agent ? agent.scopePermissions : authz.permissions,
4553
+ // [ADR-0090 D10] System capabilities on the agent principal gate business
4554
+ // ACTION invocation (`actionPermissionError` reads `ctx.systemPermissions`)
4555
+ // — a door SEPARATE from the object CRUD/FLS/RLS intersection, which is
4556
+ // driven by the resolved ceiling SETS (they carry no caps, so cap-gated
4557
+ // OBJECT access stays denied to the agent regardless of this line). The
4558
+ // `actions:execute` scope IS the user's consent to let this agent invoke
4559
+ // actions on their behalf; without it the agent holds none.
4560
+ systemPermissions: agent ? agent.delegatesActions ? authz.systemPermissions ?? [] : [] : authz.systemPermissions,
4561
+ isSystem: false,
4562
+ principalKind: agent ? "agent" : anonymous ? "guest" : "human",
4563
+ onBehalfOf: agent ? { userId: authz.userId, principalKind: "human" } : void 0,
4564
+ // [ADR-0090 D10/D11 — P1 shape] No transport resolves an external
4565
+ // (portal/partner) audience yet; `undefined` reads as 'internal'. Named
4566
+ // here rather than excluded so the gap is visible in the closed set instead
4567
+ // of being invisible outside it — when an external principal type lands,
4568
+ // this is the line that must change, on every face at once.
4569
+ audience: void 0,
4570
+ userId: authz.userId,
4571
+ tenantId: authz.tenantId,
4572
+ email: authz.email,
4573
+ accessToken,
4574
+ tabPermissions: authz.tabPermissions,
4575
+ // [ADR-0095 D2 / #2947] The derived posture rung, carried so every
4576
+ // transport presents enforcement the SAME value. Present only for an
4577
+ // authenticated principal (guest → absent).
4578
+ posture: authz.posture,
4579
+ // [ADR-0069 / #7280] The AUTHENTICATION-policy gate, carried for the seam
4580
+ // that reads it off the envelope (REST's `enforceAuth`). Anonymous → never:
4581
+ // a guest has no authenticated session for a policy gate to attach to, so
4582
+ // "gated guest" is not a state this entry can emit even if a face passed
4583
+ // one.
4584
+ authGate: anonymous ? void 0 : authGate,
4585
+ /** Fellow-org user IDs for RLS scoping of identity tables. */
4586
+ org_user_ids: authz.org_user_ids,
4587
+ // [ADR-0105 D2] The caller's org access set — the `group` posture's Layer 0
4588
+ // wall reads it directly, so every transport must carry it (#6206).
4589
+ accessible_org_ids: authz.accessible_org_ids,
4590
+ // OAuth provenance: surface the token's granted scopes so the MCP
4591
+ // dispatcher can narrow the exposed tool families (undefined for every
4592
+ // other provenance = not scope-limited).
4593
+ oauthScopes: oauth && authz.userId === oauth.userId ? oauth.scopes : void 0,
4594
+ // Anonymous → no localization (no scope to resolve against); the engine
4595
+ // default stands. [#3957] The request's OWN language preference wins over
4596
+ // the workspace default, so a rejection message is not rendered in English
4597
+ // beside the Chinese label of the very field it names.
4598
+ timezone: anonymous ? void 0 : localization?.timezone,
4599
+ locale: anonymous ? void 0 : requestLocale ?? localization?.locale,
4600
+ currency: anonymous ? void 0 : localization?.currency
4601
+ };
4602
+ }
4603
+ function assembleExecutionContext(input) {
4604
+ if (!input.authz.userId) return void 0;
4605
+ return emit(entryFields(input, false));
4606
+ }
4607
+ function assembleExecutionContextOrGuest(input) {
4608
+ return emit(entryFields(input, !input.authz.userId));
4609
+ }
4610
+
4469
4611
  // src/security/auth-gate.ts
4612
+ var DEFAULT_AUTH_GATE_MESSAGE = "Access is blocked by an authentication policy.";
4613
+ function normalizeAuthGate(sessionUser) {
4614
+ const gate = sessionUser?.authGate;
4615
+ if (!gate || typeof gate.code !== "string") return null;
4616
+ return {
4617
+ code: gate.code,
4618
+ message: typeof gate.message === "string" && gate.message ? gate.message : DEFAULT_AUTH_GATE_MESSAGE
4619
+ };
4620
+ }
4470
4621
  var ALLOW_PREFIXES = ["/api/v1/auth/", "/api/auth/", "/auth/"];
4471
4622
  var ALLOW_SUFFIXES = ["/health", "/ready", "/discovery", "/me/apps", "/me/localization"];
4472
4623
  function isAuthGateAllowlisted(rawPath) {
@@ -4485,13 +4636,10 @@ function isAuthGateAllowlisted(rawPath) {
4485
4636
  return false;
4486
4637
  }
4487
4638
  function evaluateAuthGate(sessionUser, path) {
4488
- const gate = sessionUser?.authGate;
4489
- if (!gate || typeof gate.code !== "string") return null;
4639
+ const gate = normalizeAuthGate(sessionUser);
4640
+ if (!gate) return null;
4490
4641
  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
- };
4642
+ return gate;
4495
4643
  }
4496
4644
 
4497
4645
  // src/security/anonymous-deny.ts
@@ -4513,6 +4661,17 @@ function shouldDenyAnonymous(input) {
4513
4661
  return true;
4514
4662
  }
4515
4663
 
4664
+ // src/security/operation-private-keys.ts
4665
+ var OPERATION_PRIVATE_KEY_PREFIX = "__";
4666
+ function withoutOperationPrivateKeys(exec) {
4667
+ const out = {};
4668
+ for (const [key, value] of Object.entries(exec)) {
4669
+ if (key.startsWith(OPERATION_PRIVATE_KEY_PREFIX)) continue;
4670
+ out[key] = value;
4671
+ }
4672
+ return out;
4673
+ }
4674
+
4516
4675
  // src/utils/datetime.ts
4517
4676
  var import_data = require("@objectstack/spec/data");
4518
4677
  function calendarPartsInTz(d, tz) {
@@ -6330,11 +6489,13 @@ var NamespaceResolver = class {
6330
6489
  API_KEY_PREFIX,
6331
6490
  CORE_FALLBACK_FACTORIES,
6332
6491
  DependencyResolver,
6492
+ ENTRY_EXECUTION_CONTEXT_FIELDS,
6333
6493
  HotReloadManager,
6334
6494
  LiteKernel,
6335
6495
  MigrationJournalRefusal,
6336
6496
  MigrationPlanRegistry,
6337
6497
  NamespaceResolver,
6498
+ OPERATION_PRIVATE_KEY_PREFIX,
6338
6499
  ObjectKernel,
6339
6500
  ObjectKernelBase,
6340
6501
  ObjectLogger,
@@ -6356,6 +6517,8 @@ var NamespaceResolver = class {
6356
6517
  ServiceLifecycle,
6357
6518
  UnknownFilterTokenError,
6358
6519
  UnresolvedFilterTokenError,
6520
+ assembleExecutionContext,
6521
+ assembleExecutionContextOrGuest,
6359
6522
  assertInitServiceRequirements,
6360
6523
  bucketKeyToCalendarRange,
6361
6524
  buildPermissionsFromGrants,
@@ -6392,6 +6555,7 @@ var NamespaceResolver = class {
6392
6555
  isGrantExpired,
6393
6556
  isNode,
6394
6557
  nextUtcCalendarDay,
6558
+ normalizeAuthGate,
6395
6559
  parseScopes,
6396
6560
  parseSignature,
6397
6561
  planChunks,
@@ -6419,6 +6583,7 @@ var NamespaceResolver = class {
6419
6583
  verifyPublisherSignature,
6420
6584
  wireAuthoredTranslationSync,
6421
6585
  withTransientRetry,
6586
+ withoutOperationPrivateKeys,
6422
6587
  zonedDateStartToUtcMs
6423
6588
  });
6424
6589
  //# sourceMappingURL=index.cjs.map