@brftech/filex-app-ui 0.54.0 → 0.56.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/src/protocol.ts CHANGED
@@ -52,7 +52,23 @@ export type Method =
52
52
  | 'state.get'
53
53
  | 'state.set'
54
54
  /** 0.52.0: what the app's license says (a paid app from a store), `LicenseInfo`. */
55
- | 'license.get';
55
+ | 'license.get'
56
+ /** 0.55.0: filex prints a PDF the app hands it (`PrintParams`, grant `ui:print`). */
57
+ | 'ui.print'
58
+ /**
59
+ * Editing together (`coedit.*`, task #189): the session's log the host
60
+ * seals, orders through filex's relay and opens again. Defined in filex
61
+ * 0.55, which answers each `unavailable`; offered from 0.56 to an app
62
+ * granted `files:co-edit` (see `CoEditHello`). `unavailable` = edit alone.
63
+ */
64
+ | 'coedit.join'
65
+ | 'coedit.subscribe'
66
+ | 'coedit.append'
67
+ | 'coedit.lease'
68
+ | 'coedit.cursor'
69
+ | 'coedit.blob.put'
70
+ | 'coedit.blob.get'
71
+ | 'coedit.leave';
56
72
 
57
73
  export const METHODS: readonly Method[] = [
58
74
  'session.get',
@@ -71,6 +87,15 @@ export const METHODS: readonly Method[] = [
71
87
  'state.get',
72
88
  'state.set',
73
89
  'license.get',
90
+ 'ui.print',
91
+ 'coedit.join',
92
+ 'coedit.subscribe',
93
+ 'coedit.append',
94
+ 'coedit.lease',
95
+ 'coedit.cursor',
96
+ 'coedit.blob.put',
97
+ 'coedit.blob.get',
98
+ 'coedit.leave',
74
99
  ];
75
100
 
76
101
  /** What the host tells the app, unasked. */
@@ -84,7 +109,18 @@ export type HostEvent =
84
109
  /** The person closed the frame's surroundings; last chance to say so. */
85
110
  | 'close.request'
86
111
  /** An administrator installed a new version of this app: reload to use it. */
87
- | 'app.updated';
112
+ | 'app.updated'
113
+ /** Editing together: the next entry of the session's log, opened. `data: CoEditEntry`. */
114
+ | 'coedit.entry'
115
+ /** Editing together: another member's cursor (not kept). `data: CoEditCursor`. */
116
+ | 'coedit.cursor'
117
+ /**
118
+ * Editing together: the host stopped handing entries over. `data:
119
+ * CoEditDropped` - `gone`: the membership ended (join again); `broken`: an
120
+ * entry did not check out (stop editing together). A connection that drops
121
+ * for a moment is the host's own: it reconnects and says nothing.
122
+ */
123
+ | 'coedit.dropped';
88
124
 
89
125
  /** What the host may ask the app (and wait for). */
90
126
  export type HostRequest =
@@ -178,6 +214,49 @@ export interface FileInfo {
178
214
  /** It cannot be saved over: read-only storage, view-only opening, or the
179
215
  * app holds no `files:write`. */
180
216
  readOnly: boolean;
217
+ /**
218
+ * 0.55.0: the file is end-to-end encrypted - `folder` in an encrypted
219
+ * folder, `vault` in a vault (where only one person edits at a time),
220
+ * `file` for a single encrypted file (`.fxe`). As the server says it;
221
+ * ABSENT for a file that is not encrypted.
222
+ *
223
+ * ⚠ The server holds such a file only as ciphertext and has no key. Unless
224
+ * `plaintext` is true, filex hands the app nothing of it: `file.read` is
225
+ * refused (`failed` with `encrypted`), a save is refused, and `readOnly`
226
+ * is true.
227
+ */
228
+ encrypted?: 'folder' | 'vault' | 'file';
229
+ /**
230
+ * 0.56.0: filex hands this encrypted file to the app in the clear. The
231
+ * person's browser, which holds the folder key while the folder is
232
+ * unlocked, decrypts it for `file.read` and encrypts what `file.save`
233
+ * hands over before it leaves the browser; the server sees only
234
+ * ciphertext. Only with `encrypted: 'folder'` or `'vault'`, for an app
235
+ * whose manifest says `encrypted_folders: {"open": true}` (the grant
236
+ * `files:e2e-plaintext`) - never for a `.fxe`. ABSENT otherwise.
237
+ *
238
+ * In a vault (`encrypted: 'vault'`) the browser reads the file from the
239
+ * vault's packs and writes a save as the vault's next generation, under
240
+ * the vault's write lock (one writer at a time). A save is refused -
241
+ * `failed` with `vault_locked` - while somebody else writes the vault, and
242
+ * with `vault_lock_lost` when this session's write lock ended before the
243
+ * save was committed: nothing was written, save again. The vault locked
244
+ * meanwhile (its keys left the browser): `unavailable`, and filex closes
245
+ * the app.
246
+ *
247
+ * What changes for the app: a read and a save are held whole in the page
248
+ * (at most LIMITS.maxEncryptedBytes); a save is written only over the file
249
+ * as it was opened (`failed` with `changed` when it changed meanwhile);
250
+ * `file.saveAs` answers `unavailable` (a copy elsewhere would leave the
251
+ * folder decrypted); the app's module (`engine.call`, `job.submit`) is
252
+ * never handed the file; `state.get` / `state.set` use a store of their
253
+ * own for this folder, sealed in the person's browser (the server keeps it
254
+ * and cannot read it; apart from the app's ordinary store, a little smaller
255
+ * as it is measured sealed). Everything the app holds is plaintext: keep it
256
+ * in the frame. In a folder whose names are encrypted too, `name` and
257
+ * `ext` are the decrypted name's - the server never sees them.
258
+ */
259
+ plaintext?: true;
181
260
  }
182
261
 
183
262
  /** What `session.get` answers — what the app knows about where it runs. */
@@ -221,6 +300,12 @@ export interface SaveParams {
221
300
  /** A transferred ReadableStream<Uint8Array>, an ArrayBuffer or a string. */
222
301
  data: ReadableStream<Uint8Array> | ArrayBuffer | string;
223
302
  mime?: string;
303
+ /**
304
+ * 0.55.0, editing together: the last log entry (`CoEditEntry.seq`) the
305
+ * saved document holds, so the session knows what is saved. Ignored
306
+ * outside a session.
307
+ */
308
+ through?: number;
224
309
  }
225
310
 
226
311
  export interface SaveResult {
@@ -246,6 +331,172 @@ export interface DownloadResult {
246
331
  size: number;
247
332
  }
248
333
 
334
+ /**
335
+ * `ui.print` (filex 0.55.0): a PDF for the browser's print dialog. A
336
+ * sandboxed frame may not open that dialog (measured: Chromium ignores
337
+ * `print()` there and says so, Firefox ignores it), so filex prints the PDF
338
+ * from a page of its own, with the app's `ui:print` grant, never over
339
+ * LIMITS.maxPrintBytes. filex asks every time, above the frame, and the
340
+ * dialog opens only on the person's click on filex's Allow. The print dialog
341
+ * can save the PDF too, so `ui:print` is the same kind of grant as
342
+ * `ui:download`. filex 0.54 and older answer `unknown_method`.
343
+ */
344
+ export interface PrintParams {
345
+ /** The document's name, for the question filex may ask - a name, not a path. */
346
+ name: string;
347
+ /** The PDF: a transferred ArrayBuffer or ReadableStream<Uint8Array>. */
348
+ data: ReadableStream<Uint8Array> | ArrayBuffer;
349
+ /** `application/pdf` when given; nothing else is printed. */
350
+ mime?: string;
351
+ }
352
+
353
+ export interface PrintResult {
354
+ /** The browser's print dialog was opened with the PDF. */
355
+ printed: true;
356
+ size: number;
357
+ }
358
+
359
+ /* ── editing together (coedit.*: filex 0.55 defined, 0.56 offers) ──────── */
360
+
361
+ /** What a member appends: the editor's changes, a lock request, a release. */
362
+ export type CoEditKind = 'changes' | 'lock' | 'release';
363
+
364
+ /** A member of the session. */
365
+ export interface CoEditMember {
366
+ /** The relay's id for the connection. */
367
+ client: string;
368
+ /** An opaque id of the person, the same for each of their connections in
369
+ * this session; never an account id. */
370
+ user: string;
371
+ /** The person's display name. */
372
+ name: string;
373
+ /** The editor's per-session user index: given once, never twice. */
374
+ indexUser: number;
375
+ /** May append changes (an editor role and the right to change the file). */
376
+ canEdit: boolean;
377
+ }
378
+
379
+ /** `coedit.join` params: join (or start) the session of an opened file. */
380
+ export interface CoEditJoinParams {
381
+ index?: number;
382
+ }
383
+
384
+ /**
385
+ * What `coedit.join` answers: who the app is in the session and how far
386
+ * the log has gone. The entries come as `coedit.entry` events once the app
387
+ * subscribes (`coedit.subscribe` from 0 for the whole log). `unavailable`
388
+ * when filex offers no editing together here (filex 0.55, editing together
389
+ * turned off, an encrypted file, a vault): the app edits alone - the message
390
+ * says why (`not_ready` after waiting for a session's creator, `encrypted`,
391
+ * `nothing_to_watch` for a person who may only read the file while nobody
392
+ * edits it, or the server's code: `coedit_off`, `coedit_build`,
393
+ * `coedit_file_changed`).
394
+ */
395
+ export interface CoEditHello {
396
+ session: string;
397
+ me: CoEditMember;
398
+ /**
399
+ * The last entry's seq when filex handed the session over (0: an empty
400
+ * log). filex 0.56: the changes this browser kept from an earlier opening
401
+ * of the file (a lost connection, a closed tab), which filex sends before
402
+ * it answers, are in the log up to here - an editor that starts once it
403
+ * has read this far opens with them.
404
+ */
405
+ head: number;
406
+ /** The last `changes` entry's seq, as far as `head`. */
407
+ changesHead: number;
408
+ /** The last entry the file on the storage holds. */
409
+ savedThrough: number;
410
+ /**
411
+ * filex 0.56: this member started the session. Before anybody else can
412
+ * join it puts the base document - EXACTLY the bytes it opened - with
413
+ * `coedit.blob.put('base', bytes)`. A member that did not start it opens
414
+ * `coedit.blob.get('base')`, never the file: the file may hold a save of
415
+ * the session already, and the log applies to the base.
416
+ */
417
+ created: boolean;
418
+ }
419
+
420
+ /** `coedit.dropped` event data (filex 0.56). */
421
+ export interface CoEditDropped {
422
+ /** The last entry the host handed over. */
423
+ from: number;
424
+ /** `gone`: the membership ended (the session closed, or the page was away
425
+ * too long) - join again. `broken`: an entry did not check out (missing,
426
+ * moved, altered, replayed) - stop editing together. */
427
+ reason: 'gone' | 'broken';
428
+ }
429
+
430
+ /** `coedit.subscribe` params: entries after `from` arrive as `coedit.entry`. */
431
+ export interface CoEditSubscribeParams {
432
+ from: number;
433
+ }
434
+
435
+ /** `coedit.append` params: the host seals it and the relay places it. */
436
+ export interface CoEditAppendParams {
437
+ kind: CoEditKind;
438
+ body: unknown;
439
+ }
440
+
441
+ export interface CoEditAppendResult {
442
+ /** Where it landed; it comes back as a `coedit.entry` too. */
443
+ seq: number;
444
+ }
445
+
446
+ /** `coedit.lease`: the changes lease (one writer at a time, one that has
447
+ * seen every change). */
448
+ export interface CoEditLeaseParams {
449
+ op: 'acquire' | 'release';
450
+ /** The seq of the last `changes` entry the app applied. */
451
+ changesSeen: number;
452
+ }
453
+
454
+ export interface CoEditLeaseResult {
455
+ granted: boolean;
456
+ }
457
+
458
+ /** `coedit.cursor` params: passed to the other members, not kept. The host
459
+ * sends a few a second at most (the last one wins). */
460
+ export interface CoEditCursorParams {
461
+ cursor: unknown;
462
+ }
463
+
464
+ /** `coedit.blob.put` / `coedit.blob.get`: the session's sealed blobs (the
465
+ * base document, images added while editing). A name, not a path. */
466
+ export interface CoEditBlobPutParams {
467
+ name: string;
468
+ data: ArrayBuffer;
469
+ }
470
+
471
+ export interface CoEditBlobGetParams {
472
+ name: string;
473
+ }
474
+
475
+ export interface CoEditBlobGetResult {
476
+ name: string;
477
+ bytes: ArrayBuffer;
478
+ }
479
+
480
+ interface CoEditEntryBase {
481
+ seq: number;
482
+ /** The relay's time, Unix ms - the same for every member. */
483
+ at: number;
484
+ client: string;
485
+ }
486
+
487
+ /** One entry of the session's log, opened and checked by filex. */
488
+ export type CoEditEntry =
489
+ | (CoEditEntryBase & { kind: CoEditKind; body: unknown })
490
+ | (CoEditEntryBase & { kind: 'join'; member: CoEditMember })
491
+ | (CoEditEntryBase & { kind: 'leave' })
492
+ | (CoEditEntryBase & { kind: 'saved'; through: number });
493
+
494
+ /** `coedit.cursor` event data. */
495
+ export interface CoEditCursor {
496
+ client: string;
497
+ cursor: unknown;
498
+ }
499
+
249
500
  /**
250
501
  * `file.saveAs`: a NEW file, in a folder the person picks in filex's own
251
502
  * folder dialog (the one its Move to… uses), which opens in the opened
@@ -325,6 +576,20 @@ export const LIMITS = {
325
576
  connectTimeoutMs: 10_000,
326
577
  /** The largest file `ui.download` hands the person (streamed or not). */
327
578
  maxDownloadBytes: 256 << 20,
579
+ /**
580
+ * The largest PDF `ui.print` prints (filex 0.55). Lower than a download:
581
+ * a download can stream to disk, a print is held whole in the page (one
582
+ * Blob) until the print dialog has read it, and a document worth printing
583
+ * is far smaller.
584
+ */
585
+ maxPrintBytes: 64 << 20,
586
+ /**
587
+ * The largest file of an encrypted folder filex hands an app in the clear
588
+ * or takes back from it (filex 0.56, `FileInfo.plaintext`): it is
589
+ * decrypted and encrypted whole in the page, as filex's own explorer does
590
+ * for a file of this size.
591
+ */
592
+ maxEncryptedBytes: 200 << 20,
328
593
  } as const;
329
594
 
330
595
  /* ── guards ────────────────────────────────────────────────────────────── */