@objectstack/runtime 10.3.0 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -93
- package/dist/index.cjs +441 -231
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +449 -236
- package/dist/index.js.map +1 -1
- package/package.json +21 -21
package/dist/index.js
CHANGED
|
@@ -802,7 +802,7 @@ __export(app_plugin_exports, {
|
|
|
802
802
|
collectBundleFunctions: () => collectBundleFunctions,
|
|
803
803
|
collectBundleHooks: () => collectBundleHooks
|
|
804
804
|
});
|
|
805
|
-
import {
|
|
805
|
+
import { resolveMultiOrgEnabled } from "@objectstack/types";
|
|
806
806
|
function collectBundleHooks(bundle) {
|
|
807
807
|
const out = [];
|
|
808
808
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -1254,7 +1254,7 @@ var init_app_plugin = __esm({
|
|
|
1254
1254
|
} catch (e) {
|
|
1255
1255
|
ctx.logger.warn("[Seeder] Failed to register seed-datasets/seed-replayer service", { error: e?.message });
|
|
1256
1256
|
}
|
|
1257
|
-
const multiTenant =
|
|
1257
|
+
const multiTenant = resolveMultiOrgEnabled();
|
|
1258
1258
|
if (multiTenant) {
|
|
1259
1259
|
ctx.logger.info("[Seeder] multi-tenant mode \u2014 skipping inline seed; per-org replay will run on sys_organization insert");
|
|
1260
1260
|
} else {
|
|
@@ -1525,7 +1525,7 @@ import { resolve as resolvePath2 } from "path";
|
|
|
1525
1525
|
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1526
1526
|
import { homedir } from "os";
|
|
1527
1527
|
import { z } from "zod";
|
|
1528
|
-
import { readEnvWithDeprecation
|
|
1528
|
+
import { readEnvWithDeprecation } from "@objectstack/types";
|
|
1529
1529
|
function resolveObjectStackHome() {
|
|
1530
1530
|
const raw = process.env.OS_HOME?.trim();
|
|
1531
1531
|
if (raw && raw.length > 0) {
|
|
@@ -1556,7 +1556,7 @@ async function createStandaloneStack(config) {
|
|
|
1556
1556
|
const environmentId = cfg.environmentId ?? process.env.OS_ENVIRONMENT_ID ?? "proj_local";
|
|
1557
1557
|
const artifactPathInput = cfg.artifactPath ?? process.env.OS_ARTIFACT_PATH ?? resolvePath2(cwd, "dist/objectstack.json");
|
|
1558
1558
|
const artifactPath = isHttpUrl(artifactPathInput) ? artifactPathInput : artifactPathInput.startsWith("/") ? artifactPathInput : resolvePath2(cwd, artifactPathInput);
|
|
1559
|
-
const dbUrl = cfg.databaseUrl ??
|
|
1559
|
+
const dbUrl = cfg.databaseUrl ?? readEnvWithDeprecation("OS_DATABASE_URL", "DATABASE_URL")?.trim() ?? process.env.TURSO_DATABASE_URL?.trim() ?? (process.env.OS_HOME?.trim() ? `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}` : cfg.projectRoot ? `file:${resolvePath2(cfg.projectRoot, ".objectstack/data/standalone.db")}` : `file:${resolvePath2(resolveObjectStackHome(), "data/standalone.db")}`);
|
|
1560
1560
|
const explicitDriver = cfg.databaseDriver ?? process.env.OS_DATABASE_DRIVER?.trim();
|
|
1561
1561
|
const dbDriver = explicitDriver ?? detectDriverFromUrl(dbUrl);
|
|
1562
1562
|
let driverPlugin;
|
|
@@ -2008,18 +2008,11 @@ function checkApiExposure(def, action) {
|
|
|
2008
2008
|
return { allowed: true };
|
|
2009
2009
|
}
|
|
2010
2010
|
|
|
2011
|
-
// src/security/
|
|
2011
|
+
// src/security/resolve-execution-context.ts
|
|
2012
2012
|
import {
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
generateApiKey,
|
|
2016
|
-
extractApiKey,
|
|
2017
|
-
parseScopes,
|
|
2018
|
-
isExpired,
|
|
2019
|
-
resolveApiKeyPrincipal
|
|
2013
|
+
resolveAuthzContext,
|
|
2014
|
+
resolveLocalizationContext
|
|
2020
2015
|
} from "@objectstack/core";
|
|
2021
|
-
|
|
2022
|
-
// src/security/resolve-execution-context.ts
|
|
2023
2016
|
function toHeaders(input) {
|
|
2024
2017
|
if (!input) return new Headers();
|
|
2025
2018
|
if (typeof Headers !== "undefined" && input instanceof Headers) return input;
|
|
@@ -2035,212 +2028,44 @@ function toHeaders(input) {
|
|
|
2035
2028
|
}
|
|
2036
2029
|
return h;
|
|
2037
2030
|
}
|
|
2038
|
-
function safeJsonParse2(s, fallback) {
|
|
2039
|
-
try {
|
|
2040
|
-
return JSON.parse(s);
|
|
2041
|
-
} catch {
|
|
2042
|
-
return fallback;
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
async function tryFind(ql, object, where, limit = 100) {
|
|
2046
|
-
if (!ql || typeof ql.find !== "function") return [];
|
|
2047
|
-
try {
|
|
2048
|
-
let rows = await ql.find(object, { where, limit, context: { isSystem: true } });
|
|
2049
|
-
if (rows && rows.value) rows = rows.value;
|
|
2050
|
-
return Array.isArray(rows) ? rows : [];
|
|
2051
|
-
} catch {
|
|
2052
|
-
return [];
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
function isValidTimeZone(tz) {
|
|
2056
|
-
try {
|
|
2057
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
2058
|
-
return true;
|
|
2059
|
-
} catch {
|
|
2060
|
-
return false;
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
function coerceTimeZone(value) {
|
|
2064
|
-
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
2065
|
-
return s && isValidTimeZone(s) ? s : void 0;
|
|
2066
|
-
}
|
|
2067
|
-
function coerceLocale(value) {
|
|
2068
|
-
const s = typeof value === "string" ? value.trim() : value != null ? String(value).trim() : "";
|
|
2069
|
-
return s || void 0;
|
|
2070
|
-
}
|
|
2071
|
-
function coerceCurrency(value) {
|
|
2072
|
-
const s = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
2073
|
-
return /^[A-Z]{3}$/.test(s) ? s : void 0;
|
|
2074
|
-
}
|
|
2075
|
-
async function resolveLocalization(opts, ql, sctx) {
|
|
2076
|
-
try {
|
|
2077
|
-
const settings = await opts.getService("settings");
|
|
2078
|
-
if (settings && typeof settings.get === "function") {
|
|
2079
|
-
const [tzRes, localeRes, currencyRes] = await Promise.all([
|
|
2080
|
-
settings.get("localization", "timezone", sctx).catch(() => void 0),
|
|
2081
|
-
settings.get("localization", "locale", sctx).catch(() => void 0),
|
|
2082
|
-
settings.get("localization", "currency", sctx).catch(() => void 0)
|
|
2083
|
-
]);
|
|
2084
|
-
const tz = coerceTimeZone(tzRes?.value);
|
|
2085
|
-
const locale = coerceLocale(localeRes?.value);
|
|
2086
|
-
const currency = coerceCurrency(currencyRes?.value);
|
|
2087
|
-
if (tz || locale || currency) return { timezone: tz ?? "UTC", locale: locale ?? "en-US", currency };
|
|
2088
|
-
}
|
|
2089
|
-
} catch {
|
|
2090
|
-
}
|
|
2091
|
-
const tzRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "timezone", scope: "tenant" }, 1);
|
|
2092
|
-
const localeRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "locale", scope: "tenant" }, 1);
|
|
2093
|
-
const currencyRows = await tryFind(ql, "sys_setting", { namespace: "localization", key: "currency", scope: "tenant" }, 1);
|
|
2094
|
-
return {
|
|
2095
|
-
timezone: coerceTimeZone(tzRows[0]?.value) ?? "UTC",
|
|
2096
|
-
locale: coerceLocale(localeRows[0]?.value) ?? "en-US",
|
|
2097
|
-
currency: coerceCurrency(currencyRows[0]?.value)
|
|
2098
|
-
};
|
|
2099
|
-
}
|
|
2100
2031
|
async function resolveExecutionContext(opts) {
|
|
2101
|
-
const headers = opts.request?.headers;
|
|
2102
|
-
const
|
|
2103
|
-
|
|
2104
|
-
permissions: [],
|
|
2105
|
-
systemPermissions: [],
|
|
2106
|
-
isSystem: false
|
|
2107
|
-
};
|
|
2108
|
-
let userId;
|
|
2109
|
-
let tenantId;
|
|
2110
|
-
const keyPrincipal = await resolveApiKeyPrincipal(await opts.getQl(), headers);
|
|
2111
|
-
if (keyPrincipal) {
|
|
2112
|
-
userId = keyPrincipal.userId;
|
|
2113
|
-
tenantId = keyPrincipal.tenantId;
|
|
2114
|
-
for (const scope of keyPrincipal.scopes) {
|
|
2115
|
-
if (!ctx.permissions.includes(scope)) ctx.permissions.push(scope);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
if (!userId) {
|
|
2032
|
+
const headers = toHeaders(opts.request?.headers);
|
|
2033
|
+
const ql = await opts.getQl();
|
|
2034
|
+
const getSession = async (h) => {
|
|
2119
2035
|
try {
|
|
2120
2036
|
const authService = await opts.getService("auth");
|
|
2121
2037
|
let api = authService?.api;
|
|
2122
|
-
if (!api && typeof authService?.getApi === "function")
|
|
2123
|
-
|
|
2124
|
-
}
|
|
2125
|
-
const headersInstance = toHeaders(headers);
|
|
2126
|
-
const sessionData = await api?.getSession?.({ headers: headersInstance });
|
|
2127
|
-
userId = sessionData?.user?.id ?? sessionData?.session?.userId;
|
|
2128
|
-
tenantId = tenantId ?? sessionData?.session?.activeOrganizationId;
|
|
2129
|
-
ctx.accessToken = sessionData?.session?.token ?? ctx.accessToken;
|
|
2130
|
-
if (sessionData?.user?.email) ctx.email = String(sessionData.user.email);
|
|
2038
|
+
if (!api && typeof authService?.getApi === "function") api = await authService.getApi();
|
|
2039
|
+
return await api?.getSession?.({ headers: h });
|
|
2131
2040
|
} catch {
|
|
2041
|
+
return void 0;
|
|
2132
2042
|
}
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
const userRoleRows = await tryFind(ql, "sys_user_role", { user_id: userId }, 200);
|
|
2153
|
-
for (const ur of userRoleRows) {
|
|
2154
|
-
const org = ur.organization_id ?? null;
|
|
2155
|
-
if (org && tenantId && org !== tenantId) continue;
|
|
2156
|
-
const r = ur.role;
|
|
2157
|
-
if (typeof r === "string" && r && !ctx.roles.includes(r)) ctx.roles.push(r);
|
|
2158
|
-
}
|
|
2159
|
-
if (tenantId) {
|
|
2160
|
-
const orgMembers = await tryFind(
|
|
2161
|
-
ql,
|
|
2162
|
-
"sys_member",
|
|
2163
|
-
{ organization_id: tenantId },
|
|
2164
|
-
1e3
|
|
2165
|
-
);
|
|
2166
|
-
const orgUserIds = Array.from(
|
|
2167
|
-
new Set(
|
|
2168
|
-
orgMembers.map((m) => m.user_id ?? m.userId).filter((v) => typeof v === "string" && v.length > 0)
|
|
2169
|
-
)
|
|
2170
|
-
);
|
|
2171
|
-
if (!orgUserIds.includes(userId)) orgUserIds.push(userId);
|
|
2172
|
-
ctx.org_user_ids = orgUserIds;
|
|
2173
|
-
} else {
|
|
2174
|
-
ctx.org_user_ids = [userId];
|
|
2175
|
-
}
|
|
2176
|
-
const upsRows = await tryFind(ql, "sys_user_permission_set", { user_id: userId }, 100);
|
|
2177
|
-
const psIds = new Set(
|
|
2178
|
-
upsRows.filter((r) => {
|
|
2179
|
-
const org = r.organization_id ?? r.organizationId ?? null;
|
|
2180
|
-
return !(org && tenantId && org !== tenantId);
|
|
2181
|
-
}).map((r) => r.permission_set_id ?? r.permissionSetId).filter(Boolean)
|
|
2182
|
-
);
|
|
2183
|
-
if (ctx.roles.length > 0) {
|
|
2184
|
-
const roleRows = await tryFind(ql, "sys_role", { name: { $in: ctx.roles } }, 100);
|
|
2185
|
-
const roleIds = roleRows.map((r) => r.id).filter(Boolean);
|
|
2186
|
-
if (roleIds.length > 0) {
|
|
2187
|
-
const rpsRows = await tryFind(
|
|
2188
|
-
ql,
|
|
2189
|
-
"sys_role_permission_set",
|
|
2190
|
-
{ role_id: { $in: roleIds } },
|
|
2191
|
-
500
|
|
2192
|
-
);
|
|
2193
|
-
for (const r of rpsRows) {
|
|
2194
|
-
const id = r.permission_set_id ?? r.permissionSetId;
|
|
2195
|
-
if (id) psIds.add(id);
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
if (psIds.size > 0) {
|
|
2200
|
-
const psRows = await tryFind(
|
|
2043
|
+
};
|
|
2044
|
+
const authz = await resolveAuthzContext({ ql, headers, getSession });
|
|
2045
|
+
const ctx = {
|
|
2046
|
+
roles: authz.roles,
|
|
2047
|
+
permissions: authz.permissions,
|
|
2048
|
+
systemPermissions: authz.systemPermissions,
|
|
2049
|
+
isSystem: false
|
|
2050
|
+
};
|
|
2051
|
+
if (authz.userId) ctx.userId = authz.userId;
|
|
2052
|
+
if (authz.tenantId) ctx.tenantId = authz.tenantId;
|
|
2053
|
+
if (authz.email) ctx.email = authz.email;
|
|
2054
|
+
if (authz.accessToken) ctx.accessToken = authz.accessToken;
|
|
2055
|
+
if (authz.tabPermissions) ctx.tabPermissions = authz.tabPermissions;
|
|
2056
|
+
ctx.org_user_ids = authz.org_user_ids;
|
|
2057
|
+
if (authz.userId) {
|
|
2058
|
+
const settings = await Promise.resolve(opts.getService("settings")).catch(() => void 0);
|
|
2059
|
+
const localization = await resolveLocalizationContext({
|
|
2201
2060
|
ql,
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
);
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
default_on: 2,
|
|
2210
|
-
visible: 3
|
|
2211
|
-
};
|
|
2212
|
-
const mergedTabs = {};
|
|
2213
|
-
for (const ps of psRows) {
|
|
2214
|
-
if (ps.name && !ctx.permissions.includes(ps.name)) {
|
|
2215
|
-
ctx.permissions.push(ps.name);
|
|
2216
|
-
}
|
|
2217
|
-
const sysPerms = typeof ps.system_permissions === "string" ? safeJsonParse2(ps.system_permissions, []) : ps.system_permissions ?? ps.systemPermissions;
|
|
2218
|
-
if (Array.isArray(sysPerms)) {
|
|
2219
|
-
for (const p of sysPerms) {
|
|
2220
|
-
if (typeof p === "string" && !ctx.systemPermissions.includes(p)) {
|
|
2221
|
-
ctx.systemPermissions.push(p);
|
|
2222
|
-
}
|
|
2223
|
-
}
|
|
2224
|
-
}
|
|
2225
|
-
const tabs = typeof ps.tab_permissions === "string" ? safeJsonParse2(ps.tab_permissions, {}) : ps.tab_permissions ?? ps.tabPermissions;
|
|
2226
|
-
if (tabs && typeof tabs === "object") {
|
|
2227
|
-
for (const [app, val] of Object.entries(tabs)) {
|
|
2228
|
-
if (typeof val !== "string" || !(val in tabRank)) continue;
|
|
2229
|
-
const cur = mergedTabs[app];
|
|
2230
|
-
if (!cur || tabRank[val] > tabRank[cur]) {
|
|
2231
|
-
mergedTabs[app] = val;
|
|
2232
|
-
}
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
}
|
|
2236
|
-
if (Object.keys(mergedTabs).length > 0) {
|
|
2237
|
-
ctx.tabPermissions = mergedTabs;
|
|
2238
|
-
}
|
|
2061
|
+
settings,
|
|
2062
|
+
tenantId: authz.tenantId,
|
|
2063
|
+
userId: authz.userId
|
|
2064
|
+
});
|
|
2065
|
+
ctx.timezone = localization.timezone;
|
|
2066
|
+
ctx.locale = localization.locale;
|
|
2067
|
+
if (localization.currency) ctx.currency = localization.currency;
|
|
2239
2068
|
}
|
|
2240
|
-
const localization = await resolveLocalization(opts, ql, { tenantId, userId });
|
|
2241
|
-
ctx.timezone = localization.timezone;
|
|
2242
|
-
ctx.locale = localization.locale;
|
|
2243
|
-
if (localization.currency) ctx.currency = localization.currency;
|
|
2244
2069
|
return ctx;
|
|
2245
2070
|
}
|
|
2246
2071
|
function isPermissionDeniedError(e) {
|
|
@@ -2249,6 +2074,17 @@ function isPermissionDeniedError(e) {
|
|
|
2249
2074
|
return anyE.name === "PermissionDeniedError" || anyE.code === "PERMISSION_DENIED" || typeof anyE.message === "string" && anyE.message.startsWith("[Security] Access denied");
|
|
2250
2075
|
}
|
|
2251
2076
|
|
|
2077
|
+
// src/security/api-key.ts
|
|
2078
|
+
import {
|
|
2079
|
+
API_KEY_PREFIX,
|
|
2080
|
+
hashApiKey,
|
|
2081
|
+
generateApiKey,
|
|
2082
|
+
extractApiKey,
|
|
2083
|
+
parseScopes,
|
|
2084
|
+
isExpired,
|
|
2085
|
+
resolveApiKeyPrincipal
|
|
2086
|
+
} from "@objectstack/core";
|
|
2087
|
+
|
|
2252
2088
|
// src/http-dispatcher.ts
|
|
2253
2089
|
function randomUUID() {
|
|
2254
2090
|
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") {
|
|
@@ -2260,6 +2096,9 @@ function randomUUID() {
|
|
|
2260
2096
|
return v.toString(16);
|
|
2261
2097
|
});
|
|
2262
2098
|
}
|
|
2099
|
+
function isSystemObjectName(name) {
|
|
2100
|
+
return /^sys_/i.test(name);
|
|
2101
|
+
}
|
|
2263
2102
|
var _HttpDispatcher = class _HttpDispatcher {
|
|
2264
2103
|
/**
|
|
2265
2104
|
* @param _envRegistryIgnored — RETIRED (ADR-0006 Phase 5). Environment
|
|
@@ -2377,7 +2216,10 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2377
2216
|
return extra ? { ...base, ...extra } : qlOpts ? base : void 0;
|
|
2378
2217
|
};
|
|
2379
2218
|
if (action === "create") {
|
|
2380
|
-
if (
|
|
2219
|
+
if (protocol && typeof protocol.createData === "function") {
|
|
2220
|
+
return await protocol.createData({ object: params.object, data: params.data, ...scopeId ? { environmentId: scopeId } : {}, context: executionContext });
|
|
2221
|
+
}
|
|
2222
|
+
if (ql && typeof ql.insert === "function") {
|
|
2381
2223
|
const res = await ql.insert(params.object, params.data, qlOpts);
|
|
2382
2224
|
const record = { ...params.data, ...res };
|
|
2383
2225
|
return { object: params.object, id: record.id, record };
|
|
@@ -2398,7 +2240,10 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2398
2240
|
throw { statusCode: 503, message: "Data service not available" };
|
|
2399
2241
|
}
|
|
2400
2242
|
if (action === "update") {
|
|
2401
|
-
if (
|
|
2243
|
+
if (protocol && typeof protocol.updateData === "function") {
|
|
2244
|
+
return await protocol.updateData({ object: params.object, id: params.id, data: params.data, ...scopeId ? { environmentId: scopeId } : {}, context: executionContext });
|
|
2245
|
+
}
|
|
2246
|
+
if (ql && params.id && typeof ql.update === "function") {
|
|
2402
2247
|
let all = await ql.find(params.object, findOpts({ where: { id: params.id }, limit: 1 }));
|
|
2403
2248
|
if (all && all.value) all = all.value;
|
|
2404
2249
|
if (!all) all = [];
|
|
@@ -2410,7 +2255,10 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2410
2255
|
throw { statusCode: 503, message: "Data service not available" };
|
|
2411
2256
|
}
|
|
2412
2257
|
if (action === "delete") {
|
|
2413
|
-
if (
|
|
2258
|
+
if (protocol && typeof protocol.deleteData === "function") {
|
|
2259
|
+
return await protocol.deleteData({ object: params.object, id: params.id, ...scopeId ? { environmentId: scopeId } : {}, context: executionContext });
|
|
2260
|
+
}
|
|
2261
|
+
if (ql && typeof ql.delete === "function") {
|
|
2414
2262
|
await ql.delete(params.object, findOpts({ where: { id: params.id } }));
|
|
2415
2263
|
return { object: params.object, id: params.id, deleted: true };
|
|
2416
2264
|
}
|
|
@@ -2602,9 +2450,269 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2602
2450
|
},
|
|
2603
2451
|
create: async (object, data) => await callData("create", { object, data }, driver, envId, ec),
|
|
2604
2452
|
update: async (object, id, data) => await callData("update", { object, id, data }, driver, envId, ec),
|
|
2605
|
-
remove: async (object, id) => await callData("delete", { object, id }, driver, envId, ec)
|
|
2453
|
+
remove: async (object, id) => await callData("delete", { object, id }, driver, envId, ec),
|
|
2454
|
+
// ── Business-action surface (McpActionBridge) ──────────────
|
|
2455
|
+
// Resolution + dispatch flow through the framework's own action
|
|
2456
|
+
// mechanism (engine.executeAction / automation flow runner) bound to
|
|
2457
|
+
// THIS request's ExecutionContext — the same permission + RLS path
|
|
2458
|
+
// the REST `/actions/...` route uses. No `@objectstack/service-ai`.
|
|
2459
|
+
listActions: async () => {
|
|
2460
|
+
const meta = await getMeta();
|
|
2461
|
+
const objs = await meta?.listObjects?.() ?? [];
|
|
2462
|
+
const hasAutomation = Boolean(
|
|
2463
|
+
await this.resolveService("automation", envId).catch(() => null)
|
|
2464
|
+
);
|
|
2465
|
+
const out = [];
|
|
2466
|
+
for (const obj of objs) {
|
|
2467
|
+
const objectName = obj?.name;
|
|
2468
|
+
if (!objectName || isSystemObjectName(objectName)) continue;
|
|
2469
|
+
const actions = Array.isArray(obj?.actions) ? obj.actions : [];
|
|
2470
|
+
for (const action of actions) {
|
|
2471
|
+
if (!action || typeof action.name !== "string") continue;
|
|
2472
|
+
if (!this.isHeadlessInvokableAction(action, hasAutomation)) continue;
|
|
2473
|
+
if (this.actionPermissionError(action, ec)) continue;
|
|
2474
|
+
out.push(this.summarizeAction(action, obj, objectName));
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
return out;
|
|
2478
|
+
},
|
|
2479
|
+
runAction: async (name, input) => this.invokeBusinessAction(name, input ?? {}, { driver, envId, ec, getMeta, callData })
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
// ── MCP action bridge helpers ──────────────────────────────────────
|
|
2483
|
+
/**
|
|
2484
|
+
* [ADR-0066 D4] Shared capability gate for an action invocation. Returns a
|
|
2485
|
+
* human-readable error string when the caller's `systemPermissions` don't
|
|
2486
|
+
* cover the action's declared `requiredPermissions`, or `null` when allowed.
|
|
2487
|
+
* System/engine self-invocation (`isSystem`) bypasses; an action without
|
|
2488
|
+
* `requiredPermissions` is ungated. Single-sourced so the REST `/actions/...`
|
|
2489
|
+
* route and the MCP `run_action` bridge enforce the SAME declaration.
|
|
2490
|
+
*/
|
|
2491
|
+
actionPermissionError(actionDef, ec, objectName) {
|
|
2492
|
+
const required = Array.isArray(actionDef?.requiredPermissions) ? actionDef.requiredPermissions : [];
|
|
2493
|
+
if (required.length === 0) return null;
|
|
2494
|
+
if (ec?.isSystem) return null;
|
|
2495
|
+
const held = new Set(ec?.systemPermissions ?? []);
|
|
2496
|
+
const missing = required.filter((perm) => !held.has(perm));
|
|
2497
|
+
if (missing.length === 0) return null;
|
|
2498
|
+
const on = objectName ? ` on '${objectName}'` : "";
|
|
2499
|
+
return `Action '${actionDef?.name ?? "unknown"}'${on} requires capability [${required.join(", ")}] \u2014 caller is missing [${missing.join(", ")}]`;
|
|
2500
|
+
}
|
|
2501
|
+
/**
|
|
2502
|
+
* Whether an action has a headless invocation path (so MCP can run it).
|
|
2503
|
+
* Mirrors the supported-type set of the (now cloud-side) action-tools
|
|
2504
|
+
* bridge: `script` needs a handler binding (`target`) or an inline `body`;
|
|
2505
|
+
* `flow` needs a `target` and an automation service. UI-only types
|
|
2506
|
+
* (`url`, `modal`, `form`) and `api` have no server dispatch here.
|
|
2507
|
+
*/
|
|
2508
|
+
isHeadlessInvokableAction(action, hasAutomation) {
|
|
2509
|
+
const type = action?.type ?? "script";
|
|
2510
|
+
if (type === "script") return Boolean(action?.target || action?.body);
|
|
2511
|
+
if (type === "flow") return Boolean(action?.target) && hasAutomation;
|
|
2512
|
+
return false;
|
|
2513
|
+
}
|
|
2514
|
+
/** True when an action is destructive by author signal/heuristic (HITL hint). */
|
|
2515
|
+
actionLooksDestructive(action) {
|
|
2516
|
+
if (action?.ai?.requiresConfirmation !== void 0) return Boolean(action.ai.requiresConfirmation);
|
|
2517
|
+
return Boolean(action?.confirmText || action?.mode === "delete" || action?.variant === "danger");
|
|
2518
|
+
}
|
|
2519
|
+
/** Project an action's declarative metadata into a lean MCP summary. */
|
|
2520
|
+
summarizeAction(action, obj, objectName) {
|
|
2521
|
+
const requiresRecord = Array.isArray(action?.locations) && action.locations.some(
|
|
2522
|
+
(l) => l === "list_item" || l === "record_header" || l === "record_more" || l === "record_related"
|
|
2523
|
+
);
|
|
2524
|
+
const description = (typeof action?.ai?.description === "string" ? action.ai.description : void 0) ?? (typeof action?.label === "string" ? action.label : void 0);
|
|
2525
|
+
const params = this.summarizeActionParams(action, obj);
|
|
2526
|
+
return {
|
|
2527
|
+
name: action.name,
|
|
2528
|
+
objectName,
|
|
2529
|
+
...typeof action?.label === "string" ? { label: action.label } : {},
|
|
2530
|
+
...description ? { description } : {},
|
|
2531
|
+
type: action?.type ?? "script",
|
|
2532
|
+
requiresRecord: Boolean(requiresRecord),
|
|
2533
|
+
requiresConfirmation: this.actionLooksDestructive(action),
|
|
2534
|
+
...params.length > 0 ? { params } : {}
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
/** Map an ObjectStack field type to a JSON-Schema primitive (conservative). */
|
|
2538
|
+
jsonTypeOf(t) {
|
|
2539
|
+
switch (t) {
|
|
2540
|
+
case "number":
|
|
2541
|
+
case "currency":
|
|
2542
|
+
case "percent":
|
|
2543
|
+
case "rating":
|
|
2544
|
+
case "slider":
|
|
2545
|
+
case "autonumber":
|
|
2546
|
+
return "number";
|
|
2547
|
+
case "boolean":
|
|
2548
|
+
case "toggle":
|
|
2549
|
+
return "boolean";
|
|
2550
|
+
case "multiselect":
|
|
2551
|
+
case "checkboxes":
|
|
2552
|
+
case "tags":
|
|
2553
|
+
return "array";
|
|
2554
|
+
default:
|
|
2555
|
+
return "string";
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
/** Resolve an action's params into LLM-facing summaries (field-backed types resolved). */
|
|
2559
|
+
summarizeActionParams(action, obj) {
|
|
2560
|
+
const fields = obj?.fields ?? {};
|
|
2561
|
+
const out = [];
|
|
2562
|
+
for (const p of Array.isArray(action?.params) ? action.params : []) {
|
|
2563
|
+
const fieldRef = p?.field;
|
|
2564
|
+
const field = fieldRef ? fields[fieldRef] : void 0;
|
|
2565
|
+
const name = p?.name ?? fieldRef;
|
|
2566
|
+
if (!name) continue;
|
|
2567
|
+
const type = this.jsonTypeOf(p?.type ?? field?.type);
|
|
2568
|
+
const label = typeof p?.label === "string" ? p.label : field?.label;
|
|
2569
|
+
const help = p?.helpText ?? field?.description;
|
|
2570
|
+
const description = [label, help].filter(Boolean).join(" \u2014 ") || void 0;
|
|
2571
|
+
const optionSource = p?.options ?? field?.options;
|
|
2572
|
+
const enumVals = Array.isArray(optionSource) ? optionSource.map((o) => typeof o === "string" ? o : o?.value).filter((v) => typeof v === "string") : [];
|
|
2573
|
+
out.push({
|
|
2574
|
+
name,
|
|
2575
|
+
type,
|
|
2576
|
+
required: Boolean(p?.required ?? field?.required ?? false),
|
|
2577
|
+
...description ? { description } : {},
|
|
2578
|
+
...enumVals.length > 0 ? { enum: enumVals } : {}
|
|
2579
|
+
});
|
|
2580
|
+
}
|
|
2581
|
+
return out;
|
|
2582
|
+
}
|
|
2583
|
+
/** Slim engine facade matching the ActionContext.engine shape handlers expect. */
|
|
2584
|
+
buildActionEngineFacade(ql) {
|
|
2585
|
+
return {
|
|
2586
|
+
async insert(object, data) {
|
|
2587
|
+
const res = await ql.insert(object, data);
|
|
2588
|
+
const id = (res && res.id) ?? data.id;
|
|
2589
|
+
return { id };
|
|
2590
|
+
},
|
|
2591
|
+
async update(object, id, data) {
|
|
2592
|
+
await ql.update(object, data, { where: { id } });
|
|
2593
|
+
},
|
|
2594
|
+
// Tolerant of both the single-id and array conventions handler suites
|
|
2595
|
+
// use (CRM handlers pass one id; todo handlers pass an id array).
|
|
2596
|
+
async delete(object, idOrIds) {
|
|
2597
|
+
const ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
|
2598
|
+
for (const id of ids) {
|
|
2599
|
+
if (id != null) await ql.delete(object, { where: { id } });
|
|
2600
|
+
}
|
|
2601
|
+
},
|
|
2602
|
+
async find(object, query) {
|
|
2603
|
+
const opts = query && Object.keys(query).length ? { where: query } : void 0;
|
|
2604
|
+
const rows = await ql.find(object, opts);
|
|
2605
|
+
return Array.isArray(rows) ? rows : rows?.value ?? [];
|
|
2606
|
+
}
|
|
2606
2607
|
};
|
|
2607
2608
|
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Resolve + invoke a business action by its declarative name for the MCP
|
|
2611
|
+
* `run_action` tool. Enforces the ADR-0066 D4 capability gate and RLS as the
|
|
2612
|
+
* caller, loads the subject record under RLS for row-context actions, and
|
|
2613
|
+
* dispatches through the framework's `engine.executeAction` (script/body) or
|
|
2614
|
+
* automation flow runner (flow). Throws on denial / not-found / handler
|
|
2615
|
+
* failure so the tool surfaces a clean tool-error. No service-ai dependency.
|
|
2616
|
+
*/
|
|
2617
|
+
async invokeBusinessAction(name, input, wiring) {
|
|
2618
|
+
const { driver, envId, ec, getMeta, callData } = wiring;
|
|
2619
|
+
const meta = await getMeta();
|
|
2620
|
+
const params = input?.params && typeof input.params === "object" ? input.params : {};
|
|
2621
|
+
const recordId = typeof input?.recordId === "string" && input.recordId.length > 0 ? input.recordId : void 0;
|
|
2622
|
+
const resolved = await this.resolveActionByName(meta, name, input?.objectName);
|
|
2623
|
+
if (!resolved) {
|
|
2624
|
+
throw new Error(
|
|
2625
|
+
input?.objectName ? `Action '${name}' not found on object '${input.objectName}'` : `Action '${name}' not found`
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
const { action, objectName } = resolved;
|
|
2629
|
+
if (isSystemObjectName(objectName)) {
|
|
2630
|
+
throw new Error(`Action '${name}' is on a system object and is not exposed via MCP`);
|
|
2631
|
+
}
|
|
2632
|
+
const hasAutomation = Boolean(await this.resolveService("automation", envId).catch(() => null));
|
|
2633
|
+
if (!this.isHeadlessInvokableAction(action, hasAutomation)) {
|
|
2634
|
+
throw new Error(
|
|
2635
|
+
`Action '${name}' (type='${action?.type ?? "script"}') cannot be invoked via MCP`
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
const gateError = this.actionPermissionError(action, ec, objectName);
|
|
2639
|
+
if (gateError) throw new Error(gateError);
|
|
2640
|
+
let record = {};
|
|
2641
|
+
if (recordId && objectName !== "global") {
|
|
2642
|
+
try {
|
|
2643
|
+
const got = await callData("get", { object: objectName, id: recordId }, driver, envId, ec);
|
|
2644
|
+
if (got?.record) record = got.record;
|
|
2645
|
+
} catch {
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
if (record && record.id == null && recordId) record.id = recordId;
|
|
2649
|
+
const user = ec?.userId ? { id: ec.userId, name: ec.userName ?? ec.userDisplayName ?? ec.userId } : { id: "system", name: "system" };
|
|
2650
|
+
if (action.type === "flow") {
|
|
2651
|
+
const automation = await this.resolveService("automation", envId).catch(() => null);
|
|
2652
|
+
if (!automation || typeof automation.execute !== "function") {
|
|
2653
|
+
throw new Error(`Action '${name}' is a flow but no automation service is available`);
|
|
2654
|
+
}
|
|
2655
|
+
const result = await automation.execute(action.target, {
|
|
2656
|
+
triggerData: { record, params, user, action: action.name }
|
|
2657
|
+
});
|
|
2658
|
+
if (result && typeof result === "object" && "success" in result && result.success === false) {
|
|
2659
|
+
throw new Error(`Flow '${action.target}' failed: ${result.error ?? "unknown error"}`);
|
|
2660
|
+
}
|
|
2661
|
+
return { ok: true, action: action.name, objectName, ...recordId ? { recordId } : {}, result: result ?? null };
|
|
2662
|
+
}
|
|
2663
|
+
const ql = await this.getObjectQLService(envId);
|
|
2664
|
+
if (!ql || typeof ql.executeAction !== "function") {
|
|
2665
|
+
throw new Error("Data engine not available for action dispatch");
|
|
2666
|
+
}
|
|
2667
|
+
const actionContext = {
|
|
2668
|
+
record,
|
|
2669
|
+
user,
|
|
2670
|
+
engine: this.buildActionEngineFacade(ql),
|
|
2671
|
+
params: { ...params, recordId, objectName }
|
|
2672
|
+
};
|
|
2673
|
+
const primary = action.body ? action.name : action.target || action.name;
|
|
2674
|
+
const candidates = [primary, action.target, action.name].filter(
|
|
2675
|
+
(k, i, a) => typeof k === "string" && a.indexOf(k) === i
|
|
2676
|
+
);
|
|
2677
|
+
const notRegistered = (err) => /Action '.+' on object '.+' not found/i.test(String(err?.message ?? err));
|
|
2678
|
+
for (const obj of [objectName, "*"]) {
|
|
2679
|
+
for (const key of candidates) {
|
|
2680
|
+
try {
|
|
2681
|
+
const result = await ql.executeAction(obj, key, actionContext);
|
|
2682
|
+
return { ok: true, action: action.name, objectName, ...recordId ? { recordId } : {}, result: result ?? null };
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
if (!notRegistered(err)) throw err;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
throw new Error(`No handler registered for action '${name}' on '${objectName}'`);
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Find an action's declarative definition by name across object metadata,
|
|
2692
|
+
* optionally scoped to a single object. Returns the action plus its owning
|
|
2693
|
+
* object name, or `null`. Throws when the name is ambiguous across objects
|
|
2694
|
+
* and no `objectName` was supplied (so `run_action` can ask for one).
|
|
2695
|
+
*/
|
|
2696
|
+
async resolveActionByName(meta, name, objectName) {
|
|
2697
|
+
if (objectName) {
|
|
2698
|
+
const def = await meta?.getObject?.(objectName);
|
|
2699
|
+
const action = Array.isArray(def?.actions) ? def.actions.find((a) => a?.name === name) : void 0;
|
|
2700
|
+
return action ? { action, objectName } : null;
|
|
2701
|
+
}
|
|
2702
|
+
const objs = await meta?.listObjects?.() ?? [];
|
|
2703
|
+
const matches = [];
|
|
2704
|
+
for (const obj of objs) {
|
|
2705
|
+
if (!obj?.name) continue;
|
|
2706
|
+
const action = Array.isArray(obj?.actions) ? obj.actions.find((a) => a?.name === name) : void 0;
|
|
2707
|
+
if (action) matches.push({ action, objectName: obj.name });
|
|
2708
|
+
}
|
|
2709
|
+
if (matches.length === 0) return null;
|
|
2710
|
+
if (matches.length > 1) {
|
|
2711
|
+
const where = matches.map((m) => m.objectName).join(", ");
|
|
2712
|
+
throw new Error(`Action '${name}' exists on multiple objects (${where}); pass objectName to disambiguate`);
|
|
2713
|
+
}
|
|
2714
|
+
return matches[0];
|
|
2715
|
+
}
|
|
2608
2716
|
/**
|
|
2609
2717
|
* Generate a `sys_api_key` and return the raw secret EXACTLY ONCE
|
|
2610
2718
|
* (`POST /keys`). This is the only mint path — the raw key is never stored
|
|
@@ -3585,6 +3693,61 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3585
3693
|
}
|
|
3586
3694
|
return { handled: true, response: this.error("Draft discarding not supported", 501) };
|
|
3587
3695
|
}
|
|
3696
|
+
if (parts.length === 2 && parts[1] === "commits" && m === "GET") {
|
|
3697
|
+
const id = decodeURIComponent(parts[0]);
|
|
3698
|
+
const protocol = await this.resolveService("protocol");
|
|
3699
|
+
if (protocol && typeof protocol.listCommits === "function") {
|
|
3700
|
+
try {
|
|
3701
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3702
|
+
const commits = await protocol.listCommits({
|
|
3703
|
+
packageId: id,
|
|
3704
|
+
...organizationId ? { organizationId } : {}
|
|
3705
|
+
});
|
|
3706
|
+
return { handled: true, response: this.success({ commits }) };
|
|
3707
|
+
} catch (e) {
|
|
3708
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
return { handled: true, response: this.error("Commit history not supported", 501) };
|
|
3712
|
+
}
|
|
3713
|
+
if (parts.length === 4 && parts[1] === "commits" && parts[3] === "revert" && m === "POST") {
|
|
3714
|
+
const commitId = decodeURIComponent(parts[2]);
|
|
3715
|
+
const protocol = await this.resolveService("protocol");
|
|
3716
|
+
if (protocol && typeof protocol.revertCommit === "function") {
|
|
3717
|
+
try {
|
|
3718
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3719
|
+
const result = await protocol.revertCommit({
|
|
3720
|
+
commitId,
|
|
3721
|
+
...organizationId ? { organizationId } : {},
|
|
3722
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3723
|
+
});
|
|
3724
|
+
return { handled: true, response: this.success(result) };
|
|
3725
|
+
} catch (e) {
|
|
3726
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
return { handled: true, response: this.error("Commit revert not supported", 501) };
|
|
3730
|
+
}
|
|
3731
|
+
if (parts.length === 2 && parts[1] === "rollback" && m === "POST") {
|
|
3732
|
+
const protocol = await this.resolveService("protocol");
|
|
3733
|
+
if (protocol && typeof protocol.rollbackToPackageCommit === "function") {
|
|
3734
|
+
if (!body?.commitId) {
|
|
3735
|
+
return { handled: true, response: this.error("Body { commitId } is required", 400) };
|
|
3736
|
+
}
|
|
3737
|
+
try {
|
|
3738
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3739
|
+
const result = await protocol.rollbackToPackageCommit({
|
|
3740
|
+
commitId: String(body.commitId),
|
|
3741
|
+
...organizationId ? { organizationId } : {},
|
|
3742
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3743
|
+
});
|
|
3744
|
+
return { handled: true, response: this.success(result) };
|
|
3745
|
+
} catch (e) {
|
|
3746
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
return { handled: true, response: this.error("Commit rollback not supported", 501) };
|
|
3750
|
+
}
|
|
3588
3751
|
if (parts.length === 2 && parts[1] === "revert" && m === "POST") {
|
|
3589
3752
|
const id = decodeURIComponent(parts[0]);
|
|
3590
3753
|
const metadataService = await this.getService(CoreServiceName.enum.metadata);
|
|
@@ -3602,6 +3765,49 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3602
3765
|
}
|
|
3603
3766
|
return { handled: true, response: this.success(manifest) };
|
|
3604
3767
|
}
|
|
3768
|
+
if (parts.length === 2 && parts[1] === "adopt-orphans" && m === "POST") {
|
|
3769
|
+
const id = decodeURIComponent(parts[0]);
|
|
3770
|
+
const protocol = await this.resolveService("protocol");
|
|
3771
|
+
if (!protocol || typeof protocol.reassignOrphanedMetadata !== "function") {
|
|
3772
|
+
return { handled: true, response: this.error("Orphan adoption not supported", 501) };
|
|
3773
|
+
}
|
|
3774
|
+
try {
|
|
3775
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3776
|
+
const result = await protocol.reassignOrphanedMetadata({
|
|
3777
|
+
targetPackageId: id,
|
|
3778
|
+
...organizationId ? { organizationId } : {},
|
|
3779
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3780
|
+
});
|
|
3781
|
+
return { handled: true, response: this.success(result) };
|
|
3782
|
+
} catch (e) {
|
|
3783
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
if (parts.length === 2 && parts[1] === "duplicate" && m === "POST") {
|
|
3787
|
+
const id = decodeURIComponent(parts[0]);
|
|
3788
|
+
const protocol = await this.resolveService("protocol");
|
|
3789
|
+
if (!protocol || typeof protocol.duplicatePackage !== "function") {
|
|
3790
|
+
return { handled: true, response: this.error("Package duplication not supported", 501) };
|
|
3791
|
+
}
|
|
3792
|
+
const targetPackageId = typeof body?.targetPackageId === "string" ? body.targetPackageId.trim() : "";
|
|
3793
|
+
if (!targetPackageId) {
|
|
3794
|
+
return { handled: true, response: this.error("Body { targetPackageId } is required", 400) };
|
|
3795
|
+
}
|
|
3796
|
+
try {
|
|
3797
|
+
const organizationId = await this.resolveActiveOrganizationId(_context);
|
|
3798
|
+
const result = await protocol.duplicatePackage({
|
|
3799
|
+
sourcePackageId: id,
|
|
3800
|
+
targetPackageId,
|
|
3801
|
+
...typeof body?.targetName === "string" ? { targetName: body.targetName } : {},
|
|
3802
|
+
...typeof body?.targetNamespace === "string" ? { targetNamespace: body.targetNamespace } : {},
|
|
3803
|
+
...organizationId ? { organizationId } : {},
|
|
3804
|
+
...body?.actor ? { actor: body.actor } : {}
|
|
3805
|
+
});
|
|
3806
|
+
return { handled: true, response: this.success(result) };
|
|
3807
|
+
} catch (e) {
|
|
3808
|
+
return { handled: true, response: this.error(e.message, e.statusCode || 500) };
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3605
3811
|
if (parts.length === 1 && m === "GET") {
|
|
3606
3812
|
const id = decodeURIComponent(parts[0]);
|
|
3607
3813
|
const pkg = registry.getPackage(id);
|
|
@@ -4005,8 +4211,12 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4005
4211
|
object: objectName,
|
|
4006
4212
|
event: ctxBody.event ?? "manual"
|
|
4007
4213
|
};
|
|
4008
|
-
const
|
|
4214
|
+
const ec = context?.executionContext;
|
|
4215
|
+
const userIdFromAuth = context?.user?.id ?? context?.userId ?? ec?.userId;
|
|
4009
4216
|
if (userIdFromAuth) automationContext.userId = userIdFromAuth;
|
|
4217
|
+
if (Array.isArray(ec?.roles) && ec.roles.length) automationContext.roles = ec.roles;
|
|
4218
|
+
if (Array.isArray(ec?.permissions) && ec.permissions.length) automationContext.permissions = ec.permissions;
|
|
4219
|
+
if (ec?.tenantId) automationContext.tenantId = ec.tenantId;
|
|
4010
4220
|
const result = await automationService.execute(name, automationContext);
|
|
4011
4221
|
return { handled: true, response: this.success(result) };
|
|
4012
4222
|
}
|
|
@@ -4187,22 +4397,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4187
4397
|
try {
|
|
4188
4398
|
const actionSchema = (typeof ql.getSchema === "function" ? ql.getSchema(objectName) : void 0) ?? ql.registry?.getObject?.(objectName);
|
|
4189
4399
|
const actionDef = Array.isArray(actionSchema?.actions) ? actionSchema.actions.find((a) => a?.name === actionName) : void 0;
|
|
4190
|
-
const
|
|
4191
|
-
if (
|
|
4192
|
-
|
|
4193
|
-
if (!execCtx?.isSystem) {
|
|
4194
|
-
const held = new Set(execCtx?.systemPermissions ?? []);
|
|
4195
|
-
const missing = required.filter((perm) => !held.has(perm));
|
|
4196
|
-
if (missing.length > 0) {
|
|
4197
|
-
return {
|
|
4198
|
-
handled: true,
|
|
4199
|
-
response: this.error(
|
|
4200
|
-
`Action '${actionName}' on '${objectName}' requires capability [${required.join(", ")}] \u2014 caller is missing [${missing.join(", ")}]`,
|
|
4201
|
-
403
|
|
4202
|
-
)
|
|
4203
|
-
};
|
|
4204
|
-
}
|
|
4205
|
-
}
|
|
4400
|
+
const gateError = this.actionPermissionError(actionDef, _context?.executionContext, objectName);
|
|
4401
|
+
if (gateError) {
|
|
4402
|
+
return { handled: true, response: this.error(gateError, 403) };
|
|
4206
4403
|
}
|
|
4207
4404
|
} catch {
|
|
4208
4405
|
}
|
|
@@ -4545,7 +4742,23 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4545
4742
|
try {
|
|
4546
4743
|
context.executionContext = await resolveExecutionContext({
|
|
4547
4744
|
getService: (n) => this.resolveService(n, context.environmentId),
|
|
4548
|
-
|
|
4745
|
+
// Resolve ObjectQL from the per-request kernel DIRECTLY. The scoped
|
|
4746
|
+
// `resolveService('objectql', envId)` factory can return a different
|
|
4747
|
+
// instance that doesn't see THIS env's rows (the gotcha
|
|
4748
|
+
// `handleActions` works around) — which made the api-key lookup miss
|
|
4749
|
+
// `sys_api_key` on the MCP path and reject valid keys with 401, while
|
|
4750
|
+
// REST accepted them (rest-server resolves identity via
|
|
4751
|
+
// `kernel.getServiceAsync('objectql')`). Resolving off `this.kernel`
|
|
4752
|
+
// keeps REST + MCP identity resolution aligned; falls back to the
|
|
4753
|
+
// scoped path when the kernel can't hand back an objectql directly.
|
|
4754
|
+
getQl: async () => {
|
|
4755
|
+
const k = this.kernel;
|
|
4756
|
+
if (k && typeof k.getServiceAsync === "function") {
|
|
4757
|
+
const ql = await k.getServiceAsync("objectql").catch(() => void 0);
|
|
4758
|
+
if (ql && (ql.registry || typeof ql.find === "function")) return ql;
|
|
4759
|
+
}
|
|
4760
|
+
return this.getObjectQLService(context.environmentId);
|
|
4761
|
+
},
|
|
4549
4762
|
request: context.request
|
|
4550
4763
|
});
|
|
4551
4764
|
} catch {
|
|
@@ -6018,7 +6231,7 @@ import {
|
|
|
6018
6231
|
createRestApiPlugin
|
|
6019
6232
|
} from "@objectstack/rest";
|
|
6020
6233
|
export * from "@objectstack/core";
|
|
6021
|
-
import { readEnvWithDeprecation as
|
|
6234
|
+
import { readEnvWithDeprecation as readEnvWithDeprecation2, _resetEnvDeprecationWarnings } from "@objectstack/types";
|
|
6022
6235
|
export {
|
|
6023
6236
|
AppPlugin,
|
|
6024
6237
|
DEFAULT_RATE_LIMITS,
|
|
@@ -6067,7 +6280,7 @@ export {
|
|
|
6067
6280
|
mergeRuntimeModule,
|
|
6068
6281
|
parseTraceparent,
|
|
6069
6282
|
readArtifactSource,
|
|
6070
|
-
|
|
6283
|
+
readEnvWithDeprecation2 as readEnvWithDeprecation,
|
|
6071
6284
|
resolveDefaultArtifactPath,
|
|
6072
6285
|
resolveErrorReporter,
|
|
6073
6286
|
resolveMetrics,
|