fauna-api 0.0.0-stage → 1.0.679
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/brands.d.ts +2 -0
- package/dist/brands.js +2 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +121 -0
- package/dist/src/ant/chat.d.ts +130 -0
- package/dist/src/ant/chat.js +467 -0
- package/dist/src/ant/format.d.ts +40 -0
- package/dist/src/ant/format.js +143 -0
- package/dist/src/ant/graph.d.ts +130 -0
- package/dist/src/ant/graph.js +449 -0
- package/dist/src/ant/index.d.ts +16 -0
- package/dist/src/ant/index.js +61 -0
- package/dist/src/ant/persona.d.ts +151 -0
- package/dist/src/ant/persona.js +136 -0
- package/dist/src/ant/timeline.d.ts +283 -0
- package/dist/src/ant/timeline.js +1025 -0
- package/dist/src/authentification.d.ts +45 -0
- package/dist/src/authentification.js +194 -0
- package/dist/src/billing.d.ts +135 -0
- package/dist/src/billing.js +128 -0
- package/dist/src/calendar.d.ts +83 -0
- package/dist/src/calendar.js +133 -0
- package/dist/src/chats.d.ts +113 -0
- package/dist/src/chats.js +100 -0
- package/dist/src/cicada/contacts.d.ts +31 -0
- package/dist/src/cicada/contacts.js +28 -0
- package/dist/src/cicada/conversations.d.ts +73 -0
- package/dist/src/cicada/conversations.js +236 -0
- package/dist/src/cicada/inboxes.d.ts +48 -0
- package/dist/src/cicada/inboxes.js +82 -0
- package/dist/src/cicada/index.d.ts +16 -0
- package/dist/src/cicada/index.js +61 -0
- package/dist/src/cicada/phone.d.ts +48 -0
- package/dist/src/cicada/phone.js +306 -0
- package/dist/src/drive.d.ts +80 -0
- package/dist/src/drive.js +472 -0
- package/dist/src/extensions/index.d.ts +4 -0
- package/dist/src/extensions/index.js +5 -0
- package/dist/src/extensions/web/index.d.ts +6 -0
- package/dist/src/extensions/web/index.js +7 -0
- package/dist/src/extensions/web/session.d.ts +108 -0
- package/dist/src/extensions/web/session.js +470 -0
- package/dist/src/extensions/web/tools.d.ts +13 -0
- package/dist/src/extensions/web/tools.js +72 -0
- package/dist/src/friends.d.ts +39 -0
- package/dist/src/friends.js +119 -0
- package/dist/src/groups.d.ts +82 -0
- package/dist/src/groups.js +269 -0
- package/dist/src/inbox.d.ts +11 -0
- package/dist/src/inbox.js +35 -0
- package/dist/src/integrations.d.ts +52 -0
- package/dist/src/integrations.js +205 -0
- package/dist/src/magpie/explorer.d.ts +14 -0
- package/dist/src/magpie/explorer.js +36 -0
- package/dist/src/magpie/index.d.ts +16 -0
- package/dist/src/magpie/index.js +61 -0
- package/dist/src/magpie/portfolio.d.ts +52 -0
- package/dist/src/magpie/portfolio.js +179 -0
- package/dist/src/magpie/quotes.d.ts +35 -0
- package/dist/src/magpie/quotes.js +47 -0
- package/dist/src/magpie/room.d.ts +61 -0
- package/dist/src/magpie/room.js +238 -0
- package/dist/src/maps.d.ts +159 -0
- package/dist/src/maps.js +171 -0
- package/dist/src/marmot/browser.d.ts +139 -0
- package/dist/src/marmot/browser.js +389 -0
- package/dist/src/marmot/index.d.ts +20 -0
- package/dist/src/marmot/index.js +65 -0
- package/dist/src/marmot/media.d.ts +56 -0
- package/dist/src/marmot/media.js +265 -0
- package/dist/src/marmot/research.d.ts +337 -0
- package/dist/src/marmot/research.js +1138 -0
- package/dist/src/marmot/scraper.d.ts +59 -0
- package/dist/src/marmot/scraper.js +94 -0
- package/dist/src/marmot/web.d.ts +402 -0
- package/dist/src/marmot/web.js +2586 -0
- package/dist/src/marmot/workspace.d.ts +81 -0
- package/dist/src/marmot/workspace.js +263 -0
- package/dist/src/parrot/agent.d.ts +53 -0
- package/dist/src/parrot/agent.js +93 -0
- package/dist/src/parrot/chat.d.ts +84 -0
- package/dist/src/parrot/chat.js +237 -0
- package/dist/src/parrot/format.d.ts +128 -0
- package/dist/src/parrot/format.js +639 -0
- package/dist/src/parrot/generative.d.ts +261 -0
- package/dist/src/parrot/generative.js +185 -0
- package/dist/src/parrot/index.d.ts +22 -0
- package/dist/src/parrot/index.js +67 -0
- package/dist/src/parrot/room.d.ts +225 -0
- package/dist/src/parrot/room.js +466 -0
- package/dist/src/parrot/routing.d.ts +90 -0
- package/dist/src/parrot/routing.js +78 -0
- package/dist/src/parrot/share.d.ts +126 -0
- package/dist/src/parrot/share.js +261 -0
- package/dist/src/parrot/socket.d.ts +41 -0
- package/dist/src/parrot/socket.js +478 -0
- package/dist/src/payments.d.ts +73 -0
- package/dist/src/payments.js +107 -0
- package/dist/src/private/cards.css +75 -0
- package/dist/src/rooms.d.ts +76 -0
- package/dist/src/rooms.js +168 -0
- package/dist/src/shared/schema.d.ts +24 -0
- package/dist/src/shared/schema.js +1 -0
- package/dist/src/shared/server.d.ts +65 -0
- package/dist/src/shared/server.js +302 -0
- package/dist/src/termite/environment.d.ts +110 -0
- package/dist/src/termite/environment.js +263 -0
- package/dist/src/termite/index.d.ts +14 -0
- package/dist/src/termite/index.js +59 -0
- package/dist/src/termite/os.d.ts +326 -0
- package/dist/src/termite/os.js +572 -0
- package/dist/src/termite/versioning.d.ts +94 -0
- package/dist/src/termite/versioning.js +217 -0
- package/dist/src/tickets.d.ts +43 -0
- package/dist/src/tickets.js +138 -0
- package/dist/src/travel.d.ts +85 -0
- package/dist/src/travel.js +44 -0
- package/dist/src/ui/authentification.d.ts +24 -0
- package/dist/src/ui/authentification.js +177 -0
- package/dist/src/ui/brands.css +11 -0
- package/dist/src/ui/brands.d.ts +7 -0
- package/dist/src/ui/brands.js +14 -0
- package/dist/src/ui/browser.d.ts +74 -0
- package/dist/src/ui/browser.js +470 -0
- package/dist/src/ui/chat.d.ts +183 -0
- package/dist/src/ui/chat.js +2379 -0
- package/dist/src/ui/chatPanel.d.ts +109 -0
- package/dist/src/ui/chatPanel.js +768 -0
- package/dist/src/ui/chatSequence.d.ts +36 -0
- package/dist/src/ui/chatSequence.js +42 -0
- package/dist/src/ui/crown.d.ts +19 -0
- package/dist/src/ui/crown.js +199 -0
- package/dist/src/ui/cursor.d.ts +73 -0
- package/dist/src/ui/cursor.js +175 -0
- package/dist/src/ui/device.d.ts +33 -0
- package/dist/src/ui/device.js +49 -0
- package/dist/src/ui/drive.d.ts +69 -0
- package/dist/src/ui/drive.js +1243 -0
- package/dist/src/ui/generative.d.ts +39 -0
- package/dist/src/ui/generative.js +313 -0
- package/dist/src/ui/hover.d.ts +6 -0
- package/dist/src/ui/hover.js +210 -0
- package/dist/src/ui/index.d.ts +42 -0
- package/dist/src/ui/index.js +87 -0
- package/dist/src/ui/integrations.d.ts +13 -0
- package/dist/src/ui/integrations.js +175 -0
- package/dist/src/ui/maps.d.ts +87 -0
- package/dist/src/ui/maps.js +547 -0
- package/dist/src/ui/roster.d.ts +32 -0
- package/dist/src/ui/roster.js +163 -0
- package/dist/src/ui/talk.d.ts +12 -0
- package/dist/src/ui/talk.js +560 -0
- package/dist/src/ui/tickets.d.ts +13 -0
- package/dist/src/ui/tickets.js +966 -0
- package/dist/src/ui/toast.d.ts +7 -0
- package/dist/src/ui/toast.js +258 -0
- package/dist/src/ui/tokenText.d.ts +18 -0
- package/dist/src/ui/tokenText.js +96 -0
- package/dist/src/ui/tokenized.d.ts +42 -0
- package/dist/src/ui/tokenized.js +622 -0
- package/dist/src/ui/ui.css +4763 -0
- package/dist/src/ui/view.d.ts +23 -0
- package/dist/src/ui/view.js +584 -0
- package/dist/src/view.d.ts +113 -0
- package/dist/src/view.js +386 -0
- package/dist/src/webhooks.d.ts +66 -0
- package/dist/src/webhooks.js +78 -0
- package/dist/ui.d.ts +1 -0
- package/dist/ui.js +1 -0
- package/package.json +87 -4
- package/README.md +0 -3
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { Room, Presence, Authorization } from "./shared/server.js";
|
|
3
|
+
import type { File as DriveFile } from "./drive.js";
|
|
4
|
+
import type { Attachment } from "./parrot/share.js";
|
|
5
|
+
/**
|
|
6
|
+
* What a file opens as in its live room: `text` and `table` are edited together,
|
|
7
|
+
* `image` is annotated together, and `view` (video, audio, PDF, anything else or
|
|
8
|
+
* too large to edit) is looked at together.
|
|
9
|
+
*/
|
|
10
|
+
export type Kind = 'text' | 'table' | 'image' | 'view';
|
|
11
|
+
/** What you may do in a file room: `read` for share links, `write` for owners, group members and chat members. */
|
|
12
|
+
export type Role = 'read' | 'write';
|
|
13
|
+
/** Text and tables over this size open as `view`. */
|
|
14
|
+
export declare const EDIT_MAX_BYTES: number;
|
|
15
|
+
/** True for files whose bytes are text. */
|
|
16
|
+
export declare function isText(contentType: string, name: string): boolean;
|
|
17
|
+
/** What a file opens as, by its type, name and size. */
|
|
18
|
+
export declare function kindOf(contentType: string, name: string, size: number): Kind;
|
|
19
|
+
/** A mark on an image. Points run from 0 to 1 across the image's width and height. */
|
|
20
|
+
export type Mark = {
|
|
21
|
+
id: string;
|
|
22
|
+
kind: 'pen' | 'box' | 'arrow' | 'note' | 'pin';
|
|
23
|
+
points: [number, number][];
|
|
24
|
+
/** A note's or pin's words. */
|
|
25
|
+
text?: string;
|
|
26
|
+
/** Who made it, by email. */
|
|
27
|
+
author: string;
|
|
28
|
+
color: string;
|
|
29
|
+
at: number;
|
|
30
|
+
};
|
|
31
|
+
/** Rows of cells from CSV text: quoted fields, doubled quotes and CRLF all read right. */
|
|
32
|
+
export declare function parseCSV(text: string): string[][];
|
|
33
|
+
/** CSV text from rows, quoting cells that need it. */
|
|
34
|
+
export declare function formatCSV(rows: string[][]): string;
|
|
35
|
+
/** The table's cells in order, as rows of text. */
|
|
36
|
+
export declare function tableRows(doc: Y.Doc): string[][];
|
|
37
|
+
/** Fill an empty table document from rows of text. */
|
|
38
|
+
export declare function fillTable(doc: Y.Doc, rows: string[][]): void;
|
|
39
|
+
/** Add a row (after `after`, or at the end) and return its id. */
|
|
40
|
+
export declare function addRow(doc: Y.Doc, after?: string): string;
|
|
41
|
+
/** Add a column (after `after`, or at the end) and return its id. */
|
|
42
|
+
export declare function addColumn(doc: Y.Doc, after?: string): string;
|
|
43
|
+
export declare function removeRow(doc: Y.Doc, row: string): void;
|
|
44
|
+
export declare function removeColumn(doc: Y.Doc, column: string): void;
|
|
45
|
+
/** A fresh document holding a file's content: its text, its CSV, or an image's marks as JSON. */
|
|
46
|
+
export declare function docFrom(kind: Kind, content: Uint8Array | null): Y.Doc;
|
|
47
|
+
/** What a document saves as: the text, the CSV, or the marks as JSON. */
|
|
48
|
+
export declare function contentOf(kind: Kind, doc: Y.Doc): Uint8Array;
|
|
49
|
+
/** Base64 for document updates on the wire. */
|
|
50
|
+
export declare function toBase64(bytes: Uint8Array): string;
|
|
51
|
+
export declare function fromBase64(text: string): Uint8Array;
|
|
52
|
+
/** A file to open live: a Drive file (by path or share link) or a chat attachment. */
|
|
53
|
+
export type Target = DriveFile | Attachment;
|
|
54
|
+
export type Hooks = Partial<{
|
|
55
|
+
/** Someone joined or left, or moved their pointer or selection. */
|
|
56
|
+
onPeople(room: ViewRoom): void;
|
|
57
|
+
/** The file changed outside the room. Editable kinds get a new `doc` to rebind to; others reload the file. */
|
|
58
|
+
onReload(room: ViewRoom): void;
|
|
59
|
+
/** The room's edits were written back to the file. */
|
|
60
|
+
onSaved(room: ViewRoom): void;
|
|
61
|
+
/** The room closed or the connection dropped. Connect again to rejoin. */
|
|
62
|
+
onClosed(room: ViewRoom): void;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* A live room for one file. Everyone in it sees everyone else, by seat; with
|
|
66
|
+
* `write`, edits to `doc` reach everyone and are saved back to the file.
|
|
67
|
+
* Pointers ride in each seat's position (x and y from 0 to 1, z the page) and
|
|
68
|
+
* text selections in its status.
|
|
69
|
+
*/
|
|
70
|
+
export declare class ViewRoom extends Room {
|
|
71
|
+
readonly target: Target;
|
|
72
|
+
kind: Kind;
|
|
73
|
+
role: Role;
|
|
74
|
+
/** You own the file: you can remove anyone's marks on it. */
|
|
75
|
+
owner: boolean;
|
|
76
|
+
/** The file as the room sees it. */
|
|
77
|
+
file: {
|
|
78
|
+
name: string;
|
|
79
|
+
contentType: string;
|
|
80
|
+
contentLength: number;
|
|
81
|
+
};
|
|
82
|
+
/** The shared document for `text`, `table` and `image`; empty for `view`. */
|
|
83
|
+
doc: Y.Doc;
|
|
84
|
+
private hooks;
|
|
85
|
+
private outgoing;
|
|
86
|
+
private timer;
|
|
87
|
+
private ended;
|
|
88
|
+
/** Edits up to this sequence number are in the file. */
|
|
89
|
+
private saved;
|
|
90
|
+
constructor(endpoint: string, ws: WebSocket, authorization: Authorization, id: string, target: Target, hooks: Hooks);
|
|
91
|
+
/** Replace or merge the room's hooks once the UI that renders it is ready. */
|
|
92
|
+
setHooks(hooks: Hooks): void;
|
|
93
|
+
/** Everyone else in the room. */
|
|
94
|
+
people(self?: Presence): Presence[];
|
|
95
|
+
/** The roster changed. */
|
|
96
|
+
peopleChanged(): void;
|
|
97
|
+
hydrate(state: any): void;
|
|
98
|
+
handleEvent(packet: any): boolean;
|
|
99
|
+
private replace;
|
|
100
|
+
/** Send edits waiting in the batch now. */
|
|
101
|
+
flush(): void;
|
|
102
|
+
/** The connection ended: stop sending, and tell the UI once. */
|
|
103
|
+
closed(): void;
|
|
104
|
+
disconnect(): void;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Join the live room for a file: a Drive file (by path or share link) or a chat
|
|
108
|
+
* attachment. Owners, group members and chat members can edit; share links look.
|
|
109
|
+
*/
|
|
110
|
+
export declare function connect(target: Target, hooks?: Hooks): Promise<{
|
|
111
|
+
room: ViewRoom;
|
|
112
|
+
self: Presence;
|
|
113
|
+
}>;
|
package/dist/src/view.js
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import * as Y from "yjs";
|
|
2
|
+
import { Fauna } from "../index.js";
|
|
3
|
+
import { Room, connect as connectServer } from "./shared/server.js";
|
|
4
|
+
/** Text and tables over this size open as `view`. */
|
|
5
|
+
export const EDIT_MAX_BYTES = 1024 * 1024;
|
|
6
|
+
const TEXT_TYPES = new Set(['application/json', 'application/xml', 'application/javascript', 'application/x-sh', 'application/x-yaml', 'application/toml', 'application/x-ndjson']);
|
|
7
|
+
const TEXT_EXTENSIONS = /\.(txt|md|markdown|json|jsonl|xml|html?|css|js|mjs|cjs|jsx|ts|tsx|py|rb|go|rs|java|kt|c|h|cc|cpp|hpp|cs|php|sh|bash|zsh|sql|ya?ml|toml|ini|cfg|conf|log|env|r|swift|lua|pl|scala|vue|svelte|tsv)$/i;
|
|
8
|
+
/** True for files whose bytes are text. */
|
|
9
|
+
export function isText(contentType, name) {
|
|
10
|
+
return contentType.startsWith('text/') || TEXT_TYPES.has(contentType) || TEXT_EXTENSIONS.test(name);
|
|
11
|
+
}
|
|
12
|
+
/** What a file opens as, by its type, name and size. */
|
|
13
|
+
export function kindOf(contentType, name, size) {
|
|
14
|
+
if (contentType.startsWith('image/'))
|
|
15
|
+
return 'image';
|
|
16
|
+
if (size > EDIT_MAX_BYTES)
|
|
17
|
+
return 'view';
|
|
18
|
+
if (contentType === 'text/csv' || /\.csv$/i.test(name))
|
|
19
|
+
return 'table';
|
|
20
|
+
if (isText(contentType, name))
|
|
21
|
+
return 'text';
|
|
22
|
+
return 'view';
|
|
23
|
+
}
|
|
24
|
+
// ─── CSV ─────────────────────────────────────────────────────────────────────
|
|
25
|
+
/** Rows of cells from CSV text: quoted fields, doubled quotes and CRLF all read right. */
|
|
26
|
+
export function parseCSV(text) {
|
|
27
|
+
const rows = [];
|
|
28
|
+
let row = [];
|
|
29
|
+
let cell = '';
|
|
30
|
+
let quoted = false;
|
|
31
|
+
for (let i = 0; i < text.length; i++) {
|
|
32
|
+
const c = text[i];
|
|
33
|
+
if (quoted) {
|
|
34
|
+
if (c === '"' && text[i + 1] === '"') {
|
|
35
|
+
cell += '"';
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
else if (c === '"')
|
|
39
|
+
quoted = false;
|
|
40
|
+
else
|
|
41
|
+
cell += c;
|
|
42
|
+
}
|
|
43
|
+
else if (c === '"' && cell === '') {
|
|
44
|
+
quoted = true;
|
|
45
|
+
}
|
|
46
|
+
else if (c === ',') {
|
|
47
|
+
row.push(cell);
|
|
48
|
+
cell = '';
|
|
49
|
+
}
|
|
50
|
+
else if (c === '\n' || c === '\r') {
|
|
51
|
+
if (c === '\r' && text[i + 1] === '\n')
|
|
52
|
+
i++;
|
|
53
|
+
row.push(cell);
|
|
54
|
+
rows.push(row);
|
|
55
|
+
row = [];
|
|
56
|
+
cell = '';
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
cell += c;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (cell !== '' || row.length > 0) {
|
|
63
|
+
row.push(cell);
|
|
64
|
+
rows.push(row);
|
|
65
|
+
}
|
|
66
|
+
return rows;
|
|
67
|
+
}
|
|
68
|
+
/** CSV text from rows, quoting cells that need it. */
|
|
69
|
+
export function formatCSV(rows) {
|
|
70
|
+
const quote = (cell) => /[",\r\n]/.test(cell) || /^\s|\s$/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
|
|
71
|
+
return rows.map((row) => row.map(quote).join(',')).join('\n') + (rows.length ? '\n' : '');
|
|
72
|
+
}
|
|
73
|
+
// ─── Documents ───────────────────────────────────────────────────────────────
|
|
74
|
+
//
|
|
75
|
+
// text: Y.Text 'text'
|
|
76
|
+
// table: Y.Array 'columns' and Y.Array 'rows' of ids, and Y.Map 'cells' from a
|
|
77
|
+
// row id to a Y.Map of column id to the cell's text
|
|
78
|
+
// image: Y.Array 'marks' of Mark
|
|
79
|
+
function id() {
|
|
80
|
+
return Math.random().toString(36).slice(2, 10) + Date.now().toString(36).slice(-4);
|
|
81
|
+
}
|
|
82
|
+
/** The table's cells in order, as rows of text. */
|
|
83
|
+
export function tableRows(doc) {
|
|
84
|
+
const columns = doc.getArray('columns').toArray();
|
|
85
|
+
const rows = doc.getArray('rows').toArray();
|
|
86
|
+
const cells = doc.getMap('cells');
|
|
87
|
+
return rows.map((row) => {
|
|
88
|
+
const map = cells.get(row);
|
|
89
|
+
return columns.map((column) => map?.get(column) ?? '');
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Fill an empty table document from rows of text. */
|
|
93
|
+
export function fillTable(doc, rows) {
|
|
94
|
+
const width = Math.max(1, ...rows.map((row) => row.length));
|
|
95
|
+
const columns = Array.from({ length: width }, id);
|
|
96
|
+
const rowIds = rows.map(id);
|
|
97
|
+
doc.transact(() => {
|
|
98
|
+
doc.getArray('columns').push(columns);
|
|
99
|
+
doc.getArray('rows').push(rowIds);
|
|
100
|
+
const cells = doc.getMap('cells');
|
|
101
|
+
rows.forEach((row, r) => {
|
|
102
|
+
const map = new Y.Map();
|
|
103
|
+
columns.forEach((column, c) => { if (row[c])
|
|
104
|
+
map.set(column, row[c]); });
|
|
105
|
+
cells.set(rowIds[r], map);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Add a row (after `after`, or at the end) and return its id. */
|
|
110
|
+
export function addRow(doc, after) {
|
|
111
|
+
const rows = doc.getArray('rows');
|
|
112
|
+
const row = id();
|
|
113
|
+
doc.transact(() => {
|
|
114
|
+
const index = after ? rows.toArray().indexOf(after) + 1 : rows.length;
|
|
115
|
+
rows.insert(Math.max(0, Math.min(index, rows.length)), [row]);
|
|
116
|
+
doc.getMap('cells').set(row, new Y.Map());
|
|
117
|
+
});
|
|
118
|
+
return row;
|
|
119
|
+
}
|
|
120
|
+
/** Add a column (after `after`, or at the end) and return its id. */
|
|
121
|
+
export function addColumn(doc, after) {
|
|
122
|
+
const columns = doc.getArray('columns');
|
|
123
|
+
const column = id();
|
|
124
|
+
const index = after ? columns.toArray().indexOf(after) + 1 : columns.length;
|
|
125
|
+
columns.insert(Math.max(0, Math.min(index, columns.length)), [column]);
|
|
126
|
+
return column;
|
|
127
|
+
}
|
|
128
|
+
export function removeRow(doc, row) {
|
|
129
|
+
const rows = doc.getArray('rows');
|
|
130
|
+
doc.transact(() => {
|
|
131
|
+
const index = rows.toArray().indexOf(row);
|
|
132
|
+
if (index >= 0)
|
|
133
|
+
rows.delete(index, 1);
|
|
134
|
+
doc.getMap('cells').delete(row);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
export function removeColumn(doc, column) {
|
|
138
|
+
const columns = doc.getArray('columns');
|
|
139
|
+
const index = columns.toArray().indexOf(column);
|
|
140
|
+
if (index >= 0)
|
|
141
|
+
columns.delete(index, 1);
|
|
142
|
+
}
|
|
143
|
+
/** A fresh document holding a file's content: its text, its CSV, or an image's marks as JSON. */
|
|
144
|
+
export function docFrom(kind, content) {
|
|
145
|
+
const doc = new Y.Doc();
|
|
146
|
+
const text = content ? new TextDecoder().decode(content) : '';
|
|
147
|
+
if (kind === 'text') {
|
|
148
|
+
doc.getText('text').insert(0, text);
|
|
149
|
+
}
|
|
150
|
+
else if (kind === 'table') {
|
|
151
|
+
fillTable(doc, parseCSV(text));
|
|
152
|
+
}
|
|
153
|
+
else if (kind === 'image') {
|
|
154
|
+
let marks = [];
|
|
155
|
+
try {
|
|
156
|
+
marks = text ? JSON.parse(text) : [];
|
|
157
|
+
}
|
|
158
|
+
catch { /* unreadable notes start empty */ }
|
|
159
|
+
doc.getArray('marks').push(Array.isArray(marks) ? marks : []);
|
|
160
|
+
}
|
|
161
|
+
return doc;
|
|
162
|
+
}
|
|
163
|
+
/** What a document saves as: the text, the CSV, or the marks as JSON. */
|
|
164
|
+
export function contentOf(kind, doc) {
|
|
165
|
+
if (kind === 'text')
|
|
166
|
+
return new TextEncoder().encode(doc.getText('text').toString());
|
|
167
|
+
if (kind === 'table')
|
|
168
|
+
return new TextEncoder().encode(formatCSV(tableRows(doc)));
|
|
169
|
+
if (kind === 'image')
|
|
170
|
+
return new TextEncoder().encode(JSON.stringify(doc.getArray('marks').toArray()));
|
|
171
|
+
return new Uint8Array();
|
|
172
|
+
}
|
|
173
|
+
/** Base64 for document updates on the wire. */
|
|
174
|
+
export function toBase64(bytes) {
|
|
175
|
+
let binary = '';
|
|
176
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
177
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
178
|
+
return btoa(binary);
|
|
179
|
+
}
|
|
180
|
+
export function fromBase64(text) {
|
|
181
|
+
return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
182
|
+
}
|
|
183
|
+
/** Edits are sent in batches this far apart. */
|
|
184
|
+
const BATCH_MS = 50;
|
|
185
|
+
const REMOTE = Symbol("remote");
|
|
186
|
+
/**
|
|
187
|
+
* A live room for one file. Everyone in it sees everyone else, by seat; with
|
|
188
|
+
* `write`, edits to `doc` reach everyone and are saved back to the file.
|
|
189
|
+
* Pointers ride in each seat's position (x and y from 0 to 1, z the page) and
|
|
190
|
+
* text selections in its status.
|
|
191
|
+
*/
|
|
192
|
+
export class ViewRoom extends Room {
|
|
193
|
+
constructor(endpoint, ws, authorization, id, target, hooks) {
|
|
194
|
+
super(endpoint, ws, authorization, id);
|
|
195
|
+
Object.defineProperty(this, "target", {
|
|
196
|
+
enumerable: true,
|
|
197
|
+
configurable: true,
|
|
198
|
+
writable: true,
|
|
199
|
+
value: void 0
|
|
200
|
+
});
|
|
201
|
+
Object.defineProperty(this, "kind", {
|
|
202
|
+
enumerable: true,
|
|
203
|
+
configurable: true,
|
|
204
|
+
writable: true,
|
|
205
|
+
value: 'view'
|
|
206
|
+
});
|
|
207
|
+
Object.defineProperty(this, "role", {
|
|
208
|
+
enumerable: true,
|
|
209
|
+
configurable: true,
|
|
210
|
+
writable: true,
|
|
211
|
+
value: 'read'
|
|
212
|
+
});
|
|
213
|
+
/** You own the file: you can remove anyone's marks on it. */
|
|
214
|
+
Object.defineProperty(this, "owner", {
|
|
215
|
+
enumerable: true,
|
|
216
|
+
configurable: true,
|
|
217
|
+
writable: true,
|
|
218
|
+
value: false
|
|
219
|
+
});
|
|
220
|
+
/** The file as the room sees it. */
|
|
221
|
+
Object.defineProperty(this, "file", {
|
|
222
|
+
enumerable: true,
|
|
223
|
+
configurable: true,
|
|
224
|
+
writable: true,
|
|
225
|
+
value: { name: '', contentType: '', contentLength: 0 }
|
|
226
|
+
});
|
|
227
|
+
/** The shared document for `text`, `table` and `image`; empty for `view`. */
|
|
228
|
+
Object.defineProperty(this, "doc", {
|
|
229
|
+
enumerable: true,
|
|
230
|
+
configurable: true,
|
|
231
|
+
writable: true,
|
|
232
|
+
value: new Y.Doc()
|
|
233
|
+
});
|
|
234
|
+
Object.defineProperty(this, "hooks", {
|
|
235
|
+
enumerable: true,
|
|
236
|
+
configurable: true,
|
|
237
|
+
writable: true,
|
|
238
|
+
value: void 0
|
|
239
|
+
});
|
|
240
|
+
Object.defineProperty(this, "outgoing", {
|
|
241
|
+
enumerable: true,
|
|
242
|
+
configurable: true,
|
|
243
|
+
writable: true,
|
|
244
|
+
value: []
|
|
245
|
+
});
|
|
246
|
+
Object.defineProperty(this, "timer", {
|
|
247
|
+
enumerable: true,
|
|
248
|
+
configurable: true,
|
|
249
|
+
writable: true,
|
|
250
|
+
value: null
|
|
251
|
+
});
|
|
252
|
+
Object.defineProperty(this, "ended", {
|
|
253
|
+
enumerable: true,
|
|
254
|
+
configurable: true,
|
|
255
|
+
writable: true,
|
|
256
|
+
value: false
|
|
257
|
+
});
|
|
258
|
+
/** Edits up to this sequence number are in the file. */
|
|
259
|
+
Object.defineProperty(this, "saved", {
|
|
260
|
+
enumerable: true,
|
|
261
|
+
configurable: true,
|
|
262
|
+
writable: true,
|
|
263
|
+
value: 0
|
|
264
|
+
});
|
|
265
|
+
this.target = target;
|
|
266
|
+
this.hooks = hooks;
|
|
267
|
+
}
|
|
268
|
+
/** Replace or merge the room's hooks once the UI that renders it is ready. */
|
|
269
|
+
setHooks(hooks) {
|
|
270
|
+
this.hooks = { ...this.hooks, ...hooks };
|
|
271
|
+
}
|
|
272
|
+
/** Everyone else in the room. */
|
|
273
|
+
people(self) {
|
|
274
|
+
return this.presences.filter((p) => p !== self && p.id !== self?.id);
|
|
275
|
+
}
|
|
276
|
+
/** The roster changed. */
|
|
277
|
+
peopleChanged() {
|
|
278
|
+
this.hooks.onPeople?.(this);
|
|
279
|
+
}
|
|
280
|
+
hydrate(state) {
|
|
281
|
+
this.kind = state?.kind ?? 'view';
|
|
282
|
+
this.role = state?.role === 'write' ? 'write' : 'read';
|
|
283
|
+
this.owner = state?.owner === true;
|
|
284
|
+
this.file = {
|
|
285
|
+
name: String(state?.file?.name ?? ''),
|
|
286
|
+
contentType: String(state?.file?.contentType ?? ''),
|
|
287
|
+
contentLength: Number(state?.file?.contentLength ?? 0),
|
|
288
|
+
};
|
|
289
|
+
this.saved = Number(state?.saved ?? 0);
|
|
290
|
+
this.replace(typeof state?.doc === 'string' ? state.doc : null);
|
|
291
|
+
}
|
|
292
|
+
handleEvent(packet) {
|
|
293
|
+
if (packet?.event === 'doc-update' && typeof packet.data?.update === 'string') {
|
|
294
|
+
Y.applyUpdate(this.doc, fromBase64(packet.data.update), REMOTE);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (packet?.event === 'doc-reload') {
|
|
298
|
+
if (typeof packet.data?.doc === 'string')
|
|
299
|
+
this.replace(packet.data.doc);
|
|
300
|
+
this.hooks.onReload?.(this);
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
if (packet?.event === 'doc-saved') {
|
|
304
|
+
const saved = Number(packet.data?.saved ?? 0);
|
|
305
|
+
if (saved > this.saved) {
|
|
306
|
+
this.saved = saved;
|
|
307
|
+
this.hooks.onSaved?.(this);
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
replace(snapshot) {
|
|
314
|
+
this.doc.destroy();
|
|
315
|
+
const doc = new Y.Doc();
|
|
316
|
+
if (snapshot)
|
|
317
|
+
Y.applyUpdate(doc, fromBase64(snapshot), REMOTE);
|
|
318
|
+
doc.on('update', (update, origin) => {
|
|
319
|
+
if (origin === REMOTE || this.role !== 'write' || this.ended)
|
|
320
|
+
return;
|
|
321
|
+
this.outgoing.push(update);
|
|
322
|
+
this.timer ?? (this.timer = setTimeout(() => this.flush(), BATCH_MS));
|
|
323
|
+
});
|
|
324
|
+
this.doc = doc;
|
|
325
|
+
}
|
|
326
|
+
/** Send edits waiting in the batch now. */
|
|
327
|
+
flush() {
|
|
328
|
+
if (this.timer)
|
|
329
|
+
clearTimeout(this.timer);
|
|
330
|
+
this.timer = null;
|
|
331
|
+
if (this.outgoing.length === 0)
|
|
332
|
+
return;
|
|
333
|
+
const update = this.outgoing.length === 1 ? this.outgoing[0] : Y.mergeUpdates(this.outgoing);
|
|
334
|
+
this.outgoing = [];
|
|
335
|
+
this.post({ event: 'doc-update', data: { update: toBase64(update) } });
|
|
336
|
+
}
|
|
337
|
+
/** The connection ended: stop sending, and tell the UI once. */
|
|
338
|
+
closed() {
|
|
339
|
+
if (this.ended)
|
|
340
|
+
return;
|
|
341
|
+
this.ended = true;
|
|
342
|
+
if (this.timer)
|
|
343
|
+
clearTimeout(this.timer);
|
|
344
|
+
this.timer = null;
|
|
345
|
+
this.hooks.onClosed?.(this);
|
|
346
|
+
}
|
|
347
|
+
disconnect() {
|
|
348
|
+
this.flush();
|
|
349
|
+
this.ended = true;
|
|
350
|
+
super.disconnect();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
function authorizationFor(target) {
|
|
354
|
+
if ('ref' in target && 'chat' in target) {
|
|
355
|
+
if (target.chat && target.ref) {
|
|
356
|
+
return { authorization: target.file.authorization.authorization, server: `fauna:file:chat:${target.chat}/${target.ref}` };
|
|
357
|
+
}
|
|
358
|
+
target = target.file;
|
|
359
|
+
}
|
|
360
|
+
const auth = target.authorization;
|
|
361
|
+
if ('accessCode' in auth)
|
|
362
|
+
return { authorization: auth.authorization, accessCode: auth.accessCode };
|
|
363
|
+
if ('repository' in auth)
|
|
364
|
+
throw new Error('Repository files open in Termite.');
|
|
365
|
+
return { authorization: auth.authorization, server: `fauna:file:${auth.path}`, ...(auth.group ? { group: auth.group } : {}) };
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Join the live room for a file: a Drive file (by path or share link) or a chat
|
|
369
|
+
* attachment. Owners, group members and chat members can edit; share links look.
|
|
370
|
+
*/
|
|
371
|
+
export async function connect(target, hooks = {}) {
|
|
372
|
+
let created;
|
|
373
|
+
const self = await connectServer(authorizationFor(target), `${Fauna.getURL()}/drive/live`, {
|
|
374
|
+
createRoom: (endpoint, ws, authorization, id) => {
|
|
375
|
+
created = new ViewRoom(endpoint, ws, authorization, id, target, hooks);
|
|
376
|
+
return created;
|
|
377
|
+
},
|
|
378
|
+
onConnection: (room, state) => room.hydrate(state),
|
|
379
|
+
onEvent: (room, packet) => room.handleEvent(packet),
|
|
380
|
+
onPresence: (room) => room.peopleChanged(),
|
|
381
|
+
onClosed: (room) => room.closed(),
|
|
382
|
+
});
|
|
383
|
+
const room = created;
|
|
384
|
+
room.ws.addEventListener('close', () => room.closed());
|
|
385
|
+
return { room, self };
|
|
386
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ApiKey, Credentials, Profile } from "./authentification.js";
|
|
2
|
+
/** Event types a webhook can listen for: `chat.message` for groups and chats, payment events for API keys. */
|
|
3
|
+
export type EventType = 'chat.message' | 'payment.succeeded' | 'payment.expired' | 'payment.refunded' | 'payment.disputed';
|
|
4
|
+
/** Who a webhook belongs to: a group, one chat, or the API key making the call. */
|
|
5
|
+
export type WebhookOwner = {
|
|
6
|
+
group: string;
|
|
7
|
+
} | {
|
|
8
|
+
chat: string;
|
|
9
|
+
} | 'key';
|
|
10
|
+
/** An outbound webhook. Its events are signed and POSTed to `url`. */
|
|
11
|
+
export type Webhook = {
|
|
12
|
+
id: string;
|
|
13
|
+
url: string;
|
|
14
|
+
events: EventType[];
|
|
15
|
+
created: Date;
|
|
16
|
+
};
|
|
17
|
+
/** What `create` resolves to. The secret is only ever returned here. */
|
|
18
|
+
export type Registration = {
|
|
19
|
+
webhook: Webhook;
|
|
20
|
+
secret: string;
|
|
21
|
+
};
|
|
22
|
+
/** One event as POSTed to a webhook URL, with a `Fauna-Signature` header. */
|
|
23
|
+
export type Delivery = {
|
|
24
|
+
id: string;
|
|
25
|
+
type: EventType;
|
|
26
|
+
/** Milliseconds since the epoch. */
|
|
27
|
+
created: number;
|
|
28
|
+
data: unknown;
|
|
29
|
+
};
|
|
30
|
+
/** A reaction carried by a `chat.message` event. */
|
|
31
|
+
export type Reaction = {
|
|
32
|
+
emoji: string;
|
|
33
|
+
/** Position of the message reacted to. */
|
|
34
|
+
position: number;
|
|
35
|
+
on: boolean;
|
|
36
|
+
};
|
|
37
|
+
/** `data` of a `chat.message` event: a message sent in a chat. `group` is set when a group owns the chat. */
|
|
38
|
+
export type ChatMessage = {
|
|
39
|
+
chatId: string;
|
|
40
|
+
chatName: string;
|
|
41
|
+
group?: string;
|
|
42
|
+
position: number;
|
|
43
|
+
author: Profile;
|
|
44
|
+
text: string;
|
|
45
|
+
/** Chat members tagged with `@`, never the author. */
|
|
46
|
+
mentions: Profile[];
|
|
47
|
+
/** Set when the message is a reaction, so apps can skip it. */
|
|
48
|
+
reaction?: Reaction;
|
|
49
|
+
/** Milliseconds since the epoch. */
|
|
50
|
+
created: number;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Register a webhook for a group or a chat you can read, or, signed in with an
|
|
54
|
+
* API key, for that key. In production `url` must be https on a public address.
|
|
55
|
+
* Pass `secret` to choose the signing secret; otherwise one is made.
|
|
56
|
+
*/
|
|
57
|
+
export declare function create(authorization: Credentials | ApiKey, owner: WebhookOwner, url: string, events: EventType[], secret?: string): Promise<Registration>;
|
|
58
|
+
/** An owner's webhooks. Secrets are never listed. */
|
|
59
|
+
export declare function list(authorization: Credentials | ApiKey, owner: WebhookOwner): Promise<Webhook[]>;
|
|
60
|
+
export declare function remove(authorization: Credentials | ApiKey, id: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* True when `signature` (the `Fauna-Signature` header, `t=<seconds>,v1=<hex>`)
|
|
63
|
+
* is the HMAC SHA-256 of `t.body` under `secret`, signed within `tolerance`
|
|
64
|
+
* seconds of now. Pass the raw request body, before any JSON parsing.
|
|
65
|
+
*/
|
|
66
|
+
export declare function verify(secret: string, body: string, signature: string, tolerance?: number): Promise<boolean>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Fauna } from "../index.js";
|
|
2
|
+
function hydrate(raw) {
|
|
3
|
+
return { ...raw, created: new Date(raw.created) };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Register a webhook for a group or a chat you can read, or, signed in with an
|
|
7
|
+
* API key, for that key. In production `url` must be https on a public address.
|
|
8
|
+
* Pass `secret` to choose the signing secret; otherwise one is made.
|
|
9
|
+
*/
|
|
10
|
+
export async function create(authorization, owner, url, events, secret) {
|
|
11
|
+
const response = await fetch(`${Fauna.getURL()}/webhooks`, {
|
|
12
|
+
method: "POST",
|
|
13
|
+
headers: {
|
|
14
|
+
"Content-Type": "application/json",
|
|
15
|
+
"Authorization": 'email' in authorization ? `Token ${authorization.token}` : `ApiKey ${authorization.id}`
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({ owner, url, events, secret })
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
throw new Error(await response.text());
|
|
21
|
+
}
|
|
22
|
+
const raw = await response.json();
|
|
23
|
+
return { webhook: hydrate(raw.webhook), secret: raw.secret };
|
|
24
|
+
}
|
|
25
|
+
/** An owner's webhooks. Secrets are never listed. */
|
|
26
|
+
export async function list(authorization, owner) {
|
|
27
|
+
const response = await fetch(`${Fauna.getURL()}/webhooks/list`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Authorization": 'email' in authorization ? `Token ${authorization.token}` : `ApiKey ${authorization.id}`
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ owner })
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
throw new Error(await response.text());
|
|
37
|
+
}
|
|
38
|
+
const raw = await response.json();
|
|
39
|
+
return raw.map(hydrate);
|
|
40
|
+
}
|
|
41
|
+
export async function remove(authorization, id) {
|
|
42
|
+
const response = await fetch(`${Fauna.getURL()}/webhooks/${encodeURIComponent(id)}`, {
|
|
43
|
+
method: "DELETE",
|
|
44
|
+
headers: {
|
|
45
|
+
"Authorization": 'email' in authorization ? `Token ${authorization.token}` : `ApiKey ${authorization.id}`
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(await response.text());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* True when `signature` (the `Fauna-Signature` header, `t=<seconds>,v1=<hex>`)
|
|
54
|
+
* is the HMAC SHA-256 of `t.body` under `secret`, signed within `tolerance`
|
|
55
|
+
* seconds of now. Pass the raw request body, before any JSON parsing.
|
|
56
|
+
*/
|
|
57
|
+
export async function verify(secret, body, signature, tolerance = 300) {
|
|
58
|
+
const fields = new Map(signature.split(',').map((piece) => {
|
|
59
|
+
const at = piece.indexOf('=');
|
|
60
|
+
return [piece.slice(0, at).trim(), piece.slice(at + 1).trim()];
|
|
61
|
+
}));
|
|
62
|
+
const timestamp = Number(fields.get('t'));
|
|
63
|
+
const expected = fields.get('v1') ?? '';
|
|
64
|
+
if (!Number.isInteger(timestamp) || !expected)
|
|
65
|
+
return false;
|
|
66
|
+
if (Math.abs(Date.now() / 1000 - timestamp) > tolerance)
|
|
67
|
+
return false;
|
|
68
|
+
const encoder = new TextEncoder();
|
|
69
|
+
const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
70
|
+
const digest = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(`${timestamp}.${body}`)));
|
|
71
|
+
const actual = [...digest].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
72
|
+
if (actual.length !== expected.length)
|
|
73
|
+
return false;
|
|
74
|
+
let difference = 0;
|
|
75
|
+
for (let i = 0; i < actual.length; i++)
|
|
76
|
+
difference |= actual.charCodeAt(i) ^ expected.charCodeAt(i);
|
|
77
|
+
return difference === 0;
|
|
78
|
+
}
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UI } from "./src/ui/index.js";
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { UI } from "./src/ui/index.js";
|