@opengeni/api-router 2.1.0-canary.0 → 2.2.0-canary.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/{chunk-QKDFBBUE.js → chunk-3TP54PPX.js} +564 -73
- package/dist/chunk-3TP54PPX.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/integrations/slack-bot.d.ts +8 -0
- package/dist/integrations/slack-interactions.d.ts +35 -2
- package/package.json +18 -18
- package/src/app.ts +8 -0
- package/src/integrations/slack-bot.ts +5 -0
- package/src/integrations/slack-interactions.ts +653 -71
- package/src/integrations/slack-routing.ts +25 -12
- package/src/routes/documents.ts +134 -3
- package/dist/chunk-QKDFBBUE.js.map +0 -1
|
@@ -175,11 +175,24 @@ function labelFor(
|
|
|
175
175
|
* override beats configuration beats derivation beats asking.
|
|
176
176
|
*/
|
|
177
177
|
export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteResolution {
|
|
178
|
+
// A personal workspace is only ever a destination for that person's own bot
|
|
179
|
+
// DM. Offering it in a channel would be wrong twice over: routing a shared
|
|
180
|
+
// conversation into one member's private space hides it from everyone else
|
|
181
|
+
// in the channel, and - because managed tenancy provisions a personal
|
|
182
|
+
// workspace for every member - counting it as a candidate means nobody ever
|
|
183
|
+
// has exactly one. That defeats the sole-candidate rule below, so an
|
|
184
|
+
// organization with a single shared workspace would be asked to choose in
|
|
185
|
+
// every channel despite having no choice to make.
|
|
186
|
+
const directMessage = isSlackDirectMessageConversation(input.entry);
|
|
187
|
+
const candidates = directMessage
|
|
188
|
+
? input.candidates
|
|
189
|
+
: input.candidates.filter((candidate) => !candidate.personal);
|
|
190
|
+
|
|
178
191
|
const installation = {
|
|
179
192
|
kind: "resolved" as const,
|
|
180
193
|
accountId: input.home.accountId,
|
|
181
194
|
workspaceId: input.home.workspaceId,
|
|
182
|
-
label: labelFor(
|
|
195
|
+
label: labelFor(candidates, input.home.workspaceId),
|
|
183
196
|
source: "installation" as const,
|
|
184
197
|
};
|
|
185
198
|
|
|
@@ -206,7 +219,7 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
206
219
|
kind: "resolved",
|
|
207
220
|
accountId: input.threadTenancy.accountId,
|
|
208
221
|
workspaceId: input.threadTenancy.workspaceId,
|
|
209
|
-
label: labelFor(
|
|
222
|
+
label: labelFor(candidates, input.threadTenancy.workspaceId),
|
|
210
223
|
source: "thread",
|
|
211
224
|
};
|
|
212
225
|
}
|
|
@@ -221,13 +234,13 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
221
234
|
? parseSlackWorkspacePrefix(splitSlackLeadingMention(input.entry.text, input.botUserId).rest)
|
|
222
235
|
: null;
|
|
223
236
|
if (prefix) {
|
|
224
|
-
const named = matchCandidate(
|
|
237
|
+
const named = matchCandidate(candidates, prefix.requested);
|
|
225
238
|
if (named === "ambiguous" || !named) {
|
|
226
239
|
return {
|
|
227
240
|
kind: "denied",
|
|
228
241
|
reason: "no_access_to_named",
|
|
229
242
|
requested: prefix.requested,
|
|
230
|
-
candidates:
|
|
243
|
+
candidates: candidates,
|
|
231
244
|
};
|
|
232
245
|
}
|
|
233
246
|
return {
|
|
@@ -245,7 +258,7 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
245
258
|
kind: "resolved",
|
|
246
259
|
accountId: input.channelRoute.targetAccountId,
|
|
247
260
|
workspaceId: input.channelRoute.targetWorkspaceId,
|
|
248
|
-
label: labelFor(
|
|
261
|
+
label: labelFor(candidates, input.channelRoute.targetWorkspaceId),
|
|
249
262
|
source: "channel",
|
|
250
263
|
};
|
|
251
264
|
}
|
|
@@ -254,13 +267,13 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
254
267
|
// lands in their own workspace unless they chose otherwise. The personal
|
|
255
268
|
// workspace id is DERIVED from an active organization membership pointer;
|
|
256
269
|
// it is never accepted from a Slack payload or a route row.
|
|
257
|
-
if (
|
|
270
|
+
if (directMessage) {
|
|
258
271
|
if (input.dmRoute) {
|
|
259
272
|
return {
|
|
260
273
|
kind: "resolved",
|
|
261
274
|
accountId: input.dmRoute.targetAccountId,
|
|
262
275
|
workspaceId: input.dmRoute.targetWorkspaceId,
|
|
263
|
-
label: labelFor(
|
|
276
|
+
label: labelFor(candidates, input.dmRoute.targetWorkspaceId),
|
|
264
277
|
source: "dm_route",
|
|
265
278
|
};
|
|
266
279
|
}
|
|
@@ -269,7 +282,7 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
269
282
|
kind: "resolved",
|
|
270
283
|
accountId: input.home.accountId,
|
|
271
284
|
workspaceId: input.personalWorkspaceId,
|
|
272
|
-
label: labelFor(
|
|
285
|
+
label: labelFor(candidates, input.personalWorkspaceId),
|
|
273
286
|
source: "dm_personal",
|
|
274
287
|
};
|
|
275
288
|
}
|
|
@@ -280,8 +293,8 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
280
293
|
|
|
281
294
|
// 5. One workspace is not a choice. This is what keeps the flag quiet for
|
|
282
295
|
// installs that only ever had one workspace.
|
|
283
|
-
const sole =
|
|
284
|
-
if (
|
|
296
|
+
const sole = candidates[0];
|
|
297
|
+
if (candidates.length === 1 && sole) {
|
|
285
298
|
return {
|
|
286
299
|
kind: "resolved",
|
|
287
300
|
accountId: sole.accountId,
|
|
@@ -291,13 +304,13 @@ export function resolveSlackWorkspaceRoute(input: SlackRouteInputs): SlackRouteR
|
|
|
291
304
|
};
|
|
292
305
|
}
|
|
293
306
|
|
|
294
|
-
if (
|
|
307
|
+
if (candidates.length === 0) {
|
|
295
308
|
return { kind: "denied", reason: "no_candidates", requested: null, candidates: [] };
|
|
296
309
|
}
|
|
297
310
|
|
|
298
311
|
// 6. Genuinely ambiguous. Until the picker exists, keep the installation's
|
|
299
312
|
// workspace rather than inventing an answer.
|
|
300
|
-
return input.askEnabled ? { kind: "ask", candidates:
|
|
313
|
+
return input.askEnabled ? { kind: "ask", candidates: candidates } : installation;
|
|
301
314
|
}
|
|
302
315
|
|
|
303
316
|
/**
|
package/src/routes/documents.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
DocumentAuthorityReclassification,
|
|
8
8
|
DocumentBase,
|
|
9
9
|
DocumentDefaultCollectionBackfill,
|
|
10
|
+
DocumentDefaultCollectionBackfillAudit,
|
|
11
|
+
GetDocumentDefaultCollectionBackfillAuditQuery,
|
|
10
12
|
DocumentSearchRequest,
|
|
11
13
|
DocumentSearchResponse,
|
|
12
14
|
FileAsset,
|
|
@@ -15,6 +17,9 @@ import {
|
|
|
15
17
|
KnowledgeMemorySearchRequest,
|
|
16
18
|
ListDocumentAuthorityReclassificationsQuery,
|
|
17
19
|
ListDocumentAuthorityReclassificationsResponse,
|
|
20
|
+
ListDocumentDefaultCollectionBackfillRunsResponse,
|
|
21
|
+
ListDocumentMigrationAuditQuery,
|
|
22
|
+
ListOrganizationDocumentAuthorityReclassificationsResponse,
|
|
18
23
|
MoveDocumentRequest,
|
|
19
24
|
ReclassifyDocumentAuthorityRequest,
|
|
20
25
|
RunDocumentDefaultCollectionBackfillRequest,
|
|
@@ -40,10 +45,13 @@ import {
|
|
|
40
45
|
getDocument,
|
|
41
46
|
getDocumentOriginalFile,
|
|
42
47
|
getDocumentBase,
|
|
48
|
+
getDocumentDefaultCollectionBackfillAudit,
|
|
43
49
|
listAccessibleDocuments,
|
|
44
50
|
listDocumentAuthorityReclassifications,
|
|
51
|
+
listDocumentDefaultCollectionBackfillRuns,
|
|
45
52
|
listDocumentBasesEnsuringDefault,
|
|
46
53
|
listDocuments,
|
|
54
|
+
listOrganizationDocumentAuthorityReclassifications,
|
|
47
55
|
moveDocumentToBase,
|
|
48
56
|
queueDocumentForReindex,
|
|
49
57
|
reclassifyDocumentAuthority,
|
|
@@ -132,6 +140,106 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
132
140
|
}
|
|
133
141
|
});
|
|
134
142
|
|
|
143
|
+
app.get("/v1/workspaces/:workspaceId/document-default-collection-backfills", async (c) => {
|
|
144
|
+
const workspaceId = c.req.param("workspaceId");
|
|
145
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
146
|
+
c,
|
|
147
|
+
deps,
|
|
148
|
+
workspaceId,
|
|
149
|
+
"documents:manage",
|
|
150
|
+
);
|
|
151
|
+
if (!hasAccountAdminAuthority(authorization)) {
|
|
152
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
153
|
+
}
|
|
154
|
+
const query = ListDocumentMigrationAuditQuery.safeParse(c.req.query());
|
|
155
|
+
if (!query.success) {
|
|
156
|
+
throw new HTTPException(400, { message: "invalid document migration audit query" });
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return c.json(
|
|
160
|
+
ListDocumentDefaultCollectionBackfillRunsResponse.parse(
|
|
161
|
+
await listDocumentDefaultCollectionBackfillRuns(db, {
|
|
162
|
+
accountId: authorization.grant.accountId,
|
|
163
|
+
workspaceId,
|
|
164
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
165
|
+
accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
|
|
166
|
+
...query.data,
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
throw documentHttpException(error);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
app.get("/v1/workspaces/:workspaceId/document-default-collection-backfills/:runId", async (c) => {
|
|
176
|
+
const workspaceId = c.req.param("workspaceId");
|
|
177
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
178
|
+
c,
|
|
179
|
+
deps,
|
|
180
|
+
workspaceId,
|
|
181
|
+
"documents:manage",
|
|
182
|
+
);
|
|
183
|
+
if (!hasAccountAdminAuthority(authorization)) {
|
|
184
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
185
|
+
}
|
|
186
|
+
const runId = RunDocumentDefaultCollectionBackfillRequest.shape.runId.safeParse(
|
|
187
|
+
c.req.param("runId"),
|
|
188
|
+
);
|
|
189
|
+
const query = GetDocumentDefaultCollectionBackfillAuditQuery.safeParse(c.req.query());
|
|
190
|
+
if (!runId.success || !query.success) {
|
|
191
|
+
throw new HTTPException(400, { message: "invalid document migration audit query" });
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
return c.json(
|
|
195
|
+
DocumentDefaultCollectionBackfillAudit.parse(
|
|
196
|
+
await getDocumentDefaultCollectionBackfillAudit(db, {
|
|
197
|
+
accountId: authorization.grant.accountId,
|
|
198
|
+
workspaceId,
|
|
199
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
200
|
+
accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
|
|
201
|
+
runId: runId.data,
|
|
202
|
+
...query.data,
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
throw documentHttpException(error);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
app.get("/v1/workspaces/:workspaceId/document-authority-reclassifications", async (c) => {
|
|
212
|
+
const workspaceId = c.req.param("workspaceId");
|
|
213
|
+
const authorization = await requireAccessGrantAuthorization(
|
|
214
|
+
c,
|
|
215
|
+
deps,
|
|
216
|
+
workspaceId,
|
|
217
|
+
"documents:manage",
|
|
218
|
+
);
|
|
219
|
+
if (!hasAccountAdminAuthority(authorization)) {
|
|
220
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
221
|
+
}
|
|
222
|
+
const query = ListDocumentMigrationAuditQuery.safeParse(c.req.query());
|
|
223
|
+
if (!query.success) {
|
|
224
|
+
throw new HTTPException(400, { message: "invalid document migration audit query" });
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
return c.json(
|
|
228
|
+
ListOrganizationDocumentAuthorityReclassificationsResponse.parse(
|
|
229
|
+
await listOrganizationDocumentAuthorityReclassifications(db, {
|
|
230
|
+
accountId: authorization.grant.accountId,
|
|
231
|
+
workspaceId,
|
|
232
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
233
|
+
accountAdminAuthorization: requireAccountAdminAuthorizationStamp(authorization),
|
|
234
|
+
...query.data,
|
|
235
|
+
}),
|
|
236
|
+
),
|
|
237
|
+
);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw documentHttpException(error);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
135
243
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
|
|
136
244
|
const workspaceId = c.req.param("workspaceId");
|
|
137
245
|
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "documents:manage");
|
|
@@ -879,13 +987,16 @@ function dropFilename(preferred: string | undefined): string {
|
|
|
879
987
|
}
|
|
880
988
|
|
|
881
989
|
function documentHttpException(error: unknown): HTTPException {
|
|
882
|
-
const message =
|
|
990
|
+
const message = documentDomainErrorMessage(error);
|
|
883
991
|
if (message.includes("organization document") && message.includes("exact account authority")) {
|
|
884
992
|
return new HTTPException(403, { message: "missing permission: account:admin" });
|
|
885
993
|
}
|
|
886
994
|
if (message.includes("not found")) {
|
|
887
995
|
return new HTTPException(404, { message });
|
|
888
996
|
}
|
|
997
|
+
if (message.includes("document Default collection backfill audit run is unavailable")) {
|
|
998
|
+
return new HTTPException(404, { message });
|
|
999
|
+
}
|
|
889
1000
|
if (message.includes("already exists")) {
|
|
890
1001
|
return new HTTPException(409, { message });
|
|
891
1002
|
}
|
|
@@ -898,7 +1009,9 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
898
1009
|
}
|
|
899
1010
|
if (
|
|
900
1011
|
message.includes("reclassification requires") ||
|
|
901
|
-
message.includes("backfill requires organization administration")
|
|
1012
|
+
message.includes("backfill requires organization administration") ||
|
|
1013
|
+
message.includes("document migration audit requires organization administration") ||
|
|
1014
|
+
message.includes("document migration audit scope is invalid")
|
|
902
1015
|
) {
|
|
903
1016
|
return new HTTPException(403, { message });
|
|
904
1017
|
}
|
|
@@ -906,7 +1019,12 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
906
1019
|
message.includes("reclassification input is invalid") ||
|
|
907
1020
|
message.includes("reclassification authority is invalid") ||
|
|
908
1021
|
message.includes("invalid document authority receipt cursor") ||
|
|
909
|
-
message.includes("document authority receipt limit")
|
|
1022
|
+
message.includes("document authority receipt limit") ||
|
|
1023
|
+
message.includes("invalid document migration audit cursor") ||
|
|
1024
|
+
message.includes("document migration audit cursor is invalid") ||
|
|
1025
|
+
message.includes("document migration audit limit") ||
|
|
1026
|
+
message.includes("document migration audit authority is incomplete") ||
|
|
1027
|
+
message.includes("document Default collection backfill audit run id is required")
|
|
910
1028
|
) {
|
|
911
1029
|
return new HTTPException(400, { message });
|
|
912
1030
|
}
|
|
@@ -929,6 +1047,19 @@ function documentHttpException(error: unknown): HTTPException {
|
|
|
929
1047
|
return new HTTPException(500, { message });
|
|
930
1048
|
}
|
|
931
1049
|
|
|
1050
|
+
function documentDomainErrorMessage(error: unknown): string {
|
|
1051
|
+
let current: unknown = error;
|
|
1052
|
+
let message = error instanceof Error ? error.message : String(error);
|
|
1053
|
+
const seen = new Set<unknown>();
|
|
1054
|
+
while (current && typeof current === "object" && !seen.has(current) && seen.size < 8) {
|
|
1055
|
+
seen.add(current);
|
|
1056
|
+
const record = current as { cause?: unknown; message?: unknown };
|
|
1057
|
+
if (typeof record.message === "string") message = record.message;
|
|
1058
|
+
current = record.cause;
|
|
1059
|
+
}
|
|
1060
|
+
return message;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
932
1063
|
function hasAccountAdminAuthority(
|
|
933
1064
|
authorization: Awaited<ReturnType<typeof requireAccessGrantAuthorization>>,
|
|
934
1065
|
): boolean {
|