@opengeni/core 2.8.3 → 2.9.1-canary.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.
Files changed (50) hide show
  1. package/dist/access/external-actor-authority.d.ts +47 -0
  2. package/dist/access/index.d.ts +15 -1
  3. package/dist/application/connect-authority.d.ts +13 -0
  4. package/dist/application/connect-operation.d.ts +28 -0
  5. package/dist/application/external-continuation.d.ts +15 -0
  6. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  7. package/dist/application/external-link-work-admission.d.ts +12 -0
  8. package/dist/application/external-workspace-members.d.ts +6 -0
  9. package/dist/application/host-mcp-owner.d.ts +6 -0
  10. package/dist/application/new-session-drafts.d.ts +2 -1
  11. package/dist/application/session-tenancy.d.ts +1 -0
  12. package/dist/dependencies.d.ts +2 -0
  13. package/dist/domain/capabilities.d.ts +19 -2
  14. package/dist/domain/external-creation-attribution.d.ts +6 -0
  15. package/dist/domain/host-mcp-task-admission.d.ts +18 -0
  16. package/dist/domain/product-integration-pack.d.ts +3 -9
  17. package/dist/domain/product-integration-skill.gen.d.ts +5 -0
  18. package/dist/domain/scheduled-tasks.d.ts +3 -0
  19. package/dist/domain/sessions.d.ts +15 -4
  20. package/dist/index.d.ts +7 -0
  21. package/dist/index.js +1231 -825
  22. package/dist/index.js.map +1 -1
  23. package/dist/remote-mcp-credentials.d.ts +8 -0
  24. package/dist/remote-mcp-credentials.js +219 -0
  25. package/dist/remote-mcp-credentials.js.map +1 -0
  26. package/dist/session-authorization.d.ts +5 -6
  27. package/package.json +17 -13
  28. package/src/access/external-actor-authority.ts +94 -0
  29. package/src/access/index.ts +255 -1
  30. package/src/application/connect-authority.ts +77 -0
  31. package/src/application/connect-operation.ts +51 -0
  32. package/src/application/external-continuation.ts +112 -0
  33. package/src/application/external-identity-lifecycle.ts +48 -0
  34. package/src/application/external-link-work-admission.ts +87 -0
  35. package/src/application/external-workspace-members.ts +95 -0
  36. package/src/application/host-mcp-owner.ts +41 -0
  37. package/src/application/new-session-drafts.ts +9 -2
  38. package/src/application/session-tenancy.ts +24 -3
  39. package/src/application/user-resource-grants.ts +2 -2
  40. package/src/dependencies.ts +2 -0
  41. package/src/domain/capabilities.ts +20 -4
  42. package/src/domain/external-creation-attribution.ts +22 -0
  43. package/src/domain/host-mcp-task-admission.ts +111 -0
  44. package/src/domain/product-integration-pack.ts +11 -464
  45. package/src/domain/product-integration-skill.gen.ts +52 -0
  46. package/src/domain/scheduled-tasks.ts +66 -5
  47. package/src/domain/sessions.ts +253 -23
  48. package/src/index.ts +7 -0
  49. package/src/remote-mcp-credentials.ts +293 -0
  50. package/src/session-authorization.ts +22 -17
