@archipelagolab/lobi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -0
- package/ENDOFFILE +0 -0
- package/EOF +0 -0
- package/LICENSE +21 -0
- package/SPEC-SUPPORT.md +116 -0
- package/YAMLEND +0 -0
- package/api.ts +18 -0
- package/archipelagolab-lobi-1.0.0.tgz +0 -0
- package/auth-presence.ts +56 -0
- package/channel-plugin-api.ts +3 -0
- package/cli-metadata.ts +11 -0
- package/contract-api.ts +17 -0
- package/docs/CHECKLIST.md +83 -0
- package/docs/FORK_SDK_GUIDE.md +279 -0
- package/helper-api.ts +3 -0
- package/index.test.ts +61 -0
- package/index.ts +65 -0
- package/openclaw.plugin.json +23 -0
- package/package.json +52 -0
- package/plugin-entry.handlers.runtime.ts +1 -0
- package/runtime-api.ts +54 -0
- package/runtime-heavy-api.ts +1 -0
- package/scripts/migrate-to-lobi.sh +72 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/src/account-selection.test.ts +124 -0
- package/src/account-selection.ts +226 -0
- package/src/actions.account-propagation.test.ts +251 -0
- package/src/actions.test.ts +251 -0
- package/src/actions.ts +336 -0
- package/src/approval-auth.test.ts +23 -0
- package/src/approval-auth.ts +25 -0
- package/src/approval-handler.runtime.test.ts +46 -0
- package/src/approval-handler.runtime.ts +400 -0
- package/src/approval-ids.ts +6 -0
- package/src/approval-native.test.ts +329 -0
- package/src/approval-native.ts +336 -0
- package/src/approval-reactions.test.ts +107 -0
- package/src/approval-reactions.ts +158 -0
- package/src/auth-precedence.ts +61 -0
- package/src/channel-account-paths.ts +92 -0
- package/src/channel.account-paths.test.ts +102 -0
- package/src/channel.directory.test.ts +601 -0
- package/src/channel.resolve.test.ts +38 -0
- package/src/channel.runtime.ts +16 -0
- package/src/channel.setup.test.ts +269 -0
- package/src/channel.ts +570 -0
- package/src/cli-metadata.ts +19 -0
- package/src/cli.test.ts +1015 -0
- package/src/cli.ts +1198 -0
- package/src/config-adapter.ts +41 -0
- package/src/config-schema.test.ts +90 -0
- package/src/config-schema.ts +114 -0
- package/src/directory-live.test.ts +200 -0
- package/src/directory-live.ts +238 -0
- package/src/doctor-contract.ts +287 -0
- package/src/doctor.test.ts +440 -0
- package/src/doctor.ts +262 -0
- package/src/env-vars.ts +92 -0
- package/src/exec-approval-resolver.test.ts +68 -0
- package/src/exec-approval-resolver.ts +23 -0
- package/src/exec-approvals.test.ts +483 -0
- package/src/exec-approvals.ts +290 -0
- package/src/group-mentions.ts +41 -0
- package/src/legacy-crypto-inspector-availability.test.ts +81 -0
- package/src/legacy-crypto-inspector-availability.ts +60 -0
- package/src/legacy-crypto.test.ts +234 -0
- package/src/legacy-crypto.ts +549 -0
- package/src/legacy-state.test.ts +86 -0
- package/src/legacy-state.ts +156 -0
- package/src/matrix/account-config.ts +150 -0
- package/src/matrix/accounts.readiness.test.ts +27 -0
- package/src/matrix/accounts.test.ts +757 -0
- package/src/matrix/accounts.ts +194 -0
- package/src/matrix/actions/client.test.ts +215 -0
- package/src/matrix/actions/client.ts +31 -0
- package/src/matrix/actions/devices.test.ts +114 -0
- package/src/matrix/actions/devices.ts +34 -0
- package/src/matrix/actions/limits.test.ts +15 -0
- package/src/matrix/actions/limits.ts +6 -0
- package/src/matrix/actions/messages.test.ts +289 -0
- package/src/matrix/actions/messages.ts +123 -0
- package/src/matrix/actions/pins.test.ts +74 -0
- package/src/matrix/actions/pins.ts +64 -0
- package/src/matrix/actions/polls.test.ts +71 -0
- package/src/matrix/actions/polls.ts +109 -0
- package/src/matrix/actions/profile.test.ts +109 -0
- package/src/matrix/actions/profile.ts +37 -0
- package/src/matrix/actions/reactions.test.ts +135 -0
- package/src/matrix/actions/reactions.ts +59 -0
- package/src/matrix/actions/room.test.ts +79 -0
- package/src/matrix/actions/room.ts +71 -0
- package/src/matrix/actions/summary.test.ts +87 -0
- package/src/matrix/actions/summary.ts +88 -0
- package/src/matrix/actions/types.ts +82 -0
- package/src/matrix/actions/verification.test.ts +105 -0
- package/src/matrix/actions/verification.ts +237 -0
- package/src/matrix/actions.ts +37 -0
- package/src/matrix/active-client.ts +26 -0
- package/src/matrix/async-lock.ts +18 -0
- package/src/matrix/backup-health.ts +115 -0
- package/src/matrix/client/config-runtime-api.ts +14 -0
- package/src/matrix/client/config-secret-input.runtime.ts +1 -0
- package/src/matrix/client/config.ts +982 -0
- package/src/matrix/client/create-client.test.ts +115 -0
- package/src/matrix/client/create-client.ts +101 -0
- package/src/matrix/client/env-auth.ts +6 -0
- package/src/matrix/client/file-sync-store.test.ts +265 -0
- package/src/matrix/client/file-sync-store.ts +289 -0
- package/src/matrix/client/logging.ts +123 -0
- package/src/matrix/client/migration-snapshot.runtime.ts +1 -0
- package/src/matrix/client/private-network-host.ts +56 -0
- package/src/matrix/client/runtime.ts +4 -0
- package/src/matrix/client/shared.test.ts +344 -0
- package/src/matrix/client/shared.ts +306 -0
- package/src/matrix/client/storage.test.ts +634 -0
- package/src/matrix/client/storage.ts +544 -0
- package/src/matrix/client/types.ts +50 -0
- package/src/matrix/client-bootstrap.test.ts +84 -0
- package/src/matrix/client-bootstrap.ts +164 -0
- package/src/matrix/client-resolver.test-helpers.ts +147 -0
- package/src/matrix/client.test.ts +1521 -0
- package/src/matrix/client.ts +23 -0
- package/src/matrix/config-paths.ts +31 -0
- package/src/matrix/config-update.test.ts +237 -0
- package/src/matrix/config-update.ts +291 -0
- package/src/matrix/credentials-read.ts +206 -0
- package/src/matrix/credentials-write.runtime.ts +26 -0
- package/src/matrix/credentials.test.ts +501 -0
- package/src/matrix/credentials.ts +95 -0
- package/src/matrix/deps.test.ts +74 -0
- package/src/matrix/deps.ts +225 -0
- package/src/matrix/device-health.test.ts +45 -0
- package/src/matrix/device-health.ts +31 -0
- package/src/matrix/direct-management.test.ts +350 -0
- package/src/matrix/direct-management.ts +347 -0
- package/src/matrix/direct-room.test.ts +61 -0
- package/src/matrix/direct-room.ts +128 -0
- package/src/matrix/draft-stream.test.ts +406 -0
- package/src/matrix/draft-stream.ts +216 -0
- package/src/matrix/encryption-guidance.ts +27 -0
- package/src/matrix/errors.ts +21 -0
- package/src/matrix/format.test.ts +340 -0
- package/src/matrix/format.ts +428 -0
- package/src/matrix/legacy-crypto-inspector.ts +95 -0
- package/src/matrix/media-errors.ts +20 -0
- package/src/matrix/media-text.ts +169 -0
- package/src/matrix/monitor/access-state.test.ts +45 -0
- package/src/matrix/monitor/access-state.ts +77 -0
- package/src/matrix/monitor/ack-config.test.ts +57 -0
- package/src/matrix/monitor/ack-config.ts +26 -0
- package/src/matrix/monitor/allowlist.test.ts +45 -0
- package/src/matrix/monitor/allowlist.ts +94 -0
- package/src/matrix/monitor/auto-join.test.ts +203 -0
- package/src/matrix/monitor/auto-join.ts +86 -0
- package/src/matrix/monitor/config.test.ts +197 -0
- package/src/matrix/monitor/config.ts +303 -0
- package/src/matrix/monitor/context-summary.ts +43 -0
- package/src/matrix/monitor/direct.test.ts +529 -0
- package/src/matrix/monitor/direct.ts +270 -0
- package/src/matrix/monitor/events.test.ts +1524 -0
- package/src/matrix/monitor/events.ts +213 -0
- package/src/matrix/monitor/handler.body-for-agent.test.ts +396 -0
- package/src/matrix/monitor/handler.group-history.test.ts +648 -0
- package/src/matrix/monitor/handler.media-failure.test.ts +267 -0
- package/src/matrix/monitor/handler.test-helpers.ts +308 -0
- package/src/matrix/monitor/handler.test.ts +2952 -0
- package/src/matrix/monitor/handler.thread-root-media.test.ts +82 -0
- package/src/matrix/monitor/handler.ts +1679 -0
- package/src/matrix/monitor/inbound-dedupe.test.ts +146 -0
- package/src/matrix/monitor/inbound-dedupe.ts +267 -0
- package/src/matrix/monitor/index.test.ts +920 -0
- package/src/matrix/monitor/index.ts +434 -0
- package/src/matrix/monitor/legacy-crypto-restore.test.ts +206 -0
- package/src/matrix/monitor/legacy-crypto-restore.ts +139 -0
- package/src/matrix/monitor/location.ts +100 -0
- package/src/matrix/monitor/media.test.ts +159 -0
- package/src/matrix/monitor/media.ts +119 -0
- package/src/matrix/monitor/mentions.test.ts +289 -0
- package/src/matrix/monitor/mentions.ts +177 -0
- package/src/matrix/monitor/reaction-events.test.ts +326 -0
- package/src/matrix/monitor/reaction-events.ts +187 -0
- package/src/matrix/monitor/recent-invite.test.ts +92 -0
- package/src/matrix/monitor/recent-invite.ts +30 -0
- package/src/matrix/monitor/replies.test.ts +265 -0
- package/src/matrix/monitor/replies.ts +136 -0
- package/src/matrix/monitor/reply-context.test.ts +276 -0
- package/src/matrix/monitor/reply-context.ts +92 -0
- package/src/matrix/monitor/room-history.test.ts +258 -0
- package/src/matrix/monitor/room-history.ts +301 -0
- package/src/matrix/monitor/room-info.test.ts +201 -0
- package/src/matrix/monitor/room-info.ts +126 -0
- package/src/matrix/monitor/rooms.test.ts +121 -0
- package/src/matrix/monitor/rooms.ts +52 -0
- package/src/matrix/monitor/route.test.ts +255 -0
- package/src/matrix/monitor/route.ts +178 -0
- package/src/matrix/monitor/runtime-api.ts +31 -0
- package/src/matrix/monitor/startup-verification.test.ts +294 -0
- package/src/matrix/monitor/startup-verification.ts +237 -0
- package/src/matrix/monitor/startup.test.ts +257 -0
- package/src/matrix/monitor/startup.ts +218 -0
- package/src/matrix/monitor/status.ts +111 -0
- package/src/matrix/monitor/sync-lifecycle.test.ts +224 -0
- package/src/matrix/monitor/sync-lifecycle.ts +91 -0
- package/src/matrix/monitor/task-runner.ts +38 -0
- package/src/matrix/monitor/thread-context.test.ts +149 -0
- package/src/matrix/monitor/thread-context.ts +108 -0
- package/src/matrix/monitor/threads.test.ts +68 -0
- package/src/matrix/monitor/threads.ts +85 -0
- package/src/matrix/monitor/types.ts +30 -0
- package/src/matrix/monitor/verification-events.ts +627 -0
- package/src/matrix/monitor/verification-utils.test.ts +47 -0
- package/src/matrix/monitor/verification-utils.ts +46 -0
- package/src/matrix/outbound-media-runtime.ts +1 -0
- package/src/matrix/poll-summary.ts +110 -0
- package/src/matrix/poll-types.test.ts +205 -0
- package/src/matrix/poll-types.ts +433 -0
- package/src/matrix/probe.runtime.ts +4 -0
- package/src/matrix/probe.test.ts +154 -0
- package/src/matrix/probe.ts +96 -0
- package/src/matrix/profile.test.ts +154 -0
- package/src/matrix/profile.ts +184 -0
- package/src/matrix/reaction-common.test.ts +96 -0
- package/src/matrix/reaction-common.ts +147 -0
- package/src/matrix/sdk/crypto-bootstrap.test.ts +505 -0
- package/src/matrix/sdk/crypto-bootstrap.ts +341 -0
- package/src/matrix/sdk/crypto-facade.test.ts +197 -0
- package/src/matrix/sdk/crypto-facade.ts +207 -0
- package/src/matrix/sdk/crypto-node.runtime.test.ts +27 -0
- package/src/matrix/sdk/crypto-node.runtime.ts +9 -0
- package/src/matrix/sdk/crypto-runtime.ts +11 -0
- package/src/matrix/sdk/decrypt-bridge.ts +356 -0
- package/src/matrix/sdk/event-helpers.test.ts +60 -0
- package/src/matrix/sdk/event-helpers.ts +71 -0
- package/src/matrix/sdk/http-client.test.ts +134 -0
- package/src/matrix/sdk/http-client.ts +87 -0
- package/src/matrix/sdk/idb-persistence-lock.ts +51 -0
- package/src/matrix/sdk/idb-persistence.lock-order.test.ts +108 -0
- package/src/matrix/sdk/idb-persistence.test-helpers.ts +88 -0
- package/src/matrix/sdk/idb-persistence.test.ts +149 -0
- package/src/matrix/sdk/idb-persistence.ts +283 -0
- package/src/matrix/sdk/logger.test.ts +25 -0
- package/src/matrix/sdk/logger.ts +108 -0
- package/src/matrix/sdk/read-response-with-limit.ts +19 -0
- package/src/matrix/sdk/recovery-key-store.test.ts +385 -0
- package/src/matrix/sdk/recovery-key-store.ts +430 -0
- package/src/matrix/sdk/transport.test.ts +161 -0
- package/src/matrix/sdk/transport.ts +344 -0
- package/src/matrix/sdk/types.ts +236 -0
- package/src/matrix/sdk/verification-manager.test.ts +509 -0
- package/src/matrix/sdk/verification-manager.ts +694 -0
- package/src/matrix/sdk/verification-status.ts +23 -0
- package/src/matrix/sdk.test.ts +2568 -0
- package/src/matrix/sdk.ts +1789 -0
- package/src/matrix/send/client.test.ts +174 -0
- package/src/matrix/send/client.ts +90 -0
- package/src/matrix/send/formatting.ts +189 -0
- package/src/matrix/send/media.ts +244 -0
- package/src/matrix/send/targets.test.ts +254 -0
- package/src/matrix/send/targets.ts +104 -0
- package/src/matrix/send/types.ts +134 -0
- package/src/matrix/send.test.ts +958 -0
- package/src/matrix/send.ts +609 -0
- package/src/matrix/session-store-metadata.ts +108 -0
- package/src/matrix/startup-abort.ts +44 -0
- package/src/matrix/sync-state.ts +27 -0
- package/src/matrix/target-ids.ts +102 -0
- package/src/matrix/thread-bindings-shared.ts +201 -0
- package/src/matrix/thread-bindings.test.ts +673 -0
- package/src/matrix/thread-bindings.ts +577 -0
- package/src/matrix-migration.runtime.ts +9 -0
- package/src/migration-config.test.ts +228 -0
- package/src/migration-config.ts +243 -0
- package/src/migration-snapshot-backup.ts +117 -0
- package/src/migration-snapshot.test.ts +184 -0
- package/src/migration-snapshot.ts +55 -0
- package/src/onboarding.resolve.test.ts +55 -0
- package/src/onboarding.test-harness.ts +158 -0
- package/src/onboarding.test.ts +665 -0
- package/src/onboarding.ts +773 -0
- package/src/outbound.test.ts +173 -0
- package/src/outbound.ts +78 -0
- package/src/plugin-entry.runtime.js +159 -0
- package/src/plugin-entry.runtime.test.ts +108 -0
- package/src/plugin-entry.runtime.ts +68 -0
- package/src/profile-update.ts +68 -0
- package/src/record-shared.ts +3 -0
- package/src/resolve-targets.test.ts +178 -0
- package/src/resolve-targets.ts +175 -0
- package/src/resolver.ts +21 -0
- package/src/runtime-api.ts +144 -0
- package/src/runtime.ts +7 -0
- package/src/secret-contract.ts +174 -0
- package/src/session-route.test.ts +315 -0
- package/src/session-route.ts +113 -0
- package/src/setup-bootstrap.ts +94 -0
- package/src/setup-config.ts +222 -0
- package/src/setup-contract.ts +89 -0
- package/src/setup-core.test.ts +326 -0
- package/src/setup-core.ts +50 -0
- package/src/setup-surface.ts +4 -0
- package/src/startup-maintenance.test.ts +227 -0
- package/src/startup-maintenance.ts +114 -0
- package/src/storage-paths.ts +92 -0
- package/src/test-helpers.ts +42 -0
- package/src/test-mocks.ts +55 -0
- package/src/test-runtime.ts +72 -0
- package/src/test-support/monitor-route-test-support.ts +8 -0
- package/src/tool-actions.runtime.ts +1 -0
- package/src/tool-actions.test.ts +422 -0
- package/src/tool-actions.ts +498 -0
- package/src/types.ts +230 -0
- package/test-api.ts +2 -0
- package/thread-bindings-runtime.ts +4 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { decodeRecoveryKey } from "@archipelagolab/lobi-js-sdk/lib/crypto-api/recovery-key.js";
|
|
4
|
+
import { formatMatrixErrorMessage, formatMatrixErrorReason } from "../errors.js";
|
|
5
|
+
import { LogService } from "./logger.js";
|
|
6
|
+
import type {
|
|
7
|
+
MatrixCryptoBootstrapApi,
|
|
8
|
+
MatrixCryptoCallbacks,
|
|
9
|
+
MatrixGeneratedSecretStorageKey,
|
|
10
|
+
MatrixSecretStorageStatus,
|
|
11
|
+
MatrixStoredRecoveryKey,
|
|
12
|
+
} from "./types.js";
|
|
13
|
+
|
|
14
|
+
export function isRepairableSecretStorageAccessError(err: unknown): boolean {
|
|
15
|
+
const message = formatMatrixErrorReason(err);
|
|
16
|
+
if (!message) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
if (message.includes("getsecretstoragekey callback returned falsey")) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
// The homeserver still has secret storage, but the local recovery key cannot
|
|
23
|
+
// authenticate/decrypt a required secret. During explicit bootstrap we can
|
|
24
|
+
// recreate secret storage and continue with a new local baseline.
|
|
25
|
+
if (message.includes("decrypting secret") && message.includes("bad mac")) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class MatrixRecoveryKeyStore {
|
|
32
|
+
private readonly secretStorageKeyCache = new Map<
|
|
33
|
+
string,
|
|
34
|
+
{ key: Uint8Array; keyInfo?: MatrixStoredRecoveryKey["keyInfo"] }
|
|
35
|
+
>();
|
|
36
|
+
private stagedRecoveryKey: MatrixStoredRecoveryKey | null = null;
|
|
37
|
+
private readonly stagedCacheKeyIds = new Set<string>();
|
|
38
|
+
|
|
39
|
+
constructor(private readonly recoveryKeyPath?: string) {}
|
|
40
|
+
|
|
41
|
+
buildCryptoCallbacks(): MatrixCryptoCallbacks {
|
|
42
|
+
return {
|
|
43
|
+
getSecretStorageKey: async ({ keys }) => {
|
|
44
|
+
const requestedKeyIds = Object.keys(keys ?? {});
|
|
45
|
+
if (requestedKeyIds.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const keyId of requestedKeyIds) {
|
|
50
|
+
const cached = this.secretStorageKeyCache.get(keyId);
|
|
51
|
+
if (cached) {
|
|
52
|
+
return [keyId, new Uint8Array(cached.key)];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const staged = this.stagedRecoveryKey;
|
|
57
|
+
if (staged?.privateKeyBase64) {
|
|
58
|
+
const privateKey = new Uint8Array(Buffer.from(staged.privateKeyBase64, "base64"));
|
|
59
|
+
if (privateKey.length > 0) {
|
|
60
|
+
const stagedKeyId =
|
|
61
|
+
staged.keyId && requestedKeyIds.includes(staged.keyId)
|
|
62
|
+
? staged.keyId
|
|
63
|
+
: requestedKeyIds[0];
|
|
64
|
+
if (stagedKeyId) {
|
|
65
|
+
this.rememberSecretStorageKey(stagedKeyId, privateKey, staged.keyInfo);
|
|
66
|
+
this.stagedCacheKeyIds.add(stagedKeyId);
|
|
67
|
+
return [stagedKeyId, privateKey];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const stored = this.loadStoredRecoveryKey();
|
|
73
|
+
if (!stored?.privateKeyBase64) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const privateKey = new Uint8Array(Buffer.from(stored.privateKeyBase64, "base64"));
|
|
77
|
+
if (privateKey.length === 0) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (stored.keyId && requestedKeyIds.includes(stored.keyId)) {
|
|
82
|
+
this.rememberSecretStorageKey(stored.keyId, privateKey, stored.keyInfo);
|
|
83
|
+
return [stored.keyId, privateKey];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const firstRequestedKeyId = requestedKeyIds[0];
|
|
87
|
+
if (!firstRequestedKeyId) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
this.rememberSecretStorageKey(firstRequestedKeyId, privateKey, stored.keyInfo);
|
|
91
|
+
return [firstRequestedKeyId, privateKey];
|
|
92
|
+
},
|
|
93
|
+
cacheSecretStorageKey: (keyId, keyInfo, key) => {
|
|
94
|
+
const privateKey = new Uint8Array(key);
|
|
95
|
+
const normalizedKeyInfo: MatrixStoredRecoveryKey["keyInfo"] = {
|
|
96
|
+
passphrase: keyInfo?.passphrase,
|
|
97
|
+
name: typeof keyInfo?.name === "string" ? keyInfo.name : undefined,
|
|
98
|
+
};
|
|
99
|
+
this.rememberSecretStorageKey(keyId, privateKey, normalizedKeyInfo);
|
|
100
|
+
|
|
101
|
+
const stored = this.loadStoredRecoveryKey();
|
|
102
|
+
this.saveRecoveryKeyToDisk({
|
|
103
|
+
keyId,
|
|
104
|
+
keyInfo: normalizedKeyInfo,
|
|
105
|
+
privateKey,
|
|
106
|
+
encodedPrivateKey: stored?.encodedPrivateKey,
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
getRecoveryKeySummary(): {
|
|
113
|
+
encodedPrivateKey?: string;
|
|
114
|
+
keyId?: string | null;
|
|
115
|
+
createdAt?: string;
|
|
116
|
+
} | null {
|
|
117
|
+
const stored = this.loadStoredRecoveryKey();
|
|
118
|
+
if (!stored) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
encodedPrivateKey: stored.encodedPrivateKey,
|
|
123
|
+
keyId: stored.keyId,
|
|
124
|
+
createdAt: stored.createdAt,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private resolveEncodedRecoveryKeyInput(params: {
|
|
129
|
+
encodedPrivateKey: string;
|
|
130
|
+
keyId?: string | null;
|
|
131
|
+
keyInfo?: MatrixStoredRecoveryKey["keyInfo"];
|
|
132
|
+
}): {
|
|
133
|
+
encodedPrivateKey: string;
|
|
134
|
+
privateKey: Uint8Array;
|
|
135
|
+
keyId: string | null;
|
|
136
|
+
keyInfo?: MatrixStoredRecoveryKey["keyInfo"];
|
|
137
|
+
} {
|
|
138
|
+
const encodedPrivateKey = params.encodedPrivateKey.trim();
|
|
139
|
+
if (!encodedPrivateKey) {
|
|
140
|
+
throw new Error("Matrix recovery key is required");
|
|
141
|
+
}
|
|
142
|
+
let privateKey: Uint8Array;
|
|
143
|
+
try {
|
|
144
|
+
privateKey = decodeRecoveryKey(encodedPrivateKey);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
throw new Error(`Invalid Matrix recovery key: ${formatMatrixErrorMessage(err)}`, {
|
|
147
|
+
cause: err,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const keyId =
|
|
151
|
+
typeof params.keyId === "string" && params.keyId.trim() ? params.keyId.trim() : null;
|
|
152
|
+
return {
|
|
153
|
+
encodedPrivateKey,
|
|
154
|
+
privateKey,
|
|
155
|
+
keyId,
|
|
156
|
+
keyInfo: params.keyInfo ?? this.loadStoredRecoveryKey()?.keyInfo,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
storeEncodedRecoveryKey(params: {
|
|
161
|
+
encodedPrivateKey: string;
|
|
162
|
+
keyId?: string | null;
|
|
163
|
+
keyInfo?: MatrixStoredRecoveryKey["keyInfo"];
|
|
164
|
+
}): {
|
|
165
|
+
encodedPrivateKey?: string;
|
|
166
|
+
keyId?: string | null;
|
|
167
|
+
createdAt?: string;
|
|
168
|
+
} {
|
|
169
|
+
const prepared = this.resolveEncodedRecoveryKeyInput(params);
|
|
170
|
+
this.saveRecoveryKeyToDisk({
|
|
171
|
+
keyId: prepared.keyId,
|
|
172
|
+
keyInfo: prepared.keyInfo,
|
|
173
|
+
privateKey: prepared.privateKey,
|
|
174
|
+
encodedPrivateKey: prepared.encodedPrivateKey,
|
|
175
|
+
});
|
|
176
|
+
if (prepared.keyId) {
|
|
177
|
+
this.rememberSecretStorageKey(prepared.keyId, prepared.privateKey, prepared.keyInfo);
|
|
178
|
+
}
|
|
179
|
+
return this.getRecoveryKeySummary() ?? {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
stageEncodedRecoveryKey(params: {
|
|
183
|
+
encodedPrivateKey: string;
|
|
184
|
+
keyId?: string | null;
|
|
185
|
+
keyInfo?: MatrixStoredRecoveryKey["keyInfo"];
|
|
186
|
+
}): void {
|
|
187
|
+
const prepared = this.resolveEncodedRecoveryKeyInput(params);
|
|
188
|
+
this.discardStagedRecoveryKey();
|
|
189
|
+
this.stagedRecoveryKey = {
|
|
190
|
+
version: 1,
|
|
191
|
+
createdAt: new Date().toISOString(),
|
|
192
|
+
keyId: prepared.keyId,
|
|
193
|
+
encodedPrivateKey: prepared.encodedPrivateKey,
|
|
194
|
+
privateKeyBase64: Buffer.from(prepared.privateKey).toString("base64"),
|
|
195
|
+
keyInfo: prepared.keyInfo,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
commitStagedRecoveryKey(params?: {
|
|
200
|
+
keyId?: string | null;
|
|
201
|
+
keyInfo?: MatrixStoredRecoveryKey["keyInfo"];
|
|
202
|
+
}): {
|
|
203
|
+
encodedPrivateKey?: string;
|
|
204
|
+
keyId?: string | null;
|
|
205
|
+
createdAt?: string;
|
|
206
|
+
} | null {
|
|
207
|
+
if (!this.stagedRecoveryKey) {
|
|
208
|
+
return this.getRecoveryKeySummary();
|
|
209
|
+
}
|
|
210
|
+
const staged = this.stagedRecoveryKey;
|
|
211
|
+
const privateKey = new Uint8Array(Buffer.from(staged.privateKeyBase64, "base64"));
|
|
212
|
+
const keyId =
|
|
213
|
+
typeof params?.keyId === "string" && params.keyId.trim() ? params.keyId.trim() : staged.keyId;
|
|
214
|
+
this.saveRecoveryKeyToDisk({
|
|
215
|
+
keyId,
|
|
216
|
+
keyInfo: params?.keyInfo ?? staged.keyInfo,
|
|
217
|
+
privateKey,
|
|
218
|
+
encodedPrivateKey: staged.encodedPrivateKey,
|
|
219
|
+
});
|
|
220
|
+
this.clearStagedRecoveryKeyTracking();
|
|
221
|
+
return this.getRecoveryKeySummary();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
discardStagedRecoveryKey(): void {
|
|
225
|
+
for (const keyId of this.stagedCacheKeyIds) {
|
|
226
|
+
this.secretStorageKeyCache.delete(keyId);
|
|
227
|
+
}
|
|
228
|
+
this.clearStagedRecoveryKeyTracking();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async bootstrapSecretStorageWithRecoveryKey(
|
|
232
|
+
crypto: MatrixCryptoBootstrapApi,
|
|
233
|
+
options: {
|
|
234
|
+
setupNewKeyBackup?: boolean;
|
|
235
|
+
allowSecretStorageRecreateWithoutRecoveryKey?: boolean;
|
|
236
|
+
forceNewSecretStorage?: boolean;
|
|
237
|
+
} = {},
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
let status: MatrixSecretStorageStatus | null = null;
|
|
240
|
+
const getSecretStorageStatus = crypto.getSecretStorageStatus; // pragma: allowlist secret
|
|
241
|
+
if (typeof getSecretStorageStatus === "function") {
|
|
242
|
+
try {
|
|
243
|
+
status = await getSecretStorageStatus.call(crypto);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
LogService.warn("MatrixClientLite", "Failed to read secret storage status:", err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hasDefaultSecretStorageKey = Boolean(status?.defaultKeyId);
|
|
250
|
+
const hasKnownInvalidSecrets = Object.values(status?.secretStorageKeyValidityMap ?? {}).some(
|
|
251
|
+
(valid) => !valid,
|
|
252
|
+
);
|
|
253
|
+
let generatedRecoveryKey = false;
|
|
254
|
+
const storedRecovery = this.loadStoredRecoveryKey();
|
|
255
|
+
const stagedRecovery = this.stagedRecoveryKey;
|
|
256
|
+
const sourceRecovery = stagedRecovery ?? storedRecovery;
|
|
257
|
+
let recoveryKey: MatrixGeneratedSecretStorageKey | null = sourceRecovery
|
|
258
|
+
? {
|
|
259
|
+
keyInfo: sourceRecovery.keyInfo,
|
|
260
|
+
privateKey: new Uint8Array(Buffer.from(sourceRecovery.privateKeyBase64, "base64")),
|
|
261
|
+
encodedPrivateKey: sourceRecovery.encodedPrivateKey,
|
|
262
|
+
}
|
|
263
|
+
: null;
|
|
264
|
+
|
|
265
|
+
if (recoveryKey && status?.defaultKeyId) {
|
|
266
|
+
const defaultKeyId = status.defaultKeyId;
|
|
267
|
+
this.rememberSecretStorageKey(defaultKeyId, recoveryKey.privateKey, recoveryKey.keyInfo);
|
|
268
|
+
if (!stagedRecovery && storedRecovery && storedRecovery.keyId !== defaultKeyId) {
|
|
269
|
+
this.saveRecoveryKeyToDisk({
|
|
270
|
+
keyId: defaultKeyId,
|
|
271
|
+
keyInfo: recoveryKey.keyInfo,
|
|
272
|
+
privateKey: recoveryKey.privateKey,
|
|
273
|
+
encodedPrivateKey: recoveryKey.encodedPrivateKey,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const ensureRecoveryKey = async (): Promise<MatrixGeneratedSecretStorageKey> => {
|
|
279
|
+
if (recoveryKey) {
|
|
280
|
+
return recoveryKey;
|
|
281
|
+
}
|
|
282
|
+
if (typeof crypto.createRecoveryKeyFromPassphrase !== "function") {
|
|
283
|
+
throw new Error(
|
|
284
|
+
"Matrix crypto backend does not support recovery key generation (createRecoveryKeyFromPassphrase missing)",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
recoveryKey = await crypto.createRecoveryKeyFromPassphrase();
|
|
288
|
+
this.saveRecoveryKeyToDisk(recoveryKey);
|
|
289
|
+
generatedRecoveryKey = true;
|
|
290
|
+
return recoveryKey;
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const shouldRecreateSecretStorage =
|
|
294
|
+
options.forceNewSecretStorage === true ||
|
|
295
|
+
!hasDefaultSecretStorageKey ||
|
|
296
|
+
(!recoveryKey && status?.ready === false) ||
|
|
297
|
+
hasKnownInvalidSecrets;
|
|
298
|
+
|
|
299
|
+
if (hasKnownInvalidSecrets) {
|
|
300
|
+
// Existing secret storage keys can't decrypt required secrets. Generate a fresh recovery key.
|
|
301
|
+
recoveryKey = null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const secretStorageOptions: {
|
|
305
|
+
createSecretStorageKey?: () => Promise<MatrixGeneratedSecretStorageKey>;
|
|
306
|
+
setupNewSecretStorage?: boolean;
|
|
307
|
+
setupNewKeyBackup?: boolean;
|
|
308
|
+
} = {
|
|
309
|
+
setupNewKeyBackup: options.setupNewKeyBackup === true,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
if (shouldRecreateSecretStorage) {
|
|
313
|
+
secretStorageOptions.setupNewSecretStorage = true;
|
|
314
|
+
secretStorageOptions.createSecretStorageKey = ensureRecoveryKey;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
await crypto.bootstrapSecretStorage(secretStorageOptions);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
const shouldRecreateWithoutRecoveryKey =
|
|
321
|
+
options.allowSecretStorageRecreateWithoutRecoveryKey === true &&
|
|
322
|
+
hasDefaultSecretStorageKey &&
|
|
323
|
+
isRepairableSecretStorageAccessError(err);
|
|
324
|
+
if (!shouldRecreateWithoutRecoveryKey) {
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
recoveryKey = null;
|
|
329
|
+
LogService.warn(
|
|
330
|
+
"MatrixClientLite",
|
|
331
|
+
"Secret storage exists on the server but local recovery material cannot unlock it; recreating secret storage during explicit bootstrap.",
|
|
332
|
+
);
|
|
333
|
+
await crypto.bootstrapSecretStorage({
|
|
334
|
+
setupNewSecretStorage: true,
|
|
335
|
+
setupNewKeyBackup: options.setupNewKeyBackup === true,
|
|
336
|
+
createSecretStorageKey: ensureRecoveryKey,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (generatedRecoveryKey && this.recoveryKeyPath) {
|
|
341
|
+
LogService.warn(
|
|
342
|
+
"MatrixClientLite",
|
|
343
|
+
`Generated Matrix recovery key and saved it to ${this.recoveryKeyPath}. Keep this file secure.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private clearStagedRecoveryKeyTracking(): void {
|
|
349
|
+
this.stagedRecoveryKey = null;
|
|
350
|
+
this.stagedCacheKeyIds.clear();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private rememberSecretStorageKey(
|
|
354
|
+
keyId: string,
|
|
355
|
+
key: Uint8Array,
|
|
356
|
+
keyInfo?: MatrixStoredRecoveryKey["keyInfo"],
|
|
357
|
+
): void {
|
|
358
|
+
if (!keyId.trim()) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this.secretStorageKeyCache.set(keyId, {
|
|
362
|
+
key: new Uint8Array(key),
|
|
363
|
+
keyInfo,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private loadStoredRecoveryKey(): MatrixStoredRecoveryKey | null {
|
|
368
|
+
if (!this.recoveryKeyPath) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
if (!fs.existsSync(this.recoveryKeyPath)) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const raw = fs.readFileSync(this.recoveryKeyPath, "utf8");
|
|
376
|
+
const parsed = JSON.parse(raw) as Partial<MatrixStoredRecoveryKey>;
|
|
377
|
+
if (
|
|
378
|
+
parsed.version !== 1 ||
|
|
379
|
+
typeof parsed.createdAt !== "string" ||
|
|
380
|
+
typeof parsed.privateKeyBase64 !== "string" || // pragma: allowlist secret
|
|
381
|
+
!parsed.privateKeyBase64.trim()
|
|
382
|
+
) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
version: 1,
|
|
387
|
+
createdAt: parsed.createdAt,
|
|
388
|
+
keyId: typeof parsed.keyId === "string" ? parsed.keyId : null,
|
|
389
|
+
encodedPrivateKey:
|
|
390
|
+
typeof parsed.encodedPrivateKey === "string" ? parsed.encodedPrivateKey : undefined,
|
|
391
|
+
privateKeyBase64: parsed.privateKeyBase64,
|
|
392
|
+
keyInfo:
|
|
393
|
+
parsed.keyInfo && typeof parsed.keyInfo === "object"
|
|
394
|
+
? {
|
|
395
|
+
passphrase: parsed.keyInfo.passphrase,
|
|
396
|
+
name: typeof parsed.keyInfo.name === "string" ? parsed.keyInfo.name : undefined,
|
|
397
|
+
}
|
|
398
|
+
: undefined,
|
|
399
|
+
};
|
|
400
|
+
} catch {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private saveRecoveryKeyToDisk(params: MatrixGeneratedSecretStorageKey): void {
|
|
406
|
+
if (!this.recoveryKeyPath) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const payload: MatrixStoredRecoveryKey = {
|
|
411
|
+
version: 1,
|
|
412
|
+
createdAt: new Date().toISOString(),
|
|
413
|
+
keyId: typeof params.keyId === "string" ? params.keyId : null,
|
|
414
|
+
encodedPrivateKey: params.encodedPrivateKey,
|
|
415
|
+
privateKeyBase64: Buffer.from(params.privateKey).toString("base64"),
|
|
416
|
+
keyInfo: params.keyInfo
|
|
417
|
+
? {
|
|
418
|
+
passphrase: params.keyInfo.passphrase,
|
|
419
|
+
name: params.keyInfo.name,
|
|
420
|
+
}
|
|
421
|
+
: undefined,
|
|
422
|
+
};
|
|
423
|
+
fs.mkdirSync(path.dirname(this.recoveryKeyPath), { recursive: true });
|
|
424
|
+
fs.writeFileSync(this.recoveryKeyPath, JSON.stringify(payload, null, 2), "utf8");
|
|
425
|
+
fs.chmodSync(this.recoveryKeyPath, 0o600);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
LogService.warn("MatrixClientLite", "Failed to persist recovery key:", err);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { MatrixMediaSizeLimitError } from "../media-errors.js";
|
|
3
|
+
import { performMatrixRequest } from "./transport.js";
|
|
4
|
+
|
|
5
|
+
const TEST_UNDICI_RUNTIME_DEPS_KEY = "__OPENCLAW_TEST_UNDICI_RUNTIME_DEPS__";
|
|
6
|
+
|
|
7
|
+
function clearTestUndiciRuntimeDepsOverride(): void {
|
|
8
|
+
Reflect.deleteProperty(globalThis as object, TEST_UNDICI_RUNTIME_DEPS_KEY);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe("performMatrixRequest", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.unstubAllGlobals();
|
|
14
|
+
clearTestUndiciRuntimeDepsOverride();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
clearTestUndiciRuntimeDepsOverride();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("rejects oversized raw responses before buffering the whole body", async () => {
|
|
22
|
+
vi.stubGlobal(
|
|
23
|
+
"fetch",
|
|
24
|
+
vi.fn(
|
|
25
|
+
async () =>
|
|
26
|
+
new Response("too-big", {
|
|
27
|
+
status: 200,
|
|
28
|
+
headers: {
|
|
29
|
+
"content-length": "8192",
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
await expect(
|
|
36
|
+
performMatrixRequest({
|
|
37
|
+
homeserver: "http://127.0.0.1:8008",
|
|
38
|
+
accessToken: "token",
|
|
39
|
+
method: "GET",
|
|
40
|
+
endpoint: "/_lobi/media/v3/download/example/id",
|
|
41
|
+
timeoutMs: 5000,
|
|
42
|
+
raw: true,
|
|
43
|
+
maxBytes: 1024,
|
|
44
|
+
ssrfPolicy: { allowPrivateNetwork: true },
|
|
45
|
+
}),
|
|
46
|
+
).rejects.toBeInstanceOf(MatrixMediaSizeLimitError);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("applies streaming byte limits when raw responses omit content-length", async () => {
|
|
50
|
+
const chunk = new Uint8Array(768);
|
|
51
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
52
|
+
start(controller) {
|
|
53
|
+
controller.enqueue(chunk);
|
|
54
|
+
controller.enqueue(chunk);
|
|
55
|
+
controller.close();
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
vi.stubGlobal(
|
|
59
|
+
"fetch",
|
|
60
|
+
vi.fn(
|
|
61
|
+
async () =>
|
|
62
|
+
new Response(stream, {
|
|
63
|
+
status: 200,
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
await expect(
|
|
69
|
+
performMatrixRequest({
|
|
70
|
+
homeserver: "http://127.0.0.1:8008",
|
|
71
|
+
accessToken: "token",
|
|
72
|
+
method: "GET",
|
|
73
|
+
endpoint: "/_lobi/media/v3/download/example/id",
|
|
74
|
+
timeoutMs: 5000,
|
|
75
|
+
raw: true,
|
|
76
|
+
maxBytes: 1024,
|
|
77
|
+
ssrfPolicy: { allowPrivateNetwork: true },
|
|
78
|
+
}),
|
|
79
|
+
).rejects.toBeInstanceOf(MatrixMediaSizeLimitError);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("uses the matrix-specific idle-timeout error for stalled raw downloads", async () => {
|
|
83
|
+
vi.useFakeTimers();
|
|
84
|
+
try {
|
|
85
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
86
|
+
start(controller) {
|
|
87
|
+
controller.enqueue(new Uint8Array([1, 2, 3]));
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
vi.stubGlobal(
|
|
91
|
+
"fetch",
|
|
92
|
+
vi.fn(
|
|
93
|
+
async () =>
|
|
94
|
+
new Response(stream, {
|
|
95
|
+
status: 200,
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const requestPromise = performMatrixRequest({
|
|
101
|
+
homeserver: "http://127.0.0.1:8008",
|
|
102
|
+
accessToken: "token",
|
|
103
|
+
method: "GET",
|
|
104
|
+
endpoint: "/_lobi/media/v3/download/example/id",
|
|
105
|
+
timeoutMs: 5000,
|
|
106
|
+
raw: true,
|
|
107
|
+
maxBytes: 1024,
|
|
108
|
+
readIdleTimeoutMs: 50,
|
|
109
|
+
ssrfPolicy: { allowPrivateNetwork: true },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const rejection = expect(requestPromise).rejects.toThrow(
|
|
113
|
+
"Matrix media download stalled: no data received for 50ms",
|
|
114
|
+
);
|
|
115
|
+
await vi.advanceTimersByTimeAsync(60);
|
|
116
|
+
await rejection;
|
|
117
|
+
} finally {
|
|
118
|
+
vi.useRealTimers();
|
|
119
|
+
}
|
|
120
|
+
}, 5_000);
|
|
121
|
+
|
|
122
|
+
it("uses undici runtime fetch for pinned Matrix requests so the dispatcher stays bound", async () => {
|
|
123
|
+
let ambientFetchCalls = 0;
|
|
124
|
+
vi.stubGlobal("fetch", (async () => {
|
|
125
|
+
ambientFetchCalls += 1;
|
|
126
|
+
throw new Error("expected pinned Matrix requests to avoid ambient fetch");
|
|
127
|
+
}) as typeof fetch);
|
|
128
|
+
const runtimeFetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
129
|
+
const requestInit = init as RequestInit & { dispatcher?: unknown };
|
|
130
|
+
expect(requestInit.dispatcher).toBeDefined();
|
|
131
|
+
return new Response('{"ok":true}', {
|
|
132
|
+
status: 200,
|
|
133
|
+
headers: {
|
|
134
|
+
"content-type": "application/json",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
(globalThis as Record<string, unknown>)[TEST_UNDICI_RUNTIME_DEPS_KEY] = {
|
|
139
|
+
Agent: function MockAgent() {},
|
|
140
|
+
EnvHttpProxyAgent: function MockEnvHttpProxyAgent() {},
|
|
141
|
+
ProxyAgent: function MockProxyAgent() {},
|
|
142
|
+
fetch: runtimeFetch,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = await performMatrixRequest({
|
|
146
|
+
homeserver: "http://127.0.0.1:8008",
|
|
147
|
+
accessToken: "token",
|
|
148
|
+
method: "GET",
|
|
149
|
+
endpoint: "/_lobi/client/v3/account/whoami",
|
|
150
|
+
timeoutMs: 5000,
|
|
151
|
+
ssrfPolicy: { allowPrivateNetwork: true },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(result.text).toBe('{"ok":true}');
|
|
155
|
+
expect(ambientFetchCalls).toBe(0);
|
|
156
|
+
expect(runtimeFetch).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(
|
|
158
|
+
(runtimeFetch.mock.calls[0]?.[1] as RequestInit & { dispatcher?: unknown })?.dispatcher,
|
|
159
|
+
).toBeDefined();
|
|
160
|
+
});
|
|
161
|
+
});
|