@capybara-hq/kernel 0.1.0-rc.13 → 0.1.0-rc.14
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/catalog/admin-catalog.controller.d.ts +13 -2
- package/dist/catalog/admin-catalog.controller.js +24 -3
- package/dist/catalog/admin-catalog.schemas.d.ts +10 -0
- package/dist/catalog/admin-catalog.schemas.js +7 -0
- package/dist/catalog/catalog-content.d.ts +9 -1
- package/dist/catalog/catalog-content.js +2 -1
- package/dist/catalog/catalog-files.d.ts +3 -2
- package/dist/catalog/catalog-files.js +8 -5
- package/dist/catalog/component-scope.service.d.ts +14 -1
- package/dist/catalog/component-scope.service.js +12 -2
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-catalog.controller.d.ts +5 -4
- package/dist/catalog/org-catalog.controller.js +5 -4
- package/dist/catalog/org-catalog.schemas.d.ts +18 -2
- package/dist/catalog/org-catalog.schemas.js +16 -2
- package/dist/catalog/org-catalog.service.js +8 -2
- package/dist/catalog/plugin-import.js +1 -1
- package/dist/catalog/plugin-package.js +37 -9
- package/dist/catalog/plugin-upgrade.d.ts +15 -1
- package/dist/catalog/plugin-upgrade.js +18 -5
- package/dist/channels/connections.controller.d.ts +1 -1
- package/dist/channels/connections.controller.js +1 -1
- package/dist/channels/connections.service.d.ts +22 -5
- package/dist/channels/connections.service.js +56 -10
- package/dist/channels/webhook.d.ts +21 -1
- package/dist/channels/webhook.js +39 -4
- package/dist/ctx/create-ctx.d.ts +17 -3
- package/dist/ctx/create-ctx.js +49 -2
- package/dist/db/kernel-schema.d.ts +11 -2
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +4 -0
- package/dist/extensions/guide-places.d.ts +66 -0
- package/dist/extensions/guide-places.js +152 -0
- package/dist/extensions/host.js +3 -3
- package/dist/extensions/instance.d.ts +5 -1
- package/dist/extensions/instance.js +8 -0
- package/dist/extensions/ledger.d.ts +1 -1
- package/dist/extensions/permission-set.d.ts +16 -2
- package/dist/extensions/permission-set.js +17 -3
- package/dist/extensions/plugin-guides.d.ts +207 -54
- package/dist/extensions/plugin-guides.js +436 -165
- package/dist/http/api.module.js +6 -0
- package/dist/infra/lock/lock.service.d.ts +8 -0
- package/dist/infra/lock/lock.service.js +27 -0
- package/dist/infra/throttler/named-throttlers.d.ts +4 -1
- package/dist/infra/throttler/named-throttlers.js +16 -1
- package/dist/infra/throttler/throttler.guard.js +1 -1
- package/dist/overrides.d.ts +3 -1
- package/dist/overrides.js +2 -0
- package/dist/platform/exclusions.js +10 -5
- package/dist/platform/operation.js +4 -0
- package/dist/platform/ops/connectors.ops.js +2 -0
- package/dist/platform/ops/setup.ops.d.ts +24 -0
- package/dist/platform/ops/setup.ops.js +212 -0
- package/dist/platform/platform-ops.module.d.ts +2 -1
- package/dist/platform/platform-ops.module.js +5 -0
- package/dist/platform/route-schemas.js +5 -0
- package/dist/setup/kernel-checks.d.ts +91 -0
- package/dist/setup/kernel-checks.js +295 -0
- package/dist/setup/oauth-token-probe.d.ts +47 -0
- package/dist/setup/oauth-token-probe.js +108 -0
- package/dist/setup/redact-check.d.ts +18 -0
- package/dist/setup/redact-check.js +46 -0
- package/dist/setup/setup-check-registry.d.ts +86 -0
- package/dist/setup/setup-check-registry.js +75 -0
- package/dist/setup/setup-checks.controller.d.ts +28 -0
- package/dist/setup/setup-checks.controller.js +78 -0
- package/dist/setup/setup-checks.schemas.d.ts +38 -0
- package/dist/setup/setup-checks.schemas.js +43 -0
- package/dist/setup/setup-checks.service.d.ts +144 -0
- package/dist/setup/setup-checks.service.js +683 -0
- package/dist/setup/setup-guides.controller.d.ts +18 -0
- package/dist/setup/setup-guides.controller.js +48 -0
- package/dist/setup/setup-guides.schemas.d.ts +10 -0
- package/dist/setup/setup-guides.schemas.js +6 -0
- package/dist/setup/setup-guides.service.d.ts +126 -0
- package/dist/setup/setup-guides.service.js +315 -0
- package/dist/setup/setup.module.d.ts +8 -0
- package/dist/setup/setup.module.js +31 -0
- package/dist/tools/connectors.controller.d.ts +28 -11
- package/dist/tools/connectors.controller.js +49 -14
- package/dist/tools/connectors.schemas.d.ts +4 -0
- package/dist/tools/connectors.schemas.js +4 -0
- package/dist/tools/connectors.service.d.ts +30 -2
- package/dist/tools/connectors.service.js +98 -3
- package/dist/tools/credentials.d.ts +20 -0
- package/dist/tools/credentials.js +90 -1
- package/dist/tools/oauth/tokens.d.ts +20 -0
- package/dist/tools/oauth/tokens.js +49 -24
- package/package.json +3 -3
- package/src/migrations/0106_setup_guides_v2.sql +10 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type SetupCheckResult, type SetupCheckText } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { type OnModuleInit } from "@nestjs/common";
|
|
3
|
+
import type { Redis } from "ioredis";
|
|
4
|
+
import { webhookBase } from "../channels/webhook-url.js";
|
|
5
|
+
import type { KernelConfig } from "../config/schema.js";
|
|
6
|
+
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
7
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
8
|
+
import { Credentials } from "../tools/credentials.js";
|
|
9
|
+
import { SetupCheckRegistry } from "./setup-check-registry.js";
|
|
10
|
+
/** The fixed texts of the kernel checks (only `last_error` and a time are ever interpolated). */
|
|
11
|
+
export declare const KERNEL_CHECK_TEXTS: {
|
|
12
|
+
readonly publicUrlSet: SetupCheckText;
|
|
13
|
+
readonly publicUrlMissing: SetupCheckText;
|
|
14
|
+
readonly publicUrlFix: SetupCheckText;
|
|
15
|
+
readonly webhookPublic: SetupCheckText;
|
|
16
|
+
readonly webhookBaseMissing: SetupCheckText;
|
|
17
|
+
readonly webhookBaseFix: SetupCheckText;
|
|
18
|
+
readonly webhookNone: SetupCheckText;
|
|
19
|
+
readonly connectionGone: SetupCheckText;
|
|
20
|
+
readonly connectionDisabled: SetupCheckText;
|
|
21
|
+
readonly connectionEnableFix: SetupCheckText;
|
|
22
|
+
readonly connectionFailed: SetupCheckText;
|
|
23
|
+
readonly connectionFix: SetupCheckText;
|
|
24
|
+
readonly connectionPending: SetupCheckText;
|
|
25
|
+
readonly connectionActive: SetupCheckText;
|
|
26
|
+
readonly connectorGone: SetupCheckText;
|
|
27
|
+
readonly notOAuthCode: SetupCheckText;
|
|
28
|
+
readonly dcr: SetupCheckText;
|
|
29
|
+
readonly clientStored: SetupCheckText;
|
|
30
|
+
readonly clientMissing: SetupCheckText;
|
|
31
|
+
readonly clientFix: SetupCheckText;
|
|
32
|
+
readonly noClient: SetupCheckText;
|
|
33
|
+
readonly toolsRead: SetupCheckText;
|
|
34
|
+
readonly toolsPending: SetupCheckText;
|
|
35
|
+
readonly connected: SetupCheckText;
|
|
36
|
+
readonly notConnected: SetupCheckText;
|
|
37
|
+
readonly connectFix: SetupCheckText;
|
|
38
|
+
readonly publicUrlFirst: SetupCheckText;
|
|
39
|
+
};
|
|
40
|
+
/** `capybara:public-url`: `http.publicUrl` is set. */
|
|
41
|
+
export declare function publicUrlCheck(config: Pick<KernelConfig, "http">): SetupCheckResult;
|
|
42
|
+
/**
|
|
43
|
+
* `capybara:webhook-public`: webhook callback URLs have a usable https base. The kernel builds
|
|
44
|
+
* them from `channels.webhookBaseUrl` only (`webhook-url.ts`), never from `http.publicUrl`, so
|
|
45
|
+
* that is what is checked; a loopback http base (tests only) is not public.
|
|
46
|
+
*/
|
|
47
|
+
export declare function webhookPublicCheck(config: Parameters<typeof webhookBase>[0]): SetupCheckResult;
|
|
48
|
+
/** `capybara:webhook-received`: the marker the gateway sets on an accepted webhook, or none. */
|
|
49
|
+
export declare function webhookReceivedResult(seenAt: string | null): SetupCheckResult;
|
|
50
|
+
/**
|
|
51
|
+
* `capybara:connection-active`: active with a bot identity and no recorded error is ok. A recorded
|
|
52
|
+
* `last_error` (status `error`, or a failure an active connection keeps retrying) is failing with
|
|
53
|
+
* that text, redacted by every secret the result is redacted by and kept only when the result
|
|
54
|
+
* with it passes the result validation (control, bidi and lone surrogate rules, length);
|
|
55
|
+
* otherwise the reason leaves it out. The kernel pipeline validates and redacts the result again.
|
|
56
|
+
*/
|
|
57
|
+
export declare function connectionActiveResult(row: {
|
|
58
|
+
status: string;
|
|
59
|
+
botUserId: string | null;
|
|
60
|
+
lastError: string | null;
|
|
61
|
+
} | undefined, secrets: readonly string[]): SetupCheckResult;
|
|
62
|
+
/**
|
|
63
|
+
* Registers the kernel checks on module init (static code, the same on every replica). Each reads
|
|
64
|
+
* only the target the service resolved (ids of the caller's org, after authorization) and never
|
|
65
|
+
* writes; `capybara:oauth-token-endpoint` is the only one that leaves the kernel (§10.6).
|
|
66
|
+
*/
|
|
67
|
+
export declare class KernelChecks implements OnModuleInit {
|
|
68
|
+
private readonly registry;
|
|
69
|
+
private readonly config;
|
|
70
|
+
private readonly db;
|
|
71
|
+
private readonly redis;
|
|
72
|
+
private readonly credentials;
|
|
73
|
+
constructor(registry: SetupCheckRegistry, config: Pick<KernelConfigService, "get">, db: KernelDb, redis: Pick<Redis, "get">, credentials: Pick<Credentials, "oauthClient" | "status">);
|
|
74
|
+
onModuleInit(): void;
|
|
75
|
+
private webhookReceived;
|
|
76
|
+
private connectionActive;
|
|
77
|
+
/** The connector's auth settings (a live connector of the caller's org), or undefined. */
|
|
78
|
+
private connectorAuth;
|
|
79
|
+
private oauthClient;
|
|
80
|
+
private connectorTools;
|
|
81
|
+
/** The caller's own credential (cached per account, as every kernel check). */
|
|
82
|
+
private connectorConnected;
|
|
83
|
+
/**
|
|
84
|
+
* The token endpoint probe for a manual OAuth client (`oauth-token-probe.ts`): the stored
|
|
85
|
+
* client record is the connect flow's (its `tokenEndpoint` came from discovery or the
|
|
86
|
+
* template's `tokenUrl` at install, its auth method from `tokenAuthMethod`), the redirect URI
|
|
87
|
+
* is the connect flow's callback URL. Runs per admin (kernel checks are cached per account);
|
|
88
|
+
* each run is one token request, inside the per-account rate limit and the org's cap.
|
|
89
|
+
*/
|
|
90
|
+
private tokenEndpoint;
|
|
91
|
+
}
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
// The kernel's own setup checks, `capybara:*` (spec 2026-11-09-setup-guides-v2-design.md §10.6):
|
|
11
|
+
// answered from the kernel's state with no plugin code, usable by any guide of their place. They go
|
|
12
|
+
// through the same `SetupChecksService` pipeline as plugin checks (authorization, the rendered
|
|
13
|
+
// guide, cache, single flight, the org's concurrency cap, the time limit, validate / redact /
|
|
14
|
+
// validate), and are cached per account like every kernel check.
|
|
15
|
+
import { validateSetupCheckResult, } from "@capybara-hq/plugin-sdk";
|
|
16
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
17
|
+
import { webhookSeenKey } from "../channels/webhook.js";
|
|
18
|
+
import { webhookBase } from "../channels/webhook-url.js";
|
|
19
|
+
import { KernelConfigService } from "../infra/config/kernel-config.service.js";
|
|
20
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
21
|
+
import { REDIS } from "../infra/tokens.js";
|
|
22
|
+
import { Credentials } from "../tools/credentials.js";
|
|
23
|
+
import { callbackUrl } from "../tools/oauth/flow-paths.js";
|
|
24
|
+
import { probeNetPolicy, probeTokenEndpoint } from "./oauth-token-probe.js";
|
|
25
|
+
import { checkRedactor } from "./redact-check.js";
|
|
26
|
+
import { SetupCheckRegistry } from "./setup-check-registry.js";
|
|
27
|
+
const text = (en, zh) => ({ en, zh });
|
|
28
|
+
/** The fixed texts of the kernel checks (only `last_error` and a time are ever interpolated). */
|
|
29
|
+
export const KERNEL_CHECK_TEXTS = {
|
|
30
|
+
publicUrlSet: text("The public URL (http.publicUrl) is set.", "公开地址(http.publicUrl)已设置。"),
|
|
31
|
+
publicUrlMissing: text("The public URL (http.publicUrl) is not set.", "公开地址(http.publicUrl)没有设置。"),
|
|
32
|
+
publicUrlFix: text("Ask your super admin to set http.publicUrl.", "请超级管理员设置 http.publicUrl。"),
|
|
33
|
+
webhookPublic: text("Webhook callbacks have a public https address (channels.webhookBaseUrl).", "Webhook 回调有公开的 https 地址(channels.webhookBaseUrl)。"),
|
|
34
|
+
webhookBaseMissing: text("No public https address is set for webhook callbacks (channels.webhookBaseUrl).", "没有为 Webhook 回调设置公开的 https 地址(channels.webhookBaseUrl)。"),
|
|
35
|
+
webhookBaseFix: text("Ask your super admin to set channels.webhookBaseUrl to an https address that reaches the gateway.", "请超级管理员把 channels.webhookBaseUrl 设置为能到达网关的 https 地址。"),
|
|
36
|
+
webhookNone: text("No verified webhook request has arrived in the last 7 days.", "最近 7 天没有收到通过校验的 Webhook 请求。"),
|
|
37
|
+
connectionGone: text("The connection was not found.", "找不到这个连接。"),
|
|
38
|
+
connectionDisabled: text("The connection is disabled.", "这个连接已停用。"),
|
|
39
|
+
connectionEnableFix: text("Enable the connection.", "启用这个连接。"),
|
|
40
|
+
connectionFailed: text("The connection reported an error.", "这个连接报告了错误。"),
|
|
41
|
+
connectionFix: text("Check the credentials on this page and save them again.", "检查本页的凭据并重新保存。"),
|
|
42
|
+
connectionPending: text("The connection has not reported its bot identity yet; retry in a moment.", "这个连接还没有报告机器人身份,请稍后重试。"),
|
|
43
|
+
connectionActive: text("The connection is active.", "这个连接正常运行。"),
|
|
44
|
+
connectorGone: text("The connector was not found.", "找不到这个连接器。"),
|
|
45
|
+
notOAuthCode: text("This connector does not use an OAuth client.", "这个连接器不使用 OAuth 客户端。"),
|
|
46
|
+
dcr: text("Registered automatically; nothing to check.", "客户端是自动注册的,无需检查。"),
|
|
47
|
+
clientStored: text("The OAuth client id and secret are stored.", "OAuth 客户端 ID 和密钥已保存。"),
|
|
48
|
+
clientMissing: text("The OAuth client id or secret is not stored.", "OAuth 客户端 ID 或密钥没有保存。"),
|
|
49
|
+
clientFix: text("Enter the Client ID and Client Secret on the connector's page.", "在连接器页面填写 Client ID 和 Client Secret。"),
|
|
50
|
+
noClient: text("No OAuth client is stored yet.", "还没有保存 OAuth 客户端。"),
|
|
51
|
+
toolsRead: text("The connector's tool list has been read.", "已读取这个连接器的工具清单。"),
|
|
52
|
+
toolsPending: text("The tool list is read when the first member connects.", "第一位成员连接后会读取工具清单。"),
|
|
53
|
+
connected: text("You are connected.", "你已连接。"),
|
|
54
|
+
notConnected: text("You have not connected your account yet.", "你还没有连接自己的账号。"),
|
|
55
|
+
connectFix: text("Select Connect and sign in.", "选择「连接」并登录。"),
|
|
56
|
+
publicUrlFirst: text("The callback URL needs http.publicUrl; ask your super admin to set it.", "回调地址需要 http.publicUrl,请超级管理员设置。"),
|
|
57
|
+
};
|
|
58
|
+
/** `capybara:public-url`: `http.publicUrl` is set. */
|
|
59
|
+
export function publicUrlCheck(config) {
|
|
60
|
+
return config.http.publicUrl
|
|
61
|
+
? { status: "ok", reason: KERNEL_CHECK_TEXTS.publicUrlSet }
|
|
62
|
+
: {
|
|
63
|
+
status: "failing",
|
|
64
|
+
reason: KERNEL_CHECK_TEXTS.publicUrlMissing,
|
|
65
|
+
fix: KERNEL_CHECK_TEXTS.publicUrlFix,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* `capybara:webhook-public`: webhook callback URLs have a usable https base. The kernel builds
|
|
70
|
+
* them from `channels.webhookBaseUrl` only (`webhook-url.ts`), never from `http.publicUrl`, so
|
|
71
|
+
* that is what is checked; a loopback http base (tests only) is not public.
|
|
72
|
+
*/
|
|
73
|
+
export function webhookPublicCheck(config) {
|
|
74
|
+
const base = webhookBase(config);
|
|
75
|
+
return base?.startsWith("https://")
|
|
76
|
+
? { status: "ok", reason: KERNEL_CHECK_TEXTS.webhookPublic }
|
|
77
|
+
: {
|
|
78
|
+
status: "failing",
|
|
79
|
+
reason: KERNEL_CHECK_TEXTS.webhookBaseMissing,
|
|
80
|
+
fix: KERNEL_CHECK_TEXTS.webhookBaseFix,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** `capybara:webhook-received`: the marker the gateway sets on an accepted webhook, or none. */
|
|
84
|
+
export function webhookReceivedResult(seenAt) {
|
|
85
|
+
const at = seenAt === null ? Number.NaN : Date.parse(seenAt);
|
|
86
|
+
if (Number.isNaN(at))
|
|
87
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.webhookNone };
|
|
88
|
+
const iso = new Date(at).toISOString();
|
|
89
|
+
return {
|
|
90
|
+
status: "ok",
|
|
91
|
+
reason: text(`A verified webhook request arrived at ${iso}.`, `${iso} 收到过通过校验的 Webhook 请求。`),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** The most of `last_error` a reason carries (after redaction). */
|
|
95
|
+
const LAST_ERROR_CHARS = 200;
|
|
96
|
+
/**
|
|
97
|
+
* `last_error` fit for a reason: redacted over the whole text first by every secret the result is
|
|
98
|
+
* redacted by (the connection's and the plugin's config secrets: a cut never leaves part of a
|
|
99
|
+
* secret behind), then cut to {@link LAST_ERROR_CHARS} code points.
|
|
100
|
+
*/
|
|
101
|
+
function lastErrorText(lastError, secrets) {
|
|
102
|
+
const redacted = checkRedactor(secrets)(lastError);
|
|
103
|
+
const points = [...redacted];
|
|
104
|
+
return points.length > LAST_ERROR_CHARS
|
|
105
|
+
? `${points.slice(0, LAST_ERROR_CHARS).join("")}…`
|
|
106
|
+
: redacted;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `capybara:connection-active`: active with a bot identity and no recorded error is ok. A recorded
|
|
110
|
+
* `last_error` (status `error`, or a failure an active connection keeps retrying) is failing with
|
|
111
|
+
* that text, redacted by every secret the result is redacted by and kept only when the result
|
|
112
|
+
* with it passes the result validation (control, bidi and lone surrogate rules, length);
|
|
113
|
+
* otherwise the reason leaves it out. The kernel pipeline validates and redacts the result again.
|
|
114
|
+
*/
|
|
115
|
+
export function connectionActiveResult(row, secrets) {
|
|
116
|
+
if (!row)
|
|
117
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.connectionGone };
|
|
118
|
+
if (row.status === "disabled") {
|
|
119
|
+
return {
|
|
120
|
+
status: "failing",
|
|
121
|
+
reason: KERNEL_CHECK_TEXTS.connectionDisabled,
|
|
122
|
+
fix: KERNEL_CHECK_TEXTS.connectionEnableFix,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (row.status === "error" || (row.lastError !== null && row.lastError !== "")) {
|
|
126
|
+
const generic = {
|
|
127
|
+
status: "failing",
|
|
128
|
+
reason: KERNEL_CHECK_TEXTS.connectionFailed,
|
|
129
|
+
fix: KERNEL_CHECK_TEXTS.connectionFix,
|
|
130
|
+
};
|
|
131
|
+
if (row.lastError === null || row.lastError === "")
|
|
132
|
+
return generic;
|
|
133
|
+
const detail = lastErrorText(row.lastError, secrets);
|
|
134
|
+
const withError = validateSetupCheckResult({
|
|
135
|
+
...generic,
|
|
136
|
+
reason: text(`The connection reported an error: ${detail}`, `这个连接报告了错误:${detail}`),
|
|
137
|
+
});
|
|
138
|
+
return "problem" in withError ? generic : withError;
|
|
139
|
+
}
|
|
140
|
+
if (row.botUserId === null || row.botUserId === "") {
|
|
141
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.connectionPending };
|
|
142
|
+
}
|
|
143
|
+
return { status: "ok", reason: KERNEL_CHECK_TEXTS.connectionActive };
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Registers the kernel checks on module init (static code, the same on every replica). Each reads
|
|
147
|
+
* only the target the service resolved (ids of the caller's org, after authorization) and never
|
|
148
|
+
* writes; `capybara:oauth-token-endpoint` is the only one that leaves the kernel (§10.6).
|
|
149
|
+
*/
|
|
150
|
+
let KernelChecks = class KernelChecks {
|
|
151
|
+
registry;
|
|
152
|
+
config;
|
|
153
|
+
db;
|
|
154
|
+
redis;
|
|
155
|
+
credentials;
|
|
156
|
+
constructor(registry, config, db, redis, credentials) {
|
|
157
|
+
this.registry = registry;
|
|
158
|
+
this.config = config;
|
|
159
|
+
this.db = db;
|
|
160
|
+
this.redis = redis;
|
|
161
|
+
this.credentials = credentials;
|
|
162
|
+
}
|
|
163
|
+
onModuleInit() {
|
|
164
|
+
const r = this.registry;
|
|
165
|
+
r.registerKernel("capybara:public-url", "any", async () => publicUrlCheck(this.config.get()));
|
|
166
|
+
r.registerKernel("capybara:webhook-public", "channel-connection", async () => webhookPublicCheck(this.config.get()));
|
|
167
|
+
r.registerKernel("capybara:webhook-received", "channel-connection", (a) => this.webhookReceived(a));
|
|
168
|
+
r.registerKernel("capybara:connection-active", "channel-connection", (a) => this.connectionActive(a));
|
|
169
|
+
r.registerKernel("capybara:oauth-client", "connector-template", (a) => this.oauthClient(a));
|
|
170
|
+
r.registerKernel("capybara:connector-tools", "connector-template", (a) => this.connectorTools(a));
|
|
171
|
+
r.registerKernel("capybara:connector-connected", "connector-template", (a) => this.connectorConnected(a));
|
|
172
|
+
r.registerKernel("capybara:oauth-token-endpoint", "connector-template", (a) => this.tokenEndpoint(a));
|
|
173
|
+
}
|
|
174
|
+
async webhookReceived(a) {
|
|
175
|
+
if (a.target.kind !== "channel-connection")
|
|
176
|
+
return notApplicable();
|
|
177
|
+
return webhookReceivedResult(await this.redis.get(webhookSeenKey(a.target.connectionId)));
|
|
178
|
+
}
|
|
179
|
+
async connectionActive(a) {
|
|
180
|
+
if (a.target.kind !== "channel-connection")
|
|
181
|
+
return notApplicable();
|
|
182
|
+
const row = await this.db
|
|
183
|
+
.selectFrom("channel_connections")
|
|
184
|
+
.select(["status", "bot_user_id", "last_error"])
|
|
185
|
+
.where("id", "=", a.target.connectionId)
|
|
186
|
+
.where("org_id", "=", a.orgId)
|
|
187
|
+
.where("plugin", "=", a.target.plugin)
|
|
188
|
+
.where("deleted_at", "is", null)
|
|
189
|
+
.executeTakeFirst();
|
|
190
|
+
return connectionActiveResult(row && { status: row.status, botUserId: row.bot_user_id, lastError: row.last_error }, a.secrets);
|
|
191
|
+
}
|
|
192
|
+
/** The connector's auth settings (a live connector of the caller's org), or undefined. */
|
|
193
|
+
async connectorAuth(a) {
|
|
194
|
+
if (a.target.kind !== "connector")
|
|
195
|
+
return undefined;
|
|
196
|
+
const row = await this.db
|
|
197
|
+
.selectFrom("connectors")
|
|
198
|
+
.select("auth")
|
|
199
|
+
.where("id", "=", a.target.connectorId)
|
|
200
|
+
.where("org_id", "=", a.orgId)
|
|
201
|
+
.where("deleted_at", "is", null)
|
|
202
|
+
.executeTakeFirst();
|
|
203
|
+
return row ? row.auth : undefined;
|
|
204
|
+
}
|
|
205
|
+
async oauthClient(a) {
|
|
206
|
+
const auth = await this.connectorAuth(a);
|
|
207
|
+
if (!auth || a.target.kind !== "connector")
|
|
208
|
+
return connectorGone();
|
|
209
|
+
if (auth.type !== "oauth_code")
|
|
210
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.notOAuthCode };
|
|
211
|
+
if (auth.client === "dcr")
|
|
212
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.dcr };
|
|
213
|
+
const client = await this.credentials.oauthClient(a.orgId, a.target.connectorId);
|
|
214
|
+
return client && client.clientId !== "" && client.clientSecret
|
|
215
|
+
? { status: "ok", reason: KERNEL_CHECK_TEXTS.clientStored }
|
|
216
|
+
: {
|
|
217
|
+
status: "failing",
|
|
218
|
+
reason: KERNEL_CHECK_TEXTS.clientMissing,
|
|
219
|
+
fix: KERNEL_CHECK_TEXTS.clientFix,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async connectorTools(a) {
|
|
223
|
+
if (a.target.kind !== "connector" || !(await this.connectorAuth(a)))
|
|
224
|
+
return connectorGone();
|
|
225
|
+
const tool = await this.db
|
|
226
|
+
.selectFrom("connector_tools")
|
|
227
|
+
.select("name")
|
|
228
|
+
.where("connector_id", "=", a.target.connectorId)
|
|
229
|
+
.limit(1)
|
|
230
|
+
.executeTakeFirst();
|
|
231
|
+
return tool
|
|
232
|
+
? { status: "ok", reason: KERNEL_CHECK_TEXTS.toolsRead }
|
|
233
|
+
: { status: "unknown", reason: KERNEL_CHECK_TEXTS.toolsPending };
|
|
234
|
+
}
|
|
235
|
+
/** The caller's own credential (cached per account, as every kernel check). */
|
|
236
|
+
async connectorConnected(a) {
|
|
237
|
+
const auth = await this.connectorAuth(a);
|
|
238
|
+
if (!auth || a.target.kind !== "connector")
|
|
239
|
+
return connectorGone();
|
|
240
|
+
const s = await this.credentials.status(a.orgId, a.target.connectorId, auth, a.accountId);
|
|
241
|
+
return s === "connected"
|
|
242
|
+
? { status: "ok", reason: KERNEL_CHECK_TEXTS.connected }
|
|
243
|
+
: {
|
|
244
|
+
status: "failing",
|
|
245
|
+
reason: KERNEL_CHECK_TEXTS.notConnected,
|
|
246
|
+
fix: KERNEL_CHECK_TEXTS.connectFix,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* The token endpoint probe for a manual OAuth client (`oauth-token-probe.ts`): the stored
|
|
251
|
+
* client record is the connect flow's (its `tokenEndpoint` came from discovery or the
|
|
252
|
+
* template's `tokenUrl` at install, its auth method from `tokenAuthMethod`), the redirect URI
|
|
253
|
+
* is the connect flow's callback URL. Runs per admin (kernel checks are cached per account);
|
|
254
|
+
* each run is one token request, inside the per-account rate limit and the org's cap.
|
|
255
|
+
*/
|
|
256
|
+
async tokenEndpoint(a) {
|
|
257
|
+
const auth = await this.connectorAuth(a);
|
|
258
|
+
if (!auth || a.target.kind !== "connector")
|
|
259
|
+
return connectorGone();
|
|
260
|
+
if (auth.type !== "oauth_code")
|
|
261
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.notOAuthCode };
|
|
262
|
+
if (auth.client === "dcr")
|
|
263
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.dcr };
|
|
264
|
+
const config = this.config.get();
|
|
265
|
+
const publicUrl = config.http.publicUrl;
|
|
266
|
+
if (!publicUrl)
|
|
267
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.publicUrlFirst };
|
|
268
|
+
const client = await this.credentials.oauthClient(a.orgId, a.target.connectorId);
|
|
269
|
+
if (!client)
|
|
270
|
+
return { status: "unknown", reason: KERNEL_CHECK_TEXTS.noClient };
|
|
271
|
+
return probeTokenEndpoint({
|
|
272
|
+
client,
|
|
273
|
+
redirectUri: callbackUrl(publicUrl),
|
|
274
|
+
net: probeNetPolicy(config),
|
|
275
|
+
signal: a.signal,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
KernelChecks = __decorate([
|
|
280
|
+
Injectable(),
|
|
281
|
+
__param(0, Inject(SetupCheckRegistry)),
|
|
282
|
+
__param(1, Inject(KernelConfigService)),
|
|
283
|
+
__param(2, Inject(KernelDb)),
|
|
284
|
+
__param(3, Inject(REDIS)),
|
|
285
|
+
__param(4, Inject(Credentials))
|
|
286
|
+
], KernelChecks);
|
|
287
|
+
export { KernelChecks };
|
|
288
|
+
const notApplicable = () => ({
|
|
289
|
+
status: "unknown",
|
|
290
|
+
reason: text("This check does not apply on this page.", "这项检查不适用于本页面。"),
|
|
291
|
+
});
|
|
292
|
+
const connectorGone = () => ({
|
|
293
|
+
status: "unknown",
|
|
294
|
+
reason: KERNEL_CHECK_TEXTS.connectorGone,
|
|
295
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SetupCheckResult, SetupCheckText } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import type { KernelConfig } from "../config/schema.js";
|
|
3
|
+
import type { NetPolicy } from "../net/public-address.js";
|
|
4
|
+
import { safeFetch } from "../net/safe-fetch.js";
|
|
5
|
+
import { type OAuthClientRecord } from "../tools/oauth/tokens.js";
|
|
6
|
+
/** The authorization code the probe sends: never a code any server issued. */
|
|
7
|
+
export declare const PROBE_CODE = "capybara-setup-probe";
|
|
8
|
+
/** The probe's fixed texts: nothing a token endpoint sends ever reaches a result. */
|
|
9
|
+
export declare const PROBE_TEXTS: {
|
|
10
|
+
readonly accepted: SetupCheckText;
|
|
11
|
+
readonly clientRefused: SetupCheckText;
|
|
12
|
+
readonly clientFix: SetupCheckText;
|
|
13
|
+
readonly redirectRefused: SetupCheckText;
|
|
14
|
+
readonly unknown: SetupCheckText;
|
|
15
|
+
readonly netDenied: SetupCheckText;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The fix of a redirect refusal: the kernel-built callback URL (never anything the endpoint
|
|
19
|
+
* said): `http.publicUrl` + the fixed callback path. (A public URL so long that the fix passes
|
|
20
|
+
* its limit makes the result invalid, which the kernel answers as unknown.)
|
|
21
|
+
*/
|
|
22
|
+
export declare const redirectFix: (callbackUrl: string) => SetupCheckText;
|
|
23
|
+
/**
|
|
24
|
+
* The network rules of the probe: the deployment's egress proxy (`net.httpsProxy` /
|
|
25
|
+
* `net.noProxy`), its private host exemptions (`marketplaces.allowPrivateHosts`, the one list of
|
|
26
|
+
* internal hosts the kernel may reach) and loopback http only under
|
|
27
|
+
* `tools.oauthAllowInsecureLoopback` (tests), as for every other OAuth URL.
|
|
28
|
+
*/
|
|
29
|
+
export declare function probeNetPolicy(config: KernelConfig): NetPolicy;
|
|
30
|
+
export interface TokenProbeInput {
|
|
31
|
+
/** The stored client: its `tokenEndpoint` is the one the connect flow posts codes to. */
|
|
32
|
+
client: OAuthClientRecord;
|
|
33
|
+
/** The connect flow's redirect URI (`http.publicUrl` + the callback path). */
|
|
34
|
+
redirectUri: string;
|
|
35
|
+
net: NetPolicy;
|
|
36
|
+
/** The run's signal (the kernel's time limit). */
|
|
37
|
+
signal: AbortSignal;
|
|
38
|
+
/** Tests only: the network calls (defaults to {@link safeFetch}). */
|
|
39
|
+
fetch?: typeof safeFetch;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Probes the token endpoint (spec §10.6 table): `invalid_grant` / Slack `invalid_code` → ok; a
|
|
43
|
+
* refused client (or any other 401) → failing with the copy-again fix; a refused redirect URI →
|
|
44
|
+
* failing with the callback URL; anything else (429, 5xx, a network error, a redirect, a 2xx) →
|
|
45
|
+
* unknown. Never throws, never logs; the answer is read only for its refusal code and dropped.
|
|
46
|
+
*/
|
|
47
|
+
export declare function probeTokenEndpoint(input: TokenProbeInput): Promise<SetupCheckResult>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { NetDeniedError } from "../net/public-address.js";
|
|
2
|
+
import { safeFetch } from "../net/safe-fetch.js";
|
|
3
|
+
import { parseTokenRefusal, tokenRequestInit, } from "../tools/oauth/tokens.js";
|
|
4
|
+
/** The authorization code the probe sends: never a code any server issued. */
|
|
5
|
+
export const PROBE_CODE = "capybara-setup-probe";
|
|
6
|
+
/** A fixed PKCE verifier (43 characters, RFC 7636), so a server that demands one reads the code. */
|
|
7
|
+
const PROBE_VERIFIER = "capybara-setup-probe-verifier-0000000000000";
|
|
8
|
+
/** The most of a token endpoint answer the probe reads (an error body is a few hundred bytes). */
|
|
9
|
+
const PROBE_MAX_BYTES = 64 * 1024;
|
|
10
|
+
/** Codes that say the client authenticated and only the code was bad, as intended. */
|
|
11
|
+
const ACCEPTED = new Set(["invalid_grant", "invalid_code"]);
|
|
12
|
+
/** Codes that say the client id or secret is wrong (RFC 6749 and Slack's). */
|
|
13
|
+
const CLIENT_REFUSED = new Set([
|
|
14
|
+
"invalid_client",
|
|
15
|
+
"unauthorized_client",
|
|
16
|
+
"invalid_client_id",
|
|
17
|
+
"bad_client_secret",
|
|
18
|
+
]);
|
|
19
|
+
/** Codes that say the redirect URI is not registered (RFC 6749 servers and Slack's). */
|
|
20
|
+
const REDIRECT_REFUSED = new Set(["redirect_uri_mismatch", "bad_redirect_uri"]);
|
|
21
|
+
const text = (en, zh) => ({ en, zh });
|
|
22
|
+
/** The probe's fixed texts: nothing a token endpoint sends ever reaches a result. */
|
|
23
|
+
export const PROBE_TEXTS = {
|
|
24
|
+
accepted: text("The token endpoint accepted the client.", "令牌端点接受了这个客户端。"),
|
|
25
|
+
clientRefused: text("The token endpoint refused the client id or secret.", "令牌端点拒绝了客户端 ID 或密钥。"),
|
|
26
|
+
clientFix: text("Copy the Client ID and Client Secret again from the app's Basic Information page.", "从应用的 Basic Information 页面重新复制 Client ID 和 Client Secret。"),
|
|
27
|
+
redirectRefused: text("The app does not list the callback URL.", "应用的重定向地址里没有这个回调地址。"),
|
|
28
|
+
unknown: text("Could not verify the client; retry later.", "无法验证这个客户端,请稍后重试。"),
|
|
29
|
+
netDenied: text("The token endpoint's address is refused by this deployment's network rules; ask your super admin.", "这个部署的网络规则拒绝访问令牌端点的地址,请联系超级管理员。"),
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The fix of a redirect refusal: the kernel-built callback URL (never anything the endpoint
|
|
33
|
+
* said): `http.publicUrl` + the fixed callback path. (A public URL so long that the fix passes
|
|
34
|
+
* its limit makes the result invalid, which the kernel answers as unknown.)
|
|
35
|
+
*/
|
|
36
|
+
export const redirectFix = (callbackUrl) => text(`Add ${callbackUrl} to the app's redirect URLs.`, `把 ${callbackUrl} 加到应用的重定向地址(Redirect URLs)里。`);
|
|
37
|
+
/**
|
|
38
|
+
* The network rules of the probe: the deployment's egress proxy (`net.httpsProxy` /
|
|
39
|
+
* `net.noProxy`), its private host exemptions (`marketplaces.allowPrivateHosts`, the one list of
|
|
40
|
+
* internal hosts the kernel may reach) and loopback http only under
|
|
41
|
+
* `tools.oauthAllowInsecureLoopback` (tests), as for every other OAuth URL.
|
|
42
|
+
*/
|
|
43
|
+
export function probeNetPolicy(config) {
|
|
44
|
+
return {
|
|
45
|
+
allowInsecureLoopback: config.tools.oauthAllowInsecureLoopback,
|
|
46
|
+
allowPrivateHosts: config.marketplaces.allowPrivateHosts,
|
|
47
|
+
trustedOrigins: [],
|
|
48
|
+
...(config.net.httpsProxy !== undefined && {
|
|
49
|
+
proxy: { url: config.net.httpsProxy, noProxy: config.net.noProxy },
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Probes the token endpoint (spec §10.6 table): `invalid_grant` / Slack `invalid_code` → ok; a
|
|
55
|
+
* refused client (or any other 401) → failing with the copy-again fix; a refused redirect URI →
|
|
56
|
+
* failing with the callback URL; anything else (429, 5xx, a network error, a redirect, a 2xx) →
|
|
57
|
+
* unknown. Never throws, never logs; the answer is read only for its refusal code and dropped.
|
|
58
|
+
*/
|
|
59
|
+
export async function probeTokenEndpoint(input) {
|
|
60
|
+
const { client } = input;
|
|
61
|
+
const { headers, body } = tokenRequestInit(client, {
|
|
62
|
+
grant_type: "authorization_code",
|
|
63
|
+
code: PROBE_CODE,
|
|
64
|
+
redirect_uri: input.redirectUri,
|
|
65
|
+
code_verifier: PROBE_VERIFIER,
|
|
66
|
+
resource: client.resource,
|
|
67
|
+
});
|
|
68
|
+
let status;
|
|
69
|
+
let parsed;
|
|
70
|
+
try {
|
|
71
|
+
const res = await (input.fetch ?? safeFetch)(client.tokenEndpoint, { method: "POST", headers, body: body.toString(), signal: input.signal }, input.net, { maxBytes: PROBE_MAX_BYTES });
|
|
72
|
+
status = res.status;
|
|
73
|
+
// only an answer that can carry a refusal is read (400 / 401, or a 200 error body of
|
|
74
|
+
// dialects like Slack's); other bodies are dropped unread
|
|
75
|
+
if (status === 200 || status === 400 || status === 401) {
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(await res.text());
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
parsed = undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
await res.body?.cancel().catch(() => { });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (e instanceof NetDeniedError && e.code !== "net.redirect" && e.code !== "net.dns_failed") {
|
|
89
|
+
return { status: "unknown", reason: PROBE_TEXTS.netDenied };
|
|
90
|
+
}
|
|
91
|
+
return { status: "unknown", reason: PROBE_TEXTS.unknown };
|
|
92
|
+
}
|
|
93
|
+
// only the refusal code is taken from the answer; a 2xx token is never looked at or kept
|
|
94
|
+
const code = parseTokenRefusal(status, parsed);
|
|
95
|
+
if (code !== null && ACCEPTED.has(code))
|
|
96
|
+
return { status: "ok", reason: PROBE_TEXTS.accepted };
|
|
97
|
+
if (code !== null && REDIRECT_REFUSED.has(code)) {
|
|
98
|
+
return {
|
|
99
|
+
status: "failing",
|
|
100
|
+
reason: PROBE_TEXTS.redirectRefused,
|
|
101
|
+
fix: redirectFix(input.redirectUri),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if ((code !== null && CLIENT_REFUSED.has(code)) || status === 401) {
|
|
105
|
+
return { status: "failing", reason: PROBE_TEXTS.clientRefused, fix: PROBE_TEXTS.clientFix };
|
|
106
|
+
}
|
|
107
|
+
return { status: "unknown", reason: PROBE_TEXTS.unknown };
|
|
108
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SetupCheckResult } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
/** The shortest secret value redacted from a check result (shorter values would hit plain words). */
|
|
3
|
+
export declare const CHECK_SECRET_MIN_CHARS = 8;
|
|
4
|
+
/** A redactor over the given secret values (only strings of at least 8 characters count). */
|
|
5
|
+
export declare function checkRedactor(secrets: readonly unknown[]): (s: string) => string;
|
|
6
|
+
/**
|
|
7
|
+
* A fresh plain object of a validated result with its texts redacted (validate it again: the
|
|
8
|
+
* redacted copy may be too long now). Only `status`, `reason` and `fix` are carried over.
|
|
9
|
+
*/
|
|
10
|
+
export declare function redactCheckResult(result: SetupCheckResult, redact: (s: string) => string): SetupCheckResult;
|
|
11
|
+
/**
|
|
12
|
+
* What a log line may say about an error a check threw: its name and its redacted, shortened
|
|
13
|
+
* message, never the error object (a stack or a `cause` can carry what the message does not).
|
|
14
|
+
*/
|
|
15
|
+
export declare function describeCheckError(err: unknown, redact: (s: string) => string): {
|
|
16
|
+
name: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createRedactor } from "../log.js";
|
|
2
|
+
/** The shortest secret value redacted from a check result (shorter values would hit plain words). */
|
|
3
|
+
export const CHECK_SECRET_MIN_CHARS = 8;
|
|
4
|
+
/** A redactor over the given secret values (only strings of at least 8 characters count). */
|
|
5
|
+
export function checkRedactor(secrets) {
|
|
6
|
+
return createRedactor(secrets.filter((v) => typeof v === "string"), CHECK_SECRET_MIN_CHARS);
|
|
7
|
+
}
|
|
8
|
+
const redactText = (t, redact) => ({
|
|
9
|
+
en: redact(t.en),
|
|
10
|
+
...(t.zh !== undefined && { zh: redact(t.zh) }),
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* A fresh plain object of a validated result with its texts redacted (validate it again: the
|
|
14
|
+
* redacted copy may be too long now). Only `status`, `reason` and `fix` are carried over.
|
|
15
|
+
*/
|
|
16
|
+
export function redactCheckResult(result, redact) {
|
|
17
|
+
return {
|
|
18
|
+
status: result.status,
|
|
19
|
+
...(result.reason !== undefined && { reason: redactText(result.reason, redact) }),
|
|
20
|
+
...(result.fix !== undefined && { fix: redactText(result.fix, redact) }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** The most of an extension's error message that reaches a log line (after redaction). */
|
|
24
|
+
const ERROR_LOG_CHARS = 500;
|
|
25
|
+
/**
|
|
26
|
+
* What a log line may say about an error a check threw: its name and its redacted, shortened
|
|
27
|
+
* message, never the error object (a stack or a `cause` can carry what the message does not).
|
|
28
|
+
*/
|
|
29
|
+
export function describeCheckError(err, redact) {
|
|
30
|
+
let name = "Error";
|
|
31
|
+
let message = "";
|
|
32
|
+
try {
|
|
33
|
+
if (err instanceof Error) {
|
|
34
|
+
name = typeof err.name === "string" ? err.name.slice(0, 64) : "Error";
|
|
35
|
+
message = typeof err.message === "string" ? err.message : "";
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
name = typeof err;
|
|
39
|
+
message = typeof err === "string" ? err : "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// a hostile error object (a throwing getter): nothing more to say about it
|
|
44
|
+
}
|
|
45
|
+
return { name: redact(name), message: redact(message).slice(0, ERROR_LOG_CHARS) };
|
|
46
|
+
}
|