@objectstack/runtime 12.6.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
@@ -1054,7 +1054,7 @@ var init_app_plugin = __esm({
1054
1054
  if (typeof metadata?.registerInMemory === "function") {
1055
1055
  const securityBundle = this.bundle.manifest ? { ...this.bundle.manifest, ...this.bundle } : this.bundle;
1056
1056
  const SECURITY_FIELDS = [
1057
- ["roles", "role"],
1057
+ ["positions", "position"],
1058
1058
  ["permissions", "permission"],
1059
1059
  ["sharingRules", "sharing_rule"],
1060
1060
  ["policies", "policy"]
@@ -1420,8 +1420,7 @@ var init_app_plugin = __esm({
1420
1420
  "skills",
1421
1421
  "actions",
1422
1422
  "permissions",
1423
- "roles",
1424
- "profiles",
1423
+ "positions",
1425
1424
  "translations",
1426
1425
  "sharingRules",
1427
1426
  "ragPipelines",
@@ -1776,7 +1775,7 @@ async function createStandaloneStack(config) {
1776
1775
  const objects = Array.isArray(artifactBundle?.objects) ? artifactBundle.objects : void 0;
1777
1776
  const manifest = artifactBundle?.manifest;
1778
1777
  const permissions = Array.isArray(artifactBundle?.permissions) ? artifactBundle.permissions : void 0;
1779
- const roles = Array.isArray(artifactBundle?.roles) ? artifactBundle.roles : void 0;
1778
+ const positions = Array.isArray(artifactBundle?.positions) ? artifactBundle.positions : void 0;
1780
1779
  return {
1781
1780
  plugins,
1782
1781
  api: {
@@ -1787,7 +1786,7 @@ async function createStandaloneStack(config) {
1787
1786
  ...objects ? { objects } : {},
1788
1787
  ...manifest ? { manifest } : {},
1789
1788
  ...permissions ? { permissions } : {},
1790
- ...roles ? { roles } : {}
1789
+ ...positions ? { positions } : {}
1791
1790
  };
1792
1791
  }
1793
1792
  var import_node_path3, import_node_fs2, import_node_os, import_zod, import_types2, StandaloneStackConfigSchema;
@@ -1858,7 +1857,7 @@ __export(index_exports, {
1858
1857
  SandboxError: () => SandboxError,
1859
1858
  SeedLoaderService: () => import_objectql.SeedLoaderService,
1860
1859
  UnimplementedScriptRunner: () => UnimplementedScriptRunner,
1861
- _resetEnvDeprecationWarnings: () => import_types3._resetEnvDeprecationWarnings,
1860
+ _resetEnvDeprecationWarnings: () => import_types4._resetEnvDeprecationWarnings,
1862
1861
  actionBodyRunnerFactory: () => actionBodyRunnerFactory,
1863
1862
  buildSecurityHeaders: () => buildSecurityHeaders,
1864
1863
  collectBundleActions: () => collectBundleActions,
@@ -1879,7 +1878,7 @@ __export(index_exports, {
1879
1878
  mergeRuntimeModule: () => mergeRuntimeModule,
1880
1879
  parseTraceparent: () => parseTraceparent,
1881
1880
  readArtifactSource: () => readArtifactSource,
1882
- readEnvWithDeprecation: () => import_types3.readEnvWithDeprecation,
1881
+ readEnvWithDeprecation: () => import_types4.readEnvWithDeprecation,
1883
1882
  resolveDefaultArtifactPath: () => resolveDefaultArtifactPath,
1884
1883
  resolveErrorReporter: () => resolveErrorReporter,
1885
1884
  resolveMetrics: () => resolveMetrics,
@@ -2160,7 +2159,9 @@ function safeGet(ctx, name) {
2160
2159
 
2161
2160
  // src/http-dispatcher.ts
2162
2161
  var import_core5 = require("@objectstack/core");
2162
+ var import_types3 = require("@objectstack/types");
2163
2163
  var import_system = require("@objectstack/spec/system");
2164
+ var import_ai = require("@objectstack/spec/ai");
2164
2165
  var import_shared2 = require("@objectstack/spec/shared");
2165
2166
  init_package_state_store();
2166
2167
 
@@ -2195,6 +2196,12 @@ function checkApiExposure(def, action) {
2195
2196
 
2196
2197
  // src/security/resolve-execution-context.ts
2197
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
+ }
2198
2205
  function toHeaders(input) {
2199
2206
  if (!input) return new Headers();
2200
2207
  if (typeof Headers !== "undefined" && input instanceof Headers) return input;
@@ -2213,6 +2220,22 @@ function toHeaders(input) {
2213
2220
  async function resolveExecutionContext(opts) {
2214
2221
  const headers = toHeaders(opts.request?.headers);
2215
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
+ }
2216
2239
  const getSession = async (h) => {
2217
2240
  try {
2218
2241
  const authService = await opts.getService("auth");
@@ -2223,19 +2246,29 @@ async function resolveExecutionContext(opts) {
2223
2246
  return void 0;
2224
2247
  }
2225
2248
  };
2226
- 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 });
2227
2251
  const ctx = {
2228
- roles: authz.roles,
2252
+ positions: authz.positions,
2229
2253
  permissions: authz.permissions,
2230
2254
  systemPermissions: authz.systemPermissions,
2231
2255
  isSystem: false
2232
2256
  };
2257
+ if (authz.userId) {
2258
+ ctx.principalKind = "human";
2259
+ } else {
2260
+ ctx.principalKind = "guest";
2261
+ ctx.positions = ["guest"];
2262
+ }
2233
2263
  if (authz.userId) ctx.userId = authz.userId;
2234
2264
  if (authz.tenantId) ctx.tenantId = authz.tenantId;
2235
2265
  if (authz.email) ctx.email = authz.email;
2236
2266
  if (authz.accessToken) ctx.accessToken = authz.accessToken;
2237
2267
  if (authz.tabPermissions) ctx.tabPermissions = authz.tabPermissions;
2238
2268
  ctx.org_user_ids = authz.org_user_ids;
2269
+ if (oauthPrincipal && ctx.userId === oauthPrincipal.userId) {
2270
+ ctx.oauthScopes = oauthPrincipal.scopes;
2271
+ }
2239
2272
  if (authz.userId) {
2240
2273
  const settings = await Promise.resolve(opts.getService("settings")).catch(() => void 0);
2241
2274
  const localization = await (0, import_core3.resolveLocalizationContext)({
@@ -2479,9 +2512,10 @@ var _HttpDispatcher = class _HttpDispatcher {
2479
2512
  * Handle an MCP request over the Streamable HTTP transport (`/mcp`).
2480
2513
  *
2481
2514
  * Gating + auth (fail-closed):
2482
- * - **opt-in**: only served when `OS_MCP_SERVER_ENABLED=true` (single-env
2483
- * runtime). Multi-tenant cloud overrides this gate per env. When off we
2484
- * 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.
2485
2519
  * - **auth**: requires a principal already resolved by
2486
2520
  * `resolveExecutionContext` (the `sys_api_key` Bearer/header path or a
2487
2521
  * session). Anonymous → 401.
@@ -2501,7 +2535,29 @@ var _HttpDispatcher = class _HttpDispatcher {
2501
2535
  }
2502
2536
  const ec = context.executionContext;
2503
2537
  if (!ec || !ec.userId && !ec.isSystem) {
2504
- 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 };
2505
2561
  }
2506
2562
  const webRequest = this.toMcpWebRequest(context.request, body);
2507
2563
  if (!webRequest) {
@@ -2510,7 +2566,13 @@ var _HttpDispatcher = class _HttpDispatcher {
2510
2566
  const bridge = this.buildMcpBridge(context);
2511
2567
  let webRes;
2512
2568
  try {
2513
- 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
+ });
2514
2576
  } catch (err) {
2515
2577
  return { handled: true, response: this.error(err?.message ?? "MCP request failed", 500) };
2516
2578
  }
@@ -2537,9 +2599,28 @@ var _HttpDispatcher = class _HttpDispatcher {
2537
2599
  }
2538
2600
  return { handled: true, response: { status: webRes.status, headers, body: responseBody } };
2539
2601
  }
2540
- /** 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
+ */
2541
2607
  static isMcpEnabled() {
2542
- 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
+ }
2543
2624
  }
2544
2625
  /**
2545
2626
  * Normalise the inbound request into a Web-standard `Request` for the MCP
@@ -3197,9 +3278,9 @@ var _HttpDispatcher = class _HttpDispatcher {
3197
3278
  notifications: hasNotification ? `${prefix}/notifications` : void 0,
3198
3279
  ai: hasAi ? `${prefix}/ai` : void 0,
3199
3280
  i18n: hasI18n ? `${prefix}/i18n` : void 0,
3200
- // MCP (Streamable HTTP) is opt-in per env only advertised
3201
- // when OS_MCP_SERVER_ENABLED=true so the surface isn't exposed
3202
- // 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.
3203
3284
  mcp: _HttpDispatcher.isMcpEnabled() ? `${prefix}/mcp` : void 0
3204
3285
  };
3205
3286
  const svcAvailable = (route, provider) => ({
@@ -4489,7 +4570,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4489
4570
  const ec = context?.executionContext;
4490
4571
  const userIdFromAuth = context?.user?.id ?? context?.userId ?? ec?.userId;
4491
4572
  if (userIdFromAuth) automationContext.userId = userIdFromAuth;
4492
- 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;
4493
4574
  if (Array.isArray(ec?.permissions) && ec.permissions.length) automationContext.permissions = ec.permissions;
4494
4575
  if (ec?.tenantId) automationContext.tenantId = ec.tenantId;
4495
4576
  const result = await automationService.execute(name, automationContext);
@@ -4797,7 +4878,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4797
4878
  id: ec.userId,
4798
4879
  displayName: ec.userDisplayName ?? ec.userName ?? ec.userId,
4799
4880
  email: ec.userEmail,
4800
- roles: Array.isArray(ec.roles) ? ec.roles : [],
4881
+ roles: Array.isArray(ec.positions) ? ec.positions : [],
4801
4882
  permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
4802
4883
  organizationId: ec.tenantId
4803
4884
  } : void 0;
@@ -4861,7 +4942,7 @@ var _HttpDispatcher = class _HttpDispatcher {
4861
4942
  if (!svc) {
4862
4943
  return { handled: true, response: this.error("Sharing is not configured for this environment", 501) };
4863
4944
  }
4864
- const SYSTEM_CTX = { isSystem: true, roles: [], permissions: [] };
4945
+ const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] };
4865
4946
  const m = method.toUpperCase();
4866
4947
  const parts = subPath.replace(/^\/+/, "").split("/").filter(Boolean);
4867
4948
  const ec = context.executionContext;
@@ -5040,7 +5121,13 @@ var _HttpDispatcher = class _HttpDispatcher {
5040
5121
  }
5041
5122
  return this.getObjectQLService(context.environmentId);
5042
5123
  },
5043
- 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)
5044
5131
  });
5045
5132
  } catch {
5046
5133
  }
@@ -6181,7 +6268,7 @@ function createDispatcherPlugin(config = {}) {
6181
6268
  id: userId,
6182
6269
  displayName: sessionData?.user?.name ?? sessionData?.user?.email ?? userId,
6183
6270
  email: sessionData?.user?.email,
6184
- roles: [],
6271
+ positions: [],
6185
6272
  permissions: [],
6186
6273
  organizationId: sessionData?.session?.activeOrganizationId
6187
6274
  };
@@ -6555,7 +6642,7 @@ init_body_runner();
6555
6642
  // src/index.ts
6556
6643
  var import_rest = require("@objectstack/rest");
6557
6644
  __reExport(index_exports, require("@objectstack/core"), module.exports);
6558
- var import_types3 = require("@objectstack/types");
6645
+ var import_types4 = require("@objectstack/types");
6559
6646
  // Annotate the CommonJS export names for ESM import in node:
6560
6647
  0 && (module.exports = {
6561
6648
  AppPlugin,