@opengeni/core 0.4.6 → 0.4.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/core",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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": {
@@ -8,6 +8,10 @@
8
8
  "url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
9
9
  "directory": "packages/core"
10
10
  },
11
+ "files": [
12
+ "dist",
13
+ "src"
14
+ ],
11
15
  "type": "module",
12
16
  "sideEffects": false,
13
17
  "main": "./dist/index.js",
@@ -19,33 +23,29 @@
19
23
  "import": "./dist/index.js"
20
24
  }
21
25
  },
22
- "files": [
23
- "dist",
24
- "src"
25
- ],
26
- "engines": {
27
- "node": ">=18"
28
- },
29
26
  "publishConfig": {
30
27
  "access": "public",
31
28
  "provenance": true
32
29
  },
33
30
  "scripts": {
34
- "typecheck": "tsc --noEmit",
31
+ "typecheck": "tsgo --noEmit",
35
32
  "build": "tsup",
36
33
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
37
34
  },
38
35
  "dependencies": {
39
36
  "@modelcontextprotocol/sdk": "^1.29.0",
40
- "@opengeni/codex": "^0.2.1",
41
- "@opengeni/config": "^0.4.0",
42
- "@opengeni/contracts": "^0.9.0",
43
- "@opengeni/db": "^0.6.1",
44
- "@opengeni/documents": "^0.2.8",
45
- "@opengeni/events": "^0.2.8",
37
+ "@opengeni/codex": "^0.2.2",
38
+ "@opengeni/config": "^0.5.0",
39
+ "@opengeni/contracts": "^0.10.0",
40
+ "@opengeni/db": "^0.7.1",
41
+ "@opengeni/documents": "^0.2.10",
42
+ "@opengeni/events": "^0.3.1",
46
43
  "@opengeni/observability": "^0.3.0",
47
- "@opengeni/runtime": "^0.6.1",
48
- "@opengeni/storage": "^0.2.8",
44
+ "@opengeni/runtime": "^0.7.0",
45
+ "@opengeni/storage": "^0.2.9",
49
46
  "hono": "^4.12.18"
47
+ },
48
+ "engines": {
49
+ "node": ">=18"
50
50
  }
51
51
  }
@@ -1,5 +1,10 @@
1
1
  import type { Settings } from "@opengeni/config";
2
- import { verifyDelegatedAccessToken, type AccessContext, type AccessGrant, type Permission } from "@opengeni/contracts";
2
+ import {
3
+ verifyDelegatedAccessToken,
4
+ type AccessContext,
5
+ type AccessGrant,
6
+ type Permission,
7
+ } from "@opengeni/contracts";
3
8
  import {
4
9
  bootstrapWorkspace,
5
10
  ensureManagedAccessForUser,
@@ -28,10 +33,16 @@ export async function requireAccessContext(c: Context, deps: AccessDeps): Promis
28
33
  return context;
29
34
  }
30
35
 
