@marktiderman/genesis-chat 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -13
- package/dist/{chunk-SAY24E6V.js → chunk-5D5A7I4Q.js} +166 -45
- package/dist/chunk-5D5A7I4Q.js.map +1 -0
- package/dist/{chunk-JQXZUEEL.js → chunk-7S6HYNWW.js} +382 -8
- package/dist/chunk-7S6HYNWW.js.map +1 -0
- package/dist/chunk-BJCE5O7V.js +70 -0
- package/dist/chunk-BJCE5O7V.js.map +1 -0
- package/dist/chunk-PZTOY2LY.js +576 -0
- package/dist/chunk-PZTOY2LY.js.map +1 -0
- package/dist/chunk-VEPZROQB.js +187 -0
- package/dist/chunk-VEPZROQB.js.map +1 -0
- package/dist/{client-BtdALFCx.d.ts → client-DMa2j7kz.d.ts} +152 -11
- package/dist/index.d.ts +78 -163
- package/dist/index.js +3 -2
- package/dist/native.d.ts +1910 -1
- package/dist/native.js +4900 -0
- package/dist/native.js.map +1 -1
- package/dist/outbox-B0y3phE0.d.ts +151 -0
- package/dist/push.d.ts +206 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +1 -0
- package/dist/react.d.ts +149 -7
- package/dist/react.js +3 -3
- package/dist/server.d.ts +510 -21
- package/dist/server.js +863 -21
- package/dist/server.js.map +1 -1
- package/dist/thread-DWXHKPdw.d.ts +57 -0
- package/dist/web.d.ts +313 -10
- package/dist/web.js +1731 -60
- package/dist/web.js.map +1 -1
- package/migrations/0002_genesis_chat_coaching.sql +934 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +265 -0
- package/package.json +74 -6
- package/src/__tests__/coaching-core.test.ts +332 -0
- package/src/__tests__/coaching-react.test.tsx +248 -0
- package/src/__tests__/coaching-web.test.tsx +449 -0
- package/src/__tests__/core.test.ts +91 -0
- package/src/__tests__/outbox.test.ts +122 -0
- package/src/__tests__/realtime.test.ts +70 -0
- package/src/__tests__/review-fixes-core.test.ts +193 -0
- package/src/__tests__/review-fixes-web.test.tsx +402 -0
- package/src/__tests__/server-effects.test.ts +158 -0
- package/src/__tests__/server-fakes.ts +90 -0
- package/src/__tests__/server-integrations.test.ts +247 -0
- package/src/__tests__/server-providers.test.ts +246 -0
- package/src/__tests__/server-runner.test.ts +277 -0
- package/src/__tests__/server-safety.test.ts +290 -0
- package/src/__tests__/web-markdown.test.tsx +88 -0
- package/src/__tests__/web.test.tsx +333 -0
- package/src/core/availability.ts +223 -0
- package/src/core/client.ts +757 -0
- package/src/core/extras.ts +106 -0
- package/src/core/media.ts +144 -0
- package/src/core/outbox.ts +429 -0
- package/src/core/thread.ts +171 -0
- package/src/core/types.ts +311 -0
- package/src/index.ts +13 -0
- package/src/native/__tests__/auto-retry.test.tsx +128 -0
- package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
- package/src/native/__tests__/composer-state.test.ts +188 -0
- package/src/native/__tests__/composer.test.tsx +133 -0
- package/src/native/__tests__/emoji.test.tsx +238 -0
- package/src/native/__tests__/inbox-screen.test.tsx +48 -0
- package/src/native/__tests__/message-bubble.test.tsx +127 -0
- package/src/native/__tests__/organise-logic.test.ts +164 -0
- package/src/native/__tests__/organise-screens.test.tsx +344 -0
- package/src/native/__tests__/photo-viewer.test.tsx +226 -0
- package/src/native/__tests__/push-payload.test.ts +66 -0
- package/src/native/__tests__/push.test.ts +209 -0
- package/src/native/__tests__/recorder-machine.test.ts +96 -0
- package/src/native/__tests__/rows-actions.test.ts +76 -0
- package/src/native/__tests__/thread-layout.test.ts +41 -0
- package/src/native/__tests__/thread-screen.test.tsx +116 -0
- package/src/native/__tests__/tus.test.ts +154 -0
- package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
- package/src/native/__tests__/video-media.test.tsx +421 -0
- package/src/native/__tests__/video-recorder-state.test.ts +87 -0
- package/src/native/__tests__/video-upload.test.ts +227 -0
- package/src/native/access-error.tsx +31 -0
- package/src/native/actions.ts +42 -0
- package/src/native/attachment-cards.tsx +116 -0
- package/src/native/auto-retry.ts +69 -0
- package/src/native/availability.tsx +376 -0
- package/src/native/broadcast-sheet.tsx +193 -0
- package/src/native/composer-state.ts +302 -0
- package/src/native/composer.tsx +453 -0
- package/src/native/context.tsx +70 -0
- package/src/native/copy.ts +296 -0
- package/src/native/emoji-data.ts +21 -0
- package/src/native/emoji-picker.tsx +229 -0
- package/src/native/emoji.ts +118 -0
- package/src/native/icon-button.tsx +40 -0
- package/src/native/inbox-screen.tsx +116 -0
- package/src/native/index.ts +184 -0
- package/src/native/link-preview.tsx +104 -0
- package/src/native/media-colors.ts +15 -0
- package/src/native/media.tsx +187 -0
- package/src/native/message-actions-sheet.tsx +121 -0
- package/src/native/message-bubble.tsx +374 -0
- package/src/native/moderation.tsx +42 -0
- package/src/native/modules.ts +275 -0
- package/src/native/organise-copy.ts +195 -0
- package/src/native/photo-viewer.tsx +531 -0
- package/src/native/push.ts +375 -0
- package/src/native/recorder-machine.ts +176 -0
- package/src/native/rows.ts +81 -0
- package/src/native/schedule-sheet.tsx +314 -0
- package/src/native/search-screen.tsx +168 -0
- package/src/native/stack-options.ts +36 -0
- package/src/native/start-sheet.tsx +87 -0
- package/src/native/thread-screen.tsx +620 -0
- package/src/native/topics.tsx +179 -0
- package/src/native/tus.ts +282 -0
- package/src/native/use-composer.ts +71 -0
- package/src/native/use-jump.ts +91 -0
- package/src/native/use-voice-recorder.ts +234 -0
- package/src/native/video-player.tsx +370 -0
- package/src/native/video-recorder-state.ts +140 -0
- package/src/native/video-recorder.tsx +271 -0
- package/src/native/video-upload-tray.tsx +77 -0
- package/src/native/video-upload.ts +432 -0
- package/src/native/voice-player.tsx +283 -0
- package/src/react/index.tsx +810 -0
- package/src/server/assistant.ts +235 -0
- package/src/server/crypto.ts +86 -0
- package/src/server/expo.ts +119 -0
- package/src/server/handler.ts +32 -0
- package/src/server/index.ts +57 -0
- package/src/server/moderation.ts +155 -0
- package/src/server/mux.ts +290 -0
- package/src/server/notifications.ts +130 -0
- package/src/server/runner.ts +468 -0
- package/src/server/transcribers.ts +111 -0
- package/src/server/types.ts +171 -0
- package/src/server/unfurl.ts +480 -0
- package/src/server/upload.ts +158 -0
- package/src/web/attachments.tsx +379 -0
- package/src/web/availability.tsx +299 -0
- package/src/web/broadcast.tsx +166 -0
- package/src/web/bubble.tsx +563 -0
- package/src/web/cards.tsx +136 -0
- package/src/web/composer.tsx +571 -0
- package/src/web/emoji-picker.tsx +84 -0
- package/src/web/error-boundary.tsx +33 -0
- package/src/web/format.ts +55 -0
- package/src/web/inbox.tsx +195 -0
- package/src/web/index.ts +75 -0
- package/src/web/markdown.tsx +245 -0
- package/src/web/memory-client.ts +706 -0
- package/src/web/panel.tsx +135 -0
- package/src/web/schedule.tsx +192 -0
- package/src/web/search.tsx +129 -0
- package/src/web/thread-context.ts +64 -0
- package/src/web/thread.tsx +802 -0
- package/src/web/topics.tsx +160 -0
- package/src/web/use-voice-recorder.ts +264 -0
- package/src/web/video.tsx +522 -0
- package/src/web/voice-note.tsx +197 -0
- package/dist/chunk-27UTR5FW.js +0 -322
- package/dist/chunk-27UTR5FW.js.map +0 -1
- package/dist/chunk-JQXZUEEL.js.map +0 -1
- package/dist/chunk-SAY24E6V.js.map +0 -1
package/dist/server.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { w as ChatSupabase } from './client-DMa2j7kz.js';
|
|
2
2
|
import '@supabase/supabase-js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -30,18 +30,35 @@ interface ChatNotification {
|
|
|
30
30
|
/** The preview; in a group, prefixed with the sender's name ("Ana: see you"). */
|
|
31
31
|
body: string;
|
|
32
32
|
senderId: string | null;
|
|
33
|
-
/**
|
|
34
|
-
|
|
33
|
+
/**
|
|
34
|
+
* `chat:<conversation id>`, only when the runner's `collapse` is on: a newer push
|
|
35
|
+
* replaces an older one. Off by default, because on iOS it becomes the
|
|
36
|
+
* notification's identifier, so replies from two notifications of one
|
|
37
|
+
* conversation would collide.
|
|
38
|
+
*/
|
|
39
|
+
collapseKey?: string | undefined;
|
|
35
40
|
/** The conversation id: groups a conversation's pushes on the lock screen. */
|
|
36
41
|
threadKey: string;
|
|
37
42
|
/** `push:<message id>`: the same message is never pushed twice by a sender that dedupes. */
|
|
38
43
|
idempotencyKey: string;
|
|
44
|
+
/**
|
|
45
|
+
* The push's data, built by `chatPushPayload` (genesis-chat/push): flat strings the iOS
|
|
46
|
+
* Notification Service Extension reads (sender name and https photo, group name) plus `seq`.
|
|
47
|
+
*/
|
|
39
48
|
data: {
|
|
40
49
|
url: string;
|
|
41
50
|
conversationId: string;
|
|
42
51
|
messageId: string;
|
|
43
52
|
seq: number | null;
|
|
53
|
+
senderId?: string;
|
|
54
|
+
senderName?: string;
|
|
55
|
+
senderAvatar?: string;
|
|
56
|
+
groupName?: string;
|
|
44
57
|
};
|
|
58
|
+
/** Let the iOS extension rewrite it into a Communication Notification (sender photo). */
|
|
59
|
+
mutableContent?: boolean;
|
|
60
|
+
/** The notification category the native push block registers for chat (`messages`). */
|
|
61
|
+
categoryId?: string;
|
|
45
62
|
}
|
|
46
63
|
/**
|
|
47
64
|
* Sends a message's push. Throw to have the effect retried (with backoff, up
|
|
@@ -87,8 +104,40 @@ interface VideoProvider {
|
|
|
87
104
|
supabase: ChatSupabase;
|
|
88
105
|
}): Promise<Response>;
|
|
89
106
|
}
|
|
107
|
+
/** What moderation decided: `hidden` takes a message from members' view, `flagged` files a report only. */
|
|
108
|
+
type ModerationResult = "clean" | "flagged" | "hidden";
|
|
109
|
+
/** A message to moderate: its text and signed URLs (10 minutes) for its photos. */
|
|
110
|
+
interface ModerateInput {
|
|
111
|
+
text: string;
|
|
112
|
+
imageUrls: string[];
|
|
113
|
+
}
|
|
114
|
+
interface ModerationVerdict {
|
|
115
|
+
result: ModerationResult;
|
|
116
|
+
/** A short, safe reason for the report observers see ("harassment 0.93"). Never the message itself. */
|
|
117
|
+
reason?: string | undefined;
|
|
118
|
+
}
|
|
119
|
+
/** A moderation provider. Throw on failure: the effect is retried. */
|
|
120
|
+
interface Moderator {
|
|
121
|
+
readonly name: string;
|
|
122
|
+
moderate(input: ModerateInput): Promise<ModerationVerdict>;
|
|
123
|
+
}
|
|
124
|
+
/** One link's Open Graph data, as `save_link_preview` stores it. */
|
|
125
|
+
interface LinkPreview {
|
|
126
|
+
url: string;
|
|
127
|
+
/** `blocked`: the URL is not one the server may fetch (private address, not https). */
|
|
128
|
+
status: "ok" | "failed" | "blocked";
|
|
129
|
+
title?: string | null | undefined;
|
|
130
|
+
description?: string | null | undefined;
|
|
131
|
+
/** https only. */
|
|
132
|
+
imageUrl?: string | null | undefined;
|
|
133
|
+
siteName?: string | null | undefined;
|
|
134
|
+
}
|
|
135
|
+
/** Fetches a link's preview. Resolve `failed` or `blocked` rather than throw; a throw is retried. */
|
|
136
|
+
interface LinkUnfurler {
|
|
137
|
+
unfurl(url: string): Promise<LinkPreview>;
|
|
138
|
+
}
|
|
90
139
|
/** The outbox kinds the runner works. */
|
|
91
|
-
type ChatEffectKind = "push" | "transcribe" | "storage.remove" | "video.remove";
|
|
140
|
+
type ChatEffectKind = "push" | "transcribe" | "storage.remove" | "video.remove" | "moderate" | "unfurl";
|
|
92
141
|
/** A claimed row of `genesis_chat.effects`. */
|
|
93
142
|
interface ChatEffect {
|
|
94
143
|
id: number;
|
|
@@ -103,23 +152,23 @@ interface ChatRunReport {
|
|
|
103
152
|
done: number;
|
|
104
153
|
/** Failed this time; retried later with backoff until the 8th attempt. */
|
|
105
154
|
failed: number;
|
|
155
|
+
/**
|
|
156
|
+
* Scheduled messages that were due. `skipped`: not posted this run (refused, with the reason
|
|
157
|
+
* on the row; a passing error, tried again next run; or taken by another runner).
|
|
158
|
+
* `failed`: the call itself failed. Absent with `scheduled: false`.
|
|
159
|
+
*/
|
|
160
|
+
scheduled?: {
|
|
161
|
+
due: number;
|
|
162
|
+
posted: number;
|
|
163
|
+
skipped: number;
|
|
164
|
+
failed: number;
|
|
165
|
+
} | undefined;
|
|
166
|
+
/** Messages removed past their kind's retention. Present only with `retention: true`. */
|
|
167
|
+
purged?: number | undefined;
|
|
168
|
+
/** Steps besides the outbox that failed this run (scheduled posting, retention), as short messages. */
|
|
169
|
+
errors?: string[] | undefined;
|
|
106
170
|
}
|
|
107
171
|
|
|
108
|
-
/**
|
|
109
|
-
* The outbox runner: claims due effects from `genesis_chat.effects`, does
|
|
110
|
-
* each one, and settles it. Settling with an error has `complete_effect` back
|
|
111
|
-
* it off (30 s × 2^attempts); `claim_effects` gives up after the 8th attempt.
|
|
112
|
-
*
|
|
113
|
-
* push push_targets(message) → deliver(notification), once per message
|
|
114
|
-
* transcribe signed URL (10 min) → transcriber → update_attachment
|
|
115
|
-
* storage.remove storage.from(bucket).remove([path]); not found is done
|
|
116
|
-
* video.remove video.remove(provider id), when a video provider is given
|
|
117
|
-
*
|
|
118
|
-
* Kinds it doesn't handle are never claimed, so an app's own runner can work
|
|
119
|
-
* them. Effects run concurrently per kind, up to a cap; one failure never
|
|
120
|
-
* stops the others. Errors are stored as short messages, never payloads.
|
|
121
|
-
*/
|
|
122
|
-
|
|
123
172
|
/** `claim_effects` stops claiming an effect after this many attempts. */
|
|
124
173
|
declare const CHAT_MAX_ATTEMPTS = 8;
|
|
125
174
|
interface ChatRunnerOptions {
|
|
@@ -133,7 +182,17 @@ interface ChatRunnerOptions {
|
|
|
133
182
|
transcriptLanguage?: string | undefined;
|
|
134
183
|
/** Hosted video (Mux), to remove an asset when its attachment goes. */
|
|
135
184
|
video?: Pick<VideoProvider, "name" | "remove"> | undefined;
|
|
136
|
-
/**
|
|
185
|
+
/** Moderation for kinds that ask for it (`set_kind_options(kind, p_moderate => true)`): `openAiModerator(...)`. */
|
|
186
|
+
moderator?: Moderator | undefined;
|
|
187
|
+
/** Link previews: `createLinkUnfurler()`. Without one, `unfurl` effects are left unclaimed. */
|
|
188
|
+
unfurler?: LinkUnfurler | undefined;
|
|
189
|
+
/** Post due scheduled messages on each run (default true). */
|
|
190
|
+
scheduled?: boolean | undefined;
|
|
191
|
+
/** Remove messages past their kind's retention on each run (default false: run it once a day). */
|
|
192
|
+
retention?: boolean | undefined;
|
|
193
|
+
/** Messages removed per purge_expired call (default 1000); up to 10 calls a run. */
|
|
194
|
+
retentionBatch?: number | undefined;
|
|
195
|
+
/** The kinds this runner works (default the four base kinds, plus `moderate` and `unfurl` when their adapters are given). */
|
|
137
196
|
kinds?: readonly ChatEffectKind[] | undefined;
|
|
138
197
|
batch?: {
|
|
139
198
|
/** Effects claimed per run (default 50, at most 500). */
|
|
@@ -143,8 +202,18 @@ interface ChatRunnerOptions {
|
|
|
143
202
|
};
|
|
144
203
|
/** A person's display name, for push titles and group previews. */
|
|
145
204
|
resolveName?: ((userId: string) => Promise<string | null | undefined> | string | null | undefined) | undefined;
|
|
205
|
+
/** A person's photo (an https URL), shown on iOS as the sender of a chat push. */
|
|
206
|
+
resolveAvatar?: ((userId: string) => Promise<string | null | undefined> | string | null | undefined) | undefined;
|
|
146
207
|
/** Kinds that are one-to-one (default `["direct"]`): their push title is the sender's name. */
|
|
147
208
|
directKinds?: readonly string[] | undefined;
|
|
209
|
+
/**
|
|
210
|
+
* Set `collapseKey` (`chat:<conversation id>`) so a conversation's newer push
|
|
211
|
+
* replaces the older one. Default false: on iOS the collapse id becomes the
|
|
212
|
+
* notification's identifier, and replies from one conversation's notifications collide.
|
|
213
|
+
*/
|
|
214
|
+
collapse?: boolean | undefined;
|
|
215
|
+
/** Kinds left out of the push badge, matching the app's own badge (`useUnreadTotal({ excludeKinds })`). */
|
|
216
|
+
badgeExcludeKinds?: readonly string[] | undefined;
|
|
148
217
|
/** Where a tapped notification opens (default `/messages/<conversationId>`). */
|
|
149
218
|
linkFor?: ((conversationId: string, messageId: string) => string) | undefined;
|
|
150
219
|
/** Safe log lines (effect ids, kinds, short errors). Default: nothing is logged. */
|
|
@@ -261,6 +330,7 @@ interface ExpoPushMessage {
|
|
|
261
330
|
collapseId?: string;
|
|
262
331
|
channelId: string;
|
|
263
332
|
categoryId: string;
|
|
333
|
+
mutableContent?: boolean;
|
|
264
334
|
}
|
|
265
335
|
type ExpoPushTicket = {
|
|
266
336
|
status: "ok";
|
|
@@ -308,4 +378,423 @@ interface ChatRunnerHandlerOptions {
|
|
|
308
378
|
}
|
|
309
379
|
declare function chatRunnerHandler(options: ChatRunnerHandlerOptions): (request: Request) => Promise<Response>;
|
|
310
380
|
|
|
311
|
-
|
|
381
|
+
/**
|
|
382
|
+
* OpenAI's moderation endpoint behind `Moderator`
|
|
383
|
+
* (https://developers.openai.com/api/docs/guides/moderation):
|
|
384
|
+
*
|
|
385
|
+
* POST https://api.openai.com/v1/moderations
|
|
386
|
+
* { model: "omni-moderation-latest", input: [{ type: "text", text }, { type: "image_url", image_url: { url } }] }
|
|
387
|
+
*
|
|
388
|
+
* Each result has `flagged`, `categories`, `category_scores` (0 to 1) and
|
|
389
|
+
* `category_applied_input_types`. A message's text goes with its first photo;
|
|
390
|
+
* further photos go one request each (the guide doesn't say how many images one
|
|
391
|
+
* request takes), and the highest score per category wins.
|
|
392
|
+
*
|
|
393
|
+
* Mapping: any category at or over its `hide` threshold → `hidden`; else any at
|
|
394
|
+
* or over `flag` (or OpenAI's own `flagged`, unless `trustFlagged: false`) →
|
|
395
|
+
* `flagged`; else `clean`. The reason names the categories and scores, never
|
|
396
|
+
* the text.
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
declare const OPENAI_MODERATIONS_URL = "https://api.openai.com/v1/moderations";
|
|
400
|
+
/** The categories omni-moderation scores (text and images, or text only where noted in OpenAI's guide). */
|
|
401
|
+
declare const OPENAI_MODERATION_CATEGORIES: readonly ["sexual", "sexual/minors", "harassment", "harassment/threatening", "hate", "hate/threatening", "illicit", "illicit/violent", "self-harm", "self-harm/intent", "self-harm/instructions", "violence", "violence/graphic"];
|
|
402
|
+
interface ModerationThresholds {
|
|
403
|
+
/** At or over: a report for observers (default 0.5). */
|
|
404
|
+
flag?: number | undefined;
|
|
405
|
+
/** At or over: the message is hidden from members and reported (default 0.9). */
|
|
406
|
+
hide?: number | undefined;
|
|
407
|
+
}
|
|
408
|
+
interface OpenAiModeratorOptions {
|
|
409
|
+
apiKey: string;
|
|
410
|
+
/** Default `omni-moderation-latest`. */
|
|
411
|
+
model?: string | undefined;
|
|
412
|
+
/** Defaults for every category. */
|
|
413
|
+
thresholds?: ModerationThresholds | undefined;
|
|
414
|
+
/**
|
|
415
|
+
* Per category, over the defaults. Default: `sexual/minors` hides at 0.5 and
|
|
416
|
+
* flags at 0.2; `self-harm/intent` and `self-harm/instructions` flag at 0.3
|
|
417
|
+
* (a coach should see them) and hide at 0.95.
|
|
418
|
+
*/
|
|
419
|
+
categories?: Record<string, ModerationThresholds> | undefined;
|
|
420
|
+
/** Count OpenAI's own `flagged` as at least `flagged` (default true). */
|
|
421
|
+
trustFlagged?: boolean | undefined;
|
|
422
|
+
/** Text sent per request (default 10 000 characters, a message's maximum). */
|
|
423
|
+
maxTextChars?: number | undefined;
|
|
424
|
+
fetch?: typeof fetch | undefined;
|
|
425
|
+
timeoutMs?: number | undefined;
|
|
426
|
+
}
|
|
427
|
+
/** Maps category scores to a verdict with the given thresholds (exported for tests and other providers). */
|
|
428
|
+
declare function moderationVerdict(scores: Record<string, number>, options?: {
|
|
429
|
+
thresholds?: ModerationThresholds | undefined;
|
|
430
|
+
categories?: Record<string, ModerationThresholds> | undefined;
|
|
431
|
+
flagged?: boolean;
|
|
432
|
+
}): ModerationVerdict;
|
|
433
|
+
declare function openAiModerator(options: OpenAiModeratorOptions): Moderator;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Link previews without letting a message aim the server at its own network.
|
|
437
|
+
*
|
|
438
|
+
* - https only, port 443, no user:password@; at most 3 redirects, each hop
|
|
439
|
+
* checked again (redirects are followed by hand, never by fetch);
|
|
440
|
+
* - the host must not be, or resolve to, a private, loopback, link-local,
|
|
441
|
+
* carrier-grade NAT, multicast, documentation or metadata address
|
|
442
|
+
* (169.254.169.254, fd00:ec2::254), nor an IPv6 form that carries one
|
|
443
|
+
* (::ffff:0:0/96 mapped and translated, NAT64, 6to4 2002::/16, Teredo),
|
|
444
|
+
* nor `localhost`, `*.localhost`, `*.local`, `*.internal` or a single-label
|
|
445
|
+
* name;
|
|
446
|
+
* - no URL whose query carries what looks like a credential (`token=`,
|
|
447
|
+
* `sig=`, `code=`, `X-Amz-Signature=` ...): fetching it could use up a
|
|
448
|
+
* one-time link;
|
|
449
|
+
* - HTML only (`text/html`, `application/xhtml+xml`), read up to `maxBytes`
|
|
450
|
+
* and no further; nothing is executed;
|
|
451
|
+
* - og:title, og:description, og:image (https only, resolved against the
|
|
452
|
+
* page, and its host held to the same address rules, else dropped),
|
|
453
|
+
* og:site_name, with twitter:* and <title> / meta description as
|
|
454
|
+
* fallbacks; entities decoded, whitespace collapsed, lengths capped to the
|
|
455
|
+
* columns of `link_previews`.
|
|
456
|
+
*
|
|
457
|
+
* DNS: the host is resolved first (Node's `node:dns`, Deno's `Deno.resolveDns`,
|
|
458
|
+
* or a `resolve` you pass), within the timeout, and refused if any address is
|
|
459
|
+
* private. Where nothing can resolve (some edge runtimes), every host that is
|
|
460
|
+
* not an IP literal is refused (`blocked`, logged once) unless you pass
|
|
461
|
+
* `allowUnresolved: true`, accepting that a public name pointing at a private
|
|
462
|
+
* address gets through.
|
|
463
|
+
*
|
|
464
|
+
* Rebinding: fetch resolves the name again, so a host that changes its answer
|
|
465
|
+
* between the check and the connection is not caught by the check alone. On
|
|
466
|
+
* Node, close that gap by connecting through the checked lookup: pass
|
|
467
|
+
* `dispatcher: new Agent({ connect: { lookup: guardedLookup } })` (`Agent` from
|
|
468
|
+
* `undici`), which refuses a private address at connect time. Elsewhere, run
|
|
469
|
+
* the runner where outbound traffic to private ranges is firewalled.
|
|
470
|
+
*/
|
|
471
|
+
|
|
472
|
+
interface LinkUnfurlerOptions {
|
|
473
|
+
fetch?: typeof fetch | undefined;
|
|
474
|
+
/** For the whole fetch, redirects included (default 5000). */
|
|
475
|
+
timeoutMs?: number | undefined;
|
|
476
|
+
/** Bytes of HTML read at most (default 512 KiB). */
|
|
477
|
+
maxBytes?: number | undefined;
|
|
478
|
+
/** At most this many redirects (default 3). */
|
|
479
|
+
maxRedirects?: number | undefined;
|
|
480
|
+
/**
|
|
481
|
+
* The host's addresses. Default: `node:dns` or `Deno.resolveDns` where
|
|
482
|
+
* present; `null` means "can't resolve here" and only literal and local
|
|
483
|
+
* hosts are refused. Throwing means the name doesn't resolve: `failed`.
|
|
484
|
+
*/
|
|
485
|
+
resolve?: ((hostname: string) => Promise<string[] | null>) | undefined;
|
|
486
|
+
/**
|
|
487
|
+
* Fetch a name even when nothing here can resolve it (default false: such a
|
|
488
|
+
* name is `blocked`). Only for a runner behind an egress firewall.
|
|
489
|
+
*/
|
|
490
|
+
allowUnresolved?: boolean | undefined;
|
|
491
|
+
/**
|
|
492
|
+
* Passed to fetch as `dispatcher` (Node, undici): `new Agent({ connect: {
|
|
493
|
+
* lookup: guardedLookup } })` pins each connection to an address checked at
|
|
494
|
+
* connect time, which closes DNS rebinding.
|
|
495
|
+
*/
|
|
496
|
+
dispatcher?: unknown;
|
|
497
|
+
/** Where a misconfiguration is reported (no resolver). Default `console.warn`. */
|
|
498
|
+
log?: ((line: string) => void) | undefined;
|
|
499
|
+
userAgent?: string | undefined;
|
|
500
|
+
}
|
|
501
|
+
/** True when `ip` (v4 or v6, brackets allowed) is not a public unicast address. Unparseable counts as private. */
|
|
502
|
+
declare function isPrivateAddress(ip: string): boolean;
|
|
503
|
+
/** True when the URL's query or fragment carries what looks like a credential (exported for tests). */
|
|
504
|
+
declare function carriesCredential(url: string): boolean;
|
|
505
|
+
type LookupAddress = {
|
|
506
|
+
address: string;
|
|
507
|
+
family: number;
|
|
508
|
+
};
|
|
509
|
+
type LookupCallback = (error: Error | null, address?: string | LookupAddress[], family?: number) => void;
|
|
510
|
+
/**
|
|
511
|
+
* A `dns.lookup` for Node's socket `connect.lookup` (undici `Agent({ connect: { lookup } })`):
|
|
512
|
+
* resolves the name and refuses it when any address is private, so the connection
|
|
513
|
+
* goes only to an address that was checked. That closes DNS rebinding.
|
|
514
|
+
*/
|
|
515
|
+
declare function guardedLookup(hostname: string, options: {
|
|
516
|
+
all?: boolean;
|
|
517
|
+
family?: number;
|
|
518
|
+
} | number | undefined, callback: LookupCallback): void;
|
|
519
|
+
/** Decodes HTML entities (named, decimal, hex). */
|
|
520
|
+
declare function decodeEntities(text: string): string;
|
|
521
|
+
/**
|
|
522
|
+
* Open Graph data from a page's HTML (exported for tests). `base` resolves a relative og:image.
|
|
523
|
+
* The page is hostile input and this runs synchronously, so every scan is linear in its length.
|
|
524
|
+
*/
|
|
525
|
+
declare function parseOpenGraph(html: string, base: string): Omit<LinkPreview, "url" | "status">;
|
|
526
|
+
declare function createLinkUnfurler(options?: LinkUnfurlerOptions): LinkUnfurler;
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Chat pushes through genesis-notifications: `notificationsDeliver` hands a
|
|
530
|
+
* message's `ChatNotification` to `notify()` (one call, every recipient with
|
|
531
|
+
* their own badge), and `CHAT_NOTIFICATION_TYPES` / `CHAT_NOTIFICATION_CATEGORIES`
|
|
532
|
+
* go into the app's `defineNotifications` registry. Typed structurally, so
|
|
533
|
+
* genesis-chat doesn't depend on genesis-notifications; its tests check the
|
|
534
|
+
* shapes against the real package.
|
|
535
|
+
*
|
|
536
|
+
* ```ts
|
|
537
|
+
* import { notify } from "@marktiderman/genesis-notifications/server";
|
|
538
|
+
* const deliver = notificationsDeliver({ notify, client: supabase, registry: notifications });
|
|
539
|
+
* ```
|
|
540
|
+
*
|
|
541
|
+
* The preferences, quiet hours, channels and devices are genesis-notifications'.
|
|
542
|
+
*/
|
|
543
|
+
|
|
544
|
+
/** The notification type id chat pushes go out as. */
|
|
545
|
+
declare const CHAT_NOTIFICATION_TYPE_ID = "chat.message";
|
|
546
|
+
/** What `notify()` receives for a chat message (a `NotifyInput<"chat.message">`). */
|
|
547
|
+
interface ChatNotifyInput {
|
|
548
|
+
type: string;
|
|
549
|
+
recipients: {
|
|
550
|
+
userId: string;
|
|
551
|
+
badge?: number;
|
|
552
|
+
}[];
|
|
553
|
+
data: {
|
|
554
|
+
url: string;
|
|
555
|
+
conversationId: string;
|
|
556
|
+
messageId: string;
|
|
557
|
+
seq: number | null;
|
|
558
|
+
title: string;
|
|
559
|
+
body: string;
|
|
560
|
+
senderId: string | null;
|
|
561
|
+
senderName?: string;
|
|
562
|
+
senderAvatar?: string;
|
|
563
|
+
groupName?: string;
|
|
564
|
+
};
|
|
565
|
+
idempotencyKey: string;
|
|
566
|
+
threadKey: string;
|
|
567
|
+
collapseKey?: string;
|
|
568
|
+
tenantId?: string;
|
|
569
|
+
}
|
|
570
|
+
interface NotificationsDeliverOptions<Client, Registry> {
|
|
571
|
+
/** genesis-notifications' `notify` from `@marktiderman/genesis-notifications/server`. */
|
|
572
|
+
notify: (client: Client, input: ChatNotifyInput, options?: {
|
|
573
|
+
registry?: Registry;
|
|
574
|
+
}) => Promise<unknown>;
|
|
575
|
+
/** The service-role client `notify` calls `genesis_notify` through. */
|
|
576
|
+
client: Client;
|
|
577
|
+
/** Passed to `notify` as `{ registry }`, so an unknown type fails before the database. */
|
|
578
|
+
registry?: Registry | undefined;
|
|
579
|
+
/** Default `chat.message`. `collapseKey` is passed on only when the runner set it (its `collapse` option). */
|
|
580
|
+
type?: string | undefined;
|
|
581
|
+
/** The practice or workspace the conversation belongs to, when the app has one. */
|
|
582
|
+
tenantFor?: ((notification: ChatNotification) => string | null | undefined | Promise<string | null | undefined>) | undefined;
|
|
583
|
+
}
|
|
584
|
+
/** Maps a chat push onto genesis-notifications' `notify()` (exported for tests and apps that call notify themselves). */
|
|
585
|
+
declare function chatNotifyInput(notification: ChatNotification, options?: {
|
|
586
|
+
type?: string | undefined;
|
|
587
|
+
}): ChatNotifyInput;
|
|
588
|
+
declare function notificationsDeliver<Client, Registry>(options: NotificationsDeliverOptions<Client, Registry>): ChatDeliver;
|
|
589
|
+
/** The `messages` category, for `defineNotifications({ categories })`. */
|
|
590
|
+
declare const CHAT_NOTIFICATION_CATEGORIES: {
|
|
591
|
+
readonly messages: {
|
|
592
|
+
readonly label: "Messages";
|
|
593
|
+
readonly description: "New messages in your conversations.";
|
|
594
|
+
readonly android: {
|
|
595
|
+
readonly importance: "high";
|
|
596
|
+
};
|
|
597
|
+
};
|
|
598
|
+
};
|
|
599
|
+
/**
|
|
600
|
+
* The `chat.message` type, for `defineNotifications({ types })`: in the feed and
|
|
601
|
+
* pushed, no email unless the app adds it. Spread it and override what you need
|
|
602
|
+
* (`actions: "message"` with genesis-notifications' `MESSAGE_ACTIONS` for reply
|
|
603
|
+
* and mark-as-read buttons).
|
|
604
|
+
*/
|
|
605
|
+
declare const CHAT_NOTIFICATION_TYPES: {
|
|
606
|
+
readonly "chat.message": {
|
|
607
|
+
readonly category: "messages";
|
|
608
|
+
readonly channels: {
|
|
609
|
+
readonly in_app: true;
|
|
610
|
+
readonly push: true;
|
|
611
|
+
};
|
|
612
|
+
readonly render: (data: Record<string, unknown>) => {
|
|
613
|
+
push: {
|
|
614
|
+
ids: Record<string, string | number>;
|
|
615
|
+
display: Record<string, string | number>;
|
|
616
|
+
mutableContent: boolean;
|
|
617
|
+
};
|
|
618
|
+
url?: string | undefined;
|
|
619
|
+
body?: string | undefined;
|
|
620
|
+
title: string;
|
|
621
|
+
};
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* AI help for a conversation, through the Claude API: a short recap of what
|
|
627
|
+
* came in since someone last looked, and a reply draft in a coach's voice.
|
|
628
|
+
* Nothing is ever sent: the caller shows the text, labelled as written by AI,
|
|
629
|
+
* and a person decides. Internal notes (visibility `observers`) and messages
|
|
630
|
+
* moderation hid are left out unless the caller says the reader is an
|
|
631
|
+
* observer: take `observer` from `genesis_chat.i_observe(conversation)` asked
|
|
632
|
+
* as the reader, never from the request.
|
|
633
|
+
*
|
|
634
|
+
* Pass `anthropic` (an `@anthropic-ai/sdk` client) or nothing: without one it
|
|
635
|
+
* calls `POST /v1/messages` with fetch and `apiKey`. Messages go to the API as
|
|
636
|
+
* a transcript inside one user turn, marked as data to summarise, not
|
|
637
|
+
* instructions to follow; `<` and `>` in bodies, names and the coach's voice
|
|
638
|
+
* are replaced, so nothing in a message can close or open a tag.
|
|
639
|
+
*
|
|
640
|
+
* `fallbacks` is a Claude API feature: Bedrock, Vertex AI and Foundry clients
|
|
641
|
+
* reject it, so pass `fallbacks: false` with those.
|
|
642
|
+
*/
|
|
643
|
+
declare const ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages";
|
|
644
|
+
/** The Messages API version the fetch path sends. */
|
|
645
|
+
declare const ANTHROPIC_VERSION = "2026-10-21";
|
|
646
|
+
/** The default model: the current Sonnet. */
|
|
647
|
+
declare const CHAT_ASSISTANT_MODEL = "claude-sonnet-5-5";
|
|
648
|
+
/** One message as the assistant reads it: genesis-chat's `ChatMessage` fits. */
|
|
649
|
+
interface AssistantMessage {
|
|
650
|
+
senderId: string | null;
|
|
651
|
+
body: string;
|
|
652
|
+
createdAt: string;
|
|
653
|
+
kind?: "text" | "system" | undefined;
|
|
654
|
+
visibility?: "everyone" | "observers" | undefined;
|
|
655
|
+
deletedAt?: string | null | undefined;
|
|
656
|
+
/** `hidden` by moderation: left out unless the reader is an observer. */
|
|
657
|
+
moderation?: "flagged" | "hidden" | null | undefined;
|
|
658
|
+
}
|
|
659
|
+
interface AssistantResult {
|
|
660
|
+
text: string;
|
|
661
|
+
/** The model that wrote it (a fallback model when the first declined). */
|
|
662
|
+
model: string;
|
|
663
|
+
/** Always true: show it as written by AI. */
|
|
664
|
+
labelled: true;
|
|
665
|
+
}
|
|
666
|
+
type NameResolver = (userId: string) => Promise<string | null | undefined> | string | null | undefined;
|
|
667
|
+
interface RecapInput {
|
|
668
|
+
messages: readonly AssistantMessage[];
|
|
669
|
+
/** When the reader last looked (ISO time). Messages after it are what the recap covers; up to 20 before it are context. */
|
|
670
|
+
since?: string | Date | null | undefined;
|
|
671
|
+
resolveName?: NameResolver | undefined;
|
|
672
|
+
/** The reader, so their own messages read "You". */
|
|
673
|
+
me?: string | undefined;
|
|
674
|
+
/**
|
|
675
|
+
* The reader observes the conversation (staff), so internal notes and hidden
|
|
676
|
+
* messages are included. Default false. Take it from `genesis_chat.i_observe`
|
|
677
|
+
* asked as the reader, never from the request.
|
|
678
|
+
*/
|
|
679
|
+
observer?: boolean | undefined;
|
|
680
|
+
}
|
|
681
|
+
interface DraftReplyInput {
|
|
682
|
+
messages: readonly AssistantMessage[];
|
|
683
|
+
/** How the coach writes ("warm, brief, first names, no emoji"). */
|
|
684
|
+
voice?: string | undefined;
|
|
685
|
+
/** The coach the draft is for: their messages read "You". */
|
|
686
|
+
me?: string | undefined;
|
|
687
|
+
resolveName?: NameResolver | undefined;
|
|
688
|
+
observer?: boolean | undefined;
|
|
689
|
+
}
|
|
690
|
+
/** The part of an `@anthropic-ai/sdk` client this uses. */
|
|
691
|
+
interface AnthropicLike {
|
|
692
|
+
beta: {
|
|
693
|
+
messages: {
|
|
694
|
+
create(body: Record<string, unknown>): Promise<{
|
|
695
|
+
content: {
|
|
696
|
+
type: string;
|
|
697
|
+
text?: string;
|
|
698
|
+
}[];
|
|
699
|
+
model: string;
|
|
700
|
+
stop_reason?: string | null;
|
|
701
|
+
}>;
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
interface ChatAssistantOptions {
|
|
706
|
+
/** An `@anthropic-ai/sdk` client (`new Anthropic()`); else fetch with `apiKey`. */
|
|
707
|
+
anthropic?: AnthropicLike | undefined;
|
|
708
|
+
fetch?: typeof fetch | undefined;
|
|
709
|
+
/** `ANTHROPIC_API_KEY`, for the fetch path. */
|
|
710
|
+
apiKey?: string | undefined;
|
|
711
|
+
/** Default `claude-sonnet-5-5`. */
|
|
712
|
+
model?: string | undefined;
|
|
713
|
+
/** Output cap per answer, thinking included (default 4000). */
|
|
714
|
+
maxTokens?: number | undefined;
|
|
715
|
+
/** Re-run a declined request on Anthropic's recommended fallback model (default true). */
|
|
716
|
+
fallbacks?: boolean | undefined;
|
|
717
|
+
timeoutMs?: number | undefined;
|
|
718
|
+
}
|
|
719
|
+
interface ChatAssistant {
|
|
720
|
+
recap(input: RecapInput): Promise<AssistantResult>;
|
|
721
|
+
draftReply(input: DraftReplyInput): Promise<AssistantResult>;
|
|
722
|
+
}
|
|
723
|
+
/** The transcript lines (exported for tests): notes and hidden messages stripped unless `observer`, deleted messages left out, angle brackets neutralised, oldest first. */
|
|
724
|
+
declare function transcriptLines(messages: readonly AssistantMessage[], options: {
|
|
725
|
+
resolveName?: NameResolver | undefined;
|
|
726
|
+
me?: string | undefined;
|
|
727
|
+
observer?: boolean | undefined;
|
|
728
|
+
}): Promise<{
|
|
729
|
+
at: number;
|
|
730
|
+
line: string;
|
|
731
|
+
}[]>;
|
|
732
|
+
/** The recap's user turn (exported for tests). */
|
|
733
|
+
declare function recapPrompt(input: RecapInput): Promise<string>;
|
|
734
|
+
/** The draft's user turn (exported for tests). */
|
|
735
|
+
declare function draftPrompt(input: DraftReplyInput): Promise<string>;
|
|
736
|
+
declare function createChatAssistant(options: ChatAssistantOptions): ChatAssistant;
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* A fetch handler that gives a composer a direct-upload URL for a video, after
|
|
740
|
+
* the app confirms the person may post in that conversation.
|
|
741
|
+
*
|
|
742
|
+
* POST { conversationId } → { url, providerId, provider: "mux" }
|
|
743
|
+
* OPTIONS → 204 with CORS headers, for an allowed origin
|
|
744
|
+
*
|
|
745
|
+
* The client PUTs the file to `url` and sends its message with an attachment
|
|
746
|
+
* `{ kind: "video", provider: "mux", provider_id: providerId, status: "pending" }`.
|
|
747
|
+
*
|
|
748
|
+
* The caller chooses nothing the webhook trusts. The server sets the upload's
|
|
749
|
+
* passthrough to the conversation (and the uploader, when `authorize` names
|
|
750
|
+
* them); the webhook finds the attachment only by the upload id the client sent
|
|
751
|
+
* with its message, and applies an event only when the passthrough names that
|
|
752
|
+
* attachment's conversation (and its sender). So nobody can aim their upload at
|
|
753
|
+
* another conversation's video.
|
|
754
|
+
*/
|
|
755
|
+
|
|
756
|
+
/** Who an upload is for: written by the server into the upload's passthrough, never by the client. */
|
|
757
|
+
interface ChatUploadBinding {
|
|
758
|
+
conversationId: string;
|
|
759
|
+
/** The uploader, when the app's `authorize` named them. */
|
|
760
|
+
userId?: string | undefined;
|
|
761
|
+
}
|
|
762
|
+
/** `gc1:<conversation id>:<user id or empty>`: the passthrough the upload handler sets. */
|
|
763
|
+
declare function chatUploadPassthrough(binding: ChatUploadBinding): string;
|
|
764
|
+
/** Reads a passthrough the upload handler set, or null for anything else. */
|
|
765
|
+
declare function parseChatUploadPassthrough(value: string | null | undefined): ChatUploadBinding | null;
|
|
766
|
+
interface VideoUploadHandlerOptions {
|
|
767
|
+
/** `muxVideo(...)`. */
|
|
768
|
+
video: Pick<VideoProvider, "name" | "createUpload">;
|
|
769
|
+
/**
|
|
770
|
+
* Must confirm the request's user may post in `conversationId` (verify their
|
|
771
|
+
* session, then ask the database as them, e.g. a select on
|
|
772
|
+
* `genesis_chat.members` under row level security). Resolve false to refuse.
|
|
773
|
+
* Resolve `{ userId }` to also bind the upload to its uploader: the webhook
|
|
774
|
+
* then applies it only to a message they sent.
|
|
775
|
+
*/
|
|
776
|
+
authorize: (request: Request, context: {
|
|
777
|
+
conversationId: string;
|
|
778
|
+
}) => Promise<boolean | {
|
|
779
|
+
userId: string;
|
|
780
|
+
}> | boolean | {
|
|
781
|
+
userId: string;
|
|
782
|
+
};
|
|
783
|
+
/**
|
|
784
|
+
* Required. The web origin(s) allowed to upload: one origin, or a list that
|
|
785
|
+
* the request's `origin` header must be in. Native apps send no origin; they
|
|
786
|
+
* get the first. `"*"` allows any origin (only for an app with no web client).
|
|
787
|
+
*/
|
|
788
|
+
corsOrigin: string | readonly string[];
|
|
789
|
+
/**
|
|
790
|
+
* A per-user cap on minting uploads (each one can cost money). Resolve false
|
|
791
|
+
* to answer 429. Called after `authorize`, with the user it named.
|
|
792
|
+
*/
|
|
793
|
+
rateLimit?: ((request: Request, context: {
|
|
794
|
+
conversationId: string;
|
|
795
|
+
userId: string | undefined;
|
|
796
|
+
}) => Promise<boolean> | boolean) | undefined;
|
|
797
|
+
}
|
|
798
|
+
declare function createVideoUploadHandler(options: VideoUploadHandlerOptions): (request: Request) => Promise<Response>;
|
|
799
|
+
|
|
800
|
+
export { ANTHROPIC_MESSAGES_URL, ANTHROPIC_VERSION, type AnthropicLike, type AssistantMessage, type AssistantResult, CHAT_ASSISTANT_MODEL, CHAT_MAX_ATTEMPTS, CHAT_NOTIFICATION_CATEGORIES, CHAT_NOTIFICATION_TYPES, CHAT_NOTIFICATION_TYPE_ID, type ChatAssistant, type ChatAssistantOptions, type ChatDeliver, type ChatEffect, type ChatEffectKind, type ChatNotification, type ChatNotifyInput, type ChatRecipient, type ChatRunReport, type ChatRunner, type ChatRunnerHandlerOptions, type ChatRunnerOptions, type ChatUploadBinding, type DeepgramOptions, type DraftReplyInput, EXPO_BATCH, EXPO_SEND_URL, type ExpoDeliverOptions, type ExpoDeliveryTicket, type ExpoPushMessage, type ExpoPushTicket, type LinkPreview, type LinkUnfurler, type LinkUnfurlerOptions, type ModerateInput, type ModerationResult, type ModerationThresholds, type ModerationVerdict, type Moderator, type MuxSigningKey, type MuxVideoOptions, type NotificationsDeliverOptions, OPENAI_MODERATIONS_URL, OPENAI_MODERATION_CATEGORIES, type OpenAiModeratorOptions, type OpenAiTranscriberOptions, type RecapInput, type TranscribeInput, type TranscribeResult, type Transcriber, type VideoProvider, type VideoUploadHandlerOptions, carriesCredential, chatNotifyInput, chatRunnerHandler, chatUploadPassthrough, createChatAssistant, createChatRunner, createExpoDeliver, createLinkUnfurler, createVideoUploadHandler, deadTokens, decodeEntities, deepgramTranscriber, draftPrompt, guardedLookup, isPrivateAddress, moderationVerdict, muxPlaybackToken, muxPlaybackUrl, muxVideo, notificationsDeliver, openAiModerator, openAiTranscriber, parseChatUploadPassthrough, parseOpenGraph, recapPrompt, transcriptLines, verifyMuxSignature };
|