@opengeni/core 0.28.0 → 1.0.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 (224) hide show
  1. package/README.md +1 -1
  2. package/dist/access/external-actor-authority.d.ts +47 -0
  3. package/dist/access/index.d.ts +182 -3
  4. package/dist/access/service-initiator.d.ts +3 -0
  5. package/dist/application/api-integration-servers.d.ts +20 -0
  6. package/dist/application/archived-session-imports.d.ts +6 -0
  7. package/dist/application/composer-submit.d.ts +24 -0
  8. package/dist/application/connect-authority.d.ts +13 -0
  9. package/dist/application/connect-operation.d.ts +31 -0
  10. package/dist/application/external-continuation.d.ts +15 -0
  11. package/dist/application/external-identity-lifecycle.d.ts +20 -0
  12. package/dist/application/external-link-work-admission.d.ts +12 -0
  13. package/dist/application/external-workspace-members.d.ts +34 -0
  14. package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
  15. package/dist/application/new-session-drafts.d.ts +39 -2
  16. package/dist/application/sandbox-recovery.d.ts +58 -0
  17. package/dist/application/session-commands.d.ts +26 -13
  18. package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
  19. package/dist/application/session-tenancy.d.ts +19 -0
  20. package/dist/application/user-resource-grants.d.ts +20 -0
  21. package/dist/billing/limits.d.ts +11 -0
  22. package/dist/canonical-human-identities.d.ts +7 -2
  23. package/dist/canonical-human-identities.js +17 -4
  24. package/dist/canonical-human-identities.js.map +1 -1
  25. package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
  26. package/dist/chunk-2MKMRGSY.js.map +1 -0
  27. package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
  28. package/dist/chunk-GVO3N37G.js.map +1 -0
  29. package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
  30. package/dist/chunk-LCQXNR62.js.map +1 -0
  31. package/dist/chunk-MM6N7CTO.js +387 -0
  32. package/dist/chunk-MM6N7CTO.js.map +1 -0
  33. package/dist/chunk-QO5GVFFO.js +235 -0
  34. package/dist/chunk-QO5GVFFO.js.map +1 -0
  35. package/dist/codex-model-availability.d.ts +14 -0
  36. package/dist/default-session-model.d.ts +139 -0
  37. package/dist/dependencies.d.ts +87 -4
  38. package/dist/developer-setup-credential-ceiling.d.ts +10 -0
  39. package/dist/domain/agent-config-resolution.d.ts +49 -0
  40. package/dist/domain/automations.d.ts +58 -0
  41. package/dist/domain/capabilities.d.ts +22 -4
  42. package/dist/domain/company-profile-agent-admin.d.ts +25 -0
  43. package/dist/domain/connection-account-selection-error.d.ts +8 -0
  44. package/dist/domain/connector-tool-permissions.d.ts +36 -0
  45. package/dist/domain/direct-model-provider.d.ts +3 -0
  46. package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
  47. package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
  48. package/dist/domain/editable-artifacts/ports.d.ts +10 -0
  49. package/dist/domain/editable-artifacts/service.d.ts +4 -3
  50. package/dist/domain/environments.d.ts +5 -9
  51. package/dist/domain/external-creation-attribution.d.ts +6 -0
  52. package/dist/domain/file-owner.d.ts +8 -0
  53. package/dist/domain/github-action-policies.d.ts +36 -0
  54. package/dist/domain/github-repository-bindings.d.ts +117 -0
  55. package/dist/domain/github-skill-source.d.ts +5 -0
  56. package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
  57. package/dist/domain/insights-timing.d.ts +16 -0
  58. package/dist/domain/insights-usage.d.ts +27 -0
  59. package/dist/domain/insights.d.ts +28 -1
  60. package/dist/domain/knowledge-files.d.ts +20 -0
  61. package/dist/domain/knowledge-messages.d.ts +39 -0
  62. package/dist/domain/knowledge-messages.test.d.ts +1 -0
  63. package/dist/domain/knowledge-preparation.d.ts +10 -0
  64. package/dist/domain/knowledge-preparation.test.d.ts +1 -0
  65. package/dist/domain/knowledge-search.d.ts +15 -0
  66. package/dist/domain/knowledge-search.test.d.ts +1 -0
  67. package/dist/domain/knowledge.d.ts +9 -0
  68. package/dist/domain/mcp-account-bindings.d.ts +19 -0
  69. package/dist/domain/mcp-account-routes.d.ts +21 -0
  70. package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
  71. package/dist/domain/organization-integration-catalog.d.ts +15 -0
  72. package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
  73. package/dist/domain/personal-connection-delegations.d.ts +87 -4
  74. package/dist/domain/pr-review.d.ts +249 -0
  75. package/dist/domain/resources.d.ts +24 -3
  76. package/dist/domain/scheduled-task-access.d.ts +247 -0
  77. package/dist/domain/scheduled-tasks.d.ts +103 -6
  78. package/dist/domain/session-connection-accounts.d.ts +16 -0
  79. package/dist/domain/session-file-access.d.ts +4 -0
  80. package/dist/domain/session-native-connection-replacement.d.ts +14 -0
  81. package/dist/domain/session-start-timing.d.ts +7 -0
  82. package/dist/domain/session-tool-policy.d.ts +93 -4
  83. package/dist/domain/sessions.d.ts +275 -28
  84. package/dist/domain/skill-imports.d.ts +1 -0
  85. package/dist/domain/skill-search.d.ts +30 -0
  86. package/dist/domain/skills.d.ts +117 -0
  87. package/dist/domain/slack-bot.d.ts +46 -1
  88. package/dist/domain/workspace-members.d.ts +8 -1
  89. package/dist/editable-artifact-live/application.d.ts +12 -3
  90. package/dist/editable-artifact-live/ports.d.ts +0 -1
  91. package/dist/editable-artifact-live/server.d.ts +5 -2
  92. package/dist/editable-artifact-live/ticket.d.ts +2 -2
  93. package/dist/editable-artifact-live/types.d.ts +14 -2
  94. package/dist/editable-artifact-live.js +2 -2
  95. package/dist/editable-artifacts.js +10 -4
  96. package/dist/index.d.ts +49 -3
  97. package/dist/index.js +18826 -6847
  98. package/dist/index.js.map +1 -1
  99. package/dist/managed-auth-session-sets.d.ts +131 -0
  100. package/dist/managed-auth-session-sets.js +51 -0
  101. package/dist/managed-auth-session-sets.js.map +1 -0
  102. package/dist/managed-session.d.ts +98 -11
  103. package/dist/model-catalog.d.ts +117 -0
  104. package/dist/product-usage-metrics.d.ts +30 -0
  105. package/dist/rigs/index.d.ts +22 -5
  106. package/dist/rigs/provider-images.d.ts +1 -1
  107. package/dist/sandbox/fleet.d.ts +59 -10
  108. package/dist/sandbox/routing.d.ts +22 -2
  109. package/dist/sandbox/runtime-settings.d.ts +56 -0
  110. package/dist/session-authorization.d.ts +38 -1
  111. package/dist/site-session-origin.d.ts +9 -0
  112. package/dist/transcription.d.ts +12 -1
  113. package/dist/turn-surface.d.ts +18 -0
  114. package/dist/user-presence.d.ts +32 -0
  115. package/package.json +18 -11
  116. package/src/access/external-actor-authority.ts +94 -0
  117. package/src/access/index.ts +1386 -44
  118. package/src/access/service-initiator.ts +50 -0
  119. package/src/application/api-integration-servers.ts +215 -0
  120. package/src/application/archived-session-imports.ts +161 -0
  121. package/src/application/composer-submit.ts +77 -0
  122. package/src/application/connect-authority.ts +77 -0
  123. package/src/application/connect-operation.ts +80 -0
  124. package/src/application/external-continuation.ts +129 -0
  125. package/src/application/external-identity-lifecycle.ts +48 -0
  126. package/src/application/external-link-work-admission.ts +90 -0
  127. package/src/application/external-workspace-members.ts +232 -0
  128. package/src/application/modal-native-worker-host-transport.ts +263 -0
  129. package/src/application/new-session-drafts.ts +222 -29
  130. package/src/application/sandbox-recovery.ts +78 -0
  131. package/src/application/session-commands.ts +430 -73
  132. package/src/application/session-mcp-credential-rotation.ts +217 -0
  133. package/src/application/session-tenancy.ts +445 -0
  134. package/src/application/user-resource-grants.ts +154 -0
  135. package/src/billing/limits.ts +103 -24
  136. package/src/canonical-human-identities.ts +22 -3
  137. package/src/codex-model-availability.ts +136 -0
  138. package/src/default-session-model.ts +656 -0
  139. package/src/dependencies.ts +97 -5
  140. package/src/developer-setup-credential-ceiling.ts +60 -0
  141. package/src/domain/agent-config-resolution.ts +148 -0
  142. package/src/domain/automations.ts +270 -0
  143. package/src/domain/capabilities.ts +244 -151
  144. package/src/domain/company-profile-agent-admin.ts +56 -0
  145. package/src/domain/connection-account-selection-error.ts +19 -0
  146. package/src/domain/connector-tool-permissions.ts +371 -0
  147. package/src/domain/direct-model-provider.ts +62 -0
  148. package/src/domain/editable-artifacts/agent-application.ts +61 -15
  149. package/src/domain/editable-artifacts/durable-export.ts +100 -5
  150. package/src/domain/editable-artifacts/in-memory.ts +6 -0
  151. package/src/domain/editable-artifacts/ports.ts +11 -0
  152. package/src/domain/editable-artifacts/service.ts +32 -1
  153. package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
  154. package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
  155. package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
  156. package/src/domain/environments.ts +25 -45
  157. package/src/domain/external-creation-attribution.ts +22 -0
  158. package/src/domain/file-owner.ts +67 -0
  159. package/src/domain/github-action-policies.ts +169 -0
  160. package/src/domain/github-repository-bindings.ts +295 -0
  161. package/src/domain/github-skill-source.ts +233 -0
  162. package/src/domain/governed-learning-slack-publication.ts +157 -0
  163. package/src/domain/insights-timing.ts +95 -0
  164. package/src/domain/insights-usage.ts +67 -0
  165. package/src/domain/insights.ts +270 -169
  166. package/src/domain/knowledge-files.ts +46 -0
  167. package/src/domain/knowledge-messages.test.ts +91 -0
  168. package/src/domain/knowledge-messages.ts +106 -0
  169. package/src/domain/knowledge-preparation.test.ts +276 -0
  170. package/src/domain/knowledge-preparation.ts +147 -0
  171. package/src/domain/knowledge-search.test.ts +561 -0
  172. package/src/domain/knowledge-search.ts +256 -0
  173. package/src/domain/knowledge.ts +133 -0
  174. package/src/domain/mcp-account-bindings.ts +171 -0
  175. package/src/domain/mcp-account-routes.ts +121 -0
  176. package/src/domain/native-mcp-connection-admission.ts +14 -0
  177. package/src/domain/organization-integration-catalog.ts +88 -0
  178. package/src/domain/organization-membership-lifecycle.ts +10 -0
  179. package/src/domain/personal-connection-delegations.ts +758 -115
  180. package/src/domain/pr-review.ts +687 -0
  181. package/src/domain/resources.ts +182 -25
  182. package/src/domain/scheduled-task-access.ts +1052 -0
  183. package/src/domain/scheduled-tasks.ts +1952 -215
  184. package/src/domain/session-connection-accounts.ts +67 -0
  185. package/src/domain/session-file-access.ts +47 -0
  186. package/src/domain/session-native-connection-replacement.ts +64 -0
  187. package/src/domain/session-start-timing.ts +55 -0
  188. package/src/domain/session-tool-policy.ts +443 -19
  189. package/src/domain/sessions.ts +3417 -619
  190. package/src/domain/skill-imports.ts +137 -17
  191. package/src/domain/skill-search.ts +221 -0
  192. package/src/domain/skills.ts +104 -0
  193. package/src/domain/slack-bot.ts +128 -3
  194. package/src/domain/workspace-members.ts +34 -2
  195. package/src/editable-artifact-live/application.ts +29 -4
  196. package/src/editable-artifact-live/ports.ts +0 -1
  197. package/src/editable-artifact-live/server.ts +167 -13
  198. package/src/editable-artifact-live/ticket.ts +87 -6
  199. package/src/editable-artifact-live/types.ts +16 -2
  200. package/src/editable-artifact-live/wire.ts +0 -10
  201. package/src/index.ts +69 -4
  202. package/src/managed-auth-session-sets.ts +371 -0
  203. package/src/managed-session.ts +520 -2
  204. package/src/model-catalog.ts +873 -0
  205. package/src/product-usage-metrics.ts +101 -0
  206. package/src/rigs/index.ts +179 -63
  207. package/src/rigs/provider-images.ts +2 -3
  208. package/src/sandbox/fleet.ts +471 -59
  209. package/src/sandbox/routing.ts +160 -33
  210. package/src/sandbox/runtime-settings.ts +281 -0
  211. package/src/session-authorization.ts +262 -78
  212. package/src/site-session-origin.ts +22 -0
  213. package/src/transcription.ts +13 -1
  214. package/src/turn-surface.ts +45 -0
  215. package/src/user-presence.ts +134 -0
  216. package/dist/chunk-6WKPWE5S.js.map +0 -1
  217. package/dist/chunk-IBOEYG6N.js +0 -34
  218. package/dist/chunk-IBOEYG6N.js.map +0 -1
  219. package/dist/chunk-JJU6I5XD.js.map +0 -1
  220. package/dist/chunk-NYPMQ7NO.js.map +0 -1
  221. package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
  222. package/dist/domain/packs.d.ts +0 -55
  223. package/src/domain/durable-learning-slack-publication.ts +0 -202
  224. package/src/domain/packs.ts +0 -556
