@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/CHANGELOG.md +3293 -0
- package/dist/index.cjs +370 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +588 -43
- package/dist/index.d.ts +588 -43
- package/dist/index.js +346 -63
- package/dist/index.js.map +1 -1
- package/dist/logger.cjs +9 -1
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +9 -1
- package/dist/logger.js.map +1 -1
- package/package.json +3 -3
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1551
|
+
const canonical = canonicalMetadataServiceType(type);
|
|
1552
|
+
let map = store.get(canonical);
|
|
1480
1553
|
if (!map) {
|
|
1481
1554
|
map = /* @__PURE__ */ new Map();
|
|
1482
|
-
store.set(
|
|
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
|
|
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
|
-
|
|
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
|
|
2239
|
+
* the isolating dispatcher through `ObjectKernelBase.triggerHook` (#5257).
|
|
2161
2240
|
*
|
|
2162
|
-
*
|
|
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
|
|
2165
|
-
*
|
|
2166
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
|
2644
|
+
const { id, ...fields } = data;
|
|
2536
2645
|
if (!id) throw new Error("Update record requires id in payload");
|
|
2537
|
-
const response = await fetch(
|
|
2538
|
-
method: "
|
|
2646
|
+
const response = await fetch(this.recordUrl(objectName, id), {
|
|
2647
|
+
method: "PATCH",
|
|
2539
2648
|
headers,
|
|
2540
|
-
body: JSON.stringify(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
4380
|
-
if (!gate
|
|
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
|