@zackbart/connecta 0.25.0 → 0.26.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/AGENTS.md +6 -5
- package/CHANGELOG.md +387 -0
- package/README.md +32 -15
- package/SECURITY.md +6 -5
- package/bin/connecta.mjs +5 -1
- package/dist/activity.d.ts +10 -3
- package/dist/activity.js +10 -1
- package/dist/artifacts/connector.d.ts +55 -0
- package/dist/artifacts/connector.js +724 -0
- package/dist/artifacts/document.js +80 -0
- package/dist/artifacts/guide.js +140 -0
- package/dist/artifacts/html-scan.js +329 -0
- package/dist/artifacts/json.js +68 -0
- package/dist/artifacts/kv-store.d.ts +20 -0
- package/dist/artifacts/kv-store.js +140 -0
- package/dist/artifacts/markdown.js +478 -0
- package/dist/artifacts/module.d.ts +37 -0
- package/dist/artifacts/module.js +60 -0
- package/dist/artifacts/operations.d.ts +195 -0
- package/dist/artifacts/operations.js +944 -0
- package/dist/artifacts/refresh.d.ts +53 -0
- package/dist/artifacts/refresh.js +235 -0
- package/dist/artifacts/routes.js +217 -0
- package/dist/artifacts/types.d.ts +214 -0
- package/dist/artifacts/types.js +22 -0
- package/dist/artifacts/validate.d.ts +62 -0
- package/dist/artifacts/validate.js +653 -0
- package/dist/artifacts.d.ts +6 -0
- package/dist/artifacts.js +9 -0
- package/dist/auth/downstream-oauth.js +397 -48
- package/dist/auth/oauth-start-reset.js +20 -0
- package/dist/branding.d.ts +87 -0
- package/dist/branding.js +170 -1
- package/dist/catalog-service.js +39 -29
- package/dist/catalog.js +42 -8
- package/dist/connector-access.d.ts +18 -4
- package/dist/connector-access.js +65 -20
- package/dist/connector-caller.d.ts +8 -0
- package/dist/connector-caller.js +19 -0
- package/dist/connectors/remote-mcp.js +48 -9
- package/dist/errors.d.ts +16 -1
- package/dist/errors.js +28 -0
- package/dist/execute.js +431 -75
- package/dist/executor-admission.d.ts +11 -1
- package/dist/executor-admission.js +79 -14
- package/dist/executors/quickjs.js +4 -2
- package/dist/index.d.ts +62 -6
- package/dist/index.js +230 -6
- package/dist/invocation.js +56 -10
- package/dist/meta-tools.js +7 -2
- package/dist/module-contracts.d.ts +21 -1
- package/dist/oauth-callback-outcome.js +43 -18
- package/dist/operator-ui/generated.js +2 -2
- package/dist/operator-ui/model.d.ts +62 -5
- package/dist/operator-ui/view.js +372 -28
- package/dist/page-styles.js +7 -0
- package/dist/program-source.js +116 -0
- package/dist/registry.d.ts +42 -6
- package/dist/registry.js +164 -60
- package/dist/resumable.js +1291 -0
- package/dist/routes/activity.js +6 -69
- package/dist/routes/actor-labels.js +114 -0
- package/dist/routes/credentials.js +27 -5
- package/dist/routes/mcp.js +120 -59
- package/dist/routes/oauth-management.js +87 -20
- package/dist/routes/oauth.js +54 -87
- package/dist/routes/operator.js +1 -0
- package/dist/routes/shared.d.ts +23 -1
- package/dist/routes/shared.js +20 -2
- package/dist/routes/ui-data.js +2 -1
- package/dist/routes/ui.js +19 -4
- package/dist/run-journal.js +347 -0
- package/dist/runtime/shared-read.js +106 -0
- package/dist/server.js +30 -2
- package/dist/skills.js +30 -11
- package/dist/timeout.js +26 -0
- package/dist/tool-safety.d.ts +35 -0
- package/dist/tool-safety.js +31 -0
- package/dist/types.d.ts +23 -0
- package/dist/ui.d.ts +17 -5
- package/dist/ui.js +51 -68
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/documentation/architecture.md +214 -27
- package/documentation/auth.md +186 -2
- package/documentation/code-mode.md +236 -63
- package/documentation/meta-tools.md +55 -29
- package/ethos.md +12 -10
- package/examples/worker/AGENTS.md +6 -0
- package/examples/worker/README.md +174 -10
- package/examples/worker/src/d1-activity-row.ts +14 -2
- package/examples/worker/src/d1-activity.ts +4 -2
- package/examples/worker/src/index.ts +36 -1
- package/examples/worker/src/r2-artifact-blobs.ts +29 -0
- package/examples/worker/wrangler.jsonc +5 -1
- package/package.json +6 -2
- package/templates/node/AGENTS.md +9 -2
- package/templates/node/README.md +31 -3
- package/templates/node/package.json +1 -1
- package/templates/node/src/index.ts +27 -0
package/dist/invocation.js
CHANGED
|
@@ -64,17 +64,33 @@ function isCallerCancellation(error, signal) {
|
|
|
64
64
|
return (signal?.aborted === true ||
|
|
65
65
|
(isCallAdmissionError(error) && error.admissionKind === "cancelled"));
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* A downstream MCP tool answered with `isError`. Classified exactly as the
|
|
69
|
+
* plain `Error` it used to be — same message, same heuristics — and marked so
|
|
70
|
+
* a write's outcome can tell "the service said no" from "nobody answered".
|
|
71
|
+
*/
|
|
72
|
+
class DownstreamToolError extends Error {
|
|
73
|
+
}
|
|
67
74
|
function assertRawMcpSuccess(kind, result) {
|
|
68
75
|
if (kind !== "mcp" || result == null || typeof result !== "object")
|
|
69
76
|
return;
|
|
70
77
|
const mcpResult = result;
|
|
71
78
|
if (!mcpResult.isError)
|
|
72
79
|
return;
|
|
73
|
-
throw new
|
|
80
|
+
throw new DownstreamToolError(boundedEchoText(mcpResult.content
|
|
74
81
|
?.filter((block) => block.type === "text")
|
|
75
82
|
.map((block) => block.text ?? "")
|
|
76
83
|
.join("") || "Downstream tool call failed"));
|
|
77
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether a failed attempt is the downstream tool's own answer: an MCP
|
|
87
|
+
* `isError`. A connector's typed error is not, by itself, one — a response
|
|
88
|
+
* too large to read or a refused redirect comes after the request was acted
|
|
89
|
+
* on — so its code decides instead (`classifyWriteOutcome`).
|
|
90
|
+
*/
|
|
91
|
+
function answeredFailure(error) {
|
|
92
|
+
return error instanceof DownstreamToolError;
|
|
93
|
+
}
|
|
78
94
|
export class InvocationFailure extends Error {
|
|
79
95
|
details;
|
|
80
96
|
code;
|
|
@@ -134,6 +150,10 @@ export class InvocationService {
|
|
|
134
150
|
let connectorMs = 0;
|
|
135
151
|
let resultProcessingMs = 0;
|
|
136
152
|
let attempts = 0;
|
|
153
|
+
let dispatchedToConnector = false;
|
|
154
|
+
let answered = false;
|
|
155
|
+
// How a write gate's refusal is recorded; see WriteGateDecision.
|
|
156
|
+
let gateActivity;
|
|
137
157
|
let resolved;
|
|
138
158
|
let activityTarget;
|
|
139
159
|
// The address as written, used for activity when resolution never reached
|
|
@@ -227,6 +247,25 @@ export class InvocationService {
|
|
|
227
247
|
const diagnostics = timing();
|
|
228
248
|
const target = resolved ?? activityTarget;
|
|
229
249
|
const details = enrich(error, target);
|
|
250
|
+
const outcome = () => ({
|
|
251
|
+
ok: false,
|
|
252
|
+
durationMs: Date.now() - started,
|
|
253
|
+
attempts,
|
|
254
|
+
timing: diagnostics,
|
|
255
|
+
...defined({ resolved }),
|
|
256
|
+
dispatched: dispatchedToConnector,
|
|
257
|
+
...(dispatchedToConnector ? { answered } : {}),
|
|
258
|
+
error: details,
|
|
259
|
+
});
|
|
260
|
+
// A pause is not a failure: it leaves one payload-free `paused` event
|
|
261
|
+
// naming the call that waits, and nothing in the operator's log. A call
|
|
262
|
+
// refused only because its run had already stopped leaves neither.
|
|
263
|
+
if (gateActivity === "none")
|
|
264
|
+
return outcome();
|
|
265
|
+
if (gateActivity === "paused") {
|
|
266
|
+
record("paused");
|
|
267
|
+
return outcome();
|
|
268
|
+
}
|
|
230
269
|
// Activity rows stay payload-free by construction; the operator's log is
|
|
231
270
|
// where the downstream reason goes, bounded and without arguments.
|
|
232
271
|
if (target && details.code !== "destructive_tool_requires_approval") {
|
|
@@ -249,14 +288,7 @@ export class InvocationService {
|
|
|
249
288
|
: details.code === "cancelled"
|
|
250
289
|
? "cancelled"
|
|
251
290
|
: "error", { errorCode: details.code });
|
|
252
|
-
return
|
|
253
|
-
ok: false,
|
|
254
|
-
durationMs: Date.now() - started,
|
|
255
|
-
attempts,
|
|
256
|
-
timing: diagnostics,
|
|
257
|
-
...defined({ resolved }),
|
|
258
|
-
error: details,
|
|
259
|
-
};
|
|
291
|
+
return outcome();
|
|
260
292
|
};
|
|
261
293
|
if (context.requestSignal?.aborted) {
|
|
262
294
|
// Preserve the cancelled admission-attempt count without starting discovery.
|
|
@@ -290,7 +322,11 @@ export class InvocationService {
|
|
|
290
322
|
const target = resolution.resolved;
|
|
291
323
|
resolved = target;
|
|
292
324
|
activityTarget = target;
|
|
293
|
-
|
|
325
|
+
const consequential = !isExplicitlyReadOnly(target.definition) && !context.allowDestructive;
|
|
326
|
+
const gate = consequential && (context.gates?.(target) ?? true)
|
|
327
|
+
? context.writeGate
|
|
328
|
+
: undefined;
|
|
329
|
+
if (consequential && !gate) {
|
|
294
330
|
const canonicalAddress = `${target.connector.id}.${target.toolName}`;
|
|
295
331
|
return framingError("destructive_tool_requires_approval", `Tool "${canonicalAddress}" is not explicitly read-only. Invoke it through call_destructive_tool so the MCP host can request explicit approval.`);
|
|
296
332
|
}
|
|
@@ -308,6 +344,13 @@ export class InvocationService {
|
|
|
308
344
|
if (invalid)
|
|
309
345
|
return classifyCallError(invalid);
|
|
310
346
|
}
|
|
347
|
+
if (gate) {
|
|
348
|
+
const decision = yield* gate(target, args ?? {});
|
|
349
|
+
if (decision.kind === "refuse") {
|
|
350
|
+
gateActivity = decision.activity;
|
|
351
|
+
return decision.error;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
311
354
|
try {
|
|
312
355
|
context.beforeDispatch?.();
|
|
313
356
|
}
|
|
@@ -329,6 +372,7 @@ export class InvocationService {
|
|
|
329
372
|
// Cancellation can arrive during admission or context construction.
|
|
330
373
|
if (callSignal?.aborted)
|
|
331
374
|
throw callSignal.reason;
|
|
375
|
+
dispatchedToConnector = true;
|
|
332
376
|
return target.connector.callTool(target.toolName, args ?? {}, connectorContext);
|
|
333
377
|
};
|
|
334
378
|
// The permit belongs to this scope, so success, failure, and the
|
|
@@ -351,6 +395,7 @@ export class InvocationService {
|
|
|
351
395
|
if (callSignal?.aborted)
|
|
352
396
|
return yield* Effect.fail(callSignal.reason);
|
|
353
397
|
const attemptError = Cause.squash(attempt.cause);
|
|
398
|
+
answered = answeredFailure(attemptError);
|
|
354
399
|
return isCallerCancellation(attemptError, context.requestSignal)
|
|
355
400
|
? callerCancelledDetails()
|
|
356
401
|
: classifyCallError(attemptError);
|
|
@@ -415,6 +460,7 @@ export class InvocationService {
|
|
|
415
460
|
durationMs: Date.now() - started,
|
|
416
461
|
attempts,
|
|
417
462
|
timing: diagnostics,
|
|
463
|
+
dispatched: dispatchedToConnector,
|
|
418
464
|
};
|
|
419
465
|
}
|
|
420
466
|
catch {
|
package/dist/meta-tools.js
CHANGED
|
@@ -400,6 +400,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
400
400
|
concurrency: discoveryConcurrency,
|
|
401
401
|
defer: opts.defer,
|
|
402
402
|
requestSignal: opts.requestSignal,
|
|
403
|
+
approval: opts.approval,
|
|
403
404
|
// searchRoute keeps its top-level default. In-program callers use a
|
|
404
405
|
// separate CatalogService configured for connecta.search.
|
|
405
406
|
});
|
|
@@ -470,6 +471,8 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
470
471
|
// Sanitized `unavailable` diagnostics ride the structured shape too;
|
|
471
472
|
// the plain-text path would drop them (#539).
|
|
472
473
|
outcome.error.details !== undefined ||
|
|
474
|
+
// So does a conflict's `current`: where things stand is the retry.
|
|
475
|
+
outcome.error.current !== undefined ||
|
|
473
476
|
[
|
|
474
477
|
"auth_required",
|
|
475
478
|
"invalid_args",
|
|
@@ -780,7 +783,7 @@ export function createMetaTools(registry, baseUrl, opts = {}) {
|
|
|
780
783
|
},
|
|
781
784
|
};
|
|
782
785
|
}
|
|
783
|
-
const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds
|
|
786
|
+
const SEARCH_DESC = `Use top-level search for catalog inspection or approval-required work before call_destructive_tool. Unknown-address read-only work belongs in one execute_code program that searches, calls, and returns the answer. Use 2–4 action/object terms and includeSchemas="compact"; the default limit is ${DEFAULT_SEARCH_LIMIT}. Set connector when known. safety="readOnly" finds tools that run unasked; "approvalRequired" finds the fail-closed complement. These filters grant no authority. Empty query browses.`;
|
|
784
787
|
const CALL_DESC = 'Call one known-address tool explicitly annotated readOnlyHint: true. Use execute_code for unknown-address, multiple, dependent, or reduced read-only work. Unannotated or write-capable tools fail closed to call_destructive_tool. A truncated result carries a get_result action.';
|
|
785
788
|
const CALL_DESTRUCTIVE_DESC = "Call any tool not explicitly annotated readOnlyHint: true. Include a short reason for the human reviewer after checking the schema and consequences. The reason grants no authority and is not sent downstream.";
|
|
786
789
|
const GET_RESULT_DESC = "Page a truncated direct-call result by id and byte offset. A program result is never paged; reduce it inside execute_code. Returns a one-line JSON header (offset, bytes, totalBytes, hasMore, nextAction) and then the page as raw text. maxBytes is an upper bound, clamped to the result's inline cap.";
|
|
@@ -868,7 +871,8 @@ const GET_RESULT_INPUT = advertisedSchema(z.object({
|
|
|
868
871
|
}));
|
|
869
872
|
/**
|
|
870
873
|
* Register the six explicit meta-tools onto an McpServer instance.
|
|
871
|
-
* `registerExecuteTool` adds the seventh, `execute_code
|
|
874
|
+
* `registerExecuteTool` adds the seventh, `execute_code`, and
|
|
875
|
+
* `registerResumeTool` the eighth, `resume_execution`. Broad discovery and
|
|
872
876
|
* multi-call work uses discovery and ordinary JavaScript promises inside a
|
|
873
877
|
* program, which `execute_code` builds over the same
|
|
874
878
|
* `CatalogService` and `InvocationService` these handlers use — one shared
|
|
@@ -884,6 +888,7 @@ export function registerMetaTools(server, registry, ctx) {
|
|
|
884
888
|
credentialHandoffUrl: ctx.credentialHandoffUrl,
|
|
885
889
|
requestSignal: ctx.requestSignal,
|
|
886
890
|
defer: ctx.defer,
|
|
891
|
+
approval: ctx.approval,
|
|
887
892
|
});
|
|
888
893
|
server.registerTool("skills", {
|
|
889
894
|
description: describedFor(registry, SKILLS_DESC, "skills"),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { ConnectaBranding } from "./types.js";
|
|
1
|
+
import type { ConnectaBranding, Connector } from "./types.js";
|
|
2
2
|
import type { ActivityStore, ActivityReadGate, recordToolActivity, recordCatalogDriftActivity } from "./activity.js";
|
|
3
3
|
import type { RouteContext } from "./routes/shared.js";
|
|
4
|
+
import type { ArtifactRefreshRuntime } from "./artifacts/refresh.js";
|
|
4
5
|
/** Construction-time UI contract. Core never imports the implementation. */
|
|
5
6
|
export interface OperatorSurface {
|
|
6
7
|
readonly branding?: ConnectaBranding;
|
|
@@ -17,3 +18,22 @@ export interface ActivityModule {
|
|
|
17
18
|
readonly recordTool: typeof recordToolActivity;
|
|
18
19
|
readonly recordDrift: typeof recordCatalogDriftActivity;
|
|
19
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* The artifacts module, created by `artifacts()` from /artifacts. Core appends
|
|
23
|
+
* its one prebuilt connector to the configured set — the same catalog,
|
|
24
|
+
* invocation, admission, and activity paths as any other — and imports none
|
|
25
|
+
* of its implementation.
|
|
26
|
+
*/
|
|
27
|
+
export interface ArtifactsModule {
|
|
28
|
+
/** The built-in `artifacts` connector. */
|
|
29
|
+
readonly connector: Connector;
|
|
30
|
+
/**
|
|
31
|
+
* Serve the pages' JSON API (`/artifacts/_api/*`) and the sandboxed frame
|
|
32
|
+
* (`/artifacts/_frame`); null for any other path. The operator UI delegates
|
|
33
|
+
* here and serves the page shells itself, so without `ui` there are no
|
|
34
|
+
* artifact routes at all.
|
|
35
|
+
*/
|
|
36
|
+
handle(context: RouteContext): Promise<Response | null>;
|
|
37
|
+
/** Bind the optional refresh runner after core has built its registry and executor. */
|
|
38
|
+
bindRefresh?(runtime: ArtifactRefreshRuntime): void;
|
|
39
|
+
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { renderFixPrompt } from "./fix-prompt.js";
|
|
2
2
|
const OUTCOMES = {
|
|
3
|
-
connected: {
|
|
3
|
+
connected: {
|
|
4
|
+
status: 200,
|
|
5
|
+
tone: "ok",
|
|
6
|
+
heading: "{name} is connected",
|
|
7
|
+
message: "You can close this window.",
|
|
8
|
+
namesConnector: true,
|
|
9
|
+
},
|
|
4
10
|
denied: {
|
|
5
11
|
status: 400,
|
|
6
|
-
|
|
12
|
+
tone: "declined",
|
|
13
|
+
heading: "Authorization was declined",
|
|
14
|
+
message: "Consent was declined at the provider, so nothing was stored. Start authorization again whenever you are ready.",
|
|
7
15
|
fix: {
|
|
8
16
|
problem: "Consent for a downstream OAuth connector was declined at the provider, so no grant was stored.",
|
|
9
17
|
steps: [
|
|
@@ -15,7 +23,9 @@ const OUTCOMES = {
|
|
|
15
23
|
},
|
|
16
24
|
provider_error: {
|
|
17
25
|
status: 400,
|
|
18
|
-
|
|
26
|
+
tone: "problem",
|
|
27
|
+
heading: "The provider returned an error",
|
|
28
|
+
message: "It sent back an error instead of an authorization code, so nothing was stored.",
|
|
19
29
|
fix: {
|
|
20
30
|
problem: "A provider redirected back to the OAuth callback with an OAuth error instead of an authorization code.",
|
|
21
31
|
steps: [
|
|
@@ -27,7 +37,9 @@ const OUTCOMES = {
|
|
|
27
37
|
},
|
|
28
38
|
invalid_callback: {
|
|
29
39
|
status: 400,
|
|
30
|
-
|
|
40
|
+
tone: "problem",
|
|
41
|
+
heading: "Authorization could not be completed",
|
|
42
|
+
message: "The link may be expired, already used, or replaced by a newer attempt. Start authorization again from {product}.",
|
|
31
43
|
fix: {
|
|
32
44
|
problem: "An OAuth callback arrived that the deployment could not match to an authorization flow it started: stale, already used, replaced by a newer attempt, or finished in a different session.",
|
|
33
45
|
steps: [
|
|
@@ -39,7 +51,9 @@ const OUTCOMES = {
|
|
|
39
51
|
},
|
|
40
52
|
handoff_failed: {
|
|
41
53
|
status: 500,
|
|
42
|
-
|
|
54
|
+
tone: "problem",
|
|
55
|
+
heading: "{name} could not be connected",
|
|
56
|
+
message: "Authorization could not be completed, and nothing was exchanged. Try again shortly.",
|
|
43
57
|
namesConnector: true,
|
|
44
58
|
fix: {
|
|
45
59
|
problem: "The deployment could not consume the stored principal handoff for a personal-auth OAuth flow, so no authorization code was exchanged.",
|
|
@@ -51,7 +65,9 @@ const OUTCOMES = {
|
|
|
51
65
|
},
|
|
52
66
|
exchange_failed: {
|
|
53
67
|
status: 500,
|
|
54
|
-
|
|
68
|
+
tone: "problem",
|
|
69
|
+
heading: "{name} could not be connected",
|
|
70
|
+
message: "The provider did not accept the authorization code exchange. Nothing was stored.",
|
|
55
71
|
namesConnector: true,
|
|
56
72
|
fix: {
|
|
57
73
|
problem: "A provider returned an authorization code, but exchanging it for tokens failed.",
|
|
@@ -63,7 +79,6 @@ const OUTCOMES = {
|
|
|
63
79
|
},
|
|
64
80
|
},
|
|
65
81
|
};
|
|
66
|
-
const CONNECTOR_ID_RE = /^[a-z0-9_-]+$/;
|
|
67
82
|
/**
|
|
68
83
|
* RFC 6749 names `access_denied` for a declined consent (section 4.1.2.1). Every other
|
|
69
84
|
* value, standard or invented, is the provider refusing the request itself —
|
|
@@ -73,22 +88,32 @@ const CONNECTOR_ID_RE = /^[a-z0-9_-]+$/;
|
|
|
73
88
|
export function providerErrorReason(error) {
|
|
74
89
|
return error === "access_denied" ? "denied" : "provider_error";
|
|
75
90
|
}
|
|
76
|
-
/**
|
|
77
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Page copy and fix prompt for one reason. Nothing here reads the request.
|
|
93
|
+
*
|
|
94
|
+
* `connector` is read only for a reason that sets `namesConnector`. The route
|
|
95
|
+
* passes one only once the state check has proved the flow, and this gate
|
|
96
|
+
* keeps a caller that passed one earlier from naming it anyway. `productName`
|
|
97
|
+
* is deployment configuration, the same for every connector, so it cannot
|
|
98
|
+
* tell one refusal from another.
|
|
99
|
+
*/
|
|
100
|
+
export function oauthCallbackOutcome(reason, connector, productName = "Connecta") {
|
|
78
101
|
const outcome = OUTCOMES[reason];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const message = reason === "connected" && connectorId && CONNECTOR_ID_RE.test(connectorId)
|
|
82
|
-
? `Connected "${connectorId}". You can close this window.`
|
|
83
|
-
: outcome.message;
|
|
102
|
+
const named = outcome.namesConnector ? connector : undefined;
|
|
103
|
+
const name = named ? displayName(named) : "The connection";
|
|
84
104
|
return {
|
|
85
105
|
reason,
|
|
86
106
|
status: outcome.status,
|
|
87
|
-
|
|
107
|
+
tone: outcome.tone,
|
|
108
|
+
heading: outcome.heading.replace("{name}", () => name),
|
|
109
|
+
message: outcome.message.replace("{product}", () => productName),
|
|
88
110
|
...(outcome.fix
|
|
89
|
-
? {
|
|
90
|
-
fixPrompt: renderFixPrompt(outcome.fix, outcome.namesConnector ? connectorId : undefined),
|
|
91
|
-
}
|
|
111
|
+
? { fixPrompt: renderFixPrompt(outcome.fix, named?.id) }
|
|
92
112
|
: {}),
|
|
93
113
|
};
|
|
94
114
|
}
|
|
115
|
+
/** A connector's title when it has a usable one, otherwise its id. */
|
|
116
|
+
function displayName(connector) {
|
|
117
|
+
const title = typeof connector.title === "string" ? connector.title.trim() : "";
|
|
118
|
+
return title || connector.id;
|
|
119
|
+
}
|