@opengeni/api-router 2.3.2-canary.2 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +4 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +4 -0
- package/dist/{chunk-IBV7Z6F4.js → chunk-QESX7HDK.js} +3645 -470
- package/dist/chunk-QESX7HDK.js.map +1 -0
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/mcp/server.d.ts +33 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/managed-auth-session-sets.d.ts +19 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/sessions.d.ts +17 -5
- package/dist/routes/workspaces.d.ts +1 -0
- package/dist/work-discovery-observability.d.ts +33 -0
- package/package.json +18 -18
- package/src/app.ts +133 -1
- package/src/auth/managed-auth-attempt-context.ts +24 -0
- package/src/auth/managed-auth-session-adapter.ts +205 -0
- package/src/auth/managed-auth.ts +52 -2
- package/src/fatal-process-boundary.ts +231 -0
- package/src/http/sse.ts +7 -0
- package/src/index.ts +25 -5
- package/src/integrations/slack-interactions.ts +30 -17
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/server.ts +349 -68
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/canonical-human-identities.ts +29 -14
- package/src/routes/codex.ts +5 -1
- package/src/routes/environments.ts +23 -0
- package/src/routes/interaction-resources.ts +3 -0
- package/src/routes/managed-auth-session-sets.ts +994 -0
- package/src/routes/managed-onboarding.ts +2 -0
- package/src/routes/organization-memberships.ts +2 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/sessions.ts +401 -120
- package/src/routes/supergrok.ts +5 -1
- package/src/routes/workspaces.ts +23 -1
- package/src/work-discovery-observability.ts +121 -0
- package/dist/chunk-IBV7Z6F4.js.map +0 -1
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Database } from "@opengeni/db";
|
|
2
|
+
import {
|
|
3
|
+
prepareOrganizationRecoveryNotifications,
|
|
4
|
+
settleOrganizationRecoveryNotification,
|
|
5
|
+
type OrganizationRecoveryNotificationClaim,
|
|
6
|
+
type OrganizationRecoveryNotificationSettlement,
|
|
7
|
+
} from "@opengeni/db";
|
|
8
|
+
|
|
9
|
+
export type OrganizationRecoveryNotificationDeliveryResult =
|
|
10
|
+
| { status: "sent"; providerMessageId: string | null }
|
|
11
|
+
| { status: "failed"; errorClass: string }
|
|
12
|
+
| { status: "outcome_unknown" };
|
|
13
|
+
|
|
14
|
+
export interface OrganizationRecoveryNotificationTransport {
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
send(
|
|
17
|
+
claim: OrganizationRecoveryNotificationClaim,
|
|
18
|
+
): Promise<OrganizationRecoveryNotificationDeliveryResult>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type OrganizationRecoveryNotificationLifecycle = {
|
|
22
|
+
prepare: typeof prepareOrganizationRecoveryNotifications;
|
|
23
|
+
settle: typeof settleOrganizationRecoveryNotification;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const productionLifecycle: OrganizationRecoveryNotificationLifecycle = {
|
|
27
|
+
prepare: prepareOrganizationRecoveryNotifications,
|
|
28
|
+
settle: settleOrganizationRecoveryNotification,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function dispatchOrganizationRecoveryNotifications(input: {
|
|
32
|
+
db: Database;
|
|
33
|
+
transport: OrganizationRecoveryNotificationTransport;
|
|
34
|
+
claimOwner: string;
|
|
35
|
+
limit?: number;
|
|
36
|
+
leaseSeconds?: number;
|
|
37
|
+
lifecycle?: OrganizationRecoveryNotificationLifecycle;
|
|
38
|
+
}): Promise<OrganizationRecoveryNotificationSettlement[]> {
|
|
39
|
+
const lifecycle = input.lifecycle ?? productionLifecycle;
|
|
40
|
+
const claims = await lifecycle.prepare(input.db, {
|
|
41
|
+
provider: input.transport.provider,
|
|
42
|
+
claimOwner: input.claimOwner,
|
|
43
|
+
limit: input.limit ?? 25,
|
|
44
|
+
leaseSeconds: input.leaseSeconds ?? 60,
|
|
45
|
+
});
|
|
46
|
+
return await Promise.all(
|
|
47
|
+
claims.map(async (claim) => {
|
|
48
|
+
let result: OrganizationRecoveryNotificationDeliveryResult;
|
|
49
|
+
try {
|
|
50
|
+
result = await input.transport.send(claim);
|
|
51
|
+
} catch {
|
|
52
|
+
// The provider may have accepted the stable idempotency key before the
|
|
53
|
+
// transport failed. Preserve ambiguity for explicit reconciliation.
|
|
54
|
+
result = { status: "outcome_unknown" };
|
|
55
|
+
}
|
|
56
|
+
return await lifecycle.settle(input.db, {
|
|
57
|
+
outboxId: claim.outboxId,
|
|
58
|
+
deliveryId: claim.deliveryId,
|
|
59
|
+
claimOwner: claim.claimOwner,
|
|
60
|
+
phase: result.status,
|
|
61
|
+
...(result.status === "sent" ? { providerMessageId: result.providerMessageId } : {}),
|
|
62
|
+
...(result.status === "failed" ? { errorClass: result.errorClass } : {}),
|
|
63
|
+
});
|
|
64
|
+
}),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class InMemoryOrganizationRecoveryNotificationTransport implements OrganizationRecoveryNotificationTransport {
|
|
69
|
+
readonly provider = "fake";
|
|
70
|
+
readonly attempts: Array<{
|
|
71
|
+
idempotencyKey: string;
|
|
72
|
+
payloadDigest: string;
|
|
73
|
+
recipientCanonicalIdentityId: string;
|
|
74
|
+
providerMessageId: string;
|
|
75
|
+
}> = [];
|
|
76
|
+
private readonly deliveries = new Map<string, string>();
|
|
77
|
+
private readonly scripted: OrganizationRecoveryNotificationDeliveryResult[] = [];
|
|
78
|
+
|
|
79
|
+
enqueue(...results: OrganizationRecoveryNotificationDeliveryResult[]): void {
|
|
80
|
+
this.scripted.push(...results);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
logicalDeliveryCount(): number {
|
|
84
|
+
return this.deliveries.size;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async send(
|
|
88
|
+
claim: OrganizationRecoveryNotificationClaim,
|
|
89
|
+
): Promise<OrganizationRecoveryNotificationDeliveryResult> {
|
|
90
|
+
const scripted = this.scripted.shift();
|
|
91
|
+
if (scripted) return scripted;
|
|
92
|
+
const providerMessageId =
|
|
93
|
+
this.deliveries.get(claim.idempotencyKey) ?? `fake:${crypto.randomUUID()}`;
|
|
94
|
+
this.deliveries.set(claim.idempotencyKey, providerMessageId);
|
|
95
|
+
this.attempts.push({
|
|
96
|
+
idempotencyKey: claim.idempotencyKey,
|
|
97
|
+
payloadDigest: claim.payloadDigest,
|
|
98
|
+
recipientCanonicalIdentityId: claim.recipientCanonicalIdentityId,
|
|
99
|
+
providerMessageId,
|
|
100
|
+
});
|
|
101
|
+
return { status: "sent", providerMessageId };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
CanonicalHumanIdentityProjection,
|
|
6
6
|
LinkCanonicalHumanLoginBindingRequest,
|
|
7
7
|
} from "@opengeni/contracts/canonical-human-identities";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getManagedAuthRequestActorLeaseStamp,
|
|
10
|
+
markManagedAuthRequestActorTransitionApplied,
|
|
11
|
+
requireCanonicalHumanRequestIdentity,
|
|
12
|
+
} from "@opengeni/core/canonical-human-identities";
|
|
9
13
|
import type { ApiRouteDeps } from "@opengeni/core";
|
|
10
14
|
import {
|
|
11
15
|
applyCanonicalHumanIdentityOperation,
|
|
@@ -13,6 +17,7 @@ import {
|
|
|
13
17
|
CanonicalHumanIdentityConflictError,
|
|
14
18
|
CanonicalHumanIdentityNotFoundError,
|
|
15
19
|
CanonicalHumanIdentityOperationReuseError,
|
|
20
|
+
CanonicalHumanIdentityMutationInFlightError,
|
|
16
21
|
getCanonicalHumanIdentityProjection,
|
|
17
22
|
} from "@opengeni/db/canonical-human-identities";
|
|
18
23
|
import type { Context, Hono } from "hono";
|
|
@@ -25,6 +30,8 @@ async function requestIdentity(context: Context, deps: ApiRouteDeps) {
|
|
|
25
30
|
return await requireCanonicalHumanRequestIdentity(context, {
|
|
26
31
|
db: deps.db,
|
|
27
32
|
...(deps.managedAuth === undefined ? {} : { managedAuth: deps.managedAuth }),
|
|
33
|
+
managedAuthSessionAdapter: deps.managedAuthSessionAdapter,
|
|
34
|
+
managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
|
|
28
35
|
allowRecovery: true,
|
|
29
36
|
});
|
|
30
37
|
}
|
|
@@ -50,6 +57,9 @@ function identityError(context: Context, error: unknown): Response {
|
|
|
50
57
|
if (error instanceof CanonicalHumanIdentityOperationReuseError) {
|
|
51
58
|
return context.json({ code: error.code, message: error.message }, 409);
|
|
52
59
|
}
|
|
60
|
+
if (error instanceof CanonicalHumanIdentityMutationInFlightError) {
|
|
61
|
+
return context.json({ code: error.code, message: error.message, retryable: true }, 409);
|
|
62
|
+
}
|
|
53
63
|
if (error instanceof CanonicalHumanIdentityNotFoundError) {
|
|
54
64
|
return context.json(
|
|
55
65
|
{ code: "CANONICAL_HUMAN_IDENTITY_NOT_FOUND", message: error.message },
|
|
@@ -79,21 +89,26 @@ async function mutate(
|
|
|
79
89
|
},
|
|
80
90
|
): Promise<Response> {
|
|
81
91
|
const identity = await requestIdentity(context, deps);
|
|
92
|
+
const actorFence = getManagedAuthRequestActorLeaseStamp(context.req.raw);
|
|
93
|
+
if (deps.settings.managedAuthSessionSetMode !== "legacy" && !actorFence) {
|
|
94
|
+
throw new HTTPException(409, { message: "Managed actor mutation fence is unavailable" });
|
|
95
|
+
}
|
|
82
96
|
try {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
),
|
|
97
|
+
const response = CanonicalHumanIdentityMutationResponse.parse(
|
|
98
|
+
await applyCanonicalHumanIdentityOperation(deps.db, {
|
|
99
|
+
operationId: input.operationId,
|
|
100
|
+
authUserId: identity.authUserId,
|
|
101
|
+
expectedIdentityRevision: input.expectedIdentityRevision,
|
|
102
|
+
operationType: input.operationType,
|
|
103
|
+
...(input.bindingId ? { bindingId: input.bindingId } : {}),
|
|
104
|
+
...(input.providerId ? { providerId: input.providerId } : {}),
|
|
105
|
+
...(input.providerAccountId ? { providerAccountId: input.providerAccountId } : {}),
|
|
106
|
+
reason: input.reason,
|
|
107
|
+
...(actorFence ? { actorFence } : {}),
|
|
108
|
+
}),
|
|
96
109
|
);
|
|
110
|
+
if (actorFence) markManagedAuthRequestActorTransitionApplied(context.req.raw);
|
|
111
|
+
return context.json(response);
|
|
97
112
|
} catch (error) {
|
|
98
113
|
return identityError(context, error);
|
|
99
114
|
}
|
package/src/routes/codex.ts
CHANGED
|
@@ -195,7 +195,11 @@ async function managedCookieHuman(
|
|
|
195
195
|
) {
|
|
196
196
|
return null;
|
|
197
197
|
}
|
|
198
|
-
const session = await getManagedSession(c, deps.managedAuth
|
|
198
|
+
const session = await getManagedSession(c, deps.managedAuth, {
|
|
199
|
+
db: deps.db,
|
|
200
|
+
sessionAdapter: deps.managedAuthSessionAdapter,
|
|
201
|
+
sessionSetMode: deps.settings.managedAuthSessionSetMode,
|
|
202
|
+
});
|
|
199
203
|
if (!session?.user?.id || !session.session?.id) return null;
|
|
200
204
|
return {
|
|
201
205
|
subjectId: `user:${session.user.id}`,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CreateVariableSetRequest,
|
|
3
|
+
ResolveVariableSetAttachmentsRequest,
|
|
4
|
+
ResolveVariableSetAttachmentsResponse,
|
|
3
5
|
SetVariableSetVariableRequest,
|
|
4
6
|
UpdateVariableSetRequest,
|
|
5
7
|
VariableSetVariableName,
|
|
@@ -14,6 +16,7 @@ import {
|
|
|
14
16
|
getVariableSetByName,
|
|
15
17
|
listVariableSets,
|
|
16
18
|
readVariableSetSecretAtomically,
|
|
19
|
+
resolveVariableSetAttachments,
|
|
17
20
|
setVariableSetVariable,
|
|
18
21
|
updateVariableSet,
|
|
19
22
|
VariableSetAttachedError,
|
|
@@ -135,6 +138,26 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
135
138
|
return c.json(created, 201);
|
|
136
139
|
});
|
|
137
140
|
|
|
141
|
+
if (prefix.endsWith("/variable-sets")) {
|
|
142
|
+
app.post(`${prefix}/resolve-attachments`, async (c) => {
|
|
143
|
+
const workspaceId = c.req.param("workspaceId")!;
|
|
144
|
+
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
145
|
+
requirePermission(grant, "variable-sets:attach");
|
|
146
|
+
requirePermission(grant, "variable-sets:use");
|
|
147
|
+
const payload = ResolveVariableSetAttachmentsRequest.parse(await c.req.json());
|
|
148
|
+
const variableSets = await resolveVariableSetAttachments(
|
|
149
|
+
db,
|
|
150
|
+
{
|
|
151
|
+
accountId: grant.accountId,
|
|
152
|
+
workspaceId,
|
|
153
|
+
subjectId: grant.subjectId,
|
|
154
|
+
},
|
|
155
|
+
payload.variableSetIds,
|
|
156
|
+
);
|
|
157
|
+
return c.json(ResolveVariableSetAttachmentsResponse.parse({ variableSets }));
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
138
161
|
app.get(`${prefix}/:variableSetId`, async (c) => {
|
|
139
162
|
const workspaceId = c.req.param("workspaceId")!;
|
|
140
163
|
const grant = await requireAccessGrant(c, deps, workspaceId);
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
updateSiteAuthConnection,
|
|
46
46
|
} from "@opengeni/db";
|
|
47
47
|
import {
|
|
48
|
+
getManagedAuthRequestActorEpoch,
|
|
48
49
|
SessionAuthorizationDeniedError,
|
|
49
50
|
SessionAuthorizationUnavailableError,
|
|
50
51
|
requireAccessGrant,
|
|
@@ -74,6 +75,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
|
|
|
74
75
|
context.req.raw.signal,
|
|
75
76
|
{
|
|
76
77
|
observability: deps.observability,
|
|
78
|
+
actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? undefined,
|
|
77
79
|
reauthorize: async () => {
|
|
78
80
|
const freshGrant = await requireFreshAccessGrant(
|
|
79
81
|
context,
|
|
@@ -98,6 +100,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
|
|
|
98
100
|
context.req.raw.signal,
|
|
99
101
|
{
|
|
100
102
|
observability: deps.observability,
|
|
103
|
+
actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? undefined,
|
|
101
104
|
reauthorize: async () => {
|
|
102
105
|
await requireFreshAccessGrant(context, deps, workspaceId, "sessions:read");
|
|
103
106
|
},
|