@nextclaw/channel-plugin-feishu 0.2.29-beta.0 → 0.2.29-beta.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/dist/index.d.ts +23 -0
- package/dist/index.js +45 -0
- package/dist/src/accounts.js +141 -0
- package/dist/src/app-scope-checker.js +36 -0
- package/dist/src/async.js +34 -0
- package/dist/src/auth-errors.js +72 -0
- package/dist/src/bitable.js +495 -0
- package/dist/src/bot.d.ts +35 -0
- package/dist/src/bot.js +941 -0
- package/dist/src/calendar-calendar.js +54 -0
- package/dist/src/calendar-event-attendee.js +98 -0
- package/dist/src/calendar-event.js +193 -0
- package/dist/src/calendar-freebusy.js +40 -0
- package/dist/src/calendar-shared.js +23 -0
- package/dist/src/calendar.js +16 -0
- package/dist/src/card-action.js +49 -0
- package/dist/src/channel.d.ts +7 -0
- package/dist/src/channel.js +413 -0
- package/dist/src/chat-schema.js +25 -0
- package/dist/src/chat.js +87 -0
- package/dist/src/client.d.ts +16 -0
- package/dist/src/client.js +112 -0
- package/dist/src/config-schema.d.ts +357 -0
- package/dist/src/dedup.js +126 -0
- package/dist/src/device-flow.js +109 -0
- package/dist/src/directory.js +101 -0
- package/dist/src/doc-schema.js +148 -0
- package/dist/src/docx-batch-insert.js +104 -0
- package/dist/src/docx-color-text.js +80 -0
- package/dist/src/docx-table-ops.js +197 -0
- package/dist/src/docx.js +858 -0
- package/dist/src/domains.js +14 -0
- package/dist/src/drive-schema.js +41 -0
- package/dist/src/drive.js +126 -0
- package/dist/src/dynamic-agent.js +93 -0
- package/dist/src/external-keys.js +13 -0
- package/dist/src/feishu-fetch.js +12 -0
- package/dist/src/identity.js +92 -0
- package/dist/src/lark-ticket.js +11 -0
- package/dist/src/media.d.ts +75 -0
- package/dist/src/media.js +304 -0
- package/dist/src/mention.d.ts +52 -0
- package/dist/src/mention.js +82 -0
- package/dist/src/monitor.account.d.ts +1 -0
- package/dist/src/monitor.account.js +393 -0
- package/dist/src/monitor.d.ts +11 -0
- package/dist/src/monitor.js +58 -0
- package/dist/src/monitor.startup.js +24 -0
- package/dist/src/monitor.state.d.ts +1 -0
- package/dist/src/monitor.state.js +80 -0
- package/dist/src/monitor.transport.js +167 -0
- package/dist/src/nextclaw-sdk/account-id.js +15 -0
- package/dist/src/nextclaw-sdk/core-channel.js +150 -0
- package/dist/src/nextclaw-sdk/core-pairing.js +151 -0
- package/dist/src/nextclaw-sdk/dedupe.js +164 -0
- package/dist/src/nextclaw-sdk/feishu.d.ts +1 -0
- package/dist/src/nextclaw-sdk/feishu.js +14 -0
- package/dist/src/nextclaw-sdk/history.js +69 -0
- package/dist/src/nextclaw-sdk/network-body.js +180 -0
- package/dist/src/nextclaw-sdk/network-fetch.js +63 -0
- package/dist/src/nextclaw-sdk/network-webhook.js +126 -0
- package/dist/src/nextclaw-sdk/network.js +4 -0
- package/dist/src/nextclaw-sdk/runtime-store.js +21 -0
- package/dist/src/nextclaw-sdk/secrets-config.js +65 -0
- package/dist/src/nextclaw-sdk/secrets-core.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets-core.js +68 -0
- package/dist/src/nextclaw-sdk/secrets-prompt.js +193 -0
- package/dist/src/nextclaw-sdk/secrets.d.ts +1 -0
- package/dist/src/nextclaw-sdk/secrets.js +4 -0
- package/dist/src/nextclaw-sdk/types.d.ts +242 -0
- package/dist/src/oauth.js +171 -0
- package/dist/src/onboarding.js +381 -0
- package/dist/src/outbound.js +150 -0
- package/dist/src/perm-schema.js +49 -0
- package/dist/src/perm.js +90 -0
- package/dist/src/policy.js +61 -0
- package/dist/src/post.js +160 -0
- package/dist/src/probe.d.ts +11 -0
- package/dist/src/probe.js +85 -0
- package/dist/src/raw-request.js +24 -0
- package/dist/src/reactions.d.ts +67 -0
- package/dist/src/reactions.js +91 -0
- package/dist/src/reply-dispatcher.js +250 -0
- package/dist/src/runtime.js +5 -0
- package/dist/src/secret-input.js +3 -0
- package/dist/src/send-result.js +12 -0
- package/dist/src/send-target.js +22 -0
- package/dist/src/send.d.ts +51 -0
- package/dist/src/send.js +265 -0
- package/dist/src/sheets-shared.js +193 -0
- package/dist/src/sheets.js +95 -0
- package/dist/src/streaming-card.js +263 -0
- package/dist/src/targets.js +39 -0
- package/dist/src/task-comment.js +76 -0
- package/dist/src/task-shared.js +13 -0
- package/dist/src/task-subtask.js +79 -0
- package/dist/src/task-task.js +144 -0
- package/dist/src/task-tasklist.js +136 -0
- package/dist/src/task.js +16 -0
- package/dist/src/token-store.js +154 -0
- package/dist/src/tool-account.js +65 -0
- package/dist/src/tool-result.js +18 -0
- package/dist/src/tool-scopes.js +62 -0
- package/dist/src/tools-config.js +30 -0
- package/dist/src/types.d.ts +43 -0
- package/dist/src/typing.js +145 -0
- package/dist/src/uat-client.js +102 -0
- package/dist/src/user-tool-client.js +132 -0
- package/dist/src/user-tool-helpers.js +110 -0
- package/dist/src/user-tool-result.js +10 -0
- package/dist/src/wiki-schema.js +45 -0
- package/dist/src/wiki.js +144 -0
- package/package.json +8 -4
- package/index.ts +0 -75
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/nextclaw-sdk/history.ts
|
|
2
|
+
const HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
|
|
3
|
+
const CURRENT_MESSAGE_MARKER = "[Current message]";
|
|
4
|
+
const MAX_HISTORY_KEYS = 1e3;
|
|
5
|
+
function evictOldHistoryKeys(historyMap, maxKeys = MAX_HISTORY_KEYS) {
|
|
6
|
+
if (historyMap.size <= maxKeys) return;
|
|
7
|
+
const keysToDelete = historyMap.size - maxKeys;
|
|
8
|
+
const iterator = historyMap.keys();
|
|
9
|
+
for (let index = 0; index < keysToDelete; index += 1) {
|
|
10
|
+
const key = iterator.next().value;
|
|
11
|
+
if (key !== void 0) historyMap.delete(key);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function buildHistoryContext(params) {
|
|
15
|
+
const lineBreak = params.lineBreak ?? "\n";
|
|
16
|
+
if (!params.historyText.trim()) return params.currentMessage;
|
|
17
|
+
return [
|
|
18
|
+
HISTORY_CONTEXT_MARKER,
|
|
19
|
+
params.historyText,
|
|
20
|
+
"",
|
|
21
|
+
CURRENT_MESSAGE_MARKER,
|
|
22
|
+
params.currentMessage
|
|
23
|
+
].join(lineBreak);
|
|
24
|
+
}
|
|
25
|
+
function appendHistoryEntry(params) {
|
|
26
|
+
if (params.limit <= 0) return [];
|
|
27
|
+
const history = params.historyMap.get(params.historyKey) ?? [];
|
|
28
|
+
history.push(params.entry);
|
|
29
|
+
while (history.length > params.limit) history.shift();
|
|
30
|
+
if (params.historyMap.has(params.historyKey)) params.historyMap.delete(params.historyKey);
|
|
31
|
+
params.historyMap.set(params.historyKey, history);
|
|
32
|
+
evictOldHistoryKeys(params.historyMap);
|
|
33
|
+
return history;
|
|
34
|
+
}
|
|
35
|
+
function buildHistoryContextFromEntries(params) {
|
|
36
|
+
const lineBreak = params.lineBreak ?? "\n";
|
|
37
|
+
const entries = params.excludeLast === false ? params.entries : params.entries.slice(0, -1);
|
|
38
|
+
if (entries.length === 0) return params.currentMessage;
|
|
39
|
+
return buildHistoryContext({
|
|
40
|
+
historyText: entries.map(params.formatEntry).join(lineBreak),
|
|
41
|
+
currentMessage: params.currentMessage,
|
|
42
|
+
lineBreak
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
function recordPendingHistoryEntryIfEnabled(params) {
|
|
46
|
+
if (!params.entry || params.limit <= 0) return [];
|
|
47
|
+
return appendHistoryEntry({
|
|
48
|
+
historyMap: params.historyMap,
|
|
49
|
+
historyKey: params.historyKey,
|
|
50
|
+
entry: params.entry,
|
|
51
|
+
limit: params.limit
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function buildPendingHistoryContextFromMap(params) {
|
|
55
|
+
if (params.limit <= 0) return params.currentMessage;
|
|
56
|
+
return buildHistoryContextFromEntries({
|
|
57
|
+
entries: params.historyMap.get(params.historyKey) ?? [],
|
|
58
|
+
currentMessage: params.currentMessage,
|
|
59
|
+
formatEntry: params.formatEntry,
|
|
60
|
+
lineBreak: params.lineBreak,
|
|
61
|
+
excludeLast: false
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function clearHistoryEntriesIfEnabled(params) {
|
|
65
|
+
if (params.limit <= 0) return;
|
|
66
|
+
params.historyMap.set(params.historyKey, []);
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, recordPendingHistoryEntryIfEnabled };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
//#region src/nextclaw-sdk/network-body.ts
|
|
2
|
+
var RequestBodyLimitError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
statusCode;
|
|
5
|
+
constructor(code) {
|
|
6
|
+
super(code);
|
|
7
|
+
this.name = "RequestBodyLimitError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.statusCode = code === "PAYLOAD_TOO_LARGE" ? 413 : code === "REQUEST_BODY_TIMEOUT" ? 408 : 400;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function requestBodyErrorToText(code) {
|
|
13
|
+
switch (code) {
|
|
14
|
+
case "PAYLOAD_TOO_LARGE": return "Payload too large";
|
|
15
|
+
case "REQUEST_BODY_TIMEOUT": return "Request body timeout";
|
|
16
|
+
default: return "Connection closed";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function parseContentLengthHeader(req) {
|
|
20
|
+
const header = req.headers["content-length"];
|
|
21
|
+
const raw = Array.isArray(header) ? header[0] : header;
|
|
22
|
+
if (typeof raw !== "string") return null;
|
|
23
|
+
const parsed = Number.parseInt(raw, 10);
|
|
24
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
|
|
25
|
+
}
|
|
26
|
+
async function readRequestBodyWithLimit(req, options) {
|
|
27
|
+
const maxBytes = Math.max(1, Math.floor(options.maxBytes));
|
|
28
|
+
const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? Math.floor(options.timeoutMs) : 3e4;
|
|
29
|
+
const encoding = options.encoding ?? "utf-8";
|
|
30
|
+
const declaredLength = parseContentLengthHeader(req);
|
|
31
|
+
if (declaredLength !== null && declaredLength > maxBytes) throw new RequestBodyLimitError("PAYLOAD_TOO_LARGE");
|
|
32
|
+
return await new Promise((resolve, reject) => {
|
|
33
|
+
let done = false;
|
|
34
|
+
let ended = false;
|
|
35
|
+
let totalBytes = 0;
|
|
36
|
+
const chunks = [];
|
|
37
|
+
const finish = (cb) => {
|
|
38
|
+
if (done) return;
|
|
39
|
+
done = true;
|
|
40
|
+
req.removeListener("data", onData);
|
|
41
|
+
req.removeListener("end", onEnd);
|
|
42
|
+
req.removeListener("error", onError);
|
|
43
|
+
req.removeListener("close", onClose);
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
cb();
|
|
46
|
+
};
|
|
47
|
+
const fail = (error) => finish(() => reject(error));
|
|
48
|
+
const onData = (chunk) => {
|
|
49
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
50
|
+
totalBytes += buffer.length;
|
|
51
|
+
if (totalBytes > maxBytes) {
|
|
52
|
+
if (!req.destroyed) req.destroy();
|
|
53
|
+
fail(new RequestBodyLimitError("PAYLOAD_TOO_LARGE"));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
chunks.push(buffer);
|
|
57
|
+
};
|
|
58
|
+
const onEnd = () => {
|
|
59
|
+
ended = true;
|
|
60
|
+
finish(() => resolve(Buffer.concat(chunks).toString(encoding)));
|
|
61
|
+
};
|
|
62
|
+
const onError = (error) => fail(error);
|
|
63
|
+
const onClose = () => {
|
|
64
|
+
if (!done && !ended) fail(new RequestBodyLimitError("CONNECTION_CLOSED"));
|
|
65
|
+
};
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
if (!req.destroyed) req.destroy();
|
|
68
|
+
fail(new RequestBodyLimitError("REQUEST_BODY_TIMEOUT"));
|
|
69
|
+
}, timeoutMs);
|
|
70
|
+
req.on("data", onData);
|
|
71
|
+
req.on("end", onEnd);
|
|
72
|
+
req.on("error", onError);
|
|
73
|
+
req.on("close", onClose);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function readJsonBodyWithLimit(req, options) {
|
|
77
|
+
try {
|
|
78
|
+
const trimmed = (await readRequestBodyWithLimit(req, options)).trim();
|
|
79
|
+
if (!trimmed) {
|
|
80
|
+
if (options.emptyObjectOnEmpty === false) return {
|
|
81
|
+
ok: false,
|
|
82
|
+
code: "INVALID_JSON",
|
|
83
|
+
error: "empty payload"
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
value: {}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return {
|
|
92
|
+
ok: true,
|
|
93
|
+
value: JSON.parse(trimmed)
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
code: "INVALID_JSON",
|
|
99
|
+
error: error instanceof Error ? error.message : String(error)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error instanceof RequestBodyLimitError) return {
|
|
104
|
+
ok: false,
|
|
105
|
+
code: error.code,
|
|
106
|
+
error: requestBodyErrorToText(error.code)
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
code: "INVALID_JSON",
|
|
111
|
+
error: error instanceof Error ? error.message : String(error)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function installRequestBodyLimitGuard(req, res, options) {
|
|
116
|
+
const maxBytes = Math.max(1, Math.floor(options.maxBytes));
|
|
117
|
+
const timeoutMs = typeof options.timeoutMs === "number" && options.timeoutMs > 0 ? Math.floor(options.timeoutMs) : 3e4;
|
|
118
|
+
const responseFormat = options.responseFormat ?? "json";
|
|
119
|
+
let tripped = false;
|
|
120
|
+
let code = null;
|
|
121
|
+
let done = false;
|
|
122
|
+
let ended = false;
|
|
123
|
+
let totalBytes = 0;
|
|
124
|
+
const finish = () => {
|
|
125
|
+
if (done) return;
|
|
126
|
+
done = true;
|
|
127
|
+
req.removeListener("data", onData);
|
|
128
|
+
req.removeListener("end", onEnd);
|
|
129
|
+
req.removeListener("close", onClose);
|
|
130
|
+
req.removeListener("error", onError);
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
};
|
|
133
|
+
const respond = (error) => {
|
|
134
|
+
if (res.headersSent) return;
|
|
135
|
+
const text = requestBodyErrorToText(error.code);
|
|
136
|
+
res.statusCode = error.statusCode;
|
|
137
|
+
if (responseFormat === "text") {
|
|
138
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
139
|
+
res.end(text);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
143
|
+
res.end(JSON.stringify({ error: text }));
|
|
144
|
+
};
|
|
145
|
+
const trip = (error) => {
|
|
146
|
+
if (tripped) return;
|
|
147
|
+
tripped = true;
|
|
148
|
+
code = error.code;
|
|
149
|
+
finish();
|
|
150
|
+
respond(error);
|
|
151
|
+
if (!req.destroyed) req.destroy();
|
|
152
|
+
};
|
|
153
|
+
const onData = (chunk) => {
|
|
154
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
155
|
+
totalBytes += buffer.length;
|
|
156
|
+
if (totalBytes > maxBytes) trip(new RequestBodyLimitError("PAYLOAD_TOO_LARGE"));
|
|
157
|
+
};
|
|
158
|
+
const onEnd = () => {
|
|
159
|
+
ended = true;
|
|
160
|
+
finish();
|
|
161
|
+
};
|
|
162
|
+
const onClose = () => {
|
|
163
|
+
if (!ended) finish();
|
|
164
|
+
};
|
|
165
|
+
const onError = () => finish();
|
|
166
|
+
const timer = setTimeout(() => trip(new RequestBodyLimitError("REQUEST_BODY_TIMEOUT")), timeoutMs);
|
|
167
|
+
req.on("data", onData);
|
|
168
|
+
req.on("end", onEnd);
|
|
169
|
+
req.on("close", onClose);
|
|
170
|
+
req.on("error", onError);
|
|
171
|
+
const declaredLength = parseContentLengthHeader(req);
|
|
172
|
+
if (declaredLength !== null && declaredLength > maxBytes) trip(new RequestBodyLimitError("PAYLOAD_TOO_LARGE"));
|
|
173
|
+
return {
|
|
174
|
+
dispose: finish,
|
|
175
|
+
isTripped: () => tripped,
|
|
176
|
+
code: () => code
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
export { installRequestBodyLimitGuard, readJsonBodyWithLimit };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import "node:crypto";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
5
|
+
//#region src/nextclaw-sdk/network-fetch.ts
|
|
6
|
+
function sanitizePrefix(prefix) {
|
|
7
|
+
return prefix.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "tmp";
|
|
8
|
+
}
|
|
9
|
+
function sanitizeFileName(fileName) {
|
|
10
|
+
return path.basename(fileName).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "download.bin";
|
|
11
|
+
}
|
|
12
|
+
function resolveTempRoot(tmpDir) {
|
|
13
|
+
return tmpDir ?? process.env.NEXTCLAW_TMP_DIR?.trim() ?? os.tmpdir();
|
|
14
|
+
}
|
|
15
|
+
async function withTempDownloadPath(params, fn) {
|
|
16
|
+
const root = resolveTempRoot(params.tmpDir);
|
|
17
|
+
const dir = await mkdtemp(path.join(root, `${sanitizePrefix(params.prefix)}-`));
|
|
18
|
+
const tempPath = path.join(dir, sanitizeFileName(params.fileName ?? "download.bin"));
|
|
19
|
+
try {
|
|
20
|
+
return await fn(tempPath);
|
|
21
|
+
} finally {
|
|
22
|
+
try {
|
|
23
|
+
await rm(dir, {
|
|
24
|
+
recursive: true,
|
|
25
|
+
force: true
|
|
26
|
+
});
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function fetchWithSsrFGuard(params) {
|
|
31
|
+
const fetcher = params.fetchImpl ?? globalThis.fetch;
|
|
32
|
+
if (!fetcher) throw new Error("fetch is not available");
|
|
33
|
+
const parsed = new URL(params.url);
|
|
34
|
+
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Invalid URL: must be http or https");
|
|
35
|
+
const allowedHostnames = params.policy?.allowedHostnames?.map((entry) => entry.trim()).filter(Boolean) ?? [];
|
|
36
|
+
if (allowedHostnames.length > 0 && !allowedHostnames.includes(parsed.hostname)) throw new Error(`${params.auditContext ?? "guarded-fetch"} blocked hostname "${parsed.hostname}"`);
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const timeoutId = params.timeoutMs && params.timeoutMs > 0 ? setTimeout(() => controller.abort(), params.timeoutMs) : void 0;
|
|
39
|
+
const relay = () => controller.abort();
|
|
40
|
+
if (params.signal) if (params.signal.aborted) controller.abort();
|
|
41
|
+
else params.signal.addEventListener("abort", relay, { once: true });
|
|
42
|
+
const response = await fetcher(parsed.toString(), {
|
|
43
|
+
...params.init ?? {},
|
|
44
|
+
signal: controller.signal
|
|
45
|
+
});
|
|
46
|
+
const finalUrl = response.url || parsed.toString();
|
|
47
|
+
const finalHostname = new URL(finalUrl).hostname;
|
|
48
|
+
if (allowedHostnames.length > 0 && !allowedHostnames.includes(finalHostname)) {
|
|
49
|
+
clearTimeout(timeoutId);
|
|
50
|
+
if (params.signal) params.signal.removeEventListener("abort", relay);
|
|
51
|
+
throw new Error(`${params.auditContext ?? "guarded-fetch"} blocked redirected hostname "${finalHostname}"`);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
response,
|
|
55
|
+
finalUrl,
|
|
56
|
+
release: async () => {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
if (params.signal) params.signal.removeEventListener("abort", relay);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { fetchWithSsrFGuard, withTempDownloadPath };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//#region src/nextclaw-sdk/network-webhook.ts
|
|
2
|
+
function pruneMapToMaxSize(map, maxSize) {
|
|
3
|
+
while (map.size > maxSize) {
|
|
4
|
+
const firstKey = map.keys().next().value;
|
|
5
|
+
if (firstKey === void 0) break;
|
|
6
|
+
map.delete(firstKey);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const WEBHOOK_RATE_LIMIT_DEFAULTS = Object.freeze({
|
|
10
|
+
windowMs: 6e4,
|
|
11
|
+
maxRequests: 120,
|
|
12
|
+
maxTrackedKeys: 4096
|
|
13
|
+
});
|
|
14
|
+
const WEBHOOK_ANOMALY_COUNTER_DEFAULTS = Object.freeze({
|
|
15
|
+
maxTrackedKeys: 4096,
|
|
16
|
+
ttlMs: 360 * 6e4,
|
|
17
|
+
logEvery: 25
|
|
18
|
+
});
|
|
19
|
+
function createFixedWindowRateLimiter(options) {
|
|
20
|
+
const state = /* @__PURE__ */ new Map();
|
|
21
|
+
const windowMs = Math.max(1, Math.floor(options.windowMs));
|
|
22
|
+
const maxRequests = Math.max(1, Math.floor(options.maxRequests));
|
|
23
|
+
const maxTrackedKeys = Math.max(1, Math.floor(options.maxTrackedKeys));
|
|
24
|
+
const pruneIntervalMs = Math.max(1, Math.floor(options.pruneIntervalMs ?? windowMs));
|
|
25
|
+
let lastPruneMs = 0;
|
|
26
|
+
const touch = (key, value) => {
|
|
27
|
+
state.delete(key);
|
|
28
|
+
state.set(key, value);
|
|
29
|
+
};
|
|
30
|
+
const prune = (nowMs) => {
|
|
31
|
+
for (const [key, entry] of state) if (nowMs - entry.windowStartMs >= windowMs) state.delete(key);
|
|
32
|
+
};
|
|
33
|
+
return {
|
|
34
|
+
isRateLimited(key, nowMs = Date.now()) {
|
|
35
|
+
if (!key) return false;
|
|
36
|
+
if (nowMs - lastPruneMs >= pruneIntervalMs) {
|
|
37
|
+
prune(nowMs);
|
|
38
|
+
lastPruneMs = nowMs;
|
|
39
|
+
}
|
|
40
|
+
const existing = state.get(key);
|
|
41
|
+
if (!existing || nowMs - existing.windowStartMs >= windowMs) {
|
|
42
|
+
touch(key, {
|
|
43
|
+
count: 1,
|
|
44
|
+
windowStartMs: nowMs
|
|
45
|
+
});
|
|
46
|
+
pruneMapToMaxSize(state, maxTrackedKeys);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const nextCount = existing.count + 1;
|
|
50
|
+
touch(key, {
|
|
51
|
+
count: nextCount,
|
|
52
|
+
windowStartMs: existing.windowStartMs
|
|
53
|
+
});
|
|
54
|
+
pruneMapToMaxSize(state, maxTrackedKeys);
|
|
55
|
+
return nextCount > maxRequests;
|
|
56
|
+
},
|
|
57
|
+
size: () => state.size,
|
|
58
|
+
clear: () => {
|
|
59
|
+
state.clear();
|
|
60
|
+
lastPruneMs = 0;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function createWebhookAnomalyTracker(options) {
|
|
65
|
+
const trackedStatusCodes = new Set(options?.trackedStatusCodes ?? [
|
|
66
|
+
400,
|
|
67
|
+
401,
|
|
68
|
+
408,
|
|
69
|
+
413,
|
|
70
|
+
415,
|
|
71
|
+
429
|
|
72
|
+
]);
|
|
73
|
+
const counters = /* @__PURE__ */ new Map();
|
|
74
|
+
const maxTrackedKeys = Math.max(1, Math.floor(options?.maxTrackedKeys ?? WEBHOOK_ANOMALY_COUNTER_DEFAULTS.maxTrackedKeys));
|
|
75
|
+
const ttlMs = Math.max(0, Math.floor(options?.ttlMs ?? WEBHOOK_ANOMALY_COUNTER_DEFAULTS.ttlMs));
|
|
76
|
+
const logEvery = Math.max(1, Math.floor(options?.logEvery ?? WEBHOOK_ANOMALY_COUNTER_DEFAULTS.logEvery));
|
|
77
|
+
const prune = (nowMs) => {
|
|
78
|
+
if (ttlMs <= 0) return;
|
|
79
|
+
for (const [key, entry] of counters) if (nowMs - entry.updatedAtMs >= ttlMs) counters.delete(key);
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
record(params) {
|
|
83
|
+
if (!trackedStatusCodes.has(params.statusCode)) return 0;
|
|
84
|
+
const nowMs = params.nowMs ?? Date.now();
|
|
85
|
+
prune(nowMs);
|
|
86
|
+
const nextCount = (counters.get(params.key)?.count ?? 0) + 1;
|
|
87
|
+
counters.set(params.key, {
|
|
88
|
+
count: nextCount,
|
|
89
|
+
updatedAtMs: nowMs
|
|
90
|
+
});
|
|
91
|
+
pruneMapToMaxSize(counters, maxTrackedKeys);
|
|
92
|
+
if (params.log && (nextCount === 1 || nextCount % logEvery === 0)) params.log(params.message(nextCount));
|
|
93
|
+
return nextCount;
|
|
94
|
+
},
|
|
95
|
+
size: () => counters.size,
|
|
96
|
+
clear: () => counters.clear()
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function isJsonContentType(value) {
|
|
100
|
+
const first = Array.isArray(value) ? value[0] : value;
|
|
101
|
+
if (!first) return false;
|
|
102
|
+
const mediaType = first.split(";", 1)[0]?.trim().toLowerCase();
|
|
103
|
+
return mediaType === "application/json" || Boolean(mediaType?.endsWith("+json"));
|
|
104
|
+
}
|
|
105
|
+
function applyBasicWebhookRequestGuards(params) {
|
|
106
|
+
const allowMethods = params.allowMethods?.length ? params.allowMethods : null;
|
|
107
|
+
if (allowMethods && !allowMethods.includes(params.req.method ?? "")) {
|
|
108
|
+
params.res.statusCode = 405;
|
|
109
|
+
params.res.setHeader("Allow", allowMethods.join(", "));
|
|
110
|
+
params.res.end("Method Not Allowed");
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (params.rateLimiter && params.rateLimitKey && params.rateLimiter.isRateLimited(params.rateLimitKey, params.nowMs ?? Date.now())) {
|
|
114
|
+
params.res.statusCode = 429;
|
|
115
|
+
params.res.end("Too Many Requests");
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
if (params.requireJsonContentType && params.req.method === "POST" && !isJsonContentType(params.req.headers["content-type"])) {
|
|
119
|
+
params.res.statusCode = 415;
|
|
120
|
+
params.res.end("Unsupported Media Type");
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { WEBHOOK_ANOMALY_COUNTER_DEFAULTS, WEBHOOK_RATE_LIMIT_DEFAULTS, applyBasicWebhookRequestGuards, createFixedWindowRateLimiter, createWebhookAnomalyTracker };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/nextclaw-sdk/runtime-store.ts
|
|
2
|
+
function createPluginRuntimeStore(errorMessage) {
|
|
3
|
+
let runtime = null;
|
|
4
|
+
return {
|
|
5
|
+
setRuntime(next) {
|
|
6
|
+
runtime = next;
|
|
7
|
+
},
|
|
8
|
+
clearRuntime() {
|
|
9
|
+
runtime = null;
|
|
10
|
+
},
|
|
11
|
+
tryGetRuntime() {
|
|
12
|
+
return runtime;
|
|
13
|
+
},
|
|
14
|
+
getRuntime() {
|
|
15
|
+
if (!runtime) throw new Error(errorMessage);
|
|
16
|
+
return runtime;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { createPluginRuntimeStore };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
//#region src/nextclaw-sdk/secrets-config.ts
|
|
2
|
+
function mergeAllowFromEntries(current, additions) {
|
|
3
|
+
const merged = [...current ?? [], ...additions].map((entry) => String(entry).trim()).filter(Boolean);
|
|
4
|
+
return [...new Set(merged)];
|
|
5
|
+
}
|
|
6
|
+
function splitOnboardingEntries(raw) {
|
|
7
|
+
return raw.split(/[\n,;]+/g).map((entry) => entry.trim()).filter(Boolean);
|
|
8
|
+
}
|
|
9
|
+
function patchTopLevelChannelConfig(params) {
|
|
10
|
+
const channelConfig = params.cfg.channels?.[params.channel] ?? {};
|
|
11
|
+
return {
|
|
12
|
+
...params.cfg,
|
|
13
|
+
channels: {
|
|
14
|
+
...params.cfg.channels,
|
|
15
|
+
[params.channel]: {
|
|
16
|
+
...channelConfig,
|
|
17
|
+
...params.enabled ? { enabled: true } : {},
|
|
18
|
+
...params.patch
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function addWildcardAllowFrom(allowFrom) {
|
|
24
|
+
const next = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
25
|
+
if (!next.includes("*")) next.push("*");
|
|
26
|
+
return next;
|
|
27
|
+
}
|
|
28
|
+
function setTopLevelChannelAllowFrom(params) {
|
|
29
|
+
return patchTopLevelChannelConfig({
|
|
30
|
+
cfg: params.cfg,
|
|
31
|
+
channel: params.channel,
|
|
32
|
+
enabled: params.enabled,
|
|
33
|
+
patch: { allowFrom: params.allowFrom }
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function setTopLevelChannelDmPolicyWithAllowFrom(params) {
|
|
37
|
+
const channelConfig = params.cfg.channels?.[params.channel] ?? {};
|
|
38
|
+
const existingAllowFrom = params.getAllowFrom?.(params.cfg) ?? channelConfig.allowFrom ?? void 0;
|
|
39
|
+
const allowFrom = params.dmPolicy === "open" ? addWildcardAllowFrom(existingAllowFrom) : void 0;
|
|
40
|
+
return patchTopLevelChannelConfig({
|
|
41
|
+
cfg: params.cfg,
|
|
42
|
+
channel: params.channel,
|
|
43
|
+
patch: {
|
|
44
|
+
dmPolicy: params.dmPolicy,
|
|
45
|
+
...allowFrom ? { allowFrom } : {}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function setTopLevelChannelGroupPolicy(params) {
|
|
50
|
+
return patchTopLevelChannelConfig({
|
|
51
|
+
cfg: params.cfg,
|
|
52
|
+
channel: params.channel,
|
|
53
|
+
enabled: params.enabled,
|
|
54
|
+
patch: { groupPolicy: params.groupPolicy }
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function buildSingleChannelSecretPromptState(params) {
|
|
58
|
+
return {
|
|
59
|
+
accountConfigured: params.accountConfigured,
|
|
60
|
+
hasConfigToken: params.hasConfigToken,
|
|
61
|
+
canUseEnv: params.allowEnv && Boolean(params.envValue?.trim()) && !params.hasConfigToken
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { buildSingleChannelSecretPromptState, mergeAllowFromEntries, setTopLevelChannelAllowFrom, setTopLevelChannelDmPolicyWithAllowFrom, setTopLevelChannelGroupPolicy, splitOnboardingEntries };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import "zod";
|
|
2
|
+
//#region src/nextclaw-sdk/secrets-core.ts
|
|
3
|
+
const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
|
|
4
|
+
const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
5
|
+
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
|
6
|
+
const FILE_SECRET_REF_SEGMENT_PATTERN = /^(?:[^~]|~0|~1)*$/;
|
|
7
|
+
const EXEC_SECRET_REF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
function isSecretRef(value) {
|
|
12
|
+
return isRecord(value) && (value.source === "env" || value.source === "file" || value.source === "exec") && typeof value.provider === "string" && value.provider.trim().length > 0 && typeof value.id === "string" && value.id.trim().length > 0;
|
|
13
|
+
}
|
|
14
|
+
function coerceSecretRef(value, defaults) {
|
|
15
|
+
if (isSecretRef(value)) return value;
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
const match = ENV_SECRET_TEMPLATE_RE.exec(value.trim());
|
|
18
|
+
if (match) return {
|
|
19
|
+
source: "env",
|
|
20
|
+
provider: defaults?.env ?? "default",
|
|
21
|
+
id: match[1]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (!isRecord(value)) return null;
|
|
25
|
+
if ((value.source === "env" || value.source === "file" || value.source === "exec") && typeof value.id === "string" && value.id.trim().length > 0 && value.provider === void 0) {
|
|
26
|
+
const source = value.source;
|
|
27
|
+
return {
|
|
28
|
+
source,
|
|
29
|
+
provider: source === "env" ? defaults?.env ?? "default" : source === "file" ? defaults?.file ?? "default" : defaults?.exec ?? "default",
|
|
30
|
+
id: value.id
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function resolveSecretInputRef(params) {
|
|
36
|
+
return coerceSecretRef(params.refValue, params.defaults) ?? coerceSecretRef(params.value, params.defaults);
|
|
37
|
+
}
|
|
38
|
+
function normalizeSecretInputString(value) {
|
|
39
|
+
if (typeof value !== "string") return;
|
|
40
|
+
return value.trim() || void 0;
|
|
41
|
+
}
|
|
42
|
+
function hasConfiguredSecretInput(value, defaults) {
|
|
43
|
+
return Boolean(normalizeSecretInputString(value) || coerceSecretRef(value, defaults));
|
|
44
|
+
}
|
|
45
|
+
function normalizeResolvedSecretInputString(params) {
|
|
46
|
+
const normalized = normalizeSecretInputString(params.value);
|
|
47
|
+
if (normalized) return normalized;
|
|
48
|
+
const ref = resolveSecretInputRef(params);
|
|
49
|
+
if (ref) throw new Error(`${params.path}: unresolved SecretRef "${ref.source}:${ref.provider}:${ref.id}".`);
|
|
50
|
+
}
|
|
51
|
+
function isValidFileSecretRefId(value) {
|
|
52
|
+
if (value === "value") return true;
|
|
53
|
+
if (!value.startsWith("/")) return false;
|
|
54
|
+
return value.slice(1).split("/").every((segment) => FILE_SECRET_REF_SEGMENT_PATTERN.test(segment));
|
|
55
|
+
}
|
|
56
|
+
function isValidExecSecretRefId(value) {
|
|
57
|
+
if (!EXEC_SECRET_REF_ID_PATTERN.test(value)) return false;
|
|
58
|
+
return value.split("/").every((segment) => segment !== "." && segment !== "..");
|
|
59
|
+
}
|
|
60
|
+
function formatExecSecretRefIdValidationMessage() {
|
|
61
|
+
return [
|
|
62
|
+
"Exec secret reference id must match /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/",
|
|
63
|
+
"and must not include \".\" or \"..\" path segments",
|
|
64
|
+
"(example: \"vault/openai/api-key\")."
|
|
65
|
+
].join(" ");
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { DEFAULT_SECRET_PROVIDER_ALIAS, ENV_SECRET_REF_ID_RE, formatExecSecretRefIdValidationMessage, hasConfiguredSecretInput, isValidExecSecretRefId, isValidFileSecretRefId, normalizeResolvedSecretInputString, normalizeSecretInputString };
|