@opengeni/api-router 2.6.4 → 2.8.1-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.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/{chunk-XTLI3CBH.js → chunk-HHKQMVY7.js} +3009 -1699
- package/dist/chunk-HHKQMVY7.js.map +1 -0
- package/dist/github-access.d.ts +25 -2
- package/dist/index.js +14 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/personal-github-repositories.d.ts +41 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/codex.d.ts +8 -1
- package/dist/routes/github.d.ts +2 -0
- package/dist/routes/organization-model-providers.d.ts +3 -0
- package/dist/workspace-deletion.d.ts +6 -0
- package/package.json +18 -18
- package/src/app.ts +55 -9
- package/src/auth/organization-user-setup.ts +3 -3
- package/src/github-access.ts +117 -1
- package/src/http/sse.ts +15 -7
- package/src/index.ts +13 -1
- package/src/integrations/personal-github-repositories.ts +238 -9
- package/src/integrations/slack-app-home.ts +2 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/mcp/company-brain-governed-writes.ts +2 -2
- package/src/mcp/company-profile-agent-admin.ts +1 -1
- package/src/mcp/remember.ts +1 -1
- package/src/mcp/server.ts +11 -2
- package/src/model-catalog.ts +45 -337
- package/src/routes/automations.ts +178 -19
- package/src/routes/codex.ts +136 -32
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +116 -15
- package/src/routes/organization-memberships.ts +50 -3
- package/src/routes/organization-model-providers.ts +231 -0
- package/src/routes/packs.ts +1 -0
- package/src/routes/personal-github.ts +39 -0
- package/src/routes/pr-review.ts +72 -4
- package/src/routes/scheduled-tasks.ts +40 -13
- package/src/routes/sessions.ts +88 -52
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +405 -62
- package/src/workspace-deletion.ts +64 -0
- package/dist/chunk-XTLI3CBH.js.map +0 -1
package/src/routes/codex.ts
CHANGED
|
@@ -223,7 +223,7 @@ async function managedCookieHuman(
|
|
|
223
223
|
};
|
|
224
224
|
}
|
|
225
225
|
|
|
226
|
-
async function requireOrganizationCodexHuman(
|
|
226
|
+
export async function requireOrganizationCodexHuman(
|
|
227
227
|
c: Context,
|
|
228
228
|
deps: ApiRouteDeps,
|
|
229
229
|
organizationId: string,
|
|
@@ -239,7 +239,9 @@ async function requireOrganizationCodexHuman(
|
|
|
239
239
|
};
|
|
240
240
|
}
|
|
241
241
|
if (!human) {
|
|
242
|
-
throw new HTTPException(401, {
|
|
242
|
+
throw new HTTPException(401, {
|
|
243
|
+
message: "organization administrator session required",
|
|
244
|
+
});
|
|
243
245
|
}
|
|
244
246
|
try {
|
|
245
247
|
await getOrganizationCodexRotationSettings(deps.db, {
|
|
@@ -249,7 +251,9 @@ async function requireOrganizationCodexHuman(
|
|
|
249
251
|
} catch (error) {
|
|
250
252
|
const state = nestedPostgresSqlState(error);
|
|
251
253
|
if (state === "42501") {
|
|
252
|
-
throw new HTTPException(403, {
|
|
254
|
+
throw new HTTPException(403, {
|
|
255
|
+
message: "organization administration is not authorized",
|
|
256
|
+
});
|
|
253
257
|
}
|
|
254
258
|
if (state === "P0002") {
|
|
255
259
|
throw new HTTPException(404, { message: "organization not found" });
|
|
@@ -270,11 +274,13 @@ async function requireWorkspaceCodexManagementSource(
|
|
|
270
274
|
});
|
|
271
275
|
}
|
|
272
276
|
if (source.effectiveSource === "disabled") {
|
|
273
|
-
throw new HTTPException(409, {
|
|
277
|
+
throw new HTTPException(409, {
|
|
278
|
+
message: "Codex is disabled for this workspace",
|
|
279
|
+
});
|
|
274
280
|
}
|
|
275
281
|
}
|
|
276
282
|
|
|
277
|
-
function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
|
|
283
|
+
export function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void {
|
|
278
284
|
const contentType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
279
285
|
if (contentType !== "application/json") {
|
|
280
286
|
throw new HTTPException(403, {
|
|
@@ -287,16 +293,28 @@ function requireSameOriginBrowserMutation(c: Context, deps: ApiRouteDeps): void
|
|
|
287
293
|
});
|
|
288
294
|
}
|
|
289
295
|
const origin = c.req.header("origin");
|
|
296
|
+
const localOriginMatches =
|
|
297
|
+
deps.settings.productAccessMode === "local" && localBrowserOriginMatchesRequest(c, origin);
|
|
290
298
|
if (
|
|
291
299
|
deps.settings.productAccessMode === "local"
|
|
292
|
-
? !
|
|
300
|
+
? !localOriginMatches
|
|
293
301
|
: origin !== new URL(deps.settings.publicBaseUrl!).origin
|
|
294
302
|
) {
|
|
295
303
|
throw new HTTPException(403, {
|
|
296
304
|
message: "same-origin browser request required",
|
|
297
305
|
});
|
|
298
306
|
}
|
|
299
|
-
|
|
307
|
+
const fetchSite = c.req.header("sec-fetch-site")?.toLowerCase();
|
|
308
|
+
const localFetchSiteMatches =
|
|
309
|
+
localOriginMatches &&
|
|
310
|
+
(fetchSite === "same-origin" ||
|
|
311
|
+
fetchSite === "same-site" ||
|
|
312
|
+
(fetchSite === "cross-site" && localLoopbackOriginMatchesRequest(c, origin)));
|
|
313
|
+
if (
|
|
314
|
+
deps.settings.productAccessMode === "local"
|
|
315
|
+
? !localFetchSiteMatches
|
|
316
|
+
: fetchSite !== "same-origin"
|
|
317
|
+
) {
|
|
300
318
|
throw new HTTPException(403, {
|
|
301
319
|
message: "same-origin fetch metadata required",
|
|
302
320
|
});
|
|
@@ -333,11 +351,35 @@ function localBrowserOriginMatchesRequest(c: Context, value: string | undefined)
|
|
|
333
351
|
}
|
|
334
352
|
return (
|
|
335
353
|
origin.protocol === request.protocol &&
|
|
336
|
-
origin.hostname === request.hostname
|
|
337
|
-
|
|
354
|
+
(origin.hostname === request.hostname ||
|
|
355
|
+
(isLoopbackHostname(origin.hostname) && isLoopbackHostname(request.hostname)))
|
|
338
356
|
);
|
|
339
357
|
}
|
|
340
358
|
|
|
359
|
+
function localLoopbackOriginMatchesRequest(c: Context, value: string | undefined): boolean {
|
|
360
|
+
if (!value) return false;
|
|
361
|
+
try {
|
|
362
|
+
const origin = new URL(value);
|
|
363
|
+
const forwardedProtocol = c.req.header("x-forwarded-proto")?.trim().toLowerCase();
|
|
364
|
+
const protocol = forwardedProtocol ? `${forwardedProtocol}:` : new URL(c.req.url).protocol;
|
|
365
|
+
const forwardedHost = c.req.header("x-forwarded-host") ?? c.req.header("host");
|
|
366
|
+
if (!forwardedHost || /[\s,/?#@\\]/u.test(forwardedHost)) return false;
|
|
367
|
+
const request = new URL(`${protocol}//${forwardedHost}`);
|
|
368
|
+
return (
|
|
369
|
+
origin.origin === value &&
|
|
370
|
+
origin.protocol === request.protocol &&
|
|
371
|
+
isLoopbackHostname(origin.hostname) &&
|
|
372
|
+
isLoopbackHostname(request.hostname)
|
|
373
|
+
);
|
|
374
|
+
} catch {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function isLoopbackHostname(value: string): boolean {
|
|
380
|
+
return value === "localhost" || value === "[::1]" || /^127(?:\.[0-9]{1,3}){3}$/u.test(value);
|
|
381
|
+
}
|
|
382
|
+
|
|
341
383
|
async function requireRedemptionHuman(
|
|
342
384
|
c: Context,
|
|
343
385
|
deps: ApiRouteDeps,
|
|
@@ -386,11 +428,15 @@ async function requireCodexAppsHuman(
|
|
|
386
428
|
requireSameOriginBrowserMutation(c, deps);
|
|
387
429
|
const human = await managedCookieHuman(c, deps);
|
|
388
430
|
if (!human) {
|
|
389
|
-
throw new HTTPException(401, {
|
|
431
|
+
throw new HTTPException(401, {
|
|
432
|
+
message: "managed browser session required",
|
|
433
|
+
});
|
|
390
434
|
}
|
|
391
435
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
392
436
|
if (grant.subjectId !== human.subjectId) {
|
|
393
|
-
throw new HTTPException(403, {
|
|
437
|
+
throw new HTTPException(403, {
|
|
438
|
+
message: "managed browser identity mismatch",
|
|
439
|
+
});
|
|
394
440
|
}
|
|
395
441
|
return { human, accountId: grant.accountId };
|
|
396
442
|
}
|
|
@@ -715,10 +761,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
715
761
|
const workspaceId = c.req.param("workspaceId");
|
|
716
762
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
|
|
717
763
|
const parsed = z
|
|
718
|
-
.object({
|
|
764
|
+
.object({
|
|
765
|
+
mode: z.enum(["automatic", "workspace", "organization", "disabled"]),
|
|
766
|
+
})
|
|
719
767
|
.safeParse(await c.req.json().catch(() => null));
|
|
720
768
|
if (!parsed.success) {
|
|
721
|
-
throw new HTTPException(400, {
|
|
769
|
+
throw new HTTPException(400, {
|
|
770
|
+
message: "a valid Codex source mode is required",
|
|
771
|
+
});
|
|
722
772
|
}
|
|
723
773
|
try {
|
|
724
774
|
return c.json(
|
|
@@ -795,7 +845,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
795
845
|
const organizationId = c.req.param("organizationId");
|
|
796
846
|
requireSameOriginBrowserMutation(c, deps);
|
|
797
847
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
798
|
-
const { state } = (await c.req.json().catch(() => null)) as {
|
|
848
|
+
const { state } = (await c.req.json().catch(() => null)) as {
|
|
849
|
+
state?: string;
|
|
850
|
+
};
|
|
799
851
|
const payload = (state
|
|
800
852
|
? readSignedState(state, githubStateSecret)
|
|
801
853
|
: null) as unknown as CodexConnectState | null;
|
|
@@ -806,7 +858,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
806
858
|
!payload.deviceAuthId ||
|
|
807
859
|
!payload.userCode
|
|
808
860
|
) {
|
|
809
|
-
throw new HTTPException(400, {
|
|
861
|
+
throw new HTTPException(400, {
|
|
862
|
+
message: "codex connect state is invalid or expired",
|
|
863
|
+
});
|
|
810
864
|
}
|
|
811
865
|
if (
|
|
812
866
|
typeof payload.iat === "number" &&
|
|
@@ -927,7 +981,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
927
981
|
const organizationId = c.req.param("organizationId");
|
|
928
982
|
requireSameOriginBrowserMutation(c, deps);
|
|
929
983
|
const human = await requireOrganizationCodexHuman(c, deps, organizationId);
|
|
930
|
-
const body = (await c.req.json().catch(() => null)) as {
|
|
984
|
+
const body = (await c.req.json().catch(() => null)) as {
|
|
985
|
+
label?: unknown;
|
|
986
|
+
} | null;
|
|
931
987
|
const renamed = await renameOrganizationCodexAccount(db, {
|
|
932
988
|
organizationId,
|
|
933
989
|
actorSubjectId: human.subjectId,
|
|
@@ -967,7 +1023,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
967
1023
|
throw error;
|
|
968
1024
|
}
|
|
969
1025
|
await signalCodexCapacityTargets(deps, result.wakeTargets);
|
|
970
|
-
return c.json({
|
|
1026
|
+
return c.json({
|
|
1027
|
+
disconnected: result.removed,
|
|
1028
|
+
newActiveId: result.newActiveCredentialId,
|
|
1029
|
+
});
|
|
971
1030
|
});
|
|
972
1031
|
|
|
973
1032
|
// Begin device-code login: returns the user code + verification URL and a
|
|
@@ -1065,6 +1124,8 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1065
1124
|
upserted: Awaited<ReturnType<typeof upsertCodexSubscriptionCredential>>;
|
|
1066
1125
|
isActive: boolean;
|
|
1067
1126
|
}>(db, { workspaceId, reason: "codex_credential_connected" }, async (tx) => {
|
|
1127
|
+
// The capacity mutation holds the source lock before entering this callback.
|
|
1128
|
+
const sourceBeforeConnect = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
|
|
1068
1129
|
const upserted = await upsertCodexSubscriptionCredential(tx, {
|
|
1069
1130
|
accountId: grant.accountId,
|
|
1070
1131
|
workspaceId,
|
|
@@ -1092,12 +1153,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1092
1153
|
}
|
|
1093
1154
|
await ensureCodexRotationSettings(tx, grant.accountId, workspaceId);
|
|
1094
1155
|
await setInitialActiveCodexCredential(tx, workspaceId, upserted.id);
|
|
1095
|
-
const source = await getWorkspaceCodexSubscriptionSource(tx, workspaceId);
|
|
1096
1156
|
await setWorkspaceCodexSubscriptionModeInTransaction(tx, {
|
|
1097
1157
|
accountId: grant.accountId,
|
|
1098
1158
|
workspaceId,
|
|
1099
1159
|
subjectId: grant.subjectId,
|
|
1100
|
-
mode:
|
|
1160
|
+
mode: sourceBeforeConnect.workspaceKind === "personal" ? "automatic" : "workspace",
|
|
1161
|
+
effectiveSourceBeforeMutation: sourceBeforeConnect.effectiveSource,
|
|
1101
1162
|
});
|
|
1102
1163
|
const rotation = await getCodexRotationSettings(tx, workspaceId);
|
|
1103
1164
|
return {
|
|
@@ -1107,6 +1168,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1107
1168
|
},
|
|
1108
1169
|
changed: true,
|
|
1109
1170
|
};
|
|
1171
|
+
}).catch((error: unknown) => {
|
|
1172
|
+
const cause = (error as { cause?: unknown } | null)?.cause;
|
|
1173
|
+
const message =
|
|
1174
|
+
cause instanceof Error ? cause.message : error instanceof Error ? error.message : "";
|
|
1175
|
+
if (message.includes("active turns are using it")) {
|
|
1176
|
+
throw new HTTPException(409, { message });
|
|
1177
|
+
}
|
|
1178
|
+
throw error;
|
|
1110
1179
|
});
|
|
1111
1180
|
const { upserted, isActive } = mutation.result;
|
|
1112
1181
|
if (upserted.kind === "unresolved_redemption") {
|
|
@@ -1116,7 +1185,12 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1116
1185
|
});
|
|
1117
1186
|
}
|
|
1118
1187
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1119
|
-
return c.json({
|
|
1188
|
+
return c.json({
|
|
1189
|
+
status: "connected",
|
|
1190
|
+
plan: id.planType,
|
|
1191
|
+
accountId: upserted.id,
|
|
1192
|
+
isActive,
|
|
1193
|
+
});
|
|
1120
1194
|
});
|
|
1121
1195
|
|
|
1122
1196
|
// Connection health: the cheapest real call is GET /codex/models (a 200 proves
|
|
@@ -1229,7 +1303,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1229
1303
|
const workspaceId = c.req.param("workspaceId");
|
|
1230
1304
|
await requireWorkspaceCodexManagementSource(deps, workspaceId);
|
|
1231
1305
|
if (!settings.codexConnectedAppsEnabled) {
|
|
1232
|
-
throw new HTTPException(409, {
|
|
1306
|
+
throw new HTTPException(409, {
|
|
1307
|
+
message: "Codex Apps is disabled for this deployment",
|
|
1308
|
+
});
|
|
1233
1309
|
}
|
|
1234
1310
|
const { human, accountId } = await requireCodexAppsHuman(c, deps, workspaceId);
|
|
1235
1311
|
const parsed = z
|
|
@@ -1239,7 +1315,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1239
1315
|
})
|
|
1240
1316
|
.safeParse(await c.req.json().catch(() => null));
|
|
1241
1317
|
if (!parsed.success) {
|
|
1242
|
-
throw new HTTPException(400, {
|
|
1318
|
+
throw new HTTPException(400, {
|
|
1319
|
+
message: "accountId and expectedVersion are required",
|
|
1320
|
+
});
|
|
1243
1321
|
}
|
|
1244
1322
|
const result = await designateCodexAppsCredential(db, {
|
|
1245
1323
|
accountId,
|
|
@@ -1257,10 +1335,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1257
1335
|
});
|
|
1258
1336
|
}
|
|
1259
1337
|
if (result.kind === "forbidden") {
|
|
1260
|
-
throw new HTTPException(403, {
|
|
1338
|
+
throw new HTTPException(403, {
|
|
1339
|
+
message: "missing permission: connections:write",
|
|
1340
|
+
});
|
|
1261
1341
|
}
|
|
1262
1342
|
if (result.kind === "unavailable") {
|
|
1263
|
-
throw new HTTPException(409, {
|
|
1343
|
+
throw new HTTPException(409, {
|
|
1344
|
+
message: "codex account requires relogin",
|
|
1345
|
+
});
|
|
1264
1346
|
}
|
|
1265
1347
|
const response = {
|
|
1266
1348
|
credentialId: result.credentialId,
|
|
@@ -1288,7 +1370,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1288
1370
|
expectedVersion: parsed.data.expectedVersion,
|
|
1289
1371
|
});
|
|
1290
1372
|
if (result.kind === "forbidden") {
|
|
1291
|
-
throw new HTTPException(403, {
|
|
1373
|
+
throw new HTTPException(403, {
|
|
1374
|
+
message: "missing permission: connections:write",
|
|
1375
|
+
});
|
|
1292
1376
|
}
|
|
1293
1377
|
const response = {
|
|
1294
1378
|
credentialId: result.credentialId,
|
|
@@ -1345,7 +1429,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1345
1429
|
if (patch.rotationEnabled === undefined) {
|
|
1346
1430
|
// Strategy-only writes are a deprecated no-op (no db touch): report the
|
|
1347
1431
|
// (only) truth. Callers that also flip rotationEnabled fall through.
|
|
1348
|
-
return c.json({
|
|
1432
|
+
return c.json({
|
|
1433
|
+
rotationStrategy: "sharded",
|
|
1434
|
+
rotationStrategyDeprecated: true,
|
|
1435
|
+
});
|
|
1349
1436
|
}
|
|
1350
1437
|
await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
|
|
1351
1438
|
const mutation = await withCodexCapacityMutation(
|
|
@@ -1454,7 +1541,10 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1454
1541
|
});
|
|
1455
1542
|
}
|
|
1456
1543
|
await signalCodexCapacityTargets(deps, mutation.wakeTargets);
|
|
1457
|
-
return c.json({
|
|
1544
|
+
return c.json({
|
|
1545
|
+
disconnected: result.removed,
|
|
1546
|
+
newActiveId: result.newActiveCredentialId,
|
|
1547
|
+
});
|
|
1458
1548
|
});
|
|
1459
1549
|
|
|
1460
1550
|
// Legacy "disconnect all" (old workspace-wide behavior), deprecated in favor of
|
|
@@ -1752,10 +1842,14 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1752
1842
|
});
|
|
1753
1843
|
}
|
|
1754
1844
|
if (adoption.kind === "not_found") {
|
|
1755
|
-
throw new HTTPException(409, {
|
|
1845
|
+
throw new HTTPException(409, {
|
|
1846
|
+
message: "redemption recovery state changed",
|
|
1847
|
+
});
|
|
1756
1848
|
}
|
|
1757
1849
|
if (adoption.kind === "forbidden") {
|
|
1758
|
-
throw new HTTPException(403, {
|
|
1850
|
+
throw new HTTPException(403, {
|
|
1851
|
+
message: "redemption owner is unavailable",
|
|
1852
|
+
});
|
|
1759
1853
|
}
|
|
1760
1854
|
if (adoption.kind === "conflict") {
|
|
1761
1855
|
throw new HTTPException(409, {
|
|
@@ -1769,7 +1863,9 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1769
1863
|
// and a lost HTTP response must remain replayable after a later health
|
|
1770
1864
|
// transition without another consume call.
|
|
1771
1865
|
if (account.status !== "active" && existing?.status !== "completed") {
|
|
1772
|
-
throw new HTTPException(403, {
|
|
1866
|
+
throw new HTTPException(403, {
|
|
1867
|
+
message: "redemption credential is unavailable",
|
|
1868
|
+
});
|
|
1773
1869
|
}
|
|
1774
1870
|
const secret = settings.betterAuthSecret;
|
|
1775
1871
|
if (!secret) {
|
|
@@ -1968,13 +2064,21 @@ export function registerCodexRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
1968
2064
|
if (fenced.kind !== "ready") {
|
|
1969
2065
|
if (fenced.reason === "confirmation_expired") {
|
|
1970
2066
|
return c.json(
|
|
1971
|
-
{
|
|
2067
|
+
{
|
|
2068
|
+
status: "confirmation_expired",
|
|
2069
|
+
attemptId: attempt.id,
|
|
2070
|
+
retryable: true,
|
|
2071
|
+
},
|
|
1972
2072
|
403,
|
|
1973
2073
|
);
|
|
1974
2074
|
}
|
|
1975
2075
|
if (fenced.reason === "credential_unavailable") {
|
|
1976
2076
|
return c.json(
|
|
1977
|
-
{
|
|
2077
|
+
{
|
|
2078
|
+
status: "provider_unavailable",
|
|
2079
|
+
attemptId: attempt.id,
|
|
2080
|
+
retryable: true,
|
|
2081
|
+
},
|
|
1978
2082
|
503,
|
|
1979
2083
|
);
|
|
1980
2084
|
}
|