@@ -0,0 +1,8 @@
1
+ import type { Settings } from "@opengeni/config";
2
+ import { type ConnectionCredentialsPort } from "@opengeni/contracts";
3
+ import { pinnedFetch } from "@opengeni/network";
4
+ /** Optional standalone transport for the existing request-time host port.
5
+ * No successful credential is cached or persisted. The host must authorize
6
+ * every immutable request context, including revocation and binding generation.
7
+ */
8
+ export declare function createRemoteMcpCredentialsPort(settings: Pick<Settings, "hostMcpCredentialResolversJson" | "environment" | "integrationsAllowPrivateNetworkTargets">, transport?: typeof pinnedFetch): ConnectionCredentialsPort;
@@ -0,0 +1,219 @@
1
+ // src/remote-mcp-credentials.ts
2
+ import {
3
+ McpConnectionResourceScope
4
+ } from "@opengeni/contracts";
5
+ import { pinnedFetch, readResponseJsonBounded, validateHttpUrl } from "@opengeni/network";
6
+ import { z } from "zod";
7
+ var configuration = z.array(
8
+ z.object({
9
+ accountId: z.string().uuid(),
10
+ url: z.string().max(2048),
11
+ bearerToken: z.string().min(1).max(8192).regex(/^[^\r\n]+$/),
12
+ timeoutMs: z.number().int().min(100).max(3e4).default(1e4)
13
+ }).strict()
14
+ ).max(128);
15
+ var scope = {
16
+ accountId: z.string(),
17
+ workspaceId: z.string(),
18
+ sessionId: z.string(),
19
+ providerDomain: z.string(),
20
+ provider: z.string().optional(),
21
+ scopes: z.array(z.string()).max(256).optional(),
22
+ resource: z.string().optional(),
23
+ selectedResources: z.array(McpConnectionResourceScope).max(256).optional()
24
+ };
25
+ var resolution = z.discriminatedUnion("status", [
26
+ z.object({
27
+ ...scope,
28
+ status: z.literal("ok"),
29
+ connectionId: z.string().min(1),
30
+ headers: z.record(z.string(), z.string()),
31
+ placements: z.array(
32
+ z.object({
33
+ carrier: z.enum(["header", "query", "cookie"]),
34
+ name: z.string(),
35
+ value: z.string(),
36
+ prefix: z.string().optional()
37
+ }).strict()
38
+ ).max(64).optional(),
39
+ expiresAt: z.string().datetime({ offset: true })
40
+ }).strict(),
41
+ z.object({
42
+ ...scope,
43
+ status: z.literal("auth_needed"),
44
+ connectionId: z.string().optional(),
45
+ reason: z.enum([
46
+ "missing_connection",
47
+ "expired",
48
+ "insufficient_scope",
49
+ "refresh_failed",
50
+ "personal_authority_unavailable",
51
+ "unsupported_auth",
52
+ "resource_scope_unavailable"
53
+ ]),
54
+ authorizationUrl: z.string().optional()
55
+ }).strict()
56
+ ]);
57
+ var gatewayResolution = z.discriminatedUnion("status", [
58
+ resolution.options[0].omit({ sessionId: true }).extend({ requestId: z.string().uuid() }),
59
+ resolution.options[1].omit({ sessionId: true }).extend({ requestId: z.string().uuid() })
60
+ ]);
61
+ var responseEnvelope = z.object({
62
+ version: z.literal(1),
63
+ requestId: z.string().uuid(),
64
+ destinationUrl: z.string(),
65
+ resolution: z.union([resolution, gatewayResolution])
66
+ }).strict();
67
+ function normalizeResolution(value) {
68
+ const common = {
69
+ accountId: value.accountId,
70
+ workspaceId: value.workspaceId,
71
+ ..."sessionId" in value ? { sessionId: value.sessionId } : { requestId: value.requestId },
72
+ providerDomain: value.providerDomain,
73
+ ...value.provider !== void 0 ? { provider: value.provider } : {},
74
+ ...value.scopes !== void 0 ? { scopes: value.scopes } : {},
75
+ ...value.resource !== void 0 ? { resource: value.resource } : {},
76
+ ...value.selectedResources !== void 0 ? { selectedResources: value.selectedResources } : {}
77
+ };
78
+ if (value.status === "auth_needed")
79
+ return {
80
+ ...common,
81
+ status: "auth_needed",
82
+ reason: value.reason,
83
+ ...value.connectionId !== void 0 ? { connectionId: value.connectionId } : {},
84
+ ...value.authorizationUrl !== void 0 ? { authorizationUrl: value.authorizationUrl } : {}
85
+ };
86
+ return {
87
+ ...common,
88
+ status: "ok",
89
+ connectionId: value.connectionId,
90
+ headers: value.headers,
91
+ expiresAt: value.expiresAt,
92
+ ...value.placements !== void 0 ? {
93
+ placements: value.placements.map((placement) => ({
94
+ carrier: placement.carrier,
95
+ name: placement.name,
96
+ value: placement.value,
97
+ ...placement.prefix !== void 0 ? { prefix: placement.prefix } : {}
98
+ }))
99
+ } : {}
100
+ };
101
+ }
102
+ function createRemoteMcpCredentialsPort(settings, transport = pinnedFetch) {
103
+ if (!settings.hostMcpCredentialResolversJson) return {};
104
+ let entries;
105
+ try {
106
+ entries = configuration.parse(JSON.parse(settings.hostMcpCredentialResolversJson));
107
+ for (const entry of entries) validateHttpUrl(entry.url);
108
+ if (new Set(entries.map((entry) => entry.accountId)).size !== entries.length) throw new Error();
109
+ } catch {
110
+ throw new Error("Invalid host MCP credential resolver configuration");
111
+ }
112
+ const byAccount = new Map(entries.map((entry) => [entry.accountId, entry]));
113
+ const active = /* @__PURE__ */ new Map();
114
+ let physicalRequests = 0;
115
+ const resolveRemote = async (input) => {
116
+ const serialized = JSON.stringify(input);
117
+ const request = JSON.parse(serialized);
118
+ const unavailable = (reason) => ({
119
+ status: "auth_needed",
120
+ accountId: request.accountId,
121
+ workspaceId: request.workspaceId,
122
+ ..."sessionId" in request ? { sessionId: request.sessionId } : { requestId: request.requestId },
123
+ providerDomain: request.connectionRef.providerDomain,
124
+ ...request.connectionRef.provider !== void 0 ? { provider: request.connectionRef.provider } : {},
125
+ ...request.connectionRef.connectionId !== void 0 ? { connectionId: request.connectionRef.connectionId } : {},
126
+ ...request.connectionRef.scopes !== void 0 ? { scopes: request.connectionRef.scopes } : {},
127
+ ...request.connectionRef.resource !== void 0 ? { resource: request.connectionRef.resource } : {},
128
+ ...request.connectionRef.selectedResources !== void 0 ? { selectedResources: request.connectionRef.selectedResources } : {},
129
+ reason
130
+ });
131
+ const config = byAccount.get(request.accountId);
132
+ if (!config || request.connectionRef.authoritySource !== "host")
133
+ return unavailable("unsupported_auth");
134
+ if (new TextEncoder().encode(serialized).byteLength > 65536)
135
+ return unavailable("refresh_failed");
136
+ const pending = active.get(serialized);
137
+ if (pending) return structuredClone(await pending);
138
+ if (physicalRequests >= 128) return unavailable("refresh_failed");
139
+ const run = async () => {
140
+ const requestId = crypto.randomUUID();
141
+ const controller = new AbortController();
142
+ let timer;
143
+ const deadline = new Promise((_, reject) => {
144
+ timer = setTimeout(() => {
145
+ controller.abort();
146
+ reject(new Error("Host resolver deadline"));
147
+ }, config.timeoutMs);
148
+ });
149
+ try {
150
+ physicalRequests++;
151
+ return await Promise.race([
152
+ deadline,
153
+ (async () => {
154
+ const response = await transport(
155
+ config.url,
156
+ {
157
+ method: "POST",
158
+ redirect: "manual",
159
+ signal: controller.signal,
160
+ headers: {
161
+ Authorization: `Bearer ${config.bearerToken}`,
162
+ "Content-Type": "application/json"
163
+ },
164
+ body: JSON.stringify({ version: 1, requestId, request })
165
+ },
166
+ settings,
167
+ { requireHttpsOutsideLocalTest: true, label: "Host MCP credential resolver" }
168
+ );
169
+ if (!response.ok) {
170
+ await response.body?.cancel();
171
+ return unavailable("refresh_failed");
172
+ }
173
+ const parsed = responseEnvelope.parse(
174
+ await readResponseJsonBounded(response, 65536, "Host MCP credential resolver", {
175
+ signal: controller.signal
176
+ })
177
+ );
178
+ if (parsed.requestId !== requestId || parsed.destinationUrl !== request.destinationUrl || parsed.resolution.accountId !== request.accountId || parsed.resolution.workspaceId !== request.workspaceId || ("sessionId" in request ? !("sessionId" in parsed.resolution) || parsed.resolution.sessionId !== request.sessionId : !("requestId" in parsed.resolution) || parsed.resolution.requestId !== request.requestId))
179
+ return unavailable("refresh_failed");
180
+ if (parsed.resolution.status === "ok" && (Date.parse(parsed.resolution.expiresAt) <= Date.now() + 5e3 || Date.parse(parsed.resolution.expiresAt) > Date.now() + 9e5)) {
181
+ return unavailable("refresh_failed");
182
+ }
183
+ return normalizeResolution(parsed.resolution);
184
+ })().finally(() => {
185
+ physicalRequests--;
186
+ })
187
+ ]);
188
+ } catch {
189
+ return unavailable("refresh_failed");
190
+ } finally {
191
+ clearTimeout(timer);
192
+ }
193
+ };
194
+ const task = run();
195
+ active.set(serialized, task);
196
+ try {
197
+ return structuredClone(await task);
198
+ } finally {
199
+ if (active.get(serialized) === task) active.delete(serialized);
200
+ }
201
+ };
202
+ return {
203
+ mcpAuthoritySource: "host",
204
+ async mcpCredentials(input) {
205
+ const result = await resolveRemote(input);
206
+ if (!("sessionId" in result)) throw new Error("Invalid host turn credential scope");
207
+ return result;
208
+ },
209
+ async mcpGatewayCredentials(input) {
210
+ const result = await resolveRemote(input);
211
+ if (!("requestId" in result)) throw new Error("Invalid host gateway credential scope");
212
+ return result;
213
+ }
214
+ };
215
+ }
216
+ export {
217
+ createRemoteMcpCredentialsPort
218
+ };
219
+ //# sourceMappingURL=remote-mcp-credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/remote-mcp-credentials.ts"],"sourcesContent":["import type { Settings } from \"@opengeni/config\";\nimport {\n McpConnectionResourceScope,\n type ConnectionCredentialsPort,\n type McpCredentialsRequest,\n type McpCredentialResolution,\n type McpGatewayCredentialsRequest,\n type McpGatewayCredentialResolution,\n} from \"@opengeni/contracts\";\nimport { pinnedFetch, readResponseJsonBounded, validateHttpUrl } from \"@opengeni/network\";\nimport { z } from \"zod\";\n\nconst configuration = z\n .array(\n z\n .object({\n accountId: z.string().uuid(),\n url: z.string().max(2048),\n bearerToken: z\n .string()\n .min(1)\n .max(8192)\n .regex(/^[^\\r\\n]+$/),\n timeoutMs: z.number().int().min(100).max(30_000).default(10_000),\n })\n .strict(),\n )\n .max(128);\n\nconst scope = {\n accountId: z.string(),\n workspaceId: z.string(),\n sessionId: z.string(),\n providerDomain: z.string(),\n provider: z.string().optional(),\n scopes: z.array(z.string()).max(256).optional(),\n resource: z.string().optional(),\n selectedResources: z.array(McpConnectionResourceScope).max(256).optional(),\n};\nconst resolution = z.discriminatedUnion(\"status\", [\n z\n .object({\n ...scope,\n status: z.literal(\"ok\"),\n connectionId: z.string().min(1),\n headers: z.record(z.string(), z.string()),\n placements: z\n .array(\n z\n .object({\n carrier: z.enum([\"header\", \"query\", \"cookie\"]),\n name: z.string(),\n value: z.string(),\n prefix: z.string().optional(),\n })\n .strict(),\n )\n .max(64)\n .optional(),\n expiresAt: z.string().datetime({ offset: true }),\n })\n .strict(),\n z\n .object({\n ...scope,\n status: z.literal(\"auth_needed\"),\n connectionId: z.string().optional(),\n reason: z.enum([\n \"missing_connection\",\n \"expired\",\n \"insufficient_scope\",\n \"refresh_failed\",\n \"personal_authority_unavailable\",\n \"unsupported_auth\",\n \"resource_scope_unavailable\",\n ]),\n authorizationUrl: z.string().optional(),\n })\n .strict(),\n]);\nconst gatewayResolution = z.discriminatedUnion(\"status\", [\n resolution.options[0].omit({ sessionId: true }).extend({ requestId: z.string().uuid() }),\n resolution.options[1].omit({ sessionId: true }).extend({ requestId: z.string().uuid() }),\n]);\nconst responseEnvelope = z\n .object({\n version: z.literal(1),\n requestId: z.string().uuid(),\n destinationUrl: z.string(),\n resolution: z.union([resolution, gatewayResolution]),\n })\n .strict();\n\ntype RemoteResolution = McpCredentialResolution | McpGatewayCredentialResolution;\nfunction normalizeResolution(\n value: z.infer<typeof resolution> | z.infer<typeof gatewayResolution>,\n): RemoteResolution {\n const common = {\n accountId: value.accountId,\n workspaceId: value.workspaceId,\n ...(\"sessionId\" in value ? { sessionId: value.sessionId } : { requestId: value.requestId }),\n providerDomain: value.providerDomain,\n ...(value.provider !== undefined ? { provider: value.provider } : {}),\n ...(value.scopes !== undefined ? { scopes: value.scopes } : {}),\n ...(value.resource !== undefined ? { resource: value.resource } : {}),\n ...(value.selectedResources !== undefined\n ? { selectedResources: value.selectedResources }\n : {}),\n };\n if (value.status === \"auth_needed\")\n return {\n ...common,\n status: \"auth_needed\",\n reason: value.reason,\n ...(value.connectionId !== undefined ? { connectionId: value.connectionId } : {}),\n ...(value.authorizationUrl !== undefined ? { authorizationUrl: value.authorizationUrl } : {}),\n };\n return {\n ...common,\n status: \"ok\",\n connectionId: value.connectionId,\n headers: value.headers,\n expiresAt: value.expiresAt,\n ...(value.placements !== undefined\n ? {\n placements: value.placements.map((placement) => ({\n carrier: placement.carrier,\n name: placement.name,\n value: placement.value,\n ...(placement.prefix !== undefined ? { prefix: placement.prefix } : {}),\n })),\n }\n : {}),\n };\n}\n\n/** Optional standalone transport for the existing request-time host port.\n * No successful credential is cached or persisted. The host must authorize\n * every immutable request context, including revocation and binding generation.\n */\nexport function createRemoteMcpCredentialsPort(\n settings: Pick<\n Settings,\n \"hostMcpCredentialResolversJson\" | \"environment\" | \"integrationsAllowPrivateNetworkTargets\"\n >,\n transport: typeof pinnedFetch = pinnedFetch,\n): ConnectionCredentialsPort {\n if (!settings.hostMcpCredentialResolversJson) return {};\n let entries: z.infer<typeof configuration>;\n try {\n entries = configuration.parse(JSON.parse(settings.hostMcpCredentialResolversJson));\n for (const entry of entries) validateHttpUrl(entry.url);\n if (new Set(entries.map((entry) => entry.accountId)).size !== entries.length) throw new Error();\n } catch {\n // Configuration includes credentials: never return parser input or causes.\n throw new Error(\"Invalid host MCP credential resolver configuration\");\n }\n const byAccount = new Map(entries.map((entry) => [entry.accountId, entry]));\n const active = new Map<string, Promise<RemoteResolution>>();\n let physicalRequests = 0;\n const resolveRemote = async (\n input: McpCredentialsRequest | McpGatewayCredentialsRequest,\n ): Promise<RemoteResolution> => {\n const serialized = JSON.stringify(input);\n const request: McpCredentialsRequest | McpGatewayCredentialsRequest = JSON.parse(serialized);\n const unavailable = (reason: \"unsupported_auth\" | \"refresh_failed\"): RemoteResolution => ({\n status: \"auth_needed\",\n accountId: request.accountId,\n workspaceId: request.workspaceId,\n ...(\"sessionId\" in request\n ? { sessionId: request.sessionId }\n : { requestId: request.requestId }),\n providerDomain: request.connectionRef.providerDomain,\n ...(request.connectionRef.provider !== undefined\n ? { provider: request.connectionRef.provider }\n : {}),\n ...(request.connectionRef.connectionId !== undefined\n ? { connectionId: request.connectionRef.connectionId }\n : {}),\n ...(request.connectionRef.scopes !== undefined\n ? { scopes: request.connectionRef.scopes }\n : {}),\n ...(request.connectionRef.resource !== undefined\n ? { resource: request.connectionRef.resource }\n : {}),\n ...(request.connectionRef.selectedResources !== undefined\n ? { selectedResources: request.connectionRef.selectedResources }\n : {}),\n reason,\n });\n const config = byAccount.get(request.accountId);\n if (!config || request.connectionRef.authoritySource !== \"host\")\n return unavailable(\"unsupported_auth\");\n // Snapshot before any asynchronous operation; never retain mutable caller\n // objects as the authority for a delayed response.\n if (new TextEncoder().encode(serialized).byteLength > 65_536)\n return unavailable(\"refresh_failed\");\n const pending = active.get(serialized);\n if (pending) return structuredClone(await pending);\n if (physicalRequests >= 128) return unavailable(\"refresh_failed\");\n const run = async (): Promise<RemoteResolution> => {\n const requestId = crypto.randomUUID();\n const controller = new AbortController();\n let timer: ReturnType<typeof setTimeout> | undefined;\n const deadline = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n controller.abort();\n reject(new Error(\"Host resolver deadline\"));\n }, config.timeoutMs);\n });\n try {\n physicalRequests++;\n return await Promise.race([\n deadline,\n (async () => {\n const response = await transport(\n config.url,\n {\n method: \"POST\",\n redirect: \"manual\",\n signal: controller.signal,\n headers: {\n Authorization: `Bearer ${config.bearerToken}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify({ version: 1, requestId, request }),\n },\n settings,\n { requireHttpsOutsideLocalTest: true, label: \"Host MCP credential resolver\" },\n );\n if (!response.ok) {\n await response.body?.cancel();\n return unavailable(\"refresh_failed\");\n }\n const parsed = responseEnvelope.parse(\n await readResponseJsonBounded(response, 65_536, \"Host MCP credential resolver\", {\n signal: controller.signal,\n }),\n );\n if (\n parsed.requestId !== requestId ||\n parsed.destinationUrl !== request.destinationUrl ||\n parsed.resolution.accountId !== request.accountId ||\n parsed.resolution.workspaceId !== request.workspaceId ||\n (\"sessionId\" in request\n ? !(\"sessionId\" in parsed.resolution) ||\n parsed.resolution.sessionId !== request.sessionId\n : !(\"requestId\" in parsed.resolution) ||\n parsed.resolution.requestId !== request.requestId)\n )\n return unavailable(\"refresh_failed\");\n if (\n parsed.resolution.status === \"ok\" &&\n (Date.parse(parsed.resolution.expiresAt) <= Date.now() + 5_000 ||\n Date.parse(parsed.resolution.expiresAt) > Date.now() + 900_000)\n ) {\n return unavailable(\"refresh_failed\");\n }\n // Existing buildHostConnectionTokenResolver validates the precise\n // connection, provider, scopes, resources and credential placements.\n return normalizeResolution(parsed.resolution);\n })().finally(() => {\n physicalRequests--;\n }),\n ]);\n } catch {\n return unavailable(\"refresh_failed\");\n } finally {\n clearTimeout(timer);\n }\n };\n const task = run();\n active.set(serialized, task);\n try {\n return structuredClone(await task);\n } finally {\n if (active.get(serialized) === task) active.delete(serialized);\n }\n };\n return {\n mcpAuthoritySource: \"host\",\n async mcpCredentials(input) {\n const result = await resolveRemote(input);\n if (!(\"sessionId\" in result)) throw new Error(\"Invalid host turn credential scope\");\n return result;\n },\n async mcpGatewayCredentials(input) {\n const result = await resolveRemote(input);\n if (!(\"requestId\" in result)) throw new Error(\"Invalid host gateway credential scope\");\n return result;\n },\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { SessionAuthorizationActor, SessionAuthorizationListScope, type AccessGrant, type SessionAgentAccess, type SessionAgentAccessViewer, type SessionAuthorizationOperation, type SessionAuthorizationSurface, type SessionAuthorizationTarget, type SessionEndUser } from "@opengeni/contracts";
1
+ import { SessionAuthorizationActor, SessionAuthorizationListScope, type AccessGrant, type SessionAgentAccess, type SessionAgentAccessViewer, type SessionAuthorizationOperation, type SessionAuthorizationSurface, type SessionAuthorizationTarget, type SessionScopeSubjectId } from "@opengeni/contracts";
2
2
  import { type Database, type SessionRlsActorContext } from "@opengeni/db";
