@abraca/dabra 1.0.16 → 1.0.18
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 +72 -6
- package/dist/abracadabra-provider.cjs.map +1 -1
- package/dist/abracadabra-provider.esm.js +72 -6
- package/dist/abracadabra-provider.esm.js.map +1 -1
- package/dist/index.d.ts +47 -4
- package/package.json +1 -1
- package/src/AbracadabraClient.ts +32 -0
- package/src/AbracadabraProvider.ts +21 -2
- package/src/BackgroundSyncManager.ts +59 -9
- package/src/types.ts +16 -4
package/dist/index.d.ts
CHANGED
|
@@ -371,6 +371,8 @@ declare class AbracadabraClient {
|
|
|
371
371
|
doc_type?: string;
|
|
372
372
|
label?: string;
|
|
373
373
|
}): Promise<DocumentMeta>;
|
|
374
|
+
/** Broadcast a stateless message to all connected clients on a document (requires manage permission). */
|
|
375
|
+
broadcast(docId: string, payload: string): Promise<void>;
|
|
374
376
|
/** List all permissions for a document (requires read access). */
|
|
375
377
|
listPermissions(docId: string): Promise<PermissionEntry[]>;
|
|
376
378
|
/** List effective permissions including inherited ones from ancestor documents. */
|
|
@@ -421,6 +423,26 @@ declare class AbracadabraClient {
|
|
|
421
423
|
updateSpace(spaceId: string, opts: Partial<Pick<SpaceMeta, "name" | "description" | "visibility" | "is_hub">>): Promise<SpaceMeta>;
|
|
422
424
|
/** Delete a space and its root document (Owner or admin required). */
|
|
423
425
|
deleteSpace(spaceId: string): Promise<void>;
|
|
426
|
+
/** List all users (requires elevated role: admin or service). */
|
|
427
|
+
adminListUsers(): Promise<{
|
|
428
|
+
users: (UserProfile & {
|
|
429
|
+
revoked: boolean;
|
|
430
|
+
deviceKeys: string[];
|
|
431
|
+
})[];
|
|
432
|
+
}>;
|
|
433
|
+
/** Promote a user to admin (requires service role). */
|
|
434
|
+
adminPromote(userId: string): Promise<void>;
|
|
435
|
+
/** Demote an admin user back to regular (requires service role). */
|
|
436
|
+
adminDemote(userId: string): Promise<void>;
|
|
437
|
+
/** Sweep orphaned file blobs from storage (requires elevated role). */
|
|
438
|
+
adminStorageSweep(): Promise<{
|
|
439
|
+
blobsDeleted: number;
|
|
440
|
+
}>;
|
|
441
|
+
/** Repair blob ref-counts and sweep orphans (requires elevated role). */
|
|
442
|
+
adminStorageRepair(): Promise<{
|
|
443
|
+
refCountsRepaired: number;
|
|
444
|
+
blobsSwept: number;
|
|
445
|
+
}>;
|
|
424
446
|
/** Health check — no auth required. */
|
|
425
447
|
health(): Promise<HealthStatus>;
|
|
426
448
|
/**
|
|
@@ -645,6 +667,7 @@ declare class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
645
667
|
private handleAuthChallenge;
|
|
646
668
|
private restorePermissionSnapshot;
|
|
647
669
|
get canWrite(): boolean;
|
|
670
|
+
get canAwareness(): boolean;
|
|
648
671
|
/** The AbracadabraClient instance for REST API access, if configured. */
|
|
649
672
|
get client(): AbracadabraClient | null;
|
|
650
673
|
/** The OfflineStore instance, or null if offline storage is disabled. */
|
|
@@ -864,7 +887,7 @@ declare enum WebSocketStatus {
|
|
|
864
887
|
Connected = "connected",
|
|
865
888
|
Disconnected = "disconnected"
|
|
866
889
|
}
|
|
867
|
-
type AuthorizedScope = "service" | "admin" | "owner" | "editor" | "viewer" | "read-write" | "readonly";
|
|
890
|
+
type AuthorizedScope = "service" | "admin" | "owner" | "editor" | "viewer" | "observer" | "read-write" | "readonly";
|
|
868
891
|
interface OutgoingMessageInterface {
|
|
869
892
|
encoder: Encoder;
|
|
870
893
|
type?: MessageType;
|
|
@@ -930,7 +953,7 @@ type StatesArray = {
|
|
|
930
953
|
clientId: number;
|
|
931
954
|
[key: string | number]: any;
|
|
932
955
|
}[];
|
|
933
|
-
type EffectiveRole = "service" | "admin" | "owner" | "editor" | "viewer" | null;
|
|
956
|
+
type EffectiveRole = "service" | "admin" | "owner" | "editor" | "viewer" | "observer" | null;
|
|
934
957
|
/**
|
|
935
958
|
* Ed25519 identity for passwordless crypto auth.
|
|
936
959
|
*
|
|
@@ -966,6 +989,8 @@ interface UserProfile {
|
|
|
966
989
|
interface DocumentMeta {
|
|
967
990
|
id: string;
|
|
968
991
|
parent_id: string | null;
|
|
992
|
+
doc_type?: string | null;
|
|
993
|
+
label?: string | null;
|
|
969
994
|
}
|
|
970
995
|
interface UploadMeta {
|
|
971
996
|
id: string;
|
|
@@ -986,13 +1011,13 @@ interface PublicKeyInfo {
|
|
|
986
1011
|
}
|
|
987
1012
|
interface PermissionEntry {
|
|
988
1013
|
user_id: string;
|
|
989
|
-
role: "owner" | "editor" | "viewer" | "observer";
|
|
1014
|
+
role: "service" | "admin" | "owner" | "editor" | "viewer" | "observer";
|
|
990
1015
|
username: string;
|
|
991
1016
|
display_name: string | null;
|
|
992
1017
|
}
|
|
993
1018
|
interface EffectivePermissionEntry {
|
|
994
1019
|
user_id: string;
|
|
995
|
-
role: "owner" | "editor" | "viewer" | "observer";
|
|
1020
|
+
role: "service" | "admin" | "owner" | "editor" | "viewer" | "observer";
|
|
996
1021
|
username: string;
|
|
997
1022
|
display_name: string | null;
|
|
998
1023
|
source: "direct" | "inherited";
|
|
@@ -1012,10 +1037,23 @@ interface ServerInfo {
|
|
|
1012
1037
|
name?: string;
|
|
1013
1038
|
/** Server version string. */
|
|
1014
1039
|
version?: string;
|
|
1040
|
+
/** Hocuspocus wire protocol version (currently 2). */
|
|
1041
|
+
protocol_version?: number;
|
|
1015
1042
|
/** Entry-point document ID advertised by the server, if configured. */
|
|
1016
1043
|
index_doc_id?: string;
|
|
1017
1044
|
/** Default role assigned to users without explicit permissions. */
|
|
1018
1045
|
default_role?: string;
|
|
1046
|
+
/** Enabled auth methods (e.g. ["crypto", "jwt"]). */
|
|
1047
|
+
auth_methods?: string[];
|
|
1048
|
+
/** Whether open registration is enabled. */
|
|
1049
|
+
registration_allowed?: boolean;
|
|
1050
|
+
/** Whether an invite code is required to register. */
|
|
1051
|
+
invite_only?: boolean;
|
|
1052
|
+
/** Server encryption configuration. */
|
|
1053
|
+
encryption?: {
|
|
1054
|
+
default_mode?: string;
|
|
1055
|
+
minimum_mode?: string;
|
|
1056
|
+
};
|
|
1019
1057
|
}
|
|
1020
1058
|
interface SearchResult {
|
|
1021
1059
|
docId: string;
|
|
@@ -1609,6 +1647,10 @@ interface BackgroundSyncManagerOptions {
|
|
|
1609
1647
|
syncTimeout?: number;
|
|
1610
1648
|
/** Pre-cache file blobs after syncing a doc. Default: true. */
|
|
1611
1649
|
prefetchFiles?: boolean;
|
|
1650
|
+
/** Delay (ms) between starting each doc sync to avoid server pressure. Default: 50. */
|
|
1651
|
+
throttleMs?: number;
|
|
1652
|
+
/** Max retries for failed docs within a single syncAll() run. Default: 2. */
|
|
1653
|
+
maxRetries?: number;
|
|
1612
1654
|
}
|
|
1613
1655
|
declare class BackgroundSyncManager extends EventEmitter {
|
|
1614
1656
|
private readonly rootProvider;
|
|
@@ -1654,6 +1696,7 @@ declare class BackgroundSyncManager extends EventEmitter {
|
|
|
1654
1696
|
* 3. Errored docs last
|
|
1655
1697
|
*/
|
|
1656
1698
|
private _buildQueue;
|
|
1699
|
+
/** Returns true on success (or skip), false on error. */
|
|
1657
1700
|
private _syncWithSemaphore;
|
|
1658
1701
|
private _doSyncDoc;
|
|
1659
1702
|
private _syncNonE2EDoc;
|
package/package.json
CHANGED
package/src/AbracadabraClient.ts
CHANGED
|
@@ -336,6 +336,11 @@ export class AbracadabraClient {
|
|
|
336
336
|
);
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/** Broadcast a stateless message to all connected clients on a document (requires manage permission). */
|
|
340
|
+
async broadcast(docId: string, payload: string): Promise<void> {
|
|
341
|
+
await this.request("POST", `/docs/${encodeURIComponent(docId)}/broadcast`, { body: { payload } });
|
|
342
|
+
}
|
|
343
|
+
|
|
339
344
|
// ── Permissions ──────────────────────────────────────────────────────────
|
|
340
345
|
|
|
341
346
|
/** List all permissions for a document (requires read access). */
|
|
@@ -535,6 +540,33 @@ export class AbracadabraClient {
|
|
|
535
540
|
await this.request("DELETE", `/spaces/${encodeURIComponent(spaceId)}`);
|
|
536
541
|
}
|
|
537
542
|
|
|
543
|
+
// ── Admin ───────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
/** List all users (requires elevated role: admin or service). */
|
|
546
|
+
async adminListUsers(): Promise<{ users: (UserProfile & { revoked: boolean; deviceKeys: string[] })[] }> {
|
|
547
|
+
return this.request("GET", "/admin/users");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** Promote a user to admin (requires service role). */
|
|
551
|
+
async adminPromote(userId: string): Promise<void> {
|
|
552
|
+
await this.request("POST", `/admin/users/${encodeURIComponent(userId)}/admin`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Demote an admin user back to regular (requires service role). */
|
|
556
|
+
async adminDemote(userId: string): Promise<void> {
|
|
557
|
+
await this.request("DELETE", `/admin/users/${encodeURIComponent(userId)}/admin`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** Sweep orphaned file blobs from storage (requires elevated role). */
|
|
561
|
+
async adminStorageSweep(): Promise<{ blobsDeleted: number }> {
|
|
562
|
+
return this.request("POST", "/admin/storage/sweep");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Repair blob ref-counts and sweep orphans (requires elevated role). */
|
|
566
|
+
async adminStorageRepair(): Promise<{ refCountsRepaired: number; blobsSwept: number }> {
|
|
567
|
+
return this.request("POST", "/admin/storage/repair");
|
|
568
|
+
}
|
|
569
|
+
|
|
538
570
|
// ── System ───────────────────────────────────────────────────────────────
|
|
539
571
|
|
|
540
572
|
/** Health check — no auth required. */
|
|
@@ -226,10 +226,11 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
226
226
|
owner: "owner",
|
|
227
227
|
editor: "editor",
|
|
228
228
|
viewer: "viewer",
|
|
229
|
+
observer: "observer",
|
|
229
230
|
"read-write": "editor",
|
|
230
231
|
readonly: "viewer",
|
|
231
232
|
};
|
|
232
|
-
this.effectiveRole = roleMap[scope] ?? "
|
|
233
|
+
this.effectiveRole = roleMap[scope] ?? "observer";
|
|
233
234
|
|
|
234
235
|
this.offlineStore?.savePermissionSnapshot(this.effectiveRole);
|
|
235
236
|
}
|
|
@@ -303,7 +304,11 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
get canWrite(): boolean {
|
|
306
|
-
return this.effectiveRole != null && this.effectiveRole !== "viewer";
|
|
307
|
+
return this.effectiveRole != null && this.effectiveRole !== "viewer" && this.effectiveRole !== "observer";
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
get canAwareness(): boolean {
|
|
311
|
+
return this.effectiveRole != null && this.effectiveRole !== "observer";
|
|
307
312
|
}
|
|
308
313
|
|
|
309
314
|
/** The AbracadabraClient instance for REST API access, if configured. */
|
|
@@ -579,11 +584,25 @@ export class AbracadabraProvider extends AbracadabraBaseProvider {
|
|
|
579
584
|
override destroy() {
|
|
580
585
|
this.document.off("subdocs", this.boundHandleYSubdocsChange);
|
|
581
586
|
|
|
587
|
+
// Collect child IDs before destroying — detach() may skip cleanup if
|
|
588
|
+
// a new provider already overwrote the slot during teardown/re-init races.
|
|
589
|
+
const childIds = [...this.childProviders.keys()];
|
|
590
|
+
|
|
582
591
|
for (const provider of this.childProviders.values()) {
|
|
583
592
|
provider.destroy();
|
|
584
593
|
}
|
|
585
594
|
this.childProviders.clear();
|
|
586
595
|
|
|
596
|
+
// Force-clear any stale providerMap entries for our children.
|
|
597
|
+
// detach() only removes if current === provider, so orphans can remain
|
|
598
|
+
// when a new provider attached before the old one was destroyed.
|
|
599
|
+
const wsProviderMap = this.configuration.websocketProvider?.configuration?.providerMap;
|
|
600
|
+
if (wsProviderMap) {
|
|
601
|
+
for (const childId of childIds) {
|
|
602
|
+
wsProviderMap.delete(childId);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
587
606
|
this.offlineStore?.destroy();
|
|
588
607
|
this.offlineStore = null;
|
|
589
608
|
|
|
@@ -38,6 +38,10 @@ export interface BackgroundSyncManagerOptions {
|
|
|
38
38
|
syncTimeout?: number;
|
|
39
39
|
/** Pre-cache file blobs after syncing a doc. Default: true. */
|
|
40
40
|
prefetchFiles?: boolean;
|
|
41
|
+
/** Delay (ms) between starting each doc sync to avoid server pressure. Default: 50. */
|
|
42
|
+
throttleMs?: number;
|
|
43
|
+
/** Max retries for failed docs within a single syncAll() run. Default: 2. */
|
|
44
|
+
maxRetries?: number;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
@@ -96,6 +100,8 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
96
100
|
concurrency: opts?.concurrency ?? 2,
|
|
97
101
|
syncTimeout: opts?.syncTimeout ?? 15_000,
|
|
98
102
|
prefetchFiles: opts?.prefetchFiles ?? true,
|
|
103
|
+
throttleMs: opts?.throttleMs ?? 50,
|
|
104
|
+
maxRetries: opts?.maxRetries ?? 2,
|
|
99
105
|
};
|
|
100
106
|
|
|
101
107
|
// Derive server origin from client URL for IDB namespacing
|
|
@@ -155,12 +161,54 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
155
161
|
// Build the priority queue
|
|
156
162
|
const queue = this._buildQueue(entries);
|
|
157
163
|
|
|
158
|
-
// Sync all docs
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
)
|
|
164
|
+
// Sync all docs with throttling: stagger starts to avoid server pressure.
|
|
165
|
+
// We use a feeding loop instead of Promise.all(queue.map(...)) so that
|
|
166
|
+
// each new doc start is spaced by throttleMs.
|
|
167
|
+
const failed: string[] = [];
|
|
168
|
+
let idx = 0;
|
|
169
|
+
const next = async (): Promise<void> => {
|
|
170
|
+
while (idx < queue.length) {
|
|
171
|
+
if (this._destroyed) return;
|
|
172
|
+
const docId = queue[idx++]!;
|
|
173
|
+
const updatedAt = updatedAtMap.get(docId) ?? 0;
|
|
174
|
+
const ok = await this._syncWithSemaphore(docId, updatedAt);
|
|
175
|
+
if (!ok) failed.push(docId);
|
|
176
|
+
// Throttle between starts
|
|
177
|
+
if (this.opts.throttleMs > 0 && idx < queue.length) {
|
|
178
|
+
await new Promise((r) => setTimeout(r, this.opts.throttleMs));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Launch `concurrency` parallel workers feeding from the shared index.
|
|
184
|
+
const workers = Array.from({ length: this.opts.concurrency }, () => next());
|
|
185
|
+
await Promise.all(workers);
|
|
186
|
+
|
|
187
|
+
// Retry failed docs with increasing backoff.
|
|
188
|
+
for (let retry = 0; retry < this.opts.maxRetries && failed.length > 0; retry++) {
|
|
189
|
+
if (this._destroyed) return;
|
|
190
|
+
const batch = failed.splice(0, failed.length);
|
|
191
|
+
// Backoff: 2s, 4s, 8s...
|
|
192
|
+
const backoff = 2000 * 2 ** retry;
|
|
193
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
194
|
+
|
|
195
|
+
idx = 0;
|
|
196
|
+
const retryQueue = batch;
|
|
197
|
+
const retryNext = async (): Promise<void> => {
|
|
198
|
+
while (idx < retryQueue.length) {
|
|
199
|
+
if (this._destroyed) return;
|
|
200
|
+
const docId = retryQueue[idx++]!;
|
|
201
|
+
const updatedAt = updatedAtMap.get(docId) ?? 0;
|
|
202
|
+
const ok = await this._syncWithSemaphore(docId, updatedAt);
|
|
203
|
+
if (!ok) failed.push(docId);
|
|
204
|
+
if (this.opts.throttleMs > 0 && idx < retryQueue.length) {
|
|
205
|
+
await new Promise((r) => setTimeout(r, this.opts.throttleMs));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const retryWorkers = Array.from({ length: this.opts.concurrency }, () => retryNext());
|
|
210
|
+
await Promise.all(retryWorkers);
|
|
211
|
+
}
|
|
164
212
|
}
|
|
165
213
|
|
|
166
214
|
/** Sync a single document by ID. */
|
|
@@ -273,11 +321,12 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
273
321
|
return items.map((i) => i.docId);
|
|
274
322
|
}
|
|
275
323
|
|
|
324
|
+
/** Returns true on success (or skip), false on error. */
|
|
276
325
|
private async _syncWithSemaphore(
|
|
277
326
|
docId: string,
|
|
278
327
|
updatedAt: number,
|
|
279
|
-
): Promise<
|
|
280
|
-
if (this._destroyed) return;
|
|
328
|
+
): Promise<boolean> {
|
|
329
|
+
if (this._destroyed) return true;
|
|
281
330
|
|
|
282
331
|
// Skip if already synced and doc hasn't changed since last sync
|
|
283
332
|
const existing = this.syncStates.get(docId);
|
|
@@ -288,7 +337,7 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
288
337
|
existing.lastSynced >= updatedAt
|
|
289
338
|
) {
|
|
290
339
|
this.emit("stateChanged", { docId, state: existing });
|
|
291
|
-
return;
|
|
340
|
+
return true;
|
|
292
341
|
}
|
|
293
342
|
|
|
294
343
|
await this.semaphore.acquire();
|
|
@@ -297,6 +346,7 @@ export class BackgroundSyncManager extends EventEmitter {
|
|
|
297
346
|
this.syncStates.set(docId, state);
|
|
298
347
|
await this.persistence.setState(state).catch(() => null);
|
|
299
348
|
this.emit("stateChanged", { docId, state });
|
|
349
|
+
return state.status !== "error";
|
|
300
350
|
} finally {
|
|
301
351
|
this.semaphore.release();
|
|
302
352
|
}
|
package/src/types.ts
CHANGED
|
@@ -40,7 +40,7 @@ export enum WebSocketStatus {
|
|
|
40
40
|
Disconnected = "disconnected",
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export type AuthorizedScope = "service" | "admin" | "owner" | "editor" | "viewer" | "read-write" | "readonly";
|
|
43
|
+
export type AuthorizedScope = "service" | "admin" | "owner" | "editor" | "viewer" | "observer" | "read-write" | "readonly";
|
|
44
44
|
|
|
45
45
|
export interface OutgoingMessageInterface {
|
|
46
46
|
encoder: Encoder;
|
|
@@ -128,7 +128,7 @@ export type StatesArray = { clientId: number; [key: string | number]: any }[];
|
|
|
128
128
|
|
|
129
129
|
// ── Abracadabra extensions ────────────────────────────────────────────────────
|
|
130
130
|
|
|
131
|
-
export type EffectiveRole = "service" | "admin" | "owner" | "editor" | "viewer" | null;
|
|
131
|
+
export type EffectiveRole = "service" | "admin" | "owner" | "editor" | "viewer" | "observer" | null;
|
|
132
132
|
|
|
133
133
|
/**
|
|
134
134
|
* Ed25519 identity for passwordless crypto auth.
|
|
@@ -173,6 +173,8 @@ export interface UserProfile {
|
|
|
173
173
|
export interface DocumentMeta {
|
|
174
174
|
id: string;
|
|
175
175
|
parent_id: string | null;
|
|
176
|
+
doc_type?: string | null;
|
|
177
|
+
label?: string | null;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
export interface UploadMeta {
|
|
@@ -197,14 +199,14 @@ export interface PublicKeyInfo {
|
|
|
197
199
|
|
|
198
200
|
export interface PermissionEntry {
|
|
199
201
|
user_id: string;
|
|
200
|
-
role: "owner" | "editor" | "viewer" | "observer";
|
|
202
|
+
role: "service" | "admin" | "owner" | "editor" | "viewer" | "observer";
|
|
201
203
|
username: string;
|
|
202
204
|
display_name: string | null;
|
|
203
205
|
}
|
|
204
206
|
|
|
205
207
|
export interface EffectivePermissionEntry {
|
|
206
208
|
user_id: string;
|
|
207
|
-
role: "owner" | "editor" | "viewer" | "observer";
|
|
209
|
+
role: "service" | "admin" | "owner" | "editor" | "viewer" | "observer";
|
|
208
210
|
username: string;
|
|
209
211
|
display_name: string | null;
|
|
210
212
|
source: "direct" | "inherited";
|
|
@@ -227,10 +229,20 @@ export interface ServerInfo {
|
|
|
227
229
|
name?: string;
|
|
228
230
|
/** Server version string. */
|
|
229
231
|
version?: string;
|
|
232
|
+
/** Hocuspocus wire protocol version (currently 2). */
|
|
233
|
+
protocol_version?: number;
|
|
230
234
|
/** Entry-point document ID advertised by the server, if configured. */
|
|
231
235
|
index_doc_id?: string;
|
|
232
236
|
/** Default role assigned to users without explicit permissions. */
|
|
233
237
|
default_role?: string;
|
|
238
|
+
/** Enabled auth methods (e.g. ["crypto", "jwt"]). */
|
|
239
|
+
auth_methods?: string[];
|
|
240
|
+
/** Whether open registration is enabled. */
|
|
241
|
+
registration_allowed?: boolean;
|
|
242
|
+
/** Whether an invite code is required to register. */
|
|
243
|
+
invite_only?: boolean;
|
|
244
|
+
/** Server encryption configuration. */
|
|
245
|
+
encryption?: { default_mode?: string; minimum_mode?: string };
|
|
234
246
|
}
|
|
235
247
|
|
|
236
248
|
// ── Search ───────────────────────────────────────────────────────────────────
|