31
- export async function requireAccessGrant(c: Context, deps: AccessDeps, workspaceId: string, permission?: Permission): Promise<AccessGrant> {
36
+ export async function requireAccessGrant(
37
+ c: Context,
38
+ deps: AccessDeps,
39
+ workspaceId: string,
40
+ permission?: Permission,
41
+ ): Promise<AccessGrant> {
32
42
  const context = await requireAccessContext(c, deps);
33
- const grant = context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId)
34
- ?? await getWorkspaceGrant(deps.db, context.subjectId, workspaceId);
43
+ const grant =
44
+ context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId) ??
45
+ (await getWorkspaceGrant(deps.db, context.subjectId, workspaceId));
35
46
  if (!grant) {
36
47
  const workspace = await requireWorkspace(deps.db, workspaceId).catch(() => null);
37
48
  if (!workspace) {
@@ -47,12 +58,30 @@ export async function requireAccessGrant(c: Context, deps: AccessDeps, workspace
47
58
 
48
59
  export function requirePermission(grant: AccessGrant, permission: Permission): void {
49
60
  if (!hasPermission(grant.permissions, permission)) {
61
+ if (permission === "variable-sets:use") {
62
+ throw new HTTPException(403, {
63
+ message: "missing permission: variable-sets:use (deprecated alias: environments:use)",
64
+ });
65
+ }
66
+ if (permission === "variable-sets:manage") {
67
+ throw new HTTPException(403, {
68
+ message: "missing permission: variable-sets:manage (deprecated alias: environments:manage)",
69
+ });
70
+ }
50
71
  throw new HTTPException(403, { message: `missing permission: ${permission}` });
51
72
  }
52
73
  }
53
74
 
54
75
  export function hasPermission(permissions: Permission[], permission: Permission): boolean {
55
- return permissions.includes(permission) || permissions.includes("workspace:admin");
76
+ const aliases: Partial<Record<Permission, Permission[]>> = {
77
+ "variable-sets:use": ["environments:use" as Permission],
78
+ "variable-sets:manage": ["environments:manage" as Permission],
79
+ };
80
+ return (
81
+ permissions.includes(permission) ||
82
+ (aliases[permission]?.some((alias) => permissions.includes(alias)) ?? false) ||
83
+ permissions.includes("workspace:admin")
84
+ );
56
85
  }
57
86
 
58
87
  async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext | null> {
@@ -69,16 +98,20 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
69
98
  });
70
99
  }
71
100
 
72
- if (deps.settings.productAccessMode === "configured") {
73
- const delegated = await delegatedAccessContext(c, deps, "configured");
74
- if (delegated) {
75
- return delegated;
76
- }
77
- if (deps.settings.delegationSecret) {
78
- return null;
79
- }
80
- return await bootstrapWorkspace(deps.db, {
81
- accountExternalSource: "opengeni:configured",
101
+ if (deps.settings.productAccessMode === "configured") {
102
+ const delegated = await delegatedAccessContext(c, deps, "configured");
103
+ if (delegated) {
104
+ return delegated;
105
+ }
106
+ const apiKey = await apiKeyAccessContext(c, deps, "configured");
107
+ if (apiKey) {
108
+ return apiKey;
109
+ }
110
+ if (deps.settings.delegationSecret) {
111
+ return null;
112
+ }
113
+ return await bootstrapWorkspace(deps.db, {
114
+ accountExternalSource: "opengeni:configured",
82
115
  accountExternalId: "default",
83
116
  accountName: "Configured",
84
117
  workspaceExternalSource: "opengeni:configured",
@@ -95,31 +128,9 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
95
128
  if (delegated) {
96
129
  return delegated;
97
130
  }
98
- const apiKey = await findActiveApiKeyByHash(deps.db, await sha256Hex(bearer));
131
+ const apiKey = await apiKeyAccessContext(c, deps, "managed");
99
132
  if (apiKey) {
100
- const accountPermissions = apiKey.workspaceId
101
- ? apiKey.permissions.filter((permission) => permission === "billing:read" || permission === "billing:manage")
102
- : apiKey.permissions;
103
- return {
104
- mode: "managed",
105
- subjectId: `api_key:${apiKey.id}`,
106
- subjectLabel: apiKey.name,
107
- accountGrants: [{
108
- accountId: apiKey.accountId,
109
- subjectId: `api_key:${apiKey.id}`,
110
- subjectLabel: apiKey.name,
111
- permissions: accountPermissions,
112
- }],
113
- workspaceGrants: apiKey.workspaceId ? [{
114
- workspaceId: apiKey.workspaceId,
115
- accountId: apiKey.accountId,
116
- subjectId: `api_key:${apiKey.id}`,
117
- subjectLabel: apiKey.name,
118
- permissions: apiKey.permissions,
119
- }] : [],
120
- defaultAccountId: apiKey.accountId,
121
- defaultWorkspaceId: apiKey.workspaceId,
122
- } satisfies AccessContext;
133
+ return apiKey;
123
134
  }
124
135
  }
125
136
 
@@ -137,7 +148,59 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
137
148
  return null;
138
149
  }
139
150
 
140
- async function delegatedAccessContext(c: Context, deps: AccessDeps, mode: "configured" | "managed", token = bearerToken(c)): Promise<AccessContext | null> {
151
+ async function apiKeyAccessContext(
152
+ c: Context,
153
+ deps: AccessDeps,
154
+ mode: "configured" | "managed",
155
+ ): Promise<AccessContext | null> {
156
+ const bearer = bearerToken(c);
157
+ if (!bearer) {
158
+ return null;
159
+ }
160
+ const apiKey = await findActiveApiKeyByHash(deps.db, await sha256Hex(bearer));
161
+ if (!apiKey) {
162
+ return null;
163
+ }
164
+ const subjectId = `api_key:${apiKey.id}`;
165
+ const accountPermissions = apiKey.workspaceId
166
+ ? apiKey.permissions.filter(
167
+ (permission) => permission === "billing:read" || permission === "billing:manage",
168
+ )
169
+ : apiKey.permissions;
170
+ return {
171
+ mode,
172
+ subjectId,
173
+ subjectLabel: apiKey.name,
174
+ accountGrants: [
175
+ {
176
+ accountId: apiKey.accountId,
177
+ subjectId,
178
+ subjectLabel: apiKey.name,
179
+ permissions: accountPermissions,
180
+ },
181
+ ],
182
+ workspaceGrants: apiKey.workspaceId
183
+ ? [
184
+ {
185
+ workspaceId: apiKey.workspaceId,
186
+ accountId: apiKey.accountId,
187
+ subjectId,
188
+ subjectLabel: apiKey.name,
189
+ permissions: apiKey.permissions,
190
+ },
191
+ ]
192
+ : [],
193
+ defaultAccountId: apiKey.accountId,
194
+ defaultWorkspaceId: apiKey.workspaceId,
195
+ } satisfies AccessContext;
196
+ }
197
+
198
+ async function delegatedAccessContext(
199
+ c: Context,
200
+ deps: AccessDeps,
201
+ mode: "configured" | "managed",
202
+ token = bearerToken(c),
203
+ ): Promise<AccessContext | null> {
141
204
  if (!token || !deps.settings.delegationSecret) {
142
205
  return null;
143
206
  }
@@ -149,22 +212,36 @@ async function delegatedAccessContext(c: Context, deps: AccessDeps, mode: "confi
149
212
  mode,
150
213
  subjectId: payload.subjectId,
151
214
  ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
152
- accountGrants: [{
153
- accountId: payload.accountId,
154
- subjectId: payload.subjectId,
155
- ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
156
- permissions: payload.permissions,
157
- }],
158
- workspaceGrants: [{
159
- workspaceId: payload.workspaceId,
160
- accountId: payload.accountId,
161
- subjectId: payload.subjectId,
162
- ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
163
- permissions: payload.permissions,
164
- // sessionId is worker-asserted (HMAC-signed token claim), not agent
165
- // controlled; it scopes session-bound MCP tools such as goal management.
166
- metadata: { delegated: true, ...(payload.sessionId ? { sessionId: payload.sessionId } : {}) },
167
- }],
215
+ accountGrants: [
216
+ {
217
+ accountId: payload.accountId,
218
+ subjectId: payload.subjectId,
219
+ ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
220
+ permissions: payload.permissions,
221
+ },
222
+ ],
223
+ workspaceGrants: [
224
+ {
225
+ workspaceId: payload.workspaceId,
226
+ accountId: payload.accountId,
227
+ subjectId: payload.subjectId,
228
+ ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
229
+ permissions: payload.permissions,
230
+ // sessionId is worker-asserted (HMAC-signed token claim), not agent
231
+ // controlled; it scopes session-bound MCP tools such as goal management.
232
+ metadata: {
233
+ delegated: true,
234
+ ...(payload.sessionId ? { sessionId: payload.sessionId } : {}),
235
+ // Caller identity: the turn that minted this token. Tools classify the
236
+ // CALLER from this instead of re-reading the live active pointer.
237
+ ...(payload.turnId ? { turnId: payload.turnId } : {}),
238
+ ...(payload.attemptId ? { attemptId: payload.attemptId } : {}),
239
+ ...(payload.executionGeneration
240
+ ? { executionGeneration: payload.executionGeneration }
241
+ : {}),
242
+ },
243
+ },
244
+ ],
168
245
  defaultAccountId: payload.accountId,
169
246
  defaultWorkspaceId: payload.workspaceId,
170
247
  };
@@ -182,5 +259,7 @@ function bearerToken(c: Context): string | null {
182
259
 
183
260
  async function sha256Hex(value: string): Promise<string> {
184
261
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
185
- return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
262
+ return Array.from(new Uint8Array(digest))
263
+ .map((byte) => byte.toString(16).padStart(2, "0"))
264
+ .join("");
186
265
  }