3
3
  import type { AppDependencies } from "./dependencies.js";
4
4
  export type SessionAuthorizationDependencies = Pick<AppDependencies, "db" | "sessionAuthorization">;
@@ -22,15 +22,14 @@ export type ResolvedSessionAuthorization = {
22
22
  /** The frozen agent-access facts of one session, as the pairwise rule sees them. */
23
23
  export type SessionAgentAccessFacts = {
24
24
  agentAccess: SessionAgentAccess;
25
- endUser: SessionEndUser | null;
25
+ scopeSubjectId: SessionScopeSubjectId | null;
26
26
  };
27
27
  /**
28
28
  * The agent-to-agent reach rule (migration 0427) for one caller/target pair
29
29
  * that live in DIFFERENT root trees. A caller always keeps its own tree, so
30
- * this is never consulted for same-root access. The most restrictive side
31
- * wins: a `session` side denies everything; a `user` side requires both
32
- * sessions to carry the same non-null end-user label; two `workspace` sides
33
- * are today's behaviour. Humans and API keys never pass through here.
30
+ * this is never consulted for same-root access. Only the caller's task scope
31
+ * restricts outgoing reach. The target's visibility and ownership remain
32
+ * independently enforced; its agent scope does not block incoming access.
34
33
  */
35
34
  export declare function agentAccessPermitsCrossTreeAccess(caller: SessionAgentAccessFacts, target: SessionAgentAccessFacts): boolean;
36
35
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "2.8.3",
3
+ "version": "2.9.1-canary.0",
4
4
  "description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -18,6 +18,10 @@
18
18
  "module": "./dist/index.js",
19
19
  "types": "./dist/index.d.ts",
20
20
  "exports": {
21
+ "./remote-mcp-credentials": {
22
+ "types": "./dist/remote-mcp-credentials.d.ts",
23
+ "import": "./dist/remote-mcp-credentials.js"
24
+ },
21
25
  ".": {
22
26
  "types": "./dist/index.d.ts",
23
27
  "import": "./dist/index.js"
@@ -49,22 +53,22 @@
49
53
  },
50
54
  "scripts": {
51
55
  "typecheck": "tsc --noEmit",
52
- "build": "bun ../../scripts/build-typescript-package.ts",
56
+ "build": "bun ../../scripts/sync-product-integration-skill.ts --check && bun ../../scripts/build-typescript-package.ts",
53
57
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
54
58
  },
55
59
  "dependencies": {
56
60
  "@modelcontextprotocol/sdk": "^1.29.0",
57
- "@opengeni/capabilities": "^0.3.3",
58
- "@opengeni/codex": "^0.2.22",
59
- "@opengeni/config": "^1.0.4",
60
- "@opengeni/contracts": "^2.15.2",
61
- "@opengeni/db": "^4.2.2",
62
- "@opengeni/documents": "^0.8.23",
63
- "@opengeni/events": "^0.4.21",
64
- "@opengeni/network": "^0.3.1",
65
- "@opengeni/observability": "^0.8.23",
66
- "@opengeni/runtime": "^2.4.3",
67
- "@opengeni/storage": "^0.2.124",
61
+ "@opengeni/capabilities": "^0.3.3-canary.6",
62
+ "@opengeni/codex": "^0.2.22-canary.6",
63
+ "@opengeni/config": "^1.1.0-canary.0",
64
+ "@opengeni/contracts": "^3.0.0-canary.0",
65
+ "@opengeni/db": "^4.3.0-canary.0",
66
+ "@opengeni/documents": "^0.8.24-canary.0",
67
+ "@opengeni/events": "^0.4.22-canary.0",
68
+ "@opengeni/network": "^0.3.1-canary.6",
69
+ "@opengeni/observability": "^0.8.24-canary.0",
70
+ "@opengeni/runtime": "^2.5.1-canary.0",
71
+ "@opengeni/storage": "^0.2.125-canary.0",
68
72
  "hono": "^4.12.18",
69
73
  "zod": "^4.2.1"
70
74
  },
@@ -0,0 +1,94 @@
1
+ import type { Permission } from "@opengeni/contracts";
2
+
3
+ export type ExternalAuthoritySnapshot = Readonly<{
4
+ accountId: string;
5
+ externalIdentityId: string;
6
+ identityStatus: "active" | "disabled" | "revoked";
7
+ identityRevision: number;
8
+ membershipStatus: "active" | "provisioning" | "suspended" | "revoked";
9
+ workspaceId: string;
10
+ permissions: readonly Permission[];
11
+ }>;
12
+
13
+ export type ExternalLinkAuthoritySnapshot = Readonly<{
14
+ id: string;
15
+ accountId: string;
16
+ externalIdentityId: string;
17
+ revision: number;
18
+ nativeSubjectId: string;
19
+ status: "pending" | "active" | "revoked" | "expired";
20
+ expiresAt: number | null;
21
+ permissions: readonly Permission[];
22
+ nativeAuthority: Readonly<{
23
+ subjectId: string;
24
+ accountId: string;
25
+ workspaceId: string;
26
+ active: boolean;
27
+ permissions: readonly Permission[];
28
+ }>;
29
+ }>;
30
+
31
+ /** Pure permission calculation over already authenticated, live snapshots.
32
+ * This does not authenticate assertions or establish provenance. Callers must
33
+ * read snapshots under their authorization fence, never from request JSON.
34
+ * Linked mode uses the native lane alone: external workspace permissions must
35
+ * not be unioned into that lane, nor must old external resources change owner.
36
+ */
37
+ export function externalActorPermissions(input: {
38
+ accountId: string;
39
+ workspaceId: string;
40
+ keyActive: boolean;
41
+ keyAccountId: string;
42
+ keyPermissions: readonly Permission[];
43
+ external: ExternalAuthoritySnapshot;
44
+ linked?: {
45
+ expectedId: string;
46
+ expectedRevision: number;
47
+ authority: ExternalLinkAuthoritySnapshot;
48
+ };
49
+ now: number;
50
+ }): Permission[] {
51
+ const external = input.external;
52
+ if (
53
+ !input.keyActive ||
54
+ input.keyAccountId !== input.accountId ||
55
+ external.accountId !== input.accountId ||
56
+ external.identityStatus !== "active" ||
57
+ external.membershipStatus !== "active" ||
58
+ !Number.isSafeInteger(external.identityRevision) ||
59
+ external.identityRevision < 1 ||
60
+ !Number.isFinite(input.now)
61
+ )
62
+ return [];
63
+ let ceiling: readonly Permission[];
64
+ if (input.linked) {
65
+ const { authority: link, expectedId, expectedRevision } = input.linked;
66
+ const native = link.nativeAuthority;
67
+ if (
68
+ !expectedId ||
69
+ link.id !== expectedId ||
70
+ link.accountId !== input.accountId ||
71
+ link.externalIdentityId !== external.externalIdentityId ||
72
+ link.status !== "active" ||
73
+ !Number.isSafeInteger(expectedRevision) ||
74
+ expectedRevision < 1 ||
75
+ link.revision !== expectedRevision ||
76
+ (link.expiresAt !== null &&
77
+ (!Number.isFinite(link.expiresAt) || link.expiresAt <= input.now)) ||
78
+ !native.active ||
79
+ link.nativeSubjectId.length > 1024 ||
80
+ !/^user:[^\r\n]+$(?![\s\S])/.test(link.nativeSubjectId) ||
81
+ native.subjectId !== link.nativeSubjectId ||
82
+ native.accountId !== input.accountId ||
83
+ native.workspaceId !== input.workspaceId
84
+ )
85
+ return [];
86
+ const delegation = new Set(link.permissions);
87
+ ceiling = native.permissions.filter((permission) => delegation.has(permission));
88
+ } else {
89
+ if (external.workspaceId !== input.workspaceId) return [];
90
+ ceiling = external.permissions;
91
+ }
92
+ const allowed = new Set(ceiling);
93
+ return [...new Set(input.keyPermissions)].filter((permission) => allowed.has(permission));
94
+ }