@objectstack/runtime 12.5.0 → 13.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/dist/index.cjs +127 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -7
- package/dist/index.d.ts +19 -7
- package/dist/index.js +124 -25
- package/dist/index.js.map +1 -1
- package/package.json +20 -21
package/dist/index.cjs
CHANGED
|
@@ -265,8 +265,20 @@ function installApiMethod(vm, parent, method, objectName, ctx, caps, required, o
|
|
|
265
265
|
vm.setProp(parent, method, fn);
|
|
266
266
|
fn.dispose();
|
|
267
267
|
}
|
|
268
|
+
function safeJsonStringify(v) {
|
|
269
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
270
|
+
const json = JSON.stringify(v ?? null, function(_key, value) {
|
|
271
|
+
if (typeof value === "bigint") return value.toString();
|
|
272
|
+
if (typeof value === "object" && value !== null) {
|
|
273
|
+
if (seen.has(value)) return void 0;
|
|
274
|
+
seen.add(value);
|
|
275
|
+
}
|
|
276
|
+
return value;
|
|
277
|
+
});
|
|
278
|
+
return json ?? "null";
|
|
279
|
+
}
|
|
268
280
|
function jsonToHandle(vm, v) {
|
|
269
|
-
const json =
|
|
281
|
+
const json = safeJsonStringify(v);
|
|
270
282
|
const r = vm.evalCode(`(${json})`);
|
|
271
283
|
if (r.error) {
|
|
272
284
|
const msg = vm.dump(r.error);
|
|
@@ -276,7 +288,7 @@ function jsonToHandle(vm, v) {
|
|
|
276
288
|
return r.value;
|
|
277
289
|
}
|
|
278
290
|
function setGlobalJson(vm, name, v) {
|
|
279
|
-
const json =
|
|
291
|
+
const json = safeJsonStringify(v);
|
|
280
292
|
const result = vm.evalCode(`(${json})`);
|
|
281
293
|
if (result.error) {
|
|
282
294
|
result.error.dispose();
|
|
@@ -286,7 +298,7 @@ function setGlobalJson(vm, name, v) {
|
|
|
286
298
|
result.value.dispose();
|
|
287
299
|
}
|
|
288
300
|
function setObjectJson(vm, parent, key, v) {
|
|
289
|
-
const json =
|
|
301
|
+
const json = safeJsonStringify(v);
|
|
290
302
|
const result = vm.evalCode(`(${json})`);
|
|
291
303
|
if (result.error) {
|
|
292
304
|
result.error.dispose();
|
|
@@ -1042,7 +1054,7 @@ var init_app_plugin = __esm({
|
|
|
1042
1054
|
if (typeof metadata?.registerInMemory === "function") {
|
|
1043
1055
|
const securityBundle = this.bundle.manifest ? { ...this.bundle.manifest, ...this.bundle } : this.bundle;
|
|
1044
1056
|
const SECURITY_FIELDS = [
|
|
1045
|
-
["
|
|
1057
|
+
["positions", "position"],
|
|
1046
1058
|
["permissions", "permission"],
|
|
1047
1059
|
["sharingRules", "sharing_rule"],
|
|
1048
1060
|
["policies", "policy"]
|
|
@@ -1408,8 +1420,7 @@ var init_app_plugin = __esm({
|
|
|
1408
1420
|
"skills",
|
|
1409
1421
|
"actions",
|
|
1410
1422
|
"permissions",
|
|
1411
|
-
"
|
|
1412
|
-
"profiles",
|
|
1423
|
+
"positions",
|
|
1413
1424
|
"translations",
|
|
1414
1425
|
"sharingRules",
|
|
1415
1426
|
"ragPipelines",
|
|
@@ -1764,7 +1775,7 @@ async function createStandaloneStack(config) {
|
|
|
1764
1775
|
const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
|
|
1765
1776
|
const manifest = artifactBundle?.manifest;
|
|
1766
1777
|
const permissions = Array.isArray(artifactBundle?.permissions) ? artifactBundle.permissions : void 0;
|
|
1767
|
-
const
|
|
1778
|
+
const positions = Array.isArray(artifactBundle?.positions) ? artifactBundle.positions : void 0;
|
|
1768
1779
|
return {
|
|
1769
1780
|
plugins,
|
|
1770
1781
|
api: {
|
|
@@ -1775,7 +1786,7 @@ async function createStandaloneStack(config) {
|
|
|
1775
1786
|
...objects ? { objects } : {},
|
|
1776
1787
|
...manifest ? { manifest } : {},
|
|
1777
1788
|
...permissions ? { permissions } : {},
|
|
1778
|
-
...
|
|
1789
|
+
...positions ? { positions } : {}
|
|
1779
1790
|
};
|
|
1780
1791
|
}
|
|
1781
1792
|
var import_node_path3, import_node_fs2, import_node_os, import_zod, import_types2, StandaloneStackConfigSchema;
|
|
@@ -1846,7 +1857,7 @@ __export(index_exports, {
|
|
|
1846
1857
|
SandboxError: () => SandboxError,
|
|
1847
1858
|
SeedLoaderService: () => import_objectql.SeedLoaderService,
|
|
1848
1859
|
UnimplementedScriptRunner: () => UnimplementedScriptRunner,
|
|
1849
|
-
_resetEnvDeprecationWarnings: () =>
|
|
1860
|
+
_resetEnvDeprecationWarnings: () => import_types4._resetEnvDeprecationWarnings,
|
|
1850
1861
|
actionBodyRunnerFactory: () => actionBodyRunnerFactory,
|
|
1851
1862
|
buildSecurityHeaders: () => buildSecurityHeaders,
|
|
1852
1863
|
collectBundleActions: () => collectBundleActions,
|
|
@@ -1867,7 +1878,7 @@ __export(index_exports, {
|
|
|
1867
1878
|
mergeRuntimeModule: () => mergeRuntimeModule,
|
|
1868
1879
|
parseTraceparent: () => parseTraceparent,
|
|
1869
1880
|
readArtifactSource: () => readArtifactSource,
|
|
1870
|
-
readEnvWithDeprecation: () =>
|
|
1881
|
+
readEnvWithDeprecation: () => import_types4.readEnvWithDeprecation,
|
|
1871
1882
|
resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
|
|
1872
1883
|
resolveErrorReporter: () => resolveErrorReporter,
|
|
1873
1884
|
resolveMetrics: () => resolveMetrics,
|
|
@@ -2148,7 +2159,9 @@ function safeGet(ctx, name) {
|
|
|
2148
2159
|
|
|
2149
2160
|
// src/http-dispatcher.ts
|
|
2150
2161
|
var import_core5 = require("@objectstack/core");
|
|
2162
|
+
var import_types3 = require("@objectstack/types");
|
|
2151
2163
|
var import_system = require("@objectstack/spec/system");
|
|
2164
|
+
var import_ai = require("@objectstack/spec/ai");
|
|
2152
2165
|
var import_shared2 = require("@objectstack/spec/shared");
|
|
2153
2166
|
init_package_state_store();
|
|
2154
2167
|
|
|
@@ -2183,6 +2196,12 @@ function checkApiExposure(def, action) {
|
|
|
2183
2196
|
|
|
2184
2197
|
// src/security/resolve-execution-context.ts
|
|
2185
2198
|
var import_core3 = require("@objectstack/core");
|
|
2199
|
+
function extractJwtBearer(headers) {
|
|
2200
|
+
const auth = headers.get("authorization");
|
|
2201
|
+
const bearer = auth?.match(/^Bearer\s+(\S+)$/i)?.[1];
|
|
2202
|
+
if (!bearer || bearer.startsWith("osk_")) return void 0;
|
|
2203
|
+
return bearer.split(".").length === 3 ? bearer : void 0;
|
|
2204
|
+
}
|
|
2186
2205
|
function toHeaders(input) {
|
|
2187
2206
|
if (!input) return new Headers();
|
|
2188
2207
|
if (typeof Headers !== "undefined" && input instanceof Headers) return input;
|
|
@@ -2201,6 +2220,22 @@ function toHeaders(input) {
|
|
|
2201
2220
|
async function resolveExecutionContext(opts) {
|
|
2202
2221
|
const headers = toHeaders(opts.request?.headers);
|
|
2203
2222
|
const ql = await opts.getQl();
|
|
2223
|
+
let oauthPrincipal;
|
|
2224
|
+
let oauthBearerPresented = false;
|
|
2225
|
+
if (opts.acceptOAuthAccessToken) {
|
|
2226
|
+
const jwtBearer = extractJwtBearer(headers);
|
|
2227
|
+
if (jwtBearer) {
|
|
2228
|
+
oauthBearerPresented = true;
|
|
2229
|
+
try {
|
|
2230
|
+
const authService = await opts.getService("auth");
|
|
2231
|
+
const verified = await authService?.verifyMcpAccessToken?.(jwtBearer);
|
|
2232
|
+
if (verified?.userId && Array.isArray(verified.scopes)) {
|
|
2233
|
+
oauthPrincipal = verified;
|
|
2234
|
+
}
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2204
2239
|
const getSession = async (h) => {
|
|
2205
2240
|
try {
|
|
2206
2241
|
const authService = await opts.getService("auth");
|
|
@@ -2211,19 +2246,29 @@ async function resolveExecutionContext(opts) {
|
|
|
2211
2246
|
return void 0;
|
|
2212
2247
|
}
|
|
2213
2248
|
};
|
|
2214
|
-
const
|
|
2249
|
+
const getSessionForProvenance = oauthPrincipal ? async () => ({ user: { id: oauthPrincipal.userId } }) : oauthBearerPresented ? async () => void 0 : getSession;
|
|
2250
|
+
const authz = await (0, import_core3.resolveAuthzContext)({ ql, headers, getSession: getSessionForProvenance });
|
|
2215
2251
|
const ctx = {
|
|
2216
|
-
|
|
2252
|
+
positions: authz.positions,
|
|
2217
2253
|
permissions: authz.permissions,
|
|
2218
2254
|
systemPermissions: authz.systemPermissions,
|
|
2219
2255
|
isSystem: false
|
|
2220
2256
|
};
|
|
2257
|
+
if (authz.userId) {
|
|
2258
|
+
ctx.principalKind = "human";
|
|
2259
|
+
} else {
|
|
2260
|
+
ctx.principalKind = "guest";
|
|
2261
|
+
ctx.positions = ["guest"];
|
|
2262
|
+
}
|
|
2221
2263
|
if (authz.userId) ctx.userId = authz.userId;
|
|
2222
2264
|
if (authz.tenantId) ctx.tenantId = authz.tenantId;
|
|
2223
2265
|
if (authz.email) ctx.email = authz.email;
|
|
2224
2266
|
if (authz.accessToken) ctx.accessToken = authz.accessToken;
|
|
2225
2267
|
if (authz.tabPermissions) ctx.tabPermissions = authz.tabPermissions;
|
|
2226
2268
|
ctx.org_user_ids = authz.org_user_ids;
|
|
2269
|
+
if (oauthPrincipal && ctx.userId === oauthPrincipal.userId) {
|
|
2270
|
+
ctx.oauthScopes = oauthPrincipal.scopes;
|
|
2271
|
+
}
|
|
2227
2272
|
if (authz.userId) {
|
|
2228
2273
|
const settings = await Promise.resolve(opts.getService("settings")).catch(() => void 0);
|
|
2229
2274
|
const localization = await (0, import_core3.resolveLocalizationContext)({
|
|
@@ -2467,9 +2512,10 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2467
2512
|
* Handle an MCP request over the Streamable HTTP transport (`/mcp`).
|
|
2468
2513
|
*
|
|
2469
2514
|
* Gating + auth (fail-closed):
|
|
2470
|
-
* - **
|
|
2471
|
-
* runtime
|
|
2472
|
-
* return 404 so the
|
|
2515
|
+
* - **default-on**: served unless `OS_MCP_SERVER_ENABLED=false` (single-env
|
|
2516
|
+
* runtime; MCP is a core platform capability). Multi-tenant cloud
|
|
2517
|
+
* overrides this gate per env. When opted out we return 404 so the
|
|
2518
|
+
* surface isn't advertised.
|
|
2473
2519
|
* - **auth**: requires a principal already resolved by
|
|
2474
2520
|
* `resolveExecutionContext` (the `sys_api_key` Bearer/header path or a
|
|
2475
2521
|
* session). Anonymous → 401.
|
|
@@ -2489,7 +2535,29 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2489
2535
|
}
|
|
2490
2536
|
const ec = context.executionContext;
|
|
2491
2537
|
if (!ec || !ec.userId && !ec.isSystem) {
|
|
2492
|
-
|
|
2538
|
+
const resourceMetadataUrl = await this.getMcpResourceMetadataUrl(context);
|
|
2539
|
+
const response = this.error(
|
|
2540
|
+
resourceMetadataUrl ? "Unauthorized: a valid OAuth access token or API key is required" : "Unauthorized: a valid API key is required",
|
|
2541
|
+
401
|
|
2542
|
+
);
|
|
2543
|
+
if (resourceMetadataUrl) {
|
|
2544
|
+
response.headers = {
|
|
2545
|
+
"WWW-Authenticate": `Bearer realm="ObjectStack MCP", resource_metadata="${resourceMetadataUrl}"`
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
return { handled: true, response };
|
|
2549
|
+
}
|
|
2550
|
+
const grantedScopes = Array.isArray(ec.oauthScopes) ? ec.oauthScopes : void 0;
|
|
2551
|
+
if (grantedScopes && !grantedScopes.some((s) => import_ai.MCP_OAUTH_SCOPES.includes(s))) {
|
|
2552
|
+
const resourceMetadataUrl = await this.getMcpResourceMetadataUrl(context);
|
|
2553
|
+
const response = this.error(
|
|
2554
|
+
`Forbidden: the access token grants none of the MCP scopes (${import_ai.MCP_OAUTH_SCOPES.join(", ")})`,
|
|
2555
|
+
403
|
|
2556
|
+
);
|
|
2557
|
+
response.headers = {
|
|
2558
|
+
"WWW-Authenticate": `Bearer error="insufficient_scope", scope="${import_ai.MCP_OAUTH_SCOPES.join(" ")}"` + (resourceMetadataUrl ? `, resource_metadata="${resourceMetadataUrl}"` : "")
|
|
2559
|
+
};
|
|
2560
|
+
return { handled: true, response };
|
|
2493
2561
|
}
|
|
2494
2562
|
const webRequest = this.toMcpWebRequest(context.request, body);
|
|
2495
2563
|
if (!webRequest) {
|
|
@@ -2498,7 +2566,13 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2498
2566
|
const bridge = this.buildMcpBridge(context);
|
|
2499
2567
|
let webRes;
|
|
2500
2568
|
try {
|
|
2501
|
-
webRes = await mcp.handleHttpRequest(webRequest, {
|
|
2569
|
+
webRes = await mcp.handleHttpRequest(webRequest, {
|
|
2570
|
+
bridge,
|
|
2571
|
+
parsedBody: body,
|
|
2572
|
+
// undefined = not scope-limited (API key / session); an array
|
|
2573
|
+
// narrows the registered tool families inside the MCP runtime.
|
|
2574
|
+
...grantedScopes ? { toolOptions: { grantedScopes } } : {}
|
|
2575
|
+
});
|
|
2502
2576
|
} catch (err) {
|
|
2503
2577
|
return { handled: true, response: this.error(err?.message ?? "MCP request failed", 500) };
|
|
2504
2578
|
}
|
|
@@ -2525,9 +2599,28 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2525
2599
|
}
|
|
2526
2600
|
return { handled: true, response: { status: webRes.status, headers, body: responseBody } };
|
|
2527
2601
|
}
|
|
2528
|
-
/**
|
|
2602
|
+
/**
|
|
2603
|
+
* Whether the MCP HTTP surface is on for this single-env runtime.
|
|
2604
|
+
* Default-on core capability; `OS_MCP_SERVER_ENABLED=false` opts out
|
|
2605
|
+
* (single decision point: `isMcpServerEnabled` in `@objectstack/types`).
|
|
2606
|
+
*/
|
|
2529
2607
|
static isMcpEnabled() {
|
|
2530
|
-
return
|
|
2608
|
+
return (0, import_types3.isMcpServerEnabled)();
|
|
2609
|
+
}
|
|
2610
|
+
/**
|
|
2611
|
+
* Absolute URL of the RFC 9728 protected-resource metadata for the MCP
|
|
2612
|
+
* endpoint, advertised via `WWW-Authenticate` (#2698). `null` when the
|
|
2613
|
+
* OAuth track is off — the auth service owns the decision (AS enabled +
|
|
2614
|
+
* OAuth 2.1 TLS rule), the dispatcher only relays it. Never throws.
|
|
2615
|
+
*/
|
|
2616
|
+
async getMcpResourceMetadataUrl(context) {
|
|
2617
|
+
try {
|
|
2618
|
+
const authService = await this.resolveService("auth", context.environmentId);
|
|
2619
|
+
const url = authService?.getMcpResourceMetadataUrl?.();
|
|
2620
|
+
return typeof url === "string" && url ? url : null;
|
|
2621
|
+
} catch {
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2531
2624
|
}
|
|
2532
2625
|
/**
|
|
2533
2626
|
* Normalise the inbound request into a Web-standard `Request` for the MCP
|
|
@@ -3185,9 +3278,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3185
3278
|
notifications: hasNotification ? `${prefix}/notifications` : void 0,
|
|
3186
3279
|
ai: hasAi ? `${prefix}/ai` : void 0,
|
|
3187
3280
|
i18n: hasI18n ? `${prefix}/i18n` : void 0,
|
|
3188
|
-
// MCP (Streamable HTTP) is
|
|
3189
|
-
//
|
|
3190
|
-
//
|
|
3281
|
+
// MCP (Streamable HTTP) is a default-on core capability —
|
|
3282
|
+
// advertised unless OS_MCP_SERVER_ENABLED=false opts the env
|
|
3283
|
+
// out. The objectui Integrations page reads this.
|
|
3191
3284
|
mcp: _HttpDispatcher.isMcpEnabled() ? `${prefix}/mcp` : void 0
|
|
3192
3285
|
};
|
|
3193
3286
|
const svcAvailable = (route, provider) => ({
|
|
@@ -4477,7 +4570,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4477
4570
|
const ec = context?.executionContext;
|
|
4478
4571
|
const userIdFromAuth = context?.user?.id ?? context?.userId ?? ec?.userId;
|
|
4479
4572
|
if (userIdFromAuth) automationContext.userId = userIdFromAuth;
|
|
4480
|
-
if (Array.isArray(ec?.
|
|
4573
|
+
if (Array.isArray(ec?.positions) && ec.positions.length) automationContext.positions = ec.positions;
|
|
4481
4574
|
if (Array.isArray(ec?.permissions) && ec.permissions.length) automationContext.permissions = ec.permissions;
|
|
4482
4575
|
if (ec?.tenantId) automationContext.tenantId = ec.tenantId;
|
|
4483
4576
|
const result = await automationService.execute(name, automationContext);
|
|
@@ -4785,7 +4878,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4785
4878
|
id: ec.userId,
|
|
4786
4879
|
displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
|
|
4787
4880
|
email: ec.userEmail,
|
|
4788
|
-
roles: Array.isArray(ec.
|
|
4881
|
+
roles: Array.isArray(ec.positions) ? ec.positions : [],
|
|
4789
4882
|
permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
|
|
4790
4883
|
organizationId: ec.tenantId
|
|
4791
4884
|
} : void 0;
|
|
@@ -4849,7 +4942,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4849
4942
|
if (!svc) {
|
|
4850
4943
|
return { handled: true, response: this.error("Sharing is not configured for this environment", 501) };
|
|
4851
4944
|
}
|
|
4852
|
-
const SYSTEM_CTX = { isSystem: true,
|
|
4945
|
+
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
|
|
4853
4946
|
const m = method.toUpperCase();
|
|
4854
4947
|
const parts = subPath.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
4855
4948
|
const ec = context.executionContext;
|
|
@@ -5028,7 +5121,13 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5028
5121
|
}
|
|
5029
5122
|
return this.getObjectQLService(context.environmentId);
|
|
5030
5123
|
},
|
|
5031
|
-
request: context.request
|
|
5124
|
+
request: context.request,
|
|
5125
|
+
// OAuth 2.1 access tokens are honoured ONLY on the MCP
|
|
5126
|
+
// surface (#2698): their coarse tool-family scopes are
|
|
5127
|
+
// enforced at MCP tool dispatch, which other routes don't do.
|
|
5128
|
+
// Matches the plain and `/projects/:id`-scoped route forms
|
|
5129
|
+
// (the scoped prefix is stripped only later, below).
|
|
5130
|
+
acceptOAuthAccessToken: /^(?:\/projects\/[^/]+)?\/mcp(?:[/?]|$)/.test(cleanPath)
|
|
5032
5131
|
});
|
|
5033
5132
|
} catch {
|
|
5034
5133
|
}
|
|
@@ -6169,7 +6268,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6169
6268
|
id: userId,
|
|
6170
6269
|
displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
|
|
6171
6270
|
email: sessionData?.user?.email,
|
|
6172
|
-
|
|
6271
|
+
positions: [],
|
|
6173
6272
|
permissions: [],
|
|
6174
6273
|
organizationId: sessionData?.session?.activeOrganizationId
|
|
6175
6274
|
};
|
|
@@ -6543,7 +6642,7 @@ init_body_runner();
|
|
|
6543
6642
|
// src/index.ts
|
|
6544
6643
|
var import_rest = require("@objectstack/rest");
|
|
6545
6644
|
__reExport(index_exports, require("@objectstack/core"), module.exports);
|
|
6546
|
-
var
|
|
6645
|
+
var import_types4 = require("@objectstack/types");
|
|
6547
6646
|
// Annotate the CommonJS export names for ESM import in node:
|
|
6548
6647
|
0 && (module.exports = {
|
|
6549
6648
|
AppPlugin,
|