@xmanrui/dsh-im 4.24.0 → 4.25.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/README.en.md +11 -3
- package/README.md +11 -3
- package/THIRD_PARTY_NOTICES.md +2 -0
- package/lib/client.js +965 -607
- package/lib/index.js +296 -287
- package/package.json +8 -2
- package/plugin-src/client/channel-card-meta.js +3 -9
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/index.js +9 -8
- package/plugin-src/client/channels/feishu/index.js +14 -13
- package/plugin-src/client/channels/matrix/api.js +11 -0
- package/plugin-src/client/channels/matrix/index.js +151 -0
- package/plugin-src/client/channels/matrix/styles.js +36 -0
- package/plugin-src/client/channels/qq/index.js +9 -8
- package/plugin-src/client/channels/shared/collapsible-account.js +49 -26
- package/plugin-src/client/channels/shared/token-channel.js +9 -8
- package/plugin-src/client/channels/wecom/index.js +9 -8
- package/plugin-src/client/channels/wecom-app/index.js +9 -8
- package/plugin-src/client/channels/weixin/index.js +9 -8
- package/plugin-src/client/channels/whatsapp/index.js +9 -8
- package/plugin-src/client/i18n.js +21 -0
- package/plugin-src/client/index.js +20 -0
- package/plugin-src/client/session-channel-logos.js +2 -1
- package/plugin-src/client/styles.js +15 -9
- package/plugin-src/client/update-panel.js +42 -18
- package/plugin-src/host/channels/matrix/index.mjs +31 -0
- package/plugin-src/host/channels/matrix/production.mjs +227 -0
- package/plugin-src/host/channels/matrix/rpc.mjs +228 -0
- package/plugin-src/host/channels/shared/access-policy-production.mjs +1 -1
- package/plugin-src/host/channels/shared/startup-error.mjs +2 -1
- package/plugin-src/host/delivery-adapter.mjs +18 -0
- package/plugin-src/host/delivery-suggestions.mjs +13 -0
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/host/update-service.mjs +19 -14
- package/scripts/verify-lan-management.mjs +1 -1
- package/scripts/verify-package.mjs +5 -1
- package/src/channels/feishu/bridge.mjs +91 -42
- package/src/channels/matrix/matrix-api.mjs +696 -0
- package/src/channels/matrix/matrix-bridge.mjs +20 -0
- package/src/channels/matrix/matrix-config-store.mjs +356 -0
- package/src/channels/matrix/matrix-controller.mjs +404 -0
- package/src/channels/matrix/matrix-crypto-store.mjs +279 -0
- package/src/channels/matrix/matrix-crypto.mjs +1014 -0
- package/src/channels/matrix/matrix-harness-client.mjs +11 -0
- package/src/channels/matrix/matrix-normalize.mjs +357 -0
- package/src/channels/matrix/matrix-rich-text.mjs +313 -0
- package/src/channels/matrix/matrix-runtime.mjs +900 -0
- package/src/channels/shared/command-catalog.mjs +1 -1
- package/src/channels/shared/i18n-en/matrix.mjs +75 -0
- package/src/channels/shared/i18n-en.mjs +2 -0
- package/src/channels/shared/session-channel-labels.mjs +1 -0
- package/src/channels/shared/text-harness-bridge.mjs +3 -0
- package/src/channels/shared/workspace-session.mjs +7 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createTextBridgeStatus, TextHarnessBridge } from '../shared/text-harness-bridge.mjs';
|
|
2
|
+
|
|
3
|
+
export const MATRIX_DESCRIPTOR = Object.freeze({
|
|
4
|
+
key: 'matrix',
|
|
5
|
+
label: 'Matrix',
|
|
6
|
+
connectionLabel: ' CS API 长轮询',
|
|
7
|
+
reactions: Object.freeze({
|
|
8
|
+
processing: '👀',
|
|
9
|
+
success: '✔',
|
|
10
|
+
error: '✕',
|
|
11
|
+
}),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export class MatrixHarnessBridge extends TextHarnessBridge {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
super({ descriptor: MATRIX_DESCRIPTOR, ...options });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { createTextBridgeStatus as createMatrixBridgeStatus };
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { isMatrixRoomId, isMatrixUserId, validateMatrixHomeserver } from './matrix-api.mjs';
|
|
6
|
+
|
|
7
|
+
const EMPTY_DOCUMENT = Object.freeze({ version: 1, bots: Object.freeze([]) });
|
|
8
|
+
const BOT_ID_PATTERN = /^matrix_[a-f0-9]{24}$/;
|
|
9
|
+
const TOKEN_REF_PATTERN = /^DSH_MATRIX_TOKEN_[A-F0-9]{24}$/;
|
|
10
|
+
const PASSWORD_REF_PATTERN = /^DSH_MATRIX_PASSWORD_[A-F0-9]{24}$/;
|
|
11
|
+
const DEVICE_ID_PATTERN = /^[A-Za-z0-9._=-]{1,128}$/;
|
|
12
|
+
const E2EE_MODES = new Set(['off', 'optional', 'required']);
|
|
13
|
+
const AUTO_JOIN_MODES = new Set(['authorized', 'all']);
|
|
14
|
+
const MAX_MESSAGE_LENGTH_BOUNDS = Object.freeze({ min: 500, max: 65_535 });
|
|
15
|
+
const MAX_MEDIA_BYTES_CEILING = 104_857_600;
|
|
16
|
+
|
|
17
|
+
function cleanString(value) {
|
|
18
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function boundedInteger(value, { min, max }) {
|
|
22
|
+
if (value === undefined || value === null) return undefined;
|
|
23
|
+
const numeric = Number(value);
|
|
24
|
+
if (!Number.isSafeInteger(numeric) || numeric < min || numeric > max) return null;
|
|
25
|
+
return numeric;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function roomIdList(value) {
|
|
29
|
+
if (value === undefined || value === null) return undefined;
|
|
30
|
+
if (!Array.isArray(value)) return null;
|
|
31
|
+
const list = [];
|
|
32
|
+
for (const entry of value) {
|
|
33
|
+
const clean = cleanString(entry);
|
|
34
|
+
if (!clean || !isMatrixRoomId(clean)) return null;
|
|
35
|
+
if (!list.includes(clean)) list.push(clean);
|
|
36
|
+
}
|
|
37
|
+
return list;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function patternList(value) {
|
|
41
|
+
if (value === undefined || value === null) return undefined;
|
|
42
|
+
if (!Array.isArray(value)) return null;
|
|
43
|
+
const list = [];
|
|
44
|
+
for (const entry of value) {
|
|
45
|
+
const clean = cleanString(entry);
|
|
46
|
+
if (!clean || clean.length > 512) return null;
|
|
47
|
+
try {
|
|
48
|
+
new RegExp(clean);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (!list.includes(clean)) list.push(clean);
|
|
53
|
+
}
|
|
54
|
+
return list;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function deriveMatrixBotIdentity({ homeserver, userId } = {}) {
|
|
58
|
+
const base = validateMatrixHomeserver(homeserver);
|
|
59
|
+
const identifier = cleanString(userId);
|
|
60
|
+
if (!base || !isMatrixUserId(identifier ?? '')) {
|
|
61
|
+
throw new TypeError('Matrix bot identity requires a valid homeserver and @user:server id');
|
|
62
|
+
}
|
|
63
|
+
const host = new URL(base).host.toLowerCase();
|
|
64
|
+
const digest = createHash('sha256').update(`${host}|${identifier.toLowerCase()}`).digest('hex').slice(0, 24);
|
|
65
|
+
const suffix = digest.toUpperCase();
|
|
66
|
+
return {
|
|
67
|
+
botId: `matrix_${digest}`,
|
|
68
|
+
tokenRef: `DSH_MATRIX_TOKEN_${suffix}`,
|
|
69
|
+
passwordRef: `DSH_MATRIX_PASSWORD_${suffix}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function maskMatrixBotId(platformId) {
|
|
74
|
+
const value = cleanString(platformId) ?? '';
|
|
75
|
+
const [host, userId] = value.split('|');
|
|
76
|
+
if (host && userId) return `${host.slice(0, 6)}••• · ${userId.slice(0, 8)}•••`;
|
|
77
|
+
return value ? `${value.slice(0, 6)}••••` : 'Matrix 机器人';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class MatrixConfigStore {
|
|
81
|
+
#path;
|
|
82
|
+
#value = EMPTY_DOCUMENT;
|
|
83
|
+
#writeQueue = Promise.resolve();
|
|
84
|
+
|
|
85
|
+
constructor(path) {
|
|
86
|
+
this.#path = path;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async load() {
|
|
90
|
+
try {
|
|
91
|
+
const normalized = this.#normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
|
|
92
|
+
if (!normalized) throw new Error('dsh-im Matrix config contains invalid bot data');
|
|
93
|
+
this.#value = normalized;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
96
|
+
this.#value = EMPTY_DOCUMENT;
|
|
97
|
+
}
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
list() {
|
|
102
|
+
return structuredClone(this.#value.bots);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
get(botId) {
|
|
106
|
+
const bot = this.#value.bots.find((candidate) => candidate.botId === botId);
|
|
107
|
+
return bot ? structuredClone(bot) : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getByPlatformId(platformId) {
|
|
111
|
+
const bot = this.#value.bots.find((candidate) => candidate.platformId === platformId);
|
|
112
|
+
return bot ? structuredClone(bot) : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async save(value) {
|
|
116
|
+
const normalized = this.#normalizeBot(value);
|
|
117
|
+
if (!normalized) throw new Error('Refusing to persist incomplete Matrix bot data');
|
|
118
|
+
return this.#mutate((bots) => {
|
|
119
|
+
const collision = bots.find((bot) => (
|
|
120
|
+
bot.botId !== normalized.botId
|
|
121
|
+
&& (bot.platformId === normalized.platformId
|
|
122
|
+
|| bot.tokenRef === normalized.tokenRef
|
|
123
|
+
|| bot.passwordRef === normalized.passwordRef)
|
|
124
|
+
));
|
|
125
|
+
if (collision) throw new Error('Duplicate Matrix bot identity');
|
|
126
|
+
const index = bots.findIndex((bot) => bot.botId === normalized.botId);
|
|
127
|
+
if (index === -1) bots.push(normalized);
|
|
128
|
+
else bots[index] = normalized;
|
|
129
|
+
return structuredClone(normalized);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async remove(botId) {
|
|
134
|
+
if (!BOT_ID_PATTERN.test(botId)) throw new TypeError('Invalid Matrix bot id');
|
|
135
|
+
return this.#mutate((bots) => {
|
|
136
|
+
const index = bots.findIndex((bot) => bot.botId === botId);
|
|
137
|
+
if (index === -1) return null;
|
|
138
|
+
return structuredClone(bots.splice(index, 1)[0]);
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async clear() {
|
|
143
|
+
const operation = this.#writeQueue.then(async () => {
|
|
144
|
+
try {
|
|
145
|
+
await unlink(this.#path);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
148
|
+
}
|
|
149
|
+
this.#value = EMPTY_DOCUMENT;
|
|
150
|
+
});
|
|
151
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
152
|
+
await operation;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
#normalizeBot(value) {
|
|
156
|
+
if (!value || typeof value !== 'object') return null;
|
|
157
|
+
const botId = cleanString(value.botId);
|
|
158
|
+
const platformId = cleanString(value.platformId);
|
|
159
|
+
const homeserver = validateMatrixHomeserver(value.homeserver);
|
|
160
|
+
const userId = cleanString(value.userId);
|
|
161
|
+
const tokenRef = cleanString(value.tokenRef);
|
|
162
|
+
const passwordRef = cleanString(value.passwordRef);
|
|
163
|
+
const name = cleanString(value.name);
|
|
164
|
+
const deviceId = cleanString(value.deviceId);
|
|
165
|
+
if (!botId || !platformId || !homeserver || !isMatrixUserId(userId ?? '')
|
|
166
|
+
|| !tokenRef || !passwordRef || !name
|
|
167
|
+
|| !BOT_ID_PATTERN.test(botId)
|
|
168
|
+
|| !TOKEN_REF_PATTERN.test(tokenRef)
|
|
169
|
+
|| !PASSWORD_REF_PATTERN.test(passwordRef)
|
|
170
|
+
|| (deviceId !== null && !DEVICE_ID_PATTERN.test(deviceId))) return null;
|
|
171
|
+
const derived = deriveMatrixBotIdentity({ homeserver, userId });
|
|
172
|
+
if (derived.botId !== botId || derived.tokenRef !== tokenRef || derived.passwordRef !== passwordRef) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const e2eeMode = cleanString(value.e2eeMode) ?? 'optional';
|
|
176
|
+
const autoJoinInvites = cleanString(value.autoJoinInvites) ?? 'authorized';
|
|
177
|
+
const maxMessageLength = boundedInteger(value.maxMessageLength, MAX_MESSAGE_LENGTH_BOUNDS);
|
|
178
|
+
const maxMediaBytes = boundedInteger(value.maxMediaBytes, { min: 1, max: MAX_MEDIA_BYTES_CEILING });
|
|
179
|
+
const allowedRooms = roomIdList(value.allowedRooms);
|
|
180
|
+
const freeResponseRooms = roomIdList(value.freeResponseRooms);
|
|
181
|
+
const ignoreUserPatterns = patternList(value.ignoreUserPatterns);
|
|
182
|
+
if (!E2EE_MODES.has(e2eeMode) || !AUTO_JOIN_MODES.has(autoJoinInvites)
|
|
183
|
+
|| maxMessageLength === null || maxMediaBytes === null
|
|
184
|
+
|| allowedRooms === null || freeResponseRooms === null || ignoreUserPatterns === null) return null;
|
|
185
|
+
for (const key of ['requireMention', 'processNotices', 'allowRoomMentions', 'reactions']) {
|
|
186
|
+
if (value?.[key] !== undefined && typeof value[key] !== 'boolean') return null;
|
|
187
|
+
}
|
|
188
|
+
return Object.freeze({
|
|
189
|
+
botId,
|
|
190
|
+
platformId,
|
|
191
|
+
homeserver,
|
|
192
|
+
userId,
|
|
193
|
+
...(deviceId ? { deviceId } : {}),
|
|
194
|
+
tokenRef,
|
|
195
|
+
passwordRef,
|
|
196
|
+
name,
|
|
197
|
+
username: cleanString(value.username),
|
|
198
|
+
createdAt: cleanString(value.createdAt) ?? new Date().toISOString(),
|
|
199
|
+
connectedAt: cleanString(value.connectedAt),
|
|
200
|
+
...(e2eeMode !== 'optional' ? { e2eeMode } : {}),
|
|
201
|
+
...(autoJoinInvites !== 'authorized' ? { autoJoinInvites } : {}),
|
|
202
|
+
...(typeof value.requireMention === 'boolean' ? { requireMention: value.requireMention } : {}),
|
|
203
|
+
...(typeof value.processNotices === 'boolean' ? { processNotices: value.processNotices } : {}),
|
|
204
|
+
...(typeof value.allowRoomMentions === 'boolean' ? { allowRoomMentions: value.allowRoomMentions } : {}),
|
|
205
|
+
...(typeof value.reactions === 'boolean' ? { reactions: value.reactions } : {}),
|
|
206
|
+
...(maxMessageLength !== undefined ? { maxMessageLength } : {}),
|
|
207
|
+
...(maxMediaBytes !== undefined ? { maxMediaBytes } : {}),
|
|
208
|
+
...(allowedRooms !== undefined ? { allowedRooms: Object.freeze(allowedRooms) } : {}),
|
|
209
|
+
...(freeResponseRooms !== undefined ? { freeResponseRooms: Object.freeze(freeResponseRooms) } : {}),
|
|
210
|
+
...(ignoreUserPatterns !== undefined ? { ignoreUserPatterns: Object.freeze(ignoreUserPatterns) } : {}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
#normalizeDocument(value) {
|
|
215
|
+
if (!value || value.version !== 1 || !Array.isArray(value.bots)) return null;
|
|
216
|
+
const bots = value.bots.map((bot) => this.#normalizeBot(bot));
|
|
217
|
+
if (bots.some((bot) => bot === null)) return null;
|
|
218
|
+
const ids = new Set();
|
|
219
|
+
const platformIds = new Set();
|
|
220
|
+
const refs = new Set();
|
|
221
|
+
for (const bot of bots) {
|
|
222
|
+
if (ids.has(bot.botId) || platformIds.has(bot.platformId)
|
|
223
|
+
|| refs.has(bot.tokenRef) || refs.has(bot.passwordRef)) return null;
|
|
224
|
+
ids.add(bot.botId);
|
|
225
|
+
platformIds.add(bot.platformId);
|
|
226
|
+
refs.add(bot.tokenRef);
|
|
227
|
+
refs.add(bot.passwordRef);
|
|
228
|
+
}
|
|
229
|
+
return Object.freeze({ version: 1, bots: Object.freeze(bots) });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async #mutate(mutator) {
|
|
233
|
+
let result;
|
|
234
|
+
const operation = this.#writeQueue.then(async () => {
|
|
235
|
+
const bots = [...this.#value.bots];
|
|
236
|
+
result = mutator(bots);
|
|
237
|
+
const document = Object.freeze({ version: 1, bots: Object.freeze(bots) });
|
|
238
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
239
|
+
const temporary = `${this.#path}.tmp`;
|
|
240
|
+
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
|
241
|
+
encoding: 'utf8', mode: 0o600,
|
|
242
|
+
});
|
|
243
|
+
await rename(temporary, this.#path);
|
|
244
|
+
this.#value = document;
|
|
245
|
+
});
|
|
246
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
247
|
+
await operation;
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Matrix-only durable side data kept next to the shared conversation state
|
|
254
|
+
* file: the sync token, joined rooms, DM rooms and permanently declined dead
|
|
255
|
+
* invites. Sessions, seen-message rings and deferred delivery stay in the
|
|
256
|
+
* shared `ConversationStateStore`.
|
|
257
|
+
*/
|
|
258
|
+
export class MatrixSidecarStore {
|
|
259
|
+
#path;
|
|
260
|
+
#value = { nextBatch: null, joinedRooms: [], dmRooms: [], declinedRooms: [], dmRoomByUser: {} };
|
|
261
|
+
#writeQueue = Promise.resolve();
|
|
262
|
+
|
|
263
|
+
constructor(path) {
|
|
264
|
+
this.#path = path;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async load() {
|
|
268
|
+
try {
|
|
269
|
+
const parsed = JSON.parse(await readFile(this.#path, 'utf8'));
|
|
270
|
+
this.#value = {
|
|
271
|
+
nextBatch: typeof parsed?.nextBatch === 'string' && parsed.nextBatch ? parsed.nextBatch : null,
|
|
272
|
+
joinedRooms: stringList(parsed?.joinedRooms),
|
|
273
|
+
dmRooms: stringList(parsed?.dmRooms),
|
|
274
|
+
declinedRooms: stringList(parsed?.declinedRooms),
|
|
275
|
+
dmRoomByUser: stringMap(parsed?.dmRoomByUser),
|
|
276
|
+
};
|
|
277
|
+
} catch (error) {
|
|
278
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
279
|
+
}
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
nextBatch() {
|
|
284
|
+
return this.#value.nextBatch;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
joinedRooms() {
|
|
288
|
+
return [...this.#value.joinedRooms];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
dmRooms() {
|
|
292
|
+
return [...this.#value.dmRooms];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
dmRoomByUser() {
|
|
296
|
+
return { ...this.#value.dmRoomByUser };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
declinedRooms() {
|
|
300
|
+
return [...this.#value.declinedRooms];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
isDeclined(roomId) {
|
|
304
|
+
return this.#value.declinedRooms.includes(roomId);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async apply(patch) {
|
|
308
|
+
const next = {
|
|
309
|
+
nextBatch: patch.nextBatch === undefined ? this.#value.nextBatch
|
|
310
|
+
: (typeof patch.nextBatch === 'string' && patch.nextBatch ? patch.nextBatch : null),
|
|
311
|
+
joinedRooms: patch.joinedRooms === undefined
|
|
312
|
+
? this.#value.joinedRooms : stringList(patch.joinedRooms),
|
|
313
|
+
dmRooms: patch.dmRooms === undefined ? this.#value.dmRooms : stringList(patch.dmRooms),
|
|
314
|
+
declinedRooms: patch.declinedRooms === undefined
|
|
315
|
+
? this.#value.declinedRooms : stringList(patch.declinedRooms),
|
|
316
|
+
dmRoomByUser: patch.dmRoomByUser === undefined
|
|
317
|
+
? this.#value.dmRoomByUser : stringMap(patch.dmRoomByUser),
|
|
318
|
+
};
|
|
319
|
+
const operation = this.#writeQueue.then(async () => {
|
|
320
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
321
|
+
const temporary = `${this.#path}.tmp`;
|
|
322
|
+
await writeFile(temporary, `${JSON.stringify(next, null, 2)}\n`, {
|
|
323
|
+
encoding: 'utf8', mode: 0o600,
|
|
324
|
+
});
|
|
325
|
+
await rename(temporary, this.#path);
|
|
326
|
+
this.#value = next;
|
|
327
|
+
});
|
|
328
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
329
|
+
await operation;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async remove() {
|
|
333
|
+
const operation = this.#writeQueue.then(async () => {
|
|
334
|
+
try {
|
|
335
|
+
await unlink(this.#path);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
this.#writeQueue = operation.then(() => undefined, () => undefined);
|
|
341
|
+
await operation;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function stringList(value) {
|
|
346
|
+
return Array.isArray(value)
|
|
347
|
+
? Object.freeze([...new Set(value.filter((entry) => typeof entry === 'string' && entry.trim()))])
|
|
348
|
+
: [];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function stringMap(value) {
|
|
352
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return Object.freeze({});
|
|
353
|
+
const entries = Object.entries(value)
|
|
354
|
+
.filter(([key, target]) => typeof key === 'string' && key && typeof target === 'string' && target);
|
|
355
|
+
return Object.freeze(Object.fromEntries(entries));
|
|
356
|
+
}
|