@opengeni/api-router 2.2.0-canary.0 → 2.3.2-canary.1
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.d.ts +5 -2
- package/dist/auth/managed-email.d.ts +29 -0
- package/dist/auth/organization-user-setup.d.ts +57 -0
- package/dist/{chunk-3TP54PPX.js → chunk-IBV7Z6F4.js} +3545 -2009
- package/dist/chunk-IBV7Z6F4.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-app-home.d.ts +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/routes/automations.d.ts +13 -0
- package/dist/routes/insights.d.ts +2 -1
- package/dist/routes/managed-onboarding.d.ts +29 -0
- package/dist/routes/pr-review-github.d.ts +3 -0
- package/package.json +18 -18
- package/src/app.ts +56 -5
- package/src/auth/managed-auth.ts +29 -34
- package/src/auth/managed-email.ts +174 -0
- package/src/auth/organization-user-setup.ts +217 -0
- package/src/http/auth.ts +15 -0
- package/src/http/sse.ts +62 -13
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/mcp/company-brain-governed-writes.ts +4 -4
- package/src/mcp/company-profile-agent-admin.ts +11 -18
- package/src/mcp/remember.ts +4 -4
- package/src/mcp/server.ts +50 -4
- package/src/mcp/session-view.ts +8 -2
- package/src/routes/automations.ts +3 -3
- package/src/routes/documents.ts +2 -0
- package/src/routes/insights.ts +61 -19
- package/src/routes/managed-onboarding.ts +317 -0
- package/src/routes/organization-memberships.ts +212 -155
- package/src/routes/pr-review-github.ts +844 -0
- package/src/routes/pr-review.ts +20 -0
- package/src/routes/rigs.ts +37 -4
- package/src/routes/sessions.ts +101 -0
- package/src/sandbox/channel-a.ts +34 -7
- package/src/sandbox/viewer.ts +47 -11
- package/dist/chunk-3TP54PPX.js.map +0 -1
package/dist/index.js
CHANGED
package/dist/mcp/server.d.ts
CHANGED
|
@@ -95,7 +95,7 @@ export declare function capSessionDiscoveryPage(page: Awaited<ReturnType<typeof
|
|
|
95
95
|
truncated: boolean;
|
|
96
96
|
};
|
|
97
97
|
latestMessage?: {
|
|
98
|
-
type: "agent.message.completed" | "agent.message.delta" | "agent.model.request" | "agent.model.usage" | "agent.reasoning.delta" | "agent.toolCall.created" | "agent.toolCall.output" | "agent.updated" | "artifact.created" | "codex.account.switched" | "codex.capacity.resumed" | "codex.capacity.superseded" | "codex.capacity.waiting" | "codex.credential.selected" | "codex.fleet.decision" | "credential.auth_needed" | "fs.changed" | "git.changed" | "goal.cleared" | "goal.completed" | "goal.continuation" | "goal.held" | "goal.paused" | "goal.progress" | "goal.resumed" | "goal.rewrite.proposed" | "goal.rewrite.rejected" | "goal.set" | "goal.updated" | "machine.link.lost" | "machine.link.restored" | "machine.op.failed" | "machine.op.recovered" | "machine.runner.restarted" | "memory.corrected" | "memory.saved" | "recording.available" | "recording.failed" | "recording.started" | "rig.setup.completed" | "rig.setup.failed" | "rig.setup.skipped" | "rig.setup.started" | "sandbox.box.created" | "sandbox.box.lost" | "sandbox.box.snapshot" | "sandbox.box.terminated" | "sandbox.command.output.delta" | "sandbox.env.drift" | "sandbox.operation.completed" | "sandbox.operation.failed" | "sandbox.operation.started" | "session.command.backgrounded" | "session.command.finished" | "session.context.cleared" | "session.context.compacted" | "session.context.compaction.requested" | "session.context.compaction.skipped" | "session.context.compaction.started" | "session.control.paused" | "session.control.resumed" | "session.control.steer_requested" | "session.created" | "session.event.envelope_omitted" | "session.humanInput.requested" | "session.mcp.approval_policy.updated" | "session.personal_resources.attached" | "session.queue.changed" | "session.queue.history" | "session.queue.prompt.cancelled" | "session.realtime.ended" | "session.realtime.started" | "session.requiresAction" | "session.route.reconciled" | "session.status.changed" | "session.title_set" | "session.tool_policy.updated" | "session.visibility.changed" | "stream.closed" | "stream.opened" | "stream.revoked" | "stream.url.rotated" | "system.update.cancelled" | "system.update.delivered" | "system.update.pending" | "system.update.settled" | "system.update.superseded" | "terminal.pty.exited" | "terminal.pty.output.delta" | "terminal.pty.started" | "tool.auth_needed" | "turn.cancelled" | "turn.capacity_waiting" | "turn.completed" | "turn.event.rejected_late" | "turn.failed" | "turn.queued" | "turn.recovery.requested" | "turn.started" | "turn.startup.phase.completed" | "turn.startup.phase.failed" | "turn.startup.phase.started" | "turn.superseded" | "user.approvalDecision" | "user.humanInputResponse" | "user.message" | "user.pause" | "workspace.inference.paused" | "workspace.inference.resumed" | "workspace.revision.captured" | "workspace.revision.degraded";
|
|
98
|
+
type: "agent.message.completed" | "agent.message.delta" | "agent.model.request" | "agent.model.usage" | "agent.reasoning.delta" | "agent.toolCall.created" | "agent.toolCall.output" | "agent.updated" | "artifact.created" | "codex.account.switched" | "codex.capacity.resumed" | "codex.capacity.superseded" | "codex.capacity.waiting" | "codex.credential.selected" | "codex.fleet.decision" | "credential.auth_needed" | "fs.changed" | "git.changed" | "goal.cleared" | "goal.completed" | "goal.continuation" | "goal.held" | "goal.paused" | "goal.progress" | "goal.resumed" | "goal.rewrite.proposed" | "goal.rewrite.rejected" | "goal.set" | "goal.updated" | "machine.link.lost" | "machine.link.restored" | "machine.op.failed" | "machine.op.recovered" | "machine.runner.restarted" | "memory.corrected" | "memory.saved" | "recording.available" | "recording.failed" | "recording.started" | "rig.setup.completed" | "rig.setup.failed" | "rig.setup.skipped" | "rig.setup.started" | "sandbox.box.created" | "sandbox.box.lost" | "sandbox.box.snapshot" | "sandbox.box.terminated" | "sandbox.command.output.delta" | "sandbox.env.drift" | "sandbox.operation.completed" | "sandbox.operation.failed" | "sandbox.operation.started" | "session.command.backgrounded" | "session.command.finished" | "session.context.cleared" | "session.context.compacted" | "session.context.compaction.requested" | "session.context.compaction.skipped" | "session.context.compaction.started" | "session.control.paused" | "session.control.resumed" | "session.control.steer_requested" | "session.created" | "session.event.envelope_omitted" | "session.humanInput.requested" | "session.mcp.approval_policy.updated" | "session.personal_resources.attached" | "session.queue.changed" | "session.queue.history" | "session.queue.prompt.cancelled" | "session.realtime.ended" | "session.realtime.started" | "session.requiresAction" | "session.route.reconciled" | "session.runtime.configured" | "session.status.changed" | "session.title_set" | "session.tool_policy.updated" | "session.variable_sets.updated" | "session.visibility.changed" | "stream.closed" | "stream.opened" | "stream.revoked" | "stream.url.rotated" | "system.update.cancelled" | "system.update.delivered" | "system.update.pending" | "system.update.settled" | "system.update.superseded" | "terminal.pty.exited" | "terminal.pty.output.delta" | "terminal.pty.started" | "tool.auth_needed" | "turn.cancelled" | "turn.capacity_waiting" | "turn.completed" | "turn.event.rejected_late" | "turn.failed" | "turn.queued" | "turn.recovery.requested" | "turn.started" | "turn.startup.phase.completed" | "turn.startup.phase.failed" | "turn.startup.phase.started" | "turn.superseded" | "user.approvalDecision" | "user.humanInputResponse" | "user.message" | "user.pause" | "workspace.inference.paused" | "workspace.inference.resumed" | "workspace.revision.captured" | "workspace.revision.degraded";
|
|
99
99
|
preview: string | null;
|
|
100
100
|
previewTruncated: boolean;
|
|
101
101
|
} | null;
|
|
@@ -88,6 +88,7 @@ export declare function boundSessionDetailMcp(session: Session, effectiveControl
|
|
|
88
88
|
sandboxGroupId: string;
|
|
89
89
|
activeSandboxId: string | null;
|
|
90
90
|
activeEpoch: number;
|
|
91
|
+
variableSetIds: string[];
|
|
91
92
|
variableSetId: string | null;
|
|
92
93
|
environmentId: string | null;
|
|
93
94
|
rigId: string | null;
|
|
@@ -1,4 +1,17 @@
|
|
|
1
|
+
import { AutomationNormalizedEvent } from "@opengeni/contracts";
|
|
2
|
+
import { type AutomationSourceSecret } from "@opengeni/db";
|
|
1
3
|
import { type ApiRouteDeps } from "@opengeni/core";
|
|
2
4
|
import type { Hono } from "hono";
|
|
3
5
|
export declare function registerAutomationRoutes(app: Hono, deps: ApiRouteDeps): void;
|
|
6
|
+
export declare function acceptAutomationEvent(deps: ApiRouteDeps, source: AutomationSourceSecret, input: {
|
|
7
|
+
deliveryKey: string;
|
|
8
|
+
requestDigest: string;
|
|
9
|
+
normalizedEvent: AutomationNormalizedEvent;
|
|
10
|
+
}): Promise<{
|
|
11
|
+
accepted: boolean;
|
|
12
|
+
duplicate: boolean;
|
|
13
|
+
ignoredReason: string | null;
|
|
14
|
+
eventId: string | null;
|
|
15
|
+
runIds: string[];
|
|
16
|
+
}>;
|
|
4
17
|
export declare function readAutomationWebhookBody(request: Request, maxBytes: number): Promise<Uint8Array>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { type ApiRouteDeps } from "@opengeni/core";
|
|
1
|
+
import { type ApiRouteDeps, type WorkspaceInsightsFilterField } from "@opengeni/core";
|
|
2
2
|
import type { Hono } from "hono";
|
|
3
|
+
export declare function normalizeWorkspaceInsightsQueryFilter(value: string | null | undefined, field: WorkspaceInsightsFilterField): string | null;
|
|
3
4
|
export declare function registerInsightsRoutes(app: Hono, deps: ApiRouteDeps): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ApiRouteDeps } from "@opengeni/core";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
export type ManagedOnboardingRouteOptions = {
|
|
4
|
+
accountSetupLimiter?: {
|
|
5
|
+
take(key: string): boolean;
|
|
6
|
+
};
|
|
7
|
+
hashPassword?: (password: string) => Promise<string>;
|
|
8
|
+
};
|
|
9
|
+
export declare function registerManagedOnboardingRoutes(app: Hono, deps: ApiRouteDeps, options?: ManagedOnboardingRouteOptions): void;
|
|
10
|
+
/**
|
|
11
|
+
* Bounded application-tier protection for the public setup bearer endpoint.
|
|
12
|
+
* The global bucket prevents spoofed/high-cardinality client keys from
|
|
13
|
+
* multiplying password-hash work, while the per-client bucket limits bursts.
|
|
14
|
+
* A full key map rejects new clients until an idle bucket can be pruned.
|
|
15
|
+
*/
|
|
16
|
+
export declare class PublicSetupRateLimiter {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private readonly buckets;
|
|
19
|
+
private global;
|
|
20
|
+
constructor(options?: {
|
|
21
|
+
globalCapacity?: number;
|
|
22
|
+
globalRefillPerSecond?: number;
|
|
23
|
+
clientCapacity?: number;
|
|
24
|
+
clientRefillPerSecond?: number;
|
|
25
|
+
maxClientKeys?: number;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
});
|
|
28
|
+
take(key: string): boolean;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "2.2
|
|
3
|
+
"version": "2.3.2-canary.1",
|
|
4
4
|
"description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -43,23 +43,23 @@
|
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@llamaindex/liteparse": "^1.5.3",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
-
"@opengeni/agent-proto": "^0.5.1-canary.
|
|
47
|
-
"@opengeni/artifact-tool": "^0.3.
|
|
48
|
-
"@opengeni/capabilities": "^0.3.1-canary.
|
|
49
|
-
"@opengeni/codemode": "^0.4.
|
|
50
|
-
"@opengeni/codex": "^0.2.19-canary.
|
|
51
|
-
"@opengeni/config": "^0.
|
|
52
|
-
"@opengeni/contracts": "^2.
|
|
53
|
-
"@opengeni/core": "^2.
|
|
54
|
-
"@opengeni/db": "^3.
|
|
55
|
-
"@opengeni/documents": "^0.
|
|
56
|
-
"@opengeni/events": "^0.
|
|
57
|
-
"@opengeni/github": "^0.5.
|
|
58
|
-
"@opengeni/network": "^0.2.3-canary.
|
|
59
|
-
"@opengeni/observability": "^0.8.
|
|
60
|
-
"@opengeni/runtime": "^1.
|
|
61
|
-
"@opengeni/storage": "^0.2.
|
|
62
|
-
"@opengeni/xai-subscription": "^0.1.2-canary.
|
|
46
|
+
"@opengeni/agent-proto": "^0.5.1-canary.3",
|
|
47
|
+
"@opengeni/artifact-tool": "^0.3.7-canary.1",
|
|
48
|
+
"@opengeni/capabilities": "^0.3.1-canary.2",
|
|
49
|
+
"@opengeni/codemode": "^0.4.15-canary.1",
|
|
50
|
+
"@opengeni/codex": "^0.2.19-canary.2",
|
|
51
|
+
"@opengeni/config": "^0.20.1-canary.1",
|
|
52
|
+
"@opengeni/contracts": "^2.5.0-canary.1",
|
|
53
|
+
"@opengeni/core": "^2.4.0-canary.1",
|
|
54
|
+
"@opengeni/db": "^3.4.0-canary.1",
|
|
55
|
+
"@opengeni/documents": "^0.8.2-canary.1",
|
|
56
|
+
"@opengeni/events": "^0.4.0-canary.1",
|
|
57
|
+
"@opengeni/github": "^0.5.5-canary.1",
|
|
58
|
+
"@opengeni/network": "^0.2.3-canary.2",
|
|
59
|
+
"@opengeni/observability": "^0.8.6-canary.1",
|
|
60
|
+
"@opengeni/runtime": "^1.4.0-canary.1",
|
|
61
|
+
"@opengeni/storage": "^0.2.108-canary.1",
|
|
62
|
+
"@opengeni/xai-subscription": "^0.1.2-canary.2",
|
|
63
63
|
"@temporalio/client": "^1.17.0",
|
|
64
64
|
"better-auth": "^1.6.14",
|
|
65
65
|
"hono": "^4.12.18",
|
package/src/app.ts
CHANGED
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
rlsContextForWorkspace,
|
|
41
41
|
withSessionRlsActorContext,
|
|
42
42
|
} from "@opengeni/db";
|
|
43
|
+
import { requireSessionEventDurableFanoutCapability } from "@opengeni/events";
|
|
43
44
|
import { createObservability } from "@opengeni/observability";
|
|
44
45
|
import { createObjectStorage } from "@opengeni/storage";
|
|
45
46
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
@@ -63,6 +64,8 @@ import {
|
|
|
63
64
|
SessionAuthorizationUnavailableError,
|
|
64
65
|
} from "@opengeni/core";
|
|
65
66
|
import { createManagedAuth } from "./auth/managed-auth";
|
|
67
|
+
import { createManagedEmailTransport } from "./auth/managed-email";
|
|
68
|
+
import { assertManagedEmailTransportMetadata } from "./auth/organization-user-setup";
|
|
66
69
|
import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
|
|
67
70
|
import { requireLimit } from "@opengeni/core";
|
|
68
71
|
import { buildOpenGeniMcpServer } from "./mcp/server";
|
|
@@ -132,6 +135,7 @@ import { registerEditableArtifactRoutes } from "./routes/editable-artifacts";
|
|
|
132
135
|
import { registerVideoGenerationRoutes } from "./routes/video-generation";
|
|
133
136
|
import { registerCanonicalHumanIdentityRoutes } from "./routes/canonical-human-identities";
|
|
134
137
|
import { registerOrganizationMembershipRoutes } from "./routes/organization-memberships";
|
|
138
|
+
import { registerManagedOnboardingRoutes } from "./routes/managed-onboarding";
|
|
135
139
|
import { registerUserResourceAuthorityRoutes } from "./routes/user-resource-authorities";
|
|
136
140
|
import { registerConnectionAuthorityRoutes } from "./routes/connection-authorities";
|
|
137
141
|
import { projectClientModel } from "./model-catalog";
|
|
@@ -193,7 +197,11 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
193
197
|
// Pause) run inside API-originated db commands; install the boot-validated
|
|
194
198
|
// rollout flag once for this process.
|
|
195
199
|
configureChildLifecycleNotices({ enabled: deps.settings.childLifecycleNoticesEnabled });
|
|
196
|
-
const
|
|
200
|
+
const managedEmailTransport =
|
|
201
|
+
deps.managedEmailTransport ?? createManagedEmailTransport(deps.settings);
|
|
202
|
+
assertManagedEmailTransportMetadata(managedEmailTransport);
|
|
203
|
+
const managedAuth =
|
|
204
|
+
deps.managedAuth ?? createManagedAuth(deps.settings, deps.db, managedEmailTransport);
|
|
197
205
|
const objectStorage =
|
|
198
206
|
deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
|
|
199
207
|
let documentServices: DocumentServices | null = deps.documentServices ?? null;
|
|
@@ -296,6 +304,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
296
304
|
githubStateSecret:
|
|
297
305
|
deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
|
|
298
306
|
managedAuth,
|
|
307
|
+
managedEmailTransport,
|
|
299
308
|
objectStorage,
|
|
300
309
|
documentIndexer,
|
|
301
310
|
getDocumentServices,
|
|
@@ -486,6 +495,9 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
486
495
|
await next();
|
|
487
496
|
});
|
|
488
497
|
|
|
498
|
+
// These product-owned auth routes must be registered before Better Auth's
|
|
499
|
+
// wildcard handler or the provider returns its own 404 first.
|
|
500
|
+
registerManagedOnboardingRoutes(app, routeDeps);
|
|
489
501
|
if (managedAuth) {
|
|
490
502
|
app.on(["GET", "POST"], "/v1/auth/*", (c) => managedAuth.handler(c.req.raw));
|
|
491
503
|
}
|
|
@@ -1114,19 +1126,23 @@ type ReadinessChecks = Record<ReadinessCheckName, ReadinessCheck>;
|
|
|
1114
1126
|
type ReadinessCheckResult = { ok: boolean; error?: string };
|
|
1115
1127
|
|
|
1116
1128
|
function readinessChecks(deps: AppDependencies): ReadinessChecks {
|
|
1129
|
+
const configuredNatsCheck = deps.readinessChecks?.nats;
|
|
1117
1130
|
return {
|
|
1118
1131
|
db:
|
|
1119
1132
|
deps.readinessChecks?.db ??
|
|
1120
1133
|
(async () => {
|
|
1121
1134
|
await deps.db.execute(dbSql`select 1`);
|
|
1122
1135
|
}),
|
|
1123
|
-
nats:
|
|
1124
|
-
deps.
|
|
1125
|
-
(
|
|
1136
|
+
nats: async () => {
|
|
1137
|
+
requireSessionEventDurableFanoutCapability(deps.bus);
|
|
1138
|
+
if (configuredNatsCheck) {
|
|
1139
|
+
await configuredNatsCheck();
|
|
1140
|
+
} else {
|
|
1126
1141
|
if (deps.bus.isConnected && !deps.bus.isConnected()) {
|
|
1127
1142
|
throw new Error("NATS is not connected");
|
|
1128
1143
|
}
|
|
1129
|
-
}
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1130
1146
|
temporal:
|
|
1131
1147
|
deps.readinessChecks?.temporal ??
|
|
1132
1148
|
deps.workflowClient.check ??
|
|
@@ -1637,6 +1653,22 @@ const routeLabelPatterns: Array<{
|
|
|
1637
1653
|
pattern: /^\/v1\/workspaces\/[^/]+\/github\/app-manifest$/,
|
|
1638
1654
|
label: "/v1/workspaces/:workspaceId/github/app-manifest",
|
|
1639
1655
|
},
|
|
1656
|
+
{
|
|
1657
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github$/,
|
|
1658
|
+
label: "/v1/workspaces/:workspaceId/pr-review/github",
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/connect$/,
|
|
1662
|
+
label: "/v1/workspaces/:workspaceId/pr-review/github/connect",
|
|
1663
|
+
},
|
|
1664
|
+
{
|
|
1665
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/installations\/select$/,
|
|
1666
|
+
label: "/v1/workspaces/:workspaceId/pr-review/github/installations/select",
|
|
1667
|
+
},
|
|
1668
|
+
{
|
|
1669
|
+
pattern: /^\/v1\/workspaces\/[^/]+\/pr-review\/github\/installations\/[^/]+\/configure$/,
|
|
1670
|
+
label: "/v1/workspaces/:workspaceId/pr-review/github/installations/:installationId/configure",
|
|
1671
|
+
},
|
|
1640
1672
|
{
|
|
1641
1673
|
pattern: /^\/v1\/workspaces\/[^/]+\/capabilities$/,
|
|
1642
1674
|
label: "/v1/workspaces/:workspaceId/capabilities",
|
|
@@ -1929,6 +1961,19 @@ const routeLabelPatterns: Array<{
|
|
|
1929
1961
|
pattern: /^\/v1\/github\/oauth\/callback$/,
|
|
1930
1962
|
label: "/v1/github/oauth/callback",
|
|
1931
1963
|
},
|
|
1964
|
+
{ pattern: /^\/v1\/pr-review\/github\/setup$/, label: "/v1/pr-review/github/setup" },
|
|
1965
|
+
{
|
|
1966
|
+
pattern: /^\/v1\/pr-review\/github\/install\/callback$/,
|
|
1967
|
+
label: "/v1/pr-review/github/install/callback",
|
|
1968
|
+
},
|
|
1969
|
+
{
|
|
1970
|
+
pattern: /^\/v1\/pr-review\/github\/oauth\/callback$/,
|
|
1971
|
+
label: "/v1/pr-review/github/oauth/callback",
|
|
1972
|
+
},
|
|
1973
|
+
{
|
|
1974
|
+
pattern: /^\/v1\/webhooks\/pr-review\/github$/,
|
|
1975
|
+
label: "/v1/webhooks/pr-review/github",
|
|
1976
|
+
},
|
|
1932
1977
|
{
|
|
1933
1978
|
pattern: /^\/v1\/workspaces\/[^/]+$/,
|
|
1934
1979
|
label: "/v1/workspaces/:workspaceId",
|
|
@@ -1957,6 +2002,12 @@ export function isApiContractProtectedMutation(method: string, pathname: string)
|
|
|
1957
2002
|
if (!pathname.startsWith("/v1/")) {
|
|
1958
2003
|
return false;
|
|
1959
2004
|
}
|
|
2005
|
+
if (
|
|
2006
|
+
pathname === "/v1/auth/organization-onboarding" ||
|
|
2007
|
+
pathname === "/v1/auth/organization-setup"
|
|
2008
|
+
) {
|
|
2009
|
+
return true;
|
|
2010
|
+
}
|
|
1960
2011
|
if (
|
|
1961
2012
|
pathname.startsWith("/v1/auth/") ||
|
|
1962
2013
|
pathname.startsWith("/v1/webhooks/") ||
|
package/src/auth/managed-auth.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type ManagedAuth,
|
|
4
|
+
type ManagedEmailMessage,
|
|
5
|
+
type ManagedEmailTransport,
|
|
6
|
+
} from "@opengeni/core";
|
|
3
7
|
import type { Database } from "@opengeni/db";
|
|
4
8
|
import { ensureManagedAccessForUser } from "@opengeni/db";
|
|
5
9
|
import {
|
|
@@ -9,8 +13,8 @@ import {
|
|
|
9
13
|
} from "@opengeni/db/canonical-human-identities";
|
|
10
14
|
import { betterAuth } from "better-auth";
|
|
11
15
|
import { createEmailVerificationToken } from "better-auth/api";
|
|
16
|
+
import { hashPassword } from "better-auth/crypto";
|
|
12
17
|
import { Pool } from "pg";
|
|
13
|
-
import { Resend } from "resend";
|
|
14
18
|
|
|
15
19
|
import { decideCanonicalHumanSessionAdmission } from "./canonical-human-session-admission";
|
|
16
20
|
|
|
@@ -34,7 +38,16 @@ export function managedAuthUserCreateOverride(
|
|
|
34
38
|
return { data: { ...user, emailVerified: true } };
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
/** Keep Better Auth password policy and storage format behind this boundary. */
|
|
42
|
+
export async function hashManagedAuthPassword(password: string): Promise<string> {
|
|
43
|
+
return await hashPassword(password);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createManagedAuth(
|
|
47
|
+
settings: Settings,
|
|
48
|
+
db: Database,
|
|
49
|
+
managedEmailTransport: ManagedEmailTransport,
|
|
50
|
+
): ManagedAuth | null {
|
|
38
51
|
if (settings.productAccessMode !== "managed") {
|
|
39
52
|
return null;
|
|
40
53
|
}
|
|
@@ -147,7 +160,8 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
|
|
|
147
160
|
onExistingUserSignUp: async ({ user }) => {
|
|
148
161
|
if (requireEmailVerification && !user.emailVerified) {
|
|
149
162
|
const url = await verificationUrl(settings, user.email);
|
|
150
|
-
await
|
|
163
|
+
await sendManagedAuthEmail(managedEmailTransport, {
|
|
164
|
+
kind: "email_verification",
|
|
151
165
|
to: user.email,
|
|
152
166
|
subject: "Verify your OpenGeni email",
|
|
153
167
|
text: `Verify your OpenGeni email: ${url}`,
|
|
@@ -156,7 +170,8 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
|
|
|
156
170
|
}
|
|
157
171
|
},
|
|
158
172
|
sendResetPassword: async ({ user, url }) => {
|
|
159
|
-
await
|
|
173
|
+
await sendManagedAuthEmail(managedEmailTransport, {
|
|
174
|
+
kind: "password_reset",
|
|
160
175
|
to: user.email,
|
|
161
176
|
subject: "Reset your OpenGeni password",
|
|
162
177
|
text: `Reset your OpenGeni password: ${url}`,
|
|
@@ -167,7 +182,8 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
|
|
|
167
182
|
emailVerification: {
|
|
168
183
|
sendOnSignUp: requireEmailVerification,
|
|
169
184
|
sendVerificationEmail: async ({ user, url }) => {
|
|
170
|
-
await
|
|
185
|
+
await sendManagedAuthEmail(managedEmailTransport, {
|
|
186
|
+
kind: "email_verification",
|
|
171
187
|
to: user.email,
|
|
172
188
|
subject: "Verify your OpenGeni email",
|
|
173
189
|
text: `Verify your OpenGeni email: ${url}`,
|
|
@@ -180,6 +196,7 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
|
|
|
180
196
|
email: user.email,
|
|
181
197
|
name: user.name,
|
|
182
198
|
emailVerified: true,
|
|
199
|
+
provisionFallbackOrganization: false,
|
|
183
200
|
});
|
|
184
201
|
},
|
|
185
202
|
},
|
|
@@ -266,6 +283,7 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
|
|
|
266
283
|
email: user.email,
|
|
267
284
|
name: user.name,
|
|
268
285
|
emailVerified: true,
|
|
286
|
+
provisionFallbackOrganization: false,
|
|
269
287
|
});
|
|
270
288
|
},
|
|
271
289
|
},
|
|
@@ -297,35 +315,12 @@ function betterAuthTrustedOrigins(settings: Settings): string[] {
|
|
|
297
315
|
return [...origins];
|
|
298
316
|
}
|
|
299
317
|
|
|
300
|
-
async function
|
|
301
|
-
|
|
302
|
-
input:
|
|
303
|
-
to: string;
|
|
304
|
-
subject: string;
|
|
305
|
-
text: string;
|
|
306
|
-
html: string;
|
|
307
|
-
},
|
|
318
|
+
export async function sendManagedAuthEmail(
|
|
319
|
+
transport: ManagedEmailTransport,
|
|
320
|
+
input: Omit<ManagedEmailMessage, "from">,
|
|
308
321
|
): Promise<void> {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
console.warn(
|
|
312
|
-
`[opengeni] Skipping email to ${input.to}: OPENGENI_RESEND_API_KEY is not configured`,
|
|
313
|
-
);
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
throw new Error("OPENGENI_RESEND_API_KEY is required to send managed auth email");
|
|
317
|
-
}
|
|
318
|
-
const resend = new Resend(settings.resendApiKey);
|
|
319
|
-
const result = await resend.emails.send({
|
|
320
|
-
from: settings.emailFrom,
|
|
321
|
-
to: input.to,
|
|
322
|
-
subject: input.subject,
|
|
323
|
-
text: input.text,
|
|
324
|
-
html: input.html,
|
|
325
|
-
});
|
|
326
|
-
if (result.error) {
|
|
327
|
-
throw new Error(result.error.message);
|
|
328
|
-
}
|
|
322
|
+
const result = await transport.send({ ...input, from: transport.sender });
|
|
323
|
+
if (result.status !== "sent") throw new Error(`managed email ${result.status}`);
|
|
329
324
|
}
|
|
330
325
|
|
|
331
326
|
async function verificationUrl(settings: Settings, email: string): Promise<string> {
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import type {
|
|
3
|
+
ManagedEmailDeliveryResult,
|
|
4
|
+
ManagedEmailMessage,
|
|
5
|
+
ManagedEmailTransport,
|
|
6
|
+
} from "@opengeni/core";
|
|
7
|
+
import { createHmac } from "node:crypto";
|
|
8
|
+
import { Resend } from "resend";
|
|
9
|
+
|
|
10
|
+
const CAPTURE_MAX_MESSAGES = 250;
|
|
11
|
+
const CAPTURE_TTL_MS = 15 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
export type CapturedManagedEmail = ManagedEmailMessage & {
|
|
14
|
+
capturedAt: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Process-local test/development transport. Captures are count/TTL bounded and
|
|
19
|
+
* one-time readable; there is deliberately no route, disk, database, or log
|
|
20
|
+
* projection for message bodies or setup bearers.
|
|
21
|
+
*/
|
|
22
|
+
export class InMemoryManagedEmailTransport implements ManagedEmailTransport {
|
|
23
|
+
private readonly messages: CapturedManagedEmail[] = [];
|
|
24
|
+
readonly sender: string;
|
|
25
|
+
readonly idempotency: ManagedEmailTransport["idempotency"];
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
private readonly options: {
|
|
29
|
+
maxMessages?: number;
|
|
30
|
+
ttlMs?: number;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
sender?: string;
|
|
33
|
+
idempotency?: ManagedEmailTransport["idempotency"];
|
|
34
|
+
} = {},
|
|
35
|
+
) {
|
|
36
|
+
this.sender = options.sender ?? "OpenGeni <auth@mail.opengeni.ai>";
|
|
37
|
+
this.idempotency = options.idempotency ?? {
|
|
38
|
+
scope: "opengeni-in-memory-v1",
|
|
39
|
+
retentionSeconds: 86_400,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async send(message: ManagedEmailMessage): Promise<ManagedEmailDeliveryResult> {
|
|
44
|
+
this.prune();
|
|
45
|
+
const maxMessages = this.options.maxMessages ?? CAPTURE_MAX_MESSAGES;
|
|
46
|
+
if (this.messages.length >= maxMessages) this.messages.splice(0, 1);
|
|
47
|
+
this.messages.push({
|
|
48
|
+
...message,
|
|
49
|
+
capturedAt: new Date(this.now()).toISOString(),
|
|
50
|
+
});
|
|
51
|
+
return { status: "sent", providerMessageId: null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
take(predicate: (message: CapturedManagedEmail) => boolean): CapturedManagedEmail | null {
|
|
55
|
+
this.prune();
|
|
56
|
+
const index = this.messages.findIndex(predicate);
|
|
57
|
+
if (index < 0) return null;
|
|
58
|
+
return this.messages.splice(index, 1)[0] ?? null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
size(): number {
|
|
62
|
+
this.prune();
|
|
63
|
+
return this.messages.length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private prune(): void {
|
|
67
|
+
const cutoff = this.now() - (this.options.ttlMs ?? CAPTURE_TTL_MS);
|
|
68
|
+
while (this.messages[0] && Date.parse(this.messages[0].capturedAt) <= cutoff) {
|
|
69
|
+
this.messages.shift();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private now(): number {
|
|
74
|
+
return this.options.now?.() ?? Date.now();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class UnconfiguredManagedEmailTransport implements ManagedEmailTransport {
|
|
79
|
+
readonly idempotency = {
|
|
80
|
+
scope: "opengeni-unconfigured-v1",
|
|
81
|
+
retentionSeconds: 0,
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
constructor(readonly sender: string) {}
|
|
85
|
+
|
|
86
|
+
async send(): Promise<ManagedEmailDeliveryResult> {
|
|
87
|
+
return { status: "failed", errorClass: "provider_not_configured" };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class ResendManagedEmailTransport implements ManagedEmailTransport {
|
|
92
|
+
private readonly client: Resend;
|
|
93
|
+
readonly idempotency: ManagedEmailTransport["idempotency"];
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
apiKey: string,
|
|
97
|
+
readonly sender: string,
|
|
98
|
+
scopeSecret: string,
|
|
99
|
+
) {
|
|
100
|
+
this.client = new Resend(apiKey);
|
|
101
|
+
this.idempotency = {
|
|
102
|
+
// The keyed digest binds this delivery to the Resend account without
|
|
103
|
+
// persisting an API-key derivative that can be tested offline.
|
|
104
|
+
scope: `resend-v1-24h:${createHmac("sha256", scopeSecret).update(apiKey).digest("hex")}`,
|
|
105
|
+
retentionSeconds: 86_400,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async send(message: ManagedEmailMessage): Promise<ManagedEmailDeliveryResult> {
|
|
110
|
+
if (message.from !== this.sender) {
|
|
111
|
+
return { status: "failed", errorClass: "sender_changed" };
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = await this.client.emails.send(
|
|
115
|
+
{
|
|
116
|
+
from: message.from,
|
|
117
|
+
to: message.to,
|
|
118
|
+
subject: message.subject,
|
|
119
|
+
text: message.text,
|
|
120
|
+
html: message.html,
|
|
121
|
+
},
|
|
122
|
+
message.idempotencyKey ? { idempotencyKey: message.idempotencyKey } : undefined,
|
|
123
|
+
);
|
|
124
|
+
if (!result.error) {
|
|
125
|
+
return { status: "sent", providerMessageId: result.data?.id ?? null };
|
|
126
|
+
}
|
|
127
|
+
const statusCode = "statusCode" in result.error ? Number(result.error.statusCode) : NaN;
|
|
128
|
+
return clearProviderRefusal(statusCode)
|
|
129
|
+
? { status: "failed", errorClass: boundedErrorClass(result.error.name, "provider_refused") }
|
|
130
|
+
: {
|
|
131
|
+
status: "outcome_unknown",
|
|
132
|
+
errorClass: boundedErrorClass(result.error.name, "provider_ambiguous"),
|
|
133
|
+
};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return {
|
|
136
|
+
status: "outcome_unknown",
|
|
137
|
+
errorClass: boundedErrorClass(
|
|
138
|
+
error instanceof Error ? error.name : "transport_error",
|
|
139
|
+
"transport_error",
|
|
140
|
+
),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function createManagedEmailTransport(settings: Settings): ManagedEmailTransport {
|
|
147
|
+
if (settings.resendApiKey) {
|
|
148
|
+
if (!settings.betterAuthSecret) {
|
|
149
|
+
throw new Error("OPENGENI_BETTER_AUTH_SECRET is required for managed email delivery");
|
|
150
|
+
}
|
|
151
|
+
return new ResendManagedEmailTransport(
|
|
152
|
+
settings.resendApiKey,
|
|
153
|
+
settings.emailFrom,
|
|
154
|
+
settings.betterAuthSecret,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (settings.environment === "local" || settings.environment === "test") {
|
|
158
|
+
return new InMemoryManagedEmailTransport({ sender: settings.emailFrom });
|
|
159
|
+
}
|
|
160
|
+
return new UnconfiguredManagedEmailTransport(settings.emailFrom);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function clearProviderRefusal(statusCode: number): boolean {
|
|
164
|
+
return statusCode >= 400 && statusCode < 500 && ![408, 409, 425, 429].includes(statusCode);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function boundedErrorClass(value: string, fallback: string): string {
|
|
168
|
+
const normalized = value
|
|
169
|
+
.trim()
|
|
170
|
+
.toLowerCase()
|
|
171
|
+
.replaceAll(/[^a-z0-9_-]+/g, "_")
|
|
172
|
+
.slice(0, 64);
|
|
173
|
+
return normalized || fallback;
|
|
174
|
+
}
|