fauna-api 1.0.686 → 1.0.692

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.
@@ -1,3 +1,4 @@
1
+ import { Fauna } from "../../index.js";
1
2
  import { ApiKey, Credentials, Profile } from "../authentification.js";
2
3
  export type Authorization = {
3
4
  authorization: Credentials | ApiKey;
@@ -63,3 +64,25 @@ export declare class Room {
63
64
  generateAccessCode(): Promise<string>;
64
65
  }
65
66
  export declare function connect(authorization: Authorization, endpoint: string, hooks?: Hooks): Promise<Presence>;
67
+ /** A code you own: a room you shared, or a file link. */
68
+ export type AccessCode = {
69
+ code: string;
70
+ /** What it opens, e.g. `parrot:chat:{id}` or `file:{path}`. */
71
+ reference: string;
72
+ /** True when people who join with it may act in the room, not only watch. */
73
+ write: boolean;
74
+ expires: Date;
75
+ };
76
+ export declare const AccessCode: {
77
+ fromJSON(json: any): AccessCode;
78
+ };
79
+ /**
80
+ * The access codes you own, the one that lasts longest first: your room shares
81
+ * and file links, and the ones you made for your groups.
82
+ */
83
+ export declare function listAccessCodes(authorization: Credentials | ApiKey, query: {
84
+ cursor: number;
85
+ max: number;
86
+ }): Promise<AccessCode[]>;
87
+ /** Revoke one of your access codes. It stops working at once; anyone in a room with it is disconnected. */
88
+ export declare function revokeAccessCode(authorization: Credentials | ApiKey, code: string): Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { Fauna } from "../../index.js";
1
2
  export class Presence {
2
3
  constructor(room, id, profile) {
3
4
  Object.defineProperty(this, "room", {
@@ -300,3 +301,43 @@ export async function connect(authorization, endpoint, hooks = {}) {
300
301
  };
301
302
  });
302
303
  }
304
+ export const AccessCode = {
305
+ fromJSON(json) {
306
+ return {
307
+ code: String(json.code ?? ""),
308
+ reference: String(json.reference ?? ""),
309
+ write: json.write === true,
310
+ expires: new Date(json.expires),
311
+ };
312
+ },
313
+ };
314
+ /**
315
+ * The access codes you own, the one that lasts longest first: your room shares
316
+ * and file links, and the ones you made for your groups.
317
+ */
318
+ export async function listAccessCodes(authorization, query) {
319
+ const params = new URLSearchParams({ cursor: String(query.cursor), max: String(query.max) });
320
+ const response = await fetch(`${Fauna.getURL()}/access-codes?${params}`, {
321
+ method: "GET",
322
+ headers: {
323
+ Authorization: 'email' in authorization ? `Token ${authorization.token}` : `ApiKey ${authorization.id}`,
324
+ },
325
+ });
326
+ if (!response.ok) {
327
+ throw new Error(await response.text());
328
+ }
329
+ const json = await response.json();
330
+ return (Array.isArray(json) ? json : []).map((row) => AccessCode.fromJSON(row));
331
+ }
332
+ /** Revoke one of your access codes. It stops working at once; anyone in a room with it is disconnected. */
333
+ export async function revokeAccessCode(authorization, code) {
334
+ const response = await fetch(`${Fauna.getURL()}/access-codes/${encodeURIComponent(code)}`, {
335
+ method: "DELETE",
336
+ headers: {
337
+ Authorization: 'email' in authorization ? `Token ${authorization.token}` : `ApiKey ${authorization.id}`,
338
+ },
339
+ });
340
+ if (!response.ok) {
341
+ throw new Error(await response.text());
342
+ }
343
+ }
@@ -1,13 +1,17 @@
1
+ import { Fauna } from "fauna-api";
1
2
  export declare function getToken(): string | undefined;
2
3
  export declare function setToken(token: string): void;
3
4
  export declare function clearToken(): void;
5
+ /** Signs this browser out: ends the session on the server, then forgets the login cookie. */
6
+ export declare function signOut(credentials: Fauna.Authentification.Credentials): Promise<void>;
4
7
  /** Whether this tab is a support session opened from Fauna's ticket board. */
5
8
  export declare function isSupportView(): boolean;
6
9
  /**
7
- * Takes `token` and `support` off a page's query. A support token stays in this tab; any other token
10
+ * Takes `handoff` and `support` off a page's query (and the address bar, before anything else loads),
11
+ * then redeems the handoff code for its session. A support session stays in this tab; any other
8
12
  * becomes the login cookie and ends the tab's support session. Returns whether the query changed.
9
13
  */
10
- export declare function takeTokenFromURL(params: URLSearchParams): boolean;
14
+ export declare function takeHandoffFromURL(params: URLSearchParams): Promise<boolean>;
11
15
  /** The token this tab signs in with: the support session's in a support view, else the login cookie. */
12
16
  export declare function sessionToken(): string | undefined;
13
17
  export type OAuthProvider = 'google' | 'microsoft';
@@ -44,8 +44,17 @@ export function clearToken() {
44
44
  return;
45
45
  document.cookie = `${CREDENTIALS_COOKIE}=; ${cookieSuffix(0)}`;
46
46
  }
47
- // Support sessions: Fauna's ticket board opens a reporter's page with `?token=...&support=1`.
48
- // That token belongs to the reporter, so it lives in this tab only and never replaces the login cookie.
47
+ /** Signs this browser out: ends the session on the server, then forgets the login cookie. */
48
+ export async function signOut(credentials) {
49
+ try {
50
+ await Fauna.Authentification.signOut(credentials);
51
+ }
52
+ finally {
53
+ clearToken();
54
+ }
55
+ }
56
+ // Support sessions: Fauna's ticket board opens a reporter's page with `?handoff=...&support=1`.
57
+ // That session belongs to the reporter, so it lives in this tab only and never replaces the login cookie.
49
58
  const SUPPORT_TOKEN_KEY = 'fauna-support-token';
50
59
  function readSupportToken() {
51
60
  try {
@@ -60,13 +69,32 @@ export function isSupportView() {
60
69
  return Boolean(readSupportToken());
61
70
  }
62
71
  /**
63
- * Takes `token` and `support` off a page's query. A support token stays in this tab; any other token
72
+ * Takes `handoff` and `support` off a page's query (and the address bar, before anything else loads),
73
+ * then redeems the handoff code for its session. A support session stays in this tab; any other
64
74
  * becomes the login cookie and ends the tab's support session. Returns whether the query changed.
65
75
  */
66
- export function takeTokenFromURL(params) {
67
- const token = params.get('token');
68
- const changed = Boolean(token) || params.has('support');
69
- if (token && params.get('support') === '1') {
76
+ export async function takeHandoffFromURL(params) {
77
+ const code = params.get('handoff');
78
+ const support = params.get('support') === '1';
79
+ const changed = Boolean(code) || params.has('support');
80
+ params.delete('handoff');
81
+ params.delete('support');
82
+ if (changed && typeof location !== 'undefined') {
83
+ const url = new URL(location.href);
84
+ url.searchParams.delete('handoff');
85
+ url.searchParams.delete('support');
86
+ history.replaceState(history.state, '', url);
87
+ }
88
+ if (!code)
89
+ return changed;
90
+ let token;
91
+ try {
92
+ token = await Fauna.Authentification.redeemHandoff(code);
93
+ }
94
+ catch {
95
+ return changed;
96
+ }
97
+ if (support) {
70
98
  try {
71
99
  sessionStorage.setItem(SUPPORT_TOKEN_KEY, token);
72
100
  }
@@ -74,7 +102,7 @@ export function takeTokenFromURL(params) {
74
102
  /* no tab storage: drop it rather than sign in as the reporter */
75
103
  }
76
104
  }
77
- else if (token) {
105
+ else {
78
106
  try {
79
107
  sessionStorage.removeItem(SUPPORT_TOKEN_KEY);
80
108
  }
@@ -83,8 +111,6 @@ export function takeTokenFromURL(params) {
83
111
  }
84
112
  setToken(token);
85
113
  }
86
- params.delete('token');
87
- params.delete('support');
88
114
  return changed;
89
115
  }
90
116
  /** The token this tab signs in with: the support session's in a support view, else the login cookie. */
@@ -388,7 +388,7 @@ export function createBrowser(container, options) {
388
388
  gsap.set(cursorHost, { x: 0, y: 0, scale: 1 });
389
389
  const replayer = new DomReplayer(frame);
390
390
  let toolTl = null;
391
- // Only a full document replays in its own frame; a fragment shares this page's document.
391
+ // Until the first replay's frame loads, the replayer's document is still this page's.
392
392
  const caret = caretPainter(() => {
393
393
  const doc = replayer.getDocument();
394
394
  return doc === document ? null : doc;
@@ -3,7 +3,6 @@ import { useLayoutEffect, useImperativeHandle, useMemo, useRef, useState, forwar
3
3
  import { createRoot } from "react-dom/client";
4
4
  import { flushSync } from "react-dom";
5
5
  import gsap from "gsap";
6
- import { marked } from "marked";
7
6
  import { Fauna } from "fauna-api";
8
7
  import { mimeFromFileName } from "../marmot/media.js";
9
8
  import { createTokenizedInput, parseRichText, toPlainText, hasTokens } from "./tokenized.js";
@@ -14,6 +13,7 @@ import { createTooltip } from "./hover.js";
14
13
  import { sendInformaticToast } from "./toast.js";
15
14
  import { buildAvatar, createSelector, stampModelAvatar, stampUserAvatar } from "./generative.js";
16
15
  import { authorColorSeed, colorFromSeed } from "./cursor.js";
16
+ import { render as renderMarkdown } from "./markdown.js";
17
17
  // Re-export so apps can flatten serialized token markers (e.g. before sending
18
18
  // history to the LLM) without reaching into the tokenized-input module.
19
19
  export { toPlainText } from "./tokenized.js";
@@ -56,7 +56,8 @@ function webResults(call, query) {
56
56
  for (const result of JSON.parse(String(call.result))) {
57
57
  const link = document.createElement('a');
58
58
  link.className = 'chat-tool-web-source';
59
- link.href = result.url;
59
+ if (/^https?:/i.test(result.url))
60
+ link.href = result.url;
60
61
  link.target = '_blank';
61
62
  link.rel = 'noopener';
62
63
  const title = document.createElement('span');
@@ -153,7 +154,7 @@ function buildMessageToken(token, opts) {
153
154
  // render text segments as plain text and hydrate each marker into a chip.
154
155
  function renderRichText(textEl, text, opts) {
155
156
  if (!hasTokens(text)) {
156
- textEl.innerHTML = marked.parse(text);
157
+ textEl.innerHTML = renderMarkdown(text);
157
158
  return;
158
159
  }
159
160
  for (const part of parseRichText(text)) {
@@ -369,7 +370,7 @@ function createStreamRenderer(modelEl, messagesEl, renderToolCall) {
369
370
  // just behind the incoming text.
370
371
  function renderShown() {
371
372
  if (currentTextEl)
372
- currentTextEl.innerHTML = marked.parse(currentBuffer.slice(0, shown));
373
+ currentTextEl.innerHTML = renderMarkdown(currentBuffer.slice(0, shown));
373
374
  }
374
375
  function stopReveal() {
375
376
  if (rafId !== null) {
@@ -418,6 +419,7 @@ function createStreamRenderer(modelEl, messagesEl, renderToolCall) {
418
419
  const stored = toolCalls.find((item) => item.id === id);
419
420
  const changed = stored.status !== call.status
420
421
  || stored.result !== call.result
422
+ || stored.session?.reference !== call.session?.reference
421
423
  || stored.session?.code !== call.session?.code
422
424
  || (call.status === 'running' && stored.progress !== call.progress);
423
425
  if (!changed)
@@ -464,7 +466,7 @@ function createStreamRenderer(modelEl, messagesEl, renderToolCall) {
464
466
  if (currentBuffer) {
465
467
  completedSegments.push(currentBuffer);
466
468
  if (currentTextEl) {
467
- currentTextEl.innerHTML = await marked.parse(currentBuffer);
469
+ currentTextEl.innerHTML = renderMarkdown(currentBuffer);
468
470
  }
469
471
  }
470
472
  autoScroll(messagesEl);
@@ -499,7 +501,7 @@ function createStreamRenderer(modelEl, messagesEl, renderToolCall) {
499
501
  shown = trailing.length;
500
502
  ensureTextEl();
501
503
  if (currentTextEl)
502
- currentTextEl.innerHTML = marked.parse(trailing);
504
+ currentTextEl.innerHTML = renderMarkdown(trailing);
503
505
  },
504
506
  };
505
507
  }
@@ -1,7 +1,8 @@
1
- /** The live room a tool opened, to watch it: a browser, or a machine's desktop, by access code. */
1
+ /** The live room a tool opened, to watch it: a browser, joined with its access code, or a machine's desktop, joined by reference by the people who may use the machine. */
2
2
  export type ToolSession = {
3
3
  kind: 'browser' | 'desktop';
4
- code: string;
4
+ reference: string;
5
+ code?: string;
5
6
  url?: string;
6
7
  };
7
8
  export type ChatSequenceToolCall = {
@@ -1,7 +1,12 @@
1
1
  function toolSession(raw) {
2
- if (!raw || typeof raw.code !== 'string' || !raw.code)
2
+ if (!raw || typeof raw.reference !== 'string' || !raw.reference)
3
3
  return undefined;
4
- return { kind: raw.kind === 'desktop' ? 'desktop' : 'browser', code: raw.code, ...(typeof raw.url === 'string' && raw.url ? { url: raw.url } : {}) };
4
+ return {
5
+ kind: raw.kind === 'desktop' ? 'desktop' : 'browser',
6
+ reference: raw.reference,
7
+ ...(typeof raw.code === 'string' && raw.code ? { code: raw.code } : {}),
8
+ ...(typeof raw.url === 'string' && raw.url ? { url: raw.url } : {}),
9
+ };
5
10
  }
6
11
  export function roomPartsToChatFields(parts) {
7
12
  const texts = [];
@@ -233,7 +233,7 @@ export function createFileElement(file, options = {}) {
233
233
  name.readOnly = true;
234
234
  label.appendChild(name);
235
235
  const type = document.createElement('span');
236
- type.innerHTML = extensionFor(file.contentType, file.name).toUpperCase();
236
+ type.textContent = extensionFor(file.contentType, file.name).toUpperCase();
237
237
  label.appendChild(type);
238
238
  const opts = document.createElement('div');
239
239
  opts.className = `file-${view}-options`;
@@ -15,6 +15,7 @@ import * as BrowserUI from "./browser.js";
15
15
  import * as RosterUI from "./roster.js";
16
16
  import * as MapsUI from "./maps.js";
17
17
  import * as ViewUI from "./view.js";
18
+ import * as MarkdownUI from "./markdown.js";
18
19
  export declare namespace UI {
19
20
  function setRoute(route: string): void;
20
21
  function getIP(): string;
@@ -39,4 +40,5 @@ export declare namespace UI {
39
40
  export import Roster = RosterUI;
40
41
  export import Maps = MapsUI;
41
42
  export import View = ViewUI;
43
+ export import Markdown = MarkdownUI;
42
44
  }
@@ -15,6 +15,7 @@ import * as BrowserUI from "./browser.js";
15
15
  import * as RosterUI from "./roster.js";
16
16
  import * as MapsUI from "./maps.js";
17
17
  import * as ViewUI from "./view.js";
18
+ import * as MarkdownUI from "./markdown.js";
18
19
  export var UI;
19
20
  (function (UI) {
20
21
  var PREFIX = 'https://';
@@ -84,4 +85,5 @@ export var UI;
84
85
  UI.Roster = RosterUI;
85
86
  UI.Maps = MapsUI;
86
87
  UI.View = ViewUI;
88
+ UI.Markdown = MarkdownUI;
87
89
  })(UI || (UI = {}));
@@ -1,5 +1,5 @@
1
1
  import { type CSSProperties } from "react";
2
- import maplibregl from "maplibre-gl";
2
+ import * as maplibregl from "maplibre-gl";
3
3
  import type { ApiKey, Credentials } from "../authentification.js";
4
4
  import type { Group } from "../groups.js";
5
5
  import type { Coordinates, MapTheme, Place, Route, TravelMode, Units } from "../maps.js";
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { createRoot } from "react-dom/client";
4
- import maplibregl from "maplibre-gl";
4
+ import * as maplibregl from "maplibre-gl";
5
5
  import { gsap } from "gsap";
6
6
  import { Fauna } from "fauna-api";
7
7
  function reducedMotion() {
@@ -368,6 +368,8 @@ function formatTime(value) {
368
368
  }
369
369
  /** Open where to book: a link, or a form posted in a new tab when the offer carries one. */
370
370
  export function openOffer(offer) {
371
+ if (!/^https?:/i.test(offer.url))
372
+ return;
371
373
  if (!offer.form) {
372
374
  window.open(offer.url, "_blank", "noopener");
373
375
  return;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Markdown as HTML that is safe to put in the page, for text anyone else wrote: people,
3
+ * models, mail and the web.
4
+ */
5
+ export declare function render(text: string): string;
@@ -0,0 +1,33 @@
1
+ import { Marked } from "marked";
2
+ function escapeHtml(text) {
3
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
4
+ }
5
+ const LINK = /^(https:|mailto:)/i;
6
+ const IMAGE = /^https:/i;
7
+ /** Raw HTML shows as text, links go only to https or mail, and images only load over https. */
8
+ const markdown = new Marked({
9
+ renderer: {
10
+ html({ text }) {
11
+ return escapeHtml(text);
12
+ },
13
+ link({ href, title, tokens }) {
14
+ const text = this.parser.parseInline(tokens);
15
+ if (!LINK.test(href))
16
+ return text;
17
+ return `<a href="${escapeHtml(href)}"${title ? ` title="${escapeHtml(title)}"` : ""} target="_blank" rel="noopener noreferrer">${text}</a>`;
18
+ },
19
+ image({ href, title, text, tokens }) {
20
+ const alt = escapeHtml(tokens ? this.parser.parseInline(tokens, this.parser.textRenderer) : text);
21
+ if (!IMAGE.test(href))
22
+ return alt;
23
+ return `<img src="${escapeHtml(href)}" alt="${alt}"${title ? ` title="${escapeHtml(title)}"` : ""}>`;
24
+ },
25
+ },
26
+ });
27
+ /**
28
+ * Markdown as HTML that is safe to put in the page, for text anyone else wrote: people,
29
+ * models, mail and the web.
30
+ */
31
+ export function render(text) {
32
+ return markdown.parse(text, { async: false });
33
+ }
@@ -1,6 +1,6 @@
1
1
  type ToastType = 'success' | 'error' | 'warning' | 'info';
2
- export declare function sendInformaticToast(html: string, type: ToastType, duration?: number): Promise<boolean>;
3
- export declare function sendProgressToast(html: string): {
2
+ export declare function sendInformaticToast(text: string, type: ToastType, duration?: number): Promise<boolean>;
3
+ export declare function sendProgressToast(text: string): {
4
4
  show: Promise<boolean>;
5
5
  setProgress: (progress: number) => void;
6
6
  };
@@ -41,13 +41,13 @@ async function drain() {
41
41
  PROCESSING = false;
42
42
  }
43
43
  // ─── Public API (imperative, unchanged signatures) ────────────────────────────
44
- export async function sendInformaticToast(html, type, duration = 5000) {
44
+ export async function sendInformaticToast(text, type, duration = 5000) {
45
45
  return new Promise((resolveShow) => {
46
46
  QUEUE.push(() => {
47
47
  let resolveDone;
48
48
  const done = new Promise((res) => { resolveDone = res; });
49
49
  const toast = {
50
- state: { html, type, progress: null },
50
+ state: { text, type, progress: null },
51
51
  done,
52
52
  resolveDone,
53
53
  resolveShow,
@@ -61,7 +61,7 @@ export async function sendInformaticToast(html, type, duration = 5000) {
61
61
  void drain();
62
62
  });
63
63
  }
64
- export function sendProgressToast(html) {
64
+ export function sendProgressToast(text) {
65
65
  let setter = () => { };
66
66
  let current = 0;
67
67
  const show = new Promise((resolveShow) => {
@@ -73,7 +73,7 @@ export function sendProgressToast(html) {
73
73
  let resolveDone;
74
74
  const done = new Promise((res) => { resolveDone = res; });
75
75
  const toast = {
76
- state: { html, type: 'info', progress: current },
76
+ state: { text, type: 'info', progress: current },
77
77
  done,
78
78
  resolveDone,
79
79
  resolveShow,
@@ -246,7 +246,7 @@ function ToastRoot() {
246
246
  borderRadius: 999,
247
247
  background: `${p.accent}18`,
248
248
  color: p.accent,
249
- }, children: _jsx("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: p.icon }) }) }), _jsx("span", { style: { flex: 1, minWidth: 0, overflowWrap: 'anywhere' }, dangerouslySetInnerHTML: { __html: state.html } }), _jsx("span", { "aria-hidden": "true", style: { flex: '0 0 auto', color: '#8d8981', fontSize: 16, lineHeight: 1 }, children: "\u00D7" }), state.progress !== null && (_jsx("span", { style: {
249
+ }, children: _jsx("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.4, strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: p.icon }) }) }), _jsx("span", { style: { flex: 1, minWidth: 0, overflowWrap: 'anywhere' }, children: state.text }), _jsx("span", { "aria-hidden": "true", style: { flex: '0 0 auto', color: '#8d8981', fontSize: 16, lineHeight: 1 }, children: "\u00D7" }), state.progress !== null && (_jsx("span", { style: {
250
250
  position: 'absolute',
251
251
  left: 0,
252
252
  bottom: 0,
@@ -12,6 +12,8 @@ export type Webhook = {
12
12
  id: string;
13
13
  url: string;
14
14
  events: EventType[];
15
+ /** Who made it. A webhook goes when its maker can no longer read what it listens to. */
16
+ creator?: Profile;
15
17
  created: Date;
16
18
  };
17
19
  /** What `create` resolves to. The secret is only ever returned here. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fauna-api",
3
- "version": "1.0.686",
3
+ "version": "1.0.692",
4
4
  "description": "The Fauna SDK: accounts, Drive, groups, calendar, chats, payments and every Fauna app (Parrot, Marmot, Magpie, Cicada, Ant, Termite), plus React UI.",
5
5
  "homepage": "https://docs.faunakit.com",
6
6
  "repository": {
@@ -64,7 +64,7 @@
64
64
  "codemirror": "^6.0.2",
65
65
  "gsap": "^3.13.0",
66
66
  "isomorphic-ws": "^5.0.0",
67
- "maplibre-gl": "^5.24.0",
67
+ "maplibre-gl": "^6.16.0",
68
68
  "marked": "^16.4.1",
69
69
  "y-codemirror.next": "^0.3.6",
70
70
  "yjs": "^13.6.33"