@objectstack/runtime 13.0.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 +197 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -1
- package/dist/index.d.ts +55 -1
- package/dist/index.js +193 -17
- 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
|
|
@@ -1065,6 +1092,18 @@ declare class HttpDispatcher {
|
|
|
1065
1092
|
* OAuth 2.1 TLS rule), the dispatcher only relays it. Never throws.
|
|
1066
1093
|
*/
|
|
1067
1094
|
private getMcpResourceMetadataUrl;
|
|
1095
|
+
/**
|
|
1096
|
+
* `GET /mcp/skill` — the environment-customized portable Agent Skill
|
|
1097
|
+
* (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
|
|
1098
|
+
* generic skill; only the connection URL is environment-specific).
|
|
1099
|
+
*
|
|
1100
|
+
* Served PUBLIC like `/discovery`: the content is generic agent
|
|
1101
|
+
* instructions plus a URL the caller already knows — no schema, no
|
|
1102
|
+
* tenant data. Gated on the same default-on switch as the `/mcp` route
|
|
1103
|
+
* (404 when opted out, so the surface isn't advertised) and 501 when the
|
|
1104
|
+
* MCP plugin isn't loaded, mirroring `handleMcp`.
|
|
1105
|
+
*/
|
|
1106
|
+
handleMcpSkill(method: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1068
1107
|
/**
|
|
1069
1108
|
* Normalise the inbound request into a Web-standard `Request` for the MCP
|
|
1070
1109
|
* transport. Accepts an already-Web `Request`, or a node/Hono-style req
|
|
@@ -1486,7 +1525,7 @@ declare class HttpDispatcher {
|
|
|
1486
1525
|
* Handles Analytics requests
|
|
1487
1526
|
* path: sub-path after /analytics/
|
|
1488
1527
|
*/
|
|
1489
|
-
handleAnalytics(path: string, method: string, body: any,
|
|
1528
|
+
handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1490
1529
|
/**
|
|
1491
1530
|
* Handles in-app notification requests (ADR-0030) — the
|
|
1492
1531
|
* `/api/v1/notifications` surface backed by the messaging service's inbox
|
|
@@ -1501,6 +1540,21 @@ declare class HttpDispatcher {
|
|
|
1501
1540
|
* POST /read/all → markAllRead
|
|
1502
1541
|
*/
|
|
1503
1542
|
handleNotification(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1543
|
+
/**
|
|
1544
|
+
* Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
|
|
1545
|
+
* suggested audience bindings. A package's `isDefault: true` permission
|
|
1546
|
+
* set is an install-time SUGGESTION to bind it to the `everyone` position;
|
|
1547
|
+
* these routes let an admin see and resolve those suggestions. The
|
|
1548
|
+
* `security` service does the real gating (tenant-admin pre-check, and the
|
|
1549
|
+
* confirm write runs under the audience-anchor + delegated-admin gates
|
|
1550
|
+
* with the caller's execution context — never auto-bound, never system).
|
|
1551
|
+
*
|
|
1552
|
+
* Routes:
|
|
1553
|
+
* GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
|
|
1554
|
+
* POST /security/suggested-bindings/:id/confirm → create the anchor binding
|
|
1555
|
+
* POST /security/suggested-bindings/:id/dismiss → decline the suggestion
|
|
1556
|
+
*/
|
|
1557
|
+
handleSecurity(path: string, method: string, _body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1504
1558
|
/**
|
|
1505
1559
|
* Handles i18n requests
|
|
1506
1560
|
* path: sub-path after /i18n/
|
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
|
|
@@ -1065,6 +1092,18 @@ declare class HttpDispatcher {
|
|
|
1065
1092
|
* OAuth 2.1 TLS rule), the dispatcher only relays it. Never throws.
|
|
1066
1093
|
*/
|
|
1067
1094
|
private getMcpResourceMetadataUrl;
|
|
1095
|
+
/**
|
|
1096
|
+
* `GET /mcp/skill` — the environment-customized portable Agent Skill
|
|
1097
|
+
* (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
|
|
1098
|
+
* generic skill; only the connection URL is environment-specific).
|
|
1099
|
+
*
|
|
1100
|
+
* Served PUBLIC like `/discovery`: the content is generic agent
|
|
1101
|
+
* instructions plus a URL the caller already knows — no schema, no
|
|
1102
|
+
* tenant data. Gated on the same default-on switch as the `/mcp` route
|
|
1103
|
+
* (404 when opted out, so the surface isn't advertised) and 501 when the
|
|
1104
|
+
* MCP plugin isn't loaded, mirroring `handleMcp`.
|
|
1105
|
+
*/
|
|
1106
|
+
handleMcpSkill(method: string, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1068
1107
|
/**
|
|
1069
1108
|
* Normalise the inbound request into a Web-standard `Request` for the MCP
|
|
1070
1109
|
* transport. Accepts an already-Web `Request`, or a node/Hono-style req
|
|
@@ -1486,7 +1525,7 @@ declare class HttpDispatcher {
|
|
|
1486
1525
|
* Handles Analytics requests
|
|
1487
1526
|
* path: sub-path after /analytics/
|
|
1488
1527
|
*/
|
|
1489
|
-
handleAnalytics(path: string, method: string, body: any,
|
|
1528
|
+
handleAnalytics(path: string, method: string, body: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1490
1529
|
/**
|
|
1491
1530
|
* Handles in-app notification requests (ADR-0030) — the
|
|
1492
1531
|
* `/api/v1/notifications` surface backed by the messaging service's inbox
|
|
@@ -1501,6 +1540,21 @@ declare class HttpDispatcher {
|
|
|
1501
1540
|
* POST /read/all → markAllRead
|
|
1502
1541
|
*/
|
|
1503
1542
|
handleNotification(path: string, method: string, body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1543
|
+
/**
|
|
1544
|
+
* Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
|
|
1545
|
+
* suggested audience bindings. A package's `isDefault: true` permission
|
|
1546
|
+
* set is an install-time SUGGESTION to bind it to the `everyone` position;
|
|
1547
|
+
* these routes let an admin see and resolve those suggestions. The
|
|
1548
|
+
* `security` service does the real gating (tenant-admin pre-check, and the
|
|
1549
|
+
* confirm write runs under the audience-anchor + delegated-admin gates
|
|
1550
|
+
* with the caller's execution context — never auto-bound, never system).
|
|
1551
|
+
*
|
|
1552
|
+
* Routes:
|
|
1553
|
+
* GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
|
|
1554
|
+
* POST /security/suggested-bindings/:id/confirm → create the anchor binding
|
|
1555
|
+
* POST /security/suggested-bindings/:id/dismiss → decline the suggestion
|
|
1556
|
+
*/
|
|
1557
|
+
handleSecurity(path: string, method: string, _body: any, query: any, context: HttpProtocolContext): Promise<HttpDispatcherResult>;
|
|
1504
1558
|
/**
|
|
1505
1559
|
* Handles i18n requests
|
|
1506
1560
|
* path: sub-path after /i18n/
|
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
|
}
|
|
@@ -2557,6 +2567,73 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
2557
2567
|
return null;
|
|
2558
2568
|
}
|
|
2559
2569
|
}
|
|
2570
|
+
/**
|
|
2571
|
+
* `GET /mcp/skill` — the environment-customized portable Agent Skill
|
|
2572
|
+
* (`SKILL.md`), rendered by the MCP service (ADR-0036 Amendment C: ONE
|
|
2573
|
+
* generic skill; only the connection URL is environment-specific).
|
|
2574
|
+
*
|
|
2575
|
+
* Served PUBLIC like `/discovery`: the content is generic agent
|
|
2576
|
+
* instructions plus a URL the caller already knows — no schema, no
|
|
2577
|
+
* tenant data. Gated on the same default-on switch as the `/mcp` route
|
|
2578
|
+
* (404 when opted out, so the surface isn't advertised) and 501 when the
|
|
2579
|
+
* MCP plugin isn't loaded, mirroring `handleMcp`.
|
|
2580
|
+
*/
|
|
2581
|
+
async handleMcpSkill(method, context) {
|
|
2582
|
+
if (!_HttpDispatcher.isMcpEnabled()) {
|
|
2583
|
+
return { handled: true, response: this.error("MCP server is not enabled for this environment", 404) };
|
|
2584
|
+
}
|
|
2585
|
+
if (method !== "GET") {
|
|
2586
|
+
return {
|
|
2587
|
+
handled: true,
|
|
2588
|
+
response: {
|
|
2589
|
+
status: 405,
|
|
2590
|
+
headers: { Allow: "GET" },
|
|
2591
|
+
body: { success: false, error: { message: "Method not allowed \u2014 use GET", code: 405 } }
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
}
|
|
2595
|
+
const mcp = await this.resolveService("mcp", context.environmentId);
|
|
2596
|
+
if (!mcp || typeof mcp.renderSkill !== "function") {
|
|
2597
|
+
return { handled: true, response: this.error("MCP server is not available", 501) };
|
|
2598
|
+
}
|
|
2599
|
+
let mcpUrl;
|
|
2600
|
+
try {
|
|
2601
|
+
const authService = await this.resolveService("auth", context.environmentId);
|
|
2602
|
+
const url = authService?.getMcpResourceUrl?.();
|
|
2603
|
+
if (typeof url === "string" && url) mcpUrl = url;
|
|
2604
|
+
} catch {
|
|
2605
|
+
}
|
|
2606
|
+
if (!mcpUrl) {
|
|
2607
|
+
try {
|
|
2608
|
+
const webReq = this.toMcpWebRequest(context.request, void 0);
|
|
2609
|
+
const host = webReq?.headers.get("host");
|
|
2610
|
+
if (host) {
|
|
2611
|
+
const proto = webReq?.headers.get("x-forwarded-proto") || "http";
|
|
2612
|
+
mcpUrl = `${proto}://${host}/api/v1/mcp`;
|
|
2613
|
+
}
|
|
2614
|
+
} catch {
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
const markdown = mcp.renderSkill({ mcpUrl });
|
|
2618
|
+
return {
|
|
2619
|
+
handled: true,
|
|
2620
|
+
result: {
|
|
2621
|
+
type: "stream",
|
|
2622
|
+
status: 200,
|
|
2623
|
+
contentType: "text/markdown; charset=utf-8",
|
|
2624
|
+
headers: {
|
|
2625
|
+
"content-type": "text/markdown; charset=utf-8",
|
|
2626
|
+
"content-disposition": 'inline; filename="SKILL.md"',
|
|
2627
|
+
// Same reasoning as /discovery (cloud#152): reflects mutable
|
|
2628
|
+
// runtime config (base URL), must never be edge-cached stale.
|
|
2629
|
+
"cache-control": "no-store"
|
|
2630
|
+
},
|
|
2631
|
+
events: (async function* () {
|
|
2632
|
+
yield markdown;
|
|
2633
|
+
})()
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2560
2637
|
/**
|
|
2561
2638
|
* Normalise the inbound request into a Web-standard `Request` for the MCP
|
|
2562
2639
|
* transport. Accepts an already-Web `Request`, or a node/Hono-style req
|
|
@@ -3364,6 +3441,15 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3364
3441
|
* Fallback for backward compat: /metadata (all objects), /metadata/:objectName (get object)
|
|
3365
3442
|
*/
|
|
3366
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
|
+
}
|
|
3367
3453
|
const parts = path.replace(/^\/+/, "").split("/").filter(Boolean);
|
|
3368
3454
|
if (parts[0] === "types") {
|
|
3369
3455
|
const protocol = await this.resolveService("protocol");
|
|
@@ -3663,13 +3749,13 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3663
3749
|
* Handles Analytics requests
|
|
3664
3750
|
* path: sub-path after /analytics/
|
|
3665
3751
|
*/
|
|
3666
|
-
async handleAnalytics(path, method, body,
|
|
3752
|
+
async handleAnalytics(path, method, body, context) {
|
|
3667
3753
|
const analyticsService = await this.getService(CoreServiceName.enum.analytics);
|
|
3668
3754
|
if (!analyticsService) return { handled: false };
|
|
3669
3755
|
const m = method.toUpperCase();
|
|
3670
3756
|
const subPath = path.replace(/^\/+/, "");
|
|
3671
3757
|
if (subPath === "query" && m === "POST") {
|
|
3672
|
-
const result = await analyticsService.query(body);
|
|
3758
|
+
const result = await analyticsService.query(body, context?.executionContext);
|
|
3673
3759
|
return { handled: true, response: this.success(result) };
|
|
3674
3760
|
}
|
|
3675
3761
|
if (subPath === "meta" && m === "GET") {
|
|
@@ -3677,7 +3763,7 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3677
3763
|
return { handled: true, response: this.success(result) };
|
|
3678
3764
|
}
|
|
3679
3765
|
if (subPath === "sql" && m === "POST") {
|
|
3680
|
-
const result = await analyticsService.generateSql(body);
|
|
3766
|
+
const result = await analyticsService.generateSql(body, context?.executionContext);
|
|
3681
3767
|
return { handled: true, response: this.success(result) };
|
|
3682
3768
|
}
|
|
3683
3769
|
return { handled: false };
|
|
@@ -3722,6 +3808,56 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
3722
3808
|
}
|
|
3723
3809
|
return { handled: false };
|
|
3724
3810
|
}
|
|
3811
|
+
/**
|
|
3812
|
+
* Handle the security admin surface (`/security/...`) — ADR-0090 D5/D9
|
|
3813
|
+
* suggested audience bindings. A package's `isDefault: true` permission
|
|
3814
|
+
* set is an install-time SUGGESTION to bind it to the `everyone` position;
|
|
3815
|
+
* these routes let an admin see and resolve those suggestions. The
|
|
3816
|
+
* `security` service does the real gating (tenant-admin pre-check, and the
|
|
3817
|
+
* confirm write runs under the audience-anchor + delegated-admin gates
|
|
3818
|
+
* with the caller's execution context — never auto-bound, never system).
|
|
3819
|
+
*
|
|
3820
|
+
* Routes:
|
|
3821
|
+
* GET /security/suggested-bindings?status=&packageId= → list (reconciles first)
|
|
3822
|
+
* POST /security/suggested-bindings/:id/confirm → create the anchor binding
|
|
3823
|
+
* POST /security/suggested-bindings/:id/dismiss → decline the suggestion
|
|
3824
|
+
*/
|
|
3825
|
+
async handleSecurity(path, method, _body, query, context) {
|
|
3826
|
+
const service = await this.resolveService("security", context.environmentId);
|
|
3827
|
+
if (!service || typeof service.listAudienceBindingSuggestions !== "function") {
|
|
3828
|
+
return { handled: true, response: this.error("Security service not available", 503) };
|
|
3829
|
+
}
|
|
3830
|
+
const ec = context.executionContext;
|
|
3831
|
+
if (!ec?.userId && !ec?.isSystem) {
|
|
3832
|
+
return { handled: true, response: this.error("Authentication required", 401) };
|
|
3833
|
+
}
|
|
3834
|
+
const m = method.toUpperCase();
|
|
3835
|
+
const parts = path.split("/").filter(Boolean);
|
|
3836
|
+
if (parts[0] !== "suggested-bindings") return { handled: false };
|
|
3837
|
+
try {
|
|
3838
|
+
if (parts.length === 1 && m === "GET") {
|
|
3839
|
+
const status = query?.status ? String(query.status) : void 0;
|
|
3840
|
+
const packageId = query?.packageId ? String(query.packageId) : void 0;
|
|
3841
|
+
const result = await service.listAudienceBindingSuggestions(ec, { status, packageId });
|
|
3842
|
+
return { handled: true, response: this.success(result) };
|
|
3843
|
+
}
|
|
3844
|
+
if (parts.length === 3 && m === "POST") {
|
|
3845
|
+
const id = decodeURIComponent(parts[1]);
|
|
3846
|
+
if (parts[2] === "confirm") {
|
|
3847
|
+
const result = await service.confirmAudienceBindingSuggestion(ec, id);
|
|
3848
|
+
return { handled: true, response: this.success(result) };
|
|
3849
|
+
}
|
|
3850
|
+
if (parts[2] === "dismiss") {
|
|
3851
|
+
const result = await service.dismissAudienceBindingSuggestion(ec, id);
|
|
3852
|
+
return { handled: true, response: this.success(result) };
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
return { handled: false };
|
|
3856
|
+
} catch (err) {
|
|
3857
|
+
const status = typeof err?.statusCode === "number" ? err.statusCode : 500;
|
|
3858
|
+
return { handled: true, response: this.error(err?.message ?? "Security operation failed", status) };
|
|
3859
|
+
}
|
|
3860
|
+
}
|
|
3725
3861
|
/**
|
|
3726
3862
|
* Handles i18n requests
|
|
3727
3863
|
* path: sub-path after /i18n/
|
|
@@ -4727,8 +4863,16 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4727
4863
|
return Array.isArray(rows) ? rows : rows?.value ?? [];
|
|
4728
4864
|
}
|
|
4729
4865
|
};
|
|
4730
|
-
const
|
|
4731
|
-
const userFromAuth =
|
|
4866
|
+
const ec = _context?.executionContext;
|
|
4867
|
+
const userFromAuth = ec?.userId ? {
|
|
4868
|
+
id: ec.userId,
|
|
4869
|
+
name: ec.userId,
|
|
4870
|
+
email: ec.email,
|
|
4871
|
+
roles: Array.isArray(ec.positions) ? ec.positions : [],
|
|
4872
|
+
positions: Array.isArray(ec.positions) ? ec.positions : [],
|
|
4873
|
+
permissions: Array.isArray(ec.permissions) ? ec.permissions : [],
|
|
4874
|
+
tenantId: ec.tenantId
|
|
4875
|
+
} : { id: "system", name: "system", roles: [], positions: [], permissions: [] };
|
|
4732
4876
|
const actionContext = {
|
|
4733
4877
|
record,
|
|
4734
4878
|
user: userFromAuth,
|
|
@@ -4807,6 +4951,15 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
4807
4951
|
if (route.method !== method) continue;
|
|
4808
4952
|
const params = matchRoute(route.path, fullPath);
|
|
4809
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
|
+
}
|
|
4810
4963
|
const ec = context.executionContext;
|
|
4811
4964
|
const user = ec?.userId ? {
|
|
4812
4965
|
userId: ec.userId,
|
|
@@ -5110,6 +5263,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5110
5263
|
if (cleanPath.startsWith("/data")) {
|
|
5111
5264
|
return this.handleData(cleanPath.substring(5), method, body, query, context);
|
|
5112
5265
|
}
|
|
5266
|
+
if (cleanPath === "/mcp/skill" || cleanPath.startsWith("/mcp/skill?")) {
|
|
5267
|
+
return this.handleMcpSkill(method, context);
|
|
5268
|
+
}
|
|
5113
5269
|
if (cleanPath === "/mcp" || cleanPath.startsWith("/mcp/") || cleanPath.startsWith("/mcp?")) {
|
|
5114
5270
|
return this.handleMcp(body, context);
|
|
5115
5271
|
}
|
|
@@ -5137,6 +5293,9 @@ var _HttpDispatcher = class _HttpDispatcher {
|
|
|
5137
5293
|
if (cleanPath.startsWith("/notifications")) {
|
|
5138
5294
|
return this.handleNotification(cleanPath.substring(14), method, body, query, context);
|
|
5139
5295
|
}
|
|
5296
|
+
if (cleanPath === "/security" || cleanPath.startsWith("/security/")) {
|
|
5297
|
+
return this.handleSecurity(cleanPath.substring(9), method, body, query, context);
|
|
5298
|
+
}
|
|
5140
5299
|
if (cleanPath.startsWith("/packages")) {
|
|
5141
5300
|
return this.handlePackages(cleanPath.substring(9), method, body, query, context);
|
|
5142
5301
|
}
|
|
@@ -5533,7 +5692,7 @@ function resolveErrorReporter(ctx, override) {
|
|
|
5533
5692
|
}
|
|
5534
5693
|
|
|
5535
5694
|
// src/dispatcher-plugin.ts
|
|
5536
|
-
function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser) {
|
|
5695
|
+
function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUser, requireAuth = false) {
|
|
5537
5696
|
const handler = async (req, res) => {
|
|
5538
5697
|
try {
|
|
5539
5698
|
let user;
|
|
@@ -5543,6 +5702,17 @@ function mountRouteOnServer(route, server, routePath, securityHeaders, resolveUs
|
|
|
5543
5702
|
} catch {
|
|
5544
5703
|
}
|
|
5545
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
|
+
}
|
|
5546
5716
|
const result = await route.handler({
|
|
5547
5717
|
body: req.body,
|
|
5548
5718
|
params: req.params,
|
|
@@ -5726,8 +5896,10 @@ function createDispatcherPlugin(config = {}) {
|
|
|
5726
5896
|
if (!server) return;
|
|
5727
5897
|
const kernel = ctx.getKernel();
|
|
5728
5898
|
const enforceMembership = config.enforceProjectMembership ?? (config.scoping?.enableProjectScoping ?? false);
|
|
5899
|
+
const requireAuth = config.requireAuth ?? config.scoping?.requireAuth ?? false;
|
|
5729
5900
|
const dispatcher = new HttpDispatcher(kernel, void 0, {
|
|
5730
|
-
enforceProjectMembership: enforceMembership
|
|
5901
|
+
enforceProjectMembership: enforceMembership,
|
|
5902
|
+
requireAuth
|
|
5731
5903
|
});
|
|
5732
5904
|
const prefix = config.prefix || "/api/v1";
|
|
5733
5905
|
const securityHeaders = config.securityHeaders === false ? void 0 : buildSecurityHeaders(
|
|
@@ -5855,6 +6027,14 @@ function createDispatcherPlugin(config = {}) {
|
|
|
5855
6027
|
mountMcp("POST");
|
|
5856
6028
|
mountMcp("GET");
|
|
5857
6029
|
mountMcp("DELETE");
|
|
6030
|
+
server.get(`${prefix}/mcp/skill`, async (req, res) => {
|
|
6031
|
+
try {
|
|
6032
|
+
const result = await dispatcher.dispatch("GET", "/mcp/skill", req.body, req.query, { request: req });
|
|
6033
|
+
sendResult(result, res);
|
|
6034
|
+
} catch (err) {
|
|
6035
|
+
errorResponse(err, res);
|
|
6036
|
+
}
|
|
6037
|
+
});
|
|
5858
6038
|
server.post(`${prefix}/keys`, async (req, res) => {
|
|
5859
6039
|
try {
|
|
5860
6040
|
const result = await dispatcher.dispatch("POST", "/keys", req.body, req.query, { request: req });
|
|
@@ -6159,9 +6339,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6159
6339
|
const registerActionRoutes = (base) => {
|
|
6160
6340
|
server.post(`${base}/actions/:object/:action`, async (req, res) => {
|
|
6161
6341
|
try {
|
|
6162
|
-
const
|
|
6163
|
-
if (req.params?.environmentId) ctx2.environmentId = req.params.environmentId;
|
|
6164
|
-
const result = await dispatcher.handleActions(`/${req.params.object}/${req.params.action}`, "POST", req.body, ctx2);
|
|
6342
|
+
const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}`, req.body, req.query, { request: req });
|
|
6165
6343
|
sendResult(result, res);
|
|
6166
6344
|
} catch (err) {
|
|
6167
6345
|
errorResponse(err, res);
|
|
@@ -6169,9 +6347,7 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6169
6347
|
});
|
|
6170
6348
|
server.post(`${base}/actions/:object/:action/:recordId`, async (req, res) => {
|
|
6171
6349
|
try {
|
|
6172
|
-
const
|
|
6173
|
-
if (req.params?.environmentId) ctx2.environmentId = req.params.environmentId;
|
|
6174
|
-
const result = await dispatcher.handleActions(`/${req.params.object}/${req.params.action}/${req.params.recordId}`, "POST", req.body, ctx2);
|
|
6350
|
+
const result = await dispatcher.dispatch("POST", `/actions/${req.params.object}/${req.params.action}/${req.params.recordId}`, req.body, req.query, { request: req });
|
|
6175
6351
|
sendResult(result, res);
|
|
6176
6352
|
} catch (err) {
|
|
6177
6353
|
errorResponse(err, res);
|
|
@@ -6233,11 +6409,11 @@ function createDispatcherPlugin(config = {}) {
|
|
|
6233
6409
|
const routePath = route.path.startsWith("/api/v1") ? route.path : `${prefix}${route.path}`;
|
|
6234
6410
|
let count = 0;
|
|
6235
6411
|
if (enableProjectScoping && projectResolution === "required") {
|
|
6236
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6412
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
|
|
6237
6413
|
} else {
|
|
6238
|
-
if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser)) count++;
|
|
6414
|
+
if (mountRouteOnServer(route, server, routePath, securityHeaders, resolveRequestUser, requireAuth)) count++;
|
|
6239
6415
|
if (enableProjectScoping) {
|
|
6240
|
-
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser)) count++;
|
|
6416
|
+
if (mountRouteOnServer(route, server, toScopedPath(routePath), securityHeaders, resolveRequestUser, requireAuth)) count++;
|
|
6241
6417
|
}
|
|
6242
6418
|
}
|
|
6243
6419
|
return count;
|