@opengeni/api-router 0.23.1 → 0.30.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.d.ts +3 -2
- package/dist/app.js +5 -3
- package/dist/auth/canonical-human-session-admission.d.ts +27 -0
- package/dist/browser-auth-broker.d.ts +20 -0
- package/dist/browser-controller-authority.d.ts +61 -0
- package/dist/browser-network-route.d.ts +8 -0
- package/dist/browser-state-authority.d.ts +27 -0
- package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
- package/dist/chunk-2PQMSRCB.js.map +1 -0
- package/dist/codemode.d.ts +23 -0
- package/dist/controller-data-plane.d.ts +12 -0
- package/dist/editable-artifact-live-hints.d.ts +11 -0
- package/dist/editable-artifact-native-kernel.d.ts +37 -0
- package/dist/editable-artifact-office-import.d.ts +22 -0
- package/dist/editable-artifact-production.d.ts +29 -0
- package/dist/editable-artifact-websocket.d.ts +49 -0
- package/dist/editable-artifact-workspace-files.d.ts +23 -0
- package/dist/github-browser-flow.d.ts +6 -0
- package/dist/http/auth.d.ts +1 -1
- package/dist/http/cors.d.ts +1 -0
- package/dist/http/sse.d.ts +18 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1796 -22
- package/dist/index.js.map +1 -1
- package/dist/integrations/api-integrations.d.ts +24 -0
- package/dist/integrations/atlassian.d.ts +176 -0
- package/dist/integrations/fiken.d.ts +233 -0
- package/dist/integrations/github-skill-source.d.ts +5 -0
- package/dist/integrations/google-drive.d.ts +85 -0
- package/dist/integrations/oauth-client.d.ts +22 -1
- package/dist/integrations/provider-oauth.d.ts +17 -0
- package/dist/integrations/slack-bot.d.ts +175 -1
- package/dist/integrations/slack-interactions.d.ts +24 -6
- package/dist/integrations/social-api.d.ts +2 -1
- package/dist/interaction-holder-heartbeat.d.ts +17 -0
- package/dist/interaction-metrics.d.ts +11 -0
- package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
- package/dist/mcp/editable-artifacts.d.ts +13 -0
- package/dist/mcp/scheduled-task-view.d.ts +168 -0
- package/dist/mcp/server.d.ts +16 -4
- package/dist/memory-slack-delivery.d.ts +9 -0
- package/dist/model-catalog.d.ts +1 -0
- package/dist/routes/api-integrations.d.ts +8 -0
- package/dist/routes/browser-identities.d.ts +5 -0
- package/dist/routes/browser-sessions.d.ts +6 -0
- package/dist/routes/canonical-human-identities.d.ts +3 -0
- package/dist/routes/channels.d.ts +3 -0
- package/dist/routes/company-profile.d.ts +3 -0
- package/dist/routes/computer-sessions.d.ts +6 -0
- package/dist/routes/editable-artifacts.d.ts +44 -0
- package/dist/routes/integration-facets.d.ts +3 -0
- package/dist/routes/interaction-resources.d.ts +5 -0
- package/dist/routes/memory-slack-publications.d.ts +6 -0
- package/dist/routes/organization-memberships.d.ts +3 -0
- package/dist/routes/plugins.d.ts +8 -0
- package/dist/routes/sessions.d.ts +18 -2
- package/dist/routes/skills.d.ts +6 -0
- package/dist/routes/slack-task-policy.d.ts +3 -0
- package/dist/routes/supergrok.d.ts +3 -0
- package/dist/routes/video-generation.d.ts +3 -0
- package/dist/sandbox/channel-a.d.ts +62 -2
- package/dist/sandbox/machines.d.ts +2 -2
- package/dist/sandbox/metrics-ingestion.d.ts +6 -1
- package/dist/sandbox/viewer.d.ts +7 -3
- package/dist/slack-reaction-files.d.ts +27 -0
- package/dist/temporal-schedule-cleanup.d.ts +26 -0
- package/dist/transcription/providers/xai-subscription.d.ts +8 -0
- package/dist/xai-realtime.d.ts +26 -0
- package/dist/xai-subscription-auth.d.ts +23 -0
- package/package.json +20 -14
- package/src/app.ts +604 -64
- package/src/auth/canonical-human-session-admission.ts +116 -0
- package/src/auth/managed-auth.ts +101 -0
- package/src/browser-auth-broker.ts +155 -0
- package/src/browser-controller-authority.ts +186 -0
- package/src/browser-network-route.ts +34 -0
- package/src/browser-state-authority.ts +236 -0
- package/src/codemode.ts +186 -0
- package/src/controller-data-plane.ts +35 -0
- package/src/editable-artifact-live-hints.ts +64 -0
- package/src/editable-artifact-native-kernel.ts +659 -0
- package/src/editable-artifact-office-import.ts +230 -0
- package/src/editable-artifact-production.ts +419 -0
- package/src/editable-artifact-websocket.ts +311 -0
- package/src/editable-artifact-workspace-files.ts +186 -0
- package/src/github-browser-flow.ts +35 -6
- package/src/http/auth.ts +29 -9
- package/src/http/cors.ts +3 -0
- package/src/http/sse.ts +229 -5
- package/src/index.ts +119 -20
- package/src/integrations/api-integrations.ts +365 -0
- package/src/integrations/atlassian.ts +1621 -0
- package/src/integrations/fiken.ts +1230 -0
- package/src/integrations/github-skill-source.ts +142 -0
- package/src/integrations/google-drive.ts +1005 -64
- package/src/integrations/oauth-client.ts +123 -33
- package/src/integrations/provider-oauth.ts +756 -0
- package/src/integrations/slack-bot.ts +669 -11
- package/src/integrations/slack-interactions.ts +2063 -93
- package/src/integrations/social-api.ts +11 -0
- package/src/interaction-holder-heartbeat.ts +95 -0
- package/src/interaction-metrics.ts +159 -0
- package/src/mcp/documents.ts +146 -6
- package/src/mcp/editable-artifact-query-schema.ts +236 -0
- package/src/mcp/editable-artifacts.ts +448 -0
- package/src/mcp/scheduled-task-view.ts +34 -0
- package/src/mcp/server.ts +1756 -355
- package/src/memory-slack-delivery.ts +209 -0
- package/src/model-catalog.ts +19 -6
- package/src/routes/api-integrations.ts +410 -0
- package/src/routes/browser-identities.ts +160 -0
- package/src/routes/browser-sessions.ts +4482 -0
- package/src/routes/canonical-human-identities.ts +156 -0
- package/src/routes/channels.ts +90 -0
- package/src/routes/company-profile.ts +255 -0
- package/src/routes/computer-sessions.ts +1602 -0
- package/src/routes/connections.ts +421 -99
- package/src/routes/documents.ts +22 -3
- package/src/routes/editable-artifacts.ts +1159 -0
- package/src/routes/files.ts +106 -4
- package/src/routes/github.ts +18 -2
- package/src/routes/install.ts +7 -1
- package/src/routes/integration-facets.ts +258 -0
- package/src/routes/interaction-resources.ts +595 -0
- package/src/routes/memory-slack-publications.ts +216 -0
- package/src/routes/organization-memberships.ts +53 -0
- package/src/routes/packs.ts +437 -7
- package/src/routes/plugins.ts +752 -0
- package/src/routes/rigs.ts +77 -20
- package/src/routes/scheduled-tasks.ts +67 -40
- package/src/routes/sessions.ts +830 -278
- package/src/routes/skills.ts +256 -0
- package/src/routes/slack-task-policy.ts +115 -0
- package/src/routes/supergrok.ts +717 -0
- package/src/routes/transcription-recordings.ts +9 -2
- package/src/routes/transcriptions.ts +2 -1
- package/src/routes/video-generation.ts +162 -0
- package/src/routes/workspaces.ts +68 -28
- package/src/sandbox/channel-a.ts +750 -116
- package/src/sandbox/machines.ts +5 -5
- package/src/sandbox/metrics-ingestion.ts +121 -3
- package/src/sandbox/rematerialize.ts +35 -47
- package/src/sandbox/viewer.ts +39 -12
- package/src/slack-reaction-files.ts +181 -0
- package/src/temporal-schedule-cleanup.ts +135 -0
- package/src/transcription/providers/xai-subscription.ts +110 -0
- package/src/transcription/service.ts +17 -2
- package/src/xai-realtime.ts +216 -0
- package/src/xai-subscription-auth.ts +132 -0
- package/dist/chunk-T4T2PGU4.js.map +0 -1
- package/dist/mcp/toolspace.d.ts +0 -71
- package/src/mcp/toolspace.ts +0 -1190
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
CanonicalHumanBindingOperationRequest,
|
|
4
|
+
CanonicalHumanIdentityMutationResponse,
|
|
5
|
+
CanonicalHumanIdentityProjection,
|
|
6
|
+
LinkCanonicalHumanLoginBindingRequest,
|
|
7
|
+
} from "@opengeni/contracts/canonical-human-identities";
|
|
8
|
+
import { requireCanonicalHumanRequestIdentity } from "@opengeni/core/canonical-human-identities";
|
|
9
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
10
|
+
import {
|
|
11
|
+
applyCanonicalHumanIdentityOperation,
|
|
12
|
+
CanonicalHumanIdentityAuthorityError,
|
|
13
|
+
CanonicalHumanIdentityConflictError,
|
|
14
|
+
CanonicalHumanIdentityNotFoundError,
|
|
15
|
+
CanonicalHumanIdentityOperationReuseError,
|
|
16
|
+
getCanonicalHumanIdentityProjection,
|
|
17
|
+
} from "@opengeni/db/canonical-human-identities";
|
|
18
|
+
import type { Context, Hono } from "hono";
|
|
19
|
+
import { HTTPException } from "hono/http-exception";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
const BindingId = z.string().uuid();
|
|
23
|
+
|
|
24
|
+
async function requestIdentity(context: Context, deps: ApiRouteDeps) {
|
|
25
|
+
return await requireCanonicalHumanRequestIdentity(context, {
|
|
26
|
+
db: deps.db,
|
|
27
|
+
...(deps.managedAuth === undefined ? {} : { managedAuth: deps.managedAuth }),
|
|
28
|
+
allowRecovery: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
|
|
33
|
+
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
34
|
+
if (!parsed.success) {
|
|
35
|
+
throw new HTTPException(422, { message: "Invalid canonical human identity request" });
|
|
36
|
+
}
|
|
37
|
+
return parsed.data;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function bindingId(context: Context): string {
|
|
41
|
+
const parsed = BindingId.safeParse(context.req.param("bindingId"));
|
|
42
|
+
if (!parsed.success) throw new HTTPException(422, { message: "Invalid login binding id" });
|
|
43
|
+
return parsed.data;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function identityError(context: Context, error: unknown): Response {
|
|
47
|
+
if (error instanceof CanonicalHumanIdentityConflictError) {
|
|
48
|
+
return context.json({ code: error.code, message: error.message }, 409);
|
|
49
|
+
}
|
|
50
|
+
if (error instanceof CanonicalHumanIdentityOperationReuseError) {
|
|
51
|
+
return context.json({ code: error.code, message: error.message }, 409);
|
|
52
|
+
}
|
|
53
|
+
if (error instanceof CanonicalHumanIdentityNotFoundError) {
|
|
54
|
+
return context.json(
|
|
55
|
+
{ code: "CANONICAL_HUMAN_IDENTITY_NOT_FOUND", message: error.message },
|
|
56
|
+
404,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (error instanceof CanonicalHumanIdentityAuthorityError) {
|
|
60
|
+
return context.json(
|
|
61
|
+
{ code: "CANONICAL_HUMAN_IDENTITY_AUTHORITY_DENIED", message: error.message },
|
|
62
|
+
403,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function mutate(
|
|
69
|
+
context: Context,
|
|
70
|
+
deps: ApiRouteDeps,
|
|
71
|
+
input: {
|
|
72
|
+
operationType: "link" | "unlink" | "begin_recovery" | "recover";
|
|
73
|
+
bindingId?: string;
|
|
74
|
+
providerId?: string;
|
|
75
|
+
providerAccountId?: string;
|
|
76
|
+
operationId: string;
|
|
77
|
+
expectedIdentityRevision: number;
|
|
78
|
+
reason: string;
|
|
79
|
+
},
|
|
80
|
+
): Promise<Response> {
|
|
81
|
+
const identity = await requestIdentity(context, deps);
|
|
82
|
+
try {
|
|
83
|
+
return context.json(
|
|
84
|
+
CanonicalHumanIdentityMutationResponse.parse(
|
|
85
|
+
await applyCanonicalHumanIdentityOperation(deps.db, {
|
|
86
|
+
operationId: input.operationId,
|
|
87
|
+
authUserId: identity.authUserId,
|
|
88
|
+
expectedIdentityRevision: input.expectedIdentityRevision,
|
|
89
|
+
operationType: input.operationType,
|
|
90
|
+
...(input.bindingId ? { bindingId: input.bindingId } : {}),
|
|
91
|
+
...(input.providerId ? { providerId: input.providerId } : {}),
|
|
92
|
+
...(input.providerAccountId ? { providerAccountId: input.providerAccountId } : {}),
|
|
93
|
+
reason: input.reason,
|
|
94
|
+
}),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return identityError(context, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function registerCanonicalHumanIdentityRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
103
|
+
const base = "/v1/identity";
|
|
104
|
+
|
|
105
|
+
app.get(base, async (context) => {
|
|
106
|
+
const identity = await requestIdentity(context, deps);
|
|
107
|
+
try {
|
|
108
|
+
return context.json(
|
|
109
|
+
CanonicalHumanIdentityProjection.parse(
|
|
110
|
+
await getCanonicalHumanIdentityProjection(deps.db, identity.authUserId),
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return identityError(context, error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
app.post(`${base}/login-bindings`, async (context) => {
|
|
119
|
+
const request = await parseBody(context, LinkCanonicalHumanLoginBindingRequest);
|
|
120
|
+
return await mutate(context, deps, {
|
|
121
|
+
operationType: "link",
|
|
122
|
+
...request,
|
|
123
|
+
operationId: request.operationId ?? randomUUID(),
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
app.delete(`${base}/login-bindings/:bindingId`, async (context) => {
|
|
128
|
+
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
129
|
+
return await mutate(context, deps, {
|
|
130
|
+
operationType: "unlink",
|
|
131
|
+
bindingId: bindingId(context),
|
|
132
|
+
...request,
|
|
133
|
+
operationId: request.operationId ?? randomUUID(),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
app.post(`${base}/login-bindings/:bindingId/recovery`, async (context) => {
|
|
138
|
+
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
139
|
+
return await mutate(context, deps, {
|
|
140
|
+
operationType: "begin_recovery",
|
|
141
|
+
bindingId: bindingId(context),
|
|
142
|
+
...request,
|
|
143
|
+
operationId: request.operationId ?? randomUUID(),
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
app.post(`${base}/login-bindings/:bindingId/recovery/complete`, async (context) => {
|
|
148
|
+
const request = await parseBody(context, CanonicalHumanBindingOperationRequest);
|
|
149
|
+
return await mutate(context, deps, {
|
|
150
|
+
operationType: "recover",
|
|
151
|
+
bindingId: bindingId(context),
|
|
152
|
+
...request,
|
|
153
|
+
operationId: request.operationId ?? randomUUID(),
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CreateChannelRequest, UpdateChannelRequest } from "@opengeni/contracts";
|
|
3
|
+
import {
|
|
4
|
+
ChannelNameConflictError,
|
|
5
|
+
createChannel,
|
|
6
|
+
deleteChannel,
|
|
7
|
+
listChannels,
|
|
8
|
+
updateChannel,
|
|
9
|
+
} from "@opengeni/db";
|
|
10
|
+
import type { Hono } from "hono";
|
|
11
|
+
import { HTTPException } from "hono/http-exception";
|
|
12
|
+
import { requireAccessGrant } from "@opengeni/core";
|
|
13
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
14
|
+
|
|
15
|
+
// Workspace-shared channels organize root sessions ("workstreams") by work
|
|
16
|
+
// type in the rail. Deliberately session-permission-gated (not workspace:admin):
|
|
17
|
+
// anyone who can read sessions can see how they are organized, and anyone who
|
|
18
|
+
// can create sessions can organize them.
|
|
19
|
+
// Reject malformed ids before they reach UUID-typed persistence queries,
|
|
20
|
+
// preserving the non-enumerating 404 contract over a driver-level 500.
|
|
21
|
+
function channelIdParam(value: string): string {
|
|
22
|
+
if (!z.string().uuid().safeParse(value).success) {
|
|
23
|
+
throw new HTTPException(404, { message: "channel not found" });
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function registerChannelRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
29
|
+
const { db } = deps;
|
|
30
|
+
|
|
31
|
+
app.get("/v1/workspaces/:workspaceId/channels", async (c) => {
|
|
32
|
+
const workspaceId = c.req.param("workspaceId");
|
|
33
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:read");
|
|
34
|
+
return c.json(await listChannels(db, workspaceId));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
app.post("/v1/workspaces/:workspaceId/channels", async (c) => {
|
|
38
|
+
const workspaceId = c.req.param("workspaceId");
|
|
39
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:create");
|
|
40
|
+
const payload = CreateChannelRequest.parse(await c.req.json());
|
|
41
|
+
try {
|
|
42
|
+
const channel = await createChannel(db, {
|
|
43
|
+
accountId: grant.accountId,
|
|
44
|
+
workspaceId,
|
|
45
|
+
name: payload.name,
|
|
46
|
+
description: payload.description ?? null,
|
|
47
|
+
createdBy: grant.subjectId,
|
|
48
|
+
});
|
|
49
|
+
return c.json(channel, 201);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error instanceof ChannelNameConflictError) {
|
|
52
|
+
throw new HTTPException(409, { message: error.message });
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.patch("/v1/workspaces/:workspaceId/channels/:channelId", async (c) => {
|
|
59
|
+
const workspaceId = c.req.param("workspaceId");
|
|
60
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:create");
|
|
61
|
+
const payload = UpdateChannelRequest.parse(await c.req.json());
|
|
62
|
+
try {
|
|
63
|
+
const channel = await updateChannel(
|
|
64
|
+
db,
|
|
65
|
+
workspaceId,
|
|
66
|
+
channelIdParam(c.req.param("channelId")),
|
|
67
|
+
payload,
|
|
68
|
+
);
|
|
69
|
+
if (!channel) {
|
|
70
|
+
throw new HTTPException(404, { message: "channel not found" });
|
|
71
|
+
}
|
|
72
|
+
return c.json(channel);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error instanceof ChannelNameConflictError) {
|
|
75
|
+
throw new HTTPException(409, { message: error.message });
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
app.delete("/v1/workspaces/:workspaceId/channels/:channelId", async (c) => {
|
|
82
|
+
const workspaceId = c.req.param("workspaceId");
|
|
83
|
+
await requireAccessGrant(c, deps, workspaceId, "sessions:create");
|
|
84
|
+
const deleted = await deleteChannel(db, workspaceId, channelIdParam(c.req.param("channelId")));
|
|
85
|
+
if (!deleted) {
|
|
86
|
+
throw new HTTPException(404, { message: "channel not found" });
|
|
87
|
+
}
|
|
88
|
+
return c.json({ ok: true });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
ActivateCompanyProfileRevisionRequest,
|
|
4
|
+
CompanyProfileConflictResponse,
|
|
5
|
+
CompanyProfileDiffRequest,
|
|
6
|
+
CompanyProfileDiffResponse,
|
|
7
|
+
CompanyProfileListQuery,
|
|
8
|
+
CompanyProfileListResponse,
|
|
9
|
+
CompanyProfileMutationResponse,
|
|
10
|
+
CompanyProfileOperationReuseResponse,
|
|
11
|
+
CompanyProfileRevision,
|
|
12
|
+
RollbackCompanyProfileRequest,
|
|
13
|
+
UpdateCompanyProfileRequest,
|
|
14
|
+
} from "@opengeni/contracts";
|
|
15
|
+
import {
|
|
16
|
+
requireAccessGrant,
|
|
17
|
+
requireAccessGrantAuthorization,
|
|
18
|
+
type AccessGrantAuthorization,
|
|
19
|
+
type ApiRouteDeps,
|
|
20
|
+
} from "@opengeni/core";
|
|
21
|
+
import {
|
|
22
|
+
activateCompanyProfileRevision,
|
|
23
|
+
CompanyProfileConflictError,
|
|
24
|
+
CompanyProfileInvalidOperationError,
|
|
25
|
+
CompanyProfileNotFoundError,
|
|
26
|
+
CompanyProfileOperationReuseError,
|
|
27
|
+
diffCompanyProfileRevisions,
|
|
28
|
+
getCompanyProfileRevision,
|
|
29
|
+
listCompanyProfile,
|
|
30
|
+
rollbackCompanyProfileRevision,
|
|
31
|
+
updateCompanyProfile,
|
|
32
|
+
} from "@opengeni/db";
|
|
33
|
+
import type { Context, Hono } from "hono";
|
|
34
|
+
import { HTTPException } from "hono/http-exception";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
const RevisionId = z.string().uuid();
|
|
38
|
+
|
|
39
|
+
async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
|
|
40
|
+
const parsed = schema.safeParse(await context.req.json().catch(() => null));
|
|
41
|
+
if (!parsed.success) throw new HTTPException(422, { message: "Invalid company-profile request" });
|
|
42
|
+
return parsed.data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function requireDirectAccountAdmin(access: AccessGrantAuthorization): void {
|
|
46
|
+
const { grant } = access;
|
|
47
|
+
if (
|
|
48
|
+
!access.contextIntegrity ||
|
|
49
|
+
access.authenticatedSubjectId !== grant.subjectId ||
|
|
50
|
+
grant.principalKind !== "human_session" ||
|
|
51
|
+
grant.serviceInitiator ||
|
|
52
|
+
grant.serviceInitiatorContext ||
|
|
53
|
+
grant.subjectId.startsWith("api_key:")
|
|
54
|
+
) {
|
|
55
|
+
throw new HTTPException(403, {
|
|
56
|
+
message: "Company-profile administration requires a direct human-authorized request",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!access.accountGrant?.permissions.includes("account:admin")) {
|
|
60
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function profileError(context: Context, error: unknown): Response {
|
|
65
|
+
if (error instanceof CompanyProfileConflictError) {
|
|
66
|
+
return context.json(
|
|
67
|
+
CompanyProfileConflictResponse.parse({
|
|
68
|
+
code: error.code,
|
|
69
|
+
message: error.message,
|
|
70
|
+
currentHead: error.currentHead,
|
|
71
|
+
}),
|
|
72
|
+
409,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (error instanceof CompanyProfileOperationReuseError) {
|
|
76
|
+
return context.json(
|
|
77
|
+
CompanyProfileOperationReuseResponse.parse({ code: error.code, message: error.message }),
|
|
78
|
+
409,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof CompanyProfileNotFoundError) {
|
|
82
|
+
return context.json({ code: "COMPANY_PROFILE_NOT_FOUND", message: error.message }, 404);
|
|
83
|
+
}
|
|
84
|
+
if (error instanceof CompanyProfileInvalidOperationError) {
|
|
85
|
+
return context.json({ code: "INVALID_COMPANY_PROFILE_OPERATION", message: error.message }, 422);
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function revisionId(context: Context): string {
|
|
91
|
+
const parsed = RevisionId.safeParse(context.req.param("revisionId"));
|
|
92
|
+
if (!parsed.success)
|
|
93
|
+
throw new HTTPException(422, { message: "Invalid company-profile revision id" });
|
|
94
|
+
return parsed.data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function registerCompanyProfileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
98
|
+
const base = "/v1/workspaces/:workspaceId/company-profile";
|
|
99
|
+
|
|
100
|
+
app.get(base, async (context) => {
|
|
101
|
+
const workspaceId = context.req.param("workspaceId");
|
|
102
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
103
|
+
const query = CompanyProfileListQuery.safeParse({
|
|
104
|
+
afterRevision: context.req.query("afterRevision"),
|
|
105
|
+
limit: context.req.query("limit"),
|
|
106
|
+
});
|
|
107
|
+
if (!query.success) throw new HTTPException(422, { message: "Invalid company-profile query" });
|
|
108
|
+
return context.json(
|
|
109
|
+
CompanyProfileListResponse.parse(
|
|
110
|
+
await listCompanyProfile(deps.db, {
|
|
111
|
+
accountId: grant.accountId,
|
|
112
|
+
workspaceId,
|
|
113
|
+
limit: query.data.limit,
|
|
114
|
+
...(query.data.afterRevision === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { afterRevision: query.data.afterRevision }),
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
app.put(base, async (context) => {
|
|
123
|
+
const workspaceId = context.req.param("workspaceId");
|
|
124
|
+
const access = await requireAccessGrantAuthorization(
|
|
125
|
+
context,
|
|
126
|
+
deps,
|
|
127
|
+
workspaceId,
|
|
128
|
+
"workspace:read",
|
|
129
|
+
);
|
|
130
|
+
requireDirectAccountAdmin(access);
|
|
131
|
+
const request = await parseBody(context, UpdateCompanyProfileRequest);
|
|
132
|
+
try {
|
|
133
|
+
return context.json(
|
|
134
|
+
CompanyProfileMutationResponse.parse(
|
|
135
|
+
await updateCompanyProfile(deps.db, {
|
|
136
|
+
operationId: request.operationId ?? randomUUID(),
|
|
137
|
+
accountId: access.grant.accountId,
|
|
138
|
+
workspaceId,
|
|
139
|
+
profile: request.profile,
|
|
140
|
+
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
141
|
+
expectedActivationVersion: request.expectedActivationVersion,
|
|
142
|
+
actorSubjectId: access.grant.subjectId,
|
|
143
|
+
principalKind: "human_session",
|
|
144
|
+
reason: request.reason,
|
|
145
|
+
}),
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return profileError(context, error);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
app.get(`${base}/diff`, async (context) => {
|
|
154
|
+
const workspaceId = context.req.param("workspaceId");
|
|
155
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
156
|
+
const request = CompanyProfileDiffRequest.safeParse({
|
|
157
|
+
fromRevisionId: context.req.query("fromRevisionId"),
|
|
158
|
+
toRevisionId: context.req.query("toRevisionId"),
|
|
159
|
+
});
|
|
160
|
+
if (!request.success) throw new HTTPException(422, { message: "Invalid company-profile diff" });
|
|
161
|
+
try {
|
|
162
|
+
return context.json(
|
|
163
|
+
CompanyProfileDiffResponse.parse(
|
|
164
|
+
await diffCompanyProfileRevisions(deps.db, {
|
|
165
|
+
accountId: grant.accountId,
|
|
166
|
+
workspaceId,
|
|
167
|
+
...request.data,
|
|
168
|
+
}),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return profileError(context, error);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
app.post(`${base}/rollback`, async (context) => {
|
|
177
|
+
const workspaceId = context.req.param("workspaceId");
|
|
178
|
+
const access = await requireAccessGrantAuthorization(
|
|
179
|
+
context,
|
|
180
|
+
deps,
|
|
181
|
+
workspaceId,
|
|
182
|
+
"workspace:read",
|
|
183
|
+
);
|
|
184
|
+
requireDirectAccountAdmin(access);
|
|
185
|
+
const request = await parseBody(context, RollbackCompanyProfileRequest);
|
|
186
|
+
try {
|
|
187
|
+
return context.json(
|
|
188
|
+
CompanyProfileMutationResponse.parse(
|
|
189
|
+
await rollbackCompanyProfileRevision(deps.db, {
|
|
190
|
+
operationId: request.operationId ?? randomUUID(),
|
|
191
|
+
accountId: access.grant.accountId,
|
|
192
|
+
workspaceId,
|
|
193
|
+
targetRevisionId: request.targetRevisionId,
|
|
194
|
+
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
195
|
+
expectedActivationVersion: request.expectedActivationVersion,
|
|
196
|
+
actorSubjectId: access.grant.subjectId,
|
|
197
|
+
principalKind: "human_session",
|
|
198
|
+
reason: request.reason,
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return profileError(context, error);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
app.get(`${base}/revisions/:revisionId`, async (context) => {
|
|
208
|
+
const workspaceId = context.req.param("workspaceId");
|
|
209
|
+
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
210
|
+
try {
|
|
211
|
+
return context.json(
|
|
212
|
+
CompanyProfileRevision.parse(
|
|
213
|
+
await getCompanyProfileRevision(deps.db, {
|
|
214
|
+
accountId: grant.accountId,
|
|
215
|
+
workspaceId,
|
|
216
|
+
revisionId: revisionId(context),
|
|
217
|
+
}),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return profileError(context, error);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
app.post(`${base}/revisions/:revisionId/activate`, async (context) => {
|
|
226
|
+
const workspaceId = context.req.param("workspaceId");
|
|
227
|
+
const access = await requireAccessGrantAuthorization(
|
|
228
|
+
context,
|
|
229
|
+
deps,
|
|
230
|
+
workspaceId,
|
|
231
|
+
"workspace:read",
|
|
232
|
+
);
|
|
233
|
+
requireDirectAccountAdmin(access);
|
|
234
|
+
const request = await parseBody(context, ActivateCompanyProfileRevisionRequest);
|
|
235
|
+
try {
|
|
236
|
+
return context.json(
|
|
237
|
+
CompanyProfileMutationResponse.parse(
|
|
238
|
+
await activateCompanyProfileRevision(deps.db, {
|
|
239
|
+
operationId: request.operationId ?? randomUUID(),
|
|
240
|
+
accountId: access.grant.accountId,
|
|
241
|
+
workspaceId,
|
|
242
|
+
revisionId: revisionId(context),
|
|
243
|
+
expectedCurrentRevisionId: request.expectedCurrentRevisionId,
|
|
244
|
+
expectedActivationVersion: request.expectedActivationVersion,
|
|
245
|
+
actorSubjectId: access.grant.subjectId,
|
|
246
|
+
principalKind: "human_session",
|
|
247
|
+
reason: request.reason,
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return profileError(context, error);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|