opencode-oncall 0.1.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/LICENSE +151 -0
- package/README.md +50 -0
- package/dist/common-settings-actions.d.ts +15 -0
- package/dist/common-settings-actions.js +48 -0
- package/dist/common-settings-store.d.ts +1 -0
- package/dist/common-settings-store.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin-hooks.d.ts +51 -0
- package/dist/plugin-hooks.js +288 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.js +115 -0
- package/dist/settings-store.d.ts +50 -0
- package/dist/settings-store.js +214 -0
- package/dist/store-paths.d.ts +16 -0
- package/dist/store-paths.js +61 -0
- package/dist/ui/wechat-menu.d.ts +26 -0
- package/dist/ui/wechat-menu.js +90 -0
- package/dist/wechat/bind-flow.d.ts +29 -0
- package/dist/wechat/bind-flow.js +207 -0
- package/dist/wechat/bridge.d.ts +136 -0
- package/dist/wechat/bridge.js +1059 -0
- package/dist/wechat/broker-client.d.ts +23 -0
- package/dist/wechat/broker-client.js +274 -0
- package/dist/wechat/broker-endpoint.d.ts +21 -0
- package/dist/wechat/broker-endpoint.js +78 -0
- package/dist/wechat/broker-entry.d.ts +123 -0
- package/dist/wechat/broker-entry.js +1321 -0
- package/dist/wechat/broker-launcher.d.ts +37 -0
- package/dist/wechat/broker-launcher.js +418 -0
- package/dist/wechat/broker-mutation-queue.d.ts +93 -0
- package/dist/wechat/broker-mutation-queue.js +126 -0
- package/dist/wechat/broker-server.d.ts +86 -0
- package/dist/wechat/broker-server.js +1340 -0
- package/dist/wechat/broker-state-store.d.ts +335 -0
- package/dist/wechat/broker-state-store.js +1964 -0
- package/dist/wechat/command-parser.d.ts +18 -0
- package/dist/wechat/command-parser.js +58 -0
- package/dist/wechat/compat/jiti-loader.d.ts +27 -0
- package/dist/wechat/compat/jiti-loader.js +118 -0
- package/dist/wechat/compat/openclaw-account-helpers.d.ts +29 -0
- package/dist/wechat/compat/openclaw-account-helpers.js +60 -0
- package/dist/wechat/compat/openclaw-bind-helpers.d.ts +29 -0
- package/dist/wechat/compat/openclaw-bind-helpers.js +169 -0
- package/dist/wechat/compat/openclaw-guided-smoke.d.ts +180 -0
- package/dist/wechat/compat/openclaw-guided-smoke.js +1134 -0
- package/dist/wechat/compat/openclaw-public-entry.d.ts +33 -0
- package/dist/wechat/compat/openclaw-public-entry.js +62 -0
- package/dist/wechat/compat/openclaw-public-helpers.d.ts +70 -0
- package/dist/wechat/compat/openclaw-public-helpers.js +68 -0
- package/dist/wechat/compat/openclaw-qr-gateway.d.ts +15 -0
- package/dist/wechat/compat/openclaw-qr-gateway.js +39 -0
- package/dist/wechat/compat/openclaw-smoke.d.ts +48 -0
- package/dist/wechat/compat/openclaw-smoke.js +100 -0
- package/dist/wechat/compat/openclaw-sync-buf.d.ts +24 -0
- package/dist/wechat/compat/openclaw-sync-buf.js +80 -0
- package/dist/wechat/compat/openclaw-updates-send.d.ts +47 -0
- package/dist/wechat/compat/openclaw-updates-send.js +38 -0
- package/dist/wechat/compat/qrcode-terminal-loader.d.ts +12 -0
- package/dist/wechat/compat/qrcode-terminal-loader.js +16 -0
- package/dist/wechat/compat/slash-guard.d.ts +11 -0
- package/dist/wechat/compat/slash-guard.js +24 -0
- package/dist/wechat/dead-letter-store.d.ts +48 -0
- package/dist/wechat/dead-letter-store.js +224 -0
- package/dist/wechat/debug-bundle-collector.d.ts +49 -0
- package/dist/wechat/debug-bundle-collector.js +580 -0
- package/dist/wechat/debug-bundle-flow.d.ts +37 -0
- package/dist/wechat/debug-bundle-flow.js +180 -0
- package/dist/wechat/debug-bundle-redaction.d.ts +14 -0
- package/dist/wechat/debug-bundle-redaction.js +339 -0
- package/dist/wechat/handle.d.ts +10 -0
- package/dist/wechat/handle.js +57 -0
- package/dist/wechat/ipc-auth.d.ts +6 -0
- package/dist/wechat/ipc-auth.js +39 -0
- package/dist/wechat/latest-account-state-store.d.ts +8 -0
- package/dist/wechat/latest-account-state-store.js +38 -0
- package/dist/wechat/notification-dispatcher.d.ts +34 -0
- package/dist/wechat/notification-dispatcher.js +266 -0
- package/dist/wechat/notification-format.d.ts +15 -0
- package/dist/wechat/notification-format.js +196 -0
- package/dist/wechat/notification-store.d.ts +72 -0
- package/dist/wechat/notification-store.js +807 -0
- package/dist/wechat/notification-types.d.ts +37 -0
- package/dist/wechat/notification-types.js +1 -0
- package/dist/wechat/openclaw-account-adapter.d.ts +30 -0
- package/dist/wechat/openclaw-account-adapter.js +60 -0
- package/dist/wechat/operator-store.d.ts +9 -0
- package/dist/wechat/operator-store.js +69 -0
- package/dist/wechat/protocol.d.ts +150 -0
- package/dist/wechat/protocol.js +197 -0
- package/dist/wechat/question-interaction.d.ts +24 -0
- package/dist/wechat/question-interaction.js +180 -0
- package/dist/wechat/request-store.d.ts +108 -0
- package/dist/wechat/request-store.js +669 -0
- package/dist/wechat/session-digest.d.ts +50 -0
- package/dist/wechat/session-digest.js +167 -0
- package/dist/wechat/state-paths.d.ts +26 -0
- package/dist/wechat/state-paths.js +92 -0
- package/dist/wechat/status-format.d.ts +26 -0
- package/dist/wechat/status-format.js +616 -0
- package/dist/wechat/token-store.d.ts +20 -0
- package/dist/wechat/token-store.js +193 -0
- package/dist/wechat/wechat-status-runtime.d.ts +89 -0
- package/dist/wechat/wechat-status-runtime.js +518 -0
- package/package.json +74 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { zipSync } from "fflate";
|
|
5
|
+
import { collectWechatDebugBundle } from "./debug-bundle-collector.js";
|
|
6
|
+
const EXPORT_SUCCESS_PREFIX = "微信调试包已生成:";
|
|
7
|
+
const EXPORT_FAILURE_MESSAGE = "导出微信调试包失败";
|
|
8
|
+
const MISSING_DIAGNOSTICS_MESSAGE = "没有可导出的微信诊断文件";
|
|
9
|
+
const MISSING_STATE_ROOT_MESSAGE = "微信状态目录不存在,无法导出调试包";
|
|
10
|
+
const ZIP_WRITE_FAILURE_MESSAGE = "创建压缩包失败";
|
|
11
|
+
class WechatDebugBundleFlowError extends Error {
|
|
12
|
+
failure;
|
|
13
|
+
constructor(failure) {
|
|
14
|
+
super(failure.message);
|
|
15
|
+
this.name = "WechatDebugBundleFlowError";
|
|
16
|
+
this.failure = failure;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function runWechatDebugBundleFlow(input, deps = {}) {
|
|
20
|
+
const now = input.now ?? new Date();
|
|
21
|
+
const outputRootDir = input.outputRootDir ?? path.join(tmpdir(), "opencode-wechat", "wechat-debug-bundles");
|
|
22
|
+
const bundle = await collectWechatDebugBundleOrThrow(input);
|
|
23
|
+
if (!bundle.entries.some((entry) => entry.category === "diagnostics")) {
|
|
24
|
+
throw new WechatDebugBundleFlowError({
|
|
25
|
+
ok: false,
|
|
26
|
+
mode: input.mode,
|
|
27
|
+
code: "missing-diagnostics",
|
|
28
|
+
message: MISSING_DIAGNOSTICS_MESSAGE,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
let archive;
|
|
32
|
+
try {
|
|
33
|
+
archive = zipSync(Object.fromEntries(bundle.entries.map((entry) => [entry.bundlePath, entry.content])));
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new WechatDebugBundleFlowError(createZipWriteFailureResult(input.mode, error));
|
|
37
|
+
}
|
|
38
|
+
let finalBundlePath;
|
|
39
|
+
try {
|
|
40
|
+
await mkdir(outputRootDir, { recursive: true });
|
|
41
|
+
finalBundlePath = await writeBundleArchiveCollisionSafe({
|
|
42
|
+
archive,
|
|
43
|
+
mode: input.mode,
|
|
44
|
+
now,
|
|
45
|
+
outputRootDir,
|
|
46
|
+
writeArchiveFile: deps.writeArchiveFile ?? writeFile,
|
|
47
|
+
removeArchiveFile: deps.removeArchiveFile ?? rm,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw toFlowError(error, createZipWriteFailureResult(input.mode, error));
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
mode: input.mode,
|
|
55
|
+
bundlePath: finalBundlePath,
|
|
56
|
+
message: `${EXPORT_SUCCESS_PREFIX}${finalBundlePath}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function toWechatDebugBundleFailureResult(error, input) {
|
|
60
|
+
if (error instanceof WechatDebugBundleFlowError) {
|
|
61
|
+
return error.failure;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
ok: false,
|
|
65
|
+
mode: input?.mode ?? "sanitized",
|
|
66
|
+
code: "export-failed",
|
|
67
|
+
message: EXPORT_FAILURE_MESSAGE,
|
|
68
|
+
details: {
|
|
69
|
+
cause: toErrorMessage(error),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export function toWechatDebugBundleFailureMessage(error, input) {
|
|
74
|
+
return toWechatDebugBundleFailureResult(error, input).message;
|
|
75
|
+
}
|
|
76
|
+
function buildWechatDebugBundleFileName(mode, now) {
|
|
77
|
+
const modeLabel = mode === "sanitized" ? "sanitized" : "full";
|
|
78
|
+
const timestamp = now.toISOString().replace(/\.\d{3}Z$/, "").replaceAll(":", "-");
|
|
79
|
+
return `wechat-debug-bundle-${modeLabel}-${timestamp}.zip`;
|
|
80
|
+
}
|
|
81
|
+
async function writeBundleArchiveCollisionSafe(input) {
|
|
82
|
+
const preferredName = buildWechatDebugBundleFileName(input.mode, input.now);
|
|
83
|
+
const extension = path.extname(preferredName);
|
|
84
|
+
const nameWithoutExtension = preferredName.slice(0, -extension.length);
|
|
85
|
+
for (let attempt = 0; attempt < 10_000; attempt += 1) {
|
|
86
|
+
const suffix = attempt === 0 ? "" : `-${attempt + 1}`;
|
|
87
|
+
const candidatePath = path.join(input.outputRootDir, `${nameWithoutExtension}${suffix}${extension}`);
|
|
88
|
+
try {
|
|
89
|
+
await input.writeArchiveFile(candidatePath, input.archive, { flag: "wx" });
|
|
90
|
+
return candidatePath;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (isAlreadyExistsError(error)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const cleanupError = await tryRemovePartialArchive(candidatePath, input.removeArchiveFile);
|
|
97
|
+
if (cleanupError) {
|
|
98
|
+
throw new WechatDebugBundleFlowError(createZipCleanupFailureResult(input.mode, candidatePath, error, cleanupError));
|
|
99
|
+
}
|
|
100
|
+
throw new WechatDebugBundleFlowError(createZipWriteFailureResult(input.mode, error, candidatePath));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
throw new WechatDebugBundleFlowError(createZipWriteFailureResult(input.mode, "unique-name-exhausted"));
|
|
104
|
+
}
|
|
105
|
+
async function collectWechatDebugBundleOrThrow(input) {
|
|
106
|
+
try {
|
|
107
|
+
return await collectWechatDebugBundle(input);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const message = toErrorMessage(error);
|
|
111
|
+
if (message === MISSING_STATE_ROOT_MESSAGE) {
|
|
112
|
+
throw new WechatDebugBundleFlowError({
|
|
113
|
+
ok: false,
|
|
114
|
+
mode: input.mode,
|
|
115
|
+
code: "missing-state-root",
|
|
116
|
+
message,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
throw new WechatDebugBundleFlowError({
|
|
120
|
+
ok: false,
|
|
121
|
+
mode: input.mode,
|
|
122
|
+
code: "export-failed",
|
|
123
|
+
message: EXPORT_FAILURE_MESSAGE,
|
|
124
|
+
details: {
|
|
125
|
+
cause: message,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function isAlreadyExistsError(error) {
|
|
131
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
132
|
+
}
|
|
133
|
+
function toFlowError(error, fallbackFailure) {
|
|
134
|
+
if (error instanceof WechatDebugBundleFlowError) {
|
|
135
|
+
return error;
|
|
136
|
+
}
|
|
137
|
+
return new WechatDebugBundleFlowError(fallbackFailure);
|
|
138
|
+
}
|
|
139
|
+
function createZipWriteFailureResult(mode, error, archivePath) {
|
|
140
|
+
const details = {
|
|
141
|
+
...(archivePath ? { archivePath } : {}),
|
|
142
|
+
writeCause: toErrorMessage(error),
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
mode,
|
|
147
|
+
code: "zip-write-failed",
|
|
148
|
+
message: ZIP_WRITE_FAILURE_MESSAGE,
|
|
149
|
+
details,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function createZipCleanupFailureResult(mode, archivePath, writeError, cleanupError) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
mode,
|
|
156
|
+
code: "zip-cleanup-failed",
|
|
157
|
+
message: `创建压缩包失败,请手动删除残留压缩包:${archivePath}`,
|
|
158
|
+
archivePath,
|
|
159
|
+
details: {
|
|
160
|
+
archivePath,
|
|
161
|
+
writeCause: toErrorMessage(writeError),
|
|
162
|
+
cleanupCause: toErrorMessage(cleanupError),
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function tryRemovePartialArchive(filePath, removeArchiveFile) {
|
|
167
|
+
try {
|
|
168
|
+
await removeArchiveFile(filePath, { force: true });
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
return error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function toErrorMessage(error) {
|
|
176
|
+
if (error instanceof Error && error.message.trim().length > 0) {
|
|
177
|
+
return error.message;
|
|
178
|
+
}
|
|
179
|
+
return String(error);
|
|
180
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type WechatDebugBundleMode = "sanitized" | "full";
|
|
2
|
+
export type RedactDebugBundleContentOptions = {
|
|
3
|
+
bundlePath: string;
|
|
4
|
+
mode: WechatDebugBundleMode;
|
|
5
|
+
};
|
|
6
|
+
export declare const REDACTED_CONTEXT_TOKEN = "[REDACTED_CONTEXT_TOKEN]";
|
|
7
|
+
export declare const REDACTED_ACCOUNT_ID = "[REDACTED_ACCOUNT_ID]";
|
|
8
|
+
export declare const REDACTED_USER_ID = "[REDACTED_USER_ID]";
|
|
9
|
+
export declare const REDACTED_TOKEN = "[REDACTED_TOKEN]";
|
|
10
|
+
export declare const REDACTED_CREDENTIAL = "[REDACTED_CREDENTIAL]";
|
|
11
|
+
export declare const REDACTED_MESSAGE_TEXT = "[REDACTED_MESSAGE_TEXT]";
|
|
12
|
+
export declare const REDACTED_CORRUPT_STRUCTURED_CONTENT = "[REDACTED_CORRUPT_STRUCTURED_CONTENT]";
|
|
13
|
+
export declare function redactDebugBundleContent(content: Buffer, options: RedactDebugBundleContentOptions): Buffer;
|
|
14
|
+
export declare function redactDebugBundleText(text: string, bundlePath: string): string;
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
export const REDACTED_CONTEXT_TOKEN = "[REDACTED_CONTEXT_TOKEN]";
|
|
2
|
+
export const REDACTED_ACCOUNT_ID = "[REDACTED_ACCOUNT_ID]";
|
|
3
|
+
export const REDACTED_USER_ID = "[REDACTED_USER_ID]";
|
|
4
|
+
export const REDACTED_TOKEN = "[REDACTED_TOKEN]";
|
|
5
|
+
export const REDACTED_CREDENTIAL = "[REDACTED_CREDENTIAL]";
|
|
6
|
+
export const REDACTED_MESSAGE_TEXT = "[REDACTED_MESSAGE_TEXT]";
|
|
7
|
+
export const REDACTED_CORRUPT_STRUCTURED_CONTENT = "[REDACTED_CORRUPT_STRUCTURED_CONTENT]";
|
|
8
|
+
const SAFE_DIAGNOSTIC_SUFFIX_KEYS = ["requestId", "upstream", "status"];
|
|
9
|
+
export function redactDebugBundleContent(content, options) {
|
|
10
|
+
if (options.mode === "full") {
|
|
11
|
+
return Buffer.from(content);
|
|
12
|
+
}
|
|
13
|
+
const text = content.toString("utf8");
|
|
14
|
+
return Buffer.from(redactDebugBundleText(text, options.bundlePath), "utf8");
|
|
15
|
+
}
|
|
16
|
+
export function redactDebugBundleText(text, bundlePath) {
|
|
17
|
+
if (bundlePath.endsWith(".json")) {
|
|
18
|
+
return redactJsonText(text);
|
|
19
|
+
}
|
|
20
|
+
if (bundlePath.endsWith(".jsonl")) {
|
|
21
|
+
return redactJsonLines(text, { failClosed: true });
|
|
22
|
+
}
|
|
23
|
+
if (bundlePath.endsWith(".log")) {
|
|
24
|
+
return redactJsonLines(text, { failClosed: false });
|
|
25
|
+
}
|
|
26
|
+
return redactPlainText(text);
|
|
27
|
+
}
|
|
28
|
+
function redactJsonText(text) {
|
|
29
|
+
const parsed = tryParseJson(text);
|
|
30
|
+
if (parsed === undefined) {
|
|
31
|
+
return serializeCorruptStructuredContent({ multiline: true, trailingNewline: text.endsWith("\n") });
|
|
32
|
+
}
|
|
33
|
+
const suffix = text.endsWith("\n") ? "\n" : "";
|
|
34
|
+
return `${JSON.stringify(redactStructuredValue(parsed), null, 2)}${suffix}`;
|
|
35
|
+
}
|
|
36
|
+
function redactJsonLines(text, options) {
|
|
37
|
+
const lines = text.split(/\r?\n/);
|
|
38
|
+
const redactedLines = lines.map((line) => {
|
|
39
|
+
if (line.trim().length === 0) {
|
|
40
|
+
return line;
|
|
41
|
+
}
|
|
42
|
+
const parsed = tryParseJson(line);
|
|
43
|
+
if (parsed === undefined) {
|
|
44
|
+
if (options.failClosed || looksLikeStructuredLine(line)) {
|
|
45
|
+
return serializeCorruptStructuredContent({ multiline: false, trailingNewline: false });
|
|
46
|
+
}
|
|
47
|
+
return redactPlainText(line);
|
|
48
|
+
}
|
|
49
|
+
return JSON.stringify(redactStructuredValue(parsed));
|
|
50
|
+
});
|
|
51
|
+
return redactedLines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
function redactPlainText(text) {
|
|
54
|
+
let redacted = redactQuotedEscapedJsonFragments(text);
|
|
55
|
+
redacted = redactUnquotedEscapedJsonFragments(redacted);
|
|
56
|
+
redacted = redactEmbeddedJsonFragments(redacted);
|
|
57
|
+
redacted = redacted.replace(/Bearer\s+[^\s\r\n&?,;|]+/gi, `Bearer ${REDACTED_TOKEN}`);
|
|
58
|
+
redacted = replaceField(redacted, ["authorization"], REDACTED_TOKEN, {
|
|
59
|
+
allowColonFollowers: true,
|
|
60
|
+
followerKeys: SAFE_DIAGNOSTIC_SUFFIX_KEYS,
|
|
61
|
+
preserveWholeSuffixOnly: true,
|
|
62
|
+
allowAmpersandFollowers: true,
|
|
63
|
+
forbidQuestionMarkBeforeSuffix: true,
|
|
64
|
+
});
|
|
65
|
+
redacted = replaceField(redacted, ["contextToken"], REDACTED_CONTEXT_TOKEN);
|
|
66
|
+
redacted = replaceField(redacted, ["wechatAccountId", "accountId"], REDACTED_ACCOUNT_ID);
|
|
67
|
+
redacted = replaceField(redacted, ["userId", "fromUserId", "toUserId"], REDACTED_USER_ID);
|
|
68
|
+
redacted = replaceField(redacted, ["cookie", "credential", "credentials"], REDACTED_CREDENTIAL);
|
|
69
|
+
redacted = replaceField(redacted, ["accessToken", "refreshToken", "bearerToken", "token", "secret", "password"], REDACTED_TOKEN);
|
|
70
|
+
redacted = replaceField(redacted, ["messageBody", "messageText", "message", "rawText", "rawMessage", "body", "text", "content"], REDACTED_MESSAGE_TEXT, {
|
|
71
|
+
allowColonFollowers: true,
|
|
72
|
+
followerKeys: SAFE_DIAGNOSTIC_SUFFIX_KEYS,
|
|
73
|
+
preserveWholeSuffixOnly: true,
|
|
74
|
+
requireMinimumFollowerCount: 2,
|
|
75
|
+
});
|
|
76
|
+
return redacted;
|
|
77
|
+
}
|
|
78
|
+
function redactQuotedEscapedJsonFragments(text) {
|
|
79
|
+
return text.replace(/"((?:\{(?:\\.|[^"\r\n])*\})|(?:\[(?:\\.|[^"\r\n])*\]))"/g, (match, fragment) => {
|
|
80
|
+
const parsed = tryParseEscapedJsonFragment(fragment);
|
|
81
|
+
if (parsed === undefined) {
|
|
82
|
+
return match;
|
|
83
|
+
}
|
|
84
|
+
return `"${stringifyEscapedJsonFragment(redactStructuredValue(parsed))}"`;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function redactUnquotedEscapedJsonFragments(text) {
|
|
88
|
+
return text.replace(/(?:\{(?:\\.|[^{}\r\n])*\})|(?:\[(?:\\.|[^\[\]\r\n])*\])/g, (fragment) => {
|
|
89
|
+
if (!fragment.includes('\\"')) {
|
|
90
|
+
return fragment;
|
|
91
|
+
}
|
|
92
|
+
const parsed = tryParseEscapedJsonFragment(fragment);
|
|
93
|
+
if (parsed === undefined) {
|
|
94
|
+
return fragment;
|
|
95
|
+
}
|
|
96
|
+
return stringifyEscapedJsonFragment(redactStructuredValue(parsed));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function replaceField(text, keys, replacement, options = {}) {
|
|
100
|
+
const escapedKeys = keys.map((key) => escapeRegExp(key)).join("|");
|
|
101
|
+
const followerValueSeparator = options.allowColonFollowers ? "(?:=|:)" : "=";
|
|
102
|
+
const followerKeyPattern = (options.followerKeys ?? [])
|
|
103
|
+
.map((key) => escapeRegExp(key))
|
|
104
|
+
.join("|");
|
|
105
|
+
const structuredFollowerBoundary = followerKeyPattern.length > 0
|
|
106
|
+
? `(?:\\s+(?:${followerKeyPattern})\\s*${followerValueSeparator}|[&?](?:${followerKeyPattern})\\s*=|[,;|]+\\s*(?:${followerKeyPattern})\\s*${followerValueSeparator})`
|
|
107
|
+
: null;
|
|
108
|
+
const matcher = new RegExp(options.terminal
|
|
109
|
+
? `(\\b(?:${escapedKeys})\\b\\s*[:=]\\s*)([^\\r\\n]*)`
|
|
110
|
+
: options.preserveWholeSuffixOnly
|
|
111
|
+
? `(\\b(?:${escapedKeys})\\b\\s*[:=]\\s*)([^\\r\\n]*)`
|
|
112
|
+
: structuredFollowerBoundary
|
|
113
|
+
? `(\\b(?:${escapedKeys})\\b\\s*[:=]\\s*)([^\\r\\n]*?)(?=(?:${structuredFollowerBoundary})|$)`
|
|
114
|
+
: `(\\b(?:${escapedKeys})\\b\\s*[:=]\\s*)([^\\r\\n]*)`, "gi");
|
|
115
|
+
return text.replace(matcher, (_, prefix, value) => {
|
|
116
|
+
if (options.preserveWholeSuffixOnly) {
|
|
117
|
+
const suffix = extractSafeSuffixChain(value, {
|
|
118
|
+
allowColonFollowers: options.allowColonFollowers === true,
|
|
119
|
+
followerKeys: options.followerKeys ?? [],
|
|
120
|
+
allowAmpersandFollowers: options.allowAmpersandFollowers === true,
|
|
121
|
+
forbidQuestionMarkBeforeSuffix: options.forbidQuestionMarkBeforeSuffix === true,
|
|
122
|
+
minimumCount: options.requireMinimumFollowerCount ?? 1,
|
|
123
|
+
});
|
|
124
|
+
return `${prefix}${replacement}${suffix}`;
|
|
125
|
+
}
|
|
126
|
+
return `${prefix}${replacement}`;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function extractSafeSuffixChain(value, options) {
|
|
130
|
+
if (options.followerKeys.length === 0) {
|
|
131
|
+
return "";
|
|
132
|
+
}
|
|
133
|
+
const followerKeyPattern = options.followerKeys.map((key) => escapeRegExp(key)).join("|");
|
|
134
|
+
const valueSeparator = options.allowColonFollowers ? "(?:=|:)" : "=";
|
|
135
|
+
const segmentSeparator = options.allowAmpersandFollowers ? `[\\s,;|&]+` : `[\\s,;|]+`;
|
|
136
|
+
const segment = `${segmentSeparator}(?:${followerKeyPattern})\\s*${valueSeparator}\\s*[^\\s,;|&?]+`;
|
|
137
|
+
const suffixMatcher = new RegExp(`((?:${segment})+)$`, "i");
|
|
138
|
+
const suffixMatch = value.match(suffixMatcher);
|
|
139
|
+
if (!suffixMatch) {
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
const prefixValue = value.slice(0, value.length - suffixMatch[1].length);
|
|
143
|
+
if (options.forbidQuestionMarkBeforeSuffix && prefixValue.includes("?")) {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
const segmentMatcher = new RegExp(segment, "gi");
|
|
147
|
+
const segmentCount = Array.from(suffixMatch[1].matchAll(segmentMatcher)).length;
|
|
148
|
+
return segmentCount >= options.minimumCount ? suffixMatch[1] : "";
|
|
149
|
+
}
|
|
150
|
+
function tryParseJson(text) {
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(text);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function serializeCorruptStructuredContent(options) {
|
|
159
|
+
const placeholder = { _corruptStructuredContent: REDACTED_CORRUPT_STRUCTURED_CONTENT };
|
|
160
|
+
const serialized = options.multiline ? JSON.stringify(placeholder, null, 2) : JSON.stringify(placeholder);
|
|
161
|
+
return options.trailingNewline ? `${serialized}\n` : serialized;
|
|
162
|
+
}
|
|
163
|
+
function looksLikeStructuredLine(line) {
|
|
164
|
+
const trimmed = line.trimStart();
|
|
165
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
166
|
+
}
|
|
167
|
+
function redactStructuredValue(value, key = "") {
|
|
168
|
+
if (typeof value === "string") {
|
|
169
|
+
const replacement = replacementForKey(key);
|
|
170
|
+
if (replacement !== null) {
|
|
171
|
+
return redactPlainText(replacement);
|
|
172
|
+
}
|
|
173
|
+
const nestedJson = tryParseJson(value);
|
|
174
|
+
if (nestedJson !== undefined) {
|
|
175
|
+
return JSON.stringify(redactStructuredValue(nestedJson));
|
|
176
|
+
}
|
|
177
|
+
return redactPlainText(redactEmbeddedJsonFragments(value));
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
return value.map((item) => redactStructuredValue(item, key));
|
|
181
|
+
}
|
|
182
|
+
if (!value || typeof value !== "object") {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
const next = {};
|
|
186
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
187
|
+
next[childKey] = redactStructuredValue(childValue, childKey);
|
|
188
|
+
}
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
191
|
+
function replacementForKey(key) {
|
|
192
|
+
const normalizedKey = normalizeKey(key);
|
|
193
|
+
if (normalizedKey.length === 0) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (normalizedKey === "contexttoken") {
|
|
197
|
+
return REDACTED_CONTEXT_TOKEN;
|
|
198
|
+
}
|
|
199
|
+
if (normalizedKey === "wechataccountid" || normalizedKey === "accountid") {
|
|
200
|
+
return REDACTED_ACCOUNT_ID;
|
|
201
|
+
}
|
|
202
|
+
if (normalizedKey.endsWith("userid")) {
|
|
203
|
+
return REDACTED_USER_ID;
|
|
204
|
+
}
|
|
205
|
+
if (normalizedKey.includes("cookie") || normalizedKey.includes("credential")) {
|
|
206
|
+
return REDACTED_CREDENTIAL;
|
|
207
|
+
}
|
|
208
|
+
if (normalizedKey.includes("token") ||
|
|
209
|
+
normalizedKey.includes("bearer") ||
|
|
210
|
+
normalizedKey.includes("authorization") ||
|
|
211
|
+
normalizedKey.includes("secret") ||
|
|
212
|
+
normalizedKey.includes("password")) {
|
|
213
|
+
return REDACTED_TOKEN;
|
|
214
|
+
}
|
|
215
|
+
if (normalizedKey === "body" ||
|
|
216
|
+
normalizedKey === "text" ||
|
|
217
|
+
normalizedKey === "content" ||
|
|
218
|
+
normalizedKey.includes("message") ||
|
|
219
|
+
normalizedKey.includes("rawtext") ||
|
|
220
|
+
normalizedKey.includes("rawmessage")) {
|
|
221
|
+
return REDACTED_MESSAGE_TEXT;
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
function redactEmbeddedJsonFragments(text) {
|
|
226
|
+
let output = "";
|
|
227
|
+
let cursor = 0;
|
|
228
|
+
while (cursor < text.length) {
|
|
229
|
+
const fragment = findNextEmbeddedJsonFragment(text, cursor);
|
|
230
|
+
if (!fragment) {
|
|
231
|
+
output += text.slice(cursor);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
output += text.slice(cursor, fragment.start);
|
|
235
|
+
output += fragment.stringify(redactStructuredValue(fragment.parsed));
|
|
236
|
+
cursor = fragment.end;
|
|
237
|
+
}
|
|
238
|
+
return output;
|
|
239
|
+
}
|
|
240
|
+
function findNextEmbeddedJsonFragment(text, startIndex) {
|
|
241
|
+
for (let index = startIndex; index < text.length; index++) {
|
|
242
|
+
const character = text[index];
|
|
243
|
+
if (character !== "{" && character !== "[") {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const end = findBalancedJsonEnd(text, index);
|
|
247
|
+
if (end === -1) {
|
|
248
|
+
const candidate = text.slice(index);
|
|
249
|
+
if (looksLikeBrokenStructuredFragment(candidate)) {
|
|
250
|
+
return {
|
|
251
|
+
start: index,
|
|
252
|
+
end: text.length,
|
|
253
|
+
parsed: REDACTED_CORRUPT_STRUCTURED_CONTENT,
|
|
254
|
+
stringify: stringifyCorruptStructuredFragment,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const candidate = text.slice(index, end);
|
|
260
|
+
const parsed = tryParseJson(candidate);
|
|
261
|
+
if (parsed === undefined) {
|
|
262
|
+
const escapedParsed = tryParseEscapedJsonFragment(candidate);
|
|
263
|
+
if (escapedParsed === undefined) {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
return { start: index, end, parsed: escapedParsed, stringify: stringifyEscapedJsonFragment };
|
|
267
|
+
}
|
|
268
|
+
return { start: index, end, parsed, stringify: stringifyJsonFragment };
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
function findBalancedJsonEnd(text, startIndex) {
|
|
273
|
+
const stack = [];
|
|
274
|
+
let inString = false;
|
|
275
|
+
let escaped = false;
|
|
276
|
+
for (let index = startIndex; index < text.length; index++) {
|
|
277
|
+
const character = text[index];
|
|
278
|
+
if (escaped) {
|
|
279
|
+
escaped = false;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (inString) {
|
|
283
|
+
if (character === "\\") {
|
|
284
|
+
escaped = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (character === '"') {
|
|
288
|
+
inString = false;
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (character === '"') {
|
|
293
|
+
inString = true;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (character === "{") {
|
|
297
|
+
stack.push("}");
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (character === "[") {
|
|
301
|
+
stack.push("]");
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (character === "}" || character === "]") {
|
|
305
|
+
const expected = stack.pop();
|
|
306
|
+
if (expected !== character) {
|
|
307
|
+
return -1;
|
|
308
|
+
}
|
|
309
|
+
if (stack.length === 0) {
|
|
310
|
+
return index + 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return -1;
|
|
315
|
+
}
|
|
316
|
+
function tryParseEscapedJsonFragment(text) {
|
|
317
|
+
if (!text.includes('\\"')) {
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
return tryParseJson(text.replace(/\\"/g, '"').replace(/\\\\/g, "\\"));
|
|
321
|
+
}
|
|
322
|
+
function stringifyJsonFragment(value) {
|
|
323
|
+
return JSON.stringify(value);
|
|
324
|
+
}
|
|
325
|
+
function stringifyEscapedJsonFragment(value) {
|
|
326
|
+
return JSON.stringify(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
327
|
+
}
|
|
328
|
+
function stringifyCorruptStructuredFragment() {
|
|
329
|
+
return REDACTED_CORRUPT_STRUCTURED_CONTENT;
|
|
330
|
+
}
|
|
331
|
+
function looksLikeBrokenStructuredFragment(text) {
|
|
332
|
+
return /"(?:contextToken|wechatAccountId|accountId|userId|fromUserId|toUserId|accessToken|refreshToken|bearerToken|token|cookie|credential|credentials|messageBody|messageText|message|rawText|rawMessage|body|text|content|authorization|secret|password)"\s*:/.test(text);
|
|
333
|
+
}
|
|
334
|
+
function normalizeKey(key) {
|
|
335
|
+
return key.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
336
|
+
}
|
|
337
|
+
function escapeRegExp(value) {
|
|
338
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
339
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { WechatRequestKind } from "./state-paths.js";
|
|
2
|
+
export declare function createRouteKey(input: {
|
|
3
|
+
kind: WechatRequestKind;
|
|
4
|
+
requestID: string;
|
|
5
|
+
scopeKey?: string;
|
|
6
|
+
}): string;
|
|
7
|
+
export declare function normalizeHandle(input: string): string;
|
|
8
|
+
export declare function assertValidHandleInput(input: string): void;
|
|
9
|
+
export declare function createHandle(kind: WechatRequestKind, existingHandles: Iterable<string>): string;
|
|
10
|
+
export declare function createSessionReplyHandle(existingHandles: Iterable<string>): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
const HANDLE_PREFIX = {
|
|
3
|
+
question: "q",
|
|
4
|
+
permission: "p",
|
|
5
|
+
};
|
|
6
|
+
const SESSION_REPLY_HANDLE_PREFIX = "s";
|
|
7
|
+
function normalizeRequestID(requestID) {
|
|
8
|
+
return requestID.trim().toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
export function createRouteKey(input) {
|
|
11
|
+
const normalized = normalizeRequestID(input.requestID);
|
|
12
|
+
const normalizedScope = typeof input.scopeKey === "string" ? input.scopeKey.trim().toLowerCase() : "";
|
|
13
|
+
const digest = crypto
|
|
14
|
+
.createHash("sha1")
|
|
15
|
+
.update(`${input.kind}:${normalized}:${normalizedScope}`)
|
|
16
|
+
.digest("hex")
|
|
17
|
+
.slice(0, 12);
|
|
18
|
+
return `${input.kind}-${digest}`;
|
|
19
|
+
}
|
|
20
|
+
export function normalizeHandle(input) {
|
|
21
|
+
const value = input.trim().toLowerCase();
|
|
22
|
+
if (!/^[a-z][a-z0-9]*$/.test(value)) {
|
|
23
|
+
throw new Error("invalid handle format");
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function isRawRequestIDLike(input) {
|
|
28
|
+
return /^req(?:uest)?[-_]/i.test(input.trim());
|
|
29
|
+
}
|
|
30
|
+
export function assertValidHandleInput(input) {
|
|
31
|
+
if (isRawRequestIDLike(input)) {
|
|
32
|
+
throw new Error("raw requestID cannot be used as handle");
|
|
33
|
+
}
|
|
34
|
+
normalizeHandle(input);
|
|
35
|
+
}
|
|
36
|
+
export function createHandle(kind, existingHandles) {
|
|
37
|
+
return createPrefixedHandle(HANDLE_PREFIX[kind], existingHandles);
|
|
38
|
+
}
|
|
39
|
+
export function createSessionReplyHandle(existingHandles) {
|
|
40
|
+
return createPrefixedHandle(SESSION_REPLY_HANDLE_PREFIX, existingHandles);
|
|
41
|
+
}
|
|
42
|
+
function createPrefixedHandle(prefix, existingHandles) {
|
|
43
|
+
const seen = new Set();
|
|
44
|
+
for (const item of existingHandles) {
|
|
45
|
+
try {
|
|
46
|
+
seen.add(normalizeHandle(item));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// ignore invalid historical values
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let index = 1;
|
|
53
|
+
while (seen.has(`${prefix}${index}`)) {
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
return `${prefix}${index}`;
|
|
57
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { BrokerMessageType } from "./protocol.js";
|
|
2
|
+
export declare function createSessionToken(): string;
|
|
3
|
+
export declare function registerConnection(instanceID: string, connectionRef: unknown): string;
|
|
4
|
+
export declare function validateSessionToken(instanceID: string, token: string): boolean;
|
|
5
|
+
export declare function revokeSessionToken(instanceID: string): void;
|
|
6
|
+
export declare function isAuthRequired(type: BrokerMessageType): boolean;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const sessionByInstanceID = new Map();
|
|
3
|
+
const TOKEN_FREE_TYPES = ["registerInstance", "ping"];
|
|
4
|
+
function isNonEmptyString(value) {
|
|
5
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
6
|
+
}
|
|
7
|
+
export function createSessionToken() {
|
|
8
|
+
return randomUUID();
|
|
9
|
+
}
|
|
10
|
+
export function registerConnection(instanceID, connectionRef) {
|
|
11
|
+
if (!isNonEmptyString(instanceID)) {
|
|
12
|
+
throw new Error("invalid instanceID");
|
|
13
|
+
}
|
|
14
|
+
const token = createSessionToken();
|
|
15
|
+
sessionByInstanceID.set(instanceID, { token, connectionRef });
|
|
16
|
+
return token;
|
|
17
|
+
}
|
|
18
|
+
export function validateSessionToken(instanceID, token) {
|
|
19
|
+
if (!isNonEmptyString(instanceID) || !isNonEmptyString(token)) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const current = sessionByInstanceID.get(instanceID);
|
|
23
|
+
if (!current) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return current.token === token;
|
|
27
|
+
}
|
|
28
|
+
export function revokeSessionToken(instanceID) {
|
|
29
|
+
if (!isNonEmptyString(instanceID)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
sessionByInstanceID.delete(instanceID);
|
|
33
|
+
}
|
|
34
|
+
export function isAuthRequired(type) {
|
|
35
|
+
if (TOKEN_FREE_TYPES.includes(type)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type WechatLatestAccountState = {
|
|
2
|
+
accountId: string;
|
|
3
|
+
token: string;
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
getUpdatesBuf?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function readWechatLatestAccountState(): Promise<WechatLatestAccountState | null>;
|
|
8
|
+
export declare function writeWechatLatestAccountState(input: WechatLatestAccountState): Promise<WechatLatestAccountState>;
|