@abraca/dabra 1.8.2 → 2.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/dist/abracadabra-provider.cjs +12722 -9050
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +12683 -9061
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +1485 -118
- package/package.json +1 -1
- package/src/AbracadabraBaseProvider.ts +51 -2
- package/src/AbracadabraClient.ts +516 -66
- package/src/AbracadabraProvider.ts +22 -7
- package/src/AbracadabraWS.ts +1 -1
- package/src/ChatClient.ts +193 -113
- package/src/ContentManager.ts +228 -0
- package/src/CryptoIdentityKeystore.ts +3 -3
- package/src/DocConverters.ts +1862 -0
- package/src/DocKeyManager.ts +60 -12
- package/src/DocTypes.ts +628 -0
- package/src/DocUtils.ts +89 -0
- package/src/DocumentManager.ts +319 -0
- package/src/E2EAbracadabraProvider.ts +189 -0
- package/src/EncryptedChatClient.ts +173 -0
- package/src/EncryptedY.ts +2 -2
- package/src/FileBlobStore.ts +10 -0
- package/src/IdentityDoc.ts +25 -0
- package/src/MetaManager.ts +100 -0
- package/src/MnemonicKeyDerivation.ts +4 -4
- package/src/NotificationsClient.ts +120 -98
- package/src/OutgoingMessages/SubdocMessage.ts +2 -2
- package/src/RpcClient.ts +659 -0
- package/src/TreeManager.ts +473 -0
- package/src/TreeTimestamps.ts +28 -25
- package/src/index.ts +71 -1
- package/src/messageRecord.ts +121 -0
- package/src/types.ts +174 -16
- package/src/webrtc/AbracadabraWebRTC.ts +2 -2
- package/src/webrtc/DataChannelRouter.ts +2 -2
- package/src/webrtc/E2EEChannel.ts +3 -3
- package/src/webrtc/FileTransferChannel.ts +9 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode + fold message records from a period doc's `messages` Y.Array
|
|
3
|
+
* into the flat `ChatMessage[]` shape the dashboard renders.
|
|
4
|
+
*
|
|
5
|
+
* The server appends three kinds of records to the array (see
|
|
6
|
+
* `abracadabra-rs/crates/abracadabra/src/messages.rs`):
|
|
7
|
+
*
|
|
8
|
+
* - `record_kind: "message"` — a real message (default kind).
|
|
9
|
+
* - `record_kind: "edit"` — an updated version of `target_id`.
|
|
10
|
+
* - `record_kind: "tombstone"` — `target_id` is deleted.
|
|
11
|
+
*
|
|
12
|
+
* Folding rules:
|
|
13
|
+
* - For each `target_id`, apply edits in `ts` order; the *last* edit's
|
|
14
|
+
* content wins. The original message's id, sender_id, and ts stay.
|
|
15
|
+
* `editedAt` is the latest edit's ts.
|
|
16
|
+
* - Any target_id with a tombstone is dropped from the output entirely.
|
|
17
|
+
* - Records without record_kind default to "message" (back-compat).
|
|
18
|
+
*
|
|
19
|
+
* Cross-period folding: edits and tombstones can in principle target a
|
|
20
|
+
* message in a *prior* period (e.g. user edits a message that lived on
|
|
21
|
+
* the previous period after rollover). Callers that paginate across
|
|
22
|
+
* periods should fold the joined list, not each period independently.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface MessageRecord {
|
|
26
|
+
record_kind?: 'message' | 'edit' | 'tombstone'
|
|
27
|
+
id: string
|
|
28
|
+
target_id?: string
|
|
29
|
+
sender_id: string
|
|
30
|
+
channel_doc_id: string
|
|
31
|
+
period_id: string
|
|
32
|
+
ts: number
|
|
33
|
+
content: string
|
|
34
|
+
mentions?: string[]
|
|
35
|
+
reply_to?: string | null
|
|
36
|
+
sig?: string
|
|
37
|
+
server_sig?: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FoldedMessage {
|
|
41
|
+
id: string
|
|
42
|
+
channel: string
|
|
43
|
+
senderId: string
|
|
44
|
+
senderName?: string
|
|
45
|
+
content: string
|
|
46
|
+
createdAt: number
|
|
47
|
+
/** Set if the message has been edited at least once. */
|
|
48
|
+
editedAt?: number
|
|
49
|
+
/** Reply target message id, if any. */
|
|
50
|
+
replyTo?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fold an ordered list of records (oldest-first) into the visible
|
|
55
|
+
* messages a UI should render. Records may come from one period or
|
|
56
|
+
* across multiple periods concatenated in time order.
|
|
57
|
+
*/
|
|
58
|
+
export function foldRecords(records: MessageRecord[]): FoldedMessage[] {
|
|
59
|
+
const tombstoned = new Set<string>()
|
|
60
|
+
for (const r of records) {
|
|
61
|
+
if (r.record_kind === 'tombstone' && r.target_id) tombstoned.add(r.target_id)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const base = new Map<string, FoldedMessage>()
|
|
65
|
+
for (const r of records) {
|
|
66
|
+
const kind = r.record_kind ?? 'message'
|
|
67
|
+
if (kind !== 'message') continue
|
|
68
|
+
if (tombstoned.has(r.id)) continue
|
|
69
|
+
base.set(r.id, {
|
|
70
|
+
id: r.id,
|
|
71
|
+
channel: r.channel_doc_id,
|
|
72
|
+
senderId: r.sender_id,
|
|
73
|
+
content: r.content,
|
|
74
|
+
createdAt: r.ts,
|
|
75
|
+
...(r.reply_to ? { replyTo: r.reply_to } : {}),
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const r of records) {
|
|
80
|
+
if (r.record_kind !== 'edit' || !r.target_id) continue
|
|
81
|
+
if (tombstoned.has(r.target_id)) continue
|
|
82
|
+
const original = base.get(r.target_id)
|
|
83
|
+
if (!original) continue
|
|
84
|
+
original.content = r.content
|
|
85
|
+
original.editedAt = r.ts
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return [...base.values()].sort((a, b) => a.createdAt - b.createdAt)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Decode whatever the Y.Array stored for an entry into a MessageRecord.
|
|
93
|
+
* Note: the server stamps `ts` server-side via `Any::BigInt(now_ts_ms())`,
|
|
94
|
+
* so on the wire it round-trips as a JS BigInt — `typeof` is `'bigint'`,
|
|
95
|
+
* not `'number'`. Accept both and coerce.
|
|
96
|
+
*/
|
|
97
|
+
export function recordFromYAny(any: unknown): MessageRecord | null {
|
|
98
|
+
if (!any || typeof any !== 'object') return null
|
|
99
|
+
const o = any as Record<string, unknown>
|
|
100
|
+
const tsRaw = o.ts
|
|
101
|
+
const tsIsValid = typeof tsRaw === 'number' || typeof tsRaw === 'bigint'
|
|
102
|
+
if (typeof o.id !== 'string' || typeof o.sender_id !== 'string'
|
|
103
|
+
|| typeof o.channel_doc_id !== 'string' || !tsIsValid) {
|
|
104
|
+
return null
|
|
105
|
+
}
|
|
106
|
+
const ts = typeof tsRaw === 'bigint' ? Number(tsRaw) : (tsRaw as number)
|
|
107
|
+
return {
|
|
108
|
+
record_kind: typeof o.record_kind === 'string' ? o.record_kind as MessageRecord['record_kind'] : 'message',
|
|
109
|
+
id: o.id,
|
|
110
|
+
target_id: typeof o.target_id === 'string' ? o.target_id : undefined,
|
|
111
|
+
sender_id: o.sender_id,
|
|
112
|
+
channel_doc_id: o.channel_doc_id,
|
|
113
|
+
period_id: typeof o.period_id === 'string' ? o.period_id : '',
|
|
114
|
+
ts,
|
|
115
|
+
content: typeof o.content === 'string' ? o.content : '',
|
|
116
|
+
mentions: Array.isArray(o.mentions) ? o.mentions as string[] : [],
|
|
117
|
+
reply_to: typeof o.reply_to === 'string' ? o.reply_to : null,
|
|
118
|
+
sig: typeof o.sig === 'string' ? o.sig : undefined,
|
|
119
|
+
server_sig: typeof o.server_sig === 'string' ? o.server_sig : undefined,
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -124,6 +124,14 @@ export type onStatelessParameters = {
|
|
|
124
124
|
payload: string;
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
/** Fired by E2EAbracadabraProvider when the server acknowledges an E2E
|
|
128
|
+
* client-side compaction (via the `snapshot:compacted` stateless broadcast). */
|
|
129
|
+
export type onCompactedParameters = {
|
|
130
|
+
docId: string;
|
|
131
|
+
/** User id that performed the compaction; undefined if payload was malformed. */
|
|
132
|
+
by: string | undefined;
|
|
133
|
+
};
|
|
134
|
+
|
|
127
135
|
export type onServerErrorParameters = {
|
|
128
136
|
source: string;
|
|
129
137
|
code: string;
|
|
@@ -175,20 +183,68 @@ export interface UserProfile {
|
|
|
175
183
|
role: string;
|
|
176
184
|
/** Account-level Ed25519 public key (base64url). Canonical user identity. */
|
|
177
185
|
publicKey: string | null;
|
|
186
|
+
/**
|
|
187
|
+
* Whether the user has a password set. `false` for key-based soft
|
|
188
|
+
* identities until they opt in via {@link AbracadabraClient.setPassword}.
|
|
189
|
+
* Drives the UI's "Set password" vs "Change password" branching.
|
|
190
|
+
*/
|
|
191
|
+
hasPassword?: boolean;
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
export interface DocumentMeta {
|
|
181
195
|
id: string;
|
|
182
196
|
parent_id: string | null;
|
|
197
|
+
/**
|
|
198
|
+
* Renderer hint — `"kanban"`, `"checklist"`, `"outline"`, `"graph"`,
|
|
199
|
+
* `"sheet"`, `"mindmap"`, `"doc"`, etc. Drives the dashboard's choice of
|
|
200
|
+
* Vue component for this doc. Orthogonal to {@link kind}.
|
|
201
|
+
*/
|
|
183
202
|
doc_type?: string | null;
|
|
184
203
|
label?: string | null;
|
|
185
204
|
description?: string | null;
|
|
186
205
|
public_access?: string | null;
|
|
187
206
|
owner_id?: string | null;
|
|
188
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Structural role hint — `"server"` for the reserved server-root doc,
|
|
209
|
+
* `"space"` for top-level workspace containers, `null`/absent otherwise.
|
|
210
|
+
* Distinct from {@link doc_type}: a Space (`kind: "space"`) can contain
|
|
211
|
+
* many Kanban boards (each `doc_type: "kanban"`).
|
|
212
|
+
*/
|
|
213
|
+
kind?: string | null;
|
|
189
214
|
updated_at?: number | null;
|
|
190
215
|
}
|
|
191
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Well-known structural roles for the {@link DocumentMeta.kind} field. These
|
|
219
|
+
* are conventions only — the server treats `kind` as an opaque string. Use
|
|
220
|
+
* these constants instead of string literals to avoid typos.
|
|
221
|
+
*/
|
|
222
|
+
export const Kind = {
|
|
223
|
+
/** The reserved server root document. There is exactly one per server. */
|
|
224
|
+
Server: "server",
|
|
225
|
+
/** A top-level workspace container — what the dashboard calls a "Space". */
|
|
226
|
+
Space: "space",
|
|
227
|
+
/** A regular content page rendered by the editor. */
|
|
228
|
+
Page: "page",
|
|
229
|
+
/** A group chat container under a Space. */
|
|
230
|
+
Channel: "channel",
|
|
231
|
+
/** A direct-message container at the server root with two-member permissions. */
|
|
232
|
+
Dm: "dm",
|
|
233
|
+
/** A child of channel/dm holding the messages Y.Array. */
|
|
234
|
+
ChannelPeriod: "channel-period",
|
|
235
|
+
/** A per-user inbox doc holding inbox entries; child of server root. */
|
|
236
|
+
Inbox: "inbox",
|
|
237
|
+
} as const;
|
|
238
|
+
export type Kind = typeof Kind[keyof typeof Kind];
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Hardcoded UUID of the reserved server root document. Every doc in the tree
|
|
242
|
+
* chains up to this row, so a permission grant on this doc cascades into
|
|
243
|
+
* every descendant. Mirrors the constant in the Rust crate
|
|
244
|
+
* (`crate::types::SERVER_ROOT_ID_STR`).
|
|
245
|
+
*/
|
|
246
|
+
export const SERVER_ROOT_ID = "00000000-0000-0000-0000-000000000000";
|
|
247
|
+
|
|
192
248
|
export interface UploadMeta {
|
|
193
249
|
id: string;
|
|
194
250
|
doc_id: string;
|
|
@@ -242,10 +298,30 @@ export interface SnapshotMeta {
|
|
|
242
298
|
label?: string | null;
|
|
243
299
|
created_by?: string | null;
|
|
244
300
|
created_at: number;
|
|
301
|
+
/** Number of uploads referenced by this snapshot's CRDT state.
|
|
302
|
+
* Always 0 on servers older than the `snapshot_files` migration, or for
|
|
303
|
+
* snapshots created before that migration ran (until the operator runs
|
|
304
|
+
* `POST /admin/snapshots/backfill-refs`). */
|
|
305
|
+
file_count?: number;
|
|
306
|
+
/** Signed byte delta vs. the previous-version snapshot for this doc.
|
|
307
|
+
* `null` for the first snapshot of a doc (or when the previous version
|
|
308
|
+
* has been pruned). Lets the UI render `+12 KB` / `−3 KB`. */
|
|
309
|
+
delta_bytes?: number | null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface SnapshotFileEntry {
|
|
313
|
+
id: string;
|
|
314
|
+
doc_id: string;
|
|
315
|
+
filename: string;
|
|
316
|
+
mime_type?: string | null;
|
|
317
|
+
size?: number | null;
|
|
245
318
|
}
|
|
246
319
|
|
|
247
320
|
export interface SnapshotData extends SnapshotMeta {
|
|
248
321
|
data: string;
|
|
322
|
+
/** Populated only when fetched with `?include=files`. Each entry is an
|
|
323
|
+
* upload referenced by this snapshot's CRDT state. */
|
|
324
|
+
files?: SnapshotFileEntry[];
|
|
249
325
|
}
|
|
250
326
|
|
|
251
327
|
export interface SnapshotCreateResult {
|
|
@@ -277,10 +353,23 @@ export interface ServerInfo {
|
|
|
277
353
|
version?: string;
|
|
278
354
|
/** Hocuspocus wire protocol version (currently 2). */
|
|
279
355
|
protocol_version?: number;
|
|
280
|
-
/**
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
356
|
+
/**
|
|
357
|
+
* The reserved server root document id. Every doc in the tree chains up
|
|
358
|
+
* to this row; granting Admin here cascades to every doc. Hardcoded by
|
|
359
|
+
* the server — clients can also use {@link SERVER_ROOT_ID} directly.
|
|
360
|
+
*/
|
|
361
|
+
root_doc_id?: string;
|
|
362
|
+
/**
|
|
363
|
+
* Server-wide access policy. Drives client-side decisions like "show the
|
|
364
|
+
* sign-up CTA?" (anonymous mode) or "warn before publishing?" (anonymous
|
|
365
|
+
* grants).
|
|
366
|
+
*/
|
|
367
|
+
access?: {
|
|
368
|
+
/** `"none"` rejects unauthenticated requests; `"observer"` allows public read. */
|
|
369
|
+
anonymous?: "none" | "observer";
|
|
370
|
+
/** Server-wide floor for authed users with no explicit grant. */
|
|
371
|
+
authenticated?: "none" | "observer" | "viewer" | "editor";
|
|
372
|
+
};
|
|
284
373
|
/** Enabled auth methods (e.g. ["crypto", "jwt"]). */
|
|
285
374
|
auth_methods?: string[];
|
|
286
375
|
/** Whether open registration is enabled. */
|
|
@@ -289,6 +378,13 @@ export interface ServerInfo {
|
|
|
289
378
|
invite_only?: boolean;
|
|
290
379
|
/** Server encryption configuration. */
|
|
291
380
|
encryption?: { default_mode?: string; minimum_mode?: string };
|
|
381
|
+
/**
|
|
382
|
+
* Ed25519 public key (base64url, no padding) the server uses to sign every
|
|
383
|
+
* accepted `messages:*` record (`server_sig`). Clients can verify message
|
|
384
|
+
* placement / ordering by checking each record's `server_sig` against this
|
|
385
|
+
* key over the canonical `{ msg_id, period_id, ts, client_sig }` payload.
|
|
386
|
+
*/
|
|
387
|
+
messages_signer_pubkey?: string;
|
|
292
388
|
}
|
|
293
389
|
|
|
294
390
|
// ── Search ───────────────────────────────────────────────────────────────────
|
|
@@ -299,21 +395,83 @@ export interface SearchResult {
|
|
|
299
395
|
score: number;
|
|
300
396
|
}
|
|
301
397
|
|
|
302
|
-
|
|
398
|
+
/**
|
|
399
|
+
* A single hit returned by the server's full-text search endpoint
|
|
400
|
+
* (`GET /docs/search`). Distinct from {@link SearchResult}, which is the
|
|
401
|
+
* client-side trigram index in `SearchIndex.ts`.
|
|
402
|
+
*
|
|
403
|
+
* `snippet` is server-rendered HTML with `<mark>` wrappers around the
|
|
404
|
+
* matched tokens — safe to inject after standard sanitization. `rank` is
|
|
405
|
+
* a backend-specific score (SQLite bm25 vs Postgres ts_rank); the only
|
|
406
|
+
* guarantee is that hits arrive in best-first order.
|
|
407
|
+
*/
|
|
408
|
+
export interface DocSearchHit {
|
|
409
|
+
doc_id: string;
|
|
410
|
+
parent_id: string | null;
|
|
411
|
+
label: string | null;
|
|
412
|
+
kind: string | null;
|
|
413
|
+
/** HTML-safe snippet with `<mark>` markers around the matched tokens. */
|
|
414
|
+
snippet: string;
|
|
415
|
+
rank: number;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ── Audit log (admin) ────────────────────────────────────────────────────────
|
|
303
419
|
|
|
304
|
-
|
|
420
|
+
/**
|
|
421
|
+
* A single row from `GET /admin/audit`. Mirrors the server's audit log
|
|
422
|
+
* row format (see `audit::AuditLogRow`). `metadata` is the parsed JSON
|
|
423
|
+
* object the server stored, or `null` if the row had no metadata. Each
|
|
424
|
+
* row carries an internal `prev_hash` / `row_hash` chain that admins
|
|
425
|
+
* verify with {@link AuditVerifyResult}.
|
|
426
|
+
*/
|
|
427
|
+
export interface AuditLogEntry {
|
|
305
428
|
id: string;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
429
|
+
ts: number;
|
|
430
|
+
event_type: string;
|
|
431
|
+
actor_user_id: string | null;
|
|
432
|
+
actor_ip: string | null;
|
|
433
|
+
request_id: string | null;
|
|
434
|
+
target_type: string | null;
|
|
435
|
+
target_id: string | null;
|
|
436
|
+
metadata: Record<string, unknown> | null;
|
|
437
|
+
outcome: string;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Filters for `GET /admin/audit`. All optional and AND-combined. */
|
|
441
|
+
export interface AuditQueryOpts {
|
|
442
|
+
event_type?: string;
|
|
443
|
+
actor_user_id?: string;
|
|
444
|
+
target_type?: string;
|
|
445
|
+
target_id?: string;
|
|
446
|
+
since_ts?: number;
|
|
447
|
+
until_ts?: number;
|
|
448
|
+
limit?: number;
|
|
449
|
+
offset?: number;
|
|
315
450
|
}
|
|
316
451
|
|
|
452
|
+
/** Response of `GET /admin/audit/verify`. `status: "broken"` carries `break`. */
|
|
453
|
+
export interface AuditVerifyResult {
|
|
454
|
+
status: "ok" | "broken";
|
|
455
|
+
message?: string;
|
|
456
|
+
break?: { rows_checked: number; row_id: string; reason: string };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Readiness ────────────────────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
/** Response of `GET /readyz`. HTTP 200 when ready, 503 when not. */
|
|
462
|
+
export interface ReadyzStatus {
|
|
463
|
+
status: "ready" | "unready";
|
|
464
|
+
version: string;
|
|
465
|
+
checks: { database: "ok" | "error" };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Spaces ───────────────────────────────────────────────────────────────────
|
|
469
|
+
//
|
|
470
|
+
// Spaces are documents whose `parent_id` is the server root and whose `kind`
|
|
471
|
+
// is `"space"`. Listing them is `client.listSpaces()`; they come back as
|
|
472
|
+
// regular `DocumentMeta` objects. Visibility is encoded in `public_access`:
|
|
473
|
+
// `null` (or `"none"`) = private, `"observer"` = public read-only.
|
|
474
|
+
|
|
317
475
|
// ── Invites ──────────────────────────────────────────────────────────────────
|
|
318
476
|
|
|
319
477
|
export interface InviteRow {
|
|
@@ -527,7 +527,7 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
527
527
|
pendingKeyExchangeChannel = channel;
|
|
528
528
|
return;
|
|
529
529
|
}
|
|
530
|
-
channel.send(e2ee.getKeyExchangeMessage());
|
|
530
|
+
channel.send(e2ee.getKeyExchangeMessage() as unknown as ArrayBuffer);
|
|
531
531
|
});
|
|
532
532
|
|
|
533
533
|
this.resolveE2ee().then(async (identity) => {
|
|
@@ -541,7 +541,7 @@ export class AbracadabraWebRTC extends EventEmitter {
|
|
|
541
541
|
|
|
542
542
|
// Drain buffered key-exchange channel open
|
|
543
543
|
if (pendingKeyExchangeChannel) {
|
|
544
|
-
pendingKeyExchangeChannel.send(e2ee.getKeyExchangeMessage());
|
|
544
|
+
pendingKeyExchangeChannel.send(e2ee.getKeyExchangeMessage() as unknown as ArrayBuffer);
|
|
545
545
|
}
|
|
546
546
|
// Drain buffered messages
|
|
547
547
|
for (const msg of pendingMessages) {
|
|
@@ -100,9 +100,9 @@ export class DataChannelRouter extends EventEmitter {
|
|
|
100
100
|
|
|
101
101
|
if (this.encryptor?.isEstablished && !this.plaintextChannels.has(name)) {
|
|
102
102
|
const encrypted = await this.encryptor.encrypt(data);
|
|
103
|
-
channel.send(encrypted);
|
|
103
|
+
channel.send(encrypted as unknown as ArrayBuffer);
|
|
104
104
|
} else {
|
|
105
|
-
channel.send(data);
|
|
105
|
+
channel.send(data as unknown as ArrayBuffer);
|
|
106
106
|
}
|
|
107
107
|
return true;
|
|
108
108
|
}
|
|
@@ -98,7 +98,7 @@ export class E2EEChannel extends EventEmitter {
|
|
|
98
98
|
|
|
99
99
|
this.sessionKey = await crypto.subtle.importKey(
|
|
100
100
|
"raw",
|
|
101
|
-
keyBytes,
|
|
101
|
+
keyBytes as BufferSource,
|
|
102
102
|
{ name: "AES-GCM" },
|
|
103
103
|
false,
|
|
104
104
|
["encrypt", "decrypt"],
|
|
@@ -130,9 +130,9 @@ export class E2EEChannel extends EventEmitter {
|
|
|
130
130
|
const nonce = crypto.getRandomValues(new Uint8Array(NONCE_BYTES));
|
|
131
131
|
const ciphertext = new Uint8Array(
|
|
132
132
|
await crypto.subtle.encrypt(
|
|
133
|
-
{ name: "AES-GCM", iv: nonce },
|
|
133
|
+
{ name: "AES-GCM", iv: nonce as BufferSource },
|
|
134
134
|
this.sessionKey,
|
|
135
|
-
plaintext,
|
|
135
|
+
plaintext as BufferSource,
|
|
136
136
|
),
|
|
137
137
|
);
|
|
138
138
|
|
|
@@ -45,6 +45,13 @@ export class FileTransferHandle extends EventEmitter {
|
|
|
45
45
|
_setStatus(s: FileTransferStatus): void {
|
|
46
46
|
this.status = s;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/** @internal — public alias for the protected emit so the parent
|
|
50
|
+
* `FileTransferChannel` (a different class instance) can dispatch events
|
|
51
|
+
* on this handle. Protected access is by-class, not by-hierarchy. */
|
|
52
|
+
_emit(event: string, ...args: unknown[]): void {
|
|
53
|
+
this.emit(event, ...args);
|
|
54
|
+
}
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
interface ReceiveState {
|
|
@@ -102,7 +109,7 @@ export class FileTransferChannel extends EventEmitter {
|
|
|
102
109
|
const channel = this.router.getChannel(CHANNEL_NAMES.FILE_TRANSFER);
|
|
103
110
|
if (!channel || channel.readyState !== "open") {
|
|
104
111
|
handle._setStatus("error");
|
|
105
|
-
handle.
|
|
112
|
+
handle._emit("error", new Error("File transfer channel not open"));
|
|
106
113
|
return handle;
|
|
107
114
|
}
|
|
108
115
|
|
|
@@ -174,7 +181,7 @@ export class FileTransferChannel extends EventEmitter {
|
|
|
174
181
|
channel.send(completeMsg);
|
|
175
182
|
|
|
176
183
|
handle._setStatus("complete");
|
|
177
|
-
handle.
|
|
184
|
+
handle._emit("complete");
|
|
178
185
|
|
|
179
186
|
return handle;
|
|
180
187
|
}
|