@opengeni/core 0.28.1 → 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 +18812 -6833
  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
@@ -1,33 +1,614 @@
1
1
  import { resolveFirstPartyDelegationSecret, type Settings } from "@opengeni/config";
2
+ import {
3
+ ExternalActorSelection,
4
+ ExternalActorAttribution,
5
+ type ExternalActorContinuation,
6
+ type ExternalIdentity,
7
+ } from "@opengeni/contracts/external-identities";
2
8
  import {
3
9
  verifyDelegatedAccessToken,
10
+ DEVELOPER_SETUP_API_KEY_PRESET,
11
+ organizationAccessPresetPermissions,
4
12
  type AccountGrant,
5
13
  type AccessContext,
6
14
  type AccessGrant,
7
- type Permission,
15
+ type OrganizationApiKeyAccess,
16
+ OrganizationWorkspaceScope,
17
+ OPENGENI_USER_ACTIVITY_ACTIVE,
18
+ OPENGENI_USER_ACTIVITY_HEADER,
19
+ Permission,
20
+ type Workspace,
8
21
  } from "@opengeni/contracts";
9
22
  import {
10
23
  bootstrapWorkspace,
11
24
  ensureManagedAccessForUser,
25
+ getManagedUserProfilesByIds,
26
+ ensureExternalIdentity,
27
+ ensureExternalWorkspaceMemberOnFirstUse,
28
+ USER_ISOLATION_WORKSPACE_SOURCE_PREFIX,
29
+ lockExternalWorkspaceMembershipLifecycle,
30
+ resolveExternalIdentityLink,
31
+ managedPersonalWorkspacePermissions,
32
+ nestedPostgresSqlState,
33
+ withWorkspaceSubjectRls,
34
+ withAccountRls,
35
+ listWorkspacesForSubject,
12
36
  findActiveApiKeyByHash,
13
37
  getWorkspaceGrant,
14
38
  requireWorkspace,
39
+ resolveNamedManagedPersonalWorkspaceGrant,
15
40
  type Database,
16
41
  } from "@opengeni/db";
17
42
  import type { Context } from "hono";
18
43
  import { HTTPException } from "hono/http-exception";
44
+ import { z } from "zod";
19
45
  import type { ManagedAuth } from "../managed-auth-type";
20
46
  import { getManagedSession } from "../managed-session";
47
+ import type { ManagedAuthSessionAdapter } from "../managed-auth-session-sets";
48
+ import type { UserPresenceRecorder } from "../user-presence";
49
+ import { serviceInitiatorFromHeaders } from "./service-initiator";
21
50
 
22
51
  const bearerPrefix = "Bearer ";
23
52
  const accessContextByRequest = new WeakMap<Request, Promise<AccessContext | null>>();
