@objectstack/runtime 10.3.0 → 11.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/LICENSE +202 -93
- package/README.md +3 -3
- package/dist/index.cjs +483 -232
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +60 -6
- package/dist/index.d.ts +60 -6
- package/dist/index.js +491 -237
- package/dist/index.js.map +1 -1
- package/package.json +21 -21
package/dist/index.cjs
CHANGED
|
@@ -1278,7 +1278,7 @@ var init_app_plugin = __esm({
|
|
|
1278
1278
|
} catch (e) {
|
|
1279
1279
|
ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
|
|
1280
1280
|
}
|
|
1281
|
-
const multiTenant =
|
|
1281
|
+
const multiTenant = (0, import_types.resolveMultiOrgEnabled)();
|
|
1282
1282
|
if (multiTenant) {
|
|
1283
1283
|
ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
|
|
1284
1284
|
} else {
|
|
@@ -1575,7 +1575,7 @@ async function createStandaloneStack(config) {
|
|
|
1575
1575
|
const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
|
|
1576
1576
|
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? (0, import_node_path3.resolve)(cwd, "dist/objectstack.json");
|
|
1577
1577
|
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : (0, import_node_path3.resolve)(cwd, artifactPathInput);
|
|
1578
|
-
const dbUrl = cfg.databaseUrl ?? (0, import_types2.readEnvWithDeprecation)("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path3.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path3.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path3.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
|
|
1578
|
+
const dbUrl = cfg.databaseUrl ?? (0, import_types2.readEnvWithDeprecation)("OS_DATABASE_URL", "DATABASE_URL", { silent: true })?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${(0, import_node_path3.resolve)(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${(0, import_node_path3.resolve)(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${(0, import_node_path3.resolve)(resolveObjectStackHome(), "data/standalone.db")}`);
|
|
1579
1579
|
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
1580
1580
|
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
1581
1581
|
let driverPlugin;
|
|
@@ -1736,7 +1736,7 @@ __export(index_exports, {
|
|
|
1736
1736
|
NoopMetricsRegistry: () => import_observability.NoopMetricsRegistry,
|
|
1737
1737
|
OBSERVABILITY_ERRORS_SERVICE: () => import_observability3.OBSERVABILITY_ERRORS_SERVICE,
|
|
1738
1738
|
OBSERVABILITY_METRICS_SERVICE: () => import_observability3.OBSERVABILITY_METRICS_SERVICE,
|
|
1739
|
-
ObjectKernel: () =>
|
|
1739
|
+
ObjectKernel: () => import_core5.ObjectKernel,
|
|
1740
1740
|
ObservabilityServicePlugin: () => ObservabilityServicePlugin,
|
|
1741
1741
|
QuickJSScriptRunner: () => QuickJSScriptRunner,
|
|
1742
1742
|
RUNTIME_METRICS: () => import_observability.RUNTIME_METRICS,
|
|
@@ -1778,7 +1778,7 @@ __export(index_exports, {
|
|
|
1778
1778
|
resolveRequestId: () => resolveRequestId
|
|
1779
1779
|
});
|
|
1780
1780
|
module.exports = __toCommonJS(index_exports);
|
|
1781
|
-
var
|
|
1781
|
+
var import_core5 = require("@objectstack/core");
|
|
1782
1782
|
|
|
1783
1783
|
// src/runtime.ts
|
|
1784
1784
|
var import_core = require("@objectstack/core");
|
|
@@ -2050,7 +2050,7 @@ function safeGet(ctx, name) {
|
|
|
2050
2050
|
}
|
|
2051
2051
|
|
|
2052
2052
|
// src/http-dispatcher.ts
|
|
2053
|
-
var
|
|
2053
|
+
var import_core4 = require("@objectstack/core");
|
|
2054
2054
|
var import_system = require("@objectstack/spec/system");
|
|
2055
2055
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
2056
2056
|
init_package_state_store();
|
|
@@ -2084,10 +2084,8 @@ function checkApiExposure(def, action) {
|
|
|
2084
2084
|
return { allowed: true };
|
|
2085
2085
|
}
|
|
2086
2086
|
|
|
2087
|
-
// src/security/api-key.ts
|
|
2088
|
-
var import_core2 = require("@objectstack/core");
|
|
2089
|
-
|
|
2090
2087
|
// src/security/resolve-execution-context.ts
|
|
2088
|
+
var import_core2 = require("@objectstack/core");
|
|
2091
2089
|
function toHeaders(input) {
|
|
2092
2090
|
if (!input) return new Headers();
|
|
2093
2091
|
if (typeof Headers !== "undefined" && input instanceof Headers) return input;
|
|
@@ -2103,212 +2101,44 @@ function toHeaders(input) {
|
|
|
2103
2101
|
}
|
|
2104
2102
|
return h;
|
|
2105
2103
|
}
|
|
2106
|
-
function safeJsonParse2(s, fallback) {
|
|
2107
|
-
try {
|
|
2108
|
-
return JSON.parse(s);
|
|
2109
|
-
} catch {
|
|
2110
|
-
return fallback;
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
async function tryFind(ql, object, where, limit = 100) {
|
|
2114
|
-
if (!ql || typeof ql.find !== "function") return [];
|
|
2115
|
-
try {
|
|
2116
|
-
let rows = await ql.find(object, { where, limit, context: { isSystem: true } });
|
|
2117
|
-
if (rows && rows.value) rows = rows.value;
|
|
2118
|
-
return Array.isArray(rows) ? rows : [];
|
|
2119
|
-
} catch {
|
|
2120
|
-
return [];
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
function isValidTimeZone(tz) {
|
|
2124
|
-
try {
|
|
2125
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
2126
|
-
return true;
|
|
2127
|
-
} catch {
|
|
2128
|
-
return false;
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
function coerceTimeZone(value) {
|
|
2132
|
-
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
2133
|
-
return s && isValidTimeZone(s) ? s : void 0;
|
|
2134
|
-
}
|
|
2135
|
-
function coerceLocale(value) {
|
|
2136
|
-
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
2137
|
-
return s || void 0;
|
|
2138
|
-
}
|
|
2139
|
-
function coerceCurrency(value) {
|
|
2140
|
-
const s = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
2141
|
-
return /^[A-Z]{3}$/.test(s) ? s : void 0;
|
|
2142
|
-
}
|
|
2143
|
-
async function resolveLocalization(opts, ql, sctx) {
|
|
2144
|
-
try {
|
|
2145
|
-
const settings = await opts.getService("settings");
|
|
2146
|
-
if (settings && typeof settings.get === "function") {
|
|
2147
|
-
const [tzRes, localeRes, currencyRes] = await Promise.all([
|
|
2148
|
-
settings.get("localization", "timezone", sctx).catch(() => void 0),
|
|
2149
|
-
settings.get("localization", "locale", sctx).catch(() => void 0),
|
|
2150
|
-
settings.get("localization", "currency", sctx).catch(() => void 0)
|
|
2151
|
-
]);
|
|
2152
|
-
const tz = coerceTimeZone(tzRes?.value);
|
|
2153
|
-
const locale = coerceLocale(localeRes?.value);
|
|
2154
|
-
const currency = coerceCurrency(currencyRes?.value);
|
|
2155
|
-
if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
|
|
2156
|
-
}
|
|
2157
|
-
} catch {
|
|
2158
|
-
}
|
|
2159
|
-
const tzRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "timezone", scope: "tenant" }, 1);
|
|
2160
|
-
const localeRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "locale", scope: "tenant" }, 1);
|
|
2161
|
-
const currencyRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "currency", scope: "tenant" }, 1);
|
|
2162
|
-
return {
|
|
2163
|
-
timezone: coerceTimeZone(tzRows[0]?.value) ?? "UTC",
|
|
2164
|
-
locale: coerceLocale(localeRows[0]?.value) ?? "en-US",
|
|
2165
|
-
currency: coerceCurrency(currencyRows[0]?.value)
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
2104
|
async function resolveExecutionContext(opts) {
|
|
2169
|
-
const headers = opts.request?.headers;
|
|
2170
|
-
const
|
|
2171
|
-
|
|
2172
|
-
permissions: [],
|
|
2173
|
-
systemPermissions: [],
|
|
2174
|
-
isSystem: false
|
|
2175
|
-
};
|
|
2176
|
-
let userId;
|
|
2177
|
-
let tenantId;
|
|
2178
|
-
const keyPrincipal = await (0, import_core2.resolveApiKeyPrincipal)(await opts.getQl(), headers);
|
|
2179
|
-
if (keyPrincipal) {
|
|
2180
|
-
userId = keyPrincipal.userId;
|
|
2181
|
-
tenantId = keyPrincipal.tenantId;
|
|
2182
|
-
for (const scope of keyPrincipal.scopes) {
|
|
2183
|
-
if (!ctx.permissions.includes(scope)) ctx.permissions.push(scope);
|
|
2184
|
-
}
|
|
2185
|
-
}
|
|
2186
|
-
if (!userId) {
|
|
2105
|
+
const headers = toHeaders(opts.request?.headers);
|
|
2106
|
+
const ql = await opts.getQl();
|
|
2107
|
+
const getSession = async (h) => {
|
|
2187
2108
|
try {
|
|
2188
2109
|
const authService = await opts.getService("auth");
|
|
2189
2110
|
let api = authService?.api;
|
|
2190
|
-
if (!api && typeof authService?.getApi === "function")
|
|
2191
|
-
|
|
2192
|
-
}
|
|
2193
|
-
const headersInstance = toHeaders(headers);
|
|
2194
|
-
const sessionData = await api?.getSession?.({ headers: headersInstance });
|
|
2195
|
-
userId = sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
2196
|
-
tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
|
|
2197
|
-
ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
|
|
2198
|
-
if (sessionData?.user?.email) ctx.email = String(sessionData.user.email);
|
|
2111
|
+
if (!api && typeof authService?.getApi === "function") api = await authService.getApi();
|
|
2112
|
+
return await api?.getSession?.({ headers: h });
|
|
2199
2113
|
} catch {
|
|
2114
|
+
return void 0;
|
|
2200
2115
|
}
|
|
2201
|
-
}
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
const userRoleRows = await tryFind(ql, "sys_user_role", { user_id: userId }, 200);
|
|
2221
|
-
for (const ur of userRoleRows) {
|
|
2222
|
-
const org = ur.organization_id ?? null;
|
|
2223
|
-
if (org && tenantId && org !== tenantId) continue;
|
|
2224
|
-
const r = ur.role;
|
|
2225
|
-
if (typeof r === "string" && r && !ctx.roles.includes(r)) ctx.roles.push(r);
|
|
2226
|
-
}
|
|
2227
|
-
if (tenantId) {
|
|
2228
|
-
const orgMembers = await tryFind(
|
|
2229
|
-
ql,
|
|
2230
|
-
"sys_member",
|
|
2231
|
-
{ organization_id: tenantId },
|
|
2232
|
-
1e3
|
|
2233
|
-
);
|
|
2234
|
-
const orgUserIds = Array.from(
|
|
2235
|
-
new Set(
|
|
2236
|
-
orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
|
|
2237
|
-
)
|
|
2238
|
-
);
|
|
2239
|
-
if (!orgUserIds.includes(userId)) orgUserIds.push(userId);
|
|
2240
|
-
ctx.org_user_ids = orgUserIds;
|
|
2241
|
-
} else {
|
|
2242
|
-
ctx.org_user_ids = [userId];
|
|
2243
|
-
}
|
|
2244
|
-
const upsRows = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
|
|
2245
|
-
const psIds = new Set(
|
|
2246
|
-
upsRows.filter((r) => {
|
|
2247
|
-
const org = r.organization_id ?? r.organizationId ?? null;
|
|
2248
|
-
return !(org && tenantId && org !== tenantId);
|
|
2249
|
-
}).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
|
|
2250
|
-
);
|
|
2251
|
-
if (ctx.roles.length > 0) {
|
|
2252
|
-
const roleRows = await tryFind(ql, "sys_role", { name: { $in: ctx.roles } }, 100);
|
|
2253
|
-
const roleIds = roleRows.map((r) => r.id).filter(Boolean);
|
|
2254
|
-
if (roleIds.length > 0) {
|
|
2255
|
-
const rpsRows = await tryFind(
|
|
2256
|
-
ql,
|
|
2257
|
-
"sys_role_permission_set",
|
|
2258
|
-
{ role_id: { $in: roleIds } },
|
|
2259
|
-
500
|
|
2260
|
-
);
|
|
2261
|
-
for (const r of rpsRows) {
|
|
2262
|
-
const id = r.permission_set_id ?? r.permissionSetId;
|
|
2263
|
-
if (id) psIds.add(id);
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
}
|
|
2267
|
-
if (psIds.size > 0) {
|
|
2268
|
-
const psRows = await tryFind(
|
|
2116
|
+
};
|
|
2117
|
+
const authz = await (0, import_core2.resolveAuthzContext)({ ql, headers, getSession });
|
|
2118
|
+
const ctx = {
|
|
2119
|
+
roles: authz.roles,
|
|
2120
|
+
permissions: authz.permissions,
|
|
2121
|
+
systemPermissions: authz.systemPermissions,
|
|
2122
|
+
isSystem: false
|
|
2123
|
+
};
|
|
2124
|
+
if (authz.userId) ctx.userId = authz.userId;
|
|
2125
|
+
if (authz.tenantId) ctx.tenantId = authz.tenantId;
|
|
2126
|
+
if (authz.email) ctx.email = authz.email;
|
|
2127
|
+
if (authz.accessToken) ctx.accessToken = authz.accessToken;
|
|
2128
|
+
if (authz.tabPermissions) ctx.tabPermissions = authz.tabPermissions;
|
|
2129
|
+
ctx.org_user_ids = authz.org_user_ids;
|
|
2130
|
+
if (authz.userId) {
|
|
2131
|
+
const settings = await Promise.resolve(opts.getService("settings")).catch(() => void 0);
|
|
2132
|
+
const localization = await (0, import_core2.resolveLocalizationContext)({
|
|
2269
2133
|
ql,
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
);
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
default_on: 2,
|
|
2278
|
-
visible: 3
|
|
2279
|
-
};
|
|
2280
|
-
const mergedTabs = {};
|
|
2281
|
-
for (const ps of psRows) {
|
|
2282
|
-
if (ps.name && !ctx.permissions.includes(ps.name)) {
|
|
2283
|
-
ctx.permissions.push(ps.name);
|
|
2284
|
-
}
|
|
2285
|
-
const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
|
|
2286
|
-
if (Array.isArray(sysPerms)) {
|
|
2287
|
-
for (const p of sysPerms) {
|
|
2288
|
-
if (typeof p === "string" && !ctx.systemPermissions.includes(p)) {
|
|
2289
|
-
ctx.systemPermissions.push(p);
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2293
|
-
const tabs = typeof ps.tab_permissions === "string" ? safeJsonParse2(ps.tab_permissions, {}) : ps.tab_permissions ?? ps.tabPermissions;
|
|
2294
|
-
if (tabs && typeof tabs === "object") {
|
|
2295
|
-
for (const [app, val] of Object.entries(tabs)) {
|
|
2296
|
-
if (typeof val !== "string" || !(val in tabRank)) continue;
|
|
2297
|
-
const cur = mergedTabs[app];
|
|
2298
|
-
if (!cur || tabRank[val] > tabRank[cur]) {
|
|
2299
|
-
mergedTabs[app] = val;
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
}
|
|
2304
|
-
if (Object.keys(mergedTabs).length > 0) {
|
|
2305
|
-
ctx.tabPermissions = mergedTabs;
|
|
2306
|
-
}
|
|
2134
|
+
settings,
|
|
2135
|
+
tenantId: authz.tenantId,
|
|
2136
|
+
userId: authz.userId
|
|
2137
|
+
});
|
|
2138
|
+
ctx.timezone = localization.timezone;
|
|
2139
|
+
ctx.locale = localization.locale;
|
|
2140
|
+
if (localization.currency) ctx.currency = localization.currency;
|
|
2307
2141
|
}
|
|
2308
|
-
const localization = await resolveLocalization(opts, ql, { tenantId, userId });
|
|
2309
|
-
ctx.timezone = localization.timezone;
|
|
2310
|
-
ctx.locale = localization.locale;
|
|
2311
|
-
if (localization.currency) ctx.currency = localization.currency;
|
|
2312
2142
|
return ctx;
|
|
2313
2143
|
}
|
|
2314
2144
|
function isPermissionDeniedError(e) {
|
|
@@ -2317,6 +2147,9 @@ function isPermissionDeniedError(e) {
|
|
|
2317
2147
|
return anyE.name === "PermissionDeniedError" || anyE.code === "PERMISSION_DENIED" || typeof anyE.message === "string" && anyE.message.startsWith("[Security] Access denied");
|
|
2318
2148
|
}
|
|
2319
2149
|
|
|
2150
|
+
// src/security/api-key.ts
|
|
2151
|
+
var import_core3 = require("@objectstack/core");
|
|
2152
|
+
|
|
2320
2153
|
// src/http-dispatcher.ts
|
|
2321
2154
|
function randomUUID() {
|
|
2322
2155
|
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") {
|
|
@@ -2328,6 +2161,9 @@ function randomUUID() {
|
|
|
2328
2161
|
return v.toString(16);
|
|
2329
2162
|
});
|
|
2330
2163
|
}
|
|
2164
|
+
function isSystemObjectName(name) {
|
|
2165
|
+
return /^sys_/i.test(name);
|
|
2166
|
+
}
|
|
2331
2167
|
var _HttpDispatcher = class _HttpDispatcher {
|
|
2332
2168
|
/**
|
|
2333
2169
|
* @param _envRegistryIgnored — RETIRED (ADR-0006 Phase 5). Environment
|
|
@@ -2445,7 +2281,10 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2445
2281
|
return extra ? { ...base, ...extra } : qlOpts ? base : void 0;
|
|
2446
2282
|
};
|
|
2447
2283
|
if (action === "create") {
|
|
2448
|
-
if (
|
|
2284
|
+
if (protocol && typeof protocol.createData === "function") {
|
|
2285
|
+
return await protocol.createData({ object: params.object, data: params.data, ...scopeId ? { environmentId: scopeId } : {}, context: executionContext });
|
|
2286
|
+
}
|
|
2287
|
+
if (ql && typeof ql.insert === "function") {
|
|
2449
2288
|
const res = await ql.insert(params.object, params.data, qlOpts);
|
|
2450
2289
|
const record = { ...params.data, ...res };
|
|
2451
2290
|
return { object: params.object, id: record.id, record };
|
|
@@ -2466,7 +2305,10 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2466
2305
|
throw { statusCode: 503, message: "Data service not available" };
|
|
2467
2306
|
}
|
|
2468
2307
|
if (action === "update") {
|
|
2469
|
-
if (
|
|
2308
|
+
if (protocol && typeof protocol.updateData === "function") {
|
|
2309
|
+
return await protocol.updateData({ object: params.object, id: params.id, data: params.data, ...scopeId ? { environmentId: scopeId } : {}, context: executionContext });
|
|
2310
|
+
}
|
|
2311
|
+
if (ql && params.id && typeof ql.update === "function") {
|
|
2470
2312
|
let all = await ql.find(params.object, findOpts({ where: { id: params.id }, limit: 1 }));
|
|
2471
2313
|
if (all && all.value) all = all.value;
|
|
2472
2314
|
if (!all) all = [];
|
|
@@ -2478,7 +2320,10 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2478
2320
|
throw { statusCode: 503, message: "Data service not available" };
|
|
2479
2321
|
}
|
|
2480
2322
|
if (action === "delete") {
|
|
2481
|
-
if (
|
|
2323
|
+
if (protocol && typeof protocol.deleteData === "function") {
|
|
2324
|
+
return await protocol.deleteData({ object: params.object, id: params.id, ...scopeId ? { environmentId: scopeId } : {}, context: executionContext });
|
|
2325
|
+
}
|
|
2326
|
+
if (ql && typeof ql.delete === "function") {
|
|
2482
2327
|
await ql.delete(params.object, findOpts({ where: { id: params.id } }));
|
|
2483
2328
|
return { object: params.object, id: params.id, deleted: true };
|
|
2484
2329
|
}
|
|
@@ -2670,9 +2515,269 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2670
2515
|
},
|
|
2671
2516
|
create: async (object, data) => await callData("create", { object, data }, driver, envId, ec),
|
|
2672
2517
|
update: async (object, id, data) => await callData("update", { object, id, data }, driver, envId, ec),
|
|
2673
|
-
remove: async (object, id) => await callData("delete", { object, id }, driver, envId, ec)
|
|
2518
|
+
remove: async (object, id) => await callData("delete", { object, id }, driver, envId, ec),
|
|
2519
|
+
// ── Business-action surface (McpActionBridge) ──────────────
|
|
2520
|
+
// Resolution + dispatch flow through the framework's own action
|
|
2521
|
+
// mechanism (engine.executeAction / automation flow runner) bound to
|
|
2522
|
+
// THIS request's ExecutionContext — the same permission + RLS path
|
|
2523
|
+
// the REST `/actions/...` route uses. No `@objectstack/service-ai`.
|
|
2524
|
+
listActions: async () => {
|
|
2525
|
+
const meta = await getMeta();
|
|
2526
|
+
const objs = await meta?.listObjects?.() ?? [];
|
|
2527
|
+
const hasAutomation = Boolean(
|
|
2528
|
+
await this.resolveService("automation", envId).catch(() => null)
|
|
2529
|
+
);
|
|
2530
|
+
const out = [];
|
|
2531
|
+
for (const obj of objs) {
|
|
2532
|
+
const objectName = obj?.name;
|
|
2533
|
+
if (!objectName || isSystemObjectName(objectName)) continue;
|
|
2534
|
+
const actions = Array.isArray(obj?.actions) ? obj.actions : [];
|
|
2535
|
+
for (const action of actions) {
|
|
2536
|
+
if (!action || typeof action.name !== "string") continue;
|
|
2537
|
+
if (!this.isHeadlessInvokableAction(action, hasAutomation)) continue;
|
|
2538
|
+
if (this.actionPermissionError(action, ec)) continue;
|
|
2539
|
+
out.push(this.summarizeAction(action, obj, objectName));
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
return out;
|
|
2543
|
+
},
|
|
2544
|
+
runAction: async (name, input) => this.invokeBusinessAction(name, input ?? {}, { driver, envId, ec, getMeta, callData })
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
// ── MCP action bridge helpers ──────────────────────────────────────
|
|
2548
|
+
/**
|
|
2549
|
+
* [ADR-0066 D4] Shared capability gate for an action invocation. Returns a
|
|
2550
|
+
* human-readable error string when the caller's `systemPermissions` don't
|
|
2551
|
+
* cover the action's declared `requiredPermissions`, or `null` when allowed.
|
|
2552
|
+
* System/engine self-invocation (`isSystem`) bypasses; an action without
|
|
2553
|
+
* `requiredPermissions` is ungated. Single-sourced so the REST `/actions/...`
|
|
2554
|
+
* route and the MCP `run_action` bridge enforce the SAME declaration.
|
|
2555
|
+
*/
|
|
2556
|
+
actionPermissionError(actionDef, ec, objectName) {
|
|
2557
|
+
const required = Array.isArray(actionDef?.requiredPermissions) ? actionDef.requiredPermissions : [];
|
|
2558
|
+
if (required.length === 0) return null;
|
|
2559
|
+
if (ec?.isSystem) return null;
|
|
2560
|
+
const held = new Set(ec?.systemPermissions ?? []);
|
|
2561
|
+
const missing = required.filter((perm) => !held.has(perm));
|
|
2562
|
+
if (missing.length === 0) return null;
|
|
2563
|
+
const on = objectName ? ` on '${objectName}'` : "";
|
|
2564
|
+
return `Action '${actionDef?.name ?? "unknown"}'${on} requires capability [${required.join(", ")}] \u2014 caller is missing [${missing.join(", ")}]`;
|
|
2565
|
+
}
|
|
2566
|
+
/**
|
|
2567
|
+
* Whether an action has a headless invocation path (so MCP can run it).
|
|
2568
|
+
* Mirrors the supported-type set of the (now cloud-side) action-tools
|
|
2569
|
+
* bridge: `script` needs a handler binding (`target`) or an inline `body`;
|
|
2570
|
+
* `flow` needs a `target` and an automation service. UI-only types
|
|
2571
|
+
* (`url`, `modal`, `form`) and `api` have no server dispatch here.
|
|
2572
|
+
*/
|
|
2573
|
+
isHeadlessInvokableAction(action, hasAutomation) {
|
|
2574
|
+
const type = action?.type ?? "script";
|
|
2575
|
+
if (type === "script") return Boolean(action?.target || action?.body);
|
|
2576
|
+
if (type === "flow") return Boolean(action?.target) && hasAutomation;
|
|
2577
|
+
return false;
|
|
2578
|
+
}
|
|
2579
|
+
/** True when an action is destructive by author signal/heuristic (HITL hint). */
|
|
2580
|
+
actionLooksDestructive(action) {
|
|
2581
|
+
if (action?.ai?.requiresConfirmation !== void 0) return Boolean(action.ai.requiresConfirmation);
|
|
2582
|
+
return Boolean(action?.confirmText || action?.mode === "delete" || action?.variant === "danger");
|
|
2583
|
+
}
|
|
2584
|
+
/** Project an action's declarative metadata into a lean MCP summary. */
|
|
2585
|
+
summarizeAction(action, obj, objectName) {
|
|
2586
|
+
const requiresRecord = Array.isArray(action?.locations) && action.locations.some(
|
|
2587
|
+
(l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
|
|
2588
|
+
);
|
|
2589
|
+
const description = (typeof action?.ai?.description === "string" ? action.ai.description : void 0) ?? (typeof action?.label === "string" ? action.label : void 0);
|
|
2590
|
+
const params = this.summarizeActionParams(action, obj);
|
|
2591
|
+
return {
|
|
2592
|
+
name: action.name,
|
|
2593
|
+
objectName,
|
|
2594
|
+
...typeof action?.label === "string" ? { label: action.label } : {},
|
|
2595
|
+
...description ? { description } : {},
|
|
2596
|
+
type: action?.type ?? "script",
|
|
2597
|
+
requiresRecord: Boolean(requiresRecord),
|
|
2598
|
+
requiresConfirmation: this.actionLooksDestructive(action),
|
|
2599
|
+
...params.length > 0 ? { params } : {}
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
/** Map an ObjectStack field type to a JSON-Schema primitive (conservative). */
|
|
2603
|
+
jsonTypeOf(t) {
|
|
2604
|
+
switch (t) {
|
|
2605
|
+
case "number":
|
|
2606
|
+
case "currency":
|
|
2607
|
+
case "percent":
|
|
2608
|
+
case "rating":
|
|
2609
|
+
case "slider":
|
|
2610
|
+
case "autonumber":
|
|
2611
|
+
return "number";
|
|
2612
|
+
case "boolean":
|
|
2613
|
+
case "toggle":
|
|
2614
|
+
return "boolean";
|
|
2615
|
+
case "multiselect":
|
|
2616
|
+
case "checkboxes":
|
|
2617
|
+
case "tags":
|
|
2618
|
+
return "array";
|
|
2619
|
+
default:
|
|
2620
|
+
return "string";
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
/** Resolve an action's params into LLM-facing summaries (field-backed types resolved). */
|
|
2624
|
+
summarizeActionParams(action, obj) {
|
|
2625
|
+
const fields = obj?.fields ?? {};
|
|
2626
|
+
const out = [];
|
|
2627
|
+
for (const p of Array.isArray(action?.params) ? action.params : []) {
|
|
2628
|
+
const fieldRef = p?.field;
|
|
2629
|
+
const field = fieldRef ? fields[fieldRef] : void 0;
|
|
2630
|
+
const name = p?.name ?? fieldRef;
|
|
2631
|
+
if (!name) continue;
|
|
2632
|
+
const type = this.jsonTypeOf(p?.type ?? field?.type);
|
|
2633
|
+
const label = typeof p?.label === "string" ? p.label : field?.label;
|
|
2634
|
+
const help = p?.helpText ?? field?.description;
|
|
2635
|
+
const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
|
|
2636
|
+
const optionSource = p?.options ?? field?.options;
|
|
2637
|
+
const enumVals = Array.isArray(optionSource) ? optionSource.map((o) => typeof o === "string" ? o : o?.value).filter((v) => typeof v === "string") : [];
|
|
2638
|
+
out.push({
|
|
2639
|
+
name,
|
|
2640
|
+
type,
|
|
2641
|
+
required: Boolean(p?.required ?? field?.required ?? false),
|
|
2642
|
+
...description ? { description } : {},
|
|
2643
|
+
...enumVals.length > 0 ? { enum: enumVals } : {}
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
return out;
|
|
2647
|
+
}
|
|
2648
|
+
/** Slim engine facade matching the ActionContext.engine shape handlers expect. */
|
|
2649
|
+
buildActionEngineFacade(ql) {
|
|
2650
|
+
return {
|
|
2651
|
+
async insert(object, data) {
|
|
2652
|
+
const res = await ql.insert(object, data);
|
|
2653
|
+
const id = (res && res.id) ?? data.id;
|
|
2654
|
+
return { id };
|
|
2655
|
+
},
|
|
2656
|
+
async update(object, id, data) {
|
|
2657
|
+
await ql.update(object, data, { where: { id } });
|
|
2658
|
+
},
|
|
2659
|
+
// Tolerant of both the single-id and array conventions handler suites
|
|
2660
|
+
// use (CRM handlers pass one id; todo handlers pass an id array).
|
|
2661
|
+
async delete(object, idOrIds) {
|
|
2662
|
+
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
2663
|
+
for (const id of ids) {
|
|
2664
|
+
if (id != null) await ql.delete(object, { where: { id } });
|
|
2665
|
+
}
|
|
2666
|
+
},
|
|
2667
|
+
async find(object, query) {
|
|
2668
|
+
const opts = query && Object.keys(query).length ? { where: query } : void 0;
|
|
2669
|
+
const rows = await ql.find(object, opts);
|
|
2670
|
+
return Array.isArray(rows) ? rows : rows?.value ?? [];
|
|
2671
|
+
}
|
|
2674
2672
|
};
|
|
2675
2673
|
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Resolve + invoke a business action by its declarative name for the MCP
|
|
2676
|
+
* `run_action` tool. Enforces the ADR-0066 D4 capability gate and RLS as the
|
|
2677
|
+
* caller, loads the subject record under RLS for row-context actions, and
|
|
2678
|
+
* dispatches through the framework's `engine.executeAction` (script/body) or
|
|
2679
|
+
* automation flow runner (flow). Throws on denial / not-found / handler
|
|
2680
|
+
* failure so the tool surfaces a clean tool-error. No service-ai dependency.
|
|
2681
|
+
*/
|
|
2682
|
+
async invokeBusinessAction(name, input, wiring) {
|
|
2683
|
+
const { driver, envId, ec, getMeta, callData } = wiring;
|
|
2684
|
+
const meta = await getMeta();
|
|
2685
|
+
const params = input?.params && typeof input.params === "object" ? input.params : {};
|
|
2686
|
+
const recordId = typeof input?.recordId === "string" && input.recordId.length > 0 ? input.recordId : void 0;
|
|
2687
|
+
const resolved = await this.resolveActionByName(meta, name, input?.objectName);
|
|
2688
|
+
if (!resolved) {
|
|
2689
|
+
throw new Error(
|
|
2690
|
+
input?.objectName ? `Action '${name}' not found on object '${input.objectName}'` : `Action '${name}' not found`
|
|
2691
|
+
);
|
|
2692
|
+
}
|
|
2693
|
+
const { action, objectName } = resolved;
|
|
2694
|
+
if (isSystemObjectName(objectName)) {
|
|
2695
|
+
throw new Error(`Action '${name}' is on a system object and is not exposed via MCP`);
|
|
2696
|
+
}
|
|
2697
|
+
const hasAutomation = Boolean(await this.resolveService("automation", envId).catch(() => null));
|
|
2698
|
+
if (!this.isHeadlessInvokableAction(action, hasAutomation)) {
|
|
2699
|
+
throw new Error(
|
|
2700
|
+
`Action '${name}' (type='${action?.type ?? "script"}') cannot be invoked via MCP`
|
|
2701
|
+
);
|
|
2702
|
+
}
|
|
2703
|
+
const gateError = this.actionPermissionError(action, ec, objectName);
|
|
2704
|
+
if (gateError) throw new Error(gateError);
|
|
2705
|
+
let record = {};
|
|
2706
|
+
if (recordId && objectName !== "global") {
|
|
2707
|
+
try {
|
|
2708
|
+
const got = await callData("get", { object: objectName, id: recordId }, driver, envId, ec);
|
|
2709
|
+
if (got?.record) record = got.record;
|
|
2710
|
+
} catch {
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
if (record && record.id == null && recordId) record.id = recordId;
|
|
2714
|
+
const user = ec?.userId ? { id: ec.userId, name: ec.userName ?? ec.userDisplayName ?? ec.userId } : { id: "system", name: "system" };
|
|
2715
|
+
if (action.type === "flow") {
|
|
2716
|
+
const automation = await this.resolveService("automation", envId).catch(() => null);
|
|
2717
|
+
if (!automation || typeof automation.execute !== "function") {
|
|
2718
|
+
throw new Error(`Action '${name}' is a flow but no automation service is available`);
|
|
2719
|
+
}
|
|
2720
|
+
const result = await automation.execute(action.target, {
|
|
2721
|
+
triggerData: { record, params, user, action: action.name }
|
|
2722
|
+
});
|
|
2723
|
+
if (result && typeof result === "object" && "success" in result && result.success === false) {
|
|
2724
|
+
throw new Error(`Flow '${action.target}' failed: ${result.error ?? "unknown error"}`);
|
|
2725
|
+
}
|
|
2726
|
+
return { ok: true, action: action.name, objectName, ...recordId ? { recordId } : {}, result: result ?? null };
|
|
2727
|
+
}
|
|
2728
|
+
const ql = await this.getObjectQLService(envId);
|
|
2729
|
+
if (!ql || typeof ql.executeAction !== "function") {
|
|
2730
|
+
throw new Error("Data engine not available for action dispatch");
|
|
2731
|
+
}
|
|
2732
|
+
const actionContext = {
|
|
2733
|
+
record,
|
|
2734
|
+
user,
|
|
2735
|
+
engine: this.buildActionEngineFacade(ql),
|
|
2736
|
+
params: { ...params, recordId, objectName }
|
|
2737
|
+
};
|
|
2738
|
+
const primary = action.body ? action.name : action.target || action.name;
|
|
2739
|
+
const candidates = [primary, action.target, action.name].filter(
|
|
2740
|
+
(k, i, a) => typeof k === "string" && a.indexOf(k) === i
|
|
2741
|
+
);
|
|
2742
|
+
const notRegistered = (err) => /Action '.+' on object '.+' not found/i.test(String(err?.message ?? err));
|
|
2743
|
+
for (const obj of [objectName, "*"]) {
|
|
2744
|
+
for (const key of candidates) {
|
|
2745
|
+
try {
|
|
2746
|
+
const result = await ql.executeAction(obj, key, actionContext);
|
|
2747
|
+
return { ok: true, action: action.name, objectName, ...recordId ? { recordId } : {}, result: result ?? null };
|
|
2748
|
+
} catch (err) {
|
|
2749
|
+
if (!notRegistered(err)) throw err;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
throw new Error(`No handler registered for action '${name}' on '${objectName}'`);
|
|
2754
|
+
}
|
|
2755
|
+
/**
|
|
2756
|
+
* Find an action's declarative definition by name across object metadata,
|
|
2757
|
+
* optionally scoped to a single object. Returns the action plus its owning
|
|
2758
|
+
* object name, or `null`. Throws when the name is ambiguous across objects
|
|
2759
|
+
* and no `objectName` was supplied (so `run_action` can ask for one).
|
|
2760
|
+
*/
|
|
2761
|
+
async resolveActionByName(meta, name, objectName) {
|
|
2762
|
+
if (objectName) {
|
|
2763
|
+
const def = await meta?.getObject?.(objectName);
|
|
2764
|
+
const action = Array.isArray(def?.actions) ? def.actions.find((a) => a?.name === name) : void 0;
|
|
2765
|
+
return action ? { action, objectName } : null;
|
|
2766
|
+
}
|
|
2767
|
+
const objs = await meta?.listObjects?.() ?? [];
|
|
2768
|
+
const matches = [];
|
|
2769
|
+
for (const obj of objs) {
|
|
2770
|
+
if (!obj?.name) continue;
|
|
2771
|
+
const action = Array.isArray(obj?.actions) ? obj.actions.find((a) => a?.name === name) : void 0;
|
|
2772
|
+
if (action) matches.push({ action, objectName: obj.name });
|
|
2773
|
+
}
|
|
2774
|
+
if (matches.length === 0) return null;
|
|
2775
|
+
if (matches.length > 1) {
|
|
2776
|
+
const where = matches.map((m) => m.objectName).join(", ");
|
|
2777
|
+
throw new Error(`Action '${name}' exists on multiple objects (${where}); pass objectName to disambiguate`);
|
|
2778
|
+
}
|
|
2779
|
+
return matches[0];
|
|
2780
|
+
}
|
|
2676
2781
|
/**
|
|
2677
2782
|
* Generate a `sys_api_key` and return the raw secret EXACTLY ONCE
|
|
2678
2783
|
* (`POST /keys`). This is the only mint path — the raw key is never stored
|
|
@@ -2719,7 +2824,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2719
2824
|
if (!ql || typeof ql.insert !== "function") {
|
|
2720
2825
|
return { handled: true, response: this.error("Data service not available", 503) };
|
|
2721
2826
|
}
|
|
2722
|
-
const generated = (0,
|
|
2827
|
+
const generated = (0, import_core3.generateApiKey)();
|
|
2723
2828
|
const row = {
|
|
2724
2829
|
name,
|
|
2725
2830
|
key: generated.hash,
|
|
@@ -2804,6 +2909,43 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2804
2909
|
* response object that callers should surface directly — no further
|
|
2805
2910
|
* dispatch happens.
|
|
2806
2911
|
*/
|
|
2912
|
+
/**
|
|
2913
|
+
* ADR-0069 — returns a 403 response when the resolved session is blocked by
|
|
2914
|
+
* an auth-policy gate (expired password / required MFA) on a non-allow-listed
|
|
2915
|
+
* path, else null. Mirrors the REST `enforceAuth` seam so REST + dispatcher
|
|
2916
|
+
* (MCP, GraphQL) enforce consistently. Fails open on any lookup error.
|
|
2917
|
+
*/
|
|
2918
|
+
async enforceAuthGate(context, cleanPath) {
|
|
2919
|
+
try {
|
|
2920
|
+
if ((0, import_core4.isAuthGateAllowlisted)(cleanPath)) return null;
|
|
2921
|
+
const authService = await this.resolveService("auth", context.environmentId);
|
|
2922
|
+
if (!authService || typeof authService.isAuthGateActive !== "function" || !authService.isAuthGateActive()) {
|
|
2923
|
+
return null;
|
|
2924
|
+
}
|
|
2925
|
+
let api = authService.api;
|
|
2926
|
+
if (!api && typeof authService.getApi === "function") api = await authService.getApi();
|
|
2927
|
+
if (!api?.getSession) return null;
|
|
2928
|
+
const raw = context?.request?.headers;
|
|
2929
|
+
let headers;
|
|
2930
|
+
if (raw && typeof raw.get === "function") {
|
|
2931
|
+
headers = raw;
|
|
2932
|
+
} else if (raw && typeof raw === "object") {
|
|
2933
|
+
headers = new globalThis.Headers();
|
|
2934
|
+
for (const k of Object.keys(raw)) {
|
|
2935
|
+
const v = raw[k];
|
|
2936
|
+
if (v != null) headers.set(String(k), Array.isArray(v) ? v.join(",") : String(v));
|
|
2937
|
+
}
|
|
2938
|
+
} else {
|
|
2939
|
+
return null;
|
|
2940
|
+
}
|
|
2941
|
+
const session = await api.getSession({ headers }).catch(() => void 0);
|
|
2942
|
+
const gate = (0, import_core4.evaluateAuthGate)(session?.user, cleanPath);
|
|
2943
|
+
if (!gate) return null;
|
|
2944
|
+
return this.error(gate.message, 403, { code: gate.code });
|
|
2945
|
+
} catch {
|
|
2946
|
+
return null;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2807
2949
|
async enforceProjectMembership(context, path) {
|
|
2808
2950
|
if (!this.enforceMembership) return null;
|
|
2809
2951
|
const skipPaths = ["/auth", "/cloud", "/health", "/ready", "/discovery"];
|
|
@@ -2961,7 +3103,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2961
3103
|
return {
|
|
2962
3104
|
name: "ObjectOS",
|
|
2963
3105
|
version: "1.0.0",
|
|
2964
|
-
environment: (0,
|
|
3106
|
+
environment: (0, import_core4.getEnv)("NODE_ENV", "development"),
|
|
2965
3107
|
routes,
|
|
2966
3108
|
endpoints: routes,
|
|
2967
3109
|
// Alias for backward compatibility with some clients
|
|
@@ -3466,7 +3608,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3466
3608
|
let translations = i18nService.getTranslations(locale);
|
|
3467
3609
|
if (Object.keys(translations).length === 0) {
|
|
3468
3610
|
const availableLocales = typeof i18nService.getLocales === "function" ? i18nService.getLocales() : [];
|
|
3469
|
-
const resolved = (0,
|
|
3611
|
+
const resolved = (0, import_core4.resolveLocale)(locale, availableLocales);
|
|
3470
3612
|
if (resolved && resolved !== locale) {
|
|
3471
3613
|
translations = i18nService.getTranslations(resolved);
|
|
3472
3614
|
return { handled: true, response: this.success({ locale: resolved, requestedLocale: locale, translations }) };
|
|
@@ -3479,7 +3621,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3479
3621
|
let locale = parts[2] ? decodeURIComponent(parts[2]) : query?.locale;
|
|
3480
3622
|
if (!locale) return { handled: true, response: this.error("Missing locale parameter", 400) };
|
|
3481
3623
|
const availableLocales = typeof i18nService.getLocales === "function" ? i18nService.getLocales() : [];
|
|
3482
|
-
const resolved = (0,
|
|
3624
|
+
const resolved = (0, import_core4.resolveLocale)(locale, availableLocales);
|
|
3483
3625
|
if (resolved) locale = resolved;
|
|
3484
3626
|
if (typeof i18nService.getFieldLabels === "function") {
|
|
3485
3627
|
const labels2 = i18nService.getFieldLabels(objectName, locale);
|
|
@@ -3653,6 +3795,61 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3653
3795
|
}
|
|
3654
3796
|
return { handled: true, response: this.error("Draft discarding not supported", 501) };
|
|
3655
3797
|
}
|
|
3798
|
+
if (parts.length === 2 && parts[1] === "commits" && m === "GET") {
|
|
3799
|
+
const id = decodeURIComponent(parts[0]);
|
|
3800
|
+
const protocol = await this.resolveService("protocol");
|
|
3801
|
+
if (protocol && typeof protocol.listCommits === "function") {
|
|
3802
|
+
try {
|
|
3803
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3804
|
+
const commits = await protocol.listCommits({
|
|
3805
|
+
packageId: id,
|
|
3806
|
+
...organizationId ? { organizationId } : {}
|
|
3807
|
+
});
|
|
3808
|
+
return { handled: true, response: this.success({ commits }) };
|
|
3809
|
+
} catch (e) {
|
|
3810
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
return { handled: true, response: this.error("Commit history not supported", 501) };
|
|
3814
|
+
}
|
|
3815
|
+
if (parts.length === 4 && parts[1] === "commits" && parts[3] === "revert" && m === "POST") {
|
|
3816
|
+
const commitId = decodeURIComponent(parts[2]);
|
|
3817
|
+
const protocol = await this.resolveService("protocol");
|
|
3818
|
+
if (protocol && typeof protocol.revertCommit === "function") {
|
|
3819
|
+
try {
|
|
3820
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3821
|
+
const result = await protocol.revertCommit({
|
|
3822
|
+
commitId,
|
|
3823
|
+
...organizationId ? { organizationId } : {},
|
|
3824
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3825
|
+
});
|
|
3826
|
+
return { handled: true, response: this.success(result) };
|
|
3827
|
+
} catch (e) {
|
|
3828
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
return { handled: true, response: this.error("Commit revert not supported", 501) };
|
|
3832
|
+
}
|
|
3833
|
+
if (parts.length === 2 && parts[1] === "rollback" && m === "POST") {
|
|
3834
|
+
const protocol = await this.resolveService("protocol");
|
|
3835
|
+
if (protocol && typeof protocol.rollbackToPackageCommit === "function") {
|
|
3836
|
+
if (!body?.commitId) {
|
|
3837
|
+
return { handled: true, response: this.error("Body { commitId } is required", 400) };
|
|
3838
|
+
}
|
|
3839
|
+
try {
|
|
3840
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3841
|
+
const result = await protocol.rollbackToPackageCommit({
|
|
3842
|
+
commitId: String(body.commitId),
|
|
3843
|
+
...organizationId ? { organizationId } : {},
|
|
3844
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3845
|
+
});
|
|
3846
|
+
return { handled: true, response: this.success(result) };
|
|
3847
|
+
} catch (e) {
|
|
3848
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3849
|
+
}
|
|
3850
|
+
}
|
|
3851
|
+
return { handled: true, response: this.error("Commit rollback not supported", 501) };
|
|
3852
|
+
}
|
|
3656
3853
|
if (parts.length === 2 && parts[1] === "revert" && m === "POST") {
|
|
3657
3854
|
const id = decodeURIComponent(parts[0]);
|
|
3658
3855
|
const metadataService = await this.getService(import_system.CoreServiceName.enum.metadata);
|
|
@@ -3670,6 +3867,49 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3670
3867
|
}
|
|
3671
3868
|
return { handled: true, response: this.success(manifest) };
|
|
3672
3869
|
}
|
|
3870
|
+
if (parts.length === 2 && parts[1] === "adopt-orphans" && m === "POST") {
|
|
3871
|
+
const id = decodeURIComponent(parts[0]);
|
|
3872
|
+
const protocol = await this.resolveService("protocol");
|
|
3873
|
+
if (!protocol || typeof protocol.reassignOrphanedMetadata !== "function") {
|
|
3874
|
+
return { handled: true, response: this.error("Orphan adoption not supported", 501) };
|
|
3875
|
+
}
|
|
3876
|
+
try {
|
|
3877
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3878
|
+
const result = await protocol.reassignOrphanedMetadata({
|
|
3879
|
+
targetPackageId: id,
|
|
3880
|
+
...organizationId ? { organizationId } : {},
|
|
3881
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3882
|
+
});
|
|
3883
|
+
return { handled: true, response: this.success(result) };
|
|
3884
|
+
} catch (e) {
|
|
3885
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
if (parts.length === 2 && parts[1] === "duplicate" && m === "POST") {
|
|
3889
|
+
const id = decodeURIComponent(parts[0]);
|
|
3890
|
+
const protocol = await this.resolveService("protocol");
|
|
3891
|
+
if (!protocol || typeof protocol.duplicatePackage !== "function") {
|
|
3892
|
+
return { handled: true, response: this.error("Package duplication not supported", 501) };
|
|
3893
|
+
}
|
|
3894
|
+
const targetPackageId = typeof body?.targetPackageId === "string" ? body.targetPackageId.trim() : "";
|
|
3895
|
+
if (!targetPackageId) {
|
|
3896
|
+
return { handled: true, response: this.error("Body { targetPackageId } is required", 400) };
|
|
3897
|
+
}
|
|
3898
|
+
try {
|
|
3899
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3900
|
+
const result = await protocol.duplicatePackage({
|
|
3901
|
+
sourcePackageId: id,
|
|
3902
|
+
targetPackageId,
|
|
3903
|
+
...typeof body?.targetName === "string" ? { targetName: body.targetName } : {},
|
|
3904
|
+
...typeof body?.targetNamespace === "string" ? { targetNamespace: body.targetNamespace } : {},
|
|
3905
|
+
...organizationId ? { organizationId } : {},
|
|
3906
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3907
|
+
});
|
|
3908
|
+
return { handled: true, response: this.success(result) };
|
|
3909
|
+
} catch (e) {
|
|
3910
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3673
3913
|
if (parts.length === 1 && m === "GET") {
|
|
3674
3914
|
const id = decodeURIComponent(parts[0]);
|
|
3675
3915
|
const pkg = registry.getPackage(id);
|
|
@@ -4073,8 +4313,12 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4073
4313
|
object: objectName,
|
|
4074
4314
|
event: ctxBody.event ?? "manual"
|
|
4075
4315
|
};
|
|
4076
|
-
const
|
|
4316
|
+
const ec = context?.executionContext;
|
|
4317
|
+
const userIdFromAuth = context?.user?.id ?? context?.userId ?? ec?.userId;
|
|
4077
4318
|
if (userIdFromAuth) automationContext.userId = userIdFromAuth;
|
|
4319
|
+
if (Array.isArray(ec?.roles) && ec.roles.length) automationContext.roles = ec.roles;
|
|
4320
|
+
if (Array.isArray(ec?.permissions) && ec.permissions.length) automationContext.permissions = ec.permissions;
|
|
4321
|
+
if (ec?.tenantId) automationContext.tenantId = ec.tenantId;
|
|
4078
4322
|
const result = await automationService.execute(name, automationContext);
|
|
4079
4323
|
return { handled: true, response: this.success(result) };
|
|
4080
4324
|
}
|
|
@@ -4255,22 +4499,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4255
4499
|
try {
|
|
4256
4500
|
const actionSchema = (typeof ql.getSchema === "function" ? ql.getSchema(objectName) : void 0) ?? ql.registry?.getObject?.(objectName);
|
|
4257
4501
|
const actionDef = Array.isArray(actionSchema?.actions) ? actionSchema.actions.find((a) => a?.name === actionName) : void 0;
|
|
4258
|
-
const
|
|
4259
|
-
if (
|
|
4260
|
-
|
|
4261
|
-
if (!execCtx?.isSystem) {
|
|
4262
|
-
const held = new Set(execCtx?.systemPermissions ?? []);
|
|
4263
|
-
const missing = required.filter((perm) => !held.has(perm));
|
|
4264
|
-
if (missing.length > 0) {
|
|
4265
|
-
return {
|
|
4266
|
-
handled: true,
|
|
4267
|
-
response: this.error(
|
|
4268
|
-
`Action '${actionName}' on '${objectName}' requires capability [${required.join(", ")}] \u2014 caller is missing [${missing.join(", ")}]`,
|
|
4269
|
-
403
|
|
4270
|
-
)
|
|
4271
|
-
};
|
|
4272
|
-
}
|
|
4273
|
-
}
|
|
4502
|
+
const gateError = this.actionPermissionError(actionDef, _context?.executionContext, objectName);
|
|
4503
|
+
if (gateError) {
|
|
4504
|
+
return { handled: true, response: this.error(gateError, 403) };
|
|
4274
4505
|
}
|
|
4275
4506
|
} catch {
|
|
4276
4507
|
}
|
|
@@ -4613,11 +4844,31 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4613
4844
|
try {
|
|
4614
4845
|
context.executionContext = await resolveExecutionContext({
|
|
4615
4846
|
getService: (n) => this.resolveService(n, context.environmentId),
|
|
4616
|
-
|
|
4847
|
+
// Resolve ObjectQL from the per-request kernel DIRECTLY. The scoped
|
|
4848
|
+
// `resolveService('objectql', envId)` factory can return a different
|
|
4849
|
+
// instance that doesn't see THIS env's rows (the gotcha
|
|
4850
|
+
// `handleActions` works around) — which made the api-key lookup miss
|
|
4851
|
+
// `sys_api_key` on the MCP path and reject valid keys with 401, while
|
|
4852
|
+
// REST accepted them (rest-server resolves identity via
|
|
4853
|
+
// `kernel.getServiceAsync('objectql')`). Resolving off `this.kernel`
|
|
4854
|
+
// keeps REST + MCP identity resolution aligned; falls back to the
|
|
4855
|
+
// scoped path when the kernel can't hand back an objectql directly.
|
|
4856
|
+
getQl: async () => {
|
|
4857
|
+
const k = this.kernel;
|
|
4858
|
+
if (k && typeof k.getServiceAsync === "function") {
|
|
4859
|
+
const ql = await k.getServiceAsync("objectql").catch(() => void 0);
|
|
4860
|
+
if (ql && (ql.registry || typeof ql.find === "function")) return ql;
|
|
4861
|
+
}
|
|
4862
|
+
return this.getObjectQLService(context.environmentId);
|
|
4863
|
+
},
|
|
4617
4864
|
request: context.request
|
|
4618
4865
|
});
|
|
4619
4866
|
} catch {
|
|
4620
4867
|
}
|
|
4868
|
+
const authGated = await this.enforceAuthGate(context, cleanPath);
|
|
4869
|
+
if (authGated) {
|
|
4870
|
+
return { handled: true, response: authGated };
|
|
4871
|
+
}
|
|
4621
4872
|
const forbidden = await this.enforceProjectMembership(context, cleanPath);
|
|
4622
4873
|
if (forbidden) {
|
|
4623
4874
|
return { handled: true, response: forbidden };
|