@anchrd/intel-api 0.6.2 → 0.6.4
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/adapters/db/db-flows.js +52 -3
- package/dist/adapters/portal-tokens/portal-tokens.js +9 -1
- package/dist/flows/flows.js +50 -0
- package/dist/flows/flows.types.d.ts +12 -2
- package/dist/http/http.js +18 -3
- package/dist/mcp/mcp.js +15 -1
- package/dist/tools/tools.js +6 -0
- package/package.json +1 -1
|
@@ -122,15 +122,15 @@ export function createFlowRepository(deps) {
|
|
|
122
122
|
* same reason `COUNT(*) OVER ()` is a count of those rows alone — the number the graph reports
|
|
123
123
|
* about its own size would otherwise disclose that something else is there.
|
|
124
124
|
*/
|
|
125
|
-
const visibleFlows = (scopeClause, bounded) => `${subtreeCte}
|
|
125
|
+
const visibleFlows = (scopeClause, bounded, includeArchived = false) => `${subtreeCte}
|
|
126
126
|
SELECT ${flowColumns}${bounded ? ", COUNT(*) OVER () AS total" : ""} FROM flows flow
|
|
127
|
-
WHERE ${flowInSubtree} AND flow.archived_at IS NULL ${scopeClause}
|
|
127
|
+
WHERE ${flowInSubtree} ${includeArchived ? "" : "AND flow.archived_at IS NULL"} ${scopeClause}
|
|
128
128
|
ORDER BY lower(flow.title), flow.id${bounded ? " LIMIT ?" : ""}`;
|
|
129
129
|
return {
|
|
130
130
|
async listVisible(actor, input = {}) {
|
|
131
131
|
const scope = scopeOf(input.parentId);
|
|
132
132
|
const result = await deps.db
|
|
133
|
-
.prepare(visibleFlows(scope.clause, false))
|
|
133
|
+
.prepare(visibleFlows(scope.clause, false, input.includeArchived === true))
|
|
134
134
|
.bind(...readableBindings(actor), ...flowInSubtreeBindings(actor), ...scope.bindings)
|
|
135
135
|
.all();
|
|
136
136
|
return (result.results ?? []).map(mapFlow);
|
|
@@ -388,6 +388,55 @@ export function createFlowRepository(deps) {
|
|
|
388
388
|
.first();
|
|
389
389
|
return row ? mapFlow(row) : "conflict";
|
|
390
390
|
},
|
|
391
|
+
// Archiving and restoring are one statement, told apart only by whether `archived_at` is a
|
|
392
|
+
// timestamp or null. Like `updateFlow` it writes the flow row alone: an archived flow keeps its
|
|
393
|
+
// versions and its published version, because coming back out of the archive has to return the
|
|
394
|
+
// flow that went in.
|
|
395
|
+
//
|
|
396
|
+
// ⚠️ The idempotency row is written only if the UPDATE actually matched — same guard as
|
|
397
|
+
// `knowledge.archive` — so a stale `baseUpdatedAt` leaves no key behind that would make the
|
|
398
|
+
// retry of a *lost* write look like a replay of a successful one.
|
|
399
|
+
async archiveFlow(input) {
|
|
400
|
+
try {
|
|
401
|
+
await deps.db.batch([
|
|
402
|
+
deps.db
|
|
403
|
+
.prepare(`UPDATE flows SET archived_at = ?, updated_at = ?
|
|
404
|
+
WHERE id = ? AND updated_at = ?`)
|
|
405
|
+
.bind(input.archivedAt, input.updatedAt, input.flowId, input.baseUpdatedAt),
|
|
406
|
+
deps.db
|
|
407
|
+
.prepare(`INSERT INTO idempotency_keys (
|
|
408
|
+
actor_id, operation, idempotency_key, resource_id, created_at
|
|
409
|
+
) SELECT ?, 'flows.archive', ?, ?, ?
|
|
410
|
+
WHERE EXISTS (
|
|
411
|
+
SELECT 1 FROM flows WHERE id = ? AND archived_at IS ? AND updated_at = ?
|
|
412
|
+
)`)
|
|
413
|
+
.bind(input.actorId, input.idempotencyKey, input.flowId, input.updatedAt, input.flowId, input.archivedAt, input.updatedAt),
|
|
414
|
+
deps.db
|
|
415
|
+
.prepare(`INSERT INTO audit_events (
|
|
416
|
+
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
417
|
+
) SELECT ?, ?, 'flows.archive', 'flow', ?, ?, ?
|
|
418
|
+
WHERE EXISTS (
|
|
419
|
+
SELECT 1 FROM idempotency_keys
|
|
420
|
+
WHERE actor_id = ? AND operation = 'flows.archive'
|
|
421
|
+
AND idempotency_key = ? AND resource_id = ?
|
|
422
|
+
)`)
|
|
423
|
+
.bind(input.auditId, input.actorId, input.flowId, JSON.stringify({ archived: input.archivedAt !== null }), input.updatedAt, input.actorId, input.idempotencyKey, input.flowId),
|
|
424
|
+
]);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
if (!(await this.findIdempotent(input.actorId, "flows.archive", input.idempotencyKey))) {
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const replayed = await this.findIdempotent(input.actorId, "flows.archive", input.idempotencyKey);
|
|
432
|
+
if (!replayed)
|
|
433
|
+
return "conflict";
|
|
434
|
+
const row = await deps.db
|
|
435
|
+
.prepare(`SELECT ${flowColumns} FROM flows WHERE id = ?`)
|
|
436
|
+
.bind(replayed)
|
|
437
|
+
.first();
|
|
438
|
+
return row ? mapFlow(row) : "conflict";
|
|
439
|
+
},
|
|
391
440
|
// Versions are immutable, so a set of them is a single read by definition: the trail of a nested
|
|
392
441
|
// run needs one per level and the relation graph one per flow it draws, and both used to ask
|
|
393
442
|
// level by level (#30). Missing IDs are simply absent from the answer.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { reportUnexpectedError } from "../../shared/report-unexpected-error/report-unexpected-error.js";
|
|
2
3
|
const StoredToken = z.strictObject({
|
|
3
4
|
version: z.literal(1),
|
|
4
5
|
accessToken: z.string().min(1),
|
|
@@ -41,9 +42,16 @@ export function createPortalTokenStore(deps) {
|
|
|
41
42
|
const { version: _version, ...token } = parsed;
|
|
42
43
|
return token;
|
|
43
44
|
}
|
|
44
|
-
catch {
|
|
45
|
+
catch (error) {
|
|
45
46
|
// A row that no longer decrypts (rotated secret, tampering) is treated as absent so the
|
|
46
47
|
// user is asked to reconnect instead of hitting an opaque failure on every call.
|
|
48
|
+
// ⚠️ Absent and unreadable look identical from the outside — both end in the sign-in screen
|
|
49
|
+
// — but only one of them is a defect. Without this line the difference is unobservable, and
|
|
50
|
+
// a connection that disappears has no explanation anywhere (#97). The row is named by its
|
|
51
|
+
// length alone; the sealed value never goes near a log.
|
|
52
|
+
reportUnexpectedError(new Error(`portal token for a user did not decrypt (sealed length ${row.sealed.length})`, {
|
|
53
|
+
cause: error,
|
|
54
|
+
}));
|
|
47
55
|
return null;
|
|
48
56
|
}
|
|
49
57
|
},
|
package/dist/flows/flows.js
CHANGED
|
@@ -1053,6 +1053,56 @@ export function createFlows(deps) {
|
|
|
1053
1053
|
}
|
|
1054
1054
|
return updated;
|
|
1055
1055
|
},
|
|
1056
|
+
/**
|
|
1057
|
+
* Archive a flow, or take it back out again.
|
|
1058
|
+
*
|
|
1059
|
+
* ⚠️ The one place that resolves the flow through `getVisible` instead of `requireFlow`. Every
|
|
1060
|
+
* other entry point treats an archived flow as absent — that is the whole point of archiving —
|
|
1061
|
+
* but the call that restores one has to be able to find it, and `requireFlow` answers 404 for
|
|
1062
|
+
* exactly the rows this call exists for. The visibility predicate is the same one; only the
|
|
1063
|
+
* `archivedAt` refusal is left out.
|
|
1064
|
+
*
|
|
1065
|
+
* `write` and nothing more, asked before the row is touched: archiving is organization, the same
|
|
1066
|
+
* grant that renames and moves (ADR-0004). It appends no version and withdraws no publication.
|
|
1067
|
+
*
|
|
1068
|
+
* ⚠️ It does NOT leave runs in flight alone, however much the word "archive" suggests it would.
|
|
1069
|
+
* `completeStep` writes before it shapes its answer: the step is recorded, and `step()` then
|
|
1070
|
+
* resolves the flow and throws 404 for the archived one. The caller is told the step failed
|
|
1071
|
+
* while storage has it done, and a retry replays into the same 404. Restoring the flow is what
|
|
1072
|
+
* makes the run readable again. That belongs to the run machine and has its own ticket
|
|
1073
|
+
* (issue 112) — archiving is only the easiest way to walk into it.
|
|
1074
|
+
*/
|
|
1075
|
+
async archive(actor, input) {
|
|
1076
|
+
const current = await deps.repository.getVisible(actor, input.flowId);
|
|
1077
|
+
if (!current)
|
|
1078
|
+
throw new IntelError(404, "flow_not_found", "Flow was not found");
|
|
1079
|
+
if (!(await deps.repository.can(actor, input.flowId, "write"))) {
|
|
1080
|
+
throw new IntelError(403, "flow_edit_forbidden", "Flow cannot be edited");
|
|
1081
|
+
}
|
|
1082
|
+
const replayed = await deps.repository.findIdempotent(actor.id, "flows.archive", input.idempotencyKey);
|
|
1083
|
+
// Read back the way this call reads anything, or replaying an archive would 404 on the row it
|
|
1084
|
+
// just archived.
|
|
1085
|
+
if (replayed) {
|
|
1086
|
+
const flow = await deps.repository.getVisible(actor, replayed);
|
|
1087
|
+
if (!flow)
|
|
1088
|
+
throw new IntelError(404, "flow_not_found", "Flow was not found");
|
|
1089
|
+
return flow;
|
|
1090
|
+
}
|
|
1091
|
+
const updatedAt = deps.now().toISOString();
|
|
1092
|
+
const updated = await deps.repository.archiveFlow({
|
|
1093
|
+
flowId: current.id,
|
|
1094
|
+
baseUpdatedAt: input.baseUpdatedAt,
|
|
1095
|
+
archivedAt: input.archived ? updatedAt : null,
|
|
1096
|
+
updatedAt,
|
|
1097
|
+
actorId: actor.id,
|
|
1098
|
+
idempotencyKey: input.idempotencyKey,
|
|
1099
|
+
auditId: deps.id(),
|
|
1100
|
+
});
|
|
1101
|
+
if (updated === "conflict") {
|
|
1102
|
+
throw new IntelError(409, "flow_update_conflict", "Flow was changed by another editor");
|
|
1103
|
+
}
|
|
1104
|
+
return updated;
|
|
1105
|
+
},
|
|
1056
1106
|
async save(actor, input) {
|
|
1057
1107
|
const replayed = await deps.repository.findIdempotent(actor.id, "flows.save", input.idempotencyKey);
|
|
1058
1108
|
if (replayed) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompleteFlowRunStepInput, CreateFlowInput, Flow, FlowDocument, FlowGraph, FlowPublishPreview, FlowRequirements, FlowRun, FlowRunHistory, FlowRunList, FlowRunStep, FlowValidation, FlowVersion, KnowledgeNode, ListFlowRunsInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraph, RelationGraphInput, ResourceVerb, SaveFlowVersionInput, StartFlowRunInput, UpdateFlowInput } from "@anchrd/intel-contract";
|
|
1
|
+
import type { ArchiveFlowInput, CompleteFlowRunStepInput, CreateFlowInput, Flow, FlowDocument, FlowGraph, FlowPublishPreview, FlowRequirements, FlowRun, FlowRunHistory, FlowRunList, FlowRunStep, FlowValidation, FlowVersion, KnowledgeNode, ListFlowRunsInput, ListFlowsInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraph, RelationGraphInput, ResourceVerb, SaveFlowVersionInput, StartFlowRunInput, UpdateFlowInput } from "@anchrd/intel-contract";
|
|
2
2
|
export type FlowPrincipal = Pick<FlowActor, "id" | "email" | "isAdmin">;
|
|
3
3
|
export type FlowCallReach = "subtree" | "library" | "out-of-reach";
|
|
4
4
|
export interface FlowRunChainEntry {
|
|
@@ -40,7 +40,7 @@ export interface FlowActor {
|
|
|
40
40
|
isAdmin?: boolean;
|
|
41
41
|
}
|
|
42
42
|
export type FlowVerb = Extract<ResourceVerb, "read" | "write" | "execute">;
|
|
43
|
-
export type FlowOperation = "flows.create" | "flows.update" | "flows.save" | "flows.publish" | "flows.run" | "flows.complete";
|
|
43
|
+
export type FlowOperation = "flows.create" | "flows.update" | "flows.archive" | "flows.save" | "flows.publish" | "flows.run" | "flows.complete";
|
|
44
44
|
export interface BoundedLevel<T> {
|
|
45
45
|
items: T[];
|
|
46
46
|
total: number;
|
|
@@ -76,6 +76,15 @@ export interface FlowRepository {
|
|
|
76
76
|
idempotencyKey: string;
|
|
77
77
|
auditId: string;
|
|
78
78
|
}): Promise<"conflict" | Flow>;
|
|
79
|
+
archiveFlow(input: {
|
|
80
|
+
flowId: string;
|
|
81
|
+
baseUpdatedAt: string;
|
|
82
|
+
archivedAt: string | null;
|
|
83
|
+
updatedAt: string;
|
|
84
|
+
actorId: string;
|
|
85
|
+
idempotencyKey: string;
|
|
86
|
+
auditId: string;
|
|
87
|
+
}): Promise<"conflict" | Flow>;
|
|
79
88
|
getVersion(versionId: string): Promise<FlowVersion | null>;
|
|
80
89
|
getVersions(versionIds: string[]): Promise<FlowVersion[]>;
|
|
81
90
|
insertVersion(input: {
|
|
@@ -169,6 +178,7 @@ export interface FlowService {
|
|
|
169
178
|
listRequirements(actor: FlowActor, flowId: string): Promise<FlowRequirements>;
|
|
170
179
|
create(actor: FlowActor, input: CreateFlowInput): Promise<Flow>;
|
|
171
180
|
update(actor: FlowActor, input: UpdateFlowInput): Promise<Flow>;
|
|
181
|
+
archive(actor: FlowActor, input: ArchiveFlowInput): Promise<Flow>;
|
|
172
182
|
save(actor: FlowActor, input: SaveFlowVersionInput): Promise<FlowDocument>;
|
|
173
183
|
previewPublish(actor: FlowActor, input: PreviewFlowPublishInput): Promise<FlowPublishPreview>;
|
|
174
184
|
publish(actor: FlowActor, input: PublishFlowInput): Promise<Flow>;
|
package/dist/http/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppendKnowledgeTableRowsInput, ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, KnowledgeGraphInput, ListFlowRunsInput, ListFlowsInput, ListKnowledgeNodesInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, ResolveKnowledgeLinksInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateFlowInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
|
|
1
|
+
import { AppendKnowledgeTableRowsInput, ArchiveFlowInput, ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, KnowledgeGraphInput, ListFlowRunsInput, ListFlowsInput, ListKnowledgeNodesInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, ResolveKnowledgeLinksInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateFlowInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { authorizeBearer, bearer, permits, } from "../shared/gate-authorization/gate-authorization.js";
|
|
@@ -250,11 +250,16 @@ export function createHttp(deps) {
|
|
|
250
250
|
app.get("/flows", async (context) => {
|
|
251
251
|
const auth = requireCapability(context, "flows", "read");
|
|
252
252
|
const url = new URL(context.req.url);
|
|
253
|
-
requireKnownQuery(url, ["parentId"]);
|
|
253
|
+
requireKnownQuery(url, ["parentId", "includeArchived"]);
|
|
254
254
|
// Absent means "every flow I may see"; present but empty means the root of the shared tree.
|
|
255
255
|
// Without that distinction a tree level and a full list would be the same request.
|
|
256
256
|
const parentId = url.searchParams.get("parentId");
|
|
257
|
-
|
|
257
|
+
// Same rule for the archive: absent means "without it". Spelling the default out would put a
|
|
258
|
+
// field into every call that asks nothing about the archive.
|
|
259
|
+
const input = ListFlowsInput.parse({
|
|
260
|
+
...(url.searchParams.has("parentId") ? { parentId: parentId === "" ? null : parentId } : {}),
|
|
261
|
+
...(url.searchParams.get("includeArchived") === "true" ? { includeArchived: true } : {}),
|
|
262
|
+
});
|
|
258
263
|
return context.json(await deps.flows.list(asFlowActor(auth), input));
|
|
259
264
|
});
|
|
260
265
|
// ⚠️ Before `/flows/:flowId`, or the router would read "graph" as a flow ID. Same order and same
|
|
@@ -318,6 +323,16 @@ export function createHttp(deps) {
|
|
|
318
323
|
}
|
|
319
324
|
return context.json(await deps.flows.update(asFlowActor(auth), input));
|
|
320
325
|
});
|
|
326
|
+
// `flows/write`, the same capability renaming and moving need: archiving is organization, not a
|
|
327
|
+
// change to what the flow does.
|
|
328
|
+
app.post("/flows/:flowId/archive", async (context) => {
|
|
329
|
+
const auth = requireCapability(context, "flows", "write");
|
|
330
|
+
const input = ArchiveFlowInput.parse(await context.req.json().catch(() => null));
|
|
331
|
+
if (input.flowId !== context.req.param("flowId")) {
|
|
332
|
+
throw new IntelError(400, "flow_id_mismatch", "Path and body flow IDs differ");
|
|
333
|
+
}
|
|
334
|
+
return context.json(await deps.flows.archive(asFlowActor(auth), input));
|
|
335
|
+
});
|
|
321
336
|
app.post("/flows/:flowId/versions", async (context) => {
|
|
322
337
|
const auth = requireCapability(context, "flows", "write");
|
|
323
338
|
const input = SaveFlowVersionInput.parse(await context.req.json().catch(() => null));
|
package/dist/mcp/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AppendKnowledgeTableRowsInput, ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetKnowledgeNodeInput, GetKnowledgeTableInput, KnowledgeGraphInput, ListFlowRunsInput, ListFlowsInput, ListKnowledgeGrantsInput, ListKnowledgeNodesInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, ResolveKnowledgeLinksInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateFlowInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
|
|
1
|
+
import { AppendKnowledgeTableRowsInput, ArchiveFlowInput, ArchiveKnowledgeNodeInput, CompleteFlowRunStepInput, CreateFlowInput, CreateKnowledgeNodeInput, DefineKnowledgeTableInput, ExecuteToolInput, GetFlowInput, GetFlowRunInput, GetKnowledgeNodeInput, GetKnowledgeTableInput, KnowledgeGraphInput, ListFlowRunsInput, ListFlowsInput, ListKnowledgeGrantsInput, ListKnowledgeNodesInput, PreviewFlowPublishInput, PublishFlowInput, RelationGraphInput, ResolveKnowledgeLinksInput, RevokeKnowledgeGrantInput, SaveFlowVersionInput, SaveKnowledgeAttachmentInput, SaveKnowledgeVersionInput, SearchKnowledgeInput, ShareKnowledgeInput, StartFlowRunInput, TestToolInput, UpdateFlowInput, UpdateKnowledgeNodeInput, } from "@anchrd/intel-contract";
|
|
2
2
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -473,6 +473,20 @@ export async function handleMcp(request, deps) {
|
|
|
473
473
|
openWorldHint: false,
|
|
474
474
|
},
|
|
475
475
|
}, async (input) => text(await deps.flows.update(flowActor, input)));
|
|
476
|
+
server.registerTool("flow_archive", {
|
|
477
|
+
title: "Archive flow",
|
|
478
|
+
description: "Archive or restore one flow using optimistic concurrency. An archived flow keeps its versions but can no longer be opened, started, or called by another flow.",
|
|
479
|
+
inputSchema: ArchiveFlowInput,
|
|
480
|
+
annotations: {
|
|
481
|
+
title: "Archive flow",
|
|
482
|
+
readOnlyHint: false,
|
|
483
|
+
// What it takes away is reach, not content: nothing is deleted and `archived: false` puts
|
|
484
|
+
// it back. Destructive all the same, because a flow another flow calls stops resolving.
|
|
485
|
+
destructiveHint: true,
|
|
486
|
+
idempotentHint: true,
|
|
487
|
+
openWorldHint: false,
|
|
488
|
+
},
|
|
489
|
+
}, async (input) => text(await deps.flows.archive(flowActor, input)));
|
|
476
490
|
server.registerTool("flow_save", {
|
|
477
491
|
title: "Save flow",
|
|
478
492
|
description: "Append a validated immutable flow graph version with optimistic concurrency.",
|
package/dist/tools/tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IntelError } from "../shared/intel-error/intel-error.js";
|
|
2
|
+
import { reportUnexpectedError } from "../shared/report-unexpected-error/report-unexpected-error.js";
|
|
2
3
|
const CallTimeoutMs = 15_000;
|
|
3
4
|
const MaxTools = 1_000;
|
|
4
5
|
const MaxResultBytes = 1_000_000;
|
|
@@ -24,6 +25,11 @@ export function createTools(deps) {
|
|
|
24
25
|
return stored.accessToken;
|
|
25
26
|
const refreshed = stored.refreshToken ? await deps.refresh(stored) : null;
|
|
26
27
|
if (!refreshed) {
|
|
28
|
+
// Losing a connection is a visible event for the person and an invisible one for everybody
|
|
29
|
+
// else: the screen simply asks them to sign in again, and no trace says why. The two reasons
|
|
30
|
+
// need telling apart — a portal that issues no refresh token at all is a configuration
|
|
31
|
+
// problem, a refresh that gets rejected is a different one (#97).
|
|
32
|
+
reportUnexpectedError(new Error(`portal token dropped: ${stored.refreshToken ? "refresh was rejected" : "no refresh token was ever issued"}`));
|
|
27
33
|
// A token that cannot be renewed is dropped: leaving it would keep failing every call with a
|
|
28
34
|
// stale credential. The browser answers this by signing in silently again (#60); an MCP
|
|
29
35
|
// client sees the code and repeats its own authorization.
|