@objectstack/core 17.0.0-rc.6 → 17.1.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 +3361 -0
- package/dist/index.cjs +521 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +584 -15
- package/dist/index.d.ts +584 -15
- package/dist/index.js +492 -45
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1385,6 +1385,7 @@ function createMemoryJob() {
|
|
|
1385
1385
|
}
|
|
1386
1386
|
|
|
1387
1387
|
// src/fallbacks/memory-i18n.ts
|
|
1388
|
+
import { normalizeSupportedLocales } from "@objectstack/spec/system";
|
|
1388
1389
|
function deepMerge(target, source) {
|
|
1389
1390
|
const result = { ...target };
|
|
1390
1391
|
for (const key of Object.keys(source)) {
|
|
@@ -1418,6 +1419,7 @@ function createMemoryI18n() {
|
|
|
1418
1419
|
const translations = /* @__PURE__ */ new Map();
|
|
1419
1420
|
const authored = /* @__PURE__ */ new Map();
|
|
1420
1421
|
let defaultLocale = "en";
|
|
1422
|
+
let supportedLocales;
|
|
1421
1423
|
function resolveKey(data, key) {
|
|
1422
1424
|
const parts = key.split(".");
|
|
1423
1425
|
let current = data;
|
|
@@ -1483,9 +1485,29 @@ function createMemoryI18n() {
|
|
|
1483
1485
|
authored.set(locale, { ...data });
|
|
1484
1486
|
}
|
|
1485
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
|
+
*/
|
|
1486
1503
|
getLocales() {
|
|
1504
|
+
if (supportedLocales) return [...supportedLocales];
|
|
1487
1505
|
return [.../* @__PURE__ */ new Set([...translations.keys(), ...authored.keys()])];
|
|
1488
1506
|
},
|
|
1507
|
+
/** @see II18nService.setSupportedLocales — [#7679] */
|
|
1508
|
+
setSupportedLocales(locales) {
|
|
1509
|
+
supportedLocales = normalizeSupportedLocales(locales);
|
|
1510
|
+
},
|
|
1489
1511
|
getDefaultLocale() {
|
|
1490
1512
|
return defaultLocale;
|
|
1491
1513
|
},
|
|
@@ -1495,14 +1517,42 @@ function createMemoryI18n() {
|
|
|
1495
1517
|
};
|
|
1496
1518
|
}
|
|
1497
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
|
+
|
|
1498
1547
|
// src/fallbacks/memory-metadata.ts
|
|
1499
1548
|
function createMemoryMetadata() {
|
|
1500
1549
|
const store = /* @__PURE__ */ new Map();
|
|
1501
1550
|
function getTypeMap(type) {
|
|
1502
|
-
|
|
1551
|
+
const canonical = canonicalMetadataServiceType(type);
|
|
1552
|
+
let map = store.get(canonical);
|
|
1503
1553
|
if (!map) {
|
|
1504
1554
|
map = /* @__PURE__ */ new Map();
|
|
1505
|
-
store.set(
|
|
1555
|
+
store.set(canonical, map);
|
|
1506
1556
|
}
|
|
1507
1557
|
return map;
|
|
1508
1558
|
}
|
|
@@ -1517,6 +1567,7 @@ function createMemoryMetadata() {
|
|
|
1517
1567
|
},
|
|
1518
1568
|
_serviceName: "metadata",
|
|
1519
1569
|
async register(type, name, data) {
|
|
1570
|
+
assertMetadataRegisterContract(type, name, data);
|
|
1520
1571
|
getTypeMap(type).set(name, data);
|
|
1521
1572
|
},
|
|
1522
1573
|
// Mirror MetadataManager.registerInMemory (synchronous, no persistence).
|
|
@@ -1528,7 +1579,11 @@ function createMemoryMetadata() {
|
|
|
1528
1579
|
// so `defineStack({ datasources })` entries silently never reached the
|
|
1529
1580
|
// registry and were absent from GET /api/v1/datasources and
|
|
1530
1581
|
// GET /api/v1/meta/datasource (ADR-0015 §18). This store is already
|
|
1531
|
-
// 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.
|
|
1532
1587
|
registerInMemory(type, name, data) {
|
|
1533
1588
|
getTypeMap(type).set(name, data);
|
|
1534
1589
|
},
|
|
@@ -1692,6 +1747,26 @@ var CORE_FALLBACK_FACTORIES = {
|
|
|
1692
1747
|
i18n: createMemoryI18n
|
|
1693
1748
|
};
|
|
1694
1749
|
|
|
1750
|
+
// src/plugin-registration.ts
|
|
1751
|
+
function versionLabel(plugin) {
|
|
1752
|
+
return plugin.version ? `v${plugin.version}` : "unversioned";
|
|
1753
|
+
}
|
|
1754
|
+
function describeSupersededRegistration(previous, next) {
|
|
1755
|
+
return `Plugin superseded: '${next.name}' \u2014 the later registration (${versionLabel(next)}) REPLACED the earlier one (${versionLabel(previous)}). Only the later instance is initialized and started; the earlier one is discarded without ever running init(). Duplicate registration by name is last-one-wins on both kernels by declared contract (#9864) \u2014 register the plugin once if that is not what you meant.`;
|
|
1756
|
+
}
|
|
1757
|
+
function registerPluginByName(registry, plugin, logger) {
|
|
1758
|
+
const previous = registry.get(plugin.name);
|
|
1759
|
+
if (previous !== void 0) {
|
|
1760
|
+
logger.warn(describeSupersededRegistration(previous, plugin), {
|
|
1761
|
+
plugin: plugin.name,
|
|
1762
|
+
supersededVersion: previous.version,
|
|
1763
|
+
supersedingVersion: plugin.version
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
registry.set(plugin.name, plugin);
|
|
1767
|
+
return previous;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1695
1770
|
// src/kernel.ts
|
|
1696
1771
|
var ObjectKernel = class {
|
|
1697
1772
|
constructor(config = {}) {
|
|
@@ -1776,6 +1851,14 @@ var ObjectKernel = class {
|
|
|
1776
1851
|
}
|
|
1777
1852
|
/**
|
|
1778
1853
|
* Register a plugin with enhanced validation
|
|
1854
|
+
*
|
|
1855
|
+
* Duplicate names OVERWRITE, with one `warn` naming both versions — the
|
|
1856
|
+
* declared contract in `plugin-registration.ts`, applied identically by
|
|
1857
|
+
* `LiteKernel.use()` (#9864, maintainer ruling 2026-08-19). The overwrite
|
|
1858
|
+
* itself is unchanged: it is what lets an app config's `plugins` entry
|
|
1859
|
+
* supersede a plugin the CLI auto-registered earlier in the same boot
|
|
1860
|
+
* (#9863). What changes is that it is no longer silent, and no longer
|
|
1861
|
+
* disagrees with the other kernel.
|
|
1779
1862
|
*/
|
|
1780
1863
|
async use(plugin) {
|
|
1781
1864
|
if (this.state !== "idle") {
|
|
@@ -1786,11 +1869,13 @@ var ObjectKernel = class {
|
|
|
1786
1869
|
throw new Error(`Failed to load plugin: ${plugin.name} - ${result.error?.message}`);
|
|
1787
1870
|
}
|
|
1788
1871
|
const pluginMeta = result.plugin;
|
|
1789
|
-
this.plugins
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1872
|
+
const superseded = registerPluginByName(this.plugins, pluginMeta, this.logger);
|
|
1873
|
+
if (superseded === void 0) {
|
|
1874
|
+
this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
|
|
1875
|
+
plugin: pluginMeta.name,
|
|
1876
|
+
version: pluginMeta.version
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1794
1879
|
return this;
|
|
1795
1880
|
}
|
|
1796
1881
|
/**
|
|
@@ -2268,14 +2353,21 @@ var LiteKernel = class extends ObjectKernelBase {
|
|
|
2268
2353
|
/**
|
|
2269
2354
|
* Register a plugin
|
|
2270
2355
|
* @param plugin - Plugin instance
|
|
2356
|
+
*
|
|
2357
|
+
* Duplicate names OVERWRITE, with one `warn` naming both versions — the
|
|
2358
|
+
* declared contract in `plugin-registration.ts`, applied identically by
|
|
2359
|
+
* `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
|
|
2360
|
+
*
|
|
2361
|
+
* This method used to `throw` `[Kernel] Plugin '<name>' already
|
|
2362
|
+
* registered` here while `ObjectKernel` overwrote silently, so one input
|
|
2363
|
+
* had two meanings depending on which kernel was running — and the kernel
|
|
2364
|
+
* that runs in production was the silent one. The ruling converged them on
|
|
2365
|
+
* the behaviour that already works (an app config superseding a plugin the
|
|
2366
|
+
* CLI auto-registered, #9863) and made it audible rather than removing it.
|
|
2271
2367
|
*/
|
|
2272
2368
|
use(plugin) {
|
|
2273
2369
|
this.validateIdle();
|
|
2274
|
-
|
|
2275
|
-
if (this.plugins.has(pluginName)) {
|
|
2276
|
-
throw new Error(`[Kernel] Plugin '${pluginName}' already registered`);
|
|
2277
|
-
}
|
|
2278
|
-
this.plugins.set(pluginName, plugin);
|
|
2370
|
+
registerPluginByName(this.plugins, plugin, this.logger);
|
|
2279
2371
|
return this;
|
|
2280
2372
|
}
|
|
2281
2373
|
/**
|
|
@@ -2524,11 +2616,119 @@ var TestRunner = class {
|
|
|
2524
2616
|
};
|
|
2525
2617
|
|
|
2526
2618
|
// src/qa/http-adapter.ts
|
|
2619
|
+
import { RestApiConfigSchema, CrudEndpointsConfigSchema } from "@objectstack/spec/api";
|
|
2620
|
+
var conventionCache;
|
|
2621
|
+
function conventionMounts() {
|
|
2622
|
+
if (conventionCache === void 0) {
|
|
2623
|
+
const api = RestApiConfigSchema.parse({});
|
|
2624
|
+
const crud = CrudEndpointsConfigSchema.parse({});
|
|
2625
|
+
const apiBase = api.apiPath ?? `${api.basePath}/${api.version}`;
|
|
2626
|
+
conventionCache = { apiBase, dataPath: `${apiBase}${crud.dataPrefix}` };
|
|
2627
|
+
}
|
|
2628
|
+
return conventionCache;
|
|
2629
|
+
}
|
|
2527
2630
|
var HttpTestAdapter = class {
|
|
2528
2631
|
constructor(baseUrl, authToken) {
|
|
2529
2632
|
this.baseUrl = baseUrl;
|
|
2530
2633
|
this.authToken = authToken;
|
|
2531
2634
|
}
|
|
2635
|
+
/** The resolved data mount; probes at most once per adapter. */
|
|
2636
|
+
dataMount() {
|
|
2637
|
+
if (this.mountPromise === void 0) {
|
|
2638
|
+
this.mountPromise = this.resolveDataMount();
|
|
2639
|
+
}
|
|
2640
|
+
return this.mountPromise;
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Ask the server where it serves the Data Protocol, and fall back to the
|
|
2644
|
+
* convention — loudly — when it cannot say.
|
|
2645
|
+
*
|
|
2646
|
+
* ## [#7983] What the probe recovers, measured rather than assumed
|
|
2647
|
+
*
|
|
2648
|
+
* `@objectstack/client` answers the same question through discovery
|
|
2649
|
+
* (`getRoute`, `packages/client/src/index.ts`), and this follows it: prefer
|
|
2650
|
+
* the server's own `routes.data`, fall back to the convention. Measured on a
|
|
2651
|
+
* booted stack (REST generator + dispatcher bridge, three configs):
|
|
2652
|
+
*
|
|
2653
|
+
* | deployment | `{apiBase}/discovery` | serves |
|
|
2654
|
+
* |--------------------------------|-----------------------|---------------|
|
|
2655
|
+
* | stock | 200 `/api/v1/data` | `/api/v1/data`|
|
|
2656
|
+
* | `crud.dataPrefix: '/objects'` | 200 `/api/v1/objects` | `/api/v1/objects` |
|
|
2657
|
+
* | `api.apiPath: '/api/2026-01'` | **404** | `/api/2026-01/data` |
|
|
2658
|
+
*
|
|
2659
|
+
* So the probe closes the `dataPrefix` row exactly: `RestServer`'s discovery
|
|
2660
|
+
* handler substitutes the configured prefix into `routes.data`, and reading
|
|
2661
|
+
* it is strictly better than recomputing it here. The `apiPath` row it cannot
|
|
2662
|
+
* close, and the reason is structural rather than an oversight — `apiPath`
|
|
2663
|
+
* moves the base that discovery itself is mounted under, so the document that
|
|
2664
|
+
* would name the new mount is behind the very prefix we are missing.
|
|
2665
|
+
*
|
|
2666
|
+
* ⛔ And the one discovery document at a FIXED path does not rescue it:
|
|
2667
|
+
* `/.well-known/objectstack` is mounted at the site root by the dispatcher
|
|
2668
|
+
* bridge, but its `routes.data` is the DISPATCHER's own `${prefix}/data` —
|
|
2669
|
+
* measured as `/api/v1/data` under all three configs above, including the two
|
|
2670
|
+
* where the server serves elsewhere. Falling back to it would turn "we could
|
|
2671
|
+
* not resolve the mount" into "discovery told us `/api/v1/data`": the same
|
|
2672
|
+
* 404, now with a false provenance attached. Not probed, deliberately.
|
|
2673
|
+
*
|
|
2674
|
+
* Hence: one probe, then a diagnostic that NAMES the mount, the evidence and
|
|
2675
|
+
* the remedy. `api_call` takes the path it is given and is unaffected either
|
|
2676
|
+
* way — it stays the escape hatch for a host this cannot reach.
|
|
2677
|
+
*/
|
|
2678
|
+
async resolveDataMount() {
|
|
2679
|
+
const { apiBase, dataPath } = conventionMounts();
|
|
2680
|
+
const probeUrl = `${this.baseUrl}${apiBase}/discovery`;
|
|
2681
|
+
let why;
|
|
2682
|
+
try {
|
|
2683
|
+
const headers = {};
|
|
2684
|
+
if (this.authToken) {
|
|
2685
|
+
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
2686
|
+
}
|
|
2687
|
+
const response = await fetch(probeUrl, { method: "GET", headers });
|
|
2688
|
+
if (response.ok) {
|
|
2689
|
+
const body = await response.json();
|
|
2690
|
+
const doc = body && typeof body === "object" && "routes" in body ? body : body?.data;
|
|
2691
|
+
const advertised = doc?.routes?.data;
|
|
2692
|
+
if (typeof advertised === "string" && advertised.length > 0) {
|
|
2693
|
+
return {
|
|
2694
|
+
path: advertised,
|
|
2695
|
+
source: "discovery",
|
|
2696
|
+
why: `GET ${probeUrl} advertised routes.data`
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
why = `GET ${probeUrl} answered ${response.status} but carried no routes.data`;
|
|
2700
|
+
} else {
|
|
2701
|
+
why = `GET ${probeUrl} answered ${response.status}`;
|
|
2702
|
+
}
|
|
2703
|
+
} catch (error) {
|
|
2704
|
+
why = `GET ${probeUrl} could not be reached (${error.message})`;
|
|
2705
|
+
}
|
|
2706
|
+
const mount = { path: dataPath, source: "convention", why };
|
|
2707
|
+
console.warn(
|
|
2708
|
+
`[HttpTestAdapter] Data Protocol mount NOT resolved from discovery. Record actions (create_record, read_record, update_record, delete_record, query_records) will address ${this.baseUrl}${dataPath}, the convention declared by RestApiConfigSchema (apiPath ?? basePath/version) + CrudEndpointsConfigSchema.dataPrefix. Evidence: ${why}. A deployment that sets crud.dataPrefix is normally recovered by this probe; one that sets api.apiPath moves discovery itself out from under it, and no fixed-path document reports the REST mount \u2014 write those steps as api_call, which takes the path you give it.`
|
|
2709
|
+
);
|
|
2710
|
+
return mount;
|
|
2711
|
+
}
|
|
2712
|
+
/** `{baseUrl}{dataMount}/{object}` — the collection URL. */
|
|
2713
|
+
collectionUrl(mount, objectName) {
|
|
2714
|
+
return `${this.baseUrl}${mount.path}/${encodeURIComponent(objectName)}`;
|
|
2715
|
+
}
|
|
2716
|
+
/** `{collection}/{id}` — the single-record URL. */
|
|
2717
|
+
recordUrl(mount, objectName, id) {
|
|
2718
|
+
return `${this.collectionUrl(mount, objectName)}/${encodeURIComponent(String(id))}`;
|
|
2719
|
+
}
|
|
2720
|
+
/**
|
|
2721
|
+
* The provenance clause appended to a failed record action's error.
|
|
2722
|
+
*
|
|
2723
|
+
* The card this closes is about a 404 that reads like the author's own URL
|
|
2724
|
+
* mistake; the mount is the one fact that distinguishes the two, so it rides
|
|
2725
|
+
* on the failure itself rather than only on a warning printed earlier in the
|
|
2726
|
+
* transcript.
|
|
2727
|
+
*/
|
|
2728
|
+
mountNote(mount) {
|
|
2729
|
+
const how = mount.source === "discovery" ? "resolved from discovery" : "assumed by convention \u2014 the server was not able to state it";
|
|
2730
|
+
return `record actions addressed ${this.baseUrl}${mount.path} (data mount ${how}: ${mount.why})`;
|
|
2731
|
+
}
|
|
2532
2732
|
async execute(action, _context) {
|
|
2533
2733
|
const headers = {
|
|
2534
2734
|
"Content-Type": "application/json"
|
|
@@ -2560,48 +2760,53 @@ var HttpTestAdapter = class {
|
|
|
2560
2760
|
}
|
|
2561
2761
|
}
|
|
2562
2762
|
async createRecord(objectName, data, headers) {
|
|
2563
|
-
const
|
|
2763
|
+
const mount = await this.dataMount();
|
|
2764
|
+
const response = await fetch(this.collectionUrl(mount, objectName), {
|
|
2564
2765
|
method: "POST",
|
|
2565
2766
|
headers,
|
|
2566
2767
|
body: JSON.stringify(data)
|
|
2567
2768
|
});
|
|
2568
|
-
return this.handleResponse(response);
|
|
2769
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2569
2770
|
}
|
|
2570
2771
|
async updateRecord(objectName, data, headers) {
|
|
2571
|
-
const id = data
|
|
2772
|
+
const { id, ...fields } = data;
|
|
2572
2773
|
if (!id) throw new Error("Update record requires id in payload");
|
|
2573
|
-
const
|
|
2574
|
-
|
|
2774
|
+
const mount = await this.dataMount();
|
|
2775
|
+
const response = await fetch(this.recordUrl(mount, objectName, id), {
|
|
2776
|
+
method: "PATCH",
|
|
2575
2777
|
headers,
|
|
2576
|
-
body: JSON.stringify(
|
|
2778
|
+
body: JSON.stringify(fields)
|
|
2577
2779
|
});
|
|
2578
|
-
return this.handleResponse(response);
|
|
2780
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2579
2781
|
}
|
|
2580
2782
|
async deleteRecord(objectName, data, headers) {
|
|
2581
2783
|
const id = data.id;
|
|
2582
2784
|
if (!id) throw new Error("Delete record requires id in payload");
|
|
2583
|
-
const
|
|
2785
|
+
const mount = await this.dataMount();
|
|
2786
|
+
const response = await fetch(this.recordUrl(mount, objectName, id), {
|
|
2584
2787
|
method: "DELETE",
|
|
2585
2788
|
headers
|
|
2586
2789
|
});
|
|
2587
|
-
return this.handleResponse(response);
|
|
2790
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2588
2791
|
}
|
|
2589
2792
|
async readRecord(objectName, data, headers) {
|
|
2590
2793
|
const id = data.id;
|
|
2591
2794
|
if (!id) throw new Error("Read record requires id in payload");
|
|
2592
|
-
const
|
|
2795
|
+
const mount = await this.dataMount();
|
|
2796
|
+
const response = await fetch(this.recordUrl(mount, objectName, id), {
|
|
2593
2797
|
method: "GET",
|
|
2594
2798
|
headers
|
|
2595
2799
|
});
|
|
2596
|
-
return this.handleResponse(response);
|
|
2800
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2597
2801
|
}
|
|
2598
2802
|
async queryRecords(objectName, data, headers) {
|
|
2599
|
-
const
|
|
2803
|
+
const mount = await this.dataMount();
|
|
2804
|
+
const response = await fetch(`${this.collectionUrl(mount, objectName)}/query`, {
|
|
2600
2805
|
method: "POST",
|
|
2601
2806
|
headers,
|
|
2602
2807
|
body: JSON.stringify(data)
|
|
2603
2808
|
});
|
|
2604
|
-
return this.handleResponse(response);
|
|
2809
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2605
2810
|
}
|
|
2606
2811
|
async rawApiCall(endpoint, data, headers) {
|
|
2607
2812
|
const method = data.method || "GET";
|
|
@@ -2614,10 +2819,13 @@ var HttpTestAdapter = class {
|
|
|
2614
2819
|
});
|
|
2615
2820
|
return this.handleResponse(response);
|
|
2616
2821
|
}
|
|
2617
|
-
async handleResponse(response) {
|
|
2822
|
+
async handleResponse(response, mountNote) {
|
|
2618
2823
|
if (!response.ok) {
|
|
2619
2824
|
const text = await response.text();
|
|
2620
|
-
|
|
2825
|
+
const wrongUrlShaped = response.status === 404 || response.status === 405;
|
|
2826
|
+
throw new Error(
|
|
2827
|
+
`HTTP Error ${response.status}: ${text}${wrongUrlShaped && mountNote ? ` \u2014 ${mountNote}` : ""}`
|
|
2828
|
+
);
|
|
2621
2829
|
}
|
|
2622
2830
|
const contentType = response.headers.get("content-type");
|
|
2623
2831
|
if (contentType && contentType.includes("application/json")) {
|
|
@@ -3959,6 +4167,7 @@ var PluginSecurityScanner = class {
|
|
|
3959
4167
|
|
|
3960
4168
|
// src/security/api-key.ts
|
|
3961
4169
|
import { createHash, randomBytes } from "crypto";
|
|
4170
|
+
import { postureEnforcesWall, postureUsesUnionScope, normalizeTenancyPosture } from "@objectstack/spec/security";
|
|
3962
4171
|
var API_KEY_PREFIX = "osk_";
|
|
3963
4172
|
var API_KEY_ENTROPY_BYTES = 32;
|
|
3964
4173
|
var VISIBLE_PREFIX_LEN = 12;
|
|
@@ -4012,10 +4221,18 @@ function isExpired(value, nowMs) {
|
|
|
4012
4221
|
if (Number.isNaN(ms)) return false;
|
|
4013
4222
|
return ms <= nowMs;
|
|
4014
4223
|
}
|
|
4015
|
-
|
|
4224
|
+
function effectiveTenancyPosture(tenancy) {
|
|
4225
|
+
if (!tenancy) return void 0;
|
|
4226
|
+
return normalizeTenancyPosture(tenancy.posture) ?? (tenancy.isolationActive ? "isolated" : "single");
|
|
4227
|
+
}
|
|
4228
|
+
async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now(), tenancyPosture) {
|
|
4229
|
+
const admission = await resolveApiKeyAdmission(ql, headers, nowMs, tenancyPosture);
|
|
4230
|
+
return admission.outcome === "admitted" ? admission.principal : void 0;
|
|
4231
|
+
}
|
|
4232
|
+
async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPosture) {
|
|
4016
4233
|
const apiKey = extractApiKey(headers);
|
|
4017
|
-
if (!apiKey) return
|
|
4018
|
-
if (!ql || typeof ql.find !== "function") return
|
|
4234
|
+
if (!apiKey) return { outcome: "none" };
|
|
4235
|
+
if (!ql || typeof ql.find !== "function") return { outcome: "none" };
|
|
4019
4236
|
let rows;
|
|
4020
4237
|
try {
|
|
4021
4238
|
rows = await ql.find("sys_api_key", {
|
|
@@ -4024,19 +4241,29 @@ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
|
|
|
4024
4241
|
context: { isSystem: true }
|
|
4025
4242
|
});
|
|
4026
4243
|
} catch {
|
|
4027
|
-
return
|
|
4244
|
+
return { outcome: "none" };
|
|
4028
4245
|
}
|
|
4029
4246
|
if (rows && rows.value) rows = rows.value;
|
|
4030
4247
|
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
4031
|
-
if (!row || row.revoked === true) return
|
|
4248
|
+
if (!row || row.revoked === true) return { outcome: "none" };
|
|
4032
4249
|
const expiresAt = row.expires_at ?? row.expiresAt;
|
|
4033
|
-
if (isExpired(expiresAt, nowMs)) return
|
|
4250
|
+
if (isExpired(expiresAt, nowMs)) return { outcome: "none" };
|
|
4034
4251
|
const userId = row.user_id ?? row.userId;
|
|
4035
|
-
if (!userId || typeof userId !== "string") return
|
|
4252
|
+
if (!userId || typeof userId !== "string") return { outcome: "none" };
|
|
4253
|
+
const tenantId = typeof row.active_organization_id === "string" && row.active_organization_id ? row.active_organization_id : void 0;
|
|
4254
|
+
if (!tenantId && tenancyPosture) {
|
|
4255
|
+
const posture = tenancyPosture;
|
|
4256
|
+
if (postureEnforcesWall(posture) && !postureUsesUnionScope(posture)) {
|
|
4257
|
+
return {
|
|
4258
|
+
outcome: "refused",
|
|
4259
|
+
reason: "organization_required",
|
|
4260
|
+
message: "This API key carries no organization and cannot be used under the `isolated` tenancy posture, where every organization-scoped read is walled to an active organization. Mint a replacement key \u2014 new keys inherit the minter\u2019s active organization."
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4036
4264
|
return {
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
scopes: parseScopes(row.scopes)
|
|
4265
|
+
outcome: "admitted",
|
|
4266
|
+
principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
|
|
4040
4267
|
};
|
|
4041
4268
|
}
|
|
4042
4269
|
function readHeader(headers, name) {
|
|
@@ -4069,6 +4296,7 @@ import {
|
|
|
4069
4296
|
ADMIN_FULL_ACCESS,
|
|
4070
4297
|
ORGANIZATION_ADMIN_GRANTS
|
|
4071
4298
|
} from "@objectstack/spec";
|
|
4299
|
+
import { postureEnforcesWall as postureEnforcesWall2 } from "@objectstack/spec/security";
|
|
4072
4300
|
|
|
4073
4301
|
// src/security/grant-validity.ts
|
|
4074
4302
|
function toEpochMs(value) {
|
|
@@ -4151,6 +4379,15 @@ function postureVisibleRows(posture, rows, principal) {
|
|
|
4151
4379
|
}
|
|
4152
4380
|
}
|
|
4153
4381
|
|
|
4382
|
+
// src/security/row-active.ts
|
|
4383
|
+
var DEACTIVATED_VALUES = [false, 0, "0", "false"];
|
|
4384
|
+
function isRowActive(row) {
|
|
4385
|
+
if (!row) return false;
|
|
4386
|
+
const value = row.active;
|
|
4387
|
+
if (value === void 0 || value === null) return true;
|
|
4388
|
+
return !DEACTIVATED_VALUES.includes(value);
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4154
4391
|
// src/security/resolve-authz-context.ts
|
|
4155
4392
|
function safeJsonParse2(s, fallback) {
|
|
4156
4393
|
try {
|
|
@@ -4180,7 +4417,12 @@ async function resolveAuthzContext(input) {
|
|
|
4180
4417
|
};
|
|
4181
4418
|
let userId;
|
|
4182
4419
|
let tenantId;
|
|
4183
|
-
const
|
|
4420
|
+
const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
|
|
4421
|
+
if (admission.outcome === "refused") {
|
|
4422
|
+
ctx.authRefusal = { reason: admission.reason, message: admission.message };
|
|
4423
|
+
return ctx;
|
|
4424
|
+
}
|
|
4425
|
+
const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
|
|
4184
4426
|
if (keyPrincipal) {
|
|
4185
4427
|
userId = keyPrincipal.userId;
|
|
4186
4428
|
tenantId = keyPrincipal.tenantId;
|
|
@@ -4208,6 +4450,22 @@ async function resolveAuthzContext(input) {
|
|
|
4208
4450
|
seedPermissions: ctx.permissions,
|
|
4209
4451
|
seedEmail: ctx.email
|
|
4210
4452
|
});
|
|
4453
|
+
if (keyPrincipal?.tenantId && input.tenancyPosture) {
|
|
4454
|
+
const posture = input.tenancyPosture;
|
|
4455
|
+
if (postureEnforcesWall2(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
|
|
4456
|
+
return {
|
|
4457
|
+
positions: [],
|
|
4458
|
+
permissions: [],
|
|
4459
|
+
systemPermissions: [],
|
|
4460
|
+
org_user_ids: [],
|
|
4461
|
+
accessible_org_ids: [],
|
|
4462
|
+
authRefusal: {
|
|
4463
|
+
reason: "organization_membership_ended",
|
|
4464
|
+
message: "This API key authenticates into an organization its owner is no longer a member of. The key was not revoked \u2014 the membership that backed it ended."
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4211
4469
|
ctx.positions = grants.positions;
|
|
4212
4470
|
ctx.permissions = grants.permissions;
|
|
4213
4471
|
ctx.systemPermissions = grants.systemPermissions;
|
|
@@ -4292,7 +4550,13 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4292
4550
|
if (!grants.positions.includes("everyone")) grants.positions.push("everyone");
|
|
4293
4551
|
if (grants.positions.length > 0) {
|
|
4294
4552
|
const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 100);
|
|
4295
|
-
const
|
|
4553
|
+
const deactivatedNames = new Set(
|
|
4554
|
+
positionRows.filter((r) => !isRowActive(r)).map((r) => r.name).filter(Boolean)
|
|
4555
|
+
);
|
|
4556
|
+
if (deactivatedNames.size > 0) {
|
|
4557
|
+
grants.positions = grants.positions.filter((n) => !deactivatedNames.has(n));
|
|
4558
|
+
}
|
|
4559
|
+
const positionIds = positionRows.filter((r) => isRowActive(r)).map((r) => r.id).filter(Boolean);
|
|
4296
4560
|
if (positionIds.length > 0) {
|
|
4297
4561
|
const rpsRows = await tryFind(ql, "sys_position_permission_set", { position_id: { $in: positionIds } }, 500);
|
|
4298
4562
|
for (const r of rpsRows) {
|
|
@@ -4302,7 +4566,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4302
4566
|
}
|
|
4303
4567
|
}
|
|
4304
4568
|
if (psIds.size > 0) {
|
|
4305
|
-
const
|
|
4569
|
+
const psRowsAll = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
|
|
4570
|
+
const psRows = psRowsAll.filter((r) => isRowActive(r));
|
|
4306
4571
|
const tabRank = { hidden: 0, default_off: 1, default_on: 2, visible: 3 };
|
|
4307
4572
|
const mergedTabs = {};
|
|
4308
4573
|
for (const ps of psRows) {
|
|
@@ -4533,6 +4798,7 @@ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
|
|
|
4533
4798
|
var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
|
|
4534
4799
|
var ANONYMOUS_DENY_BODY = {
|
|
4535
4800
|
error: ANONYMOUS_DENY_CODE,
|
|
4801
|
+
code: ANONYMOUS_DENY_CODE,
|
|
4536
4802
|
message: ANONYMOUS_DENY_MESSAGE
|
|
4537
4803
|
};
|
|
4538
4804
|
function shouldDenyAnonymous(input) {
|
|
@@ -4546,6 +4812,88 @@ function shouldDenyAnonymous(input) {
|
|
|
4546
4812
|
return true;
|
|
4547
4813
|
}
|
|
4548
4814
|
|
|
4815
|
+
// src/security/admin-standing-surface.ts
|
|
4816
|
+
var ADMIN_STANDING_SURFACE = {
|
|
4817
|
+
sys_permission_set: {
|
|
4818
|
+
role: "derives",
|
|
4819
|
+
reason: "The row `platform_admin` is resolved BY NAME from (\xA76b). Renaming it, deleting it or switching it off (ADR-0049 `active`, read here since #8613) un-makes every platform admin at once, with no identity table touched.",
|
|
4820
|
+
columns: [
|
|
4821
|
+
"id",
|
|
4822
|
+
"name",
|
|
4823
|
+
"active",
|
|
4824
|
+
"system_permissions",
|
|
4825
|
+
"systemPermissions",
|
|
4826
|
+
"tab_permissions",
|
|
4827
|
+
"tabPermissions"
|
|
4828
|
+
]
|
|
4829
|
+
},
|
|
4830
|
+
sys_user_permission_set: {
|
|
4831
|
+
role: "derives",
|
|
4832
|
+
reason: "The grant that makes a user a platform admin: an UNSCOPED, in-window (ADR-0091) grant of `admin_full_access` (\xA76). Re-pointing it, scoping it to an organization or moving it out of its window revokes the standing while leaving the row in place.",
|
|
4833
|
+
columns: [
|
|
4834
|
+
"user_id",
|
|
4835
|
+
"permission_set_id",
|
|
4836
|
+
"permissionSetId",
|
|
4837
|
+
"organization_id",
|
|
4838
|
+
"organizationId",
|
|
4839
|
+
"valid_from",
|
|
4840
|
+
"validFrom",
|
|
4841
|
+
"valid_until",
|
|
4842
|
+
"validUntil"
|
|
4843
|
+
]
|
|
4844
|
+
},
|
|
4845
|
+
sys_member: {
|
|
4846
|
+
role: "derives",
|
|
4847
|
+
reason: "Organization owner/admin standing (\xA73). The graded `role` is projected into `positions` here and separately drives the `organization_admin` capability grant, which is what the posture ladder reads; the break-glass guard counts the same rows one step earlier, by grade (ADR-0108). Either way a downgrade of the last graded membership is a write that can empty the administrator population.",
|
|
4848
|
+
columns: [
|
|
4849
|
+
"user_id",
|
|
4850
|
+
"userId",
|
|
4851
|
+
"organization_id",
|
|
4852
|
+
"organizationId",
|
|
4853
|
+
"role",
|
|
4854
|
+
"valid_from",
|
|
4855
|
+
"validFrom",
|
|
4856
|
+
"valid_until",
|
|
4857
|
+
"validUntil"
|
|
4858
|
+
]
|
|
4859
|
+
},
|
|
4860
|
+
sys_user: {
|
|
4861
|
+
role: "reads-only",
|
|
4862
|
+
reason: "Read for the `current_user.email` RLS fallback and the ADR-0024 `ai_seat` synthesis (\xA77). Neither confers administrator standing. The guard does watch this table, but for the ban/delete WRITE SHAPES \u2014 `banned` is never read here, so it is not a derivation column and carries no standing-key list."
|
|
4863
|
+
},
|
|
4864
|
+
sys_user_position: {
|
|
4865
|
+
role: "reads-only",
|
|
4866
|
+
reason: "ADR-0057 D4 platform-RBAC position assignments (\xA74). A position can carry permission sets (see `sys_position_permission_set`) but never platform-admin standing \u2014 \xA76b requires the set to be reached through an unscoped USER grant (`unscopedUserPsIds`), so a position-bound `admin_full_access` resolves the set name into `permissions` and leaves `hasPlatformAdminGrant` false."
|
|
4867
|
+
},
|
|
4868
|
+
sys_position: {
|
|
4869
|
+
role: "reads-only",
|
|
4870
|
+
reason: "Read to drop DEACTIVATED positions (ADR-0049, \xA76a). Same reason as `sys_user_position`: the position path cannot reach `hasPlatformAdminGrant`."
|
|
4871
|
+
},
|
|
4872
|
+
sys_position_permission_set: {
|
|
4873
|
+
role: "reads-only",
|
|
4874
|
+
reason: "Position-bound permission sets (\xA76a). Contributes ids to `psIds` \u2014 and therefore names to `permissions` \u2014 but not to `unscopedUserPsIds`, which is the set \xA76b tests for platform-admin standing."
|
|
4875
|
+
}
|
|
4876
|
+
};
|
|
4877
|
+
function adminStandingTables() {
|
|
4878
|
+
return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
|
|
4879
|
+
}
|
|
4880
|
+
function adminStandingColumns(table) {
|
|
4881
|
+
const entry = ADMIN_STANDING_SURFACE[table];
|
|
4882
|
+
return entry?.role === "derives" ? entry.columns : void 0;
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
// src/security/audience-binding-suggestion-status.ts
|
|
4886
|
+
var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
|
|
4887
|
+
pending: true,
|
|
4888
|
+
confirmed: true,
|
|
4889
|
+
dismissed: true
|
|
4890
|
+
};
|
|
4891
|
+
var AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES = Object.keys(
|
|
4892
|
+
AUDIENCE_BINDING_SUGGESTION_STATUSES
|
|
4893
|
+
);
|
|
4894
|
+
var isAudienceBindingSuggestionStatus = (value) => Object.prototype.hasOwnProperty.call(AUDIENCE_BINDING_SUGGESTION_STATUSES, value);
|
|
4895
|
+
var unknownAudienceBindingSuggestionStatusMessage = (value) => `Unknown status filter '${value}' \u2014 expected one of: ${AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES.join(", ")}`;
|
|
4896
|
+
|
|
4549
4897
|
// src/security/operation-private-keys.ts
|
|
4550
4898
|
var OPERATION_PRIVATE_KEY_PREFIX = "__";
|
|
4551
4899
|
function withoutOperationPrivateKeys(exec) {
|
|
@@ -4584,10 +4932,26 @@ function calendarPartsInTzOrUtc(d, tz) {
|
|
|
4584
4932
|
}
|
|
4585
4933
|
function zonedDateStartToUtcMs(ymd2, tz) {
|
|
4586
4934
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
|
|
4587
|
-
|
|
4935
|
+
if (!m) return NaN;
|
|
4936
|
+
return zonedWallClockToUtcMs(
|
|
4937
|
+
{ year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) },
|
|
4938
|
+
tz
|
|
4939
|
+
);
|
|
4940
|
+
}
|
|
4941
|
+
function zonedWallClockToUtcMs(parts, tz) {
|
|
4942
|
+
const wallAsUtc = Date.UTC(
|
|
4943
|
+
parts.year,
|
|
4944
|
+
parts.month - 1,
|
|
4945
|
+
parts.day,
|
|
4946
|
+
parts.hour ?? 0,
|
|
4947
|
+
parts.minute ?? 0,
|
|
4948
|
+
parts.second ?? 0,
|
|
4949
|
+
parts.millisecond ?? 0
|
|
4950
|
+
);
|
|
4588
4951
|
if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
|
|
4589
4952
|
try {
|
|
4590
4953
|
const offsetAt = (t) => {
|
|
4954
|
+
const whole = Math.floor(t / 1e3) * 1e3;
|
|
4591
4955
|
const p = new Intl.DateTimeFormat("en-US", {
|
|
4592
4956
|
timeZone: tz,
|
|
4593
4957
|
hourCycle: "h23",
|
|
@@ -4597,9 +4961,9 @@ function zonedDateStartToUtcMs(ymd2, tz) {
|
|
|
4597
4961
|
hour: "2-digit",
|
|
4598
4962
|
minute: "2-digit",
|
|
4599
4963
|
second: "2-digit"
|
|
4600
|
-
}).formatToParts(new Date(
|
|
4964
|
+
}).formatToParts(new Date(whole));
|
|
4601
4965
|
const g = (k) => Number(p.find((x) => x.type === k)?.value);
|
|
4602
|
-
return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) -
|
|
4966
|
+
return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - whole;
|
|
4603
4967
|
};
|
|
4604
4968
|
const off1 = offsetAt(wallAsUtc - offsetAt(wallAsUtc));
|
|
4605
4969
|
return wallAsUtc - off1;
|
|
@@ -4799,6 +5163,27 @@ async function bulkWrite(rows, opts) {
|
|
|
4799
5163
|
return results;
|
|
4800
5164
|
}
|
|
4801
5165
|
|
|
5166
|
+
// src/utils/internal-write-response.ts
|
|
5167
|
+
function collectInternalWriteResponseFields(schema) {
|
|
5168
|
+
const fields = schema?.fields;
|
|
5169
|
+
if (!fields || typeof fields !== "object") return [];
|
|
5170
|
+
const out = [];
|
|
5171
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
5172
|
+
if (def && def.internal === true) out.push(name);
|
|
5173
|
+
}
|
|
5174
|
+
return out;
|
|
5175
|
+
}
|
|
5176
|
+
function omitInternalFieldsFromWriteResponse(schema, records) {
|
|
5177
|
+
if (!records) return;
|
|
5178
|
+
const internalFields = collectInternalWriteResponseFields(schema);
|
|
5179
|
+
if (internalFields.length === 0) return;
|
|
5180
|
+
const list = Array.isArray(records) ? records : [records];
|
|
5181
|
+
for (const row of list) {
|
|
5182
|
+
if (!row || typeof row !== "object") continue;
|
|
5183
|
+
for (const field of internalFields) delete row[field];
|
|
5184
|
+
}
|
|
5185
|
+
}
|
|
5186
|
+
|
|
4802
5187
|
// src/utils/migration-journal.ts
|
|
4803
5188
|
import { createHash as createHash2, randomUUID } from "crypto";
|
|
4804
5189
|
import {
|
|
@@ -5394,6 +5779,46 @@ function filterTokenContextFrom(execCtx, now) {
|
|
|
5394
5779
|
};
|
|
5395
5780
|
}
|
|
5396
5781
|
|
|
5782
|
+
// src/utils/temporal-comparand.ts
|
|
5783
|
+
import { classifyFilterToken as classifyFilterToken2 } from "@objectstack/spec/data";
|
|
5784
|
+
function temporalComparandKind(fieldType) {
|
|
5785
|
+
if (fieldType === "datetime") return "datetime";
|
|
5786
|
+
if (fieldType === "date") return "date";
|
|
5787
|
+
if (fieldType === "time") return "time";
|
|
5788
|
+
return null;
|
|
5789
|
+
}
|
|
5790
|
+
function readsAsInstant(s) {
|
|
5791
|
+
if (/^-?\d+$/.test(s)) return Number.isFinite(new Date(Number(s)).getTime());
|
|
5792
|
+
const iso = /^\d{4}-\d{2}-\d{2}$/.test(s) ? `${s}T00:00:00.000Z` : /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/.test(s) ? `${s.replace(" ", "T")}Z` : s;
|
|
5793
|
+
return Number.isFinite(Date.parse(iso));
|
|
5794
|
+
}
|
|
5795
|
+
function readsAsCalendarDay(s) {
|
|
5796
|
+
return /^\d{4}-\d{2}-\d{2}/.test(s);
|
|
5797
|
+
}
|
|
5798
|
+
function readsAsWallClock(s) {
|
|
5799
|
+
const m = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(s);
|
|
5800
|
+
if (!m) return false;
|
|
5801
|
+
return Number(m[1]) <= 23 && Number(m[2]) <= 59 && Number(m[3] ?? "0") <= 59;
|
|
5802
|
+
}
|
|
5803
|
+
function isUninterpretableTemporalComparand(kind, value) {
|
|
5804
|
+
if (typeof value !== "string") return false;
|
|
5805
|
+
const s = value.trim();
|
|
5806
|
+
if (s === "") return false;
|
|
5807
|
+
if (classifyFilterToken2(value) !== null) return false;
|
|
5808
|
+
if (kind === "datetime") return !readsAsInstant(s);
|
|
5809
|
+
if (kind === "date") return !readsAsCalendarDay(s);
|
|
5810
|
+
return !(readsAsWallClock(s) || readsAsInstant(s));
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
// src/utils/record-not-found.ts
|
|
5814
|
+
function recordNotFoundError(object, id) {
|
|
5815
|
+
const err = new Error(`Record ${id} not found in ${object}`);
|
|
5816
|
+
err.code = "RECORD_NOT_FOUND";
|
|
5817
|
+
err.status = 404;
|
|
5818
|
+
err.object = object;
|
|
5819
|
+
return err;
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5397
5822
|
// src/health-monitor.ts
|
|
5398
5823
|
var PluginHealthMonitor = class {
|
|
5399
5824
|
constructor(logger) {
|
|
@@ -6370,12 +6795,18 @@ var NamespaceResolver = class {
|
|
|
6370
6795
|
return `${shortName}_${ns}`;
|
|
6371
6796
|
}
|
|
6372
6797
|
};
|
|
6798
|
+
|
|
6799
|
+
// src/index.ts
|
|
6800
|
+
import { UNMATCHED_ROUTE_PATTERN } from "@objectstack/spec/contracts";
|
|
6373
6801
|
export {
|
|
6802
|
+
ADMIN_STANDING_SURFACE,
|
|
6374
6803
|
ANONYMOUS_DENY_BODY,
|
|
6375
6804
|
ANONYMOUS_DENY_CODE,
|
|
6376
6805
|
ANONYMOUS_DENY_MESSAGE,
|
|
6377
6806
|
ANONYMOUS_DENY_STATUS,
|
|
6378
6807
|
API_KEY_PREFIX,
|
|
6808
|
+
AUDIENCE_BINDING_SUGGESTION_STATUSES,
|
|
6809
|
+
AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
|
|
6379
6810
|
CORE_FALLBACK_FACTORIES,
|
|
6380
6811
|
DependencyResolver,
|
|
6381
6812
|
ENTRY_EXECUTION_CONTEXT_FIELDS,
|
|
@@ -6404,16 +6835,22 @@ export {
|
|
|
6404
6835
|
SecurePluginContext,
|
|
6405
6836
|
SemanticVersionManager,
|
|
6406
6837
|
ServiceLifecycle,
|
|
6838
|
+
UNMATCHED_ROUTE_PATTERN,
|
|
6407
6839
|
UnknownFilterTokenError,
|
|
6408
6840
|
UnresolvedFilterTokenError,
|
|
6841
|
+
adminStandingColumns,
|
|
6842
|
+
adminStandingTables,
|
|
6409
6843
|
assembleExecutionContext,
|
|
6410
6844
|
assembleExecutionContextOrGuest,
|
|
6411
6845
|
assertInitServiceRequirements,
|
|
6846
|
+
assertMetadataRegisterContract,
|
|
6412
6847
|
bucketKeyToCalendarRange,
|
|
6413
6848
|
buildPermissionsFromGrants,
|
|
6414
6849
|
bulkWrite,
|
|
6415
6850
|
calendarPartsInTz,
|
|
6416
6851
|
calendarPartsInTzOrUtc,
|
|
6852
|
+
canonicalMetadataServiceType,
|
|
6853
|
+
collectInternalWriteResponseFields,
|
|
6417
6854
|
counterSignPayload,
|
|
6418
6855
|
createLogger,
|
|
6419
6856
|
createMemoryCache,
|
|
@@ -6427,6 +6864,7 @@ export {
|
|
|
6427
6864
|
defaultIsTransientError,
|
|
6428
6865
|
derivePosture,
|
|
6429
6866
|
describeInitOrderFault,
|
|
6867
|
+
effectiveTenancyPosture,
|
|
6430
6868
|
engineCanRollBack,
|
|
6431
6869
|
evaluateAuthGate,
|
|
6432
6870
|
extractApiKey,
|
|
@@ -6438,19 +6876,25 @@ export {
|
|
|
6438
6876
|
getMemoryUsage,
|
|
6439
6877
|
hashApiKey,
|
|
6440
6878
|
hashMigrationPlan,
|
|
6879
|
+
isAudienceBindingSuggestionStatus,
|
|
6441
6880
|
isAuthGateAllowlisted,
|
|
6442
6881
|
isExpired,
|
|
6443
6882
|
isGrantActive,
|
|
6444
6883
|
isGrantExpired,
|
|
6445
6884
|
isNode,
|
|
6885
|
+
isRowActive,
|
|
6886
|
+
isUninterpretableTemporalComparand,
|
|
6446
6887
|
nextUtcCalendarDay,
|
|
6447
6888
|
normalizeAuthGate,
|
|
6889
|
+
omitInternalFieldsFromWriteResponse,
|
|
6448
6890
|
parseScopes,
|
|
6449
6891
|
parseSignature,
|
|
6450
6892
|
planChunks,
|
|
6451
6893
|
postureVisibleRows,
|
|
6452
6894
|
readAuthoredTranslationLayer,
|
|
6453
6895
|
readRunJournal,
|
|
6896
|
+
recordNotFoundError,
|
|
6897
|
+
resolveApiKeyAdmission,
|
|
6454
6898
|
resolveApiKeyPrincipal,
|
|
6455
6899
|
resolveAuthzContext,
|
|
6456
6900
|
resolveFilterToken,
|
|
@@ -6464,6 +6908,8 @@ export {
|
|
|
6464
6908
|
safeExit,
|
|
6465
6909
|
shouldDenyAnonymous,
|
|
6466
6910
|
signPayload,
|
|
6911
|
+
temporalComparandKind,
|
|
6912
|
+
unknownAudienceBindingSuggestionStatusMessage,
|
|
6467
6913
|
utcInstantMs,
|
|
6468
6914
|
validateInitServiceContract,
|
|
6469
6915
|
verifyPayload,
|
|
@@ -6473,6 +6919,7 @@ export {
|
|
|
6473
6919
|
wireAuthoredTranslationSync,
|
|
6474
6920
|
withTransientRetry,
|
|
6475
6921
|
withoutOperationPrivateKeys,
|
|
6476
|
-
zonedDateStartToUtcMs
|
|
6922
|
+
zonedDateStartToUtcMs,
|
|
6923
|
+
zonedWallClockToUtcMs
|
|
6477
6924
|
};
|
|
6478
6925
|
//# sourceMappingURL=index.js.map
|