53
+ const accessResolvedRequests = new WeakSet<Request>();
54
+ const developerSetupApiKeyContexts = new WeakSet<AccessContext>();
55
+ const developerSetupAuthorizations = new WeakMap<AccessGrantAuthorization, AccessGrant>();
56
+ const developerSetupGrants = new WeakSet<AccessGrant>();
57
+ // Request-local permission semantics. The durable grant carries permissionMode;
58
+ // callers of the existing array API cannot accidentally expand a policy admin.
59
+ const explicitPermissionSets = new WeakSet<readonly Permission[]>();
60
+ function explicitPermissions(permissions: Permission[]): Permission[] {
61
+ explicitPermissionSets.add(permissions);
62
+ return permissions;
63
+ }
64
+
65
+ /** Only canonical authentication can prove setup-key provenance. */
66
+ export function isDeveloperSetupApiKeyContext(context: AccessContext): boolean {
67
+ return (
68
+ developerSetupApiKeyContexts.has(context) &&
69
+ accountScopedApiKeyWorkspaceAuthority(context) !== null
70
+ );
71
+ }
72
+
73
+ /** Provenance from canonical raw-key or verified restricted-token authentication. */
74
+ export function isDeveloperSetupAuthorization(
75
+ authorization: AccessGrantAuthorization | undefined,
76
+ ): boolean {
77
+ return (
78
+ authorization !== undefined &&
79
+ developerSetupAuthorizations.get(authorization) === authorization.grant
80
+ );
81
+ }
82
+
83
+ /** Exact grant objects resolved under authenticated setup-only provenance. */
84
+ export function isDeveloperSetupGrant(grant: AccessGrant): boolean {
85
+ return developerSetupGrants.has(grant);
86
+ }
87
+
88
+ /** Setup-derived credentials cannot carry literal organization or key authority. */
89
+ export function isDeveloperSetupDelegatedPermissionAllowed(permission: Permission): boolean {
90
+ return (
91
+ permission !== "secrets:read" &&
92
+ permission !== "api_keys:manage" &&
93
+ !accountScopedApiKeyWorkspaceExcludedPermissions.has(permission)
94
+ );
95
+ }
96
+
97
+ /** Canonical authentication provenance, including the backing key of asUser. */
98
+ export function requireApiKeyManagementContext(context: AccessContext): void {
99
+ if (developerSetupApiKeyContexts.has(context)) {
100
+ throw new HTTPException(403, { message: "Developer setup keys cannot manage API keys" });
101
+ }
102
+ }
103
+
104
+ /** Membership is not an escape hatch for minting durable credentials. */
105
+ export function requireApiKeyDelegationContext(
106
+ context: AccessContext,
107
+ permissions: Permission[],
108
+ ): void {
109
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
110
+ const external = externalActorContexts.get(context);
111
+ const ceiling = authority ?? external;
112
+ if (
113
+ ceiling?.permissionMode === "explicit" &&
114
+ permissions.some((permission) => !ceiling.permissions.includes(permission))
115
+ ) {
116
+ throw new HTTPException(403, {
117
+ message: "cannot delegate a permission outside the organization key policy",
118
+ });
119
+ }
120
+ if (
121
+ ceiling?.permissionMode === "explicit" &&
122
+ permissions.includes("workspace:admin") &&
123
+ organizationAccessPresetPermissions("full").some(
124
+ (permission) =>
125
+ !accountScopedApiKeyWorkspaceExcludedPermissions.has(permission) &&
126
+ !ceiling.permissions.includes(permission),
127
+ )
128
+ ) {
129
+ throw new HTTPException(403, {
130
+ message: "cannot delegate a legacy workspace-admin wildcard from a custom policy",
131
+ });
132
+ }
133
+ if (
134
+ (hasPermission(permissions, "api_keys:manage") ||
135
+ hasPermission(permissions, "members:manage")) &&
136
+ developerSetupApiKeyContexts.has(context)
137
+ ) {
138
+ throw new HTTPException(403, {
139
+ message: "Setup credentials cannot delegate API key management",
140
+ });
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Contexts that were authenticated by a verified canonical managed cookie
146
+ * (Better Auth session), stamped at the single branch of
147
+ * {@link resolveAccessContext} that performs that verification.
148
+ *
149
+ * This is provenance — HOW the request authenticated — not a shape check on a
150
+ * value. It cannot be forged by a token claim, and it FAILS CLOSED by default:
151
+ * membership is only ever added at that one branch, so every other path
152
+ * (delegated bearer, API key, local bootstrap, configured key, and any path
153
+ * added later) is absent from the set without anyone maintaining a list. A
154
+ * request that never resolves an access context has no context to test at all.
155
+ *
156
+ * Keyed on the resolved `AccessContext` object identity, deliberately NOT on
157
+ * the `Request`: a reused, wrapped, or retried request object can therefore
158
+ * never carry a stale positive into a differently-authenticated resolution, and
159
+ * the non-cached `requireFreshAccessGrant` path is stamped correctly too. Each
160
+ * resolution produces a fresh context object, so the mark travels with exactly
161
+ * the value the cookie branch produced.
162
+ */
163
+ const canonicalManagedCookieContexts = new WeakSet<AccessContext>();
164
+ /** Downstream legacy credentials cannot encode a partially selected admin. */
165
+ export function requireExplicitPermissionDelegation(
166
+ grant: Pick<AccessGrant, "permissions" | "permissionMode">,
167
+ requested: Permission[],
168
+ ): void {
169
+ if (
170
+ grant.permissionMode === "explicit" &&
171
+ requested.some((permission) => !grant.permissions.includes(permission))
172
+ ) {
173
+ throw new HTTPException(403, {
174
+ message: "cannot delegate a permission outside the organization key policy",
175
+ });
176
+ }
177
+ if (
178
+ grant.permissionMode === "explicit" &&
179
+ requested.includes("workspace:admin") &&
180
+ organizationAccessPresetPermissions("full").some(
181
+ (permission) =>
182
+ !accountScopedApiKeyWorkspaceExcludedPermissions.has(permission) &&
183
+ !grant.permissions.includes(permission),
184
+ )
185
+ ) {
186
+ throw new HTTPException(403, {
187
+ message: "cannot delegate a legacy workspace-admin wildcard from a custom policy",
188
+ });
189
+ }
190
+ }
191
+ const canonicalLocalHumanContexts = new WeakSet<AccessContext>();
192
+
193
+ export type DelegatedHumanAuthorization = Readonly<{
194
+ organizationId: string;
195
+ /** Exact native person subject, never an external identity or service. */
196
+ subjectId: string;
197
+ permissions: Permission[];
198
+ workspaceScope: OrganizationWorkspaceScope;
199
+ }>;
200
+
201
+ const delegatedHumanAuthorizationSchema = z.object({
202
+ organizationId: z.string().uuid(),
203
+ subjectId: z
204
+ .string()
205
+ .max(1024)
206
+ .regex(/^user:[^\s\u0000-\u001f\u007f]+$/),
207
+ permissions: z.array(Permission),
208
+ workspaceScope: OrganizationWorkspaceScope,
209
+ });
210
+
211
+ const delegatedHumanRequests = new WeakMap<Request, DelegatedHumanAuthorization>();
212
+ const delegatedHumanContexts = new WeakMap<AccessContext, DelegatedHumanAuthorization>();
213
+ const delegatedHumanProfileSchema = z.object({
214
+ id: z.string(),
215
+ name: z.string().nullable(),
216
+ email: z.string(),
217
+ emailVerified: z.boolean().optional(),
218
+ });
219
+
220
+ export type VerifiedDelegatedHumanContext = Readonly<{
221
+ /** Native identity projection only, never a browser/auth session. Absence of
222
+ * emailVerified means the native lookup did not prove email verification. */
223
+ user: Readonly<z.infer<typeof delegatedHumanProfileSchema>>;
224
+ subjectId: string;
225
+ authorization: DelegatedHumanAuthorization;
226
+ context: AccessContext;
227
+ }>;
228
+
229
+ const delegatedHumanProfiles = new WeakMap<AccessContext, VerifiedDelegatedHumanContext["user"]>();
230
+ const verifiedDelegatedHumanAuthorizations = new WeakMap<
231
+ AccessGrantAuthorization,
232
+ {
233
+ proof: DelegatedHumanAuthorization;
234
+ grant: AccessGrant;
235
+ accountGrant: AccountGrant;
236
+ }
237
+ >();
238
+
239
+ /**
240
+ * TRUSTED OAUTH VERIFIED DISPATCH ONLY. Before each stamp the in-process caller
241
+ * must validate the live OAuth grant, revocation, exact native person subject,
242
+ * organization, permission ceilings and workspace scope. This API does not
243
+ * verify OAuth and must NEVER be called with headers, metadata, token claims,
244
+ * a previous request's proof, or other unverified caller input.
245
+ *
246
+ * Stamp the exact raw Request that the actions dispatcher will pass to routes,
247
+ * before any access resolution. Cloning/wrapping a Request does not copy proof.
248
+ * The resolver reloads the real native person's live access on this request
249
+ * (and on fresh reauthorization); no session or cached grant is fabricated or
250
+ * transplanted. This proof never becomes managed-cookie/person-present proof.
251
+ */
252
+ export function stampDelegatedHumanAuthorization(
253
+ request: Request,
254
+ input: DelegatedHumanAuthorization,
255
+ ): void {
256
+ if (delegatedHumanRequests.has(request) || accessResolvedRequests.has(request)) {
257
+ throw new HTTPException(403, { message: "request access authorization already resolved" });
258
+ }
259
+ const parsed = delegatedHumanAuthorizationSchema.safeParse(input);
260
+ if (!parsed.success) {
261
+ throw new HTTPException(403, { message: "invalid delegated human authorization" });
262
+ }
263
+ const verified = parsed.data;
264
+ const workspaceScope: DelegatedHumanAuthorization["workspaceScope"] =
265
+ verified.workspaceScope.kind === "all"
266
+ ? { kind: "all" }
267
+ : { kind: "selected", workspaceIds: [...verified.workspaceScope.workspaceIds] };
268
+ if (workspaceScope.kind === "selected") Object.freeze(workspaceScope.workspaceIds);
269
+ Object.freeze(workspaceScope);
270
+ const proof: DelegatedHumanAuthorization = {
271
+ organizationId: verified.organizationId,
272
+ subjectId: verified.subjectId,
273
+ permissions: [...new Set(verified.permissions)],
274
+ workspaceScope,
275
+ };
276
+ Object.freeze(proof.permissions);
277
+ delegatedHumanRequests.set(request, Object.freeze(proof));
278
+ }
279
+
280
+ /** Immutable verified dispatch bounds for this exact raw Request, not a grant. */
281
+ export function verifiedDelegatedHumanAuthorizationForRequest(
282
+ request: Request,
283
+ ): DelegatedHumanAuthorization | null {
284
+ return delegatedHumanRequests.get(request) ?? null;
285
+ }
286
+
287
+ /**
288
+ * Resolve the exact verified OAuth-dispatch person and constrained live native
289
+ * access for this raw Request. Unstamped requests (even browser cookies) fail
290
+ * closed; callers must not fall back to a service or subject-shaped identity.
291
+ * Uses the same request-local native resolution as requireAccessContext.
292
+ *
293
+ * No session, session id or managed-cookie proof is created. emailVerified is
294
+ * forwarded only when the native profile lookup supplies it; missing is not
295
+ * verified and must never be filled from OAuth claims, headers or metadata.
296
+ */
297
+ export async function requireVerifiedDelegatedHumanContext(
298
+ c: Context,
299
+ deps: AccessDeps,
300
+ ): Promise<VerifiedDelegatedHumanContext> {
301
+ const authorization = verifiedDelegatedHumanAuthorizationForRequest(c.req.raw);
302
+ if (!authorization) {
303
+ throw new HTTPException(401, { message: "verified delegated human authorization required" });
304
+ }
305
+ const context = await requireAccessContext(c, deps);
306
+ const user = delegatedHumanProfiles.get(context);
307
+ if (delegatedHumanContexts.get(context) !== authorization || !user) {
308
+ throw new HTTPException(403, { message: "delegated native person authority is unavailable" });
309
+ }
310
+ return Object.freeze({ user, subjectId: context.subjectId, authorization, context });
311
+ }
312
+
313
+ /** Exact resolved owning-person authorization; clones and service claims fail.
314
+ * Supplying the raw Request additionally binds proof to that exact dispatch,
315
+ * not another request with the same native person and authorization bounds. */
316
+ export function isVerifiedDelegatedHumanAuthorization(
317
+ authorization: AccessGrantAuthorization,
318
+ request?: Request,
319
+ ): boolean {
320
+ const verified = verifiedDelegatedHumanAuthorizations.get(authorization);
321
+ return Boolean(
322
+ verified &&
323
+ (request === undefined || verified.proof === delegatedHumanRequests.get(request)) &&
324
+ authorization.contextIntegrity &&
325
+ !authorization.canonicalManagedHumanSession &&
326
+ !authorization.canonicalLocalHumanSession &&
327
+ authorization.grant === verified.grant &&
328
+ authorization.accountGrant === verified.accountGrant &&
329
+ authorization.authenticatedSubjectId === verified.proof.subjectId &&
330
+ verified.grant.subjectId === verified.proof.subjectId &&
331
+ verified.grant.accountId === verified.proof.organizationId &&
332
+ delegatedHumanWorkspaceAllowed(verified.proof, verified.grant.workspaceId),
333
+ );
334
+ }
335
+
336
+ function delegatedHumanWorkspaceAllowed(
337
+ proof: DelegatedHumanAuthorization,
338
+ workspaceId: string,
339
+ ): boolean {
340
+ return (
341
+ proof.workspaceScope.kind === "all" || proof.workspaceScope.workspaceIds.includes(workspaceId)
342
+ );
343
+ }
344
+ const externalActorContexts = new WeakMap<
345
+ AccessContext,
346
+ {
347
+ identity: ExternalIdentity;
348
+ keyId: string;
349
+ permissions: Permission[];
350
+ workspaceScope: OrganizationWorkspaceScope;
351
+ permissionMode: "legacy" | "explicit";
352
+ linked?: NonNullable<Awaited<ReturnType<typeof resolveExternalIdentityLink>>>;
353
+ /**
354
+ * Plain external mode (no native link, no service-initiator attribution):
355
+ * the only lane where a missing shared-workspace membership may be created
356
+ * on first use. See {@link provisionExternalMemberOnFirstUse}.
357
+ */
358
+ firstUseMembership: boolean;
359
+ }
360
+ >();
361
+
362
+ /**
363
+ * Default permissions for a membership created on an external user's first
364
+ * request. Keep equal to `CONVERSATION_PERMISSIONS` in
365
+ * `packages/sdk/src/tenant-workspaces.ts`: conversation use only, no admin.
366
+ */
367
+ export const EXTERNAL_FIRST_USE_MEMBER_PERMISSIONS: readonly Permission[] = [
368
+ "workspace:read",
369
+ "sessions:create",
370
+ "sessions:read",
371
+ "sessions:control",
372
+ "files:upload",
373
+ "files:read",
374
+ "mcp_servers:attach",
375
+ ];
376
+ function attributionForExternalContext(context: AccessContext): ExternalActorAttribution {
377
+ const external = externalActorContexts.get(context);
378
+ if (!external) throw new Error("Verified external context required");
379
+ return ExternalActorAttribution.parse({
380
+ accountId: external.identity.accountId,
381
+ authenticatingApiKeyId: external.keyId,
382
+ externalIdentityId: external.identity.id,
383
+ externalSubjectId: external.identity.subjectId,
384
+ externalAuthorizationRevision: external.identity.authorizationRevision,
385
+ effectiveSubjectId: context.subjectId,
386
+ actingMode: external.linked ? "linked_native" : "external",
387
+ ...(external.linked
388
+ ? { linkId: external.linked.link.id, linkRevision: external.linked.link.revision }
389
+ : {}),
390
+ });
391
+ }
392
+ const resolvedAccessGrantAuthorizations = new WeakSet<object>();
393
+ const verifiedExternalAuthorizations = new WeakMap<
394
+ AccessGrantAuthorization,
395
+ {
396
+ grant: AccessGrant;
397
+ workspaceId: string;
398
+ workspaceSettingsCeiling: boolean;
399
+ identityReference: { externalId: string; source: string };
400
+ attribution: ExternalActorAttribution;
401
+ }
402
+ >();
403
+
404
+ /** Creation audit only, never an authentication or delegation token. Metadata
405
+ * claiming to be external is insufficient; the exact resolver proof is needed. */
406
+ export function externalAttributionForAuthorization(
407
+ authorization: AccessGrantAuthorization | undefined,
408
+ grant: AccessGrant,
409
+ ): ExternalActorAttribution | null {
410
+ if (!authorization || authorization.grant !== grant) return null;
411
+ const verified = verifiedExternalAuthorizations.get(authorization);
412
+ if (
413
+ !verified ||
414
+ verified.grant !== grant ||
415
+ verified.workspaceId !== grant.workspaceId ||
416
+ verified.attribution.accountId !== grant.accountId ||
417
+ verified.attribution.effectiveSubjectId !== grant.subjectId
418
+ )
419
+ return null;
420
+ return structuredClone(verified.attribution);
421
+ }
422
+
423
+ export function externalActorContinuationForAuthorization(
424
+ authorization: AccessGrantAuthorization,
425
+ ): ExternalActorContinuation | null {
426
+ const actor = externalAttributionForAuthorization(authorization, authorization.grant);
427
+ const verified = verifiedExternalAuthorizations.get(authorization);
428
+ return actor && verified ? { actor, identity: { ...verified.identityReference } } : null;
429
+ }
430
+
431
+ /** Dedicated owning-user proof. External/OAuth admission never sets the native
432
+ * cookie stamp. The resource/session layer still checks the exact owner. */
433
+ export function hasVerifiedOwningUserAuthorization(
434
+ authorization: AccessGrantAuthorization,
435
+ ): boolean {
436
+ if (
437
+ !authorization.contextIntegrity ||
438
+ authorization.authenticatedSubjectId !== authorization.grant.subjectId
439
+ )
440
+ return false;
441
+ return (
442
+ authorization.canonicalManagedHumanSession ||
443
+ externalAttributionForAuthorization(authorization, authorization.grant) !== null ||
444
+ isVerifiedDelegatedHumanAuthorization(authorization)
445
+ );
446
+ }
447
+ const accountScopedApiKeyContexts = new WeakMap<
448
+ AccessContext,
449
+ Readonly<{
450
+ accountId: string;
451
+ permissions: readonly Permission[];
452
+ workspaceScope: OrganizationWorkspaceScope;
453
+ permissionMode: "legacy" | "explicit";
454
+ }>
455
+ >();
456
+ const apiKeyServiceContexts = new WeakMap<
457
+ AccessContext,
458
+ Pick<AccessGrant, "serviceInitiator" | "serviceInitiatorContext">
459
+ >();
460
+ const verifiedOrganizationServiceAuthorizations = new WeakMap<
461
+ AccessGrantAuthorization,
462
+ AccessGrant
463
+ >();
464
+
465
+ /** Request-local organization service-key proof, not a principal-kind claim. */
466
+ export function isVerifiedOrganizationServiceAuthorization(
467
+ authorization: AccessGrantAuthorization,
468
+ ): boolean {
469
+ return (
470
+ verifiedOrganizationServiceAuthorizations.has(authorization) &&
471
+ verifiedOrganizationServiceAuthorizations.get(authorization) === authorization.grant
472
+ );
473
+ }
474
+
475
+ const accountScopedApiKeyAccountPermissions = new Set<Permission>([
476
+ "account:read",
477
+ "account:admin",
478
+ "workspace:create",
479
+ "billing:read",
480
+ "billing:manage",
481
+ "api_keys:manage",
482
+ "usage_allowances:manage",
483
+ ]);
484
+ const accountScopedApiKeyWorkspaceExcludedPermissions = new Set<Permission>([
485
+ "account:read",
486
+ "account:admin",
487
+ "workspace:create",
488
+ "billing:read",
489
+ "billing:manage",
490
+ "usage_allowances:manage",
491
+ ]);
492
+
493
+ export type AccountScopedApiKeyWorkspaceAuthority = Readonly<{
494
+ accountId: string;
495
+ permissions: Permission[];
496
+ workspaceScope: OrganizationWorkspaceScope;
497
+ permissionMode: "legacy" | "explicit";
498
+ }>;
499
+
500
+ export function organizationWorkspaceInScope(
501
+ scope: OrganizationWorkspaceScope,
502
+ workspaceId: string,
503
+ ): boolean {
504
+ return scope.kind === "all" || scope.workspaceIds.includes(workspaceId.toLowerCase());
505
+ }
506
+
507
+ /**
508
+ * Return account-scoped API-key workspace authority only for the exact
509
+ * AccessContext object stamped by successful API-key authentication. Subject
510
+ * shape alone is deliberately insufficient.
511
+ */
512
+ export function accountScopedApiKeyWorkspaceAuthority(
513
+ context: AccessContext,
514
+ ): AccountScopedApiKeyWorkspaceAuthority | null {
515
+ const authority = accountScopedApiKeyContexts.get(context);
516
+ if (!authority) return null;
517
+ const matchingAccountGrants = context.accountGrants.filter(
518
+ (grant) => grant.accountId === authority.accountId && grant.subjectId === context.subjectId,
519
+ );
520
+ if (
521
+ !context.subjectId.startsWith("api_key:") ||
522
+ matchingAccountGrants.length !== 1 ||
523
+ context.defaultAccountId !== authority.accountId ||
524
+ context.defaultWorkspaceId !== null
525
+ ) {
526
+ return null;
527
+ }
528
+ return {
529
+ accountId: authority.accountId,
530
+ permissions:
531
+ authority.permissionMode === "explicit"
532
+ ? explicitPermissions([...authority.permissions])
533
+ : [...authority.permissions],
534
+ workspaceScope: authority.workspaceScope,
535
+ permissionMode: authority.permissionMode,
536
+ };
537
+ }
538
+
539
+ /** Classify stored scopes without changing the legacy full/read permission sets. */
540
+ export function organizationApiKeyAccess(permissions: Permission[]): OrganizationApiKeyAccess {
541
+ if (
542
+ permissions.length === DEVELOPER_SETUP_API_KEY_PRESET.permissions.length &&
543
+ DEVELOPER_SETUP_API_KEY_PRESET.permissions.every((permission) =>
544
+ permissions.includes(permission),
545
+ )
546
+ )
547
+ return "developer_setup";
548
+ return permissions.includes("workspace:admin") ? "full" : "read";
549
+ }
550
+
551
+ /**
552
+ * Opaque, request-local proof that the canonical access resolver authorized the
553
+ * exact account administrator named by the stamp. The random id is audit and
554
+ * replay provenance only; object identity is what prevents a caller from
555
+ * manufacturing this proof inside the API process.
556
+ */
557
+ export type AccountAdminAuthorizationStamp = Readonly<{
558
+ authorizationId: string;
559
+ accountId: string;
560
+ actorSubjectId: string;
561
+ permission: "account:admin";
562
+ }>;
24
563
 
25
564
  export type AccessDeps = {
26
565
  db: Database;
27
566
  settings: Settings;
28
567
  managedAuth?: ManagedAuth | null;
568
+ managedAuthSessionAdapter?: ManagedAuthSessionAdapter | null;
569
+ /** Analytics only: notes canonical managed-cookie activity, never authority. */
570
+ userPresence?: UserPresenceRecorder | null;
29
571
  };
30
572
 
573
+ /** null means this is not an authenticated external lane; [] means that lane
574
+ * has no readable inventory. Callers must not fall back from [] to service or
575
+ * bare-subject discovery. */
576
+ export async function listExternalActorWorkspaces(
577
+ context: AccessContext,
578
+ deps: AccessDeps,
579
+ ): Promise<Workspace[] | null> {
580
+ const actor = externalActorContexts.get(context);
581
+ if (!actor) return null;
582
+ if (
583
+ !hasPermission(actor.permissions, "workspace:read") ||
584
+ (actor.linked && !hasPermission(actor.linked.link.permissions, "workspace:read"))
585
+ )
586
+ return [];
587
+ const candidates = await withAccountRls(deps.db, actor.identity.accountId, (tx) =>
588
+ listWorkspacesForSubject(tx, context.subjectId),
589
+ );
590
+ const authorized: Workspace[] = [];
591
+ const personal = await withAccountRls(deps.db, actor.identity.accountId, (tx) =>
592
+ requireWorkspace(tx, actor.linked?.personalWorkspaceId ?? actor.identity.personalWorkspaceId),
593
+ );
594
+ if (
595
+ personal.accountId === actor.identity.accountId &&
596
+ personal.kind === "personal" &&
597
+ actor.permissionMode === "legacy" &&
598
+ organizationWorkspaceInScope(actor.workspaceScope, personal.id)
599
+ )
600
+ authorized.push(personal);
601
+ for (const workspace of candidates) {
602
+ if (workspace.accountId !== actor.identity.accountId || workspace.kind !== "shared") continue;
603
+ if (!organizationWorkspaceInScope(actor.workspaceScope, workspace.id)) continue;
604
+ const grant = await withWorkspaceSubjectRls(deps.db, workspace.id, context.subjectId, (tx) =>
605
+ getWorkspaceGrant(tx, context.subjectId, workspace.id),
606
+ );
607
+ if (grant && hasPermission(grant.permissions, "workspace:read")) authorized.push(workspace);
608
+ }
609
+ return authorized;
610
+ }
611
+
31
612
  export async function requireAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext> {
32
613
  let pending = accessContextByRequest.get(c.req.raw);
33
614
  if (!pending) {
@@ -50,11 +631,41 @@ export async function requireAccessGrant(
50
631
  return (await requireAccessGrantAuthorization(c, deps, workspaceId, permission)).grant;
51
632
  }
52
633
 
634
+ /**
635
+ * Re-resolve the authenticated principal and its current grants without using
636
+ * the request-local access cache. Long-lived responses use this to ensure a
637
+ * membership suspension or revocation takes effect while the connection is
638
+ * still open.
639
+ */
640
+ export async function requireFreshAccessGrant(
641
+ c: Context,
642
+ deps: AccessDeps,
643
+ workspaceId: string,
644
+ permission?: Permission,
645
+ ): Promise<AccessGrant> {
646
+ const context = await resolveAccessContext(c, deps);
647
+ if (!context) {
648
+ throw new HTTPException(401, { message: "authentication required" });
649
+ }
650
+ return (await accessGrantAuthorization(context, deps, workspaceId, permission)).grant;
651
+ }
652
+
53
653
  export type AccessGrantAuthorization = {
54
654
  grant: AccessGrant;
55
655
  accountGrant: AccountGrant | null;
56
656
  authenticatedSubjectId: string;
57
657
  contextIntegrity: boolean;
658
+ /**
659
+ * Did this request authenticate as the canonical managed-cookie (Better Auth)
660
+ * session that OWNS this grant's subject? Cookie/person-present ceremonies
661
+ * retain this strict gate; owning-user boundaries use
662
+ * `hasVerifiedOwningUserAuthorization` instead.
663
+ * False for every bearer, API-key, delegated, service, local, and configured
664
+ * principal, and for any future path that does not verify a cookie.
665
+ */
666
+ canonicalManagedHumanSession: boolean;
667
+ /** Exact in-process single-user local bootstrap, never a delegated bearer. */
668
+ canonicalLocalHumanSession: boolean;
58
669
  };
59
670
 
60
671
  export function accessGrantAuthorizationFromContext(
@@ -81,12 +692,150 @@ export function accessGrantAuthorizationFromContext(
81
692
  ) &&
82
693
  matchingAccountGrants.length === 1 &&
83
694
  matchingAccountGrants[0]?.subjectId === context.subjectId;
84
- return {
695
+ const authorization: AccessGrantAuthorization = {
85
696
  grant,
86
697
  accountGrant: contextIntegrity ? matchingAccountGrants[0]! : null,
87
698
  authenticatedSubjectId: context.subjectId,
88
699
  contextIntegrity,
700
+ canonicalManagedHumanSession: isCanonicalManagedHumanSession(context, grant),
701
+ canonicalLocalHumanSession: isCanonicalLocalHumanSession(context, grant),
89
702
  };
703
+ resolvedAccessGrantAuthorizations.add(authorization);
704
+ if (contextIntegrity && developerSetupApiKeyContexts.has(context)) {
705
+ developerSetupAuthorizations.set(authorization, grant);
706
+ developerSetupGrants.add(grant);
707
+ }
708
+ if (
709
+ contextIntegrity &&
710
+ accountScopedApiKeyWorkspaceAuthority(context)?.accountId === grant.accountId &&
711
+ grant.principalKind === "api_key"
712
+ ) {
713
+ verifiedOrganizationServiceAuthorizations.set(authorization, grant);
714
+ }
715
+ const external = externalActorContexts.get(context);
716
+ if (external && contextIntegrity && grant.accountId === external.identity.accountId) {
717
+ verifiedExternalAuthorizations.set(authorization, {
718
+ grant,
719
+ workspaceId: grant.workspaceId,
720
+ workspaceSettingsCeiling:
721
+ hasPermission(external.permissions, "workspace:admin") &&
722
+ (!external.linked || hasPermission(external.linked.link.permissions, "workspace:admin")),
723
+ identityReference: {
724
+ externalId: external.identity.externalId,
725
+ source: external.identity.source,
726
+ },
727
+ attribution: attributionForExternalContext(context),
728
+ });
729
+ }
730
+ const delegatedHuman = delegatedHumanContexts.get(context);
731
+ if (
732
+ delegatedHuman &&
733
+ contextIntegrity &&
734
+ context.workspaceGrants.includes(grant) &&
735
+ grant.accountId === delegatedHuman.organizationId &&
736
+ grant.subjectId === delegatedHuman.subjectId &&
737
+ grant.principalKind === "human_session" &&
738
+ !grant.serviceInitiator &&
739
+ !grant.serviceInitiatorContext &&
740
+ delegatedHumanWorkspaceAllowed(delegatedHuman, grant.workspaceId)
741
+ ) {
742
+ verifiedDelegatedHumanAuthorizations.set(authorization, {
743
+ proof: delegatedHuman,
744
+ grant,
745
+ accountGrant: matchingAccountGrants[0]!,
746
+ });
747
+ // Keep the exact proof-bearing value immutable: changing a cookie flag,
748
+ // subject or grant in place must not manufacture browser authentication.
749
+ Object.freeze(authorization);
750
+ }
751
+ return authorization;
752
+ }
753
+
754
+ /**
755
+ * Mint the capability passed to an account-admin database lifecycle.
756
+ *
757
+ * This deliberately accepts every canonical access mode (managed,
758
+ * local/configured, and signed delegation). Organization-membership rows are
759
+ * one possible source of account authority, not the definition of it. A plain
760
+ * object with matching fields is rejected because only values returned by the
761
+ * access resolver are present in `resolvedAccessGrantAuthorizations`.
762
+ */
763
+ export function requireAccountAdminAuthorizationStamp(
764
+ authorization: AccessGrantAuthorization,
765
+ ): AccountAdminAuthorizationStamp {
766
+ const { grant, accountGrant } = authorization;
767
+ if (
768
+ !resolvedAccessGrantAuthorizations.has(authorization) ||
769
+ !authorization.contextIntegrity ||
770
+ authorization.authenticatedSubjectId !== grant.subjectId ||
771
+ accountGrant?.accountId !== grant.accountId ||
772
+ accountGrant.subjectId !== grant.subjectId ||
773
+ !accountGrant.permissions.includes("account:admin")
774
+ ) {
775
+ throw new HTTPException(403, { message: "missing permission: account:admin" });
776
+ }
777
+ return Object.freeze({
778
+ authorizationId: crypto.randomUUID(),
779
+ accountId: grant.accountId,
780
+ actorSubjectId: grant.subjectId,
781
+ permission: "account:admin" as const,
782
+ });
783
+ }
784
+
785
+ /**
786
+ * Verify that an access authorization was minted by the canonical request
787
+ * resolver for this exact subject and workspace.
788
+ *
789
+ * This is the protocol-neutral boundary for request-local services that need
790
+ * the authenticated grant rather than a caller-supplied grant-shaped object.
791
+ * Object identity is intentional: matching fields alone are not proof that the
792
+ * request authenticated the named subject.
793
+ */
794
+ export function requireResolvedAccessGrantAuthorization(
795
+ authorization: AccessGrantAuthorization,
796
+ workspaceId: string,
797
+ ): AccessGrant {
798
+ const { grant } = authorization;
799
+ if (
800
+ !resolvedAccessGrantAuthorizations.has(authorization) ||
801
+ !authorization.contextIntegrity ||
802
+ authorization.authenticatedSubjectId !== grant.subjectId ||
803
+ grant.workspaceId !== workspaceId
804
+ ) {
805
+ throw new HTTPException(403, { message: "workspace access authorization is invalid" });
806
+ }
807
+ return grant;
808
+ }
809
+
810
+ /**
811
+ * Resolve the exact built-in single-user local administrator for an account.
812
+ *
813
+ * This is intentionally narrower than checking `context.mode === "local"` or
814
+ * the `dev` subject name. Only the in-process local bootstrap branch can place
815
+ * the resolved context in `canonicalLocalHumanContexts`, so delegated bearer
816
+ * tokens and caller-constructed contexts cannot borrow this authority.
817
+ */
818
+ export async function requireCanonicalLocalAccountAdministrator(
819
+ c: Context,
820
+ deps: AccessDeps,
821
+ accountId: string,
822
+ ): Promise<{ subjectId: string; authorization: AccessGrantAuthorization }> {
823
+ if (c.req.header("authorization")) {
824
+ throw new HTTPException(401, { message: "organization administrator session required" });
825
+ }
826
+ const context = await requireAccessContext(c, deps);
827
+ const grant = context.workspaceGrants.find((candidate) => candidate.accountId === accountId);
828
+ if (!grant) {
829
+ throw new HTTPException(403, {
830
+ message: "local organization administration is not authorized",
831
+ });
832
+ }
833
+ const authorization = accessGrantAuthorizationFromContext(context, grant);
834
+ if (!authorization.canonicalLocalHumanSession) {
835
+ throw new HTTPException(401, { message: "organization administrator session required" });
836
+ }
837
+ requireAccountAdminAuthorizationStamp(authorization);
838
+ return { subjectId: context.subjectId, authorization };
90
839
  }
91
840
 
92
841
  export async function requireAccessGrantAuthorization(
@@ -96,8 +845,228 @@ export async function requireAccessGrantAuthorization(
96
845
  permission?: Permission,
97
846
  ): Promise<AccessGrantAuthorization> {
98
847
  const context = await requireAccessContext(c, deps);
848
+ // Request entry only: a fresh re-check of a live connection never re-creates
849
+ // a membership that was removed while the connection was open.
850
+ return await accessGrantAuthorization(context, deps, workspaceId, permission, {
851
+ firstUseMembership: true,
852
+ });
853
+ }
854
+
855
+ /**
856
+ * Settings administration is narrower than workspace administration. The
857
+ * verified owning user may configure their Personal workspace, but
858
+ * never acquires the admin wildcard (and its membership/delegation powers).
859
+ * Delegated owners also need workspace:admin in their verified dispatch/key
860
+ * ceiling: a read-only delegation cannot turn ownership into a settings write.
861
+ * Only use this boundary for workspace configuration, not access management.
862
+ */
863
+ export async function requireWorkspaceSettingsGrant(
864
+ c: Context,
865
+ deps: AccessDeps,
866
+ workspaceId: string,
867
+ ): Promise<AccessGrant> {
868
+ const authorization = await requireAccessGrantAuthorization(c, deps, workspaceId);
869
+ const grant = requireResolvedAccessGrantAuthorization(authorization, workspaceId);
870
+ if (hasPermission(grant.permissions, "workspace:admin")) return grant;
871
+ const delegatedHuman = verifiedDelegatedHumanAuthorizations.get(authorization);
872
+ const ownerSettingsCeiling =
873
+ authorization.canonicalManagedHumanSession ||
874
+ verifiedExternalAuthorizations.get(authorization)?.workspaceSettingsCeiling === true ||
875
+ (delegatedHuman !== undefined && delegatedHuman.proof.permissions.includes("workspace:admin"));
876
+ if (
877
+ hasVerifiedOwningUserAuthorization(authorization) &&
878
+ ownerSettingsCeiling &&
879
+ hasPermission(grant.permissions, "workspace:read") &&
880
+ (await resolveNamedManagedPersonalWorkspaceGrant(deps.db, {
881
+ accountId: grant.accountId,
882
+ workspaceId,
883
+ subjectId: authorization.authenticatedSubjectId,
884
+ }))
885
+ ) {
886
+ return grant;
887
+ }
888
+ throw new HTTPException(403, {
889
+ message: "workspace settings require a workspace administrator or Personal workspace owner",
890
+ });
891
+ }
892
+
893
+ /**
894
+ * Automatic membership for an organization key acting as an external user
895
+ * (`asUser`): the first request to a shared workspace in the key's own
896
+ * organization creates the user's missing membership with
897
+ * {@link EXTERNAL_FIRST_USE_MEMBER_PERMISSIONS}, then the request continues.
898
+ * It is exactly the authority of explicit `addExternalWorkspaceMember`: the
899
+ * calling key must hold `members:manage` (or legacy `workspace:admin`) plus
900
+ * every default permission, with the workspace in its scope, re-checked live
901
+ * under the organization membership fence together with the identity's own
902
+ * active status. Never for linked native identities, service-initiator
903
+ * requests, or any non-key principal (agent attempts, delegated/bearer user
904
+ * tokens, browser sessions), which never reach the external lane; never for
905
+ * Personal or SDK per-user workspaces; never when the request needs a
906
+ * permission outside the defaults. Never changes an existing membership.
907
+ * Returns true only when a membership now exists; any refusal leaves the
908
+ * ordinary 403 in place.
909
+ */
910
+ async function provisionExternalMemberOnFirstUse(
911
+ deps: AccessDeps,
912
+ context: AccessContext,
913
+ external: NonNullable<ReturnType<typeof externalActorContexts.get>>,
914
+ workspaceId: string,
915
+ permission: Permission | undefined,
916
+ ): Promise<boolean> {
917
+ if (
918
+ !external.firstUseMembership ||
919
+ external.linked ||
920
+ // A request that would 403 on its own permission anyway creates nothing.
921
+ (permission !== undefined && !EXTERNAL_FIRST_USE_MEMBER_PERMISSIONS.includes(permission)) ||
922
+ context.subjectId !== external.identity.subjectId ||
923
+ !hasPermission(external.permissions, "members:manage", external.permissionMode) ||
924
+ EXTERNAL_FIRST_USE_MEMBER_PERMISSIONS.some(
925
+ (required) => !hasPermission(external.permissions, required, external.permissionMode),
926
+ )
927
+ )
928
+ return false;
929
+ const workspace = await requireWorkspace(deps.db, workspaceId).catch(() => null);
930
+ if (
931
+ !workspace ||
932
+ workspace.kind !== "shared" ||
933
+ workspace.accountId !== external.identity.accountId ||
934
+ // SDK per-user workspaces stay single-user: the SDK adds their owner.
935
+ workspace.externalSource?.startsWith(USER_ISOLATION_WORKSPACE_SOURCE_PREFIX)
936
+ )
937
+ return false;
938
+ try {
939
+ await ensureExternalWorkspaceMemberOnFirstUse(
940
+ deps.db,
941
+ {
942
+ organizationId: external.identity.accountId,
943
+ workspaceId,
944
+ actorSubjectId: `api_key:${external.keyId}`,
945
+ },
946
+ {
947
+ subjectId: external.identity.subjectId,
948
+ identity: { source: external.identity.source, externalId: external.identity.externalId },
949
+ permissions: EXTERNAL_FIRST_USE_MEMBER_PERMISSIONS,
950
+ },
951
+ );
952
+ return true;
953
+ } catch (error) {
954
+ // A changed/revoked key, narrowed policy or workspace scope keeps the
955
+ // ordinary denial. Anything else is an infrastructure failure.
956
+ if (
957
+ (error as { code?: unknown } | null)?.code === "42501" ||
958
+ nestedPostgresSqlState(error) === "42501"
959
+ )
960
+ return false;
961
+ throw error;
962
+ }
963
+ }
964
+
965
+ async function accessGrantAuthorization(
966
+ context: AccessContext,
967
+ deps: AccessDeps,
968
+ workspaceId: string,
969
+ permission?: Permission,
970
+ options: { firstUseMembership?: boolean } = {},
971
+ ): Promise<AccessGrantAuthorization> {
972
+ // No named-subject or organization-key fallback may widen verified OAuth
973
+ // bounds. These grants came from this resolution's live native access only.
974
+ if (delegatedHumanContexts.has(context)) {
975
+ const grant = context.workspaceGrants.find(
976
+ (candidate) => candidate.workspaceId === workspaceId,
977
+ );
978
+ if (!grant)
979
+ throw new HTTPException(403, { message: "delegated human workspace access denied" });
980
+ if (permission) requirePermission(grant, permission);
981
+ return accessGrantAuthorizationFromContext(context, grant);
982
+ }
983
+ const external = externalActorContexts.get(context);
984
+ if (external) {
985
+ if (!organizationWorkspaceInScope(external.workspaceScope, workspaceId)) {
986
+ throw new HTTPException(403, { message: "workspace is outside organization key scope" });
987
+ }
988
+ if (external.permissionMode === "explicit") {
989
+ const workspace = await requireWorkspace(deps.db, workspaceId);
990
+ if (workspace.kind !== "shared" || workspace.accountId !== external.identity.accountId)
991
+ throw new HTTPException(403, {
992
+ message: "organization policy requires a shared workspace",
993
+ });
994
+ }
995
+ const personal =
996
+ workspaceId ===
997
+ (external.linked?.personalWorkspaceId ?? external.identity.personalWorkspaceId);
998
+ const membershipGrant = () =>
999
+ withWorkspaceSubjectRls(deps.db, workspaceId, context.subjectId, (tx) =>
1000
+ getWorkspaceGrant(tx, context.subjectId, workspaceId, {
1001
+ principalKind: "human_session",
1002
+ }),
1003
+ );
1004
+ let grant: AccessGrant | null = personal
1005
+ ? {
1006
+ accountId: external.identity.accountId,
1007
+ workspaceId,
1008
+ subjectId: context.subjectId,
1009
+ principalKind: "human_session",
1010
+ permissions: [...managedPersonalWorkspacePermissions],
1011
+ }
1012
+ : await membershipGrant();
1013
+ if (
1014
+ !grant &&
1015
+ !personal &&
1016
+ options.firstUseMembership === true &&
1017
+ (await provisionExternalMemberOnFirstUse(deps, context, external, workspaceId, permission))
1018
+ ) {
1019
+ grant = await membershipGrant();
1020
+ }
1021
+ if (!grant || grant.accountId !== external.identity.accountId) {
1022
+ throw new HTTPException(403, { message: "external workspace access denied" });
1023
+ }
1024
+ // Intersect effective permissions using the existing wildcard/literal
1025
+ // semantics on each side independently. Never return workspace:admin
1026
+ // unless both sides hold it, and never infer literal secrets:read.
1027
+ grant.permissions = Permission.options.filter(
1028
+ (value) =>
1029
+ hasPermission(grant.permissions, value) &&
1030
+ hasPermission(external.permissions, value) &&
1031
+ (!external.linked || hasPermission(external.linked.link.permissions, value)),
1032
+ );
1033
+ if (external.permissionMode === "explicit") {
1034
+ grant.permissionMode = "explicit";
1035
+ explicitPermissions(grant.permissions);
1036
+ }
1037
+ grant.metadata = {
1038
+ ...grant.metadata,
1039
+ externalActor: attributionForExternalContext(context),
1040
+ };
1041
+ if (permission) requirePermission(grant, permission);
1042
+ return accessGrantAuthorizationFromContext(context, grant);
1043
+ }
1044
+ const organizationKey = accountScopedApiKeyWorkspaceAuthority(context);
1045
+ if (organizationKey) {
1046
+ const workspace = await requireWorkspace(deps.db, workspaceId).catch(() => null);
1047
+ if (!workspace) throw new HTTPException(404, { message: "workspace not found" });
1048
+ if (
1049
+ workspace.accountId !== organizationKey.accountId ||
1050
+ workspace.kind !== "shared" ||
1051
+ !organizationWorkspaceInScope(organizationKey.workspaceScope, workspace.id)
1052
+ ) {
1053
+ throw new HTTPException(403, { message: "workspace is outside organization key scope" });
1054
+ }
1055
+ const grant: AccessGrant = {
1056
+ accountId: workspace.accountId,
1057
+ workspaceId,
1058
+ subjectId: context.subjectId,
1059
+ ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
1060
+ principalKind: "api_key",
1061
+ permissions: organizationKey.permissions,
1062
+ permissionMode: organizationKey.permissionMode,
1063
+ ...apiKeyServiceContexts.get(context),
1064
+ };
1065
+ requirePermission(grant, permission ?? "workspace:read");
1066
+ return accessGrantAuthorizationFromContext(context, grant);
1067
+ }
99
1068
  const principalKind = hostedHumanSessionPrincipalKind(context);
100
- const grant =
1069
+ let grant =
101
1070
  context.workspaceGrants.find((candidate) => candidate.workspaceId === workspaceId) ??
102
1071
  (await getWorkspaceGrant(
103
1072
  deps.db,
@@ -105,12 +1074,53 @@ export async function requireAccessGrantAuthorization(
105
1074
  workspaceId,
106
1075
  principalKind ? { principalKind } : undefined,
107
1076
  ));
1077
+ if (grant && isDeveloperSetupApiKeyContext(context)) {
1078
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
1079
+ const workspace = await requireWorkspace(deps.db, workspaceId);
1080
+ if (!authority || workspace.accountId !== authority.accountId || workspace.kind !== "shared") {
1081
+ throw new HTTPException(403, {
1082
+ message: "Developer setup requires a shared organization workspace",
1083
+ });
1084
+ }
1085
+ // A persisted creator/membership grant cannot widen the authenticated key
1086
+ // ceiling, including literal secret reads and inherited session authority.
1087
+ const storedPermissions = grant.permissions;
1088
+ grant = {
1089
+ ...grant,
1090
+ permissions: Permission.options.filter(
1091
+ (value) =>
1092
+ value !== "api_keys:manage" &&
1093
+ !accountScopedApiKeyWorkspaceExcludedPermissions.has(value) &&
1094
+ hasPermission(storedPermissions, value) &&
1095
+ hasPermission(authority.permissions, value),
1096
+ ),
1097
+ };
1098
+ }
108
1099
  if (!grant) {
109
1100
  const workspace = await requireWorkspace(deps.db, workspaceId).catch(() => null);
110
1101
  if (!workspace) {
111
1102
  throw new HTTPException(404, { message: "workspace not found" });
112
1103
  }
113
- throw new HTTPException(403, { message: "workspace access denied" });
1104
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
1105
+ const requiredWorkspacePermission = permission ?? "workspace:read";
1106
+ if (
1107
+ authority &&
1108
+ workspace.accountId === authority.accountId &&
1109
+ workspace.kind === "shared" &&
1110
+ hasPermission(authority.permissions, requiredWorkspacePermission)
1111
+ ) {
1112
+ grant = {
1113
+ workspaceId: workspace.id,
1114
+ accountId: workspace.accountId,
1115
+ subjectId: context.subjectId,
1116
+ ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}),
1117
+ permissions: authority.permissions,
1118
+ principalKind: "api_key",
1119
+ ...apiKeyServiceContexts.get(context),
1120
+ };
1121
+ } else {
1122
+ throw new HTTPException(403, { message: "workspace access denied" });
1123
+ }
114
1124
  }
115
1125
  if (permission) {
116
1126
  requirePermission(grant, permission);
@@ -118,6 +1128,55 @@ export async function requireAccessGrantAuthorization(
118
1128
  return accessGrantAuthorizationFromContext(context, grant);
119
1129
  }
120
1130
 
1131
+ /**
1132
+ * Did the managed cookie authenticate this exact native person?
1133
+ *
1134
+ * A managed human's personal workspace carries no `workspace_memberships` row,
1135
+ * so seams that fence on one must consult the
1136
+ * `organization_memberships.personal_workspace_id` pointer instead. That is a
1137
+ * runtime property, not a schema one: migration 0219's `42501` is a
1138
+ * precondition inside the provisioning function rather than a constraint, and
1139
+ * the parity report records `personal_workspace_has_no_membership_row` as
1140
+ * `basis: "runtime"` - its own term for something nothing in the schema
1141
+ * prevents. It holds because every membership writer requires `members:manage`
1142
+ * on the target, which a personal-workspace grant deliberately omits. `AGENTS.md`
1143
+ * scopes that exception tightly: it "derives an owner-only personal-workspace
1144
+ * grant only for the canonical managed-cookie (Better Auth) session", and
1145
+ * "Bearer/delegated principals, API keys, and account or organization
1146
+ * administrators receive no personal-workspace access through that exception."
1147
+ *
1148
+ * This asks about PROVENANCE, not shape. Inspecting a grant's `principalKind`,
1149
+ * `metadata.delegated`, or `serviceInitiator` is not sufficient: a delegated
1150
+ * bearer chooses every one of those claims inside its own host-signed token and
1151
+ * can name any subject, including the owner's. So the answer comes from
1152
+ * {@link canonicalManagedCookieContexts}, stamped only where a Better Auth
1153
+ * cookie was actually verified.
1154
+ *
1155
+ * The subject equality is the second half: the exception is OWNER-only, so the
1156
+ * grant must belong to the authenticated human itself, not to some other
1157
+ * subject reached from an authenticated session. `user:` excludes machine
1158
+ * principals, which can never own an organization membership.
1159
+ */
1160
+ function isCanonicalManagedHumanSession(context: AccessContext, grant: AccessGrant): boolean {
1161
+ return (
1162
+ canonicalManagedCookieContexts.has(context) &&
1163
+ grant.subjectId === context.subjectId &&
1164
+ grant.subjectId.startsWith("user:")
1165
+ );
1166
+ }
1167
+
1168
+ function isCanonicalLocalHumanSession(context: AccessContext, grant: AccessGrant): boolean {
1169
+ return (
1170
+ canonicalLocalHumanContexts.has(context) &&
1171
+ context.mode === "local" &&
1172
+ context.subjectId === "dev" &&
1173
+ grant.subjectId === context.subjectId &&
1174
+ grant.principalKind === "human_session" &&
1175
+ grant.metadata?.delegated !== true &&
1176
+ !grant.serviceInitiator
1177
+ );
1178
+ }
1179
+
121
1180
  function hostedHumanSessionPrincipalKind(context: AccessContext): "human_session" | undefined {
122
1181
  if (context.mode !== "managed" || context.workspaceGrants.length === 0) {
123
1182
  return undefined;
@@ -133,6 +1192,7 @@ function hostedHumanSessionPrincipalKind(context: AccessContext): "human_session
133
1192
  }
134
1193
 
135
1194
  export function requirePermission(grant: AccessGrant, permission: Permission): void {
1195
+ if (grant.permissionMode === "explicit") explicitPermissions(grant.permissions);
136
1196
  if (!hasPermission(grant.permissions, permission)) {
137
1197
  if (permission === "variable-sets:use") {
138
1198
  throw new HTTPException(403, {
@@ -164,52 +1224,64 @@ export function requireLiteralPermission(grant: AccessGrant, permission: Permiss
164
1224
  }
165
1225
  }
166
1226
 
167
- export function hasLiteralPermission(permissions: Permission[], permission: Permission): boolean {
1227
+ export function hasLiteralPermission(
1228
+ permissions: readonly Permission[],
1229
+ permission: Permission,
1230
+ ): boolean {
1231
+ if (!Array.isArray(permissions)) return false;
168
1232
  return permissions.includes(permission);
169
1233
  }
170
1234
 
171
- export function hasPermission(permissions: Permission[], permission: Permission): boolean {
1235
+ export function hasPermission(
1236
+ permissions: readonly Permission[],
1237
+ permission: Permission,
1238
+ permissionMode?: AccessGrant["permissionMode"],
1239
+ ): boolean {
1240
+ if (!Array.isArray(permissions)) return false;
1241
+ if (permissionMode === "explicit" || explicitPermissionSets.has(permissions))
1242
+ return permissions.includes(permission);
172
1243
  if (permission === "secrets:read") {
173
1244
  return permissions.includes("secrets:read");
174
1245
  }
175
- const aliases: Partial<Record<Permission, Permission[]>> = {
176
- "variable-sets:use": ["environments:use" as Permission],
177
- "variable-sets:manage": ["environments:manage" as Permission],
178
- "variable-sets:list": [
179
- "variable-sets:use",
180
- "variable-sets:manage",
181
- "environments:use" as Permission,
182
- "environments:manage" as Permission,
183
- ],
184
- "variable-sets:read": [
185
- "variable-sets:use",
186
- "variable-sets:manage",
187
- "environments:use" as Permission,
188
- "environments:manage" as Permission,
189
- ],
190
- "variable-sets:write": ["variable-sets:manage", "environments:manage" as Permission],
191
- "secrets:list": [
192
- "variable-sets:use",
193
- "variable-sets:manage",
194
- "environments:use" as Permission,
195
- "environments:manage" as Permission,
196
- ],
197
- "secrets:write": ["variable-sets:manage", "environments:manage" as Permission],
198
- };
199
- return (
200
- permissions.includes(permission) ||
201
- (aliases[permission]?.some((alias) => permissions.includes(alias)) ?? false) ||
202
- permissions.includes("workspace:admin")
203
- );
1246
+ // Variable-set metadata, plaintext read, write/rotation, attachment, and
1247
+ // runtime use are independent capabilities. Deprecated broad permissions
1248
+ // remain parseable but do not imply any of the exact permissions.
1249
+ return permissions.includes(permission) || permissions.includes("workspace:admin");
204
1250
  }
205
1251
 
206
1252
  async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<AccessContext | null> {
1253
+ accessResolvedRequests.add(c.req.raw);
1254
+ const delegatedHuman = verifiedDelegatedHumanAuthorizationForRequest(c.req.raw);
1255
+ if (delegatedHuman) return resolveDelegatedHumanAccessContext(deps, delegatedHuman);
1256
+ const service = serviceInitiatorFromHeaders(c.req.raw.headers);
1257
+ if (service) {
1258
+ if (deps.settings.productAccessMode === "local") {
1259
+ throw new HTTPException(422, {
1260
+ message: "service initiator headers require an organization or workspace API key",
1261
+ });
1262
+ }
1263
+ const context = await apiKeyAccessContext(c, deps, deps.settings.productAccessMode);
1264
+ if (!context) {
1265
+ throw new HTTPException(422, {
1266
+ message: "service initiator headers require an organization or workspace API key",
1267
+ });
1268
+ }
1269
+ return context;
1270
+ }
1271
+ if (c.req.header("x-opengeni-external-actor") !== undefined) {
1272
+ if (deps.settings.productAccessMode === "local") {
1273
+ throw new HTTPException(401, {
1274
+ message: "external actors require organization key authentication",
1275
+ });
1276
+ }
1277
+ return apiKeyAccessContext(c, deps, deps.settings.productAccessMode);
1278
+ }
207
1279
  if (deps.settings.productAccessMode === "local") {
208
1280
  const delegated = await delegatedAccessContext(c, deps, "local");
209
1281
  if (delegated) {
210
1282
  return delegated;
211
1283
  }
212
- return await bootstrapWorkspace(deps.db, {
1284
+ const context = await bootstrapWorkspace(deps.db, {
213
1285
  accountExternalSource: "opengeni:local",
214
1286
  accountExternalId: "default",
215
1287
  accountName: "Local",
@@ -219,6 +1291,8 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
219
1291
  subjectId: "dev",
220
1292
  subjectLabel: "Local dev",
221
1293
  });
1294
+ canonicalLocalHumanContexts.add(context);
1295
+ return context;
222
1296
  }
223
1297
 
224
1298
  if (deps.settings.productAccessMode === "configured") {
@@ -258,19 +1332,151 @@ async function resolveAccessContext(c: Context, deps: AccessDeps): Promise<Acces
258
1332
  }
259
1333
 
260
1334
  if (deps.managedAuth) {
261
- const session = await getManagedSession(c, deps.managedAuth, { db: deps.db });
1335
+ const session = await getManagedSession(c, deps.managedAuth, {
1336
+ db: deps.db,
1337
+ sessionSetMode: deps.settings.managedAuthSessionSetMode,
1338
+ sessionAdapter: deps.managedAuthSessionAdapter,
1339
+ });
262
1340
  if (session?.user) {
263
- return await ensureManagedAccessForUser(deps.db, {
1341
+ // THE canonical managed-cookie (Better Auth) branch, and the only place
1342
+ // that may stamp a context as such. Every `return` above this point leaves
1343
+ // its context unstamped, so the owner-only personal-workspace exception
1344
+ // fails closed for them without depending on an exclusion list.
1345
+ const context = await ensureManagedAccessForUser(deps.db, {
264
1346
  userId: session.user.id,
265
1347
  email: session.user.email,
266
1348
  name: session.user.name,
1349
+ emailVerified: session.user.emailVerified,
1350
+ provisionFallbackOrganization: false,
1351
+ bindPendingInvitations: false,
267
1352
  });
1353
+ canonicalManagedCookieContexts.add(context);
1354
+ recordUserPresence(c, deps, context.subjectId);
1355
+ return context;
268
1356
  }
269
1357
  }
270
1358
 
271
1359
  return null;
272
1360
  }
273
1361
 
1362
+ async function resolveDelegatedHumanAccessContext(
1363
+ deps: AccessDeps,
1364
+ proof: DelegatedHumanAuthorization,
1365
+ ): Promise<AccessContext> {
1366
+ if (deps.settings.productAccessMode === "local") {
1367
+ throw new HTTPException(403, {
1368
+ message: "delegated humans require native organization access",
1369
+ });
1370
+ }
1371
+ const userId = proof.subjectId.slice("user:".length);
1372
+ const profiles = await getManagedUserProfilesByIds(deps.db, [userId]);
1373
+ const parsedProfile = delegatedHumanProfileSchema.safeParse(
1374
+ profiles.length === 1 && profiles[0]?.id === userId ? profiles[0] : null,
1375
+ );
1376
+ if (!parsedProfile.success)
1377
+ throw new HTTPException(403, { message: "delegated native person is unavailable" });
1378
+ const profile = parsedProfile.data;
1379
+ const live = await ensureManagedAccessForUser(deps.db, {
1380
+ userId: profile.id,
1381
+ email: profile.email,
1382
+ name: profile.name ?? "",
1383
+ provisionFallbackOrganization: false,
1384
+ bindPendingInvitations: false,
1385
+ });
1386
+ const accounts = live.accountGrants.filter((grant) => grant.accountId === proof.organizationId);
1387
+ if (
1388
+ live.mode !== "managed" ||
1389
+ live.subjectId !== proof.subjectId ||
1390
+ accounts.length !== 1 ||
1391
+ live.accountGrants.some((grant) => grant.subjectId !== proof.subjectId) ||
1392
+ live.workspaceGrants.some(
1393
+ (grant) =>
1394
+ grant.subjectId !== proof.subjectId ||
1395
+ grant.principalKind !== "human_session" ||
1396
+ grant.metadata?.delegated === true ||
1397
+ grant.serviceInitiator ||
1398
+ grant.serviceInitiatorContext,
1399
+ )
1400
+ ) {
1401
+ throw new HTTPException(403, { message: "delegated native person authority is unavailable" });
1402
+ }
1403
+ const verifiedPermissions = [...proof.permissions];
1404
+ // The person's own role expands as usual; the connection's access setting is
1405
+ // literal, and so is the result: workspace:admin in a Custom setting is never
1406
+ // a wildcard for the permissions the person left out.
1407
+ const intersectWorkspacePermissions = (permissions: Permission[]) =>
1408
+ explicitPermissions(
1409
+ Permission.options.filter(
1410
+ (permission) =>
1411
+ hasPermission(permissions, permission) &&
1412
+ hasPermission(verifiedPermissions, permission, "explicit"),
1413
+ ),
1414
+ );
1415
+ const accountGrant: AccountGrant = {
1416
+ ...accounts[0]!,
1417
+ // Organization/billing authority must be literal on BOTH sides, and so is
1418
+ // the result: a workspace:admin entry here never reads as account:admin or
1419
+ // billing to a later permission check.
1420
+ permissions: explicitPermissions(
1421
+ Permission.options.filter(
1422
+ (permission) =>
1423
+ hasLiteralPermission(accounts[0]!.permissions, permission) &&
1424
+ hasLiteralPermission(verifiedPermissions, permission),
1425
+ ),
1426
+ ),
1427
+ };
1428
+ const workspaceGrants = live.workspaceGrants
1429
+ .filter(
1430
+ (grant) =>
1431
+ grant.accountId === proof.organizationId &&
1432
+ delegatedHumanWorkspaceAllowed(proof, grant.workspaceId),
1433
+ )
1434
+ .map((grant) => ({
1435
+ ...grant,
1436
+ permissions: intersectWorkspacePermissions(grant.permissions),
1437
+ permissionMode: "explicit" as const,
1438
+ }));
1439
+ // Proof-bearing authority cannot be changed in place and then reused as if
1440
+ // the resolver had authenticated a different subject, scope or ceiling.
1441
+ for (const grant of [accountGrant, ...workspaceGrants]) {
1442
+ Object.freeze(grant.permissions);
1443
+ Object.freeze(grant);
1444
+ }
1445
+ const context: AccessContext = {
1446
+ mode: "managed",
1447
+ subjectId: proof.subjectId,
1448
+ ...(live.subjectLabel ? { subjectLabel: live.subjectLabel } : {}),
1449
+ accountGrants: [accountGrant],
1450
+ workspaceGrants,
1451
+ defaultAccountId: proof.organizationId,
1452
+ defaultWorkspaceId:
1453
+ workspaceGrants.find((grant) => grant.workspaceId === live.defaultWorkspaceId)?.workspaceId ??
1454
+ workspaceGrants[0]?.workspaceId ??
1455
+ null,
1456
+ };
1457
+ Object.freeze(context.accountGrants);
1458
+ Object.freeze(context.workspaceGrants);
1459
+ Object.freeze(context);
1460
+ delegatedHumanContexts.set(context, proof);
1461
+ delegatedHumanProfiles.set(context, Object.freeze(profile));
1462
+ return context;
1463
+ }
1464
+
1465
+ /**
1466
+ * Presence counts people, so only the verified managed browser-session branch
1467
+ * reports it, and only for a request the console marked as human activity (a
1468
+ * visible tab with recent interaction). Each request counts once: an SSE
1469
+ * stream's periodic reauthorization reuses its original request.
1470
+ */
1471
+ const presenceRecordedRequests = new WeakSet<Request>();
1472
+ function recordUserPresence(c: Context, deps: AccessDeps, subjectId: string): void {
1473
+ if (!deps.userPresence) return;
1474
+ if (c.req.header(OPENGENI_USER_ACTIVITY_HEADER) !== OPENGENI_USER_ACTIVITY_ACTIVE) return;
1475
+ if (presenceRecordedRequests.has(c.req.raw)) return;
1476
+ presenceRecordedRequests.add(c.req.raw);
1477
+ deps.userPresence.touch(subjectId);
1478
+ }
1479
+
274
1480
  async function apiKeyAccessContext(
275
1481
  c: Context,
276
1482
  deps: AccessDeps,
@@ -284,13 +1490,93 @@ async function apiKeyAccessContext(
284
1490
  if (!apiKey) {
285
1491
  return null;
286
1492
  }
1493
+ const externalHeader = c.req.header("x-opengeni-external-actor");
1494
+ const service = serviceInitiatorFromHeaders(c.req.raw.headers);
1495
+ if (
1496
+ service &&
1497
+ apiKey.credentialKind !== "organization" &&
1498
+ apiKey.credentialKind !== "workspace"
1499
+ ) {
1500
+ throw new HTTPException(422, {
1501
+ message: "service initiator headers require an organization or workspace API key",
1502
+ });
1503
+ }
1504
+ if (externalHeader !== undefined) {
1505
+ if (apiKey.workspaceId !== null || apiKey.credentialKind !== "organization") {
1506
+ throw new HTTPException(403, { message: "external actors require an organization key" });
1507
+ }
1508
+ let selection: ExternalActorSelection;
1509
+ try {
1510
+ if (externalHeader.length > 16384) throw new Error("oversize");
1511
+ selection = ExternalActorSelection.parse(JSON.parse(decodeURIComponent(externalHeader)));
1512
+ } catch {
1513
+ throw new HTTPException(400, { message: "invalid external actor selection" });
1514
+ }
1515
+ let identity: ExternalIdentity;
1516
+ try {
1517
+ identity = await withAccountRls(deps.db, apiKey.accountId, async (tx) => {
1518
+ // Provisioning locks the live membership after its identity lock.
1519
+ // Claim the lifecycle prefix first so a membership-fenced session
1520
+ // writer can recheck that identity without a lock inversion.
1521
+ await lockExternalWorkspaceMembershipLifecycle(tx, apiKey.accountId);
1522
+ return await ensureExternalIdentity(tx, {
1523
+ accountId: apiKey.accountId,
1524
+ ...selection.identity,
1525
+ });
1526
+ });
1527
+ } catch (error) {
1528
+ if (nestedPostgresSqlState(error) === "42501") {
1529
+ throw new HTTPException(403, { message: "external identity is unavailable" });
1530
+ }
1531
+ throw new HTTPException(503, { message: "external identity authority is unavailable" });
1532
+ }
1533
+ const linked =
1534
+ selection.mode === "linked_native"
1535
+ ? await resolveExternalIdentityLink(deps.db, {
1536
+ identity,
1537
+ linkId: selection.linkId,
1538
+ expectedRevision: selection.expectedLinkRevision,
1539
+ })
1540
+ : null;
1541
+ if (selection.mode === "linked_native" && !linked)
1542
+ throw new HTTPException(403, { message: "Native identity link is unavailable or changed" });
1543
+ const effectiveSubjectId = linked?.link.nativeSubjectId ?? identity.subjectId;
1544
+ const context: AccessContext = {
1545
+ mode,
1546
+ subjectId: effectiveSubjectId,
1547
+ accountGrants: [
1548
+ { accountId: identity.accountId, subjectId: effectiveSubjectId, permissions: [] },
1549
+ ],
1550
+ workspaceGrants: [],
1551
+ defaultAccountId: identity.accountId,
1552
+ defaultWorkspaceId: null,
1553
+ };
1554
+ externalActorContexts.set(context, {
1555
+ identity,
1556
+ keyId: apiKey.id,
1557
+ permissions:
1558
+ apiKey.permissionMode === "explicit"
1559
+ ? explicitPermissions([...apiKey.permissions])
1560
+ : [...apiKey.permissions],
1561
+ workspaceScope: apiKey.workspaceScope ?? { kind: "all" },
1562
+ permissionMode: apiKey.permissionMode ?? "legacy",
1563
+ ...(linked ? { linked } : {}),
1564
+ firstUseMembership: selection.mode !== "linked_native" && !linked && !service,
1565
+ });
1566
+ if (organizationApiKeyAccess(apiKey.permissions) === "developer_setup") {
1567
+ developerSetupApiKeyContexts.add(context);
1568
+ }
1569
+ return context;
1570
+ }
287
1571
  const subjectId = `api_key:${apiKey.id}`;
288
1572
  const accountPermissions = apiKey.workspaceId
289
1573
  ? apiKey.permissions.filter(
290
1574
  (permission) => permission === "billing:read" || permission === "billing:manage",
291
1575
  )
292
- : apiKey.permissions;
293
- return {
1576
+ : apiKey.permissions.filter((permission) =>
1577
+ accountScopedApiKeyAccountPermissions.has(permission),
1578
+ );
1579
+ const context: AccessContext = {
294
1580
  mode,
295
1581
  subjectId,
296
1582
  subjectLabel: apiKey.name,
@@ -311,12 +1597,62 @@ async function apiKeyAccessContext(
311
1597
  subjectLabel: apiKey.name,
312
1598
  permissions: apiKey.permissions,
313
1599
  principalKind: "api_key",
1600
+ ...service,
314
1601
  },
315
1602
  ]
316
1603
  : [],
317
1604
  defaultAccountId: apiKey.accountId,
318
1605
  defaultWorkspaceId: apiKey.workspaceId,
319
- } satisfies AccessContext;
1606
+ };
1607
+ if (service) apiKeyServiceContexts.set(context, service);
1608
+ if (
1609
+ apiKey.credentialKind === "organization" &&
1610
+ organizationApiKeyAccess(apiKey.permissions) === "developer_setup"
1611
+ ) {
1612
+ developerSetupApiKeyContexts.add(context);
1613
+ }
1614
+ if (apiKey.workspaceId === null && apiKey.credentialKind === "organization") {
1615
+ accountScopedApiKeyContexts.set(
1616
+ context,
1617
+ Object.freeze({
1618
+ accountId: apiKey.accountId,
1619
+ workspaceScope: apiKey.workspaceScope ?? { kind: "all" as const },
1620
+ permissionMode: apiKey.permissionMode ?? "legacy",
1621
+ permissions: Object.freeze(
1622
+ apiKey.permissions.filter(
1623
+ (permission) => !accountScopedApiKeyWorkspaceExcludedPermissions.has(permission),
1624
+ ),
1625
+ ),
1626
+ }),
1627
+ );
1628
+ }
1629
+ // Report the exact stamped authority consumed by accessGrantAuthorization,
1630
+ // rather than re-deriving organization-key permissions from a separate rule.
1631
+ // This projection is never an authorization input, and the asUser branch
1632
+ // above deliberately omits it instead of advertising the service's authority.
1633
+ const authority = accountScopedApiKeyWorkspaceAuthority(context);
1634
+ const workspaceGrant =
1635
+ apiKey.credentialKind === "workspace" ? context.workspaceGrants[0] : undefined;
1636
+ const workspacePermissions = authority?.permissions ?? workspaceGrant?.permissions;
1637
+ if (workspacePermissions) {
1638
+ context.credential = {
1639
+ kind: authority ? "organization_api_key" : "workspace_api_key",
1640
+ ...(authority ? { access: organizationApiKeyAccess(apiKey.permissions) } : {}),
1641
+ accountId: apiKey.accountId,
1642
+ workspaceId: apiKey.workspaceId,
1643
+ effectiveWorkspacePermissions: Permission.options.filter(
1644
+ (permission) =>
1645
+ !(developerSetupApiKeyContexts.has(context) && permission === "api_keys:manage") &&
1646
+ !accountScopedApiKeyWorkspaceExcludedPermissions.has(permission) &&
1647
+ hasPermission(workspacePermissions, permission),
1648
+ ),
1649
+ ...(authority ? { policy: apiKey.policy, workspaceScope: authority.workspaceScope } : {}),
1650
+ note: authority
1651
+ ? `These permissions apply to ${authority.workspaceScope.kind === "all" ? "every" : "selected"} shared workspace${authority.workspaceScope.kind === "all" ? "" : "s"} in this organization, not Personal workspaces. workspaceGrants need not enumerate them; accountGrants report organization-level permissions. asUser requests also require user authority.`
1652
+ : "These permissions apply only to the workspace identified by workspaceId; they grant no organization-wide workspace authority.",
1653
+ };
1654
+ }
1655
+ return context;
320
1656
  }
321
1657
 
322
1658
  async function delegatedAccessContext(
@@ -333,7 +1669,11 @@ async function delegatedAccessContext(
333
1669
  if (!payload) {
334
1670
  return null;
335
1671
  }
336
- return {
1672
+ const restricted = payload.credentialRestriction === "developer_setup";
1673
+ const workspacePermissions = restricted
1674
+ ? payload.permissions.filter(isDeveloperSetupDelegatedPermissionAllowed)
1675
+ : payload.permissions;
1676
+ const context: AccessContext = {
337
1677
  mode,
338
1678
  subjectId: payload.subjectId,
339
1679
  ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
@@ -342,7 +1682,7 @@ async function delegatedAccessContext(
342
1682
  accountId: payload.accountId,
343
1683
  subjectId: payload.subjectId,
344
1684
  ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
345
- permissions: payload.permissions,
1685
+ permissions: restricted ? [] : payload.permissions,
346
1686
  },
347
1687
  ],
348
1688
  workspaceGrants: [
@@ -351,7 +1691,7 @@ async function delegatedAccessContext(
351
1691
  accountId: payload.accountId,
352
1692
  subjectId: payload.subjectId,
353
1693
  ...(payload.subjectLabel ? { subjectLabel: payload.subjectLabel } : {}),
354
- permissions: payload.permissions,
1694
+ permissions: workspacePermissions,
355
1695
  principalKind: payload.principalKind,
356
1696
  // sessionId is worker-asserted (HMAC-signed token claim), not agent
357
1697
  // controlled; it scopes session-bound MCP tools such as goal management.
@@ -384,6 +1724,8 @@ async function delegatedAccessContext(
384
1724
  defaultAccountId: payload.accountId,
385
1725
  defaultWorkspaceId: payload.workspaceId,
386
1726
  };
1727
+ if (restricted) developerSetupApiKeyContexts.add(context);
1728
+ return context;
387
1729
  }
388
1730
 
389
1731
  function configuredSubject(c: Context): string {