@opengeni/core 3.2.2 → 3.3.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/access/index.d.ts +13 -0
- package/dist/application/site-session-cancellation.d.ts +14 -0
- package/dist/domain/insights-usage.d.ts +24 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +134 -7
- package/dist/index.js.map +1 -1
- package/package.json +13 -13
- package/src/access/index.ts +100 -0
- package/src/application/site-session-cancellation.ts +47 -0
- package/src/domain/file-owner.ts +1 -0
- package/src/domain/insights-usage.ts +20 -1
- package/src/index.ts +1 -0
- package/src/sandbox/fleet.ts +1 -0
- package/src/session-authorization.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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": {
|
|
@@ -50,18 +50,18 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
|
-
"@opengeni/capabilities": "3.
|
|
54
|
-
"@opengeni/codex": "3.
|
|
55
|
-
"@opengeni/config": "3.
|
|
56
|
-
"@opengeni/contracts": "3.
|
|
57
|
-
"@opengeni/db": "3.
|
|
58
|
-
"@opengeni/documents": "3.
|
|
59
|
-
"@opengeni/events": "3.
|
|
60
|
-
"@opengeni/network": "3.
|
|
61
|
-
"@opengeni/observability": "3.
|
|
62
|
-
"@opengeni/runtime": "3.
|
|
63
|
-
"@opengeni/storage": "3.
|
|
64
|
-
"@opengeni/vaults": "3.
|
|
53
|
+
"@opengeni/capabilities": "3.3.0",
|
|
54
|
+
"@opengeni/codex": "3.3.0",
|
|
55
|
+
"@opengeni/config": "3.3.0",
|
|
56
|
+
"@opengeni/contracts": "3.3.0",
|
|
57
|
+
"@opengeni/db": "3.3.0",
|
|
58
|
+
"@opengeni/documents": "3.3.0",
|
|
59
|
+
"@opengeni/events": "3.3.0",
|
|
60
|
+
"@opengeni/network": "3.3.0",
|
|
61
|
+
"@opengeni/observability": "3.3.0",
|
|
62
|
+
"@opengeni/runtime": "3.3.0",
|
|
63
|
+
"@opengeni/storage": "3.3.0",
|
|
64
|
+
"@opengeni/vaults": "3.3.0",
|
|
65
65
|
"hono": "^4.12.18",
|
|
66
66
|
"zod": "^4.2.1"
|
|
67
67
|
},
|
package/src/access/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
listWorkspacesForSubject,
|
|
38
38
|
findActiveApiKeyByHash,
|
|
39
39
|
getWorkspaceGrant,
|
|
40
|
+
subjectHasLiveWorkspaceAuthorityInScope,
|
|
40
41
|
requireWorkspace,
|
|
41
42
|
resolveNamedManagedPersonalWorkspaceGrant,
|
|
42
43
|
type Database,
|
|
@@ -216,6 +217,7 @@ const delegatedHumanAuthorizationSchema = z.object({
|
|
|
216
217
|
|
|
217
218
|
const delegatedHumanRequests = new WeakMap<Request, DelegatedHumanAuthorization>();
|
|
218
219
|
const delegatedHumanContexts = new WeakMap<AccessContext, DelegatedHumanAuthorization>();
|
|
220
|
+
const verifiedSiteViewerProofs = new WeakSet<DelegatedHumanAuthorization>();
|
|
219
221
|
const delegatedHumanProfileSchema = z.object({
|
|
220
222
|
id: z.string(),
|
|
221
223
|
name: z.string().nullable(),
|
|
@@ -283,6 +285,48 @@ export function stampDelegatedHumanAuthorization(
|
|
|
283
285
|
delegatedHumanRequests.set(request, Object.freeze(proof));
|
|
284
286
|
}
|
|
285
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Verified published-Site dispatch only: the caller has verified the signed
|
|
290
|
+
* gateway, served host and opaque viewer cookie. This is exact-workspace
|
|
291
|
+
* ownership for session routes, not a console login or a generic Site bearer.
|
|
292
|
+
* Native people use the existing delegated-human resolver; external owning
|
|
293
|
+
* users reload their existing membership without provisioning a new identity.
|
|
294
|
+
*/
|
|
295
|
+
export function stampSiteViewerSessionAuthorization(
|
|
296
|
+
request: Request,
|
|
297
|
+
input: {
|
|
298
|
+
organizationId: string;
|
|
299
|
+
workspaceId: string;
|
|
300
|
+
subjectId: string;
|
|
301
|
+
permissions: Permission[];
|
|
302
|
+
},
|
|
303
|
+
): void {
|
|
304
|
+
if (input.subjectId.startsWith("user:")) {
|
|
305
|
+
stampDelegatedHumanAuthorization(request, {
|
|
306
|
+
...input,
|
|
307
|
+
workspaceScope: { kind: "selected", workspaceIds: [input.workspaceId] },
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (delegatedHumanRequests.has(request) || accessResolvedRequests.has(request))
|
|
312
|
+
throw new HTTPException(403, { message: "request access authorization already resolved" });
|
|
313
|
+
const parsed = delegatedHumanAuthorizationSchema
|
|
314
|
+
.extend({
|
|
315
|
+
subjectId: z
|
|
316
|
+
.string()
|
|
317
|
+
.max(1024)
|
|
318
|
+
.regex(/^external_user:[^\s\u0000-\u001f\u007f]+$/),
|
|
319
|
+
})
|
|
320
|
+
.parse({ ...input, workspaceScope: { kind: "selected", workspaceIds: [input.workspaceId] } });
|
|
321
|
+
if (parsed.workspaceScope.kind !== "selected") throw new Error("Site workspace scope required");
|
|
322
|
+
Object.freeze(parsed.permissions);
|
|
323
|
+
Object.freeze(parsed.workspaceScope.workspaceIds);
|
|
324
|
+
Object.freeze(parsed.workspaceScope);
|
|
325
|
+
const proof = Object.freeze(parsed);
|
|
326
|
+
verifiedSiteViewerProofs.add(proof);
|
|
327
|
+
delegatedHumanRequests.set(request, proof);
|
|
328
|
+
}
|
|
329
|
+
|
|
286
330
|
/** Token prefix of personal API keys, which act as the person who made them. */
|
|
287
331
|
export const PERSONAL_API_KEY_TOKEN_PREFIX = "ogp_";
|
|
288
332
|
|
|
@@ -1521,6 +1565,62 @@ async function resolveDelegatedHumanAccessContext(
|
|
|
1521
1565
|
message: "delegated humans require native organization access",
|
|
1522
1566
|
});
|
|
1523
1567
|
}
|
|
1568
|
+
if (verifiedSiteViewerProofs.has(proof)) {
|
|
1569
|
+
const workspaceId =
|
|
1570
|
+
proof.workspaceScope.kind === "selected" && proof.workspaceScope.workspaceIds.length === 1
|
|
1571
|
+
? proof.workspaceScope.workspaceIds[0]
|
|
1572
|
+
: null;
|
|
1573
|
+
if (!workspaceId)
|
|
1574
|
+
throw new HTTPException(403, { message: "Site viewer workspace unavailable" });
|
|
1575
|
+
const grant = await withWorkspaceSubjectRls(
|
|
1576
|
+
deps.db,
|
|
1577
|
+
workspaceId,
|
|
1578
|
+
proof.subjectId,
|
|
1579
|
+
async (tx) => {
|
|
1580
|
+
if (
|
|
1581
|
+
!(await subjectHasLiveWorkspaceAuthorityInScope(tx, {
|
|
1582
|
+
accountId: proof.organizationId,
|
|
1583
|
+
workspaceId,
|
|
1584
|
+
subjectId: proof.subjectId,
|
|
1585
|
+
}))
|
|
1586
|
+
)
|
|
1587
|
+
return null;
|
|
1588
|
+
return getWorkspaceGrant(tx, proof.subjectId, workspaceId, {
|
|
1589
|
+
principalKind: "human_session",
|
|
1590
|
+
});
|
|
1591
|
+
},
|
|
1592
|
+
);
|
|
1593
|
+
if (!grant || grant.accountId !== proof.organizationId)
|
|
1594
|
+
throw new HTTPException(403, { message: "Site viewer membership unavailable" });
|
|
1595
|
+
const bounded = Object.freeze({
|
|
1596
|
+
...grant,
|
|
1597
|
+
permissionMode: "explicit" as const,
|
|
1598
|
+
permissions: Object.freeze(
|
|
1599
|
+
explicitPermissions(
|
|
1600
|
+
Permission.options.filter(
|
|
1601
|
+
(permission) =>
|
|
1602
|
+
hasPermission(grant.permissions, permission) &&
|
|
1603
|
+
hasPermission(proof.permissions, permission, "explicit"),
|
|
1604
|
+
),
|
|
1605
|
+
),
|
|
1606
|
+
) as Permission[],
|
|
1607
|
+
});
|
|
1608
|
+
const accountGrant = Object.freeze({
|
|
1609
|
+
accountId: proof.organizationId,
|
|
1610
|
+
subjectId: proof.subjectId,
|
|
1611
|
+
permissions: Object.freeze([] as Permission[]) as Permission[],
|
|
1612
|
+
});
|
|
1613
|
+
const context: AccessContext = Object.freeze({
|
|
1614
|
+
mode: "managed",
|
|
1615
|
+
subjectId: proof.subjectId,
|
|
1616
|
+
accountGrants: Object.freeze([accountGrant]) as AccountGrant[],
|
|
1617
|
+
workspaceGrants: Object.freeze([bounded]) as AccessGrant[],
|
|
1618
|
+
defaultAccountId: proof.organizationId,
|
|
1619
|
+
defaultWorkspaceId: workspaceId,
|
|
1620
|
+
});
|
|
1621
|
+
delegatedHumanContexts.set(context, proof);
|
|
1622
|
+
return context;
|
|
1623
|
+
}
|
|
1524
1624
|
const userId = proof.subjectId.slice("user:".length);
|
|
1525
1625
|
const profiles = await getManagedUserProfilesByIds(deps.db, [userId]);
|
|
1526
1626
|
const parsedProfile = delegatedHumanProfileSchema.safeParse(
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cancelSiteRuntimeConversation, type Database } from "@opengeni/db";
|
|
2
|
+
import {
|
|
3
|
+
publishDurableSessionEvents,
|
|
4
|
+
publishDurableWorkspaceControlEvent,
|
|
5
|
+
type EventBus,
|
|
6
|
+
} from "@opengeni/events";
|
|
7
|
+
import type { SessionWorkflowClient } from "../dependencies";
|
|
8
|
+
|
|
9
|
+
/** Internal host lifecycle, not a session-read/control API or a viewer grant. */
|
|
10
|
+
export async function cancelPublishedSiteSession(
|
|
11
|
+
deps: {
|
|
12
|
+
db: Database;
|
|
13
|
+
bus: EventBus;
|
|
14
|
+
workflowClient: Pick<SessionWorkflowClient, "requestSessionWorkflowWakeDispatch">;
|
|
15
|
+
},
|
|
16
|
+
input: Parameters<typeof cancelSiteRuntimeConversation>[1],
|
|
17
|
+
) {
|
|
18
|
+
const result = await cancelSiteRuntimeConversation(deps.db, input);
|
|
19
|
+
if (!result) return null;
|
|
20
|
+
// Capture only this command's event envelopes under its transaction witness.
|
|
21
|
+
// Never re-open private transcript visibility to do post-commit fanout.
|
|
22
|
+
const bySession = new Map<string, typeof result.events>();
|
|
23
|
+
for (const event of result.events) {
|
|
24
|
+
const group = bySession.get(event.sessionId) ?? [];
|
|
25
|
+
group.push(event);
|
|
26
|
+
bySession.set(event.sessionId, group);
|
|
27
|
+
}
|
|
28
|
+
const effects = await Promise.allSettled([
|
|
29
|
+
...[...bySession].map(([sessionId, events]) =>
|
|
30
|
+
publishDurableSessionEvents(deps.bus, input.workspaceId, sessionId, events),
|
|
31
|
+
),
|
|
32
|
+
...(result.workspaceEvent
|
|
33
|
+
? [publishDurableWorkspaceControlEvent(deps.bus, input.workspaceId, result.workspaceEvent)]
|
|
34
|
+
: []),
|
|
35
|
+
...(result.wakeCount > 0 ? [deps.workflowClient.requestSessionWorkflowWakeDispatch()] : []),
|
|
36
|
+
]);
|
|
37
|
+
if (effects.some((effect) => effect.status === "rejected"))
|
|
38
|
+
console.warn(
|
|
39
|
+
"[site-lifecycle] post-commit fanout failed; durable events and wake outbox retained",
|
|
40
|
+
);
|
|
41
|
+
return {
|
|
42
|
+
receiptId: result.receiptId,
|
|
43
|
+
replay: result.replay,
|
|
44
|
+
cancelledSessionCount: result.cancelledSessionCount,
|
|
45
|
+
cancelledTurnCount: result.cancelledTurnCount,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/domain/file-owner.ts
CHANGED
|
@@ -62,6 +62,7 @@ export async function fileOwnerContextForAgent(
|
|
|
62
62
|
},
|
|
63
63
|
subjectId: grant.subjectId,
|
|
64
64
|
initiatingHumanSubjectId: attempt.initiatingHumanSubjectId,
|
|
65
|
+
agentRootSessionId: attempt.callerRootSessionId,
|
|
65
66
|
privateFileOwnerSubjectId: snapshot.defaultScope === "personal" ? snapshot.subjectId : null,
|
|
66
67
|
};
|
|
67
68
|
}
|
|
@@ -4,8 +4,27 @@ import type {
|
|
|
4
4
|
InsightsUsageQuery,
|
|
5
5
|
InsightsUsageRange,
|
|
6
6
|
InsightsUsageResponse,
|
|
7
|
+
InsightsSiteUsageQuery,
|
|
7
8
|
} from "@opengeni/contracts/insights-usage";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
readInsightsCalls,
|
|
11
|
+
readInsightsUsage,
|
|
12
|
+
readInsightsSiteUsage,
|
|
13
|
+
type Database,
|
|
14
|
+
} from "@opengeni/db";
|
|
15
|
+
|
|
16
|
+
export async function getInsightsSiteUsage(
|
|
17
|
+
db: Database,
|
|
18
|
+
input: {
|
|
19
|
+
accountId: string;
|
|
20
|
+
workspaceId: string;
|
|
21
|
+
query: InsightsSiteUsageQuery;
|
|
22
|
+
viewerSubjectId: string | null;
|
|
23
|
+
now?: Date;
|
|
24
|
+
},
|
|
25
|
+
) {
|
|
26
|
+
return readInsightsSiteUsage(db, { ...input, now: input.now ?? new Date() });
|
|
27
|
+
}
|
|
9
28
|
|
|
10
29
|
export type InsightsQueryScope = {
|
|
11
30
|
accountId: string;
|
package/src/index.ts
CHANGED
|
@@ -149,6 +149,7 @@ export * from "./domain/organization-membership-lifecycle";
|
|
|
149
149
|
export * from "./application/new-session-drafts";
|
|
150
150
|
export * from "./application/composer-submit";
|
|
151
151
|
export * from "./application/session-commands";
|
|
152
|
+
export * from "./application/site-session-cancellation";
|
|
152
153
|
export * from "./application/archived-session-imports";
|
|
153
154
|
export * from "./application/session-tenancy";
|
|
154
155
|
export * from "./application/organization-privacy";
|
package/src/sandbox/fleet.ts
CHANGED
|
@@ -985,6 +985,7 @@ function runOnOperationAdmission(
|
|
|
985
985
|
operationResourcePolicySupported: enrollment.agentCapabilities.operationResourcePolicy === true,
|
|
986
986
|
operationCpuQuotaSupported: enrollment.agentCapabilities.operationCpuQuota === true,
|
|
987
987
|
transactionalFsWriteSupported: enrollment.agentCapabilities.transactionalFsWrite === true,
|
|
988
|
+
browserControlHttpSupported: enrollment.agentCapabilities.browserControlHttp === true,
|
|
988
989
|
};
|
|
989
990
|
}
|
|
990
991
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15
15
|
import {
|
|
16
16
|
currentSessionRlsActorIdentityKey,
|
|
17
|
+
currentSiteViewerRlsContext,
|
|
17
18
|
getSessionAuthorityProjection,
|
|
18
19
|
getSessionAuthorizationTargetProjection,
|
|
19
20
|
getSession,
|
|
@@ -237,6 +238,7 @@ export function sessionRlsActorForAuthorization(
|
|
|
237
238
|
? {
|
|
238
239
|
subjectId: authorization.actor.subjectId,
|
|
239
240
|
initiatingHumanSubjectId: authorization.actor.initiatingHumanSubjectId,
|
|
241
|
+
agentRootSessionId: authorization.actor.callerRootSessionId,
|
|
240
242
|
}
|
|
241
243
|
: { subjectId: authorization.actor.subjectId };
|
|
242
244
|
}
|
|
@@ -312,6 +314,25 @@ export async function requireSessionAuthorization(
|
|
|
312
314
|
throw new SessionAuthorizationDeniedError("not_found");
|
|
313
315
|
}
|
|
314
316
|
|
|
317
|
+
// Also enforce the durable Site audience for hosts using scoped (owner-role)
|
|
318
|
+
// databases. The request-local viewer proof is never taken from Site JSON.
|
|
319
|
+
if (authority?.siteAccessRootId) {
|
|
320
|
+
const viewer = currentSiteViewerRlsContext();
|
|
321
|
+
const resolved = await resolveSessionAuthorizationActor(deps.db, grant, reads);
|
|
322
|
+
const actor = resolved.actor;
|
|
323
|
+
const allowed =
|
|
324
|
+
actor.kind === "agent_attempt"
|
|
325
|
+
? (authority.siteOwnerSubjectId !== null &&
|
|
326
|
+
authority.siteOwnerSubjectId === actor.initiatingHumanSubjectId) ||
|
|
327
|
+
(actor.callerRootSessionId === authority.rootSessionId &&
|
|
328
|
+
authority.siteAccessRootId === authority.rootSessionId)
|
|
329
|
+
: authority.siteOwnerSubjectId === actor.subjectId ||
|
|
330
|
+
(actor.subjectId === `site:${authority.siteId}` &&
|
|
331
|
+
viewer?.siteId === authority.siteId &&
|
|
332
|
+
viewer.principal === authority.siteViewerPrincipal);
|
|
333
|
+
if (!allowed) throw new SessionAuthorizationDeniedError("not_found");
|
|
334
|
+
}
|
|
335
|
+
|
|
315
336
|
// A request from a Site page (an HTTP frame request, or a tool the page
|
|
316
337
|
// called) may act on a session only when that same Site, or for a frame
|
|
317
338
|
// without a published Site the same mounted frame, started it. Anything
|