@setluca/mcp 0.2.0 → 0.3.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/CHANGELOG.md +426 -0
- package/LICENSE +21 -0
- package/README.md +19 -258
- package/dist/annotations.js +35 -20
- package/dist/client-address.js +64 -0
- package/dist/completions.js +44 -27
- package/dist/config.js +89 -38
- package/dist/errors.js +99 -22
- package/dist/fields.js +68 -0
- package/dist/fixed-window.js +46 -0
- package/dist/generated/openapi-inputs.js +216 -5820
- package/dist/generated/openapi-outputs.js +19014 -14001
- package/dist/generated/route-catalog.js +80 -0
- package/dist/http.js +108 -44
- package/dist/index.js +4 -1
- package/dist/oauth-challenge.js +74 -0
- package/dist/oauth-resolver.js +58 -61
- package/dist/observability.js +31 -16
- package/dist/openapi-schema.js +175 -0
- package/dist/operations/catalog.js +17 -0
- package/dist/operations/groups/analytics.js +20 -10
- package/dist/operations/groups/bookings.js +43 -15
- package/dist/operations/groups/broadcasts.js +29 -58
- package/dist/operations/groups/cadences.js +3 -1
- package/dist/operations/groups/callEvents.js +19 -72
- package/dist/operations/groups/campaigns.js +28 -32
- package/dist/operations/groups/channels.js +23 -0
- package/dist/operations/groups/conversations.js +14 -25
- package/dist/operations/groups/insights.js +4 -0
- package/dist/operations/groups/integrations.js +15 -17
- package/dist/operations/groups/knowledge.js +5 -1
- package/dist/operations/groups/leads.js +22 -23
- package/dist/operations/groups/learning.js +13 -1
- package/dist/operations/groups/memory.js +4 -6
- package/dist/operations/groups/reports.js +1 -1
- package/dist/operations/groups/reviewQueue.js +17 -14
- package/dist/operations/groups/safety.js +1 -1
- package/dist/operations/groups/voice.js +0 -1
- package/dist/operations/groups/webhooks.js +16 -4
- package/dist/operations/manifest.js +9 -8
- package/dist/operations/registry.js +110 -116
- package/dist/operations.js +6 -5
- package/dist/optional-field.js +15 -3
- package/dist/page-contract.js +74 -0
- package/dist/pagination.js +149 -68
- package/dist/prompts.js +139 -88
- package/dist/provenance.js +2 -2
- package/dist/rate-limit.js +40 -0
- package/dist/remote.js +145 -65
- package/dist/resilience.js +120 -55
- package/dist/resolvers.js +125 -0
- package/dist/resources.js +17 -2
- package/dist/scopes.js +207 -0
- package/dist/serialization.js +32 -22
- package/dist/server.js +174 -191
- package/dist/standard-schema.js +215 -0
- package/dist/task-tools/analytics.js +145 -86
- package/dist/task-tools/bookings.js +176 -86
- package/dist/task-tools/cadences.js +30 -45
- package/dist/task-tools/calls.js +132 -125
- package/dist/task-tools/inbox.js +89 -53
- package/dist/task-tools/leads.js +49 -77
- package/dist/task-tools/runtime.js +80 -20
- package/dist/task-tools.js +1 -1
- package/dist/token-verification.js +42 -0
- package/dist/tool-cache.js +3 -2
- package/dist/tool.js +155 -100
- package/dist/version.js +1 -1
- package/package.json +40 -18
- package/dist/openapi-zod.js +0 -333
- package/dist/page-shape.js +0 -47
- package/dist/tsconfig.tsbuildinfo +0 -1
package/dist/task-tools/leads.js
CHANGED
|
@@ -1,120 +1,92 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import {
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import { MessagingChannel, NonEmptyString, WORKSPACE_INPUT_FIELDS, } from "../fields.js";
|
|
3
3
|
import { optionalField } from "../optional-field.js";
|
|
4
|
-
import { numberInput, stringInput,
|
|
5
|
-
const findLeads = {
|
|
4
|
+
import { idempotencyKeyInput, numberInput, operationResult, stringInput, taskTool, textBody, } from "./runtime.js";
|
|
5
|
+
const findLeads = taskTool({
|
|
6
6
|
name: "luca_find_leads",
|
|
7
7
|
title: "Find leads",
|
|
8
8
|
description: "Search the coach's leads by free-text query and optional channel. Use this to locate a lead before drafting, booking, or flagging. Example: call with { query: \"maria instagram\" } to find Maria's Instagram lead.",
|
|
9
9
|
inputSchema: {
|
|
10
10
|
...WORKSPACE_INPUT_FIELDS,
|
|
11
|
-
query:
|
|
12
|
-
.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
.describe("Optional channel filter (telegram, instagram, messenger, whatsapp)."),
|
|
19
|
-
limit: z
|
|
20
|
-
.number()
|
|
21
|
-
.int()
|
|
22
|
-
.min(1)
|
|
23
|
-
.max(50)
|
|
24
|
-
.optional()
|
|
25
|
-
.describe("Max results (default 10)."),
|
|
11
|
+
query: NonEmptyString.annotate({
|
|
12
|
+
description: "Free-text search: name, handle, or keyword.",
|
|
13
|
+
}),
|
|
14
|
+
channel: Schema.optionalKey(MessagingChannel.annotate({ description: "Optional channel filter." })),
|
|
15
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 50 })).annotate({
|
|
16
|
+
description: "Max results (default 10).",
|
|
17
|
+
})),
|
|
26
18
|
},
|
|
27
|
-
|
|
28
|
-
untrustedContent: true,
|
|
19
|
+
outputSchema: operationResult("leads.list"),
|
|
29
20
|
composes: ["leads.list"],
|
|
30
|
-
run: (
|
|
21
|
+
run: (call, input) => call("leads.list", {
|
|
31
22
|
query: {
|
|
32
23
|
q: stringInput(input.query),
|
|
33
24
|
...optionalField("channel", input.channel),
|
|
34
25
|
limit: numberInput(input.limit, 10),
|
|
35
26
|
},
|
|
36
|
-
})
|
|
37
|
-
};
|
|
38
|
-
const flagForHuman = {
|
|
27
|
+
}),
|
|
28
|
+
});
|
|
29
|
+
const flagForHuman = taskTool({
|
|
39
30
|
name: "luca_flag_for_human",
|
|
40
31
|
title: "Flag a lead for human attention",
|
|
41
32
|
description: 'Add a note to a lead, tagged [needs-human], that the coach sees on the lead\'s timeline. This is an ordinary note with a prefix — it does not route, alert, or reprioritize anything. Use when a conversation needs judgment an agent should not exercise (payment claims, distress, legal), and tell the coach directly if it is urgent. Example: call with { leadId, reason: "lead mentioned a refund dispute" }.',
|
|
42
33
|
inputSchema: {
|
|
43
34
|
...WORKSPACE_INPUT_FIELDS,
|
|
44
|
-
leadId:
|
|
45
|
-
reason:
|
|
46
|
-
.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
idempotencyKey: z
|
|
50
|
-
.string()
|
|
51
|
-
.optional()
|
|
52
|
-
.describe("Reuse the same key when retrying this exact flag."),
|
|
35
|
+
leadId: NonEmptyString.annotate({ description: "The lead to flag." }),
|
|
36
|
+
reason: NonEmptyString.annotate({
|
|
37
|
+
description: "Why a human should look — shown to the coach verbatim.",
|
|
38
|
+
}),
|
|
39
|
+
...idempotencyKeyInput("Reuse the same key when retrying this exact flag."),
|
|
53
40
|
},
|
|
54
|
-
|
|
55
|
-
untrustedContent: false,
|
|
41
|
+
outputSchema: operationResult("leads.notes.add"),
|
|
56
42
|
composes: ["leads.notes.add"],
|
|
57
|
-
run: (
|
|
43
|
+
run: (call, input) => call("leads.notes.add", {
|
|
58
44
|
pathParams: { id: stringInput(input.leadId) },
|
|
59
45
|
body: { body: `[needs-human] ${stringInput(input.reason)}` },
|
|
60
|
-
})
|
|
61
|
-
};
|
|
62
|
-
const draftReply = {
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
const draftReply = taskTool({
|
|
63
49
|
name: "luca_draft_reply",
|
|
64
50
|
title: "Draft a reply for a lead",
|
|
65
51
|
description: 'Draft a voice-matched reply for a lead on demand and queue it for review — the middle step of the reply loop (triage → draft → approve). Optional guidance steers tone or content. Nothing is sent; approve it with luca_approve_and_send. Example: call with { leadId, guidance: "keep it short and warm" }.',
|
|
66
52
|
inputSchema: {
|
|
67
53
|
...WORKSPACE_INPUT_FIELDS,
|
|
68
|
-
leadId:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
idempotencyKey: z
|
|
76
|
-
.string()
|
|
77
|
-
.optional()
|
|
78
|
-
.describe("Reuse the same key when retrying this exact draft."),
|
|
54
|
+
leadId: NonEmptyString.annotate({
|
|
55
|
+
description: "The lead to draft a reply for.",
|
|
56
|
+
}),
|
|
57
|
+
guidance: Schema.optionalKey(NonEmptyString.check(Schema.isMaxLength(500)).annotate({
|
|
58
|
+
description: "Optional steer for tone or content of the draft.",
|
|
59
|
+
})),
|
|
60
|
+
...idempotencyKeyInput("Reuse the same key when retrying this exact draft."),
|
|
79
61
|
},
|
|
80
|
-
|
|
81
|
-
untrustedContent: false,
|
|
62
|
+
outputSchema: operationResult("leads.draft"),
|
|
82
63
|
composes: ["leads.draft"],
|
|
83
|
-
run: (
|
|
64
|
+
run: (call, input) => call("leads.draft", {
|
|
84
65
|
pathParams: { id: stringInput(input.leadId) },
|
|
85
66
|
body: textBody("guidance", input.guidance),
|
|
86
|
-
})
|
|
87
|
-
};
|
|
88
|
-
const approveAndSend = {
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
const approveAndSend = taskTool({
|
|
89
70
|
name: "luca_approve_and_send",
|
|
90
71
|
title: "Approve a drafted reply and send it",
|
|
91
72
|
description: "Approve a pending review-queue draft and dispatch it through the lead's channel. THIS SENDS A REAL MESSAGE TO A REAL PERSON — only call it after a human has reviewed and intends to send. Pass confirm: true to proceed. Optional finalBody replaces the draft text before sending. Example: { reviewQueueId, confirm: true }.",
|
|
92
73
|
inputSchema: {
|
|
93
74
|
...WORKSPACE_INPUT_FIELDS,
|
|
94
|
-
reviewQueueId:
|
|
95
|
-
.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.optional()
|
|
102
|
-
.describe("Optional edited body to send instead of the drafted text."),
|
|
103
|
-
...confirmInput,
|
|
104
|
-
idempotencyKey: z
|
|
105
|
-
.string()
|
|
106
|
-
.optional()
|
|
107
|
-
.describe("Reuse the same key when retrying this exact approval."),
|
|
75
|
+
reviewQueueId: NonEmptyString.annotate({
|
|
76
|
+
description: "The review-queue item to approve.",
|
|
77
|
+
}),
|
|
78
|
+
finalBody: Schema.optionalKey(NonEmptyString.annotate({
|
|
79
|
+
description: "Optional edited body to send instead of the drafted text.",
|
|
80
|
+
})),
|
|
81
|
+
...idempotencyKeyInput("Reuse the same key when retrying this exact approval."),
|
|
108
82
|
},
|
|
109
|
-
|
|
110
|
-
untrustedContent: false,
|
|
111
|
-
confirmRequired: true,
|
|
83
|
+
outputSchema: operationResult("reviewQueue.approve"),
|
|
112
84
|
composes: ["reviewQueue.approve"],
|
|
113
|
-
run: (
|
|
85
|
+
run: (call, input) => call("reviewQueue.approve", {
|
|
114
86
|
pathParams: { id: stringInput(input.reviewQueueId) },
|
|
115
87
|
body: textBody("finalBody", input.finalBody),
|
|
116
|
-
})
|
|
117
|
-
};
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
118
90
|
export const leadTaskTools = [
|
|
119
91
|
findLeads,
|
|
120
92
|
flagForHuman,
|
|
@@ -1,7 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
3
|
-
import
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Option from "effect/Option";
|
|
3
|
+
import * as P from "effect/Predicate";
|
|
4
|
+
import * as R from "effect/Record";
|
|
5
|
+
import * as Result from "effect/Result";
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as Str from "effect/String";
|
|
8
|
+
import { formatError, isAuthorizationFailure, } from "../errors.js";
|
|
9
|
+
import { NonEmptyString } from "../fields.js";
|
|
10
|
+
import { operationById, } from "../operations.js";
|
|
4
11
|
import { optionalField } from "../optional-field.js";
|
|
12
|
+
/**
|
|
13
|
+
* Declares a task tool, inferring the id union from `composes` so `run` can
|
|
14
|
+
* only call what the tool declares.
|
|
15
|
+
*/
|
|
16
|
+
export function taskTool(tool) {
|
|
17
|
+
return tool;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The `result` an operation tool declares, for a task tool that hands that
|
|
21
|
+
* response back unchanged or nests it in its own output.
|
|
22
|
+
*/
|
|
23
|
+
export function operationResult(id) {
|
|
24
|
+
return operationById(id).outputSchema.result ?? Schema.Unknown;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A field that holds an operation's response, or the error a tool recorded in
|
|
28
|
+
* its place under `errorKey`.
|
|
29
|
+
*/
|
|
30
|
+
export function resultOrError(id, errorKey) {
|
|
31
|
+
return Schema.Union([
|
|
32
|
+
operationResult(id),
|
|
33
|
+
Schema.Struct({ [errorKey]: Schema.String }),
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
5
36
|
function workspaceOverride(input) {
|
|
6
37
|
const override = {
|
|
7
38
|
...optionalField("workspaceId", input.workspaceId),
|
|
@@ -9,7 +40,7 @@ function workspaceOverride(input) {
|
|
|
9
40
|
};
|
|
10
41
|
// An override naming neither field is no override at all, and sending an
|
|
11
42
|
// empty one would pin the request to nothing.
|
|
12
|
-
if (
|
|
43
|
+
if (R.isEmptyReadonlyRecord(override)) {
|
|
13
44
|
return;
|
|
14
45
|
}
|
|
15
46
|
return override;
|
|
@@ -18,7 +49,7 @@ function workspaceOverride(input) {
|
|
|
18
49
|
* Assemble the request a task tool sends: the resolved operation plus its
|
|
19
50
|
* path/query/body, with the workspace override and idempotency key folded in
|
|
20
51
|
* the same way for every tool. This is the task-tool analogue of operations.ts
|
|
21
|
-
* `commonInput
|
|
52
|
+
* `commonInput`. One seam owns "how a task request is built", so a tool body
|
|
22
53
|
* reads as the fields it maps, not the plumbing. Pass `body: undefined` to omit
|
|
23
54
|
* the body entirely (an absent optional field, never `{ body: undefined }`).
|
|
24
55
|
*/
|
|
@@ -35,8 +66,8 @@ export function taskRequest(operation, input, parts = {}) {
|
|
|
35
66
|
/**
|
|
36
67
|
* The only part of a listing page a composing tool reads for itself: the ids of
|
|
37
68
|
* the items it then fans out over. Decoding that alone leaves the raw page
|
|
38
|
-
* intact for the caller
|
|
39
|
-
*
|
|
69
|
+
* intact for the caller, returned exactly as the API sent it, while the ids the
|
|
70
|
+
* tool acts on are validated rather than asserted.
|
|
40
71
|
*/
|
|
41
72
|
const ListingIds = Schema.Struct({
|
|
42
73
|
items: Schema.optional(Schema.Array(Schema.Struct({ id: Schema.optional(Schema.String) }))),
|
|
@@ -44,7 +75,7 @@ const ListingIds = Schema.Struct({
|
|
|
44
75
|
const decodeListingIds = Schema.decodeUnknownOption(ListingIds);
|
|
45
76
|
/**
|
|
46
77
|
* Ids of the first `limit` items on a listing page. A page whose fields we
|
|
47
|
-
* cannot read, or whose items carry no id, contributes nothing
|
|
78
|
+
* cannot read, or whose items carry no id, contributes nothing. The tool still
|
|
48
79
|
* returns the page itself rather than failing over a fan-out it cannot do.
|
|
49
80
|
*/
|
|
50
81
|
export function listingIds(page, limit) {
|
|
@@ -66,27 +97,56 @@ export function listingIds(page, limit) {
|
|
|
66
97
|
}
|
|
67
98
|
/**
|
|
68
99
|
* A request body carrying a single text field, or no body at all when the input
|
|
69
|
-
* is absent or blank
|
|
100
|
+
* is absent or blank. Sending an empty string would mean something different to
|
|
70
101
|
* the API than leaving the field off.
|
|
71
102
|
*/
|
|
72
103
|
export function textBody(key, value) {
|
|
73
|
-
return Option.
|
|
104
|
+
return Option.fromNullishOr(value).pipe(Option.map(String), Option.filter((text) => Str.isNonEmpty(text)), Option.map((text) => ({ [key]: text })), Option.getOrUndefined);
|
|
74
105
|
}
|
|
75
106
|
/** Reads a numeric tool input, falling back when it is absent or not a number. */
|
|
76
107
|
export function numberInput(value, fallback) {
|
|
77
|
-
|
|
78
|
-
return parsed.success ? parsed.data : fallback;
|
|
108
|
+
return P.isNumber(value) ? value : fallback;
|
|
79
109
|
}
|
|
80
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* A required string argument. The registered tool schema has already checked
|
|
112
|
+
* it, so a non-string here is a bug in this package and dies as a defect.
|
|
113
|
+
*/
|
|
81
114
|
export function stringInput(value) {
|
|
82
|
-
return
|
|
115
|
+
return decodeString(value);
|
|
116
|
+
}
|
|
117
|
+
const decodeString = Schema.decodeUnknownSync(Schema.String);
|
|
118
|
+
/**
|
|
119
|
+
* The `idempotencyKey` input of a task tool that writes. An empty key is
|
|
120
|
+
* refused by the schema rather than read as "no key", because a keyed call
|
|
121
|
+
* skips checks a fresh call runs.
|
|
122
|
+
*/
|
|
123
|
+
export function idempotencyKeyInput(description) {
|
|
124
|
+
return {
|
|
125
|
+
idempotencyKey: Schema.optionalKey(NonEmptyString.annotate({ description })),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A failure a partial-result tool can report in place of one section, or a
|
|
130
|
+
* failure that has to end the tool call. A refused token or a missing scope
|
|
131
|
+
* fails every other request in the tool too, and only a failed tool result
|
|
132
|
+
* carries the challenge that sends the client back through sign-in.
|
|
133
|
+
*/
|
|
134
|
+
export const isPartialFailure = (error) => !isAuthorizationFailure(error);
|
|
135
|
+
/**
|
|
136
|
+
* One section of a partial-result tool, as a `Result` the tool folds into its
|
|
137
|
+
* report. An authorization failure stays in the error channel, see
|
|
138
|
+
* {@link isPartialFailure}.
|
|
139
|
+
*/
|
|
140
|
+
export function sectionResult(effect) {
|
|
141
|
+
return effect.pipe(Effect.map((value) => Result.succeed(value)), Effect.catchIf(isPartialFailure, (error) => Effect.succeed(Result.fail(error))));
|
|
83
142
|
}
|
|
84
143
|
/**
|
|
85
|
-
* A
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
144
|
+
* A request whose failure comes back as a field under `errorKey` instead of
|
|
145
|
+
* ending the tool call. A tool that files two independent writes keeps the
|
|
146
|
+
* second one reachable when the first fails, so the agent reads what failed
|
|
147
|
+
* instead of retrying the half that already landed. An authorization failure
|
|
148
|
+
* still ends the call, see {@link isPartialFailure}.
|
|
89
149
|
*/
|
|
90
|
-
export function
|
|
91
|
-
return Effect.
|
|
150
|
+
export function failureAsField(errorKey, effect) {
|
|
151
|
+
return Effect.catchIf(effect, isPartialFailure, (error) => Effect.succeed({ [errorKey]: formatError(error) }));
|
|
92
152
|
}
|
package/dist/task-tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The task-tool registry: composed, intent-level entry points layered over the
|
|
2
2
|
// generated 1:1 operation tools. It is deliberately NOT `LUCA_OPERATIONS`,
|
|
3
3
|
// whose entries the OpenAPI drift checker matches one-to-one against REST
|
|
4
|
-
// routes
|
|
4
|
+
// routes, and a task tool has no route of its own. Each tool hand-writes its
|
|
5
5
|
// input fields and composes existing operations through `LucaApi.request`, and
|
|
6
6
|
// they are split one file per domain under `task-tools/`. The shape they all
|
|
7
7
|
// share, and the request plumbing they all use, lives in `task-tools/runtime.ts`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { TokenVerificationUnavailable } from "./errors.js";
|
|
4
|
+
/**
|
|
5
|
+
* The longest a verification call may run. It sits in front of every remote
|
|
6
|
+
* request, so a slow API answers 503 within seconds instead of holding the
|
|
7
|
+
* client for the full API timeout. A shorter configured timeout still wins.
|
|
8
|
+
*/
|
|
9
|
+
const VERIFY_TIMEOUT_MS = 5000;
|
|
10
|
+
export function unavailable(message) {
|
|
11
|
+
return (cause) => new TokenVerificationUnavailable({ message, cause });
|
|
12
|
+
}
|
|
13
|
+
/** SHA-256 of a secret as hex, so a limiter or cache never stores it. */
|
|
14
|
+
export const sha256Hex = (value) => Effect.tryPromise(() => crypto.subtle.digest("SHA-256", new TextEncoder().encode(value))).pipe(Effect.map((digest) => Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")));
|
|
15
|
+
/**
|
|
16
|
+
* One bounded call to the Luca API that carries a token, with `read` turning
|
|
17
|
+
* the response into the verdict. Timeout but no retry: this is on the auth
|
|
18
|
+
* path of every request, and the client retries a 503 on its own schedule. A
|
|
19
|
+
* redirect is an error, because it would carry the token to a host we never
|
|
20
|
+
* named.
|
|
21
|
+
*/
|
|
22
|
+
export function callApi(settings, fetchImpl, request, read) {
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
const timeoutMs = Math.min(settings.requestTimeoutMs ?? VERIFY_TIMEOUT_MS, VERIFY_TIMEOUT_MS);
|
|
25
|
+
return Effect.tryPromise({
|
|
26
|
+
try: () => fetchImpl(`${settings.apiBaseUrl}${request.path}`, {
|
|
27
|
+
...request.init,
|
|
28
|
+
redirect: "error",
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
}),
|
|
31
|
+
catch: unavailable("Could not reach token verification"),
|
|
32
|
+
}).pipe(Effect.flatMap(read), Effect.timeoutOrElse({
|
|
33
|
+
duration: Duration.millis(timeoutMs),
|
|
34
|
+
orElse: () => Effect.fail(new TokenVerificationUnavailable({
|
|
35
|
+
message: `Token verification did not answer within ${timeoutMs}ms`,
|
|
36
|
+
})),
|
|
37
|
+
}),
|
|
38
|
+
// The signal belongs to the whole exchange, body read included, so abort
|
|
39
|
+
// on every exit: a timed-out or unread response cannot outlive the auth
|
|
40
|
+
// decision it was made for.
|
|
41
|
+
Effect.ensuring(Effect.sync(() => controller.abort())));
|
|
42
|
+
}
|
package/dist/tool-cache.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as Arr from "effect/Array";
|
|
1
2
|
function cache() {
|
|
2
3
|
let entries = new WeakMap();
|
|
3
4
|
return {
|
|
@@ -33,7 +34,7 @@ export function buildCache() {
|
|
|
33
34
|
* changed underneath it, which is exactly what a mutation run does.
|
|
34
35
|
*/
|
|
35
36
|
export function clearToolBuildCache() {
|
|
36
|
-
|
|
37
|
+
Arr.forEach(resets, (reset) => {
|
|
37
38
|
reset();
|
|
38
|
-
}
|
|
39
|
+
});
|
|
39
40
|
}
|