nixamp 0.28.38 → 0.28.41
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/README.md +6 -1
- package/backtoschool/dist/assets/{ar-BJrZUvbj.js → ar-BQPKp0M2.js} +1 -1
- package/backtoschool/dist/assets/{cs-DlOUfUxw.js → cs-DjJnhGNB.js} +1 -1
- package/{web/dist/assets/da-qufG3QK3.js → backtoschool/dist/assets/da-CbTvkIH7.js} +1 -1
- package/{web/dist/assets/de-BddWhvFB.js → backtoschool/dist/assets/de-SsCkny26.js} +1 -1
- package/backtoschool/dist/assets/{es-DezARIfJ.js → es-BBlBPcq1.js} +1 -1
- package/backtoschool/dist/assets/{fi-B_zd8oyM.js → fi-ffo35_4W.js} +1 -1
- package/backtoschool/dist/assets/{fr-CMtbCk6s.js → fr-BVvavrk7.js} +1 -1
- package/{web/dist/assets/hi-BMTJ6Zea.js → backtoschool/dist/assets/hi-DDy_zxu6.js} +1 -1
- package/backtoschool/dist/assets/{hls-3VKVEQE3-BFn8NctW.js → hls-3VKVEQE3-QZWXCWjR.js} +1 -1
- package/backtoschool/dist/assets/{hu-DDqnEZUk.js → hu-BoJj7pZa.js} +1 -1
- package/backtoschool/dist/assets/{id-GE_XdNH1.js → id-CXN9gchF.js} +1 -1
- package/backtoschool/dist/assets/{index-Bg1kYd8S.css → index-BaiHu9CD.css} +1 -1
- package/backtoschool/dist/assets/index-Dovv39gH.js +112 -0
- package/backtoschool/dist/assets/{it-BbCfCf0u.js → it-C9g0xD6S.js} +1 -1
- package/backtoschool/dist/assets/{mpegts-D6OaCwgB.js → mpegts-GAwbg-pX.js} +1 -1
- package/backtoschool/dist/assets/{mpegts-LO6RVLD6-CmKDQL_J.js → mpegts-LO6RVLD6-BrVQzFhM.js} +1 -1
- package/{web/dist/assets/nl-gqa06B5b.js → backtoschool/dist/assets/nl-Ciq1Yrk4.js} +1 -1
- package/{web/dist/assets/ru-Bm2pudvY.js → backtoschool/dist/assets/ru-B_ZMUvh6.js} +1 -1
- package/backtoschool/dist/assets/{sv-CCLFqo_m.js → sv-DH0ZO3Eh.js} +1 -1
- package/{web/dist/assets/uk-CncP4EX7.js → backtoschool/dist/assets/uk-Cof-TUZd.js} +1 -1
- package/backtoschool/dist/assets/{vi-D6DJBpIt.js → vi-D0QPpzbE.js} +1 -1
- package/{web/dist/assets/zh-CtcojTCQ.js → backtoschool/dist/assets/zh-CLTzTBOU.js} +1 -1
- package/backtoschool/dist/index.html +59 -8
- package/dist/live-api.d.ts +10 -0
- package/dist/live-api.js +8 -0
- package/dist/live-voice.d.ts +3 -0
- package/dist/live-voice.js +27 -4
- package/dist/nixamp-link-api.d.ts +44 -0
- package/dist/nixamp-link-api.js +190 -0
- package/dist/nixamp-link-types.d.ts +33 -0
- package/dist/nixamp-link-types.js +5 -0
- package/dist/nixamp-link.d.ts +66 -0
- package/dist/nixamp-link.js +231 -0
- package/dist/notify.d.ts +6 -4
- package/dist/notify.js +14 -25
- package/dist/oauth-api.d.ts +1 -1
- package/dist/oauth-api.js +27 -9
- package/dist/oauth-server.d.ts +9 -1
- package/dist/oauth-server.js +24 -2
- package/dist/profiles.d.ts +85 -0
- package/dist/profiles.js +0 -0
- package/dist/server.d.ts +12 -2
- package/dist/server.js +205 -13
- package/dist/voice-provider.d.ts +9 -0
- package/dist/voice-provider.js +27 -0
- package/dist/watch-party.d.ts +8 -0
- package/dist/watch-party.js +21 -0
- package/package.json +6 -6
- package/src/live-api.ts +13 -0
- package/src/live-voice.ts +25 -3
- package/src/nixamp-link-api.ts +220 -0
- package/src/nixamp-link-types.ts +36 -0
- package/src/nixamp-link.ts +265 -0
- package/src/notify.ts +19 -34
- package/src/oauth-api.ts +30 -11
- package/src/oauth-server.ts +24 -2
- package/src/profiles.ts +0 -0
- package/src/server.ts +192 -15
- package/src/voice-provider.ts +22 -0
- package/src/watch-party.ts +23 -0
- package/web/dist/assets/{ar-tv7nkpiY.js → ar-BqLaLSMR.js} +1 -1
- package/web/dist/assets/{cs-D9o3fJ2P.js → cs-CfoQJwKM.js} +1 -1
- package/{backtoschool/dist/assets/da-C724NwZt.js → web/dist/assets/da-8MmH23fh.js} +1 -1
- package/{backtoschool/dist/assets/de-DGjw5Y71.js → web/dist/assets/de-Brry3OMh.js} +1 -1
- package/web/dist/assets/{es-DURiUJ4W.js → es-CGxJs7b8.js} +1 -1
- package/web/dist/assets/{fi-D4OG-FzU.js → fi-BDAAZtto.js} +1 -1
- package/web/dist/assets/{fr-CYahUXc4.js → fr-9sxYL0Qy.js} +1 -1
- package/{backtoschool/dist/assets/hi-B33KN-Pt.js → web/dist/assets/hi-Cc7ZsdL7.js} +1 -1
- package/web/dist/assets/{hls-3VKVEQE3-BblrzMwh.js → hls-3VKVEQE3-Dx7qltzA.js} +1 -1
- package/web/dist/assets/{hu-Cu0wj9R4.js → hu-CvB9aIyN.js} +1 -1
- package/web/dist/assets/{id-DbNfLRpW.js → id-Bmrh06Hw.js} +1 -1
- package/web/dist/assets/index-BQuoiKg9.js +3 -0
- package/web/dist/assets/{index-JvFXyU1l.css → index-Bo3utcDw.css} +1 -1
- package/web/dist/assets/{it-DrOc430y.js → it-SbaxFUIP.js} +1 -1
- package/web/dist/assets/{mpegts-D2RCQTRm.js → mpegts-BdwONu8t.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-SH6yVu0A.js → mpegts-LO6RVLD6-CqW3DT1n.js} +1 -1
- package/{backtoschool/dist/assets/nl-CDHaP4Aw.js → web/dist/assets/nl-CqimSo3B.js} +1 -1
- package/{backtoschool/dist/assets/ru-DiZnvRs-.js → web/dist/assets/ru-wIfpMpU9.js} +1 -1
- package/web/dist/assets/{sv-DjYUXKKI.js → sv-BAyIUHP5.js} +1 -1
- package/{backtoschool/dist/assets/uk-BtLBQT-n.js → web/dist/assets/uk-D2jFJm6l.js} +1 -1
- package/web/dist/assets/{vi-B4xhD0GU.js → vi-BdH3SLlR.js} +1 -1
- package/{backtoschool/dist/assets/zh-C7GUPWP3.js → web/dist/assets/zh-5xz5Agt-.js} +1 -1
- package/web/dist/index.html +33 -2
- package/web/dist/sw.js +23 -23
- package/backtoschool/dist/assets/index-CPUjs97F.js +0 -112
- package/web/dist/assets/index-CzW_V6oA.js +0 -3
package/dist/oauth-api.js
CHANGED
|
@@ -121,7 +121,7 @@ function partyBody(parties, view, caller) {
|
|
|
121
121
|
party: { ...view.party, positionNow: parties.positionNow(view.party) },
|
|
122
122
|
event: view.event,
|
|
123
123
|
links: parties.links(view.party),
|
|
124
|
-
host: view.event.ownerId === caller.account.id,
|
|
124
|
+
host: caller !== null && view.event.ownerId === caller.account.id,
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
// --- the handler ---------------------------------------------------------------
|
|
@@ -282,15 +282,28 @@ export async function handleOAuthApi(request, response, url, options) {
|
|
|
282
282
|
return true;
|
|
283
283
|
}
|
|
284
284
|
const caller = await callerFor(request, options, "parties");
|
|
285
|
-
|
|
285
|
+
const rest = path.slice("/api/v1/watch-parties/".length).split("/");
|
|
286
|
+
const reference = decodeURIComponent(rest[0] ?? "");
|
|
287
|
+
const action = rest[1];
|
|
288
|
+
// Reading one party is open, the way its /live/<slug> page is: the code
|
|
289
|
+
// or the room link IS the invitation, and a person who was handed one
|
|
290
|
+
// on a television or in a terminal has no session on nixamp.com yet.
|
|
291
|
+
// Everything else -- the list, bridging, moving playback, ending --
|
|
292
|
+
// still needs a session or a token with the parties scope.
|
|
293
|
+
const openRead = path !== "/api/v1/watch-parties" && !action && request.method === "GET";
|
|
294
|
+
if (caller === null && !openRead) {
|
|
286
295
|
json(response, 401, { error: "a session, or a token granted the parties scope, is needed here" });
|
|
287
296
|
return true;
|
|
288
297
|
}
|
|
289
298
|
// A client bridges parties under its own origin, so two sites cannot
|
|
290
299
|
// collide on a six-character code; a person acting directly is filed
|
|
291
300
|
// under nixamp itself.
|
|
292
|
-
const origin = caller
|
|
301
|
+
const origin = caller?.clientId || "nixamp";
|
|
293
302
|
if (path === "/api/v1/watch-parties") {
|
|
303
|
+
if (caller === null) {
|
|
304
|
+
json(response, 401, { error: "a session, or a token granted the parties scope, is needed here" });
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
294
307
|
if (request.method === "GET") {
|
|
295
308
|
const wanted = url.searchParams.get("origin");
|
|
296
309
|
const found = await parties.list({
|
|
@@ -324,14 +337,15 @@ export async function handleOAuthApi(request, response, url, options) {
|
|
|
324
337
|
json(response, 405, { error: "GET or POST" });
|
|
325
338
|
return true;
|
|
326
339
|
}
|
|
327
|
-
const rest = path.slice("/api/v1/watch-parties/".length).split("/");
|
|
328
|
-
const reference = decodeURIComponent(rest[0] ?? "");
|
|
329
|
-
const action = rest[1];
|
|
330
340
|
// A party is findable by its code on the origin that bridged it, or by
|
|
331
341
|
// the nixamp slug or room a client was handed, because a nixamp client
|
|
332
|
-
// arriving from a share link has only the latter.
|
|
333
|
-
|
|
334
|
-
|
|
342
|
+
// arriving from a share link has only the latter. A person on
|
|
343
|
+
// nixamp.com with only a code gets the party that code names on
|
|
344
|
+
// whichever site bridged it.
|
|
345
|
+
const view = (await parties.byCode(origin, reference).catch(() => null)) ??
|
|
346
|
+
(await parties.byEvent(reference)) ??
|
|
347
|
+
(caller === null || caller.clientId === "" ? await parties.byAnyCode(reference).catch(() => null) : null);
|
|
348
|
+
if (!view || (caller === null && view.event.visibility === "private")) {
|
|
335
349
|
json(response, 404, { error: "watch party not found" });
|
|
336
350
|
return true;
|
|
337
351
|
}
|
|
@@ -339,6 +353,10 @@ export async function handleOAuthApi(request, response, url, options) {
|
|
|
339
353
|
json(response, 200, partyBody(parties, view, caller));
|
|
340
354
|
return true;
|
|
341
355
|
}
|
|
356
|
+
if (caller === null) {
|
|
357
|
+
json(response, 401, { error: "a session, or a token granted the parties scope, is needed here" });
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
342
360
|
if (action === "playback" && request.method === "POST") {
|
|
343
361
|
let input;
|
|
344
362
|
try {
|
package/dist/oauth-server.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare const SCOPES: {
|
|
|
18
18
|
readonly profile: "who you are on nixamp (your handle)";
|
|
19
19
|
readonly email: "the address on your account";
|
|
20
20
|
readonly parties: "host and join watch parties as you";
|
|
21
|
+
readonly streams: "see the servers you run on nixamp, and what is live on them";
|
|
21
22
|
readonly offline_access: "stay connected without asking again";
|
|
22
23
|
};
|
|
23
24
|
export type Scope = keyof typeof SCOPES;
|
|
@@ -35,7 +36,14 @@ export declare const REFRESH_TTL_MS: number;
|
|
|
35
36
|
*/
|
|
36
37
|
export declare const BITTORRENTED_CLIENT: OAuthClient;
|
|
37
38
|
/**
|
|
38
|
-
*
|
|
39
|
+
* backtoschool.help runs on this codebase and used to tell its teachers that
|
|
40
|
+
* their school identity *was* a nixamp account. It is not, any more: a school
|
|
41
|
+
* account is a school account, and nixamp is the broadcast backend a host may
|
|
42
|
+
* connect -- through this client, like any other site. Public, PKCE only.
|
|
43
|
+
*/
|
|
44
|
+
export declare const BACKTOSCHOOL_CLIENT: OAuthClient;
|
|
45
|
+
/**
|
|
46
|
+
* The registered clients: the built-in ones, plus whatever NIXAMP_OAUTH_CLIENTS
|
|
39
47
|
* names. The variable is a JSON list of `{id, name, redirectUris, secret?,
|
|
40
48
|
* homepage?}`; an entry with the built-in id replaces it, so a staging
|
|
41
49
|
* bittorrented can point the callback somewhere else.
|
package/dist/oauth-server.js
CHANGED
|
@@ -34,6 +34,7 @@ export const SCOPES = {
|
|
|
34
34
|
profile: "who you are on nixamp (your handle)",
|
|
35
35
|
email: "the address on your account",
|
|
36
36
|
parties: "host and join watch parties as you",
|
|
37
|
+
streams: "see the servers you run on nixamp, and what is live on them",
|
|
37
38
|
offline_access: "stay connected without asking again",
|
|
38
39
|
};
|
|
39
40
|
export const SCOPE_NAMES = Object.keys(SCOPES);
|
|
@@ -59,13 +60,32 @@ export const BITTORRENTED_CLIENT = {
|
|
|
59
60
|
],
|
|
60
61
|
};
|
|
61
62
|
/**
|
|
62
|
-
*
|
|
63
|
+
* backtoschool.help runs on this codebase and used to tell its teachers that
|
|
64
|
+
* their school identity *was* a nixamp account. It is not, any more: a school
|
|
65
|
+
* account is a school account, and nixamp is the broadcast backend a host may
|
|
66
|
+
* connect -- through this client, like any other site. Public, PKCE only.
|
|
67
|
+
*/
|
|
68
|
+
export const BACKTOSCHOOL_CLIENT = {
|
|
69
|
+
id: "backtoschool",
|
|
70
|
+
name: "BackToSchool.help",
|
|
71
|
+
homepage: "https://backtoschool.help",
|
|
72
|
+
redirectUris: [
|
|
73
|
+
"https://backtoschool.help/api/v1/nixamp/callback",
|
|
74
|
+
"https://www.backtoschool.help/api/v1/nixamp/callback",
|
|
75
|
+
"http://localhost:5174/api/v1/nixamp/callback",
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The registered clients: the built-in ones, plus whatever NIXAMP_OAUTH_CLIENTS
|
|
63
80
|
* names. The variable is a JSON list of `{id, name, redirectUris, secret?,
|
|
64
81
|
* homepage?}`; an entry with the built-in id replaces it, so a staging
|
|
65
82
|
* bittorrented can point the callback somewhere else.
|
|
66
83
|
*/
|
|
67
84
|
export function clientsFrom(env) {
|
|
68
|
-
const byId = new Map([
|
|
85
|
+
const byId = new Map([
|
|
86
|
+
[BITTORRENTED_CLIENT.id, BITTORRENTED_CLIENT],
|
|
87
|
+
[BACKTOSCHOOL_CLIENT.id, BACKTOSCHOOL_CLIENT],
|
|
88
|
+
]);
|
|
69
89
|
const raw = env["NIXAMP_OAUTH_CLIENTS"];
|
|
70
90
|
if (raw) {
|
|
71
91
|
let parsed = [];
|
|
@@ -95,6 +115,8 @@ export function clientsFrom(env) {
|
|
|
95
115
|
}
|
|
96
116
|
if (env["NIXAMP_OAUTH_BITTORRENTED"] === "off")
|
|
97
117
|
byId.delete(BITTORRENTED_CLIENT.id);
|
|
118
|
+
if (env["NIXAMP_OAUTH_BACKTOSCHOOL"] === "off")
|
|
119
|
+
byId.delete(BACKTOSCHOOL_CLIENT.id);
|
|
98
120
|
return [...byId.values()];
|
|
99
121
|
}
|
|
100
122
|
/** Loopback is the address; the port is whatever the CLI got (RFC 8252 §7.3). */
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Queryable } from "./follows.ts";
|
|
2
|
+
/** The card as the account holder sees it. */
|
|
3
|
+
export interface Profile {
|
|
4
|
+
name: string;
|
|
5
|
+
homepage: string;
|
|
6
|
+
/** A picture somewhere on the web, when they gave one instead of uploading. */
|
|
7
|
+
avatarUrl: string;
|
|
8
|
+
bio: string;
|
|
9
|
+
/** Set when a photo is stored here; the etag changes with the bytes. */
|
|
10
|
+
photo: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProfileInput {
|
|
13
|
+
name?: unknown;
|
|
14
|
+
homepage?: unknown;
|
|
15
|
+
avatarUrl?: unknown;
|
|
16
|
+
bio?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** What a classroom shows about its host. */
|
|
19
|
+
export interface HostCard {
|
|
20
|
+
hostName: string;
|
|
21
|
+
homepageUrl: string;
|
|
22
|
+
avatarUrl: string;
|
|
23
|
+
}
|
|
24
|
+
export declare const EMPTY_PROFILE: Profile;
|
|
25
|
+
/** A photo is a picture, not a document: a megabyte is plenty for a face. */
|
|
26
|
+
export declare const PHOTO_LIMIT: number;
|
|
27
|
+
export declare const NAME_LIMIT = 100;
|
|
28
|
+
export declare const BIO_LIMIT = 1000;
|
|
29
|
+
/**
|
|
30
|
+
* The type is read from the bytes, never from the header: a browser sends
|
|
31
|
+
* whatever it was told, and what is served back must be what the file is.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sniffImage(bytes: Uint8Array): string;
|
|
34
|
+
/**
|
|
35
|
+
* What may be written. Every field is optional and what is not sent is
|
|
36
|
+
* kept; an empty string clears. A link must be https without credentials,
|
|
37
|
+
* the same rule a classroom applies, so a card never fails at class time.
|
|
38
|
+
*/
|
|
39
|
+
export declare function cleanProfile(input: ProfileInput): {
|
|
40
|
+
fields: Partial<Profile>;
|
|
41
|
+
error: string;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The parts of an OpenProfile.md that say who somebody is: the `#` line is
|
|
45
|
+
* the name, the identity block gives `Avatar` and `Web`, the first prose
|
|
46
|
+
* line is the headline, and the first bullet under `## Photos` stands in
|
|
47
|
+
* for a missing Avatar. Everything else is somebody else's business.
|
|
48
|
+
*/
|
|
49
|
+
export declare function readOpenProfile(markdown: string): Partial<Profile>;
|
|
50
|
+
/** The three things a classroom shows, from the card, with a stored photo served from here. */
|
|
51
|
+
export declare function hostCard(profile: Profile, userId: string, site: string): HostCard;
|
|
52
|
+
export declare class AccountProfiles {
|
|
53
|
+
private readonly db;
|
|
54
|
+
private readonly fetcher;
|
|
55
|
+
private ready;
|
|
56
|
+
constructor(db: Queryable, fetcher?: typeof fetch);
|
|
57
|
+
private ensure;
|
|
58
|
+
of(userId: string): Promise<Profile>;
|
|
59
|
+
/** Write what was sent, keep what was not. */
|
|
60
|
+
set(userId: string, input: ProfileInput): Promise<{
|
|
61
|
+
profile: Profile | null;
|
|
62
|
+
error: string;
|
|
63
|
+
}>;
|
|
64
|
+
/** Keep a photo. The type comes from the bytes; a file that is not a picture is refused. */
|
|
65
|
+
setPhoto(userId: string, bytes: Uint8Array): Promise<{
|
|
66
|
+
profile: Profile | null;
|
|
67
|
+
error: string;
|
|
68
|
+
}>;
|
|
69
|
+
removePhoto(userId: string): Promise<Profile>;
|
|
70
|
+
/** The stored photo, to serve. Null when there is none. */
|
|
71
|
+
photo(userId: string): Promise<{
|
|
72
|
+
bytes: Uint8Array;
|
|
73
|
+
type: string;
|
|
74
|
+
etag: string;
|
|
75
|
+
} | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Fill the card from an OpenProfile.md. What the file says replaces what
|
|
78
|
+
* is here; what the file does not say is kept. A stored photo stays: an
|
|
79
|
+
* Avatar link is only used where no photo was uploaded.
|
|
80
|
+
*/
|
|
81
|
+
importFrom(userId: string, url: string): Promise<{
|
|
82
|
+
profile: Profile | null;
|
|
83
|
+
error: string;
|
|
84
|
+
}>;
|
|
85
|
+
}
|
package/dist/profiles.js
ADDED
|
Binary file
|
package/dist/server.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ import { type ResetMail } from "./password-reset.ts";
|
|
|
11
11
|
import { Handles } from "./handles.ts";
|
|
12
12
|
import { OpenDirs } from "./opendirs.ts";
|
|
13
13
|
import { Servers } from "./servers.ts";
|
|
14
|
+
import { AccountProfiles } from "./profiles.ts";
|
|
15
|
+
import { NixampLinks } from "./nixamp-link.ts";
|
|
14
16
|
import { SignIn } from "./oauth.ts";
|
|
15
17
|
import { AuthorizationServer } from "./oauth-server.ts";
|
|
16
18
|
import { WatchParties } from "./watch-party.ts";
|
|
@@ -29,6 +31,7 @@ import { Names } from "./names.ts";
|
|
|
29
31
|
import { Certs } from "./certs.ts";
|
|
30
32
|
import { type Throttle } from "@profullstack/throttle";
|
|
31
33
|
import { type Notification } from "./notify.ts";
|
|
34
|
+
import { type VapidKeys } from "@profullstack/notifications/server";
|
|
32
35
|
import { EventWriter } from "./event-writer.ts";
|
|
33
36
|
import { LiveEvents, type LiveEvent } from "./live-events.ts";
|
|
34
37
|
import { Tickets } from "./tickets.ts";
|
|
@@ -669,6 +672,10 @@ export interface HandlerOptions {
|
|
|
669
672
|
servers?: Servers;
|
|
670
673
|
/** The name other people see, which is never the address they signed up with. */
|
|
671
674
|
handles?: Handles;
|
|
675
|
+
/** The account's card: a name, a homepage, a photo, a line. One table, read by every site here. */
|
|
676
|
+
accountProfiles?: AccountProfiles;
|
|
677
|
+
/** nixamp accounts connected to accounts here, where this site is a client of nixamp.com. */
|
|
678
|
+
links?: NixampLinks;
|
|
672
679
|
/** Open directories people have found, which anyone may read. */
|
|
673
680
|
openDirs?: OpenDirs;
|
|
674
681
|
/** True when this instance is reached over https, for the cookie's Secure. */
|
|
@@ -764,8 +771,11 @@ export interface HandlerOptions {
|
|
|
764
771
|
* Response it is refused with.
|
|
765
772
|
*/
|
|
766
773
|
throttle?: Throttle;
|
|
767
|
-
/**
|
|
768
|
-
|
|
774
|
+
/**
|
|
775
|
+
* The VAPID pair, read from the environment when the server starts. Only the
|
|
776
|
+
* public half leaves: it is what a browser needs before it can subscribe.
|
|
777
|
+
*/
|
|
778
|
+
vapidKeys?: VapidKeys | null;
|
|
769
779
|
}
|
|
770
780
|
/**
|
|
771
781
|
* The whole HTTP surface, as a plain function of a request — so a test can
|
package/dist/server.js
CHANGED
|
@@ -33,10 +33,13 @@ import { passwordResetPage } from "./password-reset-page.js";
|
|
|
33
33
|
import { anonymousHandle, Handles } from "./handles.js";
|
|
34
34
|
import { OpenDirs } from "./opendirs.js";
|
|
35
35
|
import { Servers } from "./servers.js";
|
|
36
|
+
import { AccountProfiles, hostCard, PHOTO_LIMIT } from "./profiles.js";
|
|
37
|
+
import { NixampLinks, nixampExchange } from "./nixamp-link.js";
|
|
38
|
+
import { handleNixampLinkApi, nixampLinkPath } from "./nixamp-link-api.js";
|
|
36
39
|
import { DeviceGrants } from "./device.js";
|
|
37
40
|
import { BAD_KEY_LIMIT, callerOf, Guard, SIGN_IN_LIMIT } from "./guard.js";
|
|
38
41
|
import { deviceDonePage, devicePage, exchangeCode, providersFrom, signInFailedPage, SignIn, } from "./oauth.js";
|
|
39
|
-
import { AuthorizationServer, SCOPE_NAMES, clientsFrom } from "./oauth-server.js";
|
|
42
|
+
import { AuthorizationServer, BACKTOSCHOOL_CLIENT, SCOPE_NAMES, clientsFrom } from "./oauth-server.js";
|
|
40
43
|
import { handleOAuthApi, oauthApiPath } from "./oauth-api.js";
|
|
41
44
|
import { WatchParties } from "./watch-party.js";
|
|
42
45
|
import { needsAdmin, needsMember, Owner } from "./owner.js";
|
|
@@ -66,6 +69,7 @@ import { forbiddenLibrary, readLibrary } from "./library.js";
|
|
|
66
69
|
import { createThrottle, presentedCredential } from "@profullstack/throttle";
|
|
67
70
|
import { Durable } from "./durable.js";
|
|
68
71
|
import { notifyAll, resendEmail, webPush } from "./notify.js";
|
|
72
|
+
import { vapidKeysFromEnv, vapidPublicKeyResponse } from "@profullstack/notifications/server";
|
|
69
73
|
import { inviteSubject, inviteText, isEmail, isPhone, watchLink } from "./invite.js";
|
|
70
74
|
import { EventWriter } from "./event-writer.js";
|
|
71
75
|
import { handleLiveApi } from "./live-api.js";
|
|
@@ -1138,6 +1142,12 @@ export function isSignInPath(path) {
|
|
|
1138
1142
|
path === "/api/v1/servers" ||
|
|
1139
1143
|
path.startsWith("/api/v1/servers/") ||
|
|
1140
1144
|
path === "/api/v1/me/handle" ||
|
|
1145
|
+
path === "/api/v1/me/profile" ||
|
|
1146
|
+
path.startsWith("/api/v1/me/profile/") ||
|
|
1147
|
+
// A stored photo is public: it is on every class the host runs.
|
|
1148
|
+
path.startsWith("/api/v1/profiles/") ||
|
|
1149
|
+
// "Connect nixamp" on a site that is a client of nixamp.com.
|
|
1150
|
+
nixampLinkPath(path) ||
|
|
1141
1151
|
// Public to read, so it must not be behind a share key either.
|
|
1142
1152
|
path === "/api/v1/opendirs" ||
|
|
1143
1153
|
path.startsWith("/api/v1/opendirs/") ||
|
|
@@ -1447,6 +1457,18 @@ export function createHandler(engine, options) {
|
|
|
1447
1457
|
secureCookies: options.secureCookies ?? false,
|
|
1448
1458
|
}))
|
|
1449
1459
|
return;
|
|
1460
|
+
// A site here that is a client of nixamp.com: connect, callback, streams.
|
|
1461
|
+
if (options.links && options.accounts && options.authServer && await handleNixampLinkApi(request, response, url, {
|
|
1462
|
+
links: options.links,
|
|
1463
|
+
accounts: options.accounts,
|
|
1464
|
+
issuer: options.site ?? DEFAULT_DIRECTORY,
|
|
1465
|
+
site: accountSite,
|
|
1466
|
+
redirectUris: options.authServer.client(BACKTOSCHOOL_CLIENT.id)?.redirectUris ?? [],
|
|
1467
|
+
home: "/#settings",
|
|
1468
|
+
secureCookies: options.secureCookies ?? false,
|
|
1469
|
+
fetcher: options.links.fetcher,
|
|
1470
|
+
}))
|
|
1471
|
+
return;
|
|
1450
1472
|
if (options.events && await handleLiveApi(request, response, url, {
|
|
1451
1473
|
events: options.events,
|
|
1452
1474
|
...(options.eventWriter ? { eventWriter: options.eventWriter } : {}),
|
|
@@ -1457,6 +1479,9 @@ export function createHandler(engine, options) {
|
|
|
1457
1479
|
...(eventSite ? { site: eventSite } : {}),
|
|
1458
1480
|
...(options.invites?.email ? { email: options.invites.email } : {}),
|
|
1459
1481
|
...(options.onEventUpdated ? { onEventUpdated: options.onEventUpdated } : {}),
|
|
1482
|
+
...(options.accountProfiles
|
|
1483
|
+
? { hostProfile: async (userId) => hostCard(await options.accountProfiles.of(userId), userId, accountSite) }
|
|
1484
|
+
: {}),
|
|
1460
1485
|
}))
|
|
1461
1486
|
return;
|
|
1462
1487
|
// The page explaining the reminder texts. Public for the same reason the
|
|
@@ -1781,9 +1806,12 @@ export function createHandler(engine, options) {
|
|
|
1781
1806
|
// --- where to reach a follower ----------------------------------------
|
|
1782
1807
|
if (path.startsWith("/api/v1/notify") && options.follows && options.accounts) {
|
|
1783
1808
|
// The key is public by design: it is what a browser needs before it can
|
|
1784
|
-
// ask permission, and it is useless without the private half.
|
|
1809
|
+
// ask permission, and it is useless without the private half. Served at
|
|
1810
|
+
// run time, never built into the page: a key baked in at build time is
|
|
1811
|
+
// how push quietly becomes "not supported" in production. 503 when this
|
|
1812
|
+
// instance has no keys, so the browser can say so.
|
|
1785
1813
|
if (path === "/api/v1/notify/key" && request.method === "GET") {
|
|
1786
|
-
|
|
1814
|
+
await answerWith(response, vapidPublicKeyResponse(options.vapidKeys));
|
|
1787
1815
|
return;
|
|
1788
1816
|
}
|
|
1789
1817
|
const me = await options.accounts.whoIs(tokenFrom(request.headers));
|
|
@@ -1826,7 +1854,19 @@ export function createHandler(engine, options) {
|
|
|
1826
1854
|
}
|
|
1827
1855
|
if (path === "/api/v1/notify/subscribe") {
|
|
1828
1856
|
if (request.method === "DELETE") {
|
|
1829
|
-
|
|
1857
|
+
// The endpoint arrives in the query (older pages) or as the JSON
|
|
1858
|
+
// body @profullstack/notifications sends: { endpoint }.
|
|
1859
|
+
let endpoint = url.searchParams.get("endpoint") ?? "";
|
|
1860
|
+
if (!endpoint) {
|
|
1861
|
+
try {
|
|
1862
|
+
const body = JSON.parse(await readBody(request));
|
|
1863
|
+
if (typeof body.endpoint === "string")
|
|
1864
|
+
endpoint = body.endpoint;
|
|
1865
|
+
}
|
|
1866
|
+
catch {
|
|
1867
|
+
// No body: nothing to forget.
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1830
1870
|
await follows.removePush(endpoint);
|
|
1831
1871
|
json(response, 200, { ok: true });
|
|
1832
1872
|
return;
|
|
@@ -3137,6 +3177,146 @@ export function createHandler(engine, options) {
|
|
|
3137
3177
|
json(response, 405, { error: "GET or PUT" });
|
|
3138
3178
|
return;
|
|
3139
3179
|
}
|
|
3180
|
+
// --- the account's card -------------------------------------------------
|
|
3181
|
+
//
|
|
3182
|
+
// A name, a homepage, a photo and a line, said once. A class is made from
|
|
3183
|
+
// it, a settings page edits it, and an OpenProfile.md fills it in one move.
|
|
3184
|
+
if ((path === "/api/v1/me/profile" || path.startsWith("/api/v1/me/profile/")) && options.accountProfiles && options.accounts) {
|
|
3185
|
+
const profiles = options.accountProfiles;
|
|
3186
|
+
const who = await options.accounts.whoIs(tokenFrom(request.headers));
|
|
3187
|
+
if (who === null) {
|
|
3188
|
+
json(response, 401, { error: "not signed in" });
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
const persona = options.handles ? await options.handles.persona(who.id) : null;
|
|
3192
|
+
const answer = async () => {
|
|
3193
|
+
const profile = await profiles.of(who.id);
|
|
3194
|
+
// Read again: an import may just have remembered the address.
|
|
3195
|
+
const now = options.handles ? await options.handles.persona(who.id) : null;
|
|
3196
|
+
json(response, 200, {
|
|
3197
|
+
...profile,
|
|
3198
|
+
card: hostCard(profile, who.id, accountSite),
|
|
3199
|
+
openProfile: now?.profile ?? "",
|
|
3200
|
+
handle: now?.handle || fallbackHandle(who.id),
|
|
3201
|
+
});
|
|
3202
|
+
};
|
|
3203
|
+
if (path === "/api/v1/me/profile") {
|
|
3204
|
+
if (request.method === "GET") {
|
|
3205
|
+
await answer();
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
if (request.method === "PUT" || request.method === "POST") {
|
|
3209
|
+
let body;
|
|
3210
|
+
try {
|
|
3211
|
+
body = JSON.parse(await readBody(request));
|
|
3212
|
+
}
|
|
3213
|
+
catch {
|
|
3214
|
+
json(response, 400, { error: "bad JSON" });
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
const written = await profiles.set(who.id, body);
|
|
3218
|
+
if (written.error) {
|
|
3219
|
+
json(response, 422, { error: written.error });
|
|
3220
|
+
return;
|
|
3221
|
+
}
|
|
3222
|
+
await answer();
|
|
3223
|
+
return;
|
|
3224
|
+
}
|
|
3225
|
+
json(response, 405, { error: "GET or PUT" });
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
if (path === "/api/v1/me/profile/import") {
|
|
3229
|
+
if (request.method !== "POST") {
|
|
3230
|
+
json(response, 405, { error: "POST only" });
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
let body = {};
|
|
3234
|
+
try {
|
|
3235
|
+
const raw = await readBody(request);
|
|
3236
|
+
body = raw.trim() === "" ? {} : JSON.parse(raw);
|
|
3237
|
+
}
|
|
3238
|
+
catch {
|
|
3239
|
+
json(response, 400, { error: "bad JSON" });
|
|
3240
|
+
return;
|
|
3241
|
+
}
|
|
3242
|
+
const given = typeof body.url === "string" ? body.url.trim() : "";
|
|
3243
|
+
const from = given || persona?.profile || "";
|
|
3244
|
+
if (from === "") {
|
|
3245
|
+
json(response, 422, { error: "give the URL of your OpenProfile.md, like https://you.example/.well-known/openprofile.md" });
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
const imported = await profiles.importFrom(who.id, from);
|
|
3249
|
+
if (imported.error) {
|
|
3250
|
+
json(response, 422, { error: imported.error });
|
|
3251
|
+
return;
|
|
3252
|
+
}
|
|
3253
|
+
// The address is remembered, so the phone line reads the same file
|
|
3254
|
+
// for the voice, and a later import needs no URL.
|
|
3255
|
+
if (given && options.handles && given !== persona?.profile) {
|
|
3256
|
+
await options.handles.describe(who.id, fallbackHandle(who.id), { profile: given });
|
|
3257
|
+
}
|
|
3258
|
+
await answer();
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
if (path === "/api/v1/me/profile/photo") {
|
|
3262
|
+
if (request.method === "PUT" || request.method === "POST") {
|
|
3263
|
+
let bytes;
|
|
3264
|
+
try {
|
|
3265
|
+
bytes = await readBytes(request, PHOTO_LIMIT);
|
|
3266
|
+
}
|
|
3267
|
+
catch {
|
|
3268
|
+
json(response, 413, { error: "a photo may be up to 1 MB" });
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
const kept = await profiles.setPhoto(who.id, bytes);
|
|
3272
|
+
if (kept.error) {
|
|
3273
|
+
json(response, 422, { error: kept.error });
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
await answer();
|
|
3277
|
+
return;
|
|
3278
|
+
}
|
|
3279
|
+
if (request.method === "DELETE") {
|
|
3280
|
+
await profiles.removePhoto(who.id);
|
|
3281
|
+
await answer();
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
json(response, 405, { error: "PUT or DELETE" });
|
|
3285
|
+
return;
|
|
3286
|
+
}
|
|
3287
|
+
json(response, 404, { error: "no such profile route" });
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
// The stored photo, public: it is on every class the host runs, and a
|
|
3291
|
+
// page shows it to strangers by design. Cached by its etag.
|
|
3292
|
+
if (path.startsWith("/api/v1/profiles/") && path.endsWith("/photo") && options.accountProfiles) {
|
|
3293
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
3294
|
+
json(response, 405, { error: "GET only" });
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
const userId = decodeURIComponent(path.slice("/api/v1/profiles/".length, -"/photo".length));
|
|
3298
|
+
const photo = userId === "" ? null : await options.accountProfiles.photo(userId);
|
|
3299
|
+
if (photo === null) {
|
|
3300
|
+
json(response, 404, { error: "no photo" });
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
const etag = `"${photo.etag}"`;
|
|
3304
|
+
if (request.headers["if-none-match"] === etag) {
|
|
3305
|
+
response.writeHead(304, { etag, "cache-control": "public, max-age=86400" });
|
|
3306
|
+
response.end();
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
response.writeHead(200, {
|
|
3310
|
+
"content-type": photo.type,
|
|
3311
|
+
"content-length": photo.bytes.length,
|
|
3312
|
+
etag,
|
|
3313
|
+
"cache-control": "public, max-age=86400",
|
|
3314
|
+
"x-content-type-options": "nosniff",
|
|
3315
|
+
"access-control-allow-origin": "*",
|
|
3316
|
+
});
|
|
3317
|
+
response.end(request.method === "HEAD" ? undefined : Buffer.from(photo.bytes));
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
3140
3320
|
// --- the servers this account runs ----------------------------------
|
|
3141
3321
|
//
|
|
3142
3322
|
// Kept against the account rather than the machine, so the list reads the
|
|
@@ -5497,7 +5677,11 @@ function sendFile(request, response, file) {
|
|
|
5497
5677
|
export function createServer(engine, options) {
|
|
5498
5678
|
const handle = createHandler(engine, options);
|
|
5499
5679
|
const onRequest = (request, response) => {
|
|
5500
|
-
handle(request, response).catch(() => {
|
|
5680
|
+
handle(request, response).catch((error) => {
|
|
5681
|
+
// Silent by default: a stack trace per bad request is a log nobody
|
|
5682
|
+
// reads. Said aloud when asked, which is how a 500 in a test is found.
|
|
5683
|
+
if (process.env["NIXAMP_DEBUG"])
|
|
5684
|
+
console.error(error);
|
|
5501
5685
|
if (!response.headersSent)
|
|
5502
5686
|
json(response, 500, { error: "server error" });
|
|
5503
5687
|
else
|
|
@@ -5849,8 +6033,8 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
5849
6033
|
// The two things that were promises kept only in memory: a caller who was
|
|
5850
6034
|
// told they would be texted, and the ended stream a code still points at.
|
|
5851
6035
|
const durable = pool ? new Durable(pool, (message) => console.log(message)) : undefined;
|
|
5852
|
-
|
|
5853
|
-
const
|
|
6036
|
+
// Read at run time, and null unless both halves are set.
|
|
6037
|
+
const vapidKeys = vapidKeysFromEnv(process.env);
|
|
5854
6038
|
/**
|
|
5855
6039
|
* Tell a broadcaster's followers, on whatever they asked to be told on.
|
|
5856
6040
|
*
|
|
@@ -5888,11 +6072,10 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
5888
6072
|
}),
|
|
5889
6073
|
}
|
|
5890
6074
|
: {}),
|
|
5891
|
-
...(
|
|
6075
|
+
...(vapidKeys
|
|
5892
6076
|
? {
|
|
5893
6077
|
push: webPush({
|
|
5894
|
-
|
|
5895
|
-
privateKey: vapidPrivateKey,
|
|
6078
|
+
...vapidKeys,
|
|
5896
6079
|
subject: process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY,
|
|
5897
6080
|
onEvent: (message) => console.log(message),
|
|
5898
6081
|
}),
|
|
@@ -5927,7 +6110,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
5927
6110
|
};
|
|
5928
6111
|
void follows.audience(event.ownerId).then((audience) => notifyAll(audience, note, {
|
|
5929
6112
|
...(eventMail ? { email: resendEmail({ ...eventMail, onEvent: (message) => console.log(message) }) } : {}),
|
|
5930
|
-
...(
|
|
6113
|
+
...(vapidKeys ? { push: webPush({ ...vapidKeys, subject: nixampSite, onEvent: (message) => console.log(message) }) } : {}),
|
|
5931
6114
|
onGone: (endpoint) => follows.removePush(endpoint),
|
|
5932
6115
|
onEvent: (message) => console.log(message),
|
|
5933
6116
|
})).catch(() => undefined);
|
|
@@ -6249,7 +6432,7 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
6249
6432
|
load: (next) => loadSource(tools, next, false),
|
|
6250
6433
|
tag: (next) => loadTagged(tools, next),
|
|
6251
6434
|
...(directory ? { directory } : {}),
|
|
6252
|
-
...(follows ? { follows,
|
|
6435
|
+
...(follows ? { follows, vapidKeys } : {}),
|
|
6253
6436
|
...(favorites ? { favorites } : {}),
|
|
6254
6437
|
...(settingsSync ? { settingsSync } : {}),
|
|
6255
6438
|
...(events ? { events, eventWriter: new EventWriter({
|
|
@@ -6341,7 +6524,16 @@ export async function serve(argv, version = "0.1.0") {
|
|
|
6341
6524
|
// The same pool the follows and reminders use: three small tables in
|
|
6342
6525
|
// one database do not want three sets of connections.
|
|
6343
6526
|
...(pool
|
|
6344
|
-
? {
|
|
6527
|
+
? {
|
|
6528
|
+
servers: new Servers(pool),
|
|
6529
|
+
handles: new Handles(pool),
|
|
6530
|
+
openDirs: new OpenDirs(pool),
|
|
6531
|
+
accountProfiles: new AccountProfiles(pool),
|
|
6532
|
+
// The school's side of "Connect nixamp": tokens from nixamp.com,
|
|
6533
|
+
// kept against the school account. The issuer is this same
|
|
6534
|
+
// program in production, reached over HTTP like any client.
|
|
6535
|
+
links: new NixampLinks(pool, nixampExchange(nixampSite, BACKTOSCHOOL_CLIENT.id), BACKTOSCHOOL_CLIENT.id),
|
|
6536
|
+
}
|
|
6345
6537
|
: {}),
|
|
6346
6538
|
signIn: new SignIn(providersFrom(process.env), new DeviceGrants(), process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY),
|
|
6347
6539
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SpeechError } from "./speech.ts";
|
|
2
|
+
/** Only fixed messages/codes leave this boundary: provider bodies can contain
|
|
3
|
+
* account information. Authorization and billing errors must not be retried
|
|
4
|
+
* as transient 502s by the live player. */
|
|
5
|
+
export declare function voiceProviderFailure(response: Response): Promise<{
|
|
6
|
+
error: SpeechError;
|
|
7
|
+
code: string;
|
|
8
|
+
cooldownMs: number;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SpeechError } from "./speech.js";
|
|
2
|
+
/** Only fixed messages/codes leave this boundary: provider bodies can contain
|
|
3
|
+
* account information. Authorization and billing errors must not be retried
|
|
4
|
+
* as transient 502s by the live player. */
|
|
5
|
+
export async function voiceProviderFailure(response) {
|
|
6
|
+
let code = "unknown";
|
|
7
|
+
try {
|
|
8
|
+
const body = await response.json();
|
|
9
|
+
const status = body?.detail?.status;
|
|
10
|
+
if (typeof status === "string" && ["quota_exceeded", "invalid_api_key", "missing_permissions", "subscription_required", "subscription_expired", "payment_required", "payment_issue", "too_many_concurrent_requests", "rate_limit_exceeded"].includes(status))
|
|
11
|
+
code = status;
|
|
12
|
+
}
|
|
13
|
+
catch { /* Non-JSON provider failures still have an HTTP status. */ }
|
|
14
|
+
if (code === "quota_exceeded")
|
|
15
|
+
return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs credits are exhausted. The site owner needs to check the provider balance.", 402) };
|
|
16
|
+
if (code === "payment_issue")
|
|
17
|
+
return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs blocked transcription because its subscription payment failed. The site owner needs to complete the outstanding ElevenLabs invoice.", 402) };
|
|
18
|
+
if (["subscription_required", "subscription_expired", "payment_required"].includes(code) || response.status === 402)
|
|
19
|
+
return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs billing needs attention. The site owner needs to check the provider subscription.", 402) };
|
|
20
|
+
if (["invalid_api_key", "missing_permissions"].includes(code) || [401, 403].includes(response.status))
|
|
21
|
+
return { code, cooldownMs: 300_000, error: new SpeechError("ElevenLabs rejected transcription or voice access. The site owner needs to check the provider key, permissions, and billing.", 424) };
|
|
22
|
+
if (response.status === 429) {
|
|
23
|
+
const retry = Number(response.headers.get("retry-after"));
|
|
24
|
+
return { code, cooldownMs: Math.min(300_000, Math.max(30_000, Number.isFinite(retry) ? retry * 1000 : 0)), error: new SpeechError("ElevenLabs is rate limited. Wait before enabling translated audio again.", 429) };
|
|
25
|
+
}
|
|
26
|
+
return { code, cooldownMs: 10_000, error: new SpeechError("ElevenLabs is temporarily unavailable. Try translated audio again shortly.", 502) };
|
|
27
|
+
}
|
package/dist/watch-party.d.ts
CHANGED
|
@@ -86,6 +86,14 @@ export declare class WatchParties {
|
|
|
86
86
|
*/
|
|
87
87
|
bridge(ownerId: string, origin: PartyOrigin, input: BridgeInput): Promise<PartyView>;
|
|
88
88
|
byCode(origin: PartyOrigin, partyCode: string): Promise<PartyView | null>;
|
|
89
|
+
/**
|
|
90
|
+
* A party by its code alone, for somebody who has only the code.
|
|
91
|
+
*
|
|
92
|
+
* A code is unique per origin, not across them; a person typing one into
|
|
93
|
+
* nixamp.com is not asked which site it came from. The one still live, or
|
|
94
|
+
* the most recently touched, is the one they mean.
|
|
95
|
+
*/
|
|
96
|
+
byAnyCode(partyCode: string): Promise<PartyView | null>;
|
|
89
97
|
/** The party behind a nixamp room or slug, for a client that has only that. */
|
|
90
98
|
byEvent(reference: string): Promise<PartyView | null>;
|
|
91
99
|
/**
|