@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.cjs
CHANGED
|
@@ -30,11 +30,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
ADMIN_STANDING_SURFACE: () => ADMIN_STANDING_SURFACE,
|
|
33
34
|
ANONYMOUS_DENY_BODY: () => ANONYMOUS_DENY_BODY,
|
|
34
35
|
ANONYMOUS_DENY_CODE: () => ANONYMOUS_DENY_CODE,
|
|
35
36
|
ANONYMOUS_DENY_MESSAGE: () => ANONYMOUS_DENY_MESSAGE,
|
|
36
37
|
ANONYMOUS_DENY_STATUS: () => ANONYMOUS_DENY_STATUS,
|
|
37
38
|
API_KEY_PREFIX: () => API_KEY_PREFIX,
|
|
39
|
+
AUDIENCE_BINDING_SUGGESTION_STATUSES: () => AUDIENCE_BINDING_SUGGESTION_STATUSES,
|
|
40
|
+
AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES: () => AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
|
|
38
41
|
CORE_FALLBACK_FACTORIES: () => CORE_FALLBACK_FACTORIES,
|
|
39
42
|
DependencyResolver: () => DependencyResolver,
|
|
40
43
|
ENTRY_EXECUTION_CONTEXT_FIELDS: () => ENTRY_EXECUTION_CONTEXT_FIELDS,
|
|
@@ -63,16 +66,22 @@ __export(index_exports, {
|
|
|
63
66
|
SecurePluginContext: () => SecurePluginContext,
|
|
64
67
|
SemanticVersionManager: () => SemanticVersionManager,
|
|
65
68
|
ServiceLifecycle: () => ServiceLifecycle,
|
|
69
|
+
UNMATCHED_ROUTE_PATTERN: () => import_contracts.UNMATCHED_ROUTE_PATTERN,
|
|
66
70
|
UnknownFilterTokenError: () => UnknownFilterTokenError,
|
|
67
71
|
UnresolvedFilterTokenError: () => UnresolvedFilterTokenError,
|
|
72
|
+
adminStandingColumns: () => adminStandingColumns,
|
|
73
|
+
adminStandingTables: () => adminStandingTables,
|
|
68
74
|
assembleExecutionContext: () => assembleExecutionContext,
|
|
69
75
|
assembleExecutionContextOrGuest: () => assembleExecutionContextOrGuest,
|
|
70
76
|
assertInitServiceRequirements: () => assertInitServiceRequirements,
|
|
77
|
+
assertMetadataRegisterContract: () => assertMetadataRegisterContract,
|
|
71
78
|
bucketKeyToCalendarRange: () => bucketKeyToCalendarRange,
|
|
72
79
|
buildPermissionsFromGrants: () => buildPermissionsFromGrants,
|
|
73
80
|
bulkWrite: () => bulkWrite,
|
|
74
81
|
calendarPartsInTz: () => calendarPartsInTz,
|
|
75
82
|
calendarPartsInTzOrUtc: () => calendarPartsInTzOrUtc,
|
|
83
|
+
canonicalMetadataServiceType: () => canonicalMetadataServiceType,
|
|
84
|
+
collectInternalWriteResponseFields: () => collectInternalWriteResponseFields,
|
|
76
85
|
counterSignPayload: () => counterSignPayload,
|
|
77
86
|
createLogger: () => createLogger,
|
|
78
87
|
createMemoryCache: () => createMemoryCache,
|
|
@@ -86,6 +95,7 @@ __export(index_exports, {
|
|
|
86
95
|
defaultIsTransientError: () => defaultIsTransientError,
|
|
87
96
|
derivePosture: () => derivePosture,
|
|
88
97
|
describeInitOrderFault: () => describeInitOrderFault,
|
|
98
|
+
effectiveTenancyPosture: () => effectiveTenancyPosture,
|
|
89
99
|
engineCanRollBack: () => engineCanRollBack,
|
|
90
100
|
evaluateAuthGate: () => evaluateAuthGate,
|
|
91
101
|
extractApiKey: () => extractApiKey,
|
|
@@ -97,19 +107,25 @@ __export(index_exports, {
|
|
|
97
107
|
getMemoryUsage: () => getMemoryUsage,
|
|
98
108
|
hashApiKey: () => hashApiKey,
|
|
99
109
|
hashMigrationPlan: () => hashMigrationPlan,
|
|
110
|
+
isAudienceBindingSuggestionStatus: () => isAudienceBindingSuggestionStatus,
|
|
100
111
|
isAuthGateAllowlisted: () => isAuthGateAllowlisted,
|
|
101
112
|
isExpired: () => isExpired,
|
|
102
113
|
isGrantActive: () => isGrantActive,
|
|
103
114
|
isGrantExpired: () => isGrantExpired,
|
|
104
115
|
isNode: () => isNode,
|
|
116
|
+
isRowActive: () => isRowActive,
|
|
117
|
+
isUninterpretableTemporalComparand: () => isUninterpretableTemporalComparand,
|
|
105
118
|
nextUtcCalendarDay: () => import_data.nextUtcCalendarDay,
|
|
106
119
|
normalizeAuthGate: () => normalizeAuthGate,
|
|
120
|
+
omitInternalFieldsFromWriteResponse: () => omitInternalFieldsFromWriteResponse,
|
|
107
121
|
parseScopes: () => parseScopes,
|
|
108
122
|
parseSignature: () => parseSignature,
|
|
109
123
|
planChunks: () => planChunks,
|
|
110
124
|
postureVisibleRows: () => postureVisibleRows,
|
|
111
125
|
readAuthoredTranslationLayer: () => readAuthoredTranslationLayer,
|
|
112
126
|
readRunJournal: () => readRunJournal,
|
|
127
|
+
recordNotFoundError: () => recordNotFoundError,
|
|
128
|
+
resolveApiKeyAdmission: () => resolveApiKeyAdmission,
|
|
113
129
|
resolveApiKeyPrincipal: () => resolveApiKeyPrincipal,
|
|
114
130
|
resolveAuthzContext: () => resolveAuthzContext,
|
|
115
131
|
resolveFilterToken: () => resolveFilterToken,
|
|
@@ -123,6 +139,8 @@ __export(index_exports, {
|
|
|
123
139
|
safeExit: () => safeExit,
|
|
124
140
|
shouldDenyAnonymous: () => shouldDenyAnonymous,
|
|
125
141
|
signPayload: () => signPayload,
|
|
142
|
+
temporalComparandKind: () => temporalComparandKind,
|
|
143
|
+
unknownAudienceBindingSuggestionStatusMessage: () => unknownAudienceBindingSuggestionStatusMessage,
|
|
126
144
|
utcInstantMs: () => import_data.utcInstantMs,
|
|
127
145
|
validateInitServiceContract: () => validateInitServiceContract,
|
|
128
146
|
verifyPayload: () => verifyPayload,
|
|
@@ -132,7 +150,8 @@ __export(index_exports, {
|
|
|
132
150
|
wireAuthoredTranslationSync: () => wireAuthoredTranslationSync,
|
|
133
151
|
withTransientRetry: () => withTransientRetry,
|
|
134
152
|
withoutOperationPrivateKeys: () => withoutOperationPrivateKeys,
|
|
135
|
-
zonedDateStartToUtcMs: () => zonedDateStartToUtcMs
|
|
153
|
+
zonedDateStartToUtcMs: () => zonedDateStartToUtcMs,
|
|
154
|
+
zonedWallClockToUtcMs: () => zonedWallClockToUtcMs
|
|
136
155
|
});
|
|
137
156
|
module.exports = __toCommonJS(index_exports);
|
|
138
157
|
|
|
@@ -837,7 +856,7 @@ function createLogger(config) {
|
|
|
837
856
|
}
|
|
838
857
|
|
|
839
858
|
// src/kernel.ts
|
|
840
|
-
var
|
|
859
|
+
var import_system3 = require("@objectstack/spec/system");
|
|
841
860
|
|
|
842
861
|
// src/security/plugin-config-validator.ts
|
|
843
862
|
var import_zod = require("zod");
|
|
@@ -1505,6 +1524,7 @@ function createMemoryJob() {
|
|
|
1505
1524
|
}
|
|
1506
1525
|
|
|
1507
1526
|
// src/fallbacks/memory-i18n.ts
|
|
1527
|
+
var import_system = require("@objectstack/spec/system");
|
|
1508
1528
|
function deepMerge(target, source) {
|
|
1509
1529
|
const result = { ...target };
|
|
1510
1530
|
for (const key of Object.keys(source)) {
|
|
@@ -1538,6 +1558,7 @@ function createMemoryI18n() {
|
|
|
1538
1558
|
const translations = /* @__PURE__ */ new Map();
|
|
1539
1559
|
const authored = /* @__PURE__ */ new Map();
|
|
1540
1560
|
let defaultLocale = "en";
|
|
1561
|
+
let supportedLocales;
|
|
1541
1562
|
function resolveKey(data, key) {
|
|
1542
1563
|
const parts = key.split(".");
|
|
1543
1564
|
let current = data;
|
|
@@ -1603,9 +1624,29 @@ function createMemoryI18n() {
|
|
|
1603
1624
|
authored.set(locale, { ...data });
|
|
1604
1625
|
}
|
|
1605
1626
|
},
|
|
1627
|
+
/**
|
|
1628
|
+
* Report the locales this stack offers.
|
|
1629
|
+
*
|
|
1630
|
+
* [#7679] When the app declared `i18n.supportedLocales`, that declaration
|
|
1631
|
+
* IS the answer — in declared order, and including a declared locale no
|
|
1632
|
+
* bundle was ever loaded for (declared-but-unserved). Reporting the
|
|
1633
|
+
* declaration rather than an intersection is what gives a client the
|
|
1634
|
+
* signal that the locale it is being offered has nothing behind it yet;
|
|
1635
|
+
* quietly dropping it would leave the gap invisible on both sides. It is
|
|
1636
|
+
* also the only answer that does not depend on how much had loaded by the
|
|
1637
|
+
* time this was called.
|
|
1638
|
+
*
|
|
1639
|
+
* With nothing declared, the loaded set — the behaviour every app that
|
|
1640
|
+
* never opted in already has.
|
|
1641
|
+
*/
|
|
1606
1642
|
getLocales() {
|
|
1643
|
+
if (supportedLocales) return [...supportedLocales];
|
|
1607
1644
|
return [.../* @__PURE__ */ new Set([...translations.keys(), ...authored.keys()])];
|
|
1608
1645
|
},
|
|
1646
|
+
/** @see II18nService.setSupportedLocales — [#7679] */
|
|
1647
|
+
setSupportedLocales(locales) {
|
|
1648
|
+
supportedLocales = (0, import_system.normalizeSupportedLocales)(locales);
|
|
1649
|
+
},
|
|
1609
1650
|
getDefaultLocale() {
|
|
1610
1651
|
return defaultLocale;
|
|
1611
1652
|
},
|
|
@@ -1615,14 +1656,42 @@ function createMemoryI18n() {
|
|
|
1615
1656
|
};
|
|
1616
1657
|
}
|
|
1617
1658
|
|
|
1659
|
+
// src/metadata-service-contract.ts
|
|
1660
|
+
var import_shared = require("@objectstack/spec/shared");
|
|
1661
|
+
var REGISTER_REFUSAL_CODE = "VALIDATION_ERROR";
|
|
1662
|
+
function canonicalMetadataServiceType(type) {
|
|
1663
|
+
return (0, import_shared.pluralToSingular)(type);
|
|
1664
|
+
}
|
|
1665
|
+
function registerRefusal(message) {
|
|
1666
|
+
const err = new Error(message);
|
|
1667
|
+
err.code = REGISTER_REFUSAL_CODE;
|
|
1668
|
+
err.status = 400;
|
|
1669
|
+
return err;
|
|
1670
|
+
}
|
|
1671
|
+
function assertMetadataRegisterContract(type, name, data) {
|
|
1672
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
1673
|
+
const shape = data === null ? "null" : Array.isArray(data) ? "an array" : `a ${typeof data}`;
|
|
1674
|
+
throw registerRefusal(
|
|
1675
|
+
`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.`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
const documentName = data.name;
|
|
1679
|
+
if (documentName !== void 0 && documentName !== name) {
|
|
1680
|
+
throw registerRefusal(
|
|
1681
|
+
`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.`
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1618
1686
|
// src/fallbacks/memory-metadata.ts
|
|
1619
1687
|
function createMemoryMetadata() {
|
|
1620
1688
|
const store = /* @__PURE__ */ new Map();
|
|
1621
1689
|
function getTypeMap(type) {
|
|
1622
|
-
|
|
1690
|
+
const canonical = canonicalMetadataServiceType(type);
|
|
1691
|
+
let map = store.get(canonical);
|
|
1623
1692
|
if (!map) {
|
|
1624
1693
|
map = /* @__PURE__ */ new Map();
|
|
1625
|
-
store.set(
|
|
1694
|
+
store.set(canonical, map);
|
|
1626
1695
|
}
|
|
1627
1696
|
return map;
|
|
1628
1697
|
}
|
|
@@ -1637,6 +1706,7 @@ function createMemoryMetadata() {
|
|
|
1637
1706
|
},
|
|
1638
1707
|
_serviceName: "metadata",
|
|
1639
1708
|
async register(type, name, data) {
|
|
1709
|
+
assertMetadataRegisterContract(type, name, data);
|
|
1640
1710
|
getTypeMap(type).set(name, data);
|
|
1641
1711
|
},
|
|
1642
1712
|
// Mirror MetadataManager.registerInMemory (synchronous, no persistence).
|
|
@@ -1648,7 +1718,11 @@ function createMemoryMetadata() {
|
|
|
1648
1718
|
// so `defineStack({ datasources })` entries silently never reached the
|
|
1649
1719
|
// registry and were absent from GET /api/v1/datasources and
|
|
1650
1720
|
// GET /api/v1/meta/datasource (ADR-0015 §18). This store is already
|
|
1651
|
-
// in-memory only, so registerInMemory and register share
|
|
1721
|
+
// in-memory only, so registerInMemory and register share a store — but
|
|
1722
|
+
// NOT the [#7378] refusals: the ruling names `register`, and this member
|
|
1723
|
+
// is a boot-time seeding primitive for source-control-owned artefacts
|
|
1724
|
+
// (see assertMetadataRegisterContract's header for the boundary). It does
|
|
1725
|
+
// share the row-2 canonical type fold, via getTypeMap.
|
|
1652
1726
|
registerInMemory(type, name, data) {
|
|
1653
1727
|
getTypeMap(type).set(name, data);
|
|
1654
1728
|
},
|
|
@@ -1677,7 +1751,7 @@ function createMemoryMetadata() {
|
|
|
1677
1751
|
}
|
|
1678
1752
|
|
|
1679
1753
|
// src/fallbacks/authored-translation-sync.ts
|
|
1680
|
-
var
|
|
1754
|
+
var import_system2 = require("@objectstack/spec/system");
|
|
1681
1755
|
var OWNER_PROP = "__authoredTranslationSyncOwner";
|
|
1682
1756
|
var LOCALE_LIKE = /^[a-z]{2,3}([_-]([A-Za-z]{4}|[A-Za-z]{2}|[0-9]{3}))?$/;
|
|
1683
1757
|
async function readAuthoredTranslationLayer(engine, logger) {
|
|
@@ -1707,7 +1781,7 @@ async function readAuthoredTranslationLayer(engine, logger) {
|
|
|
1707
1781
|
continue;
|
|
1708
1782
|
}
|
|
1709
1783
|
if (!data || typeof data !== "object") continue;
|
|
1710
|
-
const legacyKeys =
|
|
1784
|
+
const legacyKeys = import_system2.LEGACY_OBJECT_FIRST_KEYS.filter((key) => data[key] !== void 0);
|
|
1711
1785
|
if (legacyKeys.length > 0) {
|
|
1712
1786
|
logger?.warn?.(
|
|
1713
1787
|
`[i18n] authored translation '${row?.name}' uses the retired object-first shape (${legacyKeys.join(", ")}) \u2014 nothing resolves from it; re-author it under 'objects.<object_name>' with a top-level 'locale' \u2014 skipped`
|
|
@@ -1812,6 +1886,26 @@ var CORE_FALLBACK_FACTORIES = {
|
|
|
1812
1886
|
i18n: createMemoryI18n
|
|
1813
1887
|
};
|
|
1814
1888
|
|
|
1889
|
+
// src/plugin-registration.ts
|
|
1890
|
+
function versionLabel(plugin) {
|
|
1891
|
+
return plugin.version ? `v${plugin.version}` : "unversioned";
|
|
1892
|
+
}
|
|
1893
|
+
function describeSupersededRegistration(previous, next) {
|
|
1894
|
+
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.`;
|
|
1895
|
+
}
|
|
1896
|
+
function registerPluginByName(registry, plugin, logger) {
|
|
1897
|
+
const previous = registry.get(plugin.name);
|
|
1898
|
+
if (previous !== void 0) {
|
|
1899
|
+
logger.warn(describeSupersededRegistration(previous, plugin), {
|
|
1900
|
+
plugin: plugin.name,
|
|
1901
|
+
supersededVersion: previous.version,
|
|
1902
|
+
supersedingVersion: plugin.version
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
registry.set(plugin.name, plugin);
|
|
1906
|
+
return previous;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1815
1909
|
// src/kernel.ts
|
|
1816
1910
|
var ObjectKernel = class {
|
|
1817
1911
|
constructor(config = {}) {
|
|
@@ -1896,6 +1990,14 @@ var ObjectKernel = class {
|
|
|
1896
1990
|
}
|
|
1897
1991
|
/**
|
|
1898
1992
|
* Register a plugin with enhanced validation
|
|
1993
|
+
*
|
|
1994
|
+
* Duplicate names OVERWRITE, with one `warn` naming both versions — the
|
|
1995
|
+
* declared contract in `plugin-registration.ts`, applied identically by
|
|
1996
|
+
* `LiteKernel.use()` (#9864, maintainer ruling 2026-08-19). The overwrite
|
|
1997
|
+
* itself is unchanged: it is what lets an app config's `plugins` entry
|
|
1998
|
+
* supersede a plugin the CLI auto-registered earlier in the same boot
|
|
1999
|
+
* (#9863). What changes is that it is no longer silent, and no longer
|
|
2000
|
+
* disagrees with the other kernel.
|
|
1899
2001
|
*/
|
|
1900
2002
|
async use(plugin) {
|
|
1901
2003
|
if (this.state !== "idle") {
|
|
@@ -1906,11 +2008,13 @@ var ObjectKernel = class {
|
|
|
1906
2008
|
throw new Error(`Failed to load plugin: ${plugin.name} - ${result.error?.message}`);
|
|
1907
2009
|
}
|
|
1908
2010
|
const pluginMeta = result.plugin;
|
|
1909
|
-
this.plugins
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
2011
|
+
const superseded = registerPluginByName(this.plugins, pluginMeta, this.logger);
|
|
2012
|
+
if (superseded === void 0) {
|
|
2013
|
+
this.logger.info(`Plugin registered: ${pluginMeta.name}@${pluginMeta.version}`, {
|
|
2014
|
+
plugin: pluginMeta.name,
|
|
2015
|
+
version: pluginMeta.version
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
1914
2018
|
return this;
|
|
1915
2019
|
}
|
|
1916
2020
|
/**
|
|
@@ -1945,7 +2049,7 @@ var ObjectKernel = class {
|
|
|
1945
2049
|
*/
|
|
1946
2050
|
preInjectCoreFallbacks() {
|
|
1947
2051
|
if (this.config.skipSystemValidation) return;
|
|
1948
|
-
for (const [serviceName, criticality] of Object.entries(
|
|
2052
|
+
for (const [serviceName, criticality] of Object.entries(import_system3.ServiceRequirementDef)) {
|
|
1949
2053
|
if (criticality !== "core") continue;
|
|
1950
2054
|
const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
|
|
1951
2055
|
if (!hasService) {
|
|
@@ -1969,7 +2073,7 @@ var ObjectKernel = class {
|
|
|
1969
2073
|
this.logger.debug("Validating system service requirements...");
|
|
1970
2074
|
const missingServices = [];
|
|
1971
2075
|
const missingCoreServices = [];
|
|
1972
|
-
for (const [serviceName, criticality] of Object.entries(
|
|
2076
|
+
for (const [serviceName, criticality] of Object.entries(import_system3.ServiceRequirementDef)) {
|
|
1973
2077
|
const hasService = this.services.has(serviceName) || this.pluginLoader.hasService(serviceName);
|
|
1974
2078
|
if (!hasService) {
|
|
1975
2079
|
if (criticality === "required") {
|
|
@@ -2388,14 +2492,21 @@ var LiteKernel = class extends ObjectKernelBase {
|
|
|
2388
2492
|
/**
|
|
2389
2493
|
* Register a plugin
|
|
2390
2494
|
* @param plugin - Plugin instance
|
|
2495
|
+
*
|
|
2496
|
+
* Duplicate names OVERWRITE, with one `warn` naming both versions — the
|
|
2497
|
+
* declared contract in `plugin-registration.ts`, applied identically by
|
|
2498
|
+
* `ObjectKernel.use()` (#9864, maintainer ruling 2026-08-19).
|
|
2499
|
+
*
|
|
2500
|
+
* This method used to `throw` `[Kernel] Plugin '<name>' already
|
|
2501
|
+
* registered` here while `ObjectKernel` overwrote silently, so one input
|
|
2502
|
+
* had two meanings depending on which kernel was running — and the kernel
|
|
2503
|
+
* that runs in production was the silent one. The ruling converged them on
|
|
2504
|
+
* the behaviour that already works (an app config superseding a plugin the
|
|
2505
|
+
* CLI auto-registered, #9863) and made it audible rather than removing it.
|
|
2391
2506
|
*/
|
|
2392
2507
|
use(plugin) {
|
|
2393
2508
|
this.validateIdle();
|
|
2394
|
-
|
|
2395
|
-
if (this.plugins.has(pluginName)) {
|
|
2396
|
-
throw new Error(`[Kernel] Plugin '${pluginName}' already registered`);
|
|
2397
|
-
}
|
|
2398
|
-
this.plugins.set(pluginName, plugin);
|
|
2509
|
+
registerPluginByName(this.plugins, plugin, this.logger);
|
|
2399
2510
|
return this;
|
|
2400
2511
|
}
|
|
2401
2512
|
/**
|
|
@@ -2644,11 +2755,119 @@ var TestRunner = class {
|
|
|
2644
2755
|
};
|
|
2645
2756
|
|
|
2646
2757
|
// src/qa/http-adapter.ts
|
|
2758
|
+
var import_api = require("@objectstack/spec/api");
|
|
2759
|
+
var conventionCache;
|
|
2760
|
+
function conventionMounts() {
|
|
2761
|
+
if (conventionCache === void 0) {
|
|
2762
|
+
const api = import_api.RestApiConfigSchema.parse({});
|
|
2763
|
+
const crud = import_api.CrudEndpointsConfigSchema.parse({});
|
|
2764
|
+
const apiBase = api.apiPath ?? `${api.basePath}/${api.version}`;
|
|
2765
|
+
conventionCache = { apiBase, dataPath: `${apiBase}${crud.dataPrefix}` };
|
|
2766
|
+
}
|
|
2767
|
+
return conventionCache;
|
|
2768
|
+
}
|
|
2647
2769
|
var HttpTestAdapter = class {
|
|
2648
2770
|
constructor(baseUrl, authToken) {
|
|
2649
2771
|
this.baseUrl = baseUrl;
|
|
2650
2772
|
this.authToken = authToken;
|
|
2651
2773
|
}
|
|
2774
|
+
/** The resolved data mount; probes at most once per adapter. */
|
|
2775
|
+
dataMount() {
|
|
2776
|
+
if (this.mountPromise === void 0) {
|
|
2777
|
+
this.mountPromise = this.resolveDataMount();
|
|
2778
|
+
}
|
|
2779
|
+
return this.mountPromise;
|
|
2780
|
+
}
|
|
2781
|
+
/**
|
|
2782
|
+
* Ask the server where it serves the Data Protocol, and fall back to the
|
|
2783
|
+
* convention — loudly — when it cannot say.
|
|
2784
|
+
*
|
|
2785
|
+
* ## [#7983] What the probe recovers, measured rather than assumed
|
|
2786
|
+
*
|
|
2787
|
+
* `@objectstack/client` answers the same question through discovery
|
|
2788
|
+
* (`getRoute`, `packages/client/src/index.ts`), and this follows it: prefer
|
|
2789
|
+
* the server's own `routes.data`, fall back to the convention. Measured on a
|
|
2790
|
+
* booted stack (REST generator + dispatcher bridge, three configs):
|
|
2791
|
+
*
|
|
2792
|
+
* | deployment | `{apiBase}/discovery` | serves |
|
|
2793
|
+
* |--------------------------------|-----------------------|---------------|
|
|
2794
|
+
* | stock | 200 `/api/v1/data` | `/api/v1/data`|
|
|
2795
|
+
* | `crud.dataPrefix: '/objects'` | 200 `/api/v1/objects` | `/api/v1/objects` |
|
|
2796
|
+
* | `api.apiPath: '/api/2026-01'` | **404** | `/api/2026-01/data` |
|
|
2797
|
+
*
|
|
2798
|
+
* So the probe closes the `dataPrefix` row exactly: `RestServer`'s discovery
|
|
2799
|
+
* handler substitutes the configured prefix into `routes.data`, and reading
|
|
2800
|
+
* it is strictly better than recomputing it here. The `apiPath` row it cannot
|
|
2801
|
+
* close, and the reason is structural rather than an oversight — `apiPath`
|
|
2802
|
+
* moves the base that discovery itself is mounted under, so the document that
|
|
2803
|
+
* would name the new mount is behind the very prefix we are missing.
|
|
2804
|
+
*
|
|
2805
|
+
* ⛔ And the one discovery document at a FIXED path does not rescue it:
|
|
2806
|
+
* `/.well-known/objectstack` is mounted at the site root by the dispatcher
|
|
2807
|
+
* bridge, but its `routes.data` is the DISPATCHER's own `${prefix}/data` —
|
|
2808
|
+
* measured as `/api/v1/data` under all three configs above, including the two
|
|
2809
|
+
* where the server serves elsewhere. Falling back to it would turn "we could
|
|
2810
|
+
* not resolve the mount" into "discovery told us `/api/v1/data`": the same
|
|
2811
|
+
* 404, now with a false provenance attached. Not probed, deliberately.
|
|
2812
|
+
*
|
|
2813
|
+
* Hence: one probe, then a diagnostic that NAMES the mount, the evidence and
|
|
2814
|
+
* the remedy. `api_call` takes the path it is given and is unaffected either
|
|
2815
|
+
* way — it stays the escape hatch for a host this cannot reach.
|
|
2816
|
+
*/
|
|
2817
|
+
async resolveDataMount() {
|
|
2818
|
+
const { apiBase, dataPath } = conventionMounts();
|
|
2819
|
+
const probeUrl = `${this.baseUrl}${apiBase}/discovery`;
|
|
2820
|
+
let why;
|
|
2821
|
+
try {
|
|
2822
|
+
const headers = {};
|
|
2823
|
+
if (this.authToken) {
|
|
2824
|
+
headers["Authorization"] = `Bearer ${this.authToken}`;
|
|
2825
|
+
}
|
|
2826
|
+
const response = await fetch(probeUrl, { method: "GET", headers });
|
|
2827
|
+
if (response.ok) {
|
|
2828
|
+
const body = await response.json();
|
|
2829
|
+
const doc = body && typeof body === "object" && "routes" in body ? body : body?.data;
|
|
2830
|
+
const advertised = doc?.routes?.data;
|
|
2831
|
+
if (typeof advertised === "string" && advertised.length > 0) {
|
|
2832
|
+
return {
|
|
2833
|
+
path: advertised,
|
|
2834
|
+
source: "discovery",
|
|
2835
|
+
why: `GET ${probeUrl} advertised routes.data`
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
why = `GET ${probeUrl} answered ${response.status} but carried no routes.data`;
|
|
2839
|
+
} else {
|
|
2840
|
+
why = `GET ${probeUrl} answered ${response.status}`;
|
|
2841
|
+
}
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
why = `GET ${probeUrl} could not be reached (${error.message})`;
|
|
2844
|
+
}
|
|
2845
|
+
const mount = { path: dataPath, source: "convention", why };
|
|
2846
|
+
console.warn(
|
|
2847
|
+
`[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.`
|
|
2848
|
+
);
|
|
2849
|
+
return mount;
|
|
2850
|
+
}
|
|
2851
|
+
/** `{baseUrl}{dataMount}/{object}` — the collection URL. */
|
|
2852
|
+
collectionUrl(mount, objectName) {
|
|
2853
|
+
return `${this.baseUrl}${mount.path}/${encodeURIComponent(objectName)}`;
|
|
2854
|
+
}
|
|
2855
|
+
/** `{collection}/{id}` — the single-record URL. */
|
|
2856
|
+
recordUrl(mount, objectName, id) {
|
|
2857
|
+
return `${this.collectionUrl(mount, objectName)}/${encodeURIComponent(String(id))}`;
|
|
2858
|
+
}
|
|
2859
|
+
/**
|
|
2860
|
+
* The provenance clause appended to a failed record action's error.
|
|
2861
|
+
*
|
|
2862
|
+
* The card this closes is about a 404 that reads like the author's own URL
|
|
2863
|
+
* mistake; the mount is the one fact that distinguishes the two, so it rides
|
|
2864
|
+
* on the failure itself rather than only on a warning printed earlier in the
|
|
2865
|
+
* transcript.
|
|
2866
|
+
*/
|
|
2867
|
+
mountNote(mount) {
|
|
2868
|
+
const how = mount.source === "discovery" ? "resolved from discovery" : "assumed by convention \u2014 the server was not able to state it";
|
|
2869
|
+
return `record actions addressed ${this.baseUrl}${mount.path} (data mount ${how}: ${mount.why})`;
|
|
2870
|
+
}
|
|
2652
2871
|
async execute(action, _context) {
|
|
2653
2872
|
const headers = {
|
|
2654
2873
|
"Content-Type": "application/json"
|
|
@@ -2680,48 +2899,53 @@ var HttpTestAdapter = class {
|
|
|
2680
2899
|
}
|
|
2681
2900
|
}
|
|
2682
2901
|
async createRecord(objectName, data, headers) {
|
|
2683
|
-
const
|
|
2902
|
+
const mount = await this.dataMount();
|
|
2903
|
+
const response = await fetch(this.collectionUrl(mount, objectName), {
|
|
2684
2904
|
method: "POST",
|
|
2685
2905
|
headers,
|
|
2686
2906
|
body: JSON.stringify(data)
|
|
2687
2907
|
});
|
|
2688
|
-
return this.handleResponse(response);
|
|
2908
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2689
2909
|
}
|
|
2690
2910
|
async updateRecord(objectName, data, headers) {
|
|
2691
|
-
const id = data
|
|
2911
|
+
const { id, ...fields } = data;
|
|
2692
2912
|
if (!id) throw new Error("Update record requires id in payload");
|
|
2693
|
-
const
|
|
2694
|
-
|
|
2913
|
+
const mount = await this.dataMount();
|
|
2914
|
+
const response = await fetch(this.recordUrl(mount, objectName, id), {
|
|
2915
|
+
method: "PATCH",
|
|
2695
2916
|
headers,
|
|
2696
|
-
body: JSON.stringify(
|
|
2917
|
+
body: JSON.stringify(fields)
|
|
2697
2918
|
});
|
|
2698
|
-
return this.handleResponse(response);
|
|
2919
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2699
2920
|
}
|
|
2700
2921
|
async deleteRecord(objectName, data, headers) {
|
|
2701
2922
|
const id = data.id;
|
|
2702
2923
|
if (!id) throw new Error("Delete record requires id in payload");
|
|
2703
|
-
const
|
|
2924
|
+
const mount = await this.dataMount();
|
|
2925
|
+
const response = await fetch(this.recordUrl(mount, objectName, id), {
|
|
2704
2926
|
method: "DELETE",
|
|
2705
2927
|
headers
|
|
2706
2928
|
});
|
|
2707
|
-
return this.handleResponse(response);
|
|
2929
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2708
2930
|
}
|
|
2709
2931
|
async readRecord(objectName, data, headers) {
|
|
2710
2932
|
const id = data.id;
|
|
2711
2933
|
if (!id) throw new Error("Read record requires id in payload");
|
|
2712
|
-
const
|
|
2934
|
+
const mount = await this.dataMount();
|
|
2935
|
+
const response = await fetch(this.recordUrl(mount, objectName, id), {
|
|
2713
2936
|
method: "GET",
|
|
2714
2937
|
headers
|
|
2715
2938
|
});
|
|
2716
|
-
return this.handleResponse(response);
|
|
2939
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2717
2940
|
}
|
|
2718
2941
|
async queryRecords(objectName, data, headers) {
|
|
2719
|
-
const
|
|
2942
|
+
const mount = await this.dataMount();
|
|
2943
|
+
const response = await fetch(`${this.collectionUrl(mount, objectName)}/query`, {
|
|
2720
2944
|
method: "POST",
|
|
2721
2945
|
headers,
|
|
2722
2946
|
body: JSON.stringify(data)
|
|
2723
2947
|
});
|
|
2724
|
-
return this.handleResponse(response);
|
|
2948
|
+
return this.handleResponse(response, this.mountNote(mount));
|
|
2725
2949
|
}
|
|
2726
2950
|
async rawApiCall(endpoint, data, headers) {
|
|
2727
2951
|
const method = data.method || "GET";
|
|
@@ -2734,10 +2958,13 @@ var HttpTestAdapter = class {
|
|
|
2734
2958
|
});
|
|
2735
2959
|
return this.handleResponse(response);
|
|
2736
2960
|
}
|
|
2737
|
-
async handleResponse(response) {
|
|
2961
|
+
async handleResponse(response, mountNote) {
|
|
2738
2962
|
if (!response.ok) {
|
|
2739
2963
|
const text = await response.text();
|
|
2740
|
-
|
|
2964
|
+
const wrongUrlShaped = response.status === 404 || response.status === 405;
|
|
2965
|
+
throw new Error(
|
|
2966
|
+
`HTTP Error ${response.status}: ${text}${wrongUrlShaped && mountNote ? ` \u2014 ${mountNote}` : ""}`
|
|
2967
|
+
);
|
|
2741
2968
|
}
|
|
2742
2969
|
const contentType = response.headers.get("content-type");
|
|
2743
2970
|
if (contentType && contentType.includes("application/json")) {
|
|
@@ -4079,6 +4306,7 @@ var PluginSecurityScanner = class {
|
|
|
4079
4306
|
|
|
4080
4307
|
// src/security/api-key.ts
|
|
4081
4308
|
var import_node_crypto2 = require("crypto");
|
|
4309
|
+
var import_security = require("@objectstack/spec/security");
|
|
4082
4310
|
var API_KEY_PREFIX = "osk_";
|
|
4083
4311
|
var API_KEY_ENTROPY_BYTES = 32;
|
|
4084
4312
|
var VISIBLE_PREFIX_LEN = 12;
|
|
@@ -4132,10 +4360,18 @@ function isExpired(value, nowMs) {
|
|
|
4132
4360
|
if (Number.isNaN(ms)) return false;
|
|
4133
4361
|
return ms <= nowMs;
|
|
4134
4362
|
}
|
|
4135
|
-
|
|
4363
|
+
function effectiveTenancyPosture(tenancy) {
|
|
4364
|
+
if (!tenancy) return void 0;
|
|
4365
|
+
return (0, import_security.normalizeTenancyPosture)(tenancy.posture) ?? (tenancy.isolationActive ? "isolated" : "single");
|
|
4366
|
+
}
|
|
4367
|
+
async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now(), tenancyPosture) {
|
|
4368
|
+
const admission = await resolveApiKeyAdmission(ql, headers, nowMs, tenancyPosture);
|
|
4369
|
+
return admission.outcome === "admitted" ? admission.principal : void 0;
|
|
4370
|
+
}
|
|
4371
|
+
async function resolveApiKeyAdmission(ql, headers, nowMs = Date.now(), tenancyPosture) {
|
|
4136
4372
|
const apiKey = extractApiKey(headers);
|
|
4137
|
-
if (!apiKey) return
|
|
4138
|
-
if (!ql || typeof ql.find !== "function") return
|
|
4373
|
+
if (!apiKey) return { outcome: "none" };
|
|
4374
|
+
if (!ql || typeof ql.find !== "function") return { outcome: "none" };
|
|
4139
4375
|
let rows;
|
|
4140
4376
|
try {
|
|
4141
4377
|
rows = await ql.find("sys_api_key", {
|
|
@@ -4144,19 +4380,29 @@ async function resolveApiKeyPrincipal(ql, headers, nowMs = Date.now()) {
|
|
|
4144
4380
|
context: { isSystem: true }
|
|
4145
4381
|
});
|
|
4146
4382
|
} catch {
|
|
4147
|
-
return
|
|
4383
|
+
return { outcome: "none" };
|
|
4148
4384
|
}
|
|
4149
4385
|
if (rows && rows.value) rows = rows.value;
|
|
4150
4386
|
const row = Array.isArray(rows) ? rows[0] : void 0;
|
|
4151
|
-
if (!row || row.revoked === true) return
|
|
4387
|
+
if (!row || row.revoked === true) return { outcome: "none" };
|
|
4152
4388
|
const expiresAt = row.expires_at ?? row.expiresAt;
|
|
4153
|
-
if (isExpired(expiresAt, nowMs)) return
|
|
4389
|
+
if (isExpired(expiresAt, nowMs)) return { outcome: "none" };
|
|
4154
4390
|
const userId = row.user_id ?? row.userId;
|
|
4155
|
-
if (!userId || typeof userId !== "string") return
|
|
4391
|
+
if (!userId || typeof userId !== "string") return { outcome: "none" };
|
|
4392
|
+
const tenantId = typeof row.active_organization_id === "string" && row.active_organization_id ? row.active_organization_id : void 0;
|
|
4393
|
+
if (!tenantId && tenancyPosture) {
|
|
4394
|
+
const posture = tenancyPosture;
|
|
4395
|
+
if ((0, import_security.postureEnforcesWall)(posture) && !(0, import_security.postureUsesUnionScope)(posture)) {
|
|
4396
|
+
return {
|
|
4397
|
+
outcome: "refused",
|
|
4398
|
+
reason: "organization_required",
|
|
4399
|
+
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."
|
|
4400
|
+
};
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4156
4403
|
return {
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
scopes: parseScopes(row.scopes)
|
|
4404
|
+
outcome: "admitted",
|
|
4405
|
+
principal: { userId, tenantId, scopes: parseScopes(row.scopes) }
|
|
4160
4406
|
};
|
|
4161
4407
|
}
|
|
4162
4408
|
function readHeader(headers, name) {
|
|
@@ -4184,6 +4430,7 @@ function safeJsonParse(s, fallback) {
|
|
|
4184
4430
|
|
|
4185
4431
|
// src/security/resolve-authz-context.ts
|
|
4186
4432
|
var import_spec = require("@objectstack/spec");
|
|
4433
|
+
var import_security2 = require("@objectstack/spec/security");
|
|
4187
4434
|
|
|
4188
4435
|
// src/security/grant-validity.ts
|
|
4189
4436
|
function toEpochMs(value) {
|
|
@@ -4266,6 +4513,15 @@ function postureVisibleRows(posture, rows, principal) {
|
|
|
4266
4513
|
}
|
|
4267
4514
|
}
|
|
4268
4515
|
|
|
4516
|
+
// src/security/row-active.ts
|
|
4517
|
+
var DEACTIVATED_VALUES = [false, 0, "0", "false"];
|
|
4518
|
+
function isRowActive(row) {
|
|
4519
|
+
if (!row) return false;
|
|
4520
|
+
const value = row.active;
|
|
4521
|
+
if (value === void 0 || value === null) return true;
|
|
4522
|
+
return !DEACTIVATED_VALUES.includes(value);
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4269
4525
|
// src/security/resolve-authz-context.ts
|
|
4270
4526
|
function safeJsonParse2(s, fallback) {
|
|
4271
4527
|
try {
|
|
@@ -4295,7 +4551,12 @@ async function resolveAuthzContext(input) {
|
|
|
4295
4551
|
};
|
|
4296
4552
|
let userId;
|
|
4297
4553
|
let tenantId;
|
|
4298
|
-
const
|
|
4554
|
+
const admission = await resolveApiKeyAdmission(ql, headers, input.nowMs, input.tenancyPosture);
|
|
4555
|
+
if (admission.outcome === "refused") {
|
|
4556
|
+
ctx.authRefusal = { reason: admission.reason, message: admission.message };
|
|
4557
|
+
return ctx;
|
|
4558
|
+
}
|
|
4559
|
+
const keyPrincipal = admission.outcome === "admitted" ? admission.principal : void 0;
|
|
4299
4560
|
if (keyPrincipal) {
|
|
4300
4561
|
userId = keyPrincipal.userId;
|
|
4301
4562
|
tenantId = keyPrincipal.tenantId;
|
|
@@ -4323,6 +4584,22 @@ async function resolveAuthzContext(input) {
|
|
|
4323
4584
|
seedPermissions: ctx.permissions,
|
|
4324
4585
|
seedEmail: ctx.email
|
|
4325
4586
|
});
|
|
4587
|
+
if (keyPrincipal?.tenantId && input.tenancyPosture) {
|
|
4588
|
+
const posture = input.tenancyPosture;
|
|
4589
|
+
if ((0, import_security2.postureEnforcesWall)(posture) && !grants.accessible_org_ids.includes(keyPrincipal.tenantId)) {
|
|
4590
|
+
return {
|
|
4591
|
+
positions: [],
|
|
4592
|
+
permissions: [],
|
|
4593
|
+
systemPermissions: [],
|
|
4594
|
+
org_user_ids: [],
|
|
4595
|
+
accessible_org_ids: [],
|
|
4596
|
+
authRefusal: {
|
|
4597
|
+
reason: "organization_membership_ended",
|
|
4598
|
+
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."
|
|
4599
|
+
}
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4326
4603
|
ctx.positions = grants.positions;
|
|
4327
4604
|
ctx.permissions = grants.permissions;
|
|
4328
4605
|
ctx.systemPermissions = grants.systemPermissions;
|
|
@@ -4407,7 +4684,13 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4407
4684
|
if (!grants.positions.includes("everyone")) grants.positions.push("everyone");
|
|
4408
4685
|
if (grants.positions.length > 0) {
|
|
4409
4686
|
const positionRows = await tryFind(ql, "sys_position", { name: { $in: grants.positions } }, 100);
|
|
4410
|
-
const
|
|
4687
|
+
const deactivatedNames = new Set(
|
|
4688
|
+
positionRows.filter((r) => !isRowActive(r)).map((r) => r.name).filter(Boolean)
|
|
4689
|
+
);
|
|
4690
|
+
if (deactivatedNames.size > 0) {
|
|
4691
|
+
grants.positions = grants.positions.filter((n) => !deactivatedNames.has(n));
|
|
4692
|
+
}
|
|
4693
|
+
const positionIds = positionRows.filter((r) => isRowActive(r)).map((r) => r.id).filter(Boolean);
|
|
4411
4694
|
if (positionIds.length > 0) {
|
|
4412
4695
|
const rpsRows = await tryFind(ql, "sys_position_permission_set", { position_id: { $in: positionIds } }, 500);
|
|
4413
4696
|
for (const r of rpsRows) {
|
|
@@ -4417,7 +4700,8 @@ async function resolveUserAuthzGrants(ql, userId, opts = {}) {
|
|
|
4417
4700
|
}
|
|
4418
4701
|
}
|
|
4419
4702
|
if (psIds.size > 0) {
|
|
4420
|
-
const
|
|
4703
|
+
const psRowsAll = await tryFind(ql, "sys_permission_set", { id: { $in: Array.from(psIds) } }, 500);
|
|
4704
|
+
const psRows = psRowsAll.filter((r) => isRowActive(r));
|
|
4421
4705
|
const tabRank = { hidden: 0, default_off: 1, default_on: 2, visible: 3 };
|
|
4422
4706
|
const mergedTabs = {};
|
|
4423
4707
|
for (const ps of psRows) {
|
|
@@ -4648,6 +4932,7 @@ var ANONYMOUS_DENY_CODE = "UNAUTHENTICATED";
|
|
|
4648
4932
|
var ANONYMOUS_DENY_MESSAGE = "Authentication is required to access this endpoint.";
|
|
4649
4933
|
var ANONYMOUS_DENY_BODY = {
|
|
4650
4934
|
error: ANONYMOUS_DENY_CODE,
|
|
4935
|
+
code: ANONYMOUS_DENY_CODE,
|
|
4651
4936
|
message: ANONYMOUS_DENY_MESSAGE
|
|
4652
4937
|
};
|
|
4653
4938
|
function shouldDenyAnonymous(input) {
|
|
@@ -4661,6 +4946,88 @@ function shouldDenyAnonymous(input) {
|
|
|
4661
4946
|
return true;
|
|
4662
4947
|
}
|
|
4663
4948
|
|
|
4949
|
+
// src/security/admin-standing-surface.ts
|
|
4950
|
+
var ADMIN_STANDING_SURFACE = {
|
|
4951
|
+
sys_permission_set: {
|
|
4952
|
+
role: "derives",
|
|
4953
|
+
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.",
|
|
4954
|
+
columns: [
|
|
4955
|
+
"id",
|
|
4956
|
+
"name",
|
|
4957
|
+
"active",
|
|
4958
|
+
"system_permissions",
|
|
4959
|
+
"systemPermissions",
|
|
4960
|
+
"tab_permissions",
|
|
4961
|
+
"tabPermissions"
|
|
4962
|
+
]
|
|
4963
|
+
},
|
|
4964
|
+
sys_user_permission_set: {
|
|
4965
|
+
role: "derives",
|
|
4966
|
+
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.",
|
|
4967
|
+
columns: [
|
|
4968
|
+
"user_id",
|
|
4969
|
+
"permission_set_id",
|
|
4970
|
+
"permissionSetId",
|
|
4971
|
+
"organization_id",
|
|
4972
|
+
"organizationId",
|
|
4973
|
+
"valid_from",
|
|
4974
|
+
"validFrom",
|
|
4975
|
+
"valid_until",
|
|
4976
|
+
"validUntil"
|
|
4977
|
+
]
|
|
4978
|
+
},
|
|
4979
|
+
sys_member: {
|
|
4980
|
+
role: "derives",
|
|
4981
|
+
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.",
|
|
4982
|
+
columns: [
|
|
4983
|
+
"user_id",
|
|
4984
|
+
"userId",
|
|
4985
|
+
"organization_id",
|
|
4986
|
+
"organizationId",
|
|
4987
|
+
"role",
|
|
4988
|
+
"valid_from",
|
|
4989
|
+
"validFrom",
|
|
4990
|
+
"valid_until",
|
|
4991
|
+
"validUntil"
|
|
4992
|
+
]
|
|
4993
|
+
},
|
|
4994
|
+
sys_user: {
|
|
4995
|
+
role: "reads-only",
|
|
4996
|
+
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."
|
|
4997
|
+
},
|
|
4998
|
+
sys_user_position: {
|
|
4999
|
+
role: "reads-only",
|
|
5000
|
+
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."
|
|
5001
|
+
},
|
|
5002
|
+
sys_position: {
|
|
5003
|
+
role: "reads-only",
|
|
5004
|
+
reason: "Read to drop DEACTIVATED positions (ADR-0049, \xA76a). Same reason as `sys_user_position`: the position path cannot reach `hasPlatformAdminGrant`."
|
|
5005
|
+
},
|
|
5006
|
+
sys_position_permission_set: {
|
|
5007
|
+
role: "reads-only",
|
|
5008
|
+
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."
|
|
5009
|
+
}
|
|
5010
|
+
};
|
|
5011
|
+
function adminStandingTables() {
|
|
5012
|
+
return Object.entries(ADMIN_STANDING_SURFACE).filter(([, t]) => t.role === "derives").map(([name]) => name).sort();
|
|
5013
|
+
}
|
|
5014
|
+
function adminStandingColumns(table) {
|
|
5015
|
+
const entry = ADMIN_STANDING_SURFACE[table];
|
|
5016
|
+
return entry?.role === "derives" ? entry.columns : void 0;
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
// src/security/audience-binding-suggestion-status.ts
|
|
5020
|
+
var AUDIENCE_BINDING_SUGGESTION_STATUSES = {
|
|
5021
|
+
pending: true,
|
|
5022
|
+
confirmed: true,
|
|
5023
|
+
dismissed: true
|
|
5024
|
+
};
|
|
5025
|
+
var AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES = Object.keys(
|
|
5026
|
+
AUDIENCE_BINDING_SUGGESTION_STATUSES
|
|
5027
|
+
);
|
|
5028
|
+
var isAudienceBindingSuggestionStatus = (value) => Object.prototype.hasOwnProperty.call(AUDIENCE_BINDING_SUGGESTION_STATUSES, value);
|
|
5029
|
+
var unknownAudienceBindingSuggestionStatusMessage = (value) => `Unknown status filter '${value}' \u2014 expected one of: ${AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES.join(", ")}`;
|
|
5030
|
+
|
|
4664
5031
|
// src/security/operation-private-keys.ts
|
|
4665
5032
|
var OPERATION_PRIVATE_KEY_PREFIX = "__";
|
|
4666
5033
|
function withoutOperationPrivateKeys(exec) {
|
|
@@ -4699,10 +5066,26 @@ function calendarPartsInTzOrUtc(d, tz) {
|
|
|
4699
5066
|
}
|
|
4700
5067
|
function zonedDateStartToUtcMs(ymd2, tz) {
|
|
4701
5068
|
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd2);
|
|
4702
|
-
|
|
5069
|
+
if (!m) return NaN;
|
|
5070
|
+
return zonedWallClockToUtcMs(
|
|
5071
|
+
{ year: Number(m[1]), month: Number(m[2]), day: Number(m[3]) },
|
|
5072
|
+
tz
|
|
5073
|
+
);
|
|
5074
|
+
}
|
|
5075
|
+
function zonedWallClockToUtcMs(parts, tz) {
|
|
5076
|
+
const wallAsUtc = Date.UTC(
|
|
5077
|
+
parts.year,
|
|
5078
|
+
parts.month - 1,
|
|
5079
|
+
parts.day,
|
|
5080
|
+
parts.hour ?? 0,
|
|
5081
|
+
parts.minute ?? 0,
|
|
5082
|
+
parts.second ?? 0,
|
|
5083
|
+
parts.millisecond ?? 0
|
|
5084
|
+
);
|
|
4703
5085
|
if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
|
|
4704
5086
|
try {
|
|
4705
5087
|
const offsetAt = (t) => {
|
|
5088
|
+
const whole = Math.floor(t / 1e3) * 1e3;
|
|
4706
5089
|
const p = new Intl.DateTimeFormat("en-US", {
|
|
4707
5090
|
timeZone: tz,
|
|
4708
5091
|
hourCycle: "h23",
|
|
@@ -4712,9 +5095,9 @@ function zonedDateStartToUtcMs(ymd2, tz) {
|
|
|
4712
5095
|
hour: "2-digit",
|
|
4713
5096
|
minute: "2-digit",
|
|
4714
5097
|
second: "2-digit"
|
|
4715
|
-
}).formatToParts(new Date(
|
|
5098
|
+
}).formatToParts(new Date(whole));
|
|
4716
5099
|
const g = (k) => Number(p.find((x) => x.type === k)?.value);
|
|
4717
|
-
return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) -
|
|
5100
|
+
return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - whole;
|
|
4718
5101
|
};
|
|
4719
5102
|
const off1 = offsetAt(wallAsUtc - offsetAt(wallAsUtc));
|
|
4720
5103
|
return wallAsUtc - off1;
|
|
@@ -4914,9 +5297,30 @@ async function bulkWrite(rows, opts) {
|
|
|
4914
5297
|
return results;
|
|
4915
5298
|
}
|
|
4916
5299
|
|
|
5300
|
+
// src/utils/internal-write-response.ts
|
|
5301
|
+
function collectInternalWriteResponseFields(schema) {
|
|
5302
|
+
const fields = schema?.fields;
|
|
5303
|
+
if (!fields || typeof fields !== "object") return [];
|
|
5304
|
+
const out = [];
|
|
5305
|
+
for (const [name, def] of Object.entries(fields)) {
|
|
5306
|
+
if (def && def.internal === true) out.push(name);
|
|
5307
|
+
}
|
|
5308
|
+
return out;
|
|
5309
|
+
}
|
|
5310
|
+
function omitInternalFieldsFromWriteResponse(schema, records) {
|
|
5311
|
+
if (!records) return;
|
|
5312
|
+
const internalFields = collectInternalWriteResponseFields(schema);
|
|
5313
|
+
if (internalFields.length === 0) return;
|
|
5314
|
+
const list = Array.isArray(records) ? records : [records];
|
|
5315
|
+
for (const row of list) {
|
|
5316
|
+
if (!row || typeof row !== "object") continue;
|
|
5317
|
+
for (const field of internalFields) delete row[field];
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5320
|
+
|
|
4917
5321
|
// src/utils/migration-journal.ts
|
|
4918
5322
|
var import_node_crypto3 = require("crypto");
|
|
4919
|
-
var
|
|
5323
|
+
var import_system4 = require("@objectstack/spec/system");
|
|
4920
5324
|
var SYSTEM_CTX = { isSystem: true };
|
|
4921
5325
|
var DEFAULT_CHUNK_SIZE = 200;
|
|
4922
5326
|
function engineCanRollBack(engine) {
|
|
@@ -4974,14 +5378,14 @@ function hashMigrationPlan(plan, chunks) {
|
|
|
4974
5378
|
}
|
|
4975
5379
|
async function appendEvent(engine, event, execContext) {
|
|
4976
5380
|
await engine.insert(
|
|
4977
|
-
|
|
5381
|
+
import_system4.MIGRATION_JOURNAL_OBJECT,
|
|
4978
5382
|
{ ...event, created_at: event.created_at ?? (/* @__PURE__ */ new Date()).toISOString() },
|
|
4979
5383
|
{ context: execContext ?? { ...SYSTEM_CTX } }
|
|
4980
5384
|
);
|
|
4981
5385
|
}
|
|
4982
5386
|
async function readRunJournal(engine, runId) {
|
|
4983
5387
|
const rows = await engine.find(
|
|
4984
|
-
|
|
5388
|
+
import_system4.MIGRATION_JOURNAL_OBJECT,
|
|
4985
5389
|
{ where: { run_id: runId } },
|
|
4986
5390
|
{ context: { ...SYSTEM_CTX } }
|
|
4987
5391
|
);
|
|
@@ -4996,7 +5400,7 @@ function chunkSetOf(events, kind) {
|
|
|
4996
5400
|
}
|
|
4997
5401
|
async function findInterruptedRuns(engine) {
|
|
4998
5402
|
const started = await engine.find(
|
|
4999
|
-
|
|
5403
|
+
import_system4.MIGRATION_JOURNAL_OBJECT,
|
|
5000
5404
|
{ where: { kind: "run_started" } },
|
|
5001
5405
|
{ context: { ...SYSTEM_CTX } }
|
|
5002
5406
|
);
|
|
@@ -5504,6 +5908,46 @@ function filterTokenContextFrom(execCtx, now) {
|
|
|
5504
5908
|
};
|
|
5505
5909
|
}
|
|
5506
5910
|
|
|
5911
|
+
// src/utils/temporal-comparand.ts
|
|
5912
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
5913
|
+
function temporalComparandKind(fieldType) {
|
|
5914
|
+
if (fieldType === "datetime") return "datetime";
|
|
5915
|
+
if (fieldType === "date") return "date";
|
|
5916
|
+
if (fieldType === "time") return "time";
|
|
5917
|
+
return null;
|
|
5918
|
+
}
|
|
5919
|
+
function readsAsInstant(s) {
|
|
5920
|
+
if (/^-?\d+$/.test(s)) return Number.isFinite(new Date(Number(s)).getTime());
|
|
5921
|
+
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;
|
|
5922
|
+
return Number.isFinite(Date.parse(iso));
|
|
5923
|
+
}
|
|
5924
|
+
function readsAsCalendarDay(s) {
|
|
5925
|
+
return /^\d{4}-\d{2}-\d{2}/.test(s);
|
|
5926
|
+
}
|
|
5927
|
+
function readsAsWallClock(s) {
|
|
5928
|
+
const m = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/.exec(s);
|
|
5929
|
+
if (!m) return false;
|
|
5930
|
+
return Number(m[1]) <= 23 && Number(m[2]) <= 59 && Number(m[3] ?? "0") <= 59;
|
|
5931
|
+
}
|
|
5932
|
+
function isUninterpretableTemporalComparand(kind, value) {
|
|
5933
|
+
if (typeof value !== "string") return false;
|
|
5934
|
+
const s = value.trim();
|
|
5935
|
+
if (s === "") return false;
|
|
5936
|
+
if ((0, import_data3.classifyFilterToken)(value) !== null) return false;
|
|
5937
|
+
if (kind === "datetime") return !readsAsInstant(s);
|
|
5938
|
+
if (kind === "date") return !readsAsCalendarDay(s);
|
|
5939
|
+
return !(readsAsWallClock(s) || readsAsInstant(s));
|
|
5940
|
+
}
|
|
5941
|
+
|
|
5942
|
+
// src/utils/record-not-found.ts
|
|
5943
|
+
function recordNotFoundError(object, id) {
|
|
5944
|
+
const err = new Error(`Record ${id} not found in ${object}`);
|
|
5945
|
+
err.code = "RECORD_NOT_FOUND";
|
|
5946
|
+
err.status = 404;
|
|
5947
|
+
err.object = object;
|
|
5948
|
+
return err;
|
|
5949
|
+
}
|
|
5950
|
+
|
|
5507
5951
|
// src/health-monitor.ts
|
|
5508
5952
|
var PluginHealthMonitor = class {
|
|
5509
5953
|
constructor(logger) {
|
|
@@ -6480,13 +6924,19 @@ var NamespaceResolver = class {
|
|
|
6480
6924
|
return `${shortName}_${ns}`;
|
|
6481
6925
|
}
|
|
6482
6926
|
};
|
|
6927
|
+
|
|
6928
|
+
// src/index.ts
|
|
6929
|
+
var import_contracts = require("@objectstack/spec/contracts");
|
|
6483
6930
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6484
6931
|
0 && (module.exports = {
|
|
6932
|
+
ADMIN_STANDING_SURFACE,
|
|
6485
6933
|
ANONYMOUS_DENY_BODY,
|
|
6486
6934
|
ANONYMOUS_DENY_CODE,
|
|
6487
6935
|
ANONYMOUS_DENY_MESSAGE,
|
|
6488
6936
|
ANONYMOUS_DENY_STATUS,
|
|
6489
6937
|
API_KEY_PREFIX,
|
|
6938
|
+
AUDIENCE_BINDING_SUGGESTION_STATUSES,
|
|
6939
|
+
AUDIENCE_BINDING_SUGGESTION_STATUS_VALUES,
|
|
6490
6940
|
CORE_FALLBACK_FACTORIES,
|
|
6491
6941
|
DependencyResolver,
|
|
6492
6942
|
ENTRY_EXECUTION_CONTEXT_FIELDS,
|
|
@@ -6515,16 +6965,22 @@ var NamespaceResolver = class {
|
|
|
6515
6965
|
SecurePluginContext,
|
|
6516
6966
|
SemanticVersionManager,
|
|
6517
6967
|
ServiceLifecycle,
|
|
6968
|
+
UNMATCHED_ROUTE_PATTERN,
|
|
6518
6969
|
UnknownFilterTokenError,
|
|
6519
6970
|
UnresolvedFilterTokenError,
|
|
6971
|
+
adminStandingColumns,
|
|
6972
|
+
adminStandingTables,
|
|
6520
6973
|
assembleExecutionContext,
|
|
6521
6974
|
assembleExecutionContextOrGuest,
|
|
6522
6975
|
assertInitServiceRequirements,
|
|
6976
|
+
assertMetadataRegisterContract,
|
|
6523
6977
|
bucketKeyToCalendarRange,
|
|
6524
6978
|
buildPermissionsFromGrants,
|
|
6525
6979
|
bulkWrite,
|
|
6526
6980
|
calendarPartsInTz,
|
|
6527
6981
|
calendarPartsInTzOrUtc,
|
|
6982
|
+
canonicalMetadataServiceType,
|
|
6983
|
+
collectInternalWriteResponseFields,
|
|
6528
6984
|
counterSignPayload,
|
|
6529
6985
|
createLogger,
|
|
6530
6986
|
createMemoryCache,
|
|
@@ -6538,6 +6994,7 @@ var NamespaceResolver = class {
|
|
|
6538
6994
|
defaultIsTransientError,
|
|
6539
6995
|
derivePosture,
|
|
6540
6996
|
describeInitOrderFault,
|
|
6997
|
+
effectiveTenancyPosture,
|
|
6541
6998
|
engineCanRollBack,
|
|
6542
6999
|
evaluateAuthGate,
|
|
6543
7000
|
extractApiKey,
|
|
@@ -6549,19 +7006,25 @@ var NamespaceResolver = class {
|
|
|
6549
7006
|
getMemoryUsage,
|
|
6550
7007
|
hashApiKey,
|
|
6551
7008
|
hashMigrationPlan,
|
|
7009
|
+
isAudienceBindingSuggestionStatus,
|
|
6552
7010
|
isAuthGateAllowlisted,
|
|
6553
7011
|
isExpired,
|
|
6554
7012
|
isGrantActive,
|
|
6555
7013
|
isGrantExpired,
|
|
6556
7014
|
isNode,
|
|
7015
|
+
isRowActive,
|
|
7016
|
+
isUninterpretableTemporalComparand,
|
|
6557
7017
|
nextUtcCalendarDay,
|
|
6558
7018
|
normalizeAuthGate,
|
|
7019
|
+
omitInternalFieldsFromWriteResponse,
|
|
6559
7020
|
parseScopes,
|
|
6560
7021
|
parseSignature,
|
|
6561
7022
|
planChunks,
|
|
6562
7023
|
postureVisibleRows,
|
|
6563
7024
|
readAuthoredTranslationLayer,
|
|
6564
7025
|
readRunJournal,
|
|
7026
|
+
recordNotFoundError,
|
|
7027
|
+
resolveApiKeyAdmission,
|
|
6565
7028
|
resolveApiKeyPrincipal,
|
|
6566
7029
|
resolveAuthzContext,
|
|
6567
7030
|
resolveFilterToken,
|
|
@@ -6575,6 +7038,8 @@ var NamespaceResolver = class {
|
|
|
6575
7038
|
safeExit,
|
|
6576
7039
|
shouldDenyAnonymous,
|
|
6577
7040
|
signPayload,
|
|
7041
|
+
temporalComparandKind,
|
|
7042
|
+
unknownAudienceBindingSuggestionStatusMessage,
|
|
6578
7043
|
utcInstantMs,
|
|
6579
7044
|
validateInitServiceContract,
|
|
6580
7045
|
verifyPayload,
|
|
@@ -6584,6 +7049,7 @@ var NamespaceResolver = class {
|
|
|
6584
7049
|
wireAuthoredTranslationSync,
|
|
6585
7050
|
withTransientRetry,
|
|
6586
7051
|
withoutOperationPrivateKeys,
|
|
6587
|
-
zonedDateStartToUtcMs
|
|
7052
|
+
zonedDateStartToUtcMs,
|
|
7053
|
+
zonedWallClockToUtcMs
|
|
6588
7054
|
});
|
|
6589
7055
|
//# sourceMappingURL=index.cjs.map
|