@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 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 = JSON.stringify(v ?? null);
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 = JSON.stringify(v ?? null);
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 = JSON.stringify(v ?? null);
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
- ["roles", "role"],
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
- "roles",
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 roles = Array.isArray(artifactBundle?.roles) ? artifactBundle.roles : void 0;
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
- ...roles ? { roles } : {}
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: () => import_types3._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: () => import_types3.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 authz = await (0, import_core3.resolveAuthzContext)({ ql, headers, getSession });
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
- roles: authz.roles,
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
- * - **opt-in**: only served when `OS_MCP_SERVER_ENABLED=true` (single-env
2471
- * runtime). Multi-tenant cloud overrides this gate per env. When off we
2472
- * return 404 so the surface isn't advertised.
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
- return { handled: true, response: this.error("Unauthorized: a valid API key is required", 401) };
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, { bridge, parsedBody: body });
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
- /** Whether the MCP HTTP surface is opted in for this single-env runtime. */
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 typeof process !== "undefined" && process.env?.OS_MCP_SERVER_ENABLED === "true";
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 opt-in per env only advertised
3189
- // when OS_MCP_SERVER_ENABLED=true so the surface isn't exposed
3190
- // by default. The objectui Integrations page reads this.
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?.roles) && ec.roles.length) automationContext.roles = ec.roles;
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.roles) ? ec.roles : [],
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, roles: [], permissions: [] };
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
- roles: [],
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 import_types3 = require("@objectstack/types");
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,