@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.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);
@@ -1729,11 +1752,12 @@ var ObjectKernel = class {
1729
1752
  }
1730
1753
  this.hooks.get(name).push(handler);
1731
1754
  },
1755
+ // PROPAGATING dispatch — the same shared loop `LiteKernel`'s
1756
+ // context.trigger runs, and deliberately WITHOUT a trace line:
1757
+ // `context.trigger` has never emitted one on either kernel, so no
1758
+ // logger is handed over (#5282).
1732
1759
  trigger: async (name, ...args) => {
1733
- const handlers = this.hooks.get(name) || [];
1734
- for (const handler of handlers) {
1735
- await handler(...args);
1736
- }
1760
+ await dispatchHookPropagating(name, this.hooks.get(name) || [], void 0, args);
1737
1761
  },
1738
1762
  getServices: () => {
1739
1763
  return new Map(this.services);
@@ -2157,27 +2181,21 @@ var ObjectKernel = class {
2157
2181
  * one bad handler must not amplify into leaked resources and unflushed
2158
2182
  * writes. Same reasoning, same wording, same `Hook handler failed:
2159
2183
  * kernel:shutdown` log line as `LiteKernel`'s dispatch site, which reaches
2160
- * the shared isolating dispatcher `ObjectKernelBase.triggerHook` (#5257).
2184
+ * the isolating dispatcher through `ObjectKernelBase.triggerHook` (#5257).
2161
2185
  *
2162
- * `ObjectKernel` cannot call that dispatcher: it does not extend
2186
+ * Until #5282 "same wording" was literally that the loop was typed out a
2187
+ * second time here, because `ObjectKernel` does not extend
2163
2188
  * `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.
2189
+ * so the base's `protected triggerHook` is out of reach. The loop now lives
2190
+ * in {@link dispatchHookIsolating}, which BOTH sides call: the storage is
2191
+ * still two maps (deliberately unifying it was out of #5282's scope), but
2192
+ * "isolating" is one implementation, so it can no longer drift on one
2193
+ * kernel while the other keeps the old shape. That drift is exactly the bug
2194
+ * #5170 / #5257 / #5274 each closed one hook at a time, and the paired-pin
2195
+ * gate (`scripts/check-kernel-hook-pairs.mjs`) covers the residue.
2167
2196
  */
2168
2197
  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
- }
2198
+ await dispatchHookIsolating("kernel:shutdown", this.hooks.get("kernel:shutdown") || [], this.logger);
2181
2199
  }
2182
2200
  async performShutdown() {
2183
2201
  await this.triggerShutdownHookIsolating();
@@ -2345,6 +2363,20 @@ __export(qa_exports, {
2345
2363
  });
2346
2364
 
2347
2365
  // src/qa/runner.ts
2366
+ function describeActualType(value) {
2367
+ if (value === null) return "null";
2368
+ if (Array.isArray(value)) return "array";
2369
+ return typeof value;
2370
+ }
2371
+ function containsInapplicableHint(actual) {
2372
+ if (actual === void 0) {
2373
+ 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.";
2374
+ }
2375
+ if (actual === null) {
2376
+ return "The path resolved to null. Use 'is_null' if asserting absence is what you meant.";
2377
+ }
2378
+ 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.";
2379
+ }
2348
2380
  var TestRunner = class {
2349
2381
  constructor(adapter) {
2350
2382
  this.adapter = adapter;
@@ -2472,6 +2504,10 @@ var TestRunner = class {
2472
2504
  if (!actual.includes(expected)) throw new Error(`Assertion failed: ${assertion.field} array does not contain ${expected}`);
2473
2505
  } else if (typeof actual === "string") {
2474
2506
  if (!actual.includes(String(expected))) throw new Error(`Assertion failed: ${assertion.field} string does not contain ${expected}`);
2507
+ } else {
2508
+ throw new Error(
2509
+ `Assertion failed: ${assertion.field} cannot be evaluated by 'contains' \u2014 expected an array or a string at that path, got ${describeActualType(actual)}. ` + containsInapplicableHint(actual)
2510
+ );
2475
2511
  }
2476
2512
  break;
2477
2513
  case "not_null":
@@ -4357,7 +4393,116 @@ async function resolveLocalizationContext(input) {
4357
4393
  };
4358
4394
  }
4359
4395
 
4396
+ // src/security/assemble-execution-context.ts
4397
+ var ENTRY_EXECUTION_CONTEXT_FIELDS = [
4398
+ "positions",
4399
+ "permissions",
4400
+ "systemPermissions",
4401
+ "isSystem",
4402
+ "principalKind",
4403
+ "onBehalfOf",
4404
+ "audience",
4405
+ "userId",
4406
+ "tenantId",
4407
+ "email",
4408
+ "accessToken",
4409
+ "tabPermissions",
4410
+ "posture",
4411
+ "authGate",
4412
+ "org_user_ids",
4413
+ "accessible_org_ids",
4414
+ "oauthScopes",
4415
+ "timezone",
4416
+ "locale",
4417
+ "currency"
4418
+ ];
4419
+ function emit(fields) {
4420
+ const ctx = {};
4421
+ for (const key of ENTRY_EXECUTION_CONTEXT_FIELDS) {
4422
+ const value = fields[key];
4423
+ if (value !== void 0) ctx[key] = value;
4424
+ }
4425
+ return ctx;
4426
+ }
4427
+ function entryFields(input, anonymous) {
4428
+ const { authz, oauth, localization, requestLocale, accessToken, authGate } = input;
4429
+ const agent = !anonymous && oauth?.clientId ? oauth : void 0;
4430
+ return {
4431
+ // [ADR-0090 D9/D10] Principal taxonomy at the HTTP entry: a session-backed
4432
+ // request is a human principal; a sessionless one is a guest, holding the
4433
+ // built-in `guest` position implicitly and exclusively. Internal engine
4434
+ // calls that construct bare contexts never pass through here, so the
4435
+ // security plugin's empty-context skip path keeps its meaning.
4436
+ positions: agent ? [] : anonymous ? ["guest"] : authz.positions,
4437
+ permissions: agent ? agent.scopePermissions : authz.permissions,
4438
+ // [ADR-0090 D10] System capabilities on the agent principal gate business
4439
+ // ACTION invocation (`actionPermissionError` reads `ctx.systemPermissions`)
4440
+ // — a door SEPARATE from the object CRUD/FLS/RLS intersection, which is
4441
+ // driven by the resolved ceiling SETS (they carry no caps, so cap-gated
4442
+ // OBJECT access stays denied to the agent regardless of this line). The
4443
+ // `actions:execute` scope IS the user's consent to let this agent invoke
4444
+ // actions on their behalf; without it the agent holds none.
4445
+ systemPermissions: agent ? agent.delegatesActions ? authz.systemPermissions ?? [] : [] : authz.systemPermissions,
4446
+ isSystem: false,
4447
+ principalKind: agent ? "agent" : anonymous ? "guest" : "human",
4448
+ onBehalfOf: agent ? { userId: authz.userId, principalKind: "human" } : void 0,
4449
+ // [ADR-0090 D10/D11 — P1 shape] No transport resolves an external
4450
+ // (portal/partner) audience yet; `undefined` reads as 'internal'. Named
4451
+ // here rather than excluded so the gap is visible in the closed set instead
4452
+ // of being invisible outside it — when an external principal type lands,
4453
+ // this is the line that must change, on every face at once.
4454
+ audience: void 0,
4455
+ userId: authz.userId,
4456
+ tenantId: authz.tenantId,
4457
+ email: authz.email,
4458
+ accessToken,
4459
+ tabPermissions: authz.tabPermissions,
4460
+ // [ADR-0095 D2 / #2947] The derived posture rung, carried so every
4461
+ // transport presents enforcement the SAME value. Present only for an
4462
+ // authenticated principal (guest → absent).
4463
+ posture: authz.posture,
4464
+ // [ADR-0069 / #7280] The AUTHENTICATION-policy gate, carried for the seam
4465
+ // that reads it off the envelope (REST's `enforceAuth`). Anonymous → never:
4466
+ // a guest has no authenticated session for a policy gate to attach to, so
4467
+ // "gated guest" is not a state this entry can emit even if a face passed
4468
+ // one.
4469
+ authGate: anonymous ? void 0 : authGate,
4470
+ /** Fellow-org user IDs for RLS scoping of identity tables. */
4471
+ org_user_ids: authz.org_user_ids,
4472
+ // [ADR-0105 D2] The caller's org access set — the `group` posture's Layer 0
4473
+ // wall reads it directly, so every transport must carry it (#6206).
4474
+ accessible_org_ids: authz.accessible_org_ids,
4475
+ // OAuth provenance: surface the token's granted scopes so the MCP
4476
+ // dispatcher can narrow the exposed tool families (undefined for every
4477
+ // other provenance = not scope-limited).
4478
+ oauthScopes: oauth && authz.userId === oauth.userId ? oauth.scopes : void 0,
4479
+ // Anonymous → no localization (no scope to resolve against); the engine
4480
+ // default stands. [#3957] The request's OWN language preference wins over
4481
+ // the workspace default, so a rejection message is not rendered in English
4482
+ // beside the Chinese label of the very field it names.
4483
+ timezone: anonymous ? void 0 : localization?.timezone,
4484
+ locale: anonymous ? void 0 : requestLocale ?? localization?.locale,
4485
+ currency: anonymous ? void 0 : localization?.currency
4486
+ };
4487
+ }
4488
+ function assembleExecutionContext(input) {
4489
+ if (!input.authz.userId) return void 0;
4490
+ return emit(entryFields(input, false));
4491
+ }
4492
+ function assembleExecutionContextOrGuest(input) {
4493
+ return emit(entryFields(input, !input.authz.userId));
4494
+ }
4495
+
4360
4496
  // src/security/auth-gate.ts
4497
+ var DEFAULT_AUTH_GATE_MESSAGE = "Access is blocked by an authentication policy.";
4498
+ function normalizeAuthGate(sessionUser) {
4499
+ const gate = sessionUser?.authGate;
4500
+ if (!gate || typeof gate.code !== "string") return null;
4501
+ return {
4502
+ code: gate.code,
4503
+ message: typeof gate.message === "string" && gate.message ? gate.message : DEFAULT_AUTH_GATE_MESSAGE
4504
+ };
4505
+ }
4361
4506
  var ALLOW_PREFIXES = ["/api/v1/auth/", "/api/auth/", "/auth/"];
4362
4507
  var ALLOW_SUFFIXES = ["/health", "/ready", "/discovery", "/me/apps", "/me/localization"];
4363
4508
  function isAuthGateAllowlisted(rawPath) {
@@ -4376,13 +4521,10 @@ function isAuthGateAllowlisted(rawPath) {
4376
4521
  return false;
4377
4522
  }
4378
4523
  function evaluateAuthGate(sessionUser, path) {
4379
- const gate = sessionUser?.authGate;
4380
- if (!gate || typeof gate.code !== "string") return null;
4524
+ const gate = normalizeAuthGate(sessionUser);
4525
+ if (!gate) return null;
4381
4526
  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
- };
4527
+ return gate;
4386
4528
  }
4387
4529
 
4388
4530
  // src/security/anonymous-deny.ts
@@ -4404,6 +4546,17 @@ function shouldDenyAnonymous(input) {
4404
4546
  return true;
4405
4547
  }
4406
4548
 
4549
+ // src/security/operation-private-keys.ts
4550
+ var OPERATION_PRIVATE_KEY_PREFIX = "__";
4551
+ function withoutOperationPrivateKeys(exec) {
4552
+ const out = {};
4553
+ for (const [key, value] of Object.entries(exec)) {
4554
+ if (key.startsWith(OPERATION_PRIVATE_KEY_PREFIX)) continue;
4555
+ out[key] = value;
4556
+ }
4557
+ return out;
4558
+ }
4559
+
4407
4560
  // src/utils/datetime.ts
4408
4561
  import { nextUtcCalendarDay, utcInstantMs } from "@objectstack/spec/data";
4409
4562
  function calendarPartsInTz(d, tz) {
@@ -6225,11 +6378,13 @@ export {
6225
6378
  API_KEY_PREFIX,
6226
6379
  CORE_FALLBACK_FACTORIES,
6227
6380
  DependencyResolver,
6381
+ ENTRY_EXECUTION_CONTEXT_FIELDS,
6228
6382
  HotReloadManager,
6229
6383
  LiteKernel,
6230
6384
  MigrationJournalRefusal,
6231
6385
  MigrationPlanRegistry,
6232
6386
  NamespaceResolver,
6387
+ OPERATION_PRIVATE_KEY_PREFIX,
6233
6388
  ObjectKernel,
6234
6389
  ObjectKernelBase,
6235
6390
  ObjectLogger,
@@ -6251,6 +6406,8 @@ export {
6251
6406
  ServiceLifecycle,
6252
6407
  UnknownFilterTokenError,
6253
6408
  UnresolvedFilterTokenError,
6409
+ assembleExecutionContext,
6410
+ assembleExecutionContextOrGuest,
6254
6411
  assertInitServiceRequirements,
6255
6412
  bucketKeyToCalendarRange,
6256
6413
  buildPermissionsFromGrants,
@@ -6287,6 +6444,7 @@ export {
6287
6444
  isGrantExpired,
6288
6445
  isNode,
6289
6446
  nextUtcCalendarDay,
6447
+ normalizeAuthGate,
6290
6448
  parseScopes,
6291
6449
  parseSignature,
6292
6450
  planChunks,
@@ -6314,6 +6472,7 @@ export {
6314
6472
  verifyPublisherSignature,
6315
6473
  wireAuthoredTranslationSync,
6316
6474
  withTransientRetry,
6475
+ withoutOperationPrivateKeys,
6317
6476
  zonedDateStartToUtcMs
6318
6477
  };
6319
6478
  //# sourceMappingURL=index.js.map