@zackbart/connecta 0.40.0 → 0.41.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 +87 -0
- package/bin/version.mjs +1 -1
- package/dist/activity.d.ts +30 -4
- package/dist/activity.js +28 -2
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +4 -41
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +12 -4
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +51 -21
- package/dist/catalog-cache.js +62 -21
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-service.js +39 -12
- package/dist/catalog.js +103 -38
- package/dist/config.d.ts +10 -4
- package/dist/config.js +30 -7
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/connection.js +52 -18
- package/dist/connectors/remote-mcp/oauth.js +2 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +24 -6
- package/dist/connectors/remote-mcp.js +4 -4
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +1 -0
- package/dist/describe-config.js +1 -0
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +2 -0
- package/dist/execute.js +20 -7
- package/dist/executor-result.js +1 -1
- package/dist/executors/quickjs-child.js +1 -1
- package/dist/executors/quickjs-runtime.js +1 -1
- package/dist/executors/quickjs.js +1 -1
- package/dist/guarded-transport.js +2 -0
- package/dist/http/client.js +7 -0
- package/dist/http/hatch.js +2 -0
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/index.js +10 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +72 -33
- package/dist/membership.d.ts +1 -1
- package/dist/meta-tools.js +27 -13
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-tools.js +11 -4
- package/dist/node.d.ts +7 -0
- package/dist/node.js +82 -3
- package/dist/operation-activity.js +103 -0
- package/dist/operator-record.d.ts +8 -0
- package/dist/operator-record.js +15 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/github/index.js +1 -1
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +1 -1
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/api.js +1 -0
- package/dist/registry.js +16 -4
- package/dist/resource-read.js +21 -2
- package/dist/route-names.js +44 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/mcp.js +32 -13
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/native-storage.js +12 -0
- package/dist/runtime/request-reads.js +66 -14
- package/dist/runtime/request-scope.js +2 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +132 -43
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +7 -1
- package/dist/storage/sql-native.js +60 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/types.d.ts +5 -0
- package/dist/url-safety.js +38 -20
- package/dist/utility-recovery.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +33 -8
- package/documentation/architecture.md +48 -1
- package/documentation/auth.md +116 -36
- package/documentation/code-mode.md +61 -46
- package/documentation/deploying.md +25 -1
- package/documentation/meta-tools.md +113 -11
- package/documentation/operating.md +86 -1
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +16 -14
- package/package.json +10 -8
- package/templates/node/package.json +1 -1
package/dist/native-tools.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolvedConnectorGuides } from "./skills.js";
|
|
1
2
|
import { bytesToHex } from "./bytes.js";
|
|
2
3
|
import { routeNames } from "./agent-routes.js";
|
|
3
4
|
import { duringViewEffect } from "./runtime/call.js";
|
|
@@ -129,10 +130,12 @@ function passThroughSchema(schema) {
|
|
|
129
130
|
},
|
|
130
131
|
};
|
|
131
132
|
}
|
|
132
|
-
function describe(entry, executeCode) {
|
|
133
|
+
function describe(entry, executeCode, guideName) {
|
|
133
134
|
const lines = [entry.tool.description?.trim() || entry.tool.title || entry.tool.name];
|
|
134
135
|
if (executeCode)
|
|
135
136
|
lines.push(`Program address: ${entry.connector.id}.${entry.tool.name}`);
|
|
137
|
+
if (guideName)
|
|
138
|
+
lines.push(`Guide: skills ${guideName}`);
|
|
136
139
|
return lines.join("\n\n");
|
|
137
140
|
}
|
|
138
141
|
export class NativeSurface {
|
|
@@ -177,10 +180,10 @@ export class NativeSurface {
|
|
|
177
180
|
call(name, args, request) {
|
|
178
181
|
return runEdge(authOwnMethod(NativeSurface.prototype, this, "call", [name, args, request]));
|
|
179
182
|
}
|
|
180
|
-
registerTool(server, name, entry) {
|
|
183
|
+
registerTool(server, name, entry, guideName) {
|
|
181
184
|
server.registerTool(name, {
|
|
182
185
|
...(entry?.tool.title ? { title: entry.tool.title } : {}),
|
|
183
|
-
...(entry ? { description: describe(entry, this.options.executeCode) } : {}),
|
|
186
|
+
...(entry ? { description: describe(entry, this.options.executeCode, guideName) } : {}),
|
|
184
187
|
inputSchema: passThroughSchema(entry?.tool.inputSchema),
|
|
185
188
|
...(entry ? { annotations: annotationsFor(entry.tool) } : {}),
|
|
186
189
|
...(entry ? { _meta: { "dev.connecta/address": `${entry.connector.id}.${entry.tool.name}` } } : {}),
|
|
@@ -220,8 +223,12 @@ export class NativeSurface {
|
|
|
220
223
|
});
|
|
221
224
|
return;
|
|
222
225
|
}
|
|
226
|
+
const guides = yield* upwardStep(authCall(resolvedConnectorGuides, [
|
|
227
|
+
self.registry.listConnectors(),
|
|
228
|
+
]));
|
|
229
|
+
const guideNames = new Map(guides.map(({ connector, reference }) => [connector.id, reference]));
|
|
223
230
|
for (const entry of tools)
|
|
224
|
-
self.registerTool(server, entry.name, entry);
|
|
231
|
+
self.registerTool(server, entry.name, entry, guideNames.get(entry.connector.id));
|
|
225
232
|
})).pipe(Effect.provideService(RequestScope, requestOwner(this.options.requestScope)));
|
|
226
233
|
}, NativeSurface.prototype.register);
|
|
227
234
|
registerAuthMethod(NativeSurface.prototype, "call", function (name, args, request) {
|
package/dist/node.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ export interface ListenOptions {
|
|
|
8
8
|
host?: string;
|
|
9
9
|
/** Pre-auth buffer cap, independent of the authenticated MCP cap. Default 4 MiB. */
|
|
10
10
|
maxBodyBytes?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Cap on the body bytes all in-flight requests hold together, counted as
|
|
13
|
+
* each arrives and released when its response finishes or its client leaves.
|
|
14
|
+
* Past it a request is answered 503 with `Retry-After`. Default 64 MiB, or
|
|
15
|
+
* `maxBodyBytes` when larger. May not be below `maxBodyBytes`.
|
|
16
|
+
*/
|
|
17
|
+
maxInFlightBodyBytes?: number;
|
|
11
18
|
/**
|
|
12
19
|
* Stop accepting connections on SIGTERM/SIGINT, let in-flight requests
|
|
13
20
|
* finish, drain deferred work, then exit. Default true — Docker sends
|
package/dist/node.js
CHANGED
|
@@ -5,13 +5,40 @@ import { failureRecord, logFailure } from "./operator-record.js";
|
|
|
5
5
|
import { configureGuardedTransport } from "./guarded-transport.js";
|
|
6
6
|
import { createNodeGuardedTransport } from "./http/node-transport.js";
|
|
7
7
|
import { CONFIG_DEFAULTS } from "./config-defaults.js";
|
|
8
|
+
import { ConfigError } from "./config-schema.js";
|
|
8
9
|
/** The adapter buffers before auth, bounded by the same default as the core. */
|
|
9
10
|
const DEFAULT_MAX_BODY_BYTES = CONFIG_DEFAULTS.admission.requests.maxBodyBytes;
|
|
11
|
+
/** Sixteen full default bodies; well above ordinary MCP traffic, whose bodies are small. */
|
|
12
|
+
const DEFAULT_MAX_IN_FLIGHT_BODY_BYTES = 64 * 1024 * 1024;
|
|
10
13
|
const BODY_TOO_LARGE = Symbol("body-too-large");
|
|
11
|
-
|
|
14
|
+
const BODIES_AT_CAPACITY = Symbol("bodies-at-capacity");
|
|
15
|
+
/** A name, IPv4 or bracketed IPv6 host with an optional port, and nothing else. */
|
|
16
|
+
const REQUEST_HOST = /^(?:[a-z0-9._-]+|\[[0-9a-f:.]+\])(?::\d{1,5})?$/i;
|
|
17
|
+
/** `Host`, `X-Forwarded-Proto` or the request target is not a URL part; answered 400. */
|
|
18
|
+
class InvalidRequestUrl extends Error {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The request's URL, read from values a client chooses. Each is checked on its
|
|
22
|
+
* own and the origin is parsed before the target joins it, so no value can move
|
|
23
|
+
* the boundary between authority, path and query for the routes behind it.
|
|
24
|
+
*/
|
|
25
|
+
function requestUrl(req) {
|
|
12
26
|
const host = req.headers.host ?? "localhost";
|
|
13
27
|
const proto = req.headers["x-forwarded-proto"] ?? "http";
|
|
14
|
-
|
|
28
|
+
// Origin-form only. Absolute-form and `*` have no path to route.
|
|
29
|
+
const target = req.url ?? "/";
|
|
30
|
+
if (!REQUEST_HOST.test(host) || typeof proto !== "string" || !/^https?$/i.test(proto) || !target.startsWith("/")) {
|
|
31
|
+
throw new InvalidRequestUrl();
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return `${new URL(`${proto}://${host}`).origin}${target}`;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw new InvalidRequestUrl();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function toRequest(req, maxBodyBytes, holdBodyBytes, signal) {
|
|
41
|
+
const url = requestUrl(req);
|
|
15
42
|
const method = req.method ?? "GET";
|
|
16
43
|
const headers = new Headers();
|
|
17
44
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
@@ -29,6 +56,8 @@ async function toRequest(req, maxBodyBytes, signal) {
|
|
|
29
56
|
// rest of a body we have already decided to reject.
|
|
30
57
|
if (size > maxBodyBytes)
|
|
31
58
|
return BODY_TOO_LARGE;
|
|
59
|
+
if (!holdBodyBytes(chunk.length))
|
|
60
|
+
return BODIES_AT_CAPACITY;
|
|
32
61
|
chunks.push(chunk);
|
|
33
62
|
}
|
|
34
63
|
if (chunks.length)
|
|
@@ -64,6 +93,13 @@ export function listen(connecta, portOrOptions) {
|
|
|
64
93
|
const opts = typeof portOrOptions === "number" ? { port: portOrOptions } : portOrOptions;
|
|
65
94
|
configureGuardedTransport(createNodeGuardedTransport(opts.proxy));
|
|
66
95
|
const maxBodyBytes = opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
96
|
+
const maxInFlightBodyBytes = opts.maxInFlightBodyBytes === undefined
|
|
97
|
+
? Math.max(DEFAULT_MAX_IN_FLIGHT_BODY_BYTES, maxBodyBytes)
|
|
98
|
+
: opts.maxInFlightBodyBytes;
|
|
99
|
+
if (!Number.isSafeInteger(maxInFlightBodyBytes) || maxInFlightBodyBytes < 1 || maxInFlightBodyBytes < maxBodyBytes) {
|
|
100
|
+
throw new ConfigError("listen: maxInFlightBodyBytes must be a whole number of bytes, at least maxBodyBytes.");
|
|
101
|
+
}
|
|
102
|
+
let inFlightBodyBytes = 0;
|
|
67
103
|
// Node has no ExecutionContext, so deferred work (activity sinks) would
|
|
68
104
|
// otherwise be an untracked floating promise. Track it here and drain it on
|
|
69
105
|
// shutdown so both runtimes behave the same.
|
|
@@ -101,7 +137,19 @@ export function listen(connecta, portOrOptions) {
|
|
|
101
137
|
res.once("close", finished);
|
|
102
138
|
void (async () => {
|
|
103
139
|
const controller = new AbortController();
|
|
140
|
+
let heldBodyBytes = 0;
|
|
141
|
+
let bodyBytesReleased = false;
|
|
142
|
+
// Pipeline teardown can wait forever on a response stream's cancel().
|
|
143
|
+
// Release on the HTTP lifecycle too, and forbid late upload reservations.
|
|
144
|
+
const releaseBodyBytes = () => {
|
|
145
|
+
if (bodyBytesReleased)
|
|
146
|
+
return;
|
|
147
|
+
bodyBytesReleased = true;
|
|
148
|
+
inFlightBodyBytes -= heldBodyBytes;
|
|
149
|
+
heldBodyBytes = 0;
|
|
150
|
+
};
|
|
104
151
|
const abortRequest = () => {
|
|
152
|
+
releaseBodyBytes();
|
|
105
153
|
if (!controller.signal.aborted) {
|
|
106
154
|
controller.abort(new Error("HTTP client disconnected."));
|
|
107
155
|
}
|
|
@@ -117,8 +165,28 @@ export function listen(connecta, portOrOptions) {
|
|
|
117
165
|
req.once("aborted", abortRequest);
|
|
118
166
|
req.once("close", abortIncompleteRequest);
|
|
119
167
|
res.once("close", abortIncompleteResponse);
|
|
168
|
+
res.once("finish", releaseBodyBytes);
|
|
169
|
+
res.once("close", releaseBodyBytes);
|
|
170
|
+
// A queued pipelined response gets neither finish nor close when the
|
|
171
|
+
// socket goes away, and its completed upload's request is already closed.
|
|
172
|
+
socket.once("close", abortRequest);
|
|
173
|
+
const holdBodyBytes = (bytes) => {
|
|
174
|
+
if (bodyBytesReleased || inFlightBodyBytes + bytes > maxInFlightBodyBytes)
|
|
175
|
+
return false;
|
|
176
|
+
inFlightBodyBytes += bytes;
|
|
177
|
+
heldBodyBytes += bytes;
|
|
178
|
+
return true;
|
|
179
|
+
};
|
|
120
180
|
try {
|
|
121
|
-
const request = await toRequest(req, maxBodyBytes, controller.signal);
|
|
181
|
+
const request = await toRequest(req, maxBodyBytes, holdBodyBytes, controller.signal);
|
|
182
|
+
if (request === BODIES_AT_CAPACITY) {
|
|
183
|
+
res.statusCode = 503;
|
|
184
|
+
res.setHeader("Retry-After", "1");
|
|
185
|
+
// Abandoned mid-body, like the 413 below.
|
|
186
|
+
res.setHeader("Connection", "close");
|
|
187
|
+
res.end("Service Unavailable");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
122
190
|
if (request === BODY_TOO_LARGE) {
|
|
123
191
|
res.statusCode = 413;
|
|
124
192
|
// The request stream was abandoned mid-body, so this connection
|
|
@@ -132,6 +200,13 @@ export function listen(connecta, portOrOptions) {
|
|
|
132
200
|
await writeResponse(res, response);
|
|
133
201
|
}
|
|
134
202
|
catch (error) {
|
|
203
|
+
if (error instanceof InvalidRequestUrl) {
|
|
204
|
+
res.statusCode = 400;
|
|
205
|
+
// Refused before reading the body, so the connection cannot be reused.
|
|
206
|
+
res.setHeader("Connection", "close");
|
|
207
|
+
res.end("Bad Request");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
135
210
|
// A client that deliberately went away can make either the request
|
|
136
211
|
// handler or response pipeline reject. The request's abort signal is
|
|
137
212
|
// the authoritative classification: there is no caller left to answer,
|
|
@@ -152,6 +227,10 @@ export function listen(connecta, portOrOptions) {
|
|
|
152
227
|
res.end("Internal Server Error");
|
|
153
228
|
}
|
|
154
229
|
finally {
|
|
230
|
+
releaseBodyBytes();
|
|
231
|
+
res.removeListener("finish", releaseBodyBytes);
|
|
232
|
+
res.removeListener("close", releaseBodyBytes);
|
|
233
|
+
socket.removeListener("close", abortRequest);
|
|
155
234
|
req.removeListener("aborted", abortRequest);
|
|
156
235
|
req.removeListener("close", abortIncompleteRequest);
|
|
157
236
|
res.removeListener("close", abortIncompleteResponse);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// The root graph calls the optional recorder, never the /activity implementation.
|
|
2
|
+
import { Cause, Effect, Exit } from "effect";
|
|
3
|
+
import { InvocationFailure } from "./invocation-failure.js";
|
|
4
|
+
import { ExecutorAdmissionError } from "./executor-admission.js";
|
|
5
|
+
import { DiscoveryPolicyError } from "./catalog-service.js";
|
|
6
|
+
import { classificationCode, classifyCallError } from "./errors.js";
|
|
7
|
+
import { usesCallerIdentity } from "./connector-caller.js";
|
|
8
|
+
export function operationActivity(context, registry, name, work, counts) {
|
|
9
|
+
if (!context?.recordOperation)
|
|
10
|
+
return work;
|
|
11
|
+
return Effect.suspend(() => {
|
|
12
|
+
const started = performance.now();
|
|
13
|
+
const connectors = registry.listConnectors();
|
|
14
|
+
// Human operations stay owner-only, including discovery of private catalogs.
|
|
15
|
+
let personal = Boolean(context.principalActor);
|
|
16
|
+
try {
|
|
17
|
+
personal ||= connectors.some((connector) => connector.authScope === "personal" ||
|
|
18
|
+
!connector.staticTools ||
|
|
19
|
+
usesCallerIdentity(connector) ||
|
|
20
|
+
connector.describe?.().auth?.mode === "request");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
personal = true;
|
|
24
|
+
}
|
|
25
|
+
return work.pipe(Effect.onExit((exit) => Effect.sync(() => {
|
|
26
|
+
const result = Exit.isSuccess(exit)
|
|
27
|
+
? exit.value
|
|
28
|
+
: undefined;
|
|
29
|
+
const failed = Exit.isFailure(exit) || result?.isError === true;
|
|
30
|
+
const interrupted = Exit.isFailure(exit) && exit.cause.reasons.some(Cause.isInterruptReason);
|
|
31
|
+
const failure = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined;
|
|
32
|
+
const errorCode = interrupted
|
|
33
|
+
? "cancelled"
|
|
34
|
+
: classificationCode(Exit.isFailure(exit)
|
|
35
|
+
? failure instanceof InvocationFailure ||
|
|
36
|
+
failure instanceof ExecutorAdmissionError ||
|
|
37
|
+
failure instanceof DiscoveryPolicyError
|
|
38
|
+
? failure.code
|
|
39
|
+
: classifyCallError(failure).code
|
|
40
|
+
: result?.structuredContent?.error?.code);
|
|
41
|
+
try {
|
|
42
|
+
// Result pages already measure their payload. Other operations have no
|
|
43
|
+
// reusable measurement: never serialize discovery or media for telemetry.
|
|
44
|
+
const page = name === "result" && Exit.isSuccess(exit)
|
|
45
|
+
? exit.value
|
|
46
|
+
: undefined;
|
|
47
|
+
const bytes = page?.structuredContent?.bytes ?? page?.bytes;
|
|
48
|
+
const resultBytes = typeof bytes === "number" && Number.isSafeInteger(bytes) && bytes >= 0 ? bytes : undefined;
|
|
49
|
+
context.recordOperation(context, {
|
|
50
|
+
operation: {
|
|
51
|
+
name,
|
|
52
|
+
connectors: connectors.map((connector) => connector.id),
|
|
53
|
+
personal,
|
|
54
|
+
...(counts ? { hostCalls: counts() } : {}),
|
|
55
|
+
},
|
|
56
|
+
durationMs: performance.now() - started,
|
|
57
|
+
resultBytes,
|
|
58
|
+
outcome: !failed
|
|
59
|
+
? "success"
|
|
60
|
+
: errorCode === "timeout"
|
|
61
|
+
? "timeout"
|
|
62
|
+
: errorCode === "cancelled" || errorCode === "executor_cancelled"
|
|
63
|
+
? "cancelled"
|
|
64
|
+
: "error",
|
|
65
|
+
errorCode,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* Optional recording cannot replace the operation's result. */
|
|
70
|
+
}
|
|
71
|
+
})));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const OPERATION_NAMES = new Set(["search", "describe", "result", "skill", "program"]);
|
|
75
|
+
export function checkedActivityOperation(value) {
|
|
76
|
+
if (!value || typeof value !== "object")
|
|
77
|
+
return undefined;
|
|
78
|
+
const input = value;
|
|
79
|
+
if (!OPERATION_NAMES.has(input.name) ||
|
|
80
|
+
typeof input.personal !== "boolean" ||
|
|
81
|
+
!Array.isArray(input.connectors) ||
|
|
82
|
+
input.connectors.some((id) => typeof id !== "string" || !/^[a-zA-Z0-9_-]+$/.test(id)))
|
|
83
|
+
return undefined;
|
|
84
|
+
const counts = input.hostCalls;
|
|
85
|
+
if (counts &&
|
|
86
|
+
[counts.attempted, counts.admitted, counts.succeeded, counts.failed].some((n) => typeof n !== "number" || !Number.isSafeInteger(n) || n < 0))
|
|
87
|
+
return undefined;
|
|
88
|
+
return {
|
|
89
|
+
name: input.name,
|
|
90
|
+
connectors: [...new Set(input.connectors)],
|
|
91
|
+
personal: input.personal,
|
|
92
|
+
...(counts
|
|
93
|
+
? {
|
|
94
|
+
hostCalls: {
|
|
95
|
+
attempted: counts.attempted,
|
|
96
|
+
admitted: counts.admitted,
|
|
97
|
+
succeeded: counts.succeeded,
|
|
98
|
+
failed: counts.failed,
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -23,8 +23,15 @@ export declare function labelErrorClass(ctor: {
|
|
|
23
23
|
* name is exactly one of those, else `DOMException`.
|
|
24
24
|
*/
|
|
25
25
|
export declare function errorLabel(failure: unknown): string | undefined;
|
|
26
|
+
interface IdentityLookupFact {
|
|
27
|
+
outcome: "timeout" | "rejected";
|
|
28
|
+
durationMs: number;
|
|
29
|
+
cold: boolean;
|
|
30
|
+
inFlight: number;
|
|
31
|
+
}
|
|
26
32
|
/** Facts connecta derived about a failure where it saw it. */
|
|
27
33
|
export interface FailureFacts {
|
|
34
|
+
identityLookup?: IdentityLookupFact;
|
|
28
35
|
/** Inbound refusal classification attached to its Response, never its body. */
|
|
29
36
|
reason?: AuthDenialReason;
|
|
30
37
|
step?: FailureStep;
|
|
@@ -64,6 +71,7 @@ export interface FailureSubject {
|
|
|
64
71
|
declare const recordBrand: unique symbol;
|
|
65
72
|
/** One failure as an operator may read it. Only `failureRecord` makes one. */
|
|
66
73
|
export interface FailureRecord {
|
|
74
|
+
readonly identityLookup?: IdentityLookupFact;
|
|
67
75
|
readonly [recordBrand]: true;
|
|
68
76
|
readonly reason?: AuthDenialReason;
|
|
69
77
|
readonly accessContext?: boolean;
|
package/dist/operator-record.js
CHANGED
|
@@ -165,6 +165,20 @@ export function errorLabel(failure) {
|
|
|
165
165
|
}
|
|
166
166
|
return undefined;
|
|
167
167
|
}
|
|
168
|
+
function identityLookupFact(value) {
|
|
169
|
+
if (!value ||
|
|
170
|
+
(value.outcome !== "timeout" && value.outcome !== "rejected") ||
|
|
171
|
+
count(value.durationMs, 86_400_000) === undefined ||
|
|
172
|
+
typeof value.cold !== "boolean" ||
|
|
173
|
+
count(value.inFlight, 1_000) === undefined)
|
|
174
|
+
return undefined;
|
|
175
|
+
return {
|
|
176
|
+
outcome: value.outcome,
|
|
177
|
+
durationMs: Math.round(value.durationMs),
|
|
178
|
+
cold: value.cold,
|
|
179
|
+
inFlight: Math.round(value.inFlight),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
168
182
|
function origin(value) {
|
|
169
183
|
if (typeof value !== "string")
|
|
170
184
|
return undefined;
|
|
@@ -190,6 +204,7 @@ function checkedFacts(facts) {
|
|
|
190
204
|
return {
|
|
191
205
|
...defined("reason", member(facts.reason, AUTH_DENIAL_REASONS)),
|
|
192
206
|
...defined("step", member(facts.step, STEPS)),
|
|
207
|
+
...defined("identityLookup", identityLookupFact(facts.identityLookup)),
|
|
193
208
|
...defined("origin", origin(facts.origin)),
|
|
194
209
|
...defined("httpStatus", httpStatus(facts.httpStatus)),
|
|
195
210
|
...defined("oauthError", member(facts.oauthError, OAUTH_ERROR_CODES)),
|