@@ -0,0 +1,129 @@
1
+ import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
2
+ import type { Permission } from "@opengeni/contracts";
3
+ import {
4
+ ensureExternalIdentity,
5
+ resolveExternalIdentityLink,
6
+ getWorkspaceGrant,
7
+ lockActiveExternalOrganizationKeyAuthority,
8
+ getOrganizationApiKey,
9
+ requireWorkspace,
10
+ lockExternalWorkspaceMembershipLifecycle,
11
+ managedPersonalWorkspacePermissions,
12
+ nestedPostgresSqlState,
13
+ withWorkspaceSubjectRls,
14
+ type Database,
15
+ } from "@opengeni/db";
16
+ import {
17
+ hasPermission,
18
+ organizationWorkspaceInScope,
19
+ externalActorContinuationForAuthorization,
20
+ type AccessGrantAuthorization,
21
+ } from "../access";
22
+ import { HTTPException } from "hono/http-exception";
23
+
24
+ /** Capture verified request authority before asynchronous preflight. Invoke
25
+ * inside the canonical mutation transaction after its lifecycle locks, so a
26
+ * revoked key/generation rolls the write back without changing lock order. */
27
+ export function externalContinuationCommitAuthorizer(
28
+ authorization: AccessGrantAuthorization | undefined,
29
+ ): ((tx: Database) => Promise<void>) | undefined {
30
+ const continuation = authorization
31
+ ? externalActorContinuationForAuthorization(authorization)
32
+ : null;
33
+ if (!continuation || !authorization) return undefined;
34
+ const scope = {
35
+ accountId: authorization.grant.accountId,
36
+ workspaceId: authorization.grant.workspaceId,
37
+ subjectId: authorization.grant.subjectId,
38
+ };
39
+ const permissions = [...authorization.grant.permissions];
40
+ return async (tx) => {
41
+ try {
42
+ await requireExternalContinuationAuthority(tx, continuation, scope, permissions);
43
+ } catch (error) {
44
+ const denied =
45
+ nestedPostgresSqlState(error) === "42501" ||
46
+ (error instanceof Error && error.message === "External continuation authority unavailable");
47
+ throw new HTTPException(denied ? 403 : 503, {
48
+ message: denied ? "external authority changed" : "external authority is unavailable",
49
+ cause: error,
50
+ });
51
+ }
52
+ };
53
+ }
54
+
55
+ /** Caller verifies/decrypts its server-minted continuation before invoking.
56
+ * An open transaction retains lifecycle/key/identity locks through persistence.
57
+ * Historical creator audit alone must never enter this path as a credential. */
58
+ export async function requireExternalContinuationAuthority(
59
+ tx: Database,
60
+ raw: unknown,
61
+ scope: { accountId: string; workspaceId: string; subjectId: string },
62
+ permission: Permission | readonly Permission[],
63
+ ): Promise<void> {
64
+ const { actor, identity: reference } = ExternalActorContinuation.parse(raw);
65
+ const required = typeof permission === "string" ? [permission] : [...permission];
66
+ const deny = () => {
67
+ throw new Error("External continuation authority unavailable");
68
+ };
69
+ if (actor.accountId !== scope.accountId || actor.effectiveSubjectId !== scope.subjectId) deny();
70
+ await lockExternalWorkspaceMembershipLifecycle(tx, scope.accountId);
71
+ const authority = await lockActiveExternalOrganizationKeyAuthority(
72
+ tx,
73
+ scope.accountId,
74
+ actor.authenticatingApiKeyId,
75
+ );
76
+ const key = authority
77
+ ? await getOrganizationApiKey(tx, scope.accountId, actor.authenticatingApiKeyId)
78
+ : null;
79
+ if (
80
+ !authority ||
81
+ !key ||
82
+ !organizationWorkspaceInScope(key.workspaceScope ?? { kind: "all" }, scope.workspaceId) ||
83
+ required.some((value) => !hasPermission(authority.permissions, value, authority.permissionMode))
84
+ )
85
+ deny();
86
+ if (
87
+ key?.permissionMode === "explicit" &&
88
+ (await requireWorkspace(tx, scope.workspaceId)).kind !== "shared"
89
+ )
90
+ deny();
91
+ const identity = await ensureExternalIdentity(tx, { accountId: scope.accountId, ...reference });
92
+ if (
93
+ identity.id !== actor.externalIdentityId ||
94
+ identity.subjectId !== actor.externalSubjectId ||
95
+ identity.authorizationRevision !== actor.externalAuthorizationRevision
96
+ )
97
+ deny();
98
+ const linked =
99
+ actor.actingMode === "linked_native"
100
+ ? await resolveExternalIdentityLink(tx, {
101
+ identity,
102
+ linkId: actor.linkId!,
103
+ expectedRevision: actor.linkRevision!,
104
+ })
105
+ : null;
106
+ if (
107
+ actor.actingMode === "linked_native" &&
108
+ (!linked ||
109
+ linked.link.nativeSubjectId !== scope.subjectId ||
110
+ required.some((value) => !hasPermission(linked.link.permissions, value)))
111
+ )
112
+ deny();
113
+ if (actor.actingMode === "external" && scope.subjectId !== identity.subjectId) deny();
114
+ const grant =
115
+ scope.workspaceId === (linked?.personalWorkspaceId ?? identity.personalWorkspaceId)
116
+ ? {
117
+ accountId: identity.accountId,
118
+ permissions: managedPersonalWorkspacePermissions,
119
+ }
120
+ : await withWorkspaceSubjectRls(tx, scope.workspaceId, scope.subjectId, (db) =>
121
+ getWorkspaceGrant(db, scope.subjectId, scope.workspaceId),
122
+ );
123
+ if (
124
+ !grant ||
125
+ grant.accountId !== scope.accountId ||
126
+ required.some((value) => !hasPermission(grant.permissions, value))
127
+ )
128
+ deny();
129
+ }
@@ -0,0 +1,48 @@
1
+ import type { Context } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import { UpdateExternalIdentityMembershipRequest } from "@opengeni/contracts/external-identities";
4
+ import { updateOrganizationMember } from "@opengeni/db";
5
+ import {
6
+ accountScopedApiKeyWorkspaceAuthority,
7
+ requireAccessContext,
8
+ type AccessDeps,
9
+ } from "../access";
10
+
11
+ /** Host service administration is separate from asUser and native-cookie
12
+ * administration. The database rechecks the live key and exact external target
13
+ * under the canonical organization lifecycle fences, including receipt replay. */
14
+ export async function updateExternalIdentityMembershipForRequest(
15
+ context: Context,
16
+ deps: AccessDeps,
17
+ organizationId: string,
18
+ membershipId: string,
19
+ input: unknown,
20
+ ) {
21
+ const access = await requireAccessContext(context, deps);
22
+ const service = accountScopedApiKeyWorkspaceAuthority(access);
23
+ // The workspace projection deliberately excludes account:admin. Its opaque
24
+ // proof establishes service provenance; the exact account grant supplies
25
+ // this account-level permission, independently rechecked by the database.
26
+ const account = access.accountGrants.find(
27
+ (grant) => grant.accountId === organizationId && grant.subjectId === access.subjectId,
28
+ );
29
+ if (
30
+ !service ||
31
+ service.accountId !== organizationId ||
32
+ !account?.permissions.includes("account:admin")
33
+ ) {
34
+ throw new HTTPException(403, {
35
+ message: "external identity administration requires an organization service key",
36
+ });
37
+ }
38
+ const parsed = UpdateExternalIdentityMembershipRequest.safeParse(input);
39
+ if (!parsed.success)
40
+ throw new HTTPException(422, { message: "invalid external identity transition" });
41
+ return updateOrganizationMember(deps.db, {
42
+ organizationId,
43
+ membershipId,
44
+ actorSubjectId: access.subjectId,
45
+ operationId: parsed.data.operationId,
46
+ transition: parsed.data,
47
+ });
48
+ }
@@ -0,0 +1,90 @@
1
+ import type { ScheduledTask } from "@opengeni/contracts";
2
+ import { ExternalLinkWorkSnapshot } from "@opengeni/contracts/external-identities";
3
+ import {
4
+ captureExternalLinkTurnAuthority,
5
+ captureExternalLinkTaskAuthority,
6
+ getExternalLinkTurnSnapshot,
7
+ getSessionTurnForAttempt,
8
+ type Database,
9
+ } from "@opengeni/db";
10
+ import {
11
+ externalActorContinuationForAuthorization,
12
+ hasVerifiedOwningUserAuthorization,
13
+ type AccessGrantAuthorization,
14
+ } from "../access";
15
+ import { externalContinuationCommitAuthorizer } from "./external-continuation";
16
+
17
+ function snapshotFor(authorization: AccessGrantAuthorization | undefined) {
18
+ const continuation = authorization
19
+ ? externalActorContinuationForAuthorization(authorization)
20
+ : null;
21
+ return continuation?.actor.actingMode === "linked_native"
22
+ ? ExternalLinkWorkSnapshot.parse({
23
+ ...continuation,
24
+ permissions: [...authorization!.grant.permissions],
25
+ ...(authorization!.grant.permissionMode === "explicit"
26
+ ? { permissionMode: "explicit" }
27
+ : {}),
28
+ })
29
+ : null;
30
+ }
31
+
32
+ /** Closure is created only from the access resolver's object-identity proof.
33
+ * JSON metadata and an ordinary named native subject can never enter it. */
34
+ export function prepareExternalLinkTurnAdmission(
35
+ authorization: AccessGrantAuthorization | undefined,
36
+ ) {
37
+ const snapshot = snapshotFor(authorization);
38
+ if (!snapshot || !authorization) return undefined;
39
+ const workspaceId = authorization.grant.workspaceId;
40
+ const accountId = authorization.grant.accountId;
41
+ const commit = externalContinuationCommitAuthorizer(authorization)!;
42
+ return async (tx: Database, sessionId: string, turnId: string) => {
43
+ await commit(tx);
44
+ await captureExternalLinkTurnAuthority(tx, {
45
+ accountId,
46
+ workspaceId,
47
+ sessionId,
48
+ turnId,
49
+ snapshot,
50
+ });
51
+ };
52
+ }
53
+
54
+ export function prepareExternalLinkTaskAdmission(
55
+ authorization: AccessGrantAuthorization | undefined,
56
+ actor?: {
57
+ sessionId: string;
58
+ turnId: string;
59
+ attemptId: string;
60
+ executionGeneration: number;
61
+ } | null,
62
+ ): ((tx: Database, task: ScheduledTask) => Promise<void>) | undefined {
63
+ const snapshot = snapshotFor(authorization);
64
+ const commit = externalContinuationCommitAuthorizer(authorization);
65
+ if (snapshot)
66
+ return async (tx, task) => {
67
+ await commit?.(tx);
68
+ await captureExternalLinkTaskAuthority(tx, task, snapshot);
69
+ };
70
+ if (!actor) {
71
+ // A fresh, explicitly authenticated owning-user revision uses that user's
72
+ // own lane. It does not inherit a previous editor's link by accident.
73
+ // Service lifecycle edits and internal promotion still clone old provenance.
74
+ return authorization && hasVerifiedOwningUserAuthorization(authorization)
75
+ ? async () => {}
76
+ : undefined;
77
+ }
78
+ return async (tx, task) => {
79
+ const turn = await getSessionTurnForAttempt(
80
+ tx,
81
+ task.workspaceId,
82
+ actor.sessionId,
83
+ actor.attemptId,
84
+ );
85
+ if (!turn || turn.id !== actor.turnId || turn.executionGeneration !== actor.executionGeneration)
86
+ throw new Error("Linked task creator attempt is no longer active");
87
+ const inherited = await getExternalLinkTurnSnapshot(tx, task, actor.turnId);
88
+ if (inherited) await captureExternalLinkTaskAuthority(tx, task, inherited);
89
+ };
90
+ }
@@ -0,0 +1,232 @@
1
+ import type { Context } from "hono";
2
+ import { HTTPException } from "hono/http-exception";
3
+ import {
4
+ AddExternalWorkspaceMemberRequest,
5
+ ExternalIdentityReference,
6
+ CancelExternalWorkspaceMemberGrantRequest,
7
+ UpdateExternalWorkspaceMemberRequest,
8
+ type ExternalIdentity,
9
+ } from "@opengeni/contracts/external-identities";
10
+ import {
11
+ ensureExternalIdentity,
12
+ grantWorkspaceAccess,
13
+ listWorkspaceMembers,
14
+ lockExternalWorkspaceMembershipLifecycle,
15
+ requireWorkspace,
16
+ setRlsContext,
17
+ withWorkspaceSubjectRls,
18
+ addExternalWorkspaceMemberOperation,
19
+ cancelExternalWorkspaceMemberGrant,
20
+ updateExternalWorkspaceMemberOperation,
21
+ lookupExternalIdentity,
22
+ nestedPostgresSqlState,
23
+ } from "@opengeni/db";
24
+ import { organizationMembershipHttpStatus } from "../domain/organization-membership-lifecycle";
25
+ import {
26
+ accountScopedApiKeyWorkspaceAuthority,
27
+ hasPermission,
28
+ requireAccessContext,
29
+ requireApiKeyDelegationContext,
30
+ requireFreshAccessGrant,
31
+ type AccessDeps,
32
+ } from "../access";
33
+
34
+ /** Explicit host onboarding. Ordinary asUser requests never call this
35
+ * operation; their first-use membership is `provisionExternalMemberOnFirstUse`
36
+ * in `../access`, under the same key authority and lifecycle fence.
37
+ * Existing memberships are not overwritten, including reduced permissions. */
38
+ export async function addExternalWorkspaceMemberForRequest(
39
+ c: Context,
40
+ deps: AccessDeps,
41
+ workspaceId: string,
42
+ input: unknown,
43
+ ): Promise<ExternalIdentity> {
44
+ const payload = AddExternalWorkspaceMemberRequest.parse(input);
45
+ const context = await requireAccessContext(c, deps);
46
+ requireApiKeyDelegationContext(context, payload.permissions);
47
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
48
+ if (!authority)
49
+ throw new HTTPException(403, {
50
+ message: "external onboarding requires an organization service key",
51
+ });
52
+ const grant = await requireFreshAccessGrant(c, deps, workspaceId, "members:manage");
53
+ if (
54
+ grant.accountId !== authority.accountId ||
55
+ payload.permissions.some((permission) => !hasPermission(grant.permissions, permission))
56
+ ) {
57
+ throw new HTTPException(403, { message: "membership exceeds key authority" });
58
+ }
59
+ if (payload.operationId) {
60
+ try {
61
+ return await addExternalWorkspaceMemberOperation(
62
+ deps.db,
63
+ {
64
+ organizationId: authority.accountId,
65
+ workspaceId,
66
+ actorSubjectId: grant.subjectId,
67
+ },
68
+ { ...payload, operationId: payload.operationId },
69
+ );
70
+ } catch (error) {
71
+ rethrowExternalWorkspaceOperation(error);
72
+ }
73
+ }
74
+ return withWorkspaceSubjectRls(deps.db, workspaceId, grant.subjectId, async (tx) => {
75
+ await lockExternalWorkspaceMembershipLifecycle(tx, grant.accountId);
76
+ const live = await requireFreshAccessGrant(
77
+ c,
78
+ { ...deps, db: tx },
79
+ workspaceId,
80
+ "members:manage",
81
+ );
82
+ if (
83
+ live.accountId !== grant.accountId ||
84
+ live.subjectId !== grant.subjectId ||
85
+ payload.permissions.some((permission) => !hasPermission(live.permissions, permission))
86
+ ) {
87
+ throw new HTTPException(403, { message: "membership authority changed" });
88
+ }
89
+ const workspace = await requireWorkspace(tx, workspaceId);
90
+ if (workspace.kind !== "shared" || workspace.accountId !== authority.accountId)
91
+ throw new HTTPException(403, {
92
+ message: "external onboarding requires a shared organization workspace",
93
+ });
94
+ const identity = await ensureExternalIdentity(tx, {
95
+ accountId: authority.accountId,
96
+ ...payload.identity,
97
+ });
98
+ await setRlsContext(tx, { accountId: authority.accountId, workspaceId });
99
+ const existing = (await listWorkspaceMembers(tx, workspaceId)).find(
100
+ (member) => member.subjectId === identity.subjectId,
101
+ );
102
+ const permissions = [...new Set(payload.permissions)];
103
+ if (existing) {
104
+ if (
105
+ existing.permissions.length !== permissions.length ||
106
+ existing.permissions.some((permission) => !permissions.includes(permission))
107
+ ) {
108
+ throw new HTTPException(409, {
109
+ message: "existing membership differs; onboarding does not overwrite permissions",
110
+ });
111
+ }
112
+ return identity;
113
+ }
114
+ await grantWorkspaceAccess(tx, {
115
+ accountId: authority.accountId,
116
+ workspaceId,
117
+ subjectId: identity.subjectId,
118
+ role: "member",
119
+ permissions,
120
+ });
121
+ return identity;
122
+ });
123
+ }
124
+
125
+ function rethrowExternalWorkspaceOperation(error: unknown): never {
126
+ const status = organizationMembershipHttpStatus(nestedPostgresSqlState(error));
127
+ if (status)
128
+ throw new HTTPException(status, {
129
+ message:
130
+ status === 409
131
+ ? "External membership operation changed or was cancelled"
132
+ : "External membership operation is not available",
133
+ });
134
+ throw error;
135
+ }
136
+
137
+ async function externalService(
138
+ c: Context,
139
+ deps: AccessDeps,
140
+ organizationId: string,
141
+ workspaceId?: string,
142
+ ) {
143
+ const context = await requireAccessContext(c, deps);
144
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
145
+ if (
146
+ !authority ||
147
+ authority.accountId !== organizationId ||
148
+ !hasPermission(authority.permissions, "members:manage")
149
+ ) {
150
+ throw new HTTPException(403, {
151
+ message: "External membership operation requires an organization service key",
152
+ });
153
+ }
154
+ if (workspaceId !== undefined) {
155
+ const grant = await requireFreshAccessGrant(c, deps, workspaceId, "members:manage");
156
+ if (grant.accountId !== organizationId)
157
+ throw new HTTPException(403, { message: "External membership workspace authority changed" });
158
+ }
159
+ return { organizationId, actorSubjectId: context.subjectId };
160
+ }
161
+
162
+ export async function lookupExternalIdentityForRequest(
163
+ c: Context,
164
+ deps: AccessDeps,
165
+ organizationId: string,
166
+ input: unknown,
167
+ ) {
168
+ const service = await externalService(c, deps, organizationId);
169
+ const identity = ExternalIdentityReference.safeParse(input);
170
+ if (!identity.success)
171
+ throw new HTTPException(422, { message: "Invalid external identity reference" });
172
+ try {
173
+ return await lookupExternalIdentity(deps.db, service, identity.data);
174
+ } catch (error) {
175
+ rethrowExternalWorkspaceOperation(error);
176
+ }
177
+ }
178
+
179
+ export async function cancelExternalWorkspaceMemberGrantForRequest(
180
+ c: Context,
181
+ deps: AccessDeps,
182
+ organizationId: string,
183
+ workspaceId: string,
184
+ membershipId: string,
185
+ input: unknown,
186
+ ) {
187
+ const service = await externalService(c, deps, organizationId, workspaceId);
188
+ const request = CancelExternalWorkspaceMemberGrantRequest.safeParse(input);
189
+ if (!request.success)
190
+ throw new HTTPException(422, { message: "Invalid external grant cancellation" });
191
+ try {
192
+ return await cancelExternalWorkspaceMemberGrant(
193
+ deps.db,
194
+ { ...service, workspaceId, membershipId },
195
+ request.data,
196
+ );
197
+ } catch (error) {
198
+ rethrowExternalWorkspaceOperation(error);
199
+ }
200
+ }
201
+
202
+ /** Keyed permission change for an existing external member. Widening only
203
+ * rewrites the set; narrowing also advances the member's authorization
204
+ * revision so live authority re-checks. Never cancels or tears down work. */
205
+ export async function updateExternalWorkspaceMemberForRequest(
206
+ c: Context,
207
+ deps: AccessDeps,
208
+ organizationId: string,
209
+ workspaceId: string,
210
+ membershipId: string,
211
+ input: unknown,
212
+ ) {
213
+ const service = await externalService(c, deps, organizationId, workspaceId);
214
+ const request = UpdateExternalWorkspaceMemberRequest.safeParse(input);
215
+ if (!request.success)
216
+ throw new HTTPException(422, {
217
+ message: `Invalid external member update: ${request.error.issues
218
+ .slice(0, 5)
219
+ .map((issue) => `${issue.path.map(String).join(".") || "request"}: ${issue.message}`)
220
+ .join("; ")}`,
221
+ });
222
+ requireApiKeyDelegationContext(await requireAccessContext(c, deps), request.data.permissions);
223
+ try {
224
+ return await updateExternalWorkspaceMemberOperation(
225
+ deps.db,
226
+ { ...service, workspaceId, membershipId },
227
+ request.data,
228
+ );
229
+ } catch (error) {
230
+ rethrowExternalWorkspaceOperation(error);
231
+ }
232
+ }