@objectstack/runtime 14.3.0 → 14.5.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 +54 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -1
- package/dist/index.d.ts +28 -1
- package/dist/index.js +50 -9
- package/dist/index.js.map +1 -1
- package/package.json +20 -20
package/dist/index.d.cts
CHANGED
|
@@ -682,6 +682,19 @@ interface DispatcherPluginConfig {
|
|
|
682
682
|
* where membership has not been seeded.
|
|
683
683
|
*/
|
|
684
684
|
enforceProjectMembership?: boolean;
|
|
685
|
+
/**
|
|
686
|
+
* Reject anonymous requests to `auth: true` service routes (AI, etc.) with
|
|
687
|
+
* HTTP 401, mirroring the REST API's `requireAuth` gate. Must match the
|
|
688
|
+
* REST plugin's `api.requireAuth` so `/ai` and `/meta` stay in lockstep
|
|
689
|
+
* with `/data` — otherwise the AI routes' declared `auth: true` contract is
|
|
690
|
+
* never enforced and anonymous callers reach adapter/model status routes.
|
|
691
|
+
*
|
|
692
|
+
* Defaults to `false` (backward-compatible: previously nothing enforced
|
|
693
|
+
* `RouteDefinition.auth` here). Hosts pass their `api.requireAuth` through —
|
|
694
|
+
* the framework `serve` command and the cloud apps do so from the same
|
|
695
|
+
* stack `api` config the REST plugin reads.
|
|
696
|
+
*/
|
|
697
|
+
requireAuth?: boolean;
|
|
685
698
|
/**
|
|
686
699
|
* Security response headers. When provided, every response routed
|
|
687
700
|
* through this plugin gets the headers merged in (route-specific
|
|
@@ -955,6 +968,14 @@ interface HttpDispatcherOptions {
|
|
|
955
968
|
* called on every scoped request so idle projects are evicted after TTL.
|
|
956
969
|
*/
|
|
957
970
|
scopeManager?: EnvironmentScopeManager;
|
|
971
|
+
/**
|
|
972
|
+
* Reject anonymous requests to `auth: true` service routes (AI) and to the
|
|
973
|
+
* metadata catch-all with HTTP 401, mirroring the REST API's `requireAuth`
|
|
974
|
+
* gate. Matches {@link DispatcherPluginConfig.requireAuth}; the dispatcher
|
|
975
|
+
* plugin threads the host's `api.requireAuth` here. Defaults to `false`
|
|
976
|
+
* (backward-compatible — nothing enforced `RouteDefinition.auth` before).
|
|
977
|
+
*/
|
|
978
|
+
requireAuth?: boolean;
|
|
958
979
|
}
|
|
959
980
|
/**
|
|
960
981
|
* The HTTP dispatch engine — translates an inbound (method, path, body, ctx)
|
|
@@ -982,6 +1003,12 @@ declare class HttpDispatcher {
|
|
|
982
1003
|
* `DispatcherConfig.enforceProjectMembership`).
|
|
983
1004
|
*/
|
|
984
1005
|
private enforceMembership;
|
|
1006
|
+
/**
|
|
1007
|
+
* When `true`, `auth: true` AI routes and the metadata catch-all reject
|
|
1008
|
+
* anonymous callers with 401 (mirrors the REST `requireAuth` gate). Set
|
|
1009
|
+
* from {@link HttpDispatcherOptions.requireAuth}. Defaults to `false`.
|
|
1010
|
+
*/
|
|
1011
|
+
private requireAuth;
|
|
985
1012
|
/**
|
|
986
1013
|
* In-memory cache of positive membership checks, keyed by
|
|
987
1014
|
* `${environmentId}:${userId}`. Entries expire 60 seconds after insertion
|
|
@@ -1498,7 +1525,7 @@ declare class HttpDispatcher {
|
|
|
1498
1525
|
* Handles Analytics requests
|
|
1499
1526
|
* path: sub-path after /analytics/
|
|
1500
1527
|
*/
|
|
1501
|
-
handleAnalytics(path: string, method: string, body: any,
|
|
1528
|
+
handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1502
1529
|
/**
|
|
1503
1530
|
* Handles in-app notification requests (ADR-0030) — the
|
|
1504
1531
|
* `/api/v1/notifications` surface backed by the messaging service's inbox
|
package/dist/index.d.ts
CHANGED
|
@@ -682,6 +682,19 @@ interface DispatcherPluginConfig {
|
|
|
682
682
|
* where membership has not been seeded.
|
|
683
683
|
*/
|
|
684
684
|
enforceProjectMembership?: boolean;
|
|
685
|
+
/**
|
|
686
|
+
* Reject anonymous requests to `auth: true` service routes (AI, etc.) with
|
|
687
|
+
* HTTP 401, mirroring the REST API's `requireAuth` gate. Must match the
|
|
688
|
+
* REST plugin's `api.requireAuth` so `/ai` and `/meta` stay in lockstep
|
|
689
|
+
* with `/data` — otherwise the AI routes' declared `auth: true` contract is
|
|
690
|
+
* never enforced and anonymous callers reach adapter/model status routes.
|
|
691
|
+
*
|
|
692
|
+
* Defaults to `false` (backward-compatible: previously nothing enforced
|
|
693
|
+
* `RouteDefinition.auth` here). Hosts pass their `api.requireAuth` through —
|
|
694
|
+
* the framework `serve` command and the cloud apps do so from the same
|
|
695
|
+
* stack `api` config the REST plugin reads.
|
|
696
|
+
*/
|
|
697
|
+
requireAuth?: boolean;
|
|
685
698
|
/**
|
|
686
699
|
* Security response headers. When provided, every response routed
|
|
687
700
|
* through this plugin gets the headers merged in (route-specific
|
|
@@ -955,6 +968,14 @@ interface HttpDispatcherOptions {
|
|
|
955
968
|
* called on every scoped request so idle projects are evicted after TTL.
|
|
956
969
|
*/
|
|
957
970
|
scopeManager?: EnvironmentScopeManager;
|
|
971
|
+
/**
|
|
972
|
+
* Reject anonymous requests to `auth: true` service routes (AI) and to the
|
|
973
|
+
* metadata catch-all with HTTP 401, mirroring the REST API's `requireAuth`
|
|
974
|
+
* gate. Matches {@link DispatcherPluginConfig.requireAuth}; the dispatcher
|
|
975
|
+
* plugin threads the host's `api.requireAuth` here. Defaults to `false`
|
|
976
|
+
* (backward-compatible — nothing enforced `RouteDefinition.auth` before).
|
|
977
|
+
*/
|
|
978
|
+
requireAuth?: boolean;
|
|
958
979
|
}
|
|
959
980
|
/**
|
|
960
981
|
* The HTTP dispatch engine — translates an inbound (method, path, body, ctx)
|
|
@@ -982,6 +1003,12 @@ declare class HttpDispatcher {
|
|
|
982
1003
|
* `DispatcherConfig.enforceProjectMembership`).
|
|
983
1004
|
*/
|
|
984
1005
|
private enforceMembership;
|
|
1006
|
+
/**
|
|
1007
|
+
* When `true`, `auth: true` AI routes and the metadata catch-all reject
|
|
1008
|
+
* anonymous callers with 401 (mirrors the REST `requireAuth` gate). Set
|
|
1009
|
+
* from {@link HttpDispatcherOptions.requireAuth}. Defaults to `false`.
|
|
1010
|
+
*/
|
|
1011
|
+
private requireAuth;
|
|
985
1012
|
/**
|
|
986
1013
|
* In-memory cache of positive membership checks, keyed by
|
|
987
1014
|
* `${environmentId}:${userId}`. Entries expire 60 seconds after insertion
|
|
@@ -1498,7 +1525,7 @@ declare class HttpDispatcher {
|
|
|
1498
1525
|
* Handles Analytics requests
|
|
1499
1526
|
* path: sub-path after /analytics/
|
|
1500
1527
|
*/
|
|
1501
|
-
handleAnalytics(path: string, method: string, body: any,
|
|
1528
|
+
handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1502
1529
|
/**
|
|
1503
1530
|
* Handles in-app notification requests (ADR-0030) — the
|
|
1504
1531
|
* `/api/v1/notifications` surface backed by the messaging service's inbox
|
package/dist/index.js
CHANGED
|
@@ -2119,6 +2119,7 @@ function checkApiExposure(def, action) {
|
|
|
2119
2119
|
}
|
|
2120
2120
|
|
|
2121
2121
|
// src/security/resolve-execution-context.ts
|
|
2122
|
+
import { scopesToAgentPermissionSets, MCP_OAUTH_SCOPE_ACTIONS } from "@objectstack/spec/ai";
|
|
2122
2123
|
import {
|
|
2123
2124
|
resolveAuthzContext,
|
|
2124
2125
|
resolveLocalizationContext
|
|
@@ -2182,7 +2183,15 @@ async function resolveExecutionContext(opts) {
|
|
|
2182
2183
|
isSystem: false
|
|
2183
2184
|
};
|
|
2184
2185
|
if (authz.userId) {
|
|
2185
|
-
|
|
2186
|
+
if (oauthPrincipal?.clientId) {
|
|
2187
|
+
ctx.principalKind = "agent";
|
|
2188
|
+
ctx.onBehalfOf = { userId: authz.userId, principalKind: "human" };
|
|
2189
|
+
ctx.permissions = scopesToAgentPermissionSets(oauthPrincipal.scopes);
|
|
2190
|
+
ctx.positions = [];
|
|
2191
|
+
ctx.systemPermissions = oauthPrincipal.scopes?.includes(MCP_OAUTH_SCOPE_ACTIONS) ? authz.systemPermissions ?? [] : [];
|
|
2192
|
+
} else {
|
|
2193
|
+
ctx.principalKind = "human";
|
|
2194
|
+
}
|
|
2186
2195
|
} else {
|
|
2187
2196
|
ctx.principalKind = "guest";
|
|
2188
2197
|
ctx.positions = ["guest"];
|
|
@@ -2266,6 +2275,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2266
2275
|
}
|
|
2267
2276
|
};
|
|
2268
2277
|
this.enforceMembership = options?.enforceProjectMembership ?? true;
|
|
2278
|
+
this.requireAuth = options?.requireAuth ?? false;
|
|
2269
2279
|
this.kernelResolver = options?.kernelResolver ?? resolveService("kernel-resolver");
|
|
2270
2280
|
this.scopeManager = options?.scopeManager ?? resolveService("scope-manager");
|
|
2271
2281
|
}
|
|
@@ -3431,6 +3441,15 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3431
3441
|
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
3432
3442
|
*/
|
|
3433
3443
|
async handleMetadata(path, _context, method, body, query) {
|
|
3444
|
+
if (this.requireAuth) {
|
|
3445
|
+
const ec = _context.executionContext;
|
|
3446
|
+
if (!ec?.userId && !ec?.isSystem) {
|
|
3447
|
+
return {
|
|
3448
|
+
handled: true,
|
|
3449
|
+
response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
|
|
3450
|
+
};
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3434
3453
|
const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
3435
3454
|
if (parts[0] === "types") {
|
|
3436
3455
|
const protocol = await this.resolveService("protocol");
|
|
@@ -3730,13 +3749,13 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3730
3749
|
* Handles Analytics requests
|
|
3731
3750
|
* path: sub-path after /analytics/
|
|
3732
3751
|
*/
|
|
3733
|
-
async handleAnalytics(path, method, body,
|
|
3752
|
+
async handleAnalytics(path, method, body, context) {
|
|
3734
3753
|
const analyticsService = await this.getService(CoreServiceName.enum.analytics);
|
|
3735
3754
|
if (!analyticsService) return { handled: false };
|
|
3736
3755
|
const m = method.toUpperCase();
|
|
3737
3756
|
const subPath = path.replace(/^\/+/, "");
|
|
3738
3757
|
if (subPath === "query" && m === "POST") {
|
|
3739
|
-
const result = await analyticsService.query(body);
|
|
3758
|
+
const result = await analyticsService.query(body, context?.executionContext);
|
|
3740
3759
|
return { handled: true, response: this.success(result) };
|
|
3741
3760
|
}
|
|
3742
3761
|
if (subPath === "meta" && m === "GET") {
|
|
@@ -3744,7 +3763,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3744
3763
|
return { handled: true, response: this.success(result) };
|
|
3745
3764
|
}
|
|
3746
3765
|
if (subPath === "sql" && m === "POST") {
|
|
3747
|
-
const result = await analyticsService.generateSql(body);
|
|
3766
|
+
const result = await analyticsService.generateSql(body, context?.executionContext);
|
|
3748
3767
|
return { handled: true, response: this.success(result) };
|
|
3749
3768
|
}
|
|
3750
3769
|
return { handled: false };
|
|
@@ -4932,6 +4951,15 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4932
4951
|
if (route.method !== method) continue;
|
|
4933
4952
|
const params = matchRoute(route.path, fullPath);
|
|
4934
4953
|
if (params === null) continue;
|
|
4954
|
+
if (this.requireAuth && route.auth !== false) {
|
|
4955
|
+
const gec = context.executionContext;
|
|
4956
|
+
if (!gec?.userId && !gec?.isSystem) {
|
|
4957
|
+
return {
|
|
4958
|
+
handled: true,
|
|
4959
|
+
response: this.error("Authentication is required to access this endpoint.", 401, { code: "unauthenticated" })
|
|
4960
|
+
};
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4935
4963
|
const ec = context.executionContext;
|
|
4936
4964
|
const user = ec?.userId ? {
|
|
4937
4965
|
userId: ec.userId,
|
|
@@ -5664,7 +5692,7 @@ function resolveErrorReporter(ctx, override) {
|
|
|
5664
5692
|
}
|
|
5665
5693
|
|
|
5666
5694
|
// src/dispatcher-plugin.ts
|
|
5667
|
-
function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
|
|
5695
|
+
function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser, requireAuth = false) {
|
|
5668
5696
|
const handler = async (req, res) => {
|
|
5669
5697
|
try {
|
|
5670
5698
|
let user;
|
|
@@ -5674,6 +5702,17 @@ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUs
|
|
|
5674
5702
|
} catch {
|
|
5675
5703
|
}
|
|
5676
5704
|
}
|
|
5705
|
+
if (requireAuth && route.auth !== false && !user) {
|
|
5706
|
+
res.status(401);
|
|
5707
|
+
if (securityHeaders) {
|
|
5708
|
+
for (const [k, v] of Object.entries(securityHeaders)) res.header(k, v);
|
|
5709
|
+
}
|
|
5710
|
+
res.json({
|
|
5711
|
+
error: "unauthenticated",
|
|
5712
|
+
message: "Authentication is required to access this endpoint."
|
|
5713
|
+
});
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5677
5716
|
const result = await route.handler({
|
|
5678
5717
|
body: req.body,
|
|
5679
5718
|
params: req.params,
|
|
@@ -5857,8 +5896,10 @@ function createDispatcherPlugin(config = {}) {
|
|
|
5857
5896
|
if (!server) return;
|
|
5858
5897
|
const kernel = ctx.getKernel();
|
|
5859
5898
|
const enforceMembership = config.enforceProjectMembership ?? (config.scoping?.enableProjectScoping ?? false);
|
|
5899
|
+
const requireAuth = config.requireAuth ?? config.scoping?.requireAuth ?? false;
|
|
5860
5900
|
const dispatcher = new HttpDispatcher(kernel, void 0, {
|
|
5861
|
-
enforceProjectMembership: enforceMembership
|
|
5901
|
+
enforceProjectMembership: enforceMembership,
|
|
5902
|
+
requireAuth
|
|
5862
5903
|
});
|
|
5863
5904
|
const prefix = config.prefix || "/api/v1";
|
|
5864
5905
|
const securityHeaders = config.securityHeaders === false ? void 0 : buildSecurityHeaders(
|
|
@@ -6368,11 +6409,11 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6368
6409
|
const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
|
|
6369
6410
|
let count = 0;
|
|
6370
6411
|
if (enableProjectScoping && projectResolution === "required") {
|
|
6371
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6412
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
|
|
6372
6413
|
} else {
|
|
6373
|
-
if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
|
|
6414
|
+
if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser, requireAuth)) count++;
|
|
6374
6415
|
if (enableProjectScoping) {
|
|
6375
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6416
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
|
|
6376
6417
|
}
|
|
6377
6418
|
}
|
|
6378
6419
|
return count;
|