@spectrum-ts/core 5.0.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/dist/index.js ADDED
@@ -0,0 +1,4529 @@
1
+ import { A as resolveContents, B as fromVCard, C as poll, F as drainStreamText, G as attachment, H as buildPhotoAction, I as asCustom, J as fetchLinkMetadata, L as custom, M as text, O as group, P as StreamConsumedError, R as asContact, S as option, T as markdown, U as photoActionSchema, V as toVCard, W as asAttachment, a as contentAttrs, c as markdownToPlainText, f as voice, g as read, i as stream, j as asText, m as richlink, n as createAsyncQueue, o as errorAttrs, p as asRichlink, q as fetchImage, r as mergeStreams, s as senderAttrs, t as broadcast, u as classifyIdentifier, v as reaction, w as asMarkdown, z as contact } from "./stream-BLWs7NJ5.js";
2
+ import z from "zod";
3
+ import { createHmac, timingSafeEqual } from "node:crypto";
4
+ import { createLogger, setLogLevel, setupOtel, withSpan } from "@photon-ai/otel";
5
+ import { RawInboundEvent } from "@photon-ai/proto/photon/fusor/v1/inbound";
6
+ //#region src/content/app.ts
7
+ /**
8
+ * Visible layout of an app card. Mirrors Apple's `MSMessageTemplateLayout`
9
+ * (the iMessage mini-app surface) — the iMessage provider renders it natively;
10
+ * other platforms ignore it and fall back to the bare URL. At least one of
11
+ * `caption`, `subcaption`, `trailingCaption`, `trailingSubcaption`, or `image`
12
+ * must be set so the bubble is not empty — `summary` is the fallback text shown
13
+ * on surfaces that cannot render the card and is not a visible slot on its own.
14
+ * `image` and `imageTitle` must be set together; `imageSubtitle` requires
15
+ * `image`.
16
+ */
17
+ const appLayoutSchema = z.object({
18
+ caption: z.string().nonempty().optional(),
19
+ subcaption: z.string().nonempty().optional(),
20
+ trailingCaption: z.string().nonempty().optional(),
21
+ trailingSubcaption: z.string().nonempty().optional(),
22
+ image: z.instanceof(Uint8Array).optional(),
23
+ imageTitle: z.string().nonempty().optional(),
24
+ imageSubtitle: z.string().nonempty().optional(),
25
+ summary: z.string().nonempty().optional()
26
+ }).refine((layout) => layout.caption !== void 0 || layout.subcaption !== void 0 || layout.trailingCaption !== void 0 || layout.trailingSubcaption !== void 0 || layout.image !== void 0, { message: "layout must set at least one of caption, subcaption, trailingCaption, trailingSubcaption, image" }).refine((layout) => layout.image === void 0 === (layout.imageTitle === void 0), {
27
+ message: "layout.image and layout.imageTitle must be set together",
28
+ path: ["imageTitle"]
29
+ }).refine((layout) => layout.imageSubtitle === void 0 || layout.image !== void 0, {
30
+ message: "layout.imageSubtitle requires layout.image",
31
+ path: ["imageSubtitle"]
32
+ });
33
+ const urlAccessor = z.function({
34
+ input: [],
35
+ output: z.promise(z.url())
36
+ });
37
+ const layoutAccessor = z.function({
38
+ input: [],
39
+ output: z.promise(appLayoutSchema)
40
+ });
41
+ const appSchema = z.object({
42
+ type: z.literal("app"),
43
+ url: urlAccessor,
44
+ layout: layoutAccessor
45
+ });
46
+ const memoize = (factory) => {
47
+ let cached;
48
+ return () => {
49
+ cached ??= factory();
50
+ return cached;
51
+ };
52
+ };
53
+ const resolveUrl = (url) => memoize(async () => typeof url === "function" ? await url() : await url);
54
+ const WWW_PREFIX = /^www\./;
55
+ const siteHost = (url) => {
56
+ try {
57
+ return new URL(url).host.replace(WWW_PREFIX, "");
58
+ } catch {
59
+ return url;
60
+ }
61
+ };
62
+ const JPEG_PROXY = "https://wsrv.nl/";
63
+ const MAX_IMAGE_WIDTH = 1200;
64
+ const toJpegUrl = (imageUrl) => `${JPEG_PROXY}?url=${encodeURIComponent(imageUrl)}&output=jpg&w=${MAX_IMAGE_WIDTH}`;
65
+ const isJpeg = (bytes) => bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
66
+ const buildLayout = (metadata, url, image) => {
67
+ const title = metadata.title ?? metadata.siteName ?? siteHost(url);
68
+ if (image) return appLayoutSchema.parse({
69
+ caption: title,
70
+ subcaption: metadata.summary,
71
+ image,
72
+ imageTitle: metadata.siteName ?? title,
73
+ summary: title
74
+ });
75
+ return appLayoutSchema.parse({
76
+ caption: title,
77
+ subcaption: metadata.summary,
78
+ summary: title
79
+ });
80
+ };
81
+ /**
82
+ * Construct an `app` content value.
83
+ *
84
+ * `url` is stored as a lazy accessor; `layout` is derived from the URL's Open
85
+ * Graph / link metadata (title → caption, og:site_name → image overlay,
86
+ * description → subcaption, og:image → JPEG-transcoded image) using the same
87
+ * machinery as `richlink`. A single metadata fetch is shared and memoized
88
+ * across `url()` / `layout()`; fetch / parse failures resolve to a host-only
89
+ * caption (no throw, no retry).
90
+ */
91
+ const asApp = (url) => {
92
+ const getUrl = resolveUrl(url);
93
+ const getMetadata = memoize(async () => fetchLinkMetadata(await getUrl()));
94
+ const getLayout = memoize(async () => {
95
+ const resolvedUrl = await getUrl();
96
+ const metadata = await getMetadata();
97
+ let image;
98
+ if (metadata.image) try {
99
+ const bytes = (await fetchImage(toJpegUrl(metadata.image.url))).data;
100
+ image = isJpeg(bytes) ? bytes : void 0;
101
+ } catch {
102
+ image = void 0;
103
+ }
104
+ return buildLayout(metadata, resolvedUrl, image);
105
+ });
106
+ return appSchema.parse({
107
+ type: "app",
108
+ url: getUrl,
109
+ layout: getLayout
110
+ });
111
+ };
112
+ function app(url) {
113
+ return { build: async () => asApp(url) };
114
+ }
115
+ //#endregion
116
+ //#region src/content/avatar.ts
117
+ /**
118
+ * Set or clear the chat avatar (group icon). Universal content — providers
119
+ * dispatch by `content.type === "avatar"` in their `send` action and decide
120
+ * their own support story (e.g. iMessage only supports it for remote group
121
+ * chats).
122
+ *
123
+ * `space.send(avatar(...))` is the canonical form; `space.avatar(...)` is
124
+ * universal sugar that delegates here. Per-platform constraints (e.g.
125
+ * group-only, remote-only) surface as `UnsupportedError` from the provider's
126
+ * `send` action so the canonical and sugar forms share one error path.
127
+ */
128
+ const avatarSchema = z.object({
129
+ type: z.literal("avatar"),
130
+ action: photoActionSchema
131
+ });
132
+ function avatar(input, options) {
133
+ const action = buildPhotoAction(input, options, "avatar");
134
+ return { build: async () => avatarSchema.parse({
135
+ type: "avatar",
136
+ action
137
+ }) };
138
+ }
139
+ //#endregion
140
+ //#region src/content/edit.ts
141
+ const isMessage$2 = (v) => typeof v === "object" && v !== null && "id" in v && "content" in v;
142
+ const isContent$1 = (v) => typeof v === "object" && v !== null && "type" in v && typeof v.type === "string";
143
+ /**
144
+ * An `edit` rewrites the content of a previously-sent outbound message.
145
+ *
146
+ * `space.send(edit(newContent, message))` is the canonical outbound API;
147
+ * `message.edit(newContent)` and `space.edit(message, newContent)` are
148
+ * sugar that delegate here. Edits are fire-and-forget — providers handle
149
+ * them inside their `send` action and the resolved value is `undefined`
150
+ * (no new message id is produced; the existing message mutates in place).
151
+ *
152
+ * Edit cannot wrap `edit`, `reply`, `reaction`, `group`, `typing`, `rename`,
153
+ * `avatar`, `unsend`, or `read` content.
154
+ */
155
+ const editSchema = z.object({
156
+ type: z.literal("edit"),
157
+ content: z.custom(isContent$1, { message: "edit content must be a Content value" }),
158
+ target: z.custom(isMessage$2, { message: "edit target must be a Message" })
159
+ });
160
+ const asEdit = (input) => editSchema.parse({
161
+ type: "edit",
162
+ ...input
163
+ });
164
+ /**
165
+ * Construct an `edit` content value rewriting `target`'s content.
166
+ *
167
+ * Only outbound messages (those sent by the agent) can be edited; calling
168
+ * this with an inbound target throws at build time so the misuse surfaces
169
+ * before the send pipeline runs.
170
+ */
171
+ function edit(content, target) {
172
+ return { build: async () => {
173
+ if (!target) throw new Error("edit() target is undefined — the targeted message was never sent (space.send resolves undefined when a platform skips unsupported content)");
174
+ if (target.direction !== "outbound") throw new Error(`edit() target must be an outbound message (got direction "${target.direction}", message id "${target.id}")`);
175
+ const [resolved] = await resolveContents([content]);
176
+ if (!resolved) throw new Error("edit() requires content");
177
+ if (resolved.type === "edit" || resolved.type === "reply" || resolved.type === "reaction" || resolved.type === "group" || resolved.type === "typing" || resolved.type === "rename" || resolved.type === "avatar" || resolved.type === "unsend" || resolved.type === "read") throw new Error(`edit() cannot wrap "${resolved.type}" content`);
178
+ return asEdit({
179
+ content: resolved,
180
+ target
181
+ });
182
+ } };
183
+ }
184
+ //#endregion
185
+ //#region src/content/rename.ts
186
+ /**
187
+ * Rename the current chat. Universal content — providers dispatch by
188
+ * `content.type === "rename"` in their `send` action and decide their own
189
+ * support story (e.g. iMessage only supports it for remote group chats).
190
+ *
191
+ * `space.send(rename("New Name"))` is the canonical form; `space.rename(...)`
192
+ * is universal sugar that delegates here.
193
+ *
194
+ * Throws at build time if `displayName` is empty. Per-platform constraints
195
+ * (e.g. group-only, remote-only) surface as `UnsupportedError` from the
196
+ * provider's `send` action so the canonical and sugar forms share one
197
+ * error path.
198
+ */
199
+ const renameSchema = z.object({
200
+ type: z.literal("rename"),
201
+ displayName: z.string().min(1, "rename() displayName must be non-empty")
202
+ });
203
+ function rename(displayName) {
204
+ return { build: async () => renameSchema.parse({
205
+ type: "rename",
206
+ displayName
207
+ }) };
208
+ }
209
+ //#endregion
210
+ //#region src/content/reply.ts
211
+ const isMessage$1 = (v) => typeof v === "object" && v !== null && "id" in v && "content" in v;
212
+ const isContent = (v) => typeof v === "object" && v !== null && "type" in v && typeof v.type === "string";
213
+ /**
214
+ * A `reply` wraps inner content with the message it replies to.
215
+ *
216
+ * `space.send(reply(content, message))` is the canonical outbound API;
217
+ * `message.reply(content)` is sugar that delegates here. Providers see
218
+ * `reply` like any other content type and route to a threaded send.
219
+ *
220
+ * Reply cannot wrap `reply`, `edit`, `reaction`, `group`, `typing`,
221
+ * `rename`, `avatar`, `unsend`, or `read` content.
222
+ */
223
+ const replySchema = z.object({
224
+ type: z.literal("reply"),
225
+ content: z.custom(isContent, { message: "reply content must be a Content value" }),
226
+ target: z.custom(isMessage$1, { message: "reply target must be a Message" })
227
+ });
228
+ const asReply = (input) => replySchema.parse({
229
+ type: "reply",
230
+ ...input
231
+ });
232
+ function reply(content, target) {
233
+ return { build: async () => {
234
+ if (!target) throw new Error("reply() target is undefined — the targeted message was never sent (space.send resolves undefined when a platform skips unsupported content)");
235
+ const [resolved] = await resolveContents([content]);
236
+ if (!resolved) throw new Error("reply() requires content");
237
+ if (resolved.type === "reply" || resolved.type === "edit" || resolved.type === "reaction" || resolved.type === "group" || resolved.type === "typing" || resolved.type === "rename" || resolved.type === "avatar" || resolved.type === "unsend" || resolved.type === "read") throw new Error(`reply() cannot wrap "${resolved.type}" content`);
238
+ return asReply({
239
+ content: resolved,
240
+ target
241
+ });
242
+ } };
243
+ }
244
+ //#endregion
245
+ //#region src/content/typing.ts
246
+ /**
247
+ * A `typing` content value carries a typing-indicator signal — either
248
+ * `"start"` or `"stop"`. Like `edit`, it's fire-and-forget: providers
249
+ * dispatch on `content.type === "typing"` inside their `send()` action and
250
+ * `space.send(typing(...))` resolves to `undefined`.
251
+ *
252
+ * `space.startTyping()` / `space.stopTyping()` / `space.responding()` are
253
+ * sugar over `space.send(typing("start" | "stop"))`. Platforms that have no
254
+ * typing-indicator API (e.g. WhatsApp Business) silently no-op so the
255
+ * signal is best-effort everywhere.
256
+ */
257
+ const typingSchema = z.object({
258
+ type: z.literal("typing"),
259
+ state: z.enum(["start", "stop"])
260
+ });
261
+ /**
262
+ * Construct a `typing` content value. Defaults to `"start"`.
263
+ *
264
+ * `space.send(typing())` is equivalent to `space.startTyping()`;
265
+ * `space.send(typing("stop"))` is equivalent to `space.stopTyping()`.
266
+ */
267
+ function typing(state = "start") {
268
+ return { build: async () => typingSchema.parse({
269
+ type: "typing",
270
+ state
271
+ }) };
272
+ }
273
+ //#endregion
274
+ //#region src/content/unsend.ts
275
+ const isMessage = (v) => typeof v === "object" && v !== null && "id" in v && "content" in v;
276
+ /**
277
+ * An `unsend` retracts a previously-sent outbound message.
278
+ *
279
+ * `space.send(unsend(message))` is the canonical outbound API;
280
+ * `message.unsend()` and `space.unsend(message)` are sugar that delegate
281
+ * here. Unsends are fire-and-forget — providers handle them inside their
282
+ * `send` action and the resolved value is `undefined` (no new message id is
283
+ * produced; the existing message is retracted in place).
284
+ *
285
+ * Platform constraints surface from the provider at send time — e.g.
286
+ * iMessage enforces Apple's ~2-minute unsend window for regular messages
287
+ * (reaction removal is not time-limited), and a late or repeated unsend
288
+ * rejects with the provider's error. `space.getMessage(id)` results are
289
+ * wrapped as inbound, so a message cannot be unsent from a refetched id
290
+ * after a restart — keep the Message returned by `send` (same limitation
291
+ * as `edit`).
292
+ */
293
+ const unsendSchema = z.object({
294
+ type: z.literal("unsend"),
295
+ target: z.custom(isMessage, { message: "unsend target must be a Message" })
296
+ });
297
+ const asUnsend = (input) => unsendSchema.parse({
298
+ type: "unsend",
299
+ ...input
300
+ });
301
+ /**
302
+ * Construct an `unsend` content value retracting `target`.
303
+ *
304
+ * Only outbound messages (those sent by the agent) can be unsent; calling
305
+ * this with an inbound target throws at build time so the misuse surfaces
306
+ * before the send pipeline runs.
307
+ *
308
+ * Accepts `Message | undefined` so `space.send` results chain without
309
+ * narrowing (`send` resolves `undefined` when a platform skips unsupported
310
+ * content); an undefined target throws at build time.
311
+ */
312
+ function unsend(target) {
313
+ return { build: async () => {
314
+ if (!target) throw new Error("unsend() target is undefined — the targeted message was never sent (space.send resolves undefined when a platform skips unsupported content)");
315
+ if (target.direction !== "outbound") throw new Error(`unsend() target must be an outbound message (got direction "${target.direction}", message id "${target.id}")`);
316
+ return asUnsend({ target });
317
+ } };
318
+ }
319
+ //#endregion
320
+ //#region src/emoji/generated.ts
321
+ const GeneratedEmoji = {
322
+ _1stPlaceMedal: "🥇",
323
+ _2ndPlaceMedal: "🥈",
324
+ _3rdPlaceMedal: "🥉",
325
+ abacus: "🧮",
326
+ abButton: "🆎",
327
+ aButton: "🅰️",
328
+ accordion: "🪗",
329
+ adhesiveBandage: "🩹",
330
+ admissionTickets: "🎟️",
331
+ aerialTramway: "🚡",
332
+ airplane: "✈️",
333
+ airplaneArrival: "🛬",
334
+ airplaneDeparture: "🛫",
335
+ alarmClock: "⏰",
336
+ alembic: "⚗️",
337
+ alien: "👽",
338
+ alienMonster: "👾",
339
+ ambulance: "🚑",
340
+ americanFootball: "🏈",
341
+ amphora: "🏺",
342
+ anatomicalHeart: "🫀",
343
+ anchor: "⚓",
344
+ angerSymbol: "💢",
345
+ angryFace: "😠",
346
+ angryFaceWithHorns: "👿",
347
+ anguishedFace: "😧",
348
+ ant: "🐜",
349
+ antennaBars: "📶",
350
+ anxiousFaceWithSweat: "😰",
351
+ aquarius: "♒",
352
+ aries: "♈",
353
+ articulatedLorry: "🚛",
354
+ artist: "🧑‍🎨",
355
+ artistPalette: "🎨",
356
+ astonishedFace: "😲",
357
+ astronaut: "🧑‍🚀",
358
+ atmSign: "🏧",
359
+ atomSymbol: "⚛️",
360
+ automobile: "🚗",
361
+ autoRickshaw: "🛺",
362
+ avocado: "🥑",
363
+ axe: "🪓",
364
+ baby: "👶",
365
+ babyAngel: "👼",
366
+ babyBottle: "🍼",
367
+ babyChick: "🐤",
368
+ babySymbol: "🚼",
369
+ backArrow: "🔙",
370
+ backhandIndexPointingDown: "👇",
371
+ backhandIndexPointingLeft: "👈",
372
+ backhandIndexPointingRight: "👉",
373
+ backhandIndexPointingUp: "👆",
374
+ backpack: "🎒",
375
+ bacon: "🥓",
376
+ badger: "🦡",
377
+ badminton: "🏸",
378
+ bagel: "🥯",
379
+ baggageClaim: "🛄",
380
+ baguetteBread: "🥖",
381
+ balanceScale: "⚖️",
382
+ balletDancer: "🧑‍🩰",
383
+ balletShoes: "🩰",
384
+ balloon: "🎈",
385
+ ballotBoxWithBallot: "🗳️",
386
+ banana: "🍌",
387
+ banjo: "🪕",
388
+ bank: "🏦",
389
+ barberPole: "💈",
390
+ barChart: "📊",
391
+ baseball: "⚾",
392
+ basket: "🧺",
393
+ basketball: "🏀",
394
+ bat: "🦇",
395
+ bathtub: "🛁",
396
+ battery: "🔋",
397
+ bButton: "🅱️",
398
+ beachWithUmbrella: "🏖️",
399
+ beamingFaceWithSmilingEyes: "😁",
400
+ beans: "🫘",
401
+ bear: "🐻",
402
+ beatingHeart: "💓",
403
+ beaver: "🦫",
404
+ bed: "🛏️",
405
+ beerMug: "🍺",
406
+ beetle: "🪲",
407
+ bell: "🔔",
408
+ bellhopBell: "🛎️",
409
+ bellPepper: "🫑",
410
+ bellWithSlash: "🔕",
411
+ bentoBox: "🍱",
412
+ beverageBox: "🧃",
413
+ bicycle: "🚲",
414
+ bikini: "👙",
415
+ billedCap: "🧢",
416
+ biohazard: "☣️",
417
+ bird: "🐦",
418
+ birthdayCake: "🎂",
419
+ bison: "🦬",
420
+ bitingLip: "🫦",
421
+ blackBird: "🐦‍⬛",
422
+ blackCat: "🐈‍⬛",
423
+ blackCircle: "⚫",
424
+ blackFlag: "🏴",
425
+ blackHeart: "🖤",
426
+ blackLargeSquare: "⬛",
427
+ blackMediumSmallSquare: "◾",
428
+ blackMediumSquare: "◼️",
429
+ blackNib: "✒️",
430
+ blackSmallSquare: "▪️",
431
+ blackSquareButton: "🔲",
432
+ blossom: "🌼",
433
+ blowfish: "🐡",
434
+ blueberries: "🫐",
435
+ blueBook: "📘",
436
+ blueCircle: "🔵",
437
+ blueHeart: "💙",
438
+ blueSquare: "🟦",
439
+ boar: "🐗",
440
+ bomb: "💣",
441
+ bone: "🦴",
442
+ bookmark: "🔖",
443
+ bookmarkTabs: "📑",
444
+ books: "📚",
445
+ boomerang: "🪃",
446
+ bottleWithPoppingCork: "🍾",
447
+ bouquet: "💐",
448
+ bowAndArrow: "🏹",
449
+ bowling: "🎳",
450
+ bowlWithSpoon: "🥣",
451
+ boxingGlove: "🥊",
452
+ boy: "👦",
453
+ brain: "🧠",
454
+ bread: "🍞",
455
+ breastFeeding: "🤱",
456
+ brick: "🧱",
457
+ bridgeAtNight: "🌉",
458
+ briefcase: "💼",
459
+ briefs: "🩲",
460
+ brightButton: "🔆",
461
+ broccoli: "🥦",
462
+ brokenChain: "⛓️‍💥",
463
+ brokenHeart: "💔",
464
+ broom: "🧹",
465
+ brownCircle: "🟤",
466
+ brownHeart: "🤎",
467
+ brownMushroom: "🍄‍🟫",
468
+ brownSquare: "🟫",
469
+ bubbles: "🫧",
470
+ bubbleTea: "🧋",
471
+ bucket: "🪣",
472
+ bug: "🐛",
473
+ buildingConstruction: "🏗️",
474
+ bulletTrain: "🚅",
475
+ bullseye: "🎯",
476
+ burrito: "🌯",
477
+ bus: "🚌",
478
+ busStop: "🚏",
479
+ bustInSilhouette: "👤",
480
+ bustsInSilhouette: "👥",
481
+ butter: "🧈",
482
+ butterfly: "🦋",
483
+ cactus: "🌵",
484
+ calendar: "📅",
485
+ callMeHand: "🤙",
486
+ camel: "🐪",
487
+ camera: "📷",
488
+ cameraWithFlash: "📸",
489
+ camping: "🏕️",
490
+ cancer: "♋",
491
+ candle: "🕯️",
492
+ candy: "🍬",
493
+ cannedFood: "🥫",
494
+ canoe: "🛶",
495
+ capricorn: "♑",
496
+ cardFileBox: "🗃️",
497
+ cardIndex: "📇",
498
+ cardIndexDividers: "🗂️",
499
+ carouselHorse: "🎠",
500
+ carpentrySaw: "🪚",
501
+ carpStreamer: "🎏",
502
+ carrot: "🥕",
503
+ castle: "🏰",
504
+ cat: "🐈",
505
+ catFace: "🐱",
506
+ catWithTearsOfJoy: "😹",
507
+ catWithWrySmile: "😼",
508
+ chains: "⛓️",
509
+ chair: "🪑",
510
+ chartDecreasing: "📉",
511
+ chartIncreasing: "📈",
512
+ chartIncreasingWithYen: "💹",
513
+ checkBoxWithCheck: "☑️",
514
+ checkMark: "✔️",
515
+ checkMarkButton: "✅",
516
+ cheeseWedge: "🧀",
517
+ chequeredFlag: "🏁",
518
+ cherries: "🍒",
519
+ cherryBlossom: "🌸",
520
+ chessPawn: "♟️",
521
+ chestnut: "🌰",
522
+ chicken: "🐔",
523
+ child: "🧒",
524
+ childrenCrossing: "🚸",
525
+ chipmunk: "🐿️",
526
+ chocolateBar: "🍫",
527
+ chopsticks: "🥢",
528
+ christmasTree: "🎄",
529
+ church: "⛪",
530
+ cigarette: "🚬",
531
+ cinema: "🎦",
532
+ circledM: "Ⓜ️",
533
+ circusTent: "🎪",
534
+ cityscape: "🏙️",
535
+ cityscapeAtDusk: "🌆",
536
+ clamp: "🗜️",
537
+ clapperBoard: "🎬",
538
+ clappingHands: "👏",
539
+ classicalBuilding: "🏛️",
540
+ clButton: "🆑",
541
+ clinkingBeerMugs: "🍻",
542
+ clinkingGlasses: "🥂",
543
+ clipboard: "📋",
544
+ clockwiseVerticalArrows: "🔃",
545
+ closedBook: "📕",
546
+ closedMailboxWithLoweredFlag: "📪",
547
+ closedMailboxWithRaisedFlag: "📫",
548
+ closedUmbrella: "🌂",
549
+ cloud: "☁️",
550
+ cloudWithLightning: "🌩️",
551
+ cloudWithLightningAndRain: "⛈️",
552
+ cloudWithRain: "🌧️",
553
+ cloudWithSnow: "🌨️",
554
+ clownFace: "🤡",
555
+ clubSuit: "♣️",
556
+ clutchBag: "👝",
557
+ coat: "🧥",
558
+ cockroach: "🪳",
559
+ cocktailGlass: "🍸",
560
+ coconut: "🥥",
561
+ coffin: "⚰️",
562
+ coin: "🪙",
563
+ coldFace: "🥶",
564
+ collision: "💥",
565
+ comet: "☄️",
566
+ compass: "🧭",
567
+ computerDisk: "💽",
568
+ computerMouse: "🖱️",
569
+ confettiBall: "🎊",
570
+ confoundedFace: "😖",
571
+ confusedFace: "😕",
572
+ construction: "🚧",
573
+ constructionWorker: "👷",
574
+ controlKnobs: "🎛️",
575
+ convenienceStore: "🏪",
576
+ cook: "🧑‍🍳",
577
+ cookedRice: "🍚",
578
+ cookie: "🍪",
579
+ cooking: "🍳",
580
+ coolButton: "🆒",
581
+ copyright: "©️",
582
+ coral: "🪸",
583
+ couchAndLamp: "🛋️",
584
+ counterclockwiseArrowsButton: "🔄",
585
+ coupleWithHeart: "💑",
586
+ coupleWithHeartManMan: "👨‍❤️‍👨",
587
+ coupleWithHeartWomanMan: "👩‍❤️‍👨",
588
+ coupleWithHeartWomanWoman: "👩‍❤️‍👩",
589
+ cow: "🐄",
590
+ cowboyHatFace: "🤠",
591
+ cowFace: "🐮",
592
+ crab: "🦀",
593
+ crayon: "🖍️",
594
+ creditCard: "💳",
595
+ crescentMoon: "🌙",
596
+ cricket: "🦗",
597
+ cricketGame: "🏏",
598
+ crocodile: "🐊",
599
+ croissant: "🥐",
600
+ crossedFingers: "🤞",
601
+ crossedFlags: "🎌",
602
+ crossedSwords: "⚔️",
603
+ crossMark: "❌",
604
+ crossMarkButton: "❎",
605
+ crown: "👑",
606
+ crutch: "🩼",
607
+ cryingCat: "😿",
608
+ cryingFace: "😢",
609
+ crystalBall: "🔮",
610
+ cucumber: "🥒",
611
+ cupcake: "🧁",
612
+ cupWithStraw: "🥤",
613
+ curlingStone: "🥌",
614
+ curlyLoop: "➰",
615
+ currencyExchange: "💱",
616
+ curryRice: "🍛",
617
+ custard: "🍮",
618
+ customs: "🛃",
619
+ cutOfMeat: "🥩",
620
+ cyclone: "🌀",
621
+ dagger: "🗡️",
622
+ dango: "🍡",
623
+ dashingAway: "💨",
624
+ deafMan: "🧏‍♂️",
625
+ deafPerson: "🧏",
626
+ deafWoman: "🧏‍♀️",
627
+ deciduousTree: "🌳",
628
+ deer: "🦌",
629
+ deliveryTruck: "🚚",
630
+ departmentStore: "🏬",
631
+ derelictHouse: "🏚️",
632
+ desert: "🏜️",
633
+ desertIsland: "🏝️",
634
+ desktopComputer: "🖥️",
635
+ detective: "🕵️",
636
+ diamondSuit: "♦️",
637
+ diamondWithADot: "💠",
638
+ dimButton: "🔅",
639
+ disappointedFace: "😞",
640
+ disguisedFace: "🥸",
641
+ distortedFace: "🫪",
642
+ divide: "➗",
643
+ divingMask: "🤿",
644
+ diyaLamp: "🪔",
645
+ dizzy: "💫",
646
+ dna: "🧬",
647
+ dodo: "🦤",
648
+ dog: "🐕",
649
+ dogFace: "🐶",
650
+ dollarBanknote: "💵",
651
+ dolphin: "🐬",
652
+ donkey: "🫏",
653
+ door: "🚪",
654
+ dottedLineFace: "🫥",
655
+ dottedSixPointedStar: "🔯",
656
+ doubleCurlyLoop: "➿",
657
+ doubleExclamationMark: "‼️",
658
+ doughnut: "🍩",
659
+ dove: "🕊️",
660
+ downArrow: "⬇️",
661
+ downcastFaceWithSweat: "😓",
662
+ downLeftArrow: "↙️",
663
+ downRightArrow: "↘️",
664
+ downwardsButton: "🔽",
665
+ dragon: "🐉",
666
+ dragonFace: "🐲",
667
+ dress: "👗",
668
+ droolingFace: "🤤",
669
+ droplet: "💧",
670
+ dropOfBlood: "🩸",
671
+ drum: "🥁",
672
+ duck: "🦆",
673
+ dumpling: "🥟",
674
+ dvd: "📀",
675
+ eagle: "🦅",
676
+ ear: "👂",
677
+ earOfCorn: "🌽",
678
+ earWithHearingAid: "🦻",
679
+ egg: "🥚",
680
+ eggplant: "🍆",
681
+ eightOClock: "🕗",
682
+ eightPointedStar: "✴️",
683
+ eightSpokedAsterisk: "✳️",
684
+ eightThirty: "🕣",
685
+ ejectButton: "⏏️",
686
+ electricPlug: "🔌",
687
+ elephant: "🐘",
688
+ elevator: "🛗",
689
+ elevenOClock: "🕚",
690
+ elevenThirty: "🕦",
691
+ elf: "🧝",
692
+ eMail: "📧",
693
+ emptyNest: "🪹",
694
+ endArrow: "🔚",
695
+ enragedFace: "😡",
696
+ envelope: "✉️",
697
+ envelopeWithArrow: "📩",
698
+ euroBanknote: "💶",
699
+ evergreenTree: "🌲",
700
+ ewe: "🐑",
701
+ exclamationQuestionMark: "⁉️",
702
+ explodingHead: "🤯",
703
+ expressionlessFace: "😑",
704
+ eye: "👁️",
705
+ eyeInSpeechBubble: "👁️‍🗨️",
706
+ eyes: "👀",
707
+ faceBlowingAKiss: "😘",
708
+ faceExhaling: "😮‍💨",
709
+ faceHoldingBackTears: "🥹",
710
+ faceInClouds: "😶‍🌫️",
711
+ faceSavoringFood: "😋",
712
+ faceScreamingInFear: "😱",
713
+ faceVomiting: "🤮",
714
+ faceWithBagsUnderEyes: "🫩",
715
+ faceWithCrossedOutEyes: "😵",
716
+ faceWithDiagonalMouth: "🫤",
717
+ faceWithHandOverMouth: "🤭",
718
+ faceWithHeadBandage: "🤕",
719
+ faceWithMedicalMask: "😷",
720
+ faceWithMonocle: "🧐",
721
+ faceWithOpenEyesAndHandOverMouth: "🫢",
722
+ faceWithOpenMouth: "😮",
723
+ faceWithoutMouth: "😶",
724
+ faceWithPeekingEye: "🫣",
725
+ faceWithRaisedEyebrow: "🤨",
726
+ faceWithRollingEyes: "🙄",
727
+ faceWithSpiralEyes: "😵‍💫",
728
+ faceWithSteamFromNose: "😤",
729
+ faceWithSymbolsOnMouth: "🤬",
730
+ faceWithTearsOfJoy: "😂",
731
+ faceWithThermometer: "🤒",
732
+ faceWithTongue: "😛",
733
+ factory: "🏭",
734
+ factoryWorker: "🧑‍🏭",
735
+ fairy: "🧚",
736
+ falafel: "🧆",
737
+ fallenLeaf: "🍂",
738
+ family: "👪",
739
+ familyAdultAdultChild: "🧑‍🧑‍🧒",
740
+ familyAdultAdultChildChild: "🧑‍🧑‍🧒‍🧒",
741
+ familyAdultChild: "🧑‍🧒",
742
+ familyAdultChildChild: "🧑‍🧒‍🧒",
743
+ familyManBoy: "👨‍👦",
744
+ familyManBoyBoy: "👨‍👦‍👦",
745
+ familyManGirl: "👨‍👧",
746
+ familyManGirlBoy: "👨‍👧‍👦",
747
+ familyManGirlGirl: "👨‍👧‍👧",
748
+ familyManManBoy: "👨‍👨‍👦",
749
+ familyManManBoyBoy: "👨‍👨‍👦‍👦",
750
+ familyManManGirl: "👨‍👨‍👧",
751
+ familyManManGirlBoy: "👨‍👨‍👧‍👦",
752
+ familyManManGirlGirl: "👨‍👨‍👧‍👧",
753
+ familyManWomanBoy: "👨‍👩‍👦",
754
+ familyManWomanBoyBoy: "👨‍👩‍👦‍👦",
755
+ familyManWomanGirl: "👨‍👩‍👧",
756
+ familyManWomanGirlBoy: "👨‍👩‍👧‍👦",
757
+ familyManWomanGirlGirl: "👨‍👩‍👧‍👧",
758
+ familyWomanBoy: "👩‍👦",
759
+ familyWomanBoyBoy: "👩‍👦‍👦",
760
+ familyWomanGirl: "👩‍👧",
761
+ familyWomanGirlBoy: "👩‍👧‍👦",
762
+ familyWomanGirlGirl: "👩‍👧‍👧",
763
+ familyWomanWomanBoy: "👩‍👩‍👦",
764
+ familyWomanWomanBoyBoy: "👩‍👩‍👦‍👦",
765
+ familyWomanWomanGirl: "👩‍👩‍👧",
766
+ familyWomanWomanGirlBoy: "👩‍👩‍👧‍👦",
767
+ familyWomanWomanGirlGirl: "👩‍👩‍👧‍👧",
768
+ farmer: "🧑‍🌾",
769
+ fastDownButton: "⏬",
770
+ fastForwardButton: "⏩",
771
+ fastReverseButton: "⏪",
772
+ fastUpButton: "⏫",
773
+ faxMachine: "📠",
774
+ fearfulFace: "😨",
775
+ feather: "🪶",
776
+ femaleSign: "♀️",
777
+ ferrisWheel: "🎡",
778
+ ferry: "⛴️",
779
+ fieldHockey: "🏑",
780
+ fightCloud: "🫯",
781
+ fileCabinet: "🗄️",
782
+ fileFolder: "📁",
783
+ filmFrames: "🎞️",
784
+ filmProjector: "📽️",
785
+ fingerprint: "🫆",
786
+ fire: "🔥",
787
+ firecracker: "🧨",
788
+ fireEngine: "🚒",
789
+ fireExtinguisher: "🧯",
790
+ firefighter: "🧑‍🚒",
791
+ fireworks: "🎆",
792
+ firstQuarterMoon: "🌓",
793
+ firstQuarterMoonFace: "🌛",
794
+ fish: "🐟",
795
+ fishCakeWithSwirl: "🍥",
796
+ fishingPole: "🎣",
797
+ fiveOClock: "🕔",
798
+ fiveThirty: "🕠",
799
+ flagAfghanistan: "🇦🇫",
800
+ flagAlandIslands: "🇦🇽",
801
+ flagAlbania: "🇦🇱",
802
+ flagAlgeria: "🇩🇿",
803
+ flagAmericanSamoa: "🇦🇸",
804
+ flagAndorra: "🇦🇩",
805
+ flagAngola: "🇦🇴",
806
+ flagAnguilla: "🇦🇮",
807
+ flagAntarctica: "🇦🇶",
808
+ flagAntiguaBarbuda: "🇦🇬",
809
+ flagArgentina: "🇦🇷",
810
+ flagArmenia: "🇦🇲",
811
+ flagAruba: "🇦🇼",
812
+ flagAscensionIsland: "🇦🇨",
813
+ flagAustralia: "🇦🇺",
814
+ flagAustria: "🇦🇹",
815
+ flagAzerbaijan: "🇦🇿",
816
+ flagBahamas: "🇧🇸",
817
+ flagBahrain: "🇧🇭",
818
+ flagBangladesh: "🇧🇩",
819
+ flagBarbados: "🇧🇧",
820
+ flagBelarus: "🇧🇾",
821
+ flagBelgium: "🇧🇪",
822
+ flagBelize: "🇧🇿",
823
+ flagBenin: "🇧🇯",
824
+ flagBermuda: "🇧🇲",
825
+ flagBhutan: "🇧🇹",
826
+ flagBolivia: "🇧🇴",
827
+ flagBosniaHerzegovina: "🇧🇦",
828
+ flagBotswana: "🇧🇼",
829
+ flagBouvetIsland: "🇧🇻",
830
+ flagBrazil: "🇧🇷",
831
+ flagBritishIndianOceanTerritory: "🇮🇴",
832
+ flagBritishVirginIslands: "🇻🇬",
833
+ flagBrunei: "🇧🇳",
834
+ flagBulgaria: "🇧🇬",
835
+ flagBurkinaFaso: "🇧🇫",
836
+ flagBurundi: "🇧🇮",
837
+ flagCambodia: "🇰🇭",
838
+ flagCameroon: "🇨🇲",
839
+ flagCanada: "🇨🇦",
840
+ flagCanaryIslands: "🇮🇨",
841
+ flagCapeVerde: "🇨🇻",
842
+ flagCaribbeanNetherlands: "🇧🇶",
843
+ flagCaymanIslands: "🇰🇾",
844
+ flagCentralAfricanRepublic: "🇨🇫",
845
+ flagCeutaMelilla: "🇪🇦",
846
+ flagChad: "🇹🇩",
847
+ flagChile: "🇨🇱",
848
+ flagChina: "🇨🇳",
849
+ flagChristmasIsland: "🇨🇽",
850
+ flagClippertonIsland: "🇨🇵",
851
+ flagCocosIslands: "🇨🇨",
852
+ flagColombia: "🇨🇴",
853
+ flagComoros: "🇰🇲",
854
+ flagCongoBrazzaville: "🇨🇬",
855
+ flagCongoKinshasa: "🇨🇩",
856
+ flagCookIslands: "🇨🇰",
857
+ flagCostaRica: "🇨🇷",
858
+ flagCoteDIvoire: "🇨🇮",
859
+ flagCroatia: "🇭🇷",
860
+ flagCuba: "🇨🇺",
861
+ flagCuracao: "🇨🇼",
862
+ flagCyprus: "🇨🇾",
863
+ flagCzechia: "🇨🇿",
864
+ flagDenmark: "🇩🇰",
865
+ flagDiegoGarcia: "🇩🇬",
866
+ flagDjibouti: "🇩🇯",
867
+ flagDominica: "🇩🇲",
868
+ flagDominicanRepublic: "🇩🇴",
869
+ flagEcuador: "🇪🇨",
870
+ flagEgypt: "🇪🇬",
871
+ flagElSalvador: "🇸🇻",
872
+ flagEngland: "🏴󠁧󠁢󠁥󠁮󠁧󠁿",
873
+ flagEquatorialGuinea: "🇬🇶",
874
+ flagEritrea: "🇪🇷",
875
+ flagEstonia: "🇪🇪",
876
+ flagEswatini: "🇸🇿",
877
+ flagEthiopia: "🇪🇹",
878
+ flagEuropeanUnion: "🇪🇺",
879
+ flagFalklandIslands: "🇫🇰",
880
+ flagFaroeIslands: "🇫🇴",
881
+ flagFiji: "🇫🇯",
882
+ flagFinland: "🇫🇮",
883
+ flagFrance: "🇫🇷",
884
+ flagFrenchGuiana: "🇬🇫",
885
+ flagFrenchPolynesia: "🇵🇫",
886
+ flagFrenchSouthernTerritories: "🇹🇫",
887
+ flagGabon: "🇬🇦",
888
+ flagGambia: "🇬🇲",
889
+ flagGeorgia: "🇬🇪",
890
+ flagGermany: "🇩🇪",
891
+ flagGhana: "🇬🇭",
892
+ flagGibraltar: "🇬🇮",
893
+ flagGreece: "🇬🇷",
894
+ flagGreenland: "🇬🇱",
895
+ flagGrenada: "🇬🇩",
896
+ flagGuadeloupe: "🇬🇵",
897
+ flagGuam: "🇬🇺",
898
+ flagGuatemala: "🇬🇹",
899
+ flagGuernsey: "🇬🇬",
900
+ flagGuinea: "🇬🇳",
901
+ flagGuineaBissau: "🇬🇼",
902
+ flagGuyana: "🇬🇾",
903
+ flagHaiti: "🇭🇹",
904
+ flagHeardMcdonaldIslands: "🇭🇲",
905
+ flagHonduras: "🇭🇳",
906
+ flagHongKongSarChina: "🇭🇰",
907
+ flagHungary: "🇭🇺",
908
+ flagIceland: "🇮🇸",
909
+ flagIndia: "🇮🇳",
910
+ flagIndonesia: "🇮🇩",
911
+ flagInHole: "⛳",
912
+ flagIran: "🇮🇷",
913
+ flagIraq: "🇮🇶",
914
+ flagIreland: "🇮🇪",
915
+ flagIsleOfMan: "🇮🇲",
916
+ flagIsrael: "🇮🇱",
917
+ flagItaly: "🇮🇹",
918
+ flagJamaica: "🇯🇲",
919
+ flagJapan: "🇯🇵",
920
+ flagJersey: "🇯🇪",
921
+ flagJordan: "🇯🇴",
922
+ flagKazakhstan: "🇰🇿",
923
+ flagKenya: "🇰🇪",
924
+ flagKiribati: "🇰🇮",
925
+ flagKosovo: "🇽🇰",
926
+ flagKuwait: "🇰🇼",
927
+ flagKyrgyzstan: "🇰🇬",
928
+ flagLaos: "🇱🇦",
929
+ flagLatvia: "🇱🇻",
930
+ flagLebanon: "🇱🇧",
931
+ flagLesotho: "🇱🇸",
932
+ flagLiberia: "🇱🇷",
933
+ flagLibya: "🇱🇾",
934
+ flagLiechtenstein: "🇱🇮",
935
+ flagLithuania: "🇱🇹",
936
+ flagLuxembourg: "🇱🇺",
937
+ flagMacaoSarChina: "🇲🇴",
938
+ flagMadagascar: "🇲🇬",
939
+ flagMalawi: "🇲🇼",
940
+ flagMalaysia: "🇲🇾",
941
+ flagMaldives: "🇲🇻",
942
+ flagMali: "🇲🇱",
943
+ flagMalta: "🇲🇹",
944
+ flagMarshallIslands: "🇲🇭",
945
+ flagMartinique: "🇲🇶",
946
+ flagMauritania: "🇲🇷",
947
+ flagMauritius: "🇲🇺",
948
+ flagMayotte: "🇾🇹",
949
+ flagMexico: "🇲🇽",
950
+ flagMicronesia: "🇫🇲",
951
+ flagMoldova: "🇲🇩",
952
+ flagMonaco: "🇲🇨",
953
+ flagMongolia: "🇲🇳",
954
+ flagMontenegro: "🇲🇪",
955
+ flagMontserrat: "🇲🇸",
956
+ flagMorocco: "🇲🇦",
957
+ flagMozambique: "🇲🇿",
958
+ flagMyanmar: "🇲🇲",
959
+ flagNamibia: "🇳🇦",
960
+ flagNauru: "🇳🇷",
961
+ flagNepal: "🇳🇵",
962
+ flagNetherlands: "🇳🇱",
963
+ flagNewCaledonia: "🇳🇨",
964
+ flagNewZealand: "🇳🇿",
965
+ flagNicaragua: "🇳🇮",
966
+ flagNiger: "🇳🇪",
967
+ flagNigeria: "🇳🇬",
968
+ flagNiue: "🇳🇺",
969
+ flagNorfolkIsland: "🇳🇫",
970
+ flagNorthernMarianaIslands: "🇲🇵",
971
+ flagNorthKorea: "🇰🇵",
972
+ flagNorthMacedonia: "🇲🇰",
973
+ flagNorway: "🇳🇴",
974
+ flagOman: "🇴🇲",
975
+ flagPakistan: "🇵🇰",
976
+ flagPalau: "🇵🇼",
977
+ flagPalestinianTerritories: "🇵🇸",
978
+ flagPanama: "🇵🇦",
979
+ flagPapuaNewGuinea: "🇵🇬",
980
+ flagParaguay: "🇵🇾",
981
+ flagPeru: "🇵🇪",
982
+ flagPhilippines: "🇵🇭",
983
+ flagPitcairnIslands: "🇵🇳",
984
+ flagPoland: "🇵🇱",
985
+ flagPortugal: "🇵🇹",
986
+ flagPuertoRico: "🇵🇷",
987
+ flagQatar: "🇶🇦",
988
+ flagReunion: "🇷🇪",
989
+ flagRomania: "🇷🇴",
990
+ flagRussia: "🇷🇺",
991
+ flagRwanda: "🇷🇼",
992
+ flagSamoa: "🇼🇸",
993
+ flagSanMarino: "🇸🇲",
994
+ flagSaoTomePrincipe: "🇸🇹",
995
+ flagSark: "🇨🇶",
996
+ flagSaudiArabia: "🇸🇦",
997
+ flagScotland: "🏴󠁧󠁢󠁳󠁣󠁴󠁿",
998
+ flagSenegal: "🇸🇳",
999
+ flagSerbia: "🇷🇸",
1000
+ flagSeychelles: "🇸🇨",
1001
+ flagSierraLeone: "🇸🇱",
1002
+ flagSingapore: "🇸🇬",
1003
+ flagSintMaarten: "🇸🇽",
1004
+ flagSlovakia: "🇸🇰",
1005
+ flagSlovenia: "🇸🇮",
1006
+ flagSolomonIslands: "🇸🇧",
1007
+ flagSomalia: "🇸🇴",
1008
+ flagSouthAfrica: "🇿🇦",
1009
+ flagSouthGeorgiaSouthSandwichIslands: "🇬🇸",
1010
+ flagSouthKorea: "🇰🇷",
1011
+ flagSouthSudan: "🇸🇸",
1012
+ flagSpain: "🇪🇸",
1013
+ flagSriLanka: "🇱🇰",
1014
+ flagStBarthelemy: "🇧🇱",
1015
+ flagStHelena: "🇸🇭",
1016
+ flagStKittsNevis: "🇰🇳",
1017
+ flagStLucia: "🇱🇨",
1018
+ flagStMartin: "🇲🇫",
1019
+ flagStPierreMiquelon: "🇵🇲",
1020
+ flagStVincentGrenadines: "🇻🇨",
1021
+ flagSudan: "🇸🇩",
1022
+ flagSuriname: "🇸🇷",
1023
+ flagSvalbardJanMayen: "🇸🇯",
1024
+ flagSweden: "🇸🇪",
1025
+ flagSwitzerland: "🇨🇭",
1026
+ flagSyria: "🇸🇾",
1027
+ flagTaiwan: "🇹🇼",
1028
+ flagTajikistan: "🇹🇯",
1029
+ flagTanzania: "🇹🇿",
1030
+ flagThailand: "🇹🇭",
1031
+ flagTimorLeste: "🇹🇱",
1032
+ flagTogo: "🇹🇬",
1033
+ flagTokelau: "🇹🇰",
1034
+ flagTonga: "🇹🇴",
1035
+ flagTrinidadTobago: "🇹🇹",
1036
+ flagTristanDaCunha: "🇹🇦",
1037
+ flagTunisia: "🇹🇳",
1038
+ flagTurkiye: "🇹🇷",
1039
+ flagTurkmenistan: "🇹🇲",
1040
+ flagTurksCaicosIslands: "🇹🇨",
1041
+ flagTuvalu: "🇹🇻",
1042
+ flagUganda: "🇺🇬",
1043
+ flagUkraine: "🇺🇦",
1044
+ flagUnitedArabEmirates: "🇦🇪",
1045
+ flagUnitedKingdom: "🇬🇧",
1046
+ flagUnitedNations: "🇺🇳",
1047
+ flagUnitedStates: "🇺🇸",
1048
+ flagUruguay: "🇺🇾",
1049
+ flagUSOutlyingIslands: "🇺🇲",
1050
+ flagUSVirginIslands: "🇻🇮",
1051
+ flagUzbekistan: "🇺🇿",
1052
+ flagVanuatu: "🇻🇺",
1053
+ flagVaticanCity: "🇻🇦",
1054
+ flagVenezuela: "🇻🇪",
1055
+ flagVietnam: "🇻🇳",
1056
+ flagWales: "🏴󠁧󠁢󠁷󠁬󠁳󠁿",
1057
+ flagWallisFutuna: "🇼🇫",
1058
+ flagWesternSahara: "🇪🇭",
1059
+ flagYemen: "🇾🇪",
1060
+ flagZambia: "🇿🇲",
1061
+ flagZimbabwe: "🇿🇼",
1062
+ flamingo: "🦩",
1063
+ flashlight: "🔦",
1064
+ flatbread: "🫓",
1065
+ flatShoe: "🥿",
1066
+ fleurDeLis: "⚜️",
1067
+ flexedBiceps: "💪",
1068
+ floppyDisk: "💾",
1069
+ flowerPlayingCards: "🎴",
1070
+ flushedFace: "😳",
1071
+ flute: "🪈",
1072
+ fly: "🪰",
1073
+ flyingDisc: "🥏",
1074
+ flyingSaucer: "🛸",
1075
+ fog: "🌫️",
1076
+ foggy: "🌁",
1077
+ foldedHands: "🙏",
1078
+ foldingHandFan: "🪭",
1079
+ fondue: "🫕",
1080
+ foot: "🦶",
1081
+ footprints: "👣",
1082
+ forkAndKnife: "🍴",
1083
+ forkAndKnifeWithPlate: "🍽️",
1084
+ fortuneCookie: "🥠",
1085
+ fountain: "⛲",
1086
+ fountainPen: "🖋️",
1087
+ fourLeafClover: "🍀",
1088
+ fourOClock: "🕓",
1089
+ fourThirty: "🕟",
1090
+ fox: "🦊",
1091
+ framedPicture: "🖼️",
1092
+ freeButton: "🆓",
1093
+ frenchFries: "🍟",
1094
+ friedShrimp: "🍤",
1095
+ frog: "🐸",
1096
+ frontFacingBabyChick: "🐥",
1097
+ frowningFace: "☹️",
1098
+ frowningFaceWithOpenMouth: "😦",
1099
+ fuelPump: "⛽",
1100
+ fullMoon: "🌕",
1101
+ fullMoonFace: "🌝",
1102
+ funeralUrn: "⚱️",
1103
+ gameDie: "🎲",
1104
+ garlic: "🧄",
1105
+ gear: "⚙️",
1106
+ gemini: "♊",
1107
+ gemStone: "💎",
1108
+ genie: "🧞",
1109
+ ghost: "👻",
1110
+ gingerRoot: "🫚",
1111
+ giraffe: "🦒",
1112
+ girl: "👧",
1113
+ glasses: "👓",
1114
+ glassOfMilk: "🥛",
1115
+ globeShowingAmericas: "🌎",
1116
+ globeShowingAsiaAustralia: "🌏",
1117
+ globeShowingEuropeAfrica: "🌍",
1118
+ globeWithMeridians: "🌐",
1119
+ gloves: "🧤",
1120
+ glowingStar: "🌟",
1121
+ goalNet: "🥅",
1122
+ goat: "🐐",
1123
+ goblin: "👺",
1124
+ goggles: "🥽",
1125
+ goose: "🪿",
1126
+ gorilla: "🦍",
1127
+ graduationCap: "🎓",
1128
+ grapes: "🍇",
1129
+ greenApple: "🍏",
1130
+ greenBook: "📗",
1131
+ greenCircle: "🟢",
1132
+ greenHeart: "💚",
1133
+ greenSalad: "🥗",
1134
+ greenSquare: "🟩",
1135
+ greyHeart: "🩶",
1136
+ grimacingFace: "😬",
1137
+ grinningCat: "😺",
1138
+ grinningCatWithSmilingEyes: "😸",
1139
+ grinningFace: "😀",
1140
+ grinningFaceWithBigEyes: "😃",
1141
+ grinningFaceWithSmilingEyes: "😄",
1142
+ grinningFaceWithSweat: "😅",
1143
+ grinningSquintingFace: "😆",
1144
+ growingHeart: "💗",
1145
+ guard: "💂",
1146
+ guideDog: "🦮",
1147
+ guitar: "🎸",
1148
+ hairPick: "🪮",
1149
+ hairyCreature: "🫈",
1150
+ hamburger: "🍔",
1151
+ hammer: "🔨",
1152
+ hammerAndPick: "⚒️",
1153
+ hammerAndWrench: "🛠️",
1154
+ hamsa: "🪬",
1155
+ hamster: "🐹",
1156
+ handbag: "👜",
1157
+ handshake: "🤝",
1158
+ handWithFingersSplayed: "🖐️",
1159
+ handWithIndexFingerAndThumbCrossed: "🫰",
1160
+ harp: "🪉",
1161
+ hatchingChick: "🐣",
1162
+ headphone: "🎧",
1163
+ headShakingHorizontally: "🙂‍↔️",
1164
+ headShakingVertically: "🙂‍↕️",
1165
+ headstone: "🪦",
1166
+ healthWorker: "🧑‍⚕️",
1167
+ hearNoEvilMonkey: "🙉",
1168
+ heartDecoration: "💟",
1169
+ heartExclamation: "❣️",
1170
+ heartHands: "🫶",
1171
+ heartOnFire: "❤️‍🔥",
1172
+ heartSuit: "♥️",
1173
+ heartWithArrow: "💘",
1174
+ heartWithRibbon: "💝",
1175
+ heavyDollarSign: "💲",
1176
+ heavyEqualsSign: "🟰",
1177
+ hedgehog: "🦔",
1178
+ helicopter: "🚁",
1179
+ herb: "🌿",
1180
+ hibiscus: "🌺",
1181
+ highHeeledShoe: "👠",
1182
+ highSpeedTrain: "🚄",
1183
+ highVoltage: "⚡",
1184
+ hikingBoot: "🥾",
1185
+ hinduTemple: "🛕",
1186
+ hippopotamus: "🦛",
1187
+ hole: "🕳️",
1188
+ hollowRedCircle: "⭕",
1189
+ honeybee: "🐝",
1190
+ honeyPot: "🍯",
1191
+ hook: "🪝",
1192
+ horizontalTrafficLight: "🚥",
1193
+ horse: "🐎",
1194
+ horseFace: "🐴",
1195
+ horseRacing: "🏇",
1196
+ hospital: "🏥",
1197
+ hotBeverage: "☕",
1198
+ hotDog: "🌭",
1199
+ hotel: "🏨",
1200
+ hotFace: "🥵",
1201
+ hotPepper: "🌶️",
1202
+ hotSprings: "♨️",
1203
+ hourglassDone: "⌛",
1204
+ hourglassNotDone: "⏳",
1205
+ house: "🏠",
1206
+ houses: "🏘️",
1207
+ houseWithGarden: "🏡",
1208
+ hundredPoints: "💯",
1209
+ hushedFace: "😯",
1210
+ hut: "🛖",
1211
+ hyacinth: "🪻",
1212
+ ice: "🧊",
1213
+ iceCream: "🍨",
1214
+ iceHockey: "🏒",
1215
+ iceSkate: "⛸️",
1216
+ idButton: "🆔",
1217
+ identificationCard: "🪪",
1218
+ inboxTray: "📥",
1219
+ incomingEnvelope: "📨",
1220
+ indexPointingAtTheViewer: "🫵",
1221
+ indexPointingUp: "☝️",
1222
+ infinity: "♾️",
1223
+ information: "ℹ️",
1224
+ inputLatinLetters: "🔤",
1225
+ inputLatinLowercase: "🔡",
1226
+ inputLatinUppercase: "🔠",
1227
+ inputNumbers: "🔢",
1228
+ inputSymbols: "🔣",
1229
+ jackOLantern: "🎃",
1230
+ japaneseAcceptableButton: "🉑",
1231
+ japaneseApplicationButton: "🈸",
1232
+ japaneseBargainButton: "🉐",
1233
+ japaneseCastle: "🏯",
1234
+ japaneseCongratulationsButton: "㊗️",
1235
+ japaneseDiscountButton: "🈹",
1236
+ japaneseDolls: "🎎",
1237
+ japaneseFreeOfChargeButton: "🈚",
1238
+ japaneseHereButton: "🈁",
1239
+ japaneseMonthlyAmountButton: "🈷️",
1240
+ japaneseNotFreeOfChargeButton: "🈶",
1241
+ japaneseNoVacancyButton: "🈵",
1242
+ japaneseOpenForBusinessButton: "🈺",
1243
+ japanesePassingGradeButton: "🈴",
1244
+ japanesePostOffice: "🏣",
1245
+ japaneseProhibitedButton: "🈲",
1246
+ japaneseReservedButton: "🈯",
1247
+ japaneseSecretButton: "㊙️",
1248
+ japaneseServiceChargeButton: "🈂️",
1249
+ japaneseSymbolForBeginner: "🔰",
1250
+ japaneseVacancyButton: "🈳",
1251
+ jar: "🫙",
1252
+ jeans: "👖",
1253
+ jellyfish: "🪼",
1254
+ joker: "🃏",
1255
+ joystick: "🕹️",
1256
+ judge: "🧑‍⚖️",
1257
+ kaaba: "🕋",
1258
+ kangaroo: "🦘",
1259
+ key: "🔑",
1260
+ keyboard: "⌨️",
1261
+ keycap0: "0️⃣",
1262
+ keycap1: "1️⃣",
1263
+ keycap10: "🔟",
1264
+ keycap2: "2️⃣",
1265
+ keycap3: "3️⃣",
1266
+ keycap4: "4️⃣",
1267
+ keycap5: "5️⃣",
1268
+ keycap6: "6️⃣",
1269
+ keycap7: "7️⃣",
1270
+ keycap8: "8️⃣",
1271
+ keycap9: "9️⃣",
1272
+ keycapAsterisk: "*️⃣",
1273
+ keycapNumberSign: "#️⃣",
1274
+ khanda: "🪯",
1275
+ kickScooter: "🛴",
1276
+ kimono: "👘",
1277
+ kiss: "💏",
1278
+ kissingCat: "😽",
1279
+ kissingFace: "😗",
1280
+ kissingFaceWithClosedEyes: "😚",
1281
+ kissingFaceWithSmilingEyes: "😙",
1282
+ kissManMan: "👨‍❤️‍💋‍👨",
1283
+ kissMark: "💋",
1284
+ kissWomanMan: "👩‍❤️‍💋‍👨",
1285
+ kissWomanWoman: "👩‍❤️‍💋‍👩",
1286
+ kitchenKnife: "🔪",
1287
+ kite: "🪁",
1288
+ kiwiFruit: "🥝",
1289
+ knot: "🪢",
1290
+ koala: "🐨",
1291
+ labCoat: "🥼",
1292
+ label: "🏷️",
1293
+ lacrosse: "🥍",
1294
+ ladder: "🪜",
1295
+ ladyBeetle: "🐞",
1296
+ landslide: "🛘",
1297
+ laptop: "💻",
1298
+ largeBlueDiamond: "🔷",
1299
+ largeOrangeDiamond: "🔶",
1300
+ lastQuarterMoon: "🌗",
1301
+ lastQuarterMoonFace: "🌜",
1302
+ lastTrackButton: "⏮️",
1303
+ latinCross: "✝️",
1304
+ leafFlutteringInWind: "🍃",
1305
+ leaflessTree: "🪾",
1306
+ leafyGreen: "🥬",
1307
+ ledger: "📒",
1308
+ leftArrow: "⬅️",
1309
+ leftArrowCurvingRight: "↪️",
1310
+ leftFacingFist: "🤛",
1311
+ leftLuggage: "🛅",
1312
+ leftRightArrow: "↔️",
1313
+ leftSpeechBubble: "🗨️",
1314
+ leftwardsHand: "🫲",
1315
+ leftwardsPushingHand: "🫷",
1316
+ leg: "🦵",
1317
+ lemon: "🍋",
1318
+ leo: "♌",
1319
+ leopard: "🐆",
1320
+ levelSlider: "🎚️",
1321
+ libra: "♎",
1322
+ lightBlueHeart: "🩵",
1323
+ lightBulb: "💡",
1324
+ lightRail: "🚈",
1325
+ lime: "🍋‍🟩",
1326
+ link: "🔗",
1327
+ linkedPaperclips: "🖇️",
1328
+ lion: "🦁",
1329
+ lipstick: "💄",
1330
+ litterInBinSign: "🚮",
1331
+ lizard: "🦎",
1332
+ llama: "🦙",
1333
+ lobster: "🦞",
1334
+ locked: "🔒",
1335
+ lockedWithKey: "🔐",
1336
+ lockedWithPen: "🔏",
1337
+ locomotive: "🚂",
1338
+ lollipop: "🍭",
1339
+ longDrum: "🪘",
1340
+ lotionBottle: "🧴",
1341
+ lotus: "🪷",
1342
+ loudlyCryingFace: "😭",
1343
+ loudspeaker: "📢",
1344
+ loveHotel: "🏩",
1345
+ loveLetter: "💌",
1346
+ loveYouGesture: "🤟",
1347
+ lowBattery: "🪫",
1348
+ luggage: "🧳",
1349
+ lungs: "🫁",
1350
+ lyingFace: "🤥",
1351
+ mage: "🧙",
1352
+ magicWand: "🪄",
1353
+ magnet: "🧲",
1354
+ magnifyingGlassTiltedLeft: "🔍",
1355
+ magnifyingGlassTiltedRight: "🔎",
1356
+ mahjongRedDragon: "🀄",
1357
+ maleSign: "♂️",
1358
+ mammoth: "🦣",
1359
+ man: "👨",
1360
+ manArtist: "👨‍🎨",
1361
+ manAstronaut: "👨‍🚀",
1362
+ manBald: "👨‍🦲",
1363
+ manBeard: "🧔‍♂️",
1364
+ manBiking: "🚴‍♂️",
1365
+ manBlondHair: "👱‍♂️",
1366
+ manBouncingBall: "⛹️‍♂️",
1367
+ manBowing: "🙇‍♂️",
1368
+ manCartwheeling: "🤸‍♂️",
1369
+ manClimbing: "🧗‍♂️",
1370
+ manConstructionWorker: "👷‍♂️",
1371
+ manCook: "👨‍🍳",
1372
+ manCurlyHair: "👨‍🦱",
1373
+ manDancing: "🕺",
1374
+ manDetective: "🕵️‍♂️",
1375
+ manElf: "🧝‍♂️",
1376
+ manFacepalming: "🤦‍♂️",
1377
+ manFactoryWorker: "👨‍🏭",
1378
+ manFairy: "🧚‍♂️",
1379
+ manFarmer: "👨‍🌾",
1380
+ manFeedingBaby: "👨‍🍼",
1381
+ manFirefighter: "👨‍🚒",
1382
+ manFrowning: "🙍‍♂️",
1383
+ manGenie: "🧞‍♂️",
1384
+ manGesturingNo: "🙅‍♂️",
1385
+ manGesturingOk: "🙆‍♂️",
1386
+ manGettingHaircut: "💇‍♂️",
1387
+ manGettingMassage: "💆‍♂️",
1388
+ mango: "🥭",
1389
+ manGolfing: "🏌️‍♂️",
1390
+ manGuard: "💂‍♂️",
1391
+ manHealthWorker: "👨‍⚕️",
1392
+ manInLotusPosition: "🧘‍♂️",
1393
+ manInManualWheelchair: "👨‍🦽",
1394
+ manInManualWheelchairFacingRight: "👨‍🦽‍➡️",
1395
+ manInMotorizedWheelchair: "👨‍🦼",
1396
+ manInMotorizedWheelchairFacingRight: "👨‍🦼‍➡️",
1397
+ manInSteamyRoom: "🧖‍♂️",
1398
+ manInTuxedo: "🤵‍♂️",
1399
+ manJudge: "👨‍⚖️",
1400
+ manJuggling: "🤹‍♂️",
1401
+ manKneeling: "🧎‍♂️",
1402
+ manKneelingFacingRight: "🧎‍♂️‍➡️",
1403
+ manLiftingWeights: "🏋️‍♂️",
1404
+ manMage: "🧙‍♂️",
1405
+ manMechanic: "👨‍🔧",
1406
+ manMountainBiking: "🚵‍♂️",
1407
+ manOfficeWorker: "👨‍💼",
1408
+ manPilot: "👨‍✈️",
1409
+ manPlayingHandball: "🤾‍♂️",
1410
+ manPlayingWaterPolo: "🤽‍♂️",
1411
+ manPoliceOfficer: "👮‍♂️",
1412
+ manPouting: "🙎‍♂️",
1413
+ manRaisingHand: "🙋‍♂️",
1414
+ manRedHair: "👨‍🦰",
1415
+ manRowingBoat: "🚣‍♂️",
1416
+ manRunning: "🏃‍♂️",
1417
+ manRunningFacingRight: "🏃‍♂️‍➡️",
1418
+ manScientist: "👨‍🔬",
1419
+ manShrugging: "🤷‍♂️",
1420
+ manSinger: "👨‍🎤",
1421
+ manSShoe: "👞",
1422
+ manStanding: "🧍‍♂️",
1423
+ manStudent: "👨‍🎓",
1424
+ manSuperhero: "🦸‍♂️",
1425
+ manSupervillain: "🦹‍♂️",
1426
+ manSurfing: "🏄‍♂️",
1427
+ manSwimming: "🏊‍♂️",
1428
+ manTeacher: "👨‍🏫",
1429
+ manTechnologist: "👨‍💻",
1430
+ mantelpieceClock: "🕰️",
1431
+ manTippingHand: "💁‍♂️",
1432
+ manualWheelchair: "🦽",
1433
+ manVampire: "🧛‍♂️",
1434
+ manWalking: "🚶‍♂️",
1435
+ manWalkingFacingRight: "🚶‍♂️‍➡️",
1436
+ manWearingTurban: "👳‍♂️",
1437
+ manWhiteHair: "👨‍🦳",
1438
+ manWithVeil: "👰‍♂️",
1439
+ manWithWhiteCane: "👨‍🦯",
1440
+ manWithWhiteCaneFacingRight: "👨‍🦯‍➡️",
1441
+ manZombie: "🧟‍♂️",
1442
+ mapleLeaf: "🍁",
1443
+ mapOfJapan: "🗾",
1444
+ maracas: "🪇",
1445
+ martialArtsUniform: "🥋",
1446
+ mate: "🧉",
1447
+ meatOnBone: "🍖",
1448
+ mechanic: "🧑‍🔧",
1449
+ mechanicalArm: "🦾",
1450
+ mechanicalLeg: "🦿",
1451
+ medicalSymbol: "⚕️",
1452
+ megaphone: "📣",
1453
+ melon: "🍈",
1454
+ meltingFace: "🫠",
1455
+ memo: "📝",
1456
+ mendingHeart: "❤️‍🩹",
1457
+ menHoldingHands: "👬",
1458
+ menorah: "🕎",
1459
+ menSRoom: "🚹",
1460
+ menWithBunnyEars: "👯‍♂️",
1461
+ menWrestling: "🤼‍♂️",
1462
+ mermaid: "🧜‍♀️",
1463
+ merman: "🧜‍♂️",
1464
+ merperson: "🧜",
1465
+ metro: "🚇",
1466
+ microbe: "🦠",
1467
+ microphone: "🎤",
1468
+ microscope: "🔬",
1469
+ middleFinger: "🖕",
1470
+ militaryHelmet: "🪖",
1471
+ militaryMedal: "🎖️",
1472
+ milkyWay: "🌌",
1473
+ minibus: "🚐",
1474
+ minus: "➖",
1475
+ mirror: "🪞",
1476
+ mirrorBall: "🪩",
1477
+ moai: "🗿",
1478
+ mobilePhone: "📱",
1479
+ mobilePhoneOff: "📴",
1480
+ mobilePhoneWithArrow: "📲",
1481
+ moneyBag: "💰",
1482
+ moneyMouthFace: "🤑",
1483
+ moneyWithWings: "💸",
1484
+ monkey: "🐒",
1485
+ monkeyFace: "🐵",
1486
+ monorail: "🚝",
1487
+ moonCake: "🥮",
1488
+ moonViewingCeremony: "🎑",
1489
+ moose: "🫎",
1490
+ mosque: "🕌",
1491
+ mosquito: "🦟",
1492
+ motorBoat: "🛥️",
1493
+ motorcycle: "🏍️",
1494
+ motorizedWheelchair: "🦼",
1495
+ motorScooter: "🛵",
1496
+ motorway: "🛣️",
1497
+ mountain: "⛰️",
1498
+ mountainCableway: "🚠",
1499
+ mountainRailway: "🚞",
1500
+ mountFuji: "🗻",
1501
+ mouse: "🐁",
1502
+ mouseFace: "🐭",
1503
+ mouseTrap: "🪤",
1504
+ mouth: "👄",
1505
+ movieCamera: "🎥",
1506
+ mrsClaus: "🤶",
1507
+ multiply: "✖️",
1508
+ mushroom: "🍄",
1509
+ musicalKeyboard: "🎹",
1510
+ musicalNote: "🎵",
1511
+ musicalNotes: "🎶",
1512
+ musicalScore: "🎼",
1513
+ mutedSpeaker: "🔇",
1514
+ mxClaus: "🧑‍🎄",
1515
+ nailPolish: "💅",
1516
+ nameBadge: "📛",
1517
+ nationalPark: "🏞️",
1518
+ nauseatedFace: "🤢",
1519
+ nazarAmulet: "🧿",
1520
+ necktie: "👔",
1521
+ nerdFace: "🤓",
1522
+ nestingDolls: "🪆",
1523
+ nestWithEggs: "🪺",
1524
+ neutralFace: "😐",
1525
+ newButton: "🆕",
1526
+ newMoon: "🌑",
1527
+ newMoonFace: "🌚",
1528
+ newspaper: "📰",
1529
+ nextTrackButton: "⏭️",
1530
+ ngButton: "🆖",
1531
+ nightWithStars: "🌃",
1532
+ nineOClock: "🕘",
1533
+ nineThirty: "🕤",
1534
+ ninja: "🥷",
1535
+ noBicycles: "🚳",
1536
+ noEntry: "⛔",
1537
+ noLittering: "🚯",
1538
+ noMobilePhones: "📵",
1539
+ nonPotableWater: "🚱",
1540
+ noOneUnderEighteen: "🔞",
1541
+ noPedestrians: "🚷",
1542
+ nose: "👃",
1543
+ noSmoking: "🚭",
1544
+ notebook: "📓",
1545
+ notebookWithDecorativeCover: "📔",
1546
+ nutAndBolt: "🔩",
1547
+ oButton: "🅾️",
1548
+ octopus: "🐙",
1549
+ oden: "🍢",
1550
+ officeBuilding: "🏢",
1551
+ officeWorker: "🧑‍💼",
1552
+ ogre: "👹",
1553
+ oilDrum: "🛢️",
1554
+ okButton: "🆗",
1555
+ okHand: "👌",
1556
+ olderPerson: "🧓",
1557
+ oldKey: "🗝️",
1558
+ oldMan: "👴",
1559
+ oldWoman: "👵",
1560
+ olive: "🫒",
1561
+ om: "🕉️",
1562
+ onArrow: "🔛",
1563
+ oncomingAutomobile: "🚘",
1564
+ oncomingBus: "🚍",
1565
+ oncomingFist: "👊",
1566
+ oncomingPoliceCar: "🚔",
1567
+ oncomingTaxi: "🚖",
1568
+ oneOClock: "🕐",
1569
+ onePieceSwimsuit: "🩱",
1570
+ oneThirty: "🕜",
1571
+ onion: "🧅",
1572
+ openBook: "📖",
1573
+ openFileFolder: "📂",
1574
+ openHands: "👐",
1575
+ openMailboxWithLoweredFlag: "📭",
1576
+ openMailboxWithRaisedFlag: "📬",
1577
+ ophiuchus: "⛎",
1578
+ opticalDisk: "💿",
1579
+ orangeBook: "📙",
1580
+ orangeCircle: "🟠",
1581
+ orangeHeart: "🧡",
1582
+ orangeSquare: "🟧",
1583
+ orangutan: "🦧",
1584
+ orca: "🫍",
1585
+ orthodoxCross: "☦️",
1586
+ otter: "🦦",
1587
+ outboxTray: "📤",
1588
+ owl: "🦉",
1589
+ ox: "🐂",
1590
+ oyster: "🦪",
1591
+ package_: "📦",
1592
+ pageFacingUp: "📄",
1593
+ pager: "📟",
1594
+ pageWithCurl: "📃",
1595
+ paintbrush: "🖌️",
1596
+ palmDownHand: "🫳",
1597
+ palmsUpTogether: "🤲",
1598
+ palmTree: "🌴",
1599
+ palmUpHand: "🫴",
1600
+ pancakes: "🥞",
1601
+ panda: "🐼",
1602
+ paperclip: "📎",
1603
+ parachute: "🪂",
1604
+ parrot: "🦜",
1605
+ partAlternationMark: "〽️",
1606
+ partyingFace: "🥳",
1607
+ partyPopper: "🎉",
1608
+ passengerShip: "🛳️",
1609
+ passportControl: "🛂",
1610
+ pauseButton: "⏸️",
1611
+ pawPrints: "🐾",
1612
+ pButton: "🅿️",
1613
+ peaceSymbol: "☮️",
1614
+ peach: "🍑",
1615
+ peacock: "🦚",
1616
+ peanuts: "🥜",
1617
+ peaPod: "🫛",
1618
+ pear: "🍐",
1619
+ pen: "🖊️",
1620
+ pencil: "✏️",
1621
+ penguin: "🐧",
1622
+ pensiveFace: "😔",
1623
+ peopleHoldingHands: "🧑‍🤝‍🧑",
1624
+ peopleHugging: "🫂",
1625
+ peopleWithBunnyEars: "👯",
1626
+ peopleWrestling: "🤼",
1627
+ performingArts: "🎭",
1628
+ perseveringFace: "😣",
1629
+ person: "🧑",
1630
+ personBald: "🧑‍🦲",
1631
+ personBeard: "🧔",
1632
+ personBiking: "🚴",
1633
+ personBlondHair: "👱",
1634
+ personBouncingBall: "⛹️",
1635
+ personBowing: "🙇",
1636
+ personCartwheeling: "🤸",
1637
+ personClimbing: "🧗",
1638
+ personCurlyHair: "🧑‍🦱",
1639
+ personFacepalming: "🤦",
1640
+ personFeedingBaby: "🧑‍🍼",
1641
+ personFencing: "🤺",
1642
+ personFrowning: "🙍",
1643
+ personGesturingNo: "🙅",
1644
+ personGesturingOk: "🙆",
1645
+ personGettingHaircut: "💇",
1646
+ personGettingMassage: "💆",
1647
+ personGolfing: "🏌️",
1648
+ personInBed: "🛌",
1649
+ personInLotusPosition: "🧘",
1650
+ personInManualWheelchair: "🧑‍🦽",
1651
+ personInManualWheelchairFacingRight: "🧑‍🦽‍➡️",
1652
+ personInMotorizedWheelchair: "🧑‍🦼",
1653
+ personInMotorizedWheelchairFacingRight: "🧑‍🦼‍➡️",
1654
+ personInSteamyRoom: "🧖",
1655
+ personInSuitLevitating: "🕴️",
1656
+ personInTuxedo: "🤵",
1657
+ personJuggling: "🤹",
1658
+ personKneeling: "🧎",
1659
+ personKneelingFacingRight: "🧎‍➡️",
1660
+ personLiftingWeights: "🏋️",
1661
+ personMountainBiking: "🚵",
1662
+ personPlayingHandball: "🤾",
1663
+ personPlayingWaterPolo: "🤽",
1664
+ personPouting: "🙎",
1665
+ personRaisingHand: "🙋",
1666
+ personRedHair: "🧑‍🦰",
1667
+ personRowingBoat: "🚣",
1668
+ personRunning: "🏃",
1669
+ personRunningFacingRight: "🏃‍➡️",
1670
+ personShrugging: "🤷",
1671
+ personStanding: "🧍",
1672
+ personSurfing: "🏄",
1673
+ personSwimming: "🏊",
1674
+ personTakingBath: "🛀",
1675
+ personTippingHand: "💁",
1676
+ personWalking: "🚶",
1677
+ personWalkingFacingRight: "🚶‍➡️",
1678
+ personWearingTurban: "👳",
1679
+ personWhiteHair: "🧑‍🦳",
1680
+ personWithCrown: "🫅",
1681
+ personWithSkullcap: "👲",
1682
+ personWithVeil: "👰",
1683
+ personWithWhiteCane: "🧑‍🦯",
1684
+ personWithWhiteCaneFacingRight: "🧑‍🦯‍➡️",
1685
+ petriDish: "🧫",
1686
+ phoenix: "🐦‍🔥",
1687
+ pick: "⛏️",
1688
+ pickupTruck: "🛻",
1689
+ pie: "🥧",
1690
+ pig: "🐖",
1691
+ pigFace: "🐷",
1692
+ pigNose: "🐽",
1693
+ pileOfPoo: "💩",
1694
+ pill: "💊",
1695
+ pilot: "🧑‍✈️",
1696
+ pinata: "🪅",
1697
+ pinchedFingers: "🤌",
1698
+ pinchingHand: "🤏",
1699
+ pineapple: "🍍",
1700
+ pineDecoration: "🎍",
1701
+ pingPong: "🏓",
1702
+ pinkHeart: "🩷",
1703
+ pirateFlag: "🏴‍☠️",
1704
+ pisces: "♓",
1705
+ pizza: "🍕",
1706
+ placard: "🪧",
1707
+ placeOfWorship: "🛐",
1708
+ playButton: "▶️",
1709
+ playgroundSlide: "🛝",
1710
+ playOrPauseButton: "⏯️",
1711
+ pleadingFace: "🥺",
1712
+ plunger: "🪠",
1713
+ plus: "➕",
1714
+ polarBear: "🐻‍❄️",
1715
+ policeCar: "🚓",
1716
+ policeCarLight: "🚨",
1717
+ policeOfficer: "👮",
1718
+ poodle: "🐩",
1719
+ pool8Ball: "🎱",
1720
+ popcorn: "🍿",
1721
+ postalHorn: "📯",
1722
+ postbox: "📮",
1723
+ postOffice: "🏤",
1724
+ potableWater: "🚰",
1725
+ potato: "🥔",
1726
+ potOfFood: "🍲",
1727
+ pottedPlant: "🪴",
1728
+ poultryLeg: "🍗",
1729
+ poundBanknote: "💷",
1730
+ pouringLiquid: "🫗",
1731
+ poutingCat: "😾",
1732
+ prayerBeads: "📿",
1733
+ pregnantMan: "🫃",
1734
+ pregnantPerson: "🫄",
1735
+ pregnantWoman: "🤰",
1736
+ pretzel: "🥨",
1737
+ prince: "🤴",
1738
+ princess: "👸",
1739
+ printer: "🖨️",
1740
+ prohibited: "🚫",
1741
+ purpleCircle: "🟣",
1742
+ purpleHeart: "💜",
1743
+ purpleSquare: "🟪",
1744
+ purse: "👛",
1745
+ pushpin: "📌",
1746
+ puzzlePiece: "🧩",
1747
+ rabbit: "🐇",
1748
+ rabbitFace: "🐰",
1749
+ raccoon: "🦝",
1750
+ racingCar: "🏎️",
1751
+ radio: "📻",
1752
+ radioactive: "☢️",
1753
+ radioButton: "🔘",
1754
+ railwayCar: "🚃",
1755
+ railwayTrack: "🛤️",
1756
+ rainbow: "🌈",
1757
+ rainbowFlag: "🏳️‍🌈",
1758
+ raisedBackOfHand: "🤚",
1759
+ raisedFist: "✊",
1760
+ raisedHand: "✋",
1761
+ raisingHands: "🙌",
1762
+ ram: "🐏",
1763
+ rat: "🐀",
1764
+ razor: "🪒",
1765
+ receipt: "🧾",
1766
+ recordButton: "⏺️",
1767
+ recyclingSymbol: "♻️",
1768
+ redApple: "🍎",
1769
+ redCircle: "🔴",
1770
+ redEnvelope: "🧧",
1771
+ redExclamationMark: "❗",
1772
+ redHeart: "❤️",
1773
+ redPaperLantern: "🏮",
1774
+ redQuestionMark: "❓",
1775
+ redSquare: "🟥",
1776
+ redTrianglePointedDown: "🔻",
1777
+ redTrianglePointedUp: "🔺",
1778
+ registered: "®️",
1779
+ relievedFace: "😌",
1780
+ reminderRibbon: "🎗️",
1781
+ repeatButton: "🔁",
1782
+ repeatSingleButton: "🔂",
1783
+ rescueWorkerSHelmet: "⛑️",
1784
+ restroom: "🚻",
1785
+ reverseButton: "◀️",
1786
+ revolvingHearts: "💞",
1787
+ rhinoceros: "🦏",
1788
+ ribbon: "🎀",
1789
+ riceBall: "🍙",
1790
+ riceCracker: "🍘",
1791
+ rightAngerBubble: "🗯️",
1792
+ rightArrow: "➡️",
1793
+ rightArrowCurvingDown: "⤵️",
1794
+ rightArrowCurvingLeft: "↩️",
1795
+ rightArrowCurvingUp: "⤴️",
1796
+ rightFacingFist: "🤜",
1797
+ rightwardsHand: "🫱",
1798
+ rightwardsPushingHand: "🫸",
1799
+ ring: "💍",
1800
+ ringBuoy: "🛟",
1801
+ ringedPlanet: "🪐",
1802
+ roastedSweetPotato: "🍠",
1803
+ robot: "🤖",
1804
+ rock: "🪨",
1805
+ rocket: "🚀",
1806
+ rolledUpNewspaper: "🗞️",
1807
+ rollerCoaster: "🎢",
1808
+ rollerSkate: "🛼",
1809
+ rollingOnTheFloorLaughing: "🤣",
1810
+ rollOfPaper: "🧻",
1811
+ rooster: "🐓",
1812
+ rootVegetable: "🫜",
1813
+ rose: "🌹",
1814
+ rosette: "🏵️",
1815
+ roundPushpin: "📍",
1816
+ rugbyFootball: "🏉",
1817
+ runningShirt: "🎽",
1818
+ runningShoe: "👟",
1819
+ sadButRelievedFace: "😥",
1820
+ safetyPin: "🧷",
1821
+ safetyVest: "🦺",
1822
+ sagittarius: "♐",
1823
+ sailboat: "⛵",
1824
+ sake: "🍶",
1825
+ salt: "🧂",
1826
+ salutingFace: "🫡",
1827
+ sandwich: "🥪",
1828
+ santaClaus: "🎅",
1829
+ sari: "🥻",
1830
+ satellite: "🛰️",
1831
+ satelliteAntenna: "📡",
1832
+ sauropod: "🦕",
1833
+ saxophone: "🎷",
1834
+ scarf: "🧣",
1835
+ school: "🏫",
1836
+ scientist: "🧑‍🔬",
1837
+ scissors: "✂️",
1838
+ scorpio: "♏",
1839
+ scorpion: "🦂",
1840
+ screwdriver: "🪛",
1841
+ scroll: "📜",
1842
+ seal: "🦭",
1843
+ seat: "💺",
1844
+ seedling: "🌱",
1845
+ seeNoEvilMonkey: "🙈",
1846
+ selfie: "🤳",
1847
+ serviceDog: "🐕‍🦺",
1848
+ sevenOClock: "🕖",
1849
+ sevenThirty: "🕢",
1850
+ sewingNeedle: "🪡",
1851
+ shakingFace: "🫨",
1852
+ shallowPanOfFood: "🥘",
1853
+ shamrock: "☘️",
1854
+ shark: "🦈",
1855
+ shavedIce: "🍧",
1856
+ sheafOfRice: "🌾",
1857
+ shield: "🛡️",
1858
+ shintoShrine: "⛩️",
1859
+ ship: "🚢",
1860
+ shootingStar: "🌠",
1861
+ shoppingBags: "🛍️",
1862
+ shoppingCart: "🛒",
1863
+ shortcake: "🍰",
1864
+ shorts: "🩳",
1865
+ shovel: "🪏",
1866
+ shower: "🚿",
1867
+ shrimp: "🦐",
1868
+ shuffleTracksButton: "🔀",
1869
+ shushingFace: "🤫",
1870
+ signOfTheHorns: "🤘",
1871
+ singer: "🧑‍🎤",
1872
+ sixOClock: "🕕",
1873
+ sixThirty: "🕡",
1874
+ skateboard: "🛹",
1875
+ skier: "⛷️",
1876
+ skis: "🎿",
1877
+ skull: "💀",
1878
+ skullAndCrossbones: "☠️",
1879
+ skunk: "🦨",
1880
+ sled: "🛷",
1881
+ sleepingFace: "😴",
1882
+ sleepyFace: "😪",
1883
+ slightlyFrowningFace: "🙁",
1884
+ slightlySmilingFace: "🙂",
1885
+ sloth: "🦥",
1886
+ slotMachine: "🎰",
1887
+ smallAirplane: "🛩️",
1888
+ smallBlueDiamond: "🔹",
1889
+ smallOrangeDiamond: "🔸",
1890
+ smilingCatWithHeartEyes: "😻",
1891
+ smilingFace: "☺️",
1892
+ smilingFaceWithHalo: "😇",
1893
+ smilingFaceWithHeartEyes: "😍",
1894
+ smilingFaceWithHearts: "🥰",
1895
+ smilingFaceWithHorns: "😈",
1896
+ smilingFaceWithOpenHands: "🤗",
1897
+ smilingFaceWithSmilingEyes: "😊",
1898
+ smilingFaceWithSunglasses: "😎",
1899
+ smilingFaceWithTear: "🥲",
1900
+ smirkingFace: "😏",
1901
+ snail: "🐌",
1902
+ snake: "🐍",
1903
+ sneezingFace: "🤧",
1904
+ snowboarder: "🏂",
1905
+ snowCappedMountain: "🏔️",
1906
+ snowflake: "❄️",
1907
+ snowman: "☃️",
1908
+ snowmanWithoutSnow: "⛄",
1909
+ soap: "🧼",
1910
+ soccerBall: "⚽",
1911
+ socks: "🧦",
1912
+ softball: "🥎",
1913
+ softIceCream: "🍦",
1914
+ soonArrow: "🔜",
1915
+ sosButton: "🆘",
1916
+ spadeSuit: "♠️",
1917
+ spaghetti: "🍝",
1918
+ sparkle: "❇️",
1919
+ sparkler: "🎇",
1920
+ sparkles: "✨",
1921
+ sparklingHeart: "💖",
1922
+ speakerHighVolume: "🔊",
1923
+ speakerLowVolume: "🔈",
1924
+ speakerMediumVolume: "🔉",
1925
+ speakingHead: "🗣️",
1926
+ speakNoEvilMonkey: "🙊",
1927
+ speechBalloon: "💬",
1928
+ speedboat: "🚤",
1929
+ spider: "🕷️",
1930
+ spiderWeb: "🕸️",
1931
+ spiralCalendar: "🗓️",
1932
+ spiralNotepad: "🗒️",
1933
+ spiralShell: "🐚",
1934
+ splatter: "🫟",
1935
+ sponge: "🧽",
1936
+ spoon: "🥄",
1937
+ sportsMedal: "🏅",
1938
+ sportUtilityVehicle: "🚙",
1939
+ spoutingWhale: "🐳",
1940
+ squid: "🦑",
1941
+ squintingFaceWithTongue: "😝",
1942
+ stadium: "🏟️",
1943
+ star: "⭐",
1944
+ starAndCrescent: "☪️",
1945
+ starOfDavid: "✡️",
1946
+ starStruck: "🤩",
1947
+ station: "🚉",
1948
+ statueOfLiberty: "🗽",
1949
+ steamingBowl: "🍜",
1950
+ stethoscope: "🩺",
1951
+ stopButton: "⏹️",
1952
+ stopSign: "🛑",
1953
+ stopwatch: "⏱️",
1954
+ straightRuler: "📏",
1955
+ strawberry: "🍓",
1956
+ student: "🧑‍🎓",
1957
+ studioMicrophone: "🎙️",
1958
+ stuffedFlatbread: "🥙",
1959
+ sun: "☀️",
1960
+ sunBehindCloud: "⛅",
1961
+ sunBehindLargeCloud: "🌥️",
1962
+ sunBehindRainCloud: "🌦️",
1963
+ sunBehindSmallCloud: "🌤️",
1964
+ sunflower: "🌻",
1965
+ sunglasses: "🕶️",
1966
+ sunrise: "🌅",
1967
+ sunriseOverMountains: "🌄",
1968
+ sunset: "🌇",
1969
+ sunWithFace: "🌞",
1970
+ superhero: "🦸",
1971
+ supervillain: "🦹",
1972
+ sushi: "🍣",
1973
+ suspensionRailway: "🚟",
1974
+ swan: "🦢",
1975
+ sweatDroplets: "💦",
1976
+ synagogue: "🕍",
1977
+ syringe: "💉",
1978
+ taco: "🌮",
1979
+ takeoutBox: "🥡",
1980
+ tamale: "🫔",
1981
+ tanabataTree: "🎋",
1982
+ tangerine: "🍊",
1983
+ taurus: "♉",
1984
+ taxi: "🚕",
1985
+ teacher: "🧑‍🏫",
1986
+ teacupWithoutHandle: "🍵",
1987
+ teapot: "🫖",
1988
+ tearOffCalendar: "📆",
1989
+ technologist: "🧑‍💻",
1990
+ teddyBear: "🧸",
1991
+ telephone: "☎️",
1992
+ telephoneReceiver: "📞",
1993
+ telescope: "🔭",
1994
+ television: "📺",
1995
+ tennis: "🎾",
1996
+ tenOClock: "🕙",
1997
+ tent: "⛺",
1998
+ tenThirty: "🕥",
1999
+ testTube: "🧪",
2000
+ thermometer: "🌡️",
2001
+ thinkingFace: "🤔",
2002
+ thongSandal: "🩴",
2003
+ thoughtBalloon: "💭",
2004
+ thread: "🧵",
2005
+ threeOClock: "🕒",
2006
+ threeThirty: "🕞",
2007
+ thumbsDown: "👎",
2008
+ thumbsUp: "👍",
2009
+ ticket: "🎫",
2010
+ tiger: "🐅",
2011
+ tigerFace: "🐯",
2012
+ timerClock: "⏲️",
2013
+ tiredFace: "😫",
2014
+ toilet: "🚽",
2015
+ tokyoTower: "🗼",
2016
+ tomato: "🍅",
2017
+ tongue: "👅",
2018
+ toolbox: "🧰",
2019
+ tooth: "🦷",
2020
+ toothbrush: "🪥",
2021
+ topArrow: "🔝",
2022
+ topHat: "🎩",
2023
+ tornado: "🌪️",
2024
+ trackball: "🖲️",
2025
+ tractor: "🚜",
2026
+ tradeMark: "™️",
2027
+ train: "🚆",
2028
+ tram: "🚊",
2029
+ tramCar: "🚋",
2030
+ transgenderFlag: "🏳️‍⚧️",
2031
+ transgenderSymbol: "⚧️",
2032
+ treasureChest: "🪎",
2033
+ tRex: "🦖",
2034
+ triangularFlag: "🚩",
2035
+ triangularRuler: "📐",
2036
+ tridentEmblem: "🔱",
2037
+ troll: "🧌",
2038
+ trolleybus: "🚎",
2039
+ trombone: "🪊",
2040
+ trophy: "🏆",
2041
+ tropicalDrink: "🍹",
2042
+ tropicalFish: "🐠",
2043
+ trumpet: "🎺",
2044
+ tShirt: "👕",
2045
+ tulip: "🌷",
2046
+ tumblerGlass: "🥃",
2047
+ turkey: "🦃",
2048
+ turtle: "🐢",
2049
+ twelveOClock: "🕛",
2050
+ twelveThirty: "🕧",
2051
+ twoHearts: "💕",
2052
+ twoHumpCamel: "🐫",
2053
+ twoOClock: "🕑",
2054
+ twoThirty: "🕝",
2055
+ umbrella: "☂️",
2056
+ umbrellaOnGround: "⛱️",
2057
+ umbrellaWithRainDrops: "☔",
2058
+ unamusedFace: "😒",
2059
+ unicorn: "🦄",
2060
+ unlocked: "🔓",
2061
+ upArrow: "⬆️",
2062
+ upButton: "🆙",
2063
+ upDownArrow: "↕️",
2064
+ upLeftArrow: "↖️",
2065
+ upRightArrow: "↗️",
2066
+ upsideDownFace: "🙃",
2067
+ upwardsButton: "🔼",
2068
+ vampire: "🧛",
2069
+ verticalTrafficLight: "🚦",
2070
+ vibrationMode: "📳",
2071
+ victoryHand: "✌️",
2072
+ videoCamera: "📹",
2073
+ videocassette: "📼",
2074
+ videoGame: "🎮",
2075
+ violin: "🎻",
2076
+ virgo: "♍",
2077
+ volcano: "🌋",
2078
+ volleyball: "🏐",
2079
+ vsButton: "🆚",
2080
+ vulcanSalute: "🖖",
2081
+ waffle: "🧇",
2082
+ waningCrescentMoon: "🌘",
2083
+ waningGibbousMoon: "🌖",
2084
+ warning: "⚠️",
2085
+ wastebasket: "🗑️",
2086
+ watch: "⌚",
2087
+ waterBuffalo: "🐃",
2088
+ waterCloset: "🚾",
2089
+ watermelon: "🍉",
2090
+ waterPistol: "🔫",
2091
+ waterWave: "🌊",
2092
+ wavingHand: "👋",
2093
+ wavyDash: "〰️",
2094
+ waxingCrescentMoon: "🌒",
2095
+ waxingGibbousMoon: "🌔",
2096
+ wearyCat: "🙀",
2097
+ wearyFace: "😩",
2098
+ wedding: "💒",
2099
+ whale: "🐋",
2100
+ wheel: "🛞",
2101
+ wheelchairSymbol: "♿",
2102
+ wheelOfDharma: "☸️",
2103
+ whiteCane: "🦯",
2104
+ whiteCircle: "⚪",
2105
+ whiteExclamationMark: "❕",
2106
+ whiteFlag: "🏳️",
2107
+ whiteFlower: "💮",
2108
+ whiteHeart: "🤍",
2109
+ whiteLargeSquare: "⬜",
2110
+ whiteMediumSmallSquare: "◽",
2111
+ whiteMediumSquare: "◻️",
2112
+ whiteQuestionMark: "❔",
2113
+ whiteSmallSquare: "▫️",
2114
+ whiteSquareButton: "🔳",
2115
+ wiltedFlower: "🥀",
2116
+ windChime: "🎐",
2117
+ windFace: "🌬️",
2118
+ window: "🪟",
2119
+ wineGlass: "🍷",
2120
+ wing: "🪽",
2121
+ winkingFace: "😉",
2122
+ winkingFaceWithTongue: "😜",
2123
+ wireless: "🛜",
2124
+ wolf: "🐺",
2125
+ woman: "👩",
2126
+ womanAndManHoldingHands: "👫",
2127
+ womanArtist: "👩‍🎨",
2128
+ womanAstronaut: "👩‍🚀",
2129
+ womanBald: "👩‍🦲",
2130
+ womanBeard: "🧔‍♀️",
2131
+ womanBiking: "🚴‍♀️",
2132
+ womanBlondHair: "👱‍♀️",
2133
+ womanBouncingBall: "⛹️‍♀️",
2134
+ womanBowing: "🙇‍♀️",
2135
+ womanCartwheeling: "🤸‍♀️",
2136
+ womanClimbing: "🧗‍♀️",
2137
+ womanConstructionWorker: "👷‍♀️",
2138
+ womanCook: "👩‍🍳",
2139
+ womanCurlyHair: "👩‍🦱",
2140
+ womanDancing: "💃",
2141
+ womanDetective: "🕵️‍♀️",
2142
+ womanElf: "🧝‍♀️",
2143
+ womanFacepalming: "🤦‍♀️",
2144
+ womanFactoryWorker: "👩‍🏭",
2145
+ womanFairy: "🧚‍♀️",
2146
+ womanFarmer: "👩‍🌾",
2147
+ womanFeedingBaby: "👩‍🍼",
2148
+ womanFirefighter: "👩‍🚒",
2149
+ womanFrowning: "🙍‍♀️",
2150
+ womanGenie: "🧞‍♀️",
2151
+ womanGesturingNo: "🙅‍♀️",
2152
+ womanGesturingOk: "🙆‍♀️",
2153
+ womanGettingHaircut: "💇‍♀️",
2154
+ womanGettingMassage: "💆‍♀️",
2155
+ womanGolfing: "🏌️‍♀️",
2156
+ womanGuard: "💂‍♀️",
2157
+ womanHealthWorker: "👩‍⚕️",
2158
+ womanInLotusPosition: "🧘‍♀️",
2159
+ womanInManualWheelchair: "👩‍🦽",
2160
+ womanInManualWheelchairFacingRight: "👩‍🦽‍➡️",
2161
+ womanInMotorizedWheelchair: "👩‍🦼",
2162
+ womanInMotorizedWheelchairFacingRight: "👩‍🦼‍➡️",
2163
+ womanInSteamyRoom: "🧖‍♀️",
2164
+ womanInTuxedo: "🤵‍♀️",
2165
+ womanJudge: "👩‍⚖️",
2166
+ womanJuggling: "🤹‍♀️",
2167
+ womanKneeling: "🧎‍♀️",
2168
+ womanKneelingFacingRight: "🧎‍♀️‍➡️",
2169
+ womanLiftingWeights: "🏋️‍♀️",
2170
+ womanMage: "🧙‍♀️",
2171
+ womanMechanic: "👩‍🔧",
2172
+ womanMountainBiking: "🚵‍♀️",
2173
+ womanOfficeWorker: "👩‍💼",
2174
+ womanPilot: "👩‍✈️",
2175
+ womanPlayingHandball: "🤾‍♀️",
2176
+ womanPlayingWaterPolo: "🤽‍♀️",
2177
+ womanPoliceOfficer: "👮‍♀️",
2178
+ womanPouting: "🙎‍♀️",
2179
+ womanRaisingHand: "🙋‍♀️",
2180
+ womanRedHair: "👩‍🦰",
2181
+ womanRowingBoat: "🚣‍♀️",
2182
+ womanRunning: "🏃‍♀️",
2183
+ womanRunningFacingRight: "🏃‍♀️‍➡️",
2184
+ womanSBoot: "👢",
2185
+ womanScientist: "👩‍🔬",
2186
+ womanSClothes: "👚",
2187
+ womanSHat: "👒",
2188
+ womanShrugging: "🤷‍♀️",
2189
+ womanSinger: "👩‍🎤",
2190
+ womanSSandal: "👡",
2191
+ womanStanding: "🧍‍♀️",
2192
+ womanStudent: "👩‍🎓",
2193
+ womanSuperhero: "🦸‍♀️",
2194
+ womanSupervillain: "🦹‍♀️",
2195
+ womanSurfing: "🏄‍♀️",
2196
+ womanSwimming: "🏊‍♀️",
2197
+ womanTeacher: "👩‍🏫",
2198
+ womanTechnologist: "👩‍💻",
2199
+ womanTippingHand: "💁‍♀️",
2200
+ womanVampire: "🧛‍♀️",
2201
+ womanWalking: "🚶‍♀️",
2202
+ womanWalkingFacingRight: "🚶‍♀️‍➡️",
2203
+ womanWearingTurban: "👳‍♀️",
2204
+ womanWhiteHair: "👩‍🦳",
2205
+ womanWithHeadscarf: "🧕",
2206
+ womanWithVeil: "👰‍♀️",
2207
+ womanWithWhiteCane: "👩‍🦯",
2208
+ womanWithWhiteCaneFacingRight: "👩‍🦯‍➡️",
2209
+ womanZombie: "🧟‍♀️",
2210
+ womenHoldingHands: "👭",
2211
+ womenSRoom: "🚺",
2212
+ womenWithBunnyEars: "👯‍♀️",
2213
+ womenWrestling: "🤼‍♀️",
2214
+ wood: "🪵",
2215
+ woozyFace: "🥴",
2216
+ worldMap: "🗺️",
2217
+ worm: "🪱",
2218
+ worriedFace: "😟",
2219
+ wrappedGift: "🎁",
2220
+ wrench: "🔧",
2221
+ writingHand: "✍️",
2222
+ xRay: "🩻",
2223
+ yarn: "🧶",
2224
+ yawningFace: "🥱",
2225
+ yellowCircle: "🟡",
2226
+ yellowHeart: "💛",
2227
+ yellowSquare: "🟨",
2228
+ yenBanknote: "💴",
2229
+ yinYang: "☯️",
2230
+ yoYo: "🪀",
2231
+ zanyFace: "🤪",
2232
+ zebra: "🦓",
2233
+ zipperMouthFace: "🤐",
2234
+ zombie: "🧟",
2235
+ zzz: "💤"
2236
+ };
2237
+ //#endregion
2238
+ //#region src/emoji/index.ts
2239
+ const aliases = {
2240
+ love: GeneratedEmoji.redHeart,
2241
+ like: GeneratedEmoji.thumbsUp,
2242
+ dislike: GeneratedEmoji.thumbsDown,
2243
+ laugh: GeneratedEmoji.faceWithTearsOfJoy,
2244
+ emphasize: GeneratedEmoji.doubleExclamationMark,
2245
+ question: GeneratedEmoji.redQuestionMark
2246
+ };
2247
+ const Emoji = {
2248
+ ...GeneratedEmoji,
2249
+ ...aliases
2250
+ };
2251
+ //#endregion
2252
+ //#region src/fusor/types.ts
2253
+ const FUSOR_BRAND = Symbol.for("spectrum.fusor.client");
2254
+ //#endregion
2255
+ //#region src/fusor/event.ts
2256
+ const FUSOR_EVENT_BRAND = Symbol.for("spectrum.fusor.event");
2257
+ function fusorEvent(name, data) {
2258
+ return {
2259
+ [FUSOR_EVENT_BRAND]: true,
2260
+ name,
2261
+ data
2262
+ };
2263
+ }
2264
+ function isFusorEvent(value) {
2265
+ return typeof value === "object" && value !== null && value[FUSOR_EVENT_BRAND] === true;
2266
+ }
2267
+ //#endregion
2268
+ //#region src/fusor/index.ts
2269
+ function fusor(platform, verify) {
2270
+ return {
2271
+ [FUSOR_BRAND]: true,
2272
+ platform,
2273
+ verify
2274
+ };
2275
+ }
2276
+ function isFusorClient(value) {
2277
+ return typeof value === "object" && value !== null && value[FUSOR_BRAND] === true;
2278
+ }
2279
+ //#endregion
2280
+ //#region src/utils/errors.ts
2281
+ const composeMessage = (opts) => {
2282
+ return `${opts.platform ?? "platform"} does not support ${opts.kind === "content" ? `content type "${opts.contentType ?? "unknown"}"` : `action "${opts.action ?? "unknown"}"`}${opts.detail ? `: ${opts.detail}` : ""}`;
2283
+ };
2284
+ var UnsupportedError = class UnsupportedError extends Error {
2285
+ kind;
2286
+ platform;
2287
+ contentType;
2288
+ action;
2289
+ detail;
2290
+ constructor(opts) {
2291
+ super(composeMessage(opts));
2292
+ this.name = "UnsupportedError";
2293
+ this.kind = opts.kind;
2294
+ this.platform = opts.platform;
2295
+ this.contentType = opts.contentType;
2296
+ this.action = opts.action;
2297
+ this.detail = opts.detail;
2298
+ }
2299
+ static content(contentType, platform, detail) {
2300
+ return new UnsupportedError({
2301
+ kind: "content",
2302
+ contentType,
2303
+ platform,
2304
+ detail
2305
+ });
2306
+ }
2307
+ static action(action, platform, detail) {
2308
+ return new UnsupportedError({
2309
+ kind: "action",
2310
+ action,
2311
+ platform,
2312
+ detail
2313
+ });
2314
+ }
2315
+ withPlatform(platform) {
2316
+ if (this.platform) return this;
2317
+ return new UnsupportedError({
2318
+ kind: this.kind,
2319
+ platform,
2320
+ contentType: this.contentType,
2321
+ action: this.action,
2322
+ detail: this.detail
2323
+ });
2324
+ }
2325
+ };
2326
+ //#endregion
2327
+ //#region src/platform/build.ts
2328
+ const platformLog$1 = createLogger("spectrum.platform");
2329
+ const FIRE_AND_FORGET_TYPES = new Set([
2330
+ "typing",
2331
+ "edit",
2332
+ "rename",
2333
+ "avatar",
2334
+ "unsend",
2335
+ "read"
2336
+ ]);
2337
+ const isFireAndForget = (item) => FIRE_AND_FORGET_TYPES.has(item.type) || item.__fireAndForget === true;
2338
+ const RESERVED_SPACE_KEYS = new Set([
2339
+ "__platform",
2340
+ "id",
2341
+ "send",
2342
+ "edit",
2343
+ "unsend",
2344
+ "read",
2345
+ "getMessage",
2346
+ "rename",
2347
+ "avatar",
2348
+ "startTyping",
2349
+ "stopTyping",
2350
+ "responding"
2351
+ ]);
2352
+ const PLATFORM_WISE_ACTION_KEYS = new Set(["getMessage"]);
2353
+ const RESERVED_MESSAGE_KEYS = new Set([
2354
+ "content",
2355
+ "direction",
2356
+ "edit",
2357
+ "id",
2358
+ "platform",
2359
+ "react",
2360
+ "read",
2361
+ "reply",
2362
+ "sender",
2363
+ "space",
2364
+ "timestamp",
2365
+ "unsend"
2366
+ ]);
2367
+ const scopeLabel = (scope) => {
2368
+ if (scope === "space") return "Space";
2369
+ if (scope === "message") return "Message";
2370
+ return "PlatformInstance";
2371
+ };
2372
+ const warnReservedAction = (scope, name, platform) => {
2373
+ platformLog$1.warn(`${platform} declared ${scope} action "${name}" which collides with a reserved ${scopeLabel(scope)} key; skipping.`, {
2374
+ "spectrum.provider": platform,
2375
+ "spectrum.reserved.scope": scope,
2376
+ "spectrum.reserved.action": name
2377
+ });
2378
+ };
2379
+ const warnUnsupported = (err, fallbackPlatform) => {
2380
+ const platform = err.platform ?? fallbackPlatform;
2381
+ const subject = err.kind === "content" ? `content type "${err.contentType ?? "unknown"}"` : `action "${err.action ?? "unknown"}"`;
2382
+ const detail = err.detail ? `: ${err.detail}` : "";
2383
+ platformLog$1.warn(`${platform} does not support ${subject}${detail}; skipping.`, {
2384
+ "spectrum.provider": platform,
2385
+ "spectrum.unsupported.kind": err.kind,
2386
+ "spectrum.unsupported.content_type": err.contentType,
2387
+ "spectrum.unsupported.action": err.action
2388
+ });
2389
+ };
2390
+ const contentPlatform = (content) => {
2391
+ const platform = content.__platform;
2392
+ return typeof platform === "string" ? platform : void 0;
2393
+ };
2394
+ const findUnsupportedPlatformContent = (content, platform) => {
2395
+ const scopedPlatform = contentPlatform(content);
2396
+ if (scopedPlatform && scopedPlatform !== platform) return scopedPlatform;
2397
+ if (content.type === "reply" || content.type === "edit") return findUnsupportedPlatformContent(content.content, platform);
2398
+ if (content.type !== "group") return;
2399
+ for (const item of content.items) {
2400
+ const nested = item.content;
2401
+ if (typeof nested !== "object" || nested === null || !("type" in nested)) continue;
2402
+ const unsupported = findUnsupportedPlatformContent(nested, platform);
2403
+ if (unsupported) return unsupported;
2404
+ }
2405
+ };
2406
+ const unsupportedPlatformContentError = (content, platform) => {
2407
+ const requiredPlatform = findUnsupportedPlatformContent(content, platform);
2408
+ if (!requiredPlatform) return;
2409
+ return UnsupportedError.content(content.type, platform, `requires ${requiredPlatform}`);
2410
+ };
2411
+ const findStreamText = (item) => {
2412
+ if (item.type === "streamText") return item;
2413
+ if ((item.type === "reply" || item.type === "edit") && item.content.type === "streamText") return item.content;
2414
+ };
2415
+ const replaceStreamText = (item, source, full) => {
2416
+ const inner = source.format === "markdown" ? asMarkdown(full) : asText(full);
2417
+ if (item.type === "reply" || item.type === "edit") return {
2418
+ ...item,
2419
+ content: inner
2420
+ };
2421
+ return inner;
2422
+ };
2423
+ const downgradeMarkdown = (md) => {
2424
+ const plain = markdownToPlainText(md.markdown);
2425
+ return plain ? asText(plain) : void 0;
2426
+ };
2427
+ const replaceMarkdown = (item) => {
2428
+ if (item.type === "markdown") return downgradeMarkdown(item) ?? item;
2429
+ if ((item.type === "reply" || item.type === "edit") && item.content.type === "markdown") {
2430
+ const downgraded = downgradeMarkdown(item.content);
2431
+ return downgraded ? {
2432
+ ...item,
2433
+ content: downgraded
2434
+ } : item;
2435
+ }
2436
+ if (item.type === "group") {
2437
+ let changed = false;
2438
+ const items = item.items.map((member) => {
2439
+ if (member.content.type !== "markdown") return member;
2440
+ const downgraded = downgradeMarkdown(member.content);
2441
+ if (!downgraded) return member;
2442
+ changed = true;
2443
+ return {
2444
+ ...member,
2445
+ content: downgraded
2446
+ };
2447
+ });
2448
+ return changed ? {
2449
+ ...item,
2450
+ items
2451
+ } : item;
2452
+ }
2453
+ return item;
2454
+ };
2455
+ async function resendDrainedStream(send, item, source, platform, unsupported) {
2456
+ platformLog$1.info(`${platform} does not support streaming text; waiting for the stream to finish to send the full text as one message.`, {
2457
+ "spectrum.provider": platform,
2458
+ "spectrum.stream_text.fallback": true
2459
+ });
2460
+ let full;
2461
+ try {
2462
+ full = await drainStreamText(source);
2463
+ } catch (drainErr) {
2464
+ if (drainErr instanceof StreamConsumedError) throw unsupported;
2465
+ throw drainErr;
2466
+ }
2467
+ if (!full) throw unsupported;
2468
+ return await sendWithFallbacks(send, replaceStreamText(item, source, full), platform);
2469
+ }
2470
+ /**
2471
+ * Dispatch `content` to the provider, downgrading on platforms that reject it
2472
+ * with `UnsupportedError`:
2473
+ *
2474
+ * - `streamText` (top-level or inside `reply`/`edit`): wait for the stream to
2475
+ * finish and re-send the accumulated text — as `markdown` content for a
2476
+ * markdown-formatted stream, plain `text` otherwise — so `streamText`
2477
+ * works everywhere, just without live updates.
2478
+ * - `markdown` (top-level, inside `reply`/`edit`, or a `group` item): re-send
2479
+ * with each markdown occurrence rendered to readable plain text — so
2480
+ * `markdown` works everywhere, just without styling.
2481
+ *
2482
+ * The two chain rather than compete: a drained markdown stream re-enters this
2483
+ * function as `markdown` content and can downgrade once more to plain text.
2484
+ * Rethrows the original error when no fallback applies; an `UnsupportedError`
2485
+ * from the final fallback send itself propagates too. Both land in the
2486
+ * caller's warn-and-skip handling.
2487
+ */
2488
+ async function sendWithFallbacks(send, item, platform) {
2489
+ try {
2490
+ return await send(item);
2491
+ } catch (err) {
2492
+ if (!(err instanceof UnsupportedError)) throw err;
2493
+ const source = findStreamText(item);
2494
+ if (source) return await resendDrainedStream(send, item, source, platform, err);
2495
+ const downgraded = replaceMarkdown(item);
2496
+ if (downgraded === item) throw err;
2497
+ platformLog$1.info(`${platform} does not support markdown; sending the content as plain text instead.`, {
2498
+ "spectrum.provider": platform,
2499
+ "spectrum.markdown.fallback": true
2500
+ });
2501
+ return await send(downgraded);
2502
+ }
2503
+ }
2504
+ const providerMessageCoreKeys = new Set([
2505
+ "content",
2506
+ "direction",
2507
+ "id",
2508
+ "sender",
2509
+ "space",
2510
+ "timestamp"
2511
+ ]);
2512
+ const extractExtras = (raw, definition) => {
2513
+ const entries = Object.entries(raw).filter(([key]) => !providerMessageCoreKeys.has(key));
2514
+ const extra = Object.fromEntries(entries);
2515
+ return definition.message?.schema ? definition.message.schema.parse(extra) : extra;
2516
+ };
2517
+ const rawDirection = (raw) => raw.direction === "inbound" || raw.direction === "outbound" ? raw.direction : void 0;
2518
+ /**
2519
+ * Wrap a raw provider message record (and any nested raw targets/items inside
2520
+ * its content) into a fully-built `Message`. The same path serves inbound
2521
+ * (`messages`, `actions.getMessage`) and outbound (`send`) flows — the only
2522
+ * difference is `direction`, which decides whether the resulting Message
2523
+ * exposes inbound (`react`/`reply`) or outbound (`edit`) affordances. A raw
2524
+ * record can carry its own `direction` when the provider knows better than the
2525
+ * wrapping context, which matters for inbound reactions targeting outbound
2526
+ * messages.
2527
+ * Recursion through `wrapNestedContent` handles reaction targets and group
2528
+ * items, which providers return as nested raw records.
2529
+ */
2530
+ function wrapProviderMessage(raw, ctx, direction) {
2531
+ const effectiveDirection = rawDirection(raw) ?? direction;
2532
+ const wrappedContent = wrapNestedContent(raw.content, ctx, effectiveDirection);
2533
+ const base = {
2534
+ id: raw.id,
2535
+ content: wrappedContent,
2536
+ timestamp: raw.timestamp ?? /* @__PURE__ */ new Date(),
2537
+ extras: extractExtras(raw, ctx.definition),
2538
+ spaceRef: ctx.spaceRef,
2539
+ space: ctx.space,
2540
+ definition: ctx.definition,
2541
+ client: ctx.client,
2542
+ config: ctx.config,
2543
+ store: ctx.store
2544
+ };
2545
+ if (effectiveDirection === "inbound") return buildMessage({
2546
+ ...base,
2547
+ sender: raw.sender,
2548
+ direction: "inbound"
2549
+ });
2550
+ return buildMessage({
2551
+ ...base,
2552
+ sender: raw.sender,
2553
+ direction: "outbound"
2554
+ });
2555
+ }
2556
+ const wrapNestedContent = (content, ctx, direction) => {
2557
+ if (content.type === "reaction") {
2558
+ const target = content.target;
2559
+ if (isRawProviderRecord(target)) return {
2560
+ ...content,
2561
+ target: wrapProviderMessage(target, ctx, "inbound")
2562
+ };
2563
+ return content;
2564
+ }
2565
+ if (content.type === "edit") {
2566
+ const target = content.target;
2567
+ if (isRawProviderRecord(target)) return {
2568
+ ...content,
2569
+ target: wrapProviderMessage(target, ctx, "outbound")
2570
+ };
2571
+ return content;
2572
+ }
2573
+ if (content.type === "group") {
2574
+ const items = content.items.map((item) => {
2575
+ const raw = item;
2576
+ if (!isRawProviderRecord(raw)) return item;
2577
+ return direction === "inbound" ? wrapProviderMessage(raw, ctx, "inbound") : wrapProviderMessage(raw, ctx, "outbound");
2578
+ });
2579
+ return {
2580
+ ...content,
2581
+ items
2582
+ };
2583
+ }
2584
+ return content;
2585
+ };
2586
+ const isRawProviderRecord = (v) => {
2587
+ if (typeof v !== "object" || v === null) return false;
2588
+ const record = v;
2589
+ return "id" in record && "content" in record && typeof record.react !== "function" && typeof record.reply !== "function";
2590
+ };
2591
+ function buildSpace(params) {
2592
+ const { spaceRef, extras, actionCtx, definition, client, config, store } = params;
2593
+ let space;
2594
+ async function dispatchSend(item) {
2595
+ return withSpan("spectrum.message.send", {
2596
+ "spectrum.provider": definition.name,
2597
+ "spectrum.space.id": spaceRef.id,
2598
+ "spectrum.message.fire_and_forget": isFireAndForget(item),
2599
+ ...contentAttrs(item)
2600
+ }, async () => {
2601
+ const platformError = unsupportedPlatformContentError(item, definition.name);
2602
+ if (platformError) {
2603
+ warnUnsupported(platformError, definition.name);
2604
+ return;
2605
+ }
2606
+ const providerSend = async (content) => await definition.send({
2607
+ ...actionCtx,
2608
+ content
2609
+ });
2610
+ let raw;
2611
+ try {
2612
+ raw = await sendWithFallbacks(providerSend, item, definition.name);
2613
+ } catch (err) {
2614
+ if (err instanceof UnsupportedError) {
2615
+ warnUnsupported(err, definition.name);
2616
+ return;
2617
+ }
2618
+ throw err;
2619
+ }
2620
+ if (!raw?.id) {
2621
+ if (isFireAndForget(item)) return;
2622
+ throw new Error(`Platform "${definition.name}" send did not return a message id`);
2623
+ }
2624
+ return wrapProviderMessage(raw, {
2625
+ client,
2626
+ config,
2627
+ definition,
2628
+ space,
2629
+ spaceRef,
2630
+ store
2631
+ }, "outbound");
2632
+ });
2633
+ }
2634
+ async function sendImpl(...content) {
2635
+ const resolved = await resolveContents(content);
2636
+ const results = [];
2637
+ for (const item of resolved) {
2638
+ const sent = await dispatchSend(item);
2639
+ if (sent) results.push(sent);
2640
+ }
2641
+ if (content.length === 1) return results[0];
2642
+ return results;
2643
+ }
2644
+ async function getMessageImpl(id) {
2645
+ const getMessage = definition.actions?.getMessage;
2646
+ if (!getMessage) throw UnsupportedError.action("getMessage", definition.name);
2647
+ return withSpan("spectrum.message.get", {
2648
+ "spectrum.provider": definition.name,
2649
+ "spectrum.space.id": spaceRef.id,
2650
+ "spectrum.message.id": id
2651
+ }, async () => {
2652
+ const raw = await getMessage({
2653
+ client,
2654
+ config,
2655
+ store
2656
+ }, spaceRef, id);
2657
+ if (!raw) return;
2658
+ return wrapProviderMessage(raw, {
2659
+ client,
2660
+ config,
2661
+ definition,
2662
+ space,
2663
+ spaceRef,
2664
+ store
2665
+ }, "inbound");
2666
+ });
2667
+ }
2668
+ const platformActions = {};
2669
+ const declaredActions = definition.space.actions;
2670
+ if (declaredActions) for (const [name, factory] of Object.entries(declaredActions)) {
2671
+ if (RESERVED_SPACE_KEYS.has(name)) {
2672
+ warnReservedAction("space", name, definition.name);
2673
+ continue;
2674
+ }
2675
+ platformActions[name] = async (...args) => {
2676
+ await factory(space, ...args);
2677
+ };
2678
+ }
2679
+ space = {
2680
+ ...extras,
2681
+ ...spaceRef,
2682
+ ...platformActions,
2683
+ send: sendImpl,
2684
+ edit: async (message, newContent) => {
2685
+ await space.send(edit(newContent, message));
2686
+ },
2687
+ unsend: async (message) => {
2688
+ await space.send(unsend(message));
2689
+ },
2690
+ read: async (message) => {
2691
+ await space.send(read(message));
2692
+ },
2693
+ getMessage: getMessageImpl,
2694
+ rename: async (displayName) => {
2695
+ await space.send(rename(displayName));
2696
+ },
2697
+ avatar: (async (input, options) => {
2698
+ if (typeof input === "string" || input instanceof URL) {
2699
+ await space.send(avatar(input, options));
2700
+ return;
2701
+ }
2702
+ if (!options?.mimeType) throw new Error("space.avatar(Buffer) requires options.mimeType — pass { mimeType: '...' }");
2703
+ await space.send(avatar(input, { mimeType: options.mimeType }));
2704
+ }),
2705
+ startTyping: async () => {
2706
+ await space.send(typing("start"));
2707
+ },
2708
+ stopTyping: async () => {
2709
+ await space.send(typing("stop"));
2710
+ },
2711
+ responding: async (fn) => {
2712
+ await space.send(typing("start"));
2713
+ try {
2714
+ return await fn();
2715
+ } finally {
2716
+ await space.send(typing("stop")).catch(() => {});
2717
+ }
2718
+ }
2719
+ };
2720
+ return space;
2721
+ }
2722
+ function buildMessage(params) {
2723
+ const { definition, space } = params;
2724
+ let self;
2725
+ const requireBuiltMessage = (action) => {
2726
+ if (!self) throw new Error(`${action}() called before message construction completed (internal bug)`);
2727
+ return self;
2728
+ };
2729
+ const react = async (emoji) => {
2730
+ const target = requireBuiltMessage("react");
2731
+ return await space.send(reaction(emoji, target));
2732
+ };
2733
+ async function reply$1(...content) {
2734
+ const target = requireBuiltMessage("reply");
2735
+ const wrapped = content.map((c) => reply(c, target));
2736
+ return space.send(...wrapped);
2737
+ }
2738
+ const edit$1 = async (newContent) => {
2739
+ const target = requireBuiltMessage("edit");
2740
+ if (target.direction !== "outbound") throw new Error(`cannot edit message ${target.id}: only outbound messages can be edited (direction: "${target.direction}")`);
2741
+ await space.send(edit(newContent, target));
2742
+ };
2743
+ const unsend$1 = async () => {
2744
+ const target = requireBuiltMessage("unsend");
2745
+ if (target.direction !== "outbound") throw new Error(`cannot unsend message ${target.id}: only outbound messages can be unsent (direction: "${target.direction}")`);
2746
+ await space.send(unsend(target));
2747
+ };
2748
+ const read$1 = async () => {
2749
+ const target = requireBuiltMessage("read");
2750
+ if (target.direction !== "inbound") throw new Error(`cannot mark message ${target.id} as read: only inbound messages can be marked read (direction: "${target.direction}")`);
2751
+ await space.send(read(target));
2752
+ };
2753
+ const buildSenderWithPlatform = () => {
2754
+ if (params.sender === void 0) return;
2755
+ if (params.direction === "outbound") return {
2756
+ ...params.sender,
2757
+ __platform: definition.name,
2758
+ kind: "agent"
2759
+ };
2760
+ return {
2761
+ ...params.sender,
2762
+ __platform: definition.name
2763
+ };
2764
+ };
2765
+ const senderWithPlatform = buildSenderWithPlatform();
2766
+ const messagePlatformActions = {};
2767
+ const declaredMessageActions = definition.message?.actions;
2768
+ if (declaredMessageActions) for (const [name, factory] of Object.entries(declaredMessageActions)) {
2769
+ if (RESERVED_MESSAGE_KEYS.has(name)) {
2770
+ warnReservedAction("message", name, definition.name);
2771
+ continue;
2772
+ }
2773
+ messagePlatformActions[name] = async (...args) => {
2774
+ await factory(requireBuiltMessage(name), ...args);
2775
+ };
2776
+ }
2777
+ const message = {
2778
+ ...params.extras,
2779
+ ...messagePlatformActions,
2780
+ id: params.id,
2781
+ content: params.content,
2782
+ direction: params.direction,
2783
+ platform: definition.name,
2784
+ react,
2785
+ read: read$1,
2786
+ reply: reply$1,
2787
+ edit: edit$1,
2788
+ unsend: unsend$1,
2789
+ sender: senderWithPlatform,
2790
+ space,
2791
+ timestamp: params.timestamp
2792
+ };
2793
+ self = message;
2794
+ return message;
2795
+ }
2796
+ //#endregion
2797
+ //#region src/platform/define.ts
2798
+ const platformLog = createLogger("spectrum.platform");
2799
+ function buildInstanceActions(platformName, declared, reservedKeys, buildCtx) {
2800
+ const out = {};
2801
+ for (const key of PLATFORM_WISE_ACTION_KEYS) {
2802
+ const override = declared?.[key];
2803
+ if (override && typeof override === "function") out[key] = (...args) => override(buildCtx(), ...args);
2804
+ else out[key] = () => {
2805
+ throw UnsupportedError.action(key, platformName);
2806
+ };
2807
+ }
2808
+ if (!declared) return out;
2809
+ for (const [name, factory] of Object.entries(declared)) {
2810
+ if (PLATFORM_WISE_ACTION_KEYS.has(name)) continue;
2811
+ if (reservedKeys.has(name)) {
2812
+ warnReservedAction("instance", name, platformName);
2813
+ continue;
2814
+ }
2815
+ if (typeof factory !== "function") continue;
2816
+ out[name] = (...args) => factory(buildCtx(), ...args);
2817
+ }
2818
+ return out;
2819
+ }
2820
+ function createPlatformInstance(def, runtime) {
2821
+ const resolveUserID = async (userID) => {
2822
+ return {
2823
+ ...await def.user.resolve({
2824
+ input: { userID },
2825
+ client: runtime.client,
2826
+ config: runtime.config,
2827
+ store: runtime.store
2828
+ }),
2829
+ __platform: def.name
2830
+ };
2831
+ };
2832
+ const providerCtx = () => ({
2833
+ client: runtime.client,
2834
+ config: runtime.config,
2835
+ store: runtime.store
2836
+ });
2837
+ const parseSpaceParams = (params) => params !== void 0 && def.space.params ? def.space.params.parse(params) : params;
2838
+ const finalizeSpace = (resolved) => {
2839
+ const parsedSpace = def.space.schema ? def.space.schema.parse(resolved) : resolved;
2840
+ const spaceRef = {
2841
+ ...parsedSpace,
2842
+ id: parsedSpace.id,
2843
+ __platform: def.name
2844
+ };
2845
+ return buildSpace({
2846
+ spaceRef,
2847
+ extras: parsedSpace,
2848
+ actionCtx: {
2849
+ space: spaceRef,
2850
+ ...providerCtx()
2851
+ },
2852
+ definition: def,
2853
+ client: runtime.client,
2854
+ config: runtime.config,
2855
+ store: runtime.store
2856
+ });
2857
+ };
2858
+ const base = {
2859
+ async user(userID) {
2860
+ return await resolveUserID(userID);
2861
+ },
2862
+ space: {
2863
+ create: async (users, params) => {
2864
+ const userList = Array.isArray(users) ? users : [users];
2865
+ const first = userList.length === 1 ? userList[0] : void 0;
2866
+ const single = typeof first === "string" ? classifyIdentifier(first) : void 0;
2867
+ const kind = userList.length > 1 ? "group" : single?.kind ?? "unknown";
2868
+ return await withSpan("spectrum.space.create", {
2869
+ "spectrum.provider": def.name,
2870
+ "spectrum.space.user_count": userList.length,
2871
+ "spectrum.space.identifier_kind": kind,
2872
+ "spectrum.space.identifier": kind === "unknown" ? void 0 : single?.identifier
2873
+ }, async () => {
2874
+ const resolvedUsers = await Promise.all(userList.map((u) => typeof u === "string" ? resolveUserID(u) : u));
2875
+ return finalizeSpace(await def.space.create({
2876
+ input: {
2877
+ users: resolvedUsers,
2878
+ params: parseSpaceParams(params)
2879
+ },
2880
+ ...providerCtx()
2881
+ }));
2882
+ });
2883
+ },
2884
+ get: async (id, params) => await withSpan("spectrum.space.get", {
2885
+ "spectrum.provider": def.name,
2886
+ "spectrum.space.id": id
2887
+ }, async () => {
2888
+ const parsedParams = parseSpaceParams(params);
2889
+ if (def.space.get) return finalizeSpace(await def.space.get({
2890
+ input: {
2891
+ id,
2892
+ params: parsedParams
2893
+ },
2894
+ ...providerCtx()
2895
+ }));
2896
+ const candidate = { id };
2897
+ if (def.space.schema) {
2898
+ const parsed = def.space.schema.safeParse(candidate);
2899
+ if (!parsed.success) throw new Error(`Platform "${def.name}" cannot construct a space from an id alone — its space schema requires more fields. Implement \`space.get\` in the "${def.name}" provider definition.`, { cause: parsed.error });
2900
+ }
2901
+ return finalizeSpace(candidate);
2902
+ })
2903
+ }
2904
+ };
2905
+ const eventProperties = {};
2906
+ const customEvents = def.events ?? {};
2907
+ for (const eventName of Object.keys(customEvents)) {
2908
+ const declared = customEvents[eventName];
2909
+ if (typeof declared === "function") {
2910
+ eventProperties[eventName] = declared({
2911
+ client: runtime.client,
2912
+ config: runtime.config,
2913
+ projectConfig: runtime.projectConfig,
2914
+ store: runtime.store
2915
+ });
2916
+ continue;
2917
+ }
2918
+ const fusorEvents = runtime.subscribeEvent?.(eventName);
2919
+ if (fusorEvents) eventProperties[eventName] = fusorEvents;
2920
+ }
2921
+ let messagesIterable;
2922
+ Object.defineProperty(base, "messages", {
2923
+ enumerable: true,
2924
+ get() {
2925
+ messagesIterable ??= runtime.subscribeMessages();
2926
+ return messagesIterable;
2927
+ }
2928
+ });
2929
+ const instanceActions = buildInstanceActions(def.name, def.actions, new Set([
2930
+ "user",
2931
+ "space",
2932
+ "messages",
2933
+ ...Object.keys(customEvents)
2934
+ ]), () => ({
2935
+ client: runtime.client,
2936
+ config: runtime.config,
2937
+ store: runtime.store
2938
+ }));
2939
+ return Object.assign(base, instanceActions, eventProperties);
2940
+ }
2941
+ function definePlatform(name, rawDef) {
2942
+ const def = rawDef;
2943
+ const fullDef = {
2944
+ ...def,
2945
+ name
2946
+ };
2947
+ const platformCache = /* @__PURE__ */ new WeakMap();
2948
+ const narrowSpectrum = (spectrum) => {
2949
+ const cached = platformCache.get(spectrum);
2950
+ if (cached) return cached;
2951
+ const runtime = spectrum.__internal.platforms.get(name);
2952
+ if (!runtime) throw new Error(`Platform "${name}" is not registered`);
2953
+ const instance = createPlatformInstance(fullDef, runtime);
2954
+ platformCache.set(spectrum, instance);
2955
+ return instance;
2956
+ };
2957
+ const narrowSpace = (input) => {
2958
+ if (input.__platform !== name) platformLog.warn("space platform mismatch; narrowing skipped", {
2959
+ "spectrum.platform.expected": name,
2960
+ "spectrum.platform.actual": input.__platform
2961
+ });
2962
+ return input;
2963
+ };
2964
+ const narrowMessage = (input) => {
2965
+ if (input.platform !== name) platformLog.warn("message platform mismatch; narrowing skipped", {
2966
+ "spectrum.platform.expected": name,
2967
+ "spectrum.platform.actual": input.platform
2968
+ });
2969
+ return input;
2970
+ };
2971
+ const narrower = ((input) => {
2972
+ if ("__providers" in input && "__internal" in input) return narrowSpectrum(input);
2973
+ if ("__platform" in input && "send" in input) return narrowSpace(input);
2974
+ if ("platform" in input && "sender" in input && "space" in input) return narrowMessage(input);
2975
+ throw new Error("Invalid input to platform narrowing function");
2976
+ });
2977
+ narrower.config = (config) => {
2978
+ return {
2979
+ __tag: "PlatformProviderConfig",
2980
+ __def: void 0,
2981
+ __name: name,
2982
+ config: config ?? {},
2983
+ __definition: fullDef
2984
+ };
2985
+ };
2986
+ narrower.is = ((input) => {
2987
+ if (typeof input !== "object" || input === null) return false;
2988
+ if ("__platform" in input) return input.__platform === name;
2989
+ if ("platform" in input) return input.platform === name;
2990
+ return false;
2991
+ });
2992
+ if (def.static) Object.assign(narrower, def.static);
2993
+ return narrower;
2994
+ }
2995
+ //#endregion
2996
+ //#region src/build-env.ts
2997
+ const SPECTRUM_SDK_VERSION = "5.0.0";
2998
+ //#endregion
2999
+ //#region src/utils/provider-packages.ts
3000
+ const OFFICIAL_PROVIDER_PACKAGES = {
3001
+ imessage: "@spectrum-ts/imessage",
3002
+ slack: "@spectrum-ts/slack",
3003
+ telegram: "@spectrum-ts/telegram",
3004
+ terminal: "@spectrum-ts/terminal",
3005
+ "whatsapp-business": "@spectrum-ts/whatsapp-business"
3006
+ };
3007
+ const SEPARATORS = /[\s_]+/g;
3008
+ const normalizePlatformKey = (platform) => platform.trim().toLowerCase().replace(SEPARATORS, "-");
3009
+ const officialProviderPackage = (platform) => OFFICIAL_PROVIDER_PACKAGES[normalizePlatformKey(platform)];
3010
+ const installCommand = (pkg) => process.versions.bun ? `bun add ${pkg}` : `npm install ${pkg}`;
3011
+ /**
3012
+ * One-line install hint for a platform provided by an official package, or
3013
+ * undefined for unknown/custom platforms. Appended to "no handler" style
3014
+ * warnings — advisory only, callers must not change behavior based on it.
3015
+ */
3016
+ const officialProviderInstallHint = (platform) => {
3017
+ const pkg = officialProviderPackage(platform);
3018
+ if (!pkg) return;
3019
+ return `the "${platform}" platform is provided by the optional package ${pkg} — install it (\`${installCommand(pkg)}\`) and pass it to Spectrum({ providers: [...] })`;
3020
+ };
3021
+ //#endregion
3022
+ //#region src/utils/cloud.ts
3023
+ const SPECTRUM_CLOUD_URL = process.env.SPECTRUM_CLOUD_URL ?? "https://spectrum.photon.codes";
3024
+ var SpectrumCloudError = class extends Error {
3025
+ status;
3026
+ code;
3027
+ constructor(status, code, message) {
3028
+ super(message);
3029
+ this.name = "SpectrumCloudError";
3030
+ this.status = status;
3031
+ this.code = code;
3032
+ }
3033
+ };
3034
+ const request = async (path, init) => {
3035
+ const response = await fetch(`${SPECTRUM_CLOUD_URL}${path}`, init);
3036
+ if (!response.ok) {
3037
+ const body = await response.text().catch(() => "");
3038
+ try {
3039
+ const parsed = JSON.parse(body);
3040
+ throw new SpectrumCloudError(response.status, parsed.code, parsed.message);
3041
+ } catch (error) {
3042
+ if (error instanceof SpectrumCloudError) throw error;
3043
+ throw new SpectrumCloudError(response.status, "UNKNOWN", body || response.statusText);
3044
+ }
3045
+ }
3046
+ const json = await response.json();
3047
+ if (!json.succeed) throw new SpectrumCloudError(response.status, "UNKNOWN", "Server returned succeed=false");
3048
+ return json.data;
3049
+ };
3050
+ const basicAuth = (projectId, projectSecret) => `Basic ${btoa(`${projectId}:${projectSecret}`)}`;
3051
+ const cloud = {
3052
+ getProject: (projectId, projectSecret) => request(`/projects/${projectId}/`, { headers: { Authorization: basicAuth(projectId, projectSecret) } }),
3053
+ getSubscription: (projectId) => request(`/projects/${projectId}/billing/subscription`),
3054
+ issueImessageTokens: (projectId, projectSecret) => request(`/projects/${projectId}/imessage/tokens`, {
3055
+ method: "POST",
3056
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
3057
+ }),
3058
+ getImessageInfo: (projectId) => request(`/projects/${projectId}/imessage/`),
3059
+ issueWhatsappBusinessTokens: (projectId, projectSecret) => request(`/projects/${projectId}/whatsapp-business/tokens`, {
3060
+ method: "POST",
3061
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
3062
+ }),
3063
+ issueSlackTokens: (projectId, projectSecret) => request(`/projects/${projectId}/slack/tokens`, {
3064
+ method: "POST",
3065
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
3066
+ }),
3067
+ issueFusorToken: (projectId, projectSecret) => request(`/projects/${projectId}/fusor/token`, {
3068
+ method: "POST",
3069
+ headers: { Authorization: basicAuth(projectId, projectSecret) }
3070
+ }),
3071
+ getPlatforms: (projectId) => request(`/projects/${projectId}/platforms/`),
3072
+ togglePlatform: (projectId, projectSecret, platform, enabled) => request(`/projects/${projectId}/platforms/`, {
3073
+ method: "PATCH",
3074
+ headers: {
3075
+ Authorization: basicAuth(projectId, projectSecret),
3076
+ "Content-Type": "application/json"
3077
+ },
3078
+ body: JSON.stringify({
3079
+ platform,
3080
+ enabled
3081
+ })
3082
+ })
3083
+ };
3084
+ //#endregion
3085
+ //#region src/fusor/auth.ts
3086
+ const log$2 = createLogger("spectrum.fusor.auth");
3087
+ const RENEWAL_RATIO = .8;
3088
+ const EXPIRY_BUFFER_MS = 3e4;
3089
+ const RETRY_DELAY_MS = 3e4;
3090
+ /**
3091
+ * Single-token provider for the fusor stream. Mirrors the renewal cadence
3092
+ * of the slack provider package's auth but without per-team bookkeeping —
3093
+ * fusor issues one bearer JWT per project.
3094
+ */
3095
+ function createFusorTokenProvider(projectId, projectSecret) {
3096
+ return (async () => {
3097
+ let tokenData = await cloud.issueFusorToken(projectId, projectSecret);
3098
+ let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
3099
+ let disposed = false;
3100
+ let renewalTimer;
3101
+ let refreshFailures = 0;
3102
+ const clearRenewalTimer = () => {
3103
+ if (renewalTimer !== void 0) {
3104
+ clearTimeout(renewalTimer);
3105
+ renewalTimer = void 0;
3106
+ }
3107
+ };
3108
+ const refresh = async () => {
3109
+ tokenData = await cloud.issueFusorToken(projectId, projectSecret);
3110
+ tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
3111
+ };
3112
+ const onRefreshSuccess = () => {
3113
+ if (refreshFailures > 0) {
3114
+ log$2.info("fusor token refresh recovered", { "spectrum.fusor.auth.attempt": refreshFailures });
3115
+ refreshFailures = 0;
3116
+ }
3117
+ };
3118
+ const onRefreshFailure = (error) => {
3119
+ refreshFailures += 1;
3120
+ log$2.warn("fusor token refresh failed; retrying", {
3121
+ "spectrum.fusor.auth.attempt": refreshFailures,
3122
+ "spectrum.fusor.auth.retry_in_ms": RETRY_DELAY_MS,
3123
+ ...errorAttrs(error)
3124
+ }, error);
3125
+ };
3126
+ const scheduleRetry = () => {
3127
+ if (disposed) return;
3128
+ clearRenewalTimer();
3129
+ renewalTimer = setTimeout(async () => {
3130
+ if (disposed) return;
3131
+ try {
3132
+ await refresh();
3133
+ onRefreshSuccess();
3134
+ scheduleRenewal();
3135
+ } catch (retryErr) {
3136
+ onRefreshFailure(retryErr);
3137
+ scheduleRetry();
3138
+ }
3139
+ }, RETRY_DELAY_MS);
3140
+ renewalTimer?.unref?.();
3141
+ };
3142
+ const scheduleRenewal = () => {
3143
+ if (disposed) return;
3144
+ clearRenewalTimer();
3145
+ const ttlMs = tokenData.expiresIn * 1e3;
3146
+ const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
3147
+ renewalTimer = setTimeout(async () => {
3148
+ try {
3149
+ await refresh();
3150
+ onRefreshSuccess();
3151
+ scheduleRenewal();
3152
+ } catch (err) {
3153
+ onRefreshFailure(err);
3154
+ scheduleRetry();
3155
+ }
3156
+ }, renewInMs);
3157
+ renewalTimer?.unref?.();
3158
+ };
3159
+ scheduleRenewal();
3160
+ return {
3161
+ async getToken() {
3162
+ if (Date.now() >= tokenExpiresAt - EXPIRY_BUFFER_MS) {
3163
+ await refresh();
3164
+ onRefreshSuccess();
3165
+ scheduleRenewal();
3166
+ }
3167
+ return tokenData.token;
3168
+ },
3169
+ invalidate() {
3170
+ tokenExpiresAt = 0;
3171
+ },
3172
+ async dispose() {
3173
+ disposed = true;
3174
+ clearRenewalTimer();
3175
+ }
3176
+ };
3177
+ })();
3178
+ }
3179
+ //#endregion
3180
+ //#region src/fusor/parse.ts
3181
+ const CR = 13;
3182
+ const LF = 10;
3183
+ function findHeaderEnd(bytes) {
3184
+ for (let i = 0; i + 3 < bytes.length; i++) if (bytes[i] === CR && bytes[i + 1] === LF && bytes[i + 2] === CR && bytes[i + 3] === LF) return i;
3185
+ return -1;
3186
+ }
3187
+ /**
3188
+ * Parses an HTTP/1.1 wire-format request out of `raw_request` from
3189
+ * `RawInboundEvent`. Headers are lowercased. Multiple header values with the
3190
+ * same name are joined with ", " (RFC 7230 §3.2.2).
3191
+ */
3192
+ function parseHttpRequest(bytes) {
3193
+ const headerEnd = findHeaderEnd(bytes);
3194
+ if (headerEnd < 0) throw new Error("fusor: raw_request missing CRLFCRLF header terminator");
3195
+ const headerText = new TextDecoder("utf-8").decode(bytes.subarray(0, headerEnd));
3196
+ const rawBody = bytes.subarray(headerEnd + 4);
3197
+ const lines = headerText.split("\r\n");
3198
+ const requestLine = lines[0];
3199
+ if (!requestLine) throw new Error("fusor: raw_request missing request line");
3200
+ const firstSpace = requestLine.indexOf(" ");
3201
+ const lastSpace = requestLine.lastIndexOf(" ");
3202
+ if (firstSpace < 0 || lastSpace <= firstSpace) throw new Error(`fusor: malformed request line: ${requestLine}`);
3203
+ const method = requestLine.slice(0, firstSpace);
3204
+ const path = requestLine.slice(firstSpace + 1, lastSpace);
3205
+ const headers = {};
3206
+ for (let i = 1; i < lines.length; i++) {
3207
+ const line = lines[i];
3208
+ if (!line) continue;
3209
+ const colon = line.indexOf(":");
3210
+ if (colon < 0) continue;
3211
+ const key = line.slice(0, colon).trim().toLowerCase();
3212
+ const value = line.slice(colon + 1).trim();
3213
+ if (!key) continue;
3214
+ const existing = headers[key];
3215
+ headers[key] = existing ? `${existing}, ${value}` : value;
3216
+ }
3217
+ return {
3218
+ method,
3219
+ path,
3220
+ headers,
3221
+ rawBody
3222
+ };
3223
+ }
3224
+ //#endregion
3225
+ //#region src/fusor/websocket.ts
3226
+ const log$1 = createLogger("spectrum.fusor.ws");
3227
+ const FUSOR_WS_SUBPROTOCOL = "fusor.v1.json";
3228
+ const STALENESS_GRACE_MS = 5e3;
3229
+ var FusorWsError = class extends Error {
3230
+ closeCode;
3231
+ errorCode;
3232
+ constructor(message, closeCode, errorCode) {
3233
+ super(message);
3234
+ this.name = "FusorWsError";
3235
+ this.closeCode = closeCode;
3236
+ this.errorCode = errorCode;
3237
+ }
3238
+ };
3239
+ function isWsAuthError(error) {
3240
+ return error instanceof FusorWsError && (error.closeCode === 4401 || error.errorCode === "unauthenticated");
3241
+ }
3242
+ function decodeBase64(value) {
3243
+ if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64"));
3244
+ return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
3245
+ }
3246
+ function encodeBase64(bytes) {
3247
+ if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
3248
+ const chunkSize = 32768;
3249
+ const parts = [];
3250
+ for (let i = 0; i < bytes.length; i += chunkSize) parts.push(String.fromCharCode(...bytes.subarray(i, i + chunkSize)));
3251
+ return btoa(parts.join(""));
3252
+ }
3253
+ function toRawInboundEvent(frame) {
3254
+ const e = frame.event;
3255
+ return {
3256
+ eventId: e.eventId,
3257
+ projectId: e.projectId,
3258
+ platform: e.platform,
3259
+ receivedAt: e.receivedAt ? new Date(e.receivedAt) : void 0,
3260
+ sourceId: e.sourceId ?? "",
3261
+ prevSubjectSeq: e.prevSubjectSeq ?? 0,
3262
+ rawRequest: decodeBase64(e.rawRequest)
3263
+ };
3264
+ }
3265
+ function runFusorWsSession(options) {
3266
+ const WebSocketCtor = globalThis.WebSocket;
3267
+ if (typeof WebSocketCtor !== "function") throw new FusorWsError("global WebSocket is not available in this runtime — the fusor websocket transport needs Bun, Node >= 22, or a browser/worker environment");
3268
+ const wsOpen = WebSocketCtor.OPEN;
3269
+ const ws = new WebSocketCtor(options.url, [FUSOR_WS_SUBPROTOCOL]);
3270
+ let settled = false;
3271
+ let closedByUs = false;
3272
+ let pendingError = null;
3273
+ let stalenessBudgetMs = 65e3;
3274
+ let watchdog;
3275
+ let tail = Promise.resolve();
3276
+ let resolveDone;
3277
+ let rejectDone;
3278
+ const done = new Promise((resolve, reject) => {
3279
+ resolveDone = resolve;
3280
+ rejectDone = reject;
3281
+ });
3282
+ const settle = (error) => {
3283
+ if (settled) return;
3284
+ settled = true;
3285
+ if (watchdog) {
3286
+ clearTimeout(watchdog);
3287
+ watchdog = void 0;
3288
+ }
3289
+ if (error) rejectDone(error);
3290
+ else resolveDone();
3291
+ };
3292
+ const armWatchdog = () => {
3293
+ if (settled) return;
3294
+ if (watchdog) clearTimeout(watchdog);
3295
+ watchdog = setTimeout(() => {
3296
+ log$1.warn("fusor ws: no frame within staleness budget; closing", { "spectrum.fusor.ws.staleness_budget_ms": stalenessBudgetMs });
3297
+ settle(new FusorWsError("websocket heartbeat timeout"));
3298
+ try {
3299
+ ws.close();
3300
+ } catch {}
3301
+ }, stalenessBudgetMs);
3302
+ watchdog.unref?.();
3303
+ };
3304
+ const sendReplyFor = (eventId) => (reply) => {
3305
+ if (ws.readyState !== wsOpen) return;
3306
+ ws.send(JSON.stringify({
3307
+ type: "reply",
3308
+ eventId,
3309
+ status: reply.status,
3310
+ headers: reply.headers,
3311
+ ...reply.body.length > 0 && { body: encodeBase64(reply.body) },
3312
+ ...reply.errorReason && { errorReason: reply.errorReason }
3313
+ }));
3314
+ };
3315
+ const handleReadyFrame = (frame) => {
3316
+ const interval = frame.heartbeatIntervalMs;
3317
+ if (typeof interval === "number" && interval > 0) stalenessBudgetMs = 2 * interval + STALENESS_GRACE_MS;
3318
+ log$1.info("fusor ws stream ready", {
3319
+ "spectrum.fusor.ws.project_id": typeof frame.projectId === "string" ? frame.projectId : "",
3320
+ "spectrum.fusor.ws.heartbeat_interval_ms": typeof interval === "number" ? interval : 0
3321
+ });
3322
+ };
3323
+ const handleEventFrame = (frame) => {
3324
+ const eventFrame = frame;
3325
+ let event;
3326
+ try {
3327
+ event = toRawInboundEvent(eventFrame);
3328
+ } catch (error) {
3329
+ log$1.warn("fusor ws: undecodable event frame; skipping", errorAttrs(error), error);
3330
+ return;
3331
+ }
3332
+ const sendReply = eventFrame.replyExpected ? sendReplyFor(event.eventId) : void 0;
3333
+ tail = tail.then(() => options.onEvent(event, sendReply)).catch((error) => {
3334
+ log$1.warn("fusor ws: event handler failed", {
3335
+ "spectrum.fusor.ws.event_id": event.eventId,
3336
+ ...errorAttrs(error)
3337
+ }, error);
3338
+ });
3339
+ };
3340
+ const handleErrorFrame = (frame) => {
3341
+ const code = typeof frame.code === "string" ? frame.code : "unknown";
3342
+ const message = typeof frame.message === "string" ? frame.message : "server error";
3343
+ const reason = typeof frame.reason === "string" ? frame.reason : void 0;
3344
+ if (frame.fatal === true) pendingError = {
3345
+ code,
3346
+ message,
3347
+ reason
3348
+ };
3349
+ else log$1.warn("fusor ws: server notice", {
3350
+ "spectrum.fusor.ws.notice_code": code,
3351
+ "spectrum.fusor.ws.notice_message": message,
3352
+ "spectrum.fusor.ws.notice_reason": reason
3353
+ });
3354
+ };
3355
+ const handleFrame = (raw) => {
3356
+ if (typeof raw !== "string") return;
3357
+ let frame;
3358
+ try {
3359
+ frame = JSON.parse(raw);
3360
+ } catch {
3361
+ log$1.warn("fusor ws: unparseable server frame; ignoring");
3362
+ return;
3363
+ }
3364
+ switch (frame.type) {
3365
+ case "ready":
3366
+ handleReadyFrame(frame);
3367
+ return;
3368
+ case "event":
3369
+ handleEventFrame(frame);
3370
+ return;
3371
+ case "error":
3372
+ handleErrorFrame(frame);
3373
+ return;
3374
+ default: return;
3375
+ }
3376
+ };
3377
+ ws.onopen = () => {
3378
+ armWatchdog();
3379
+ ws.send(JSON.stringify({
3380
+ type: "init",
3381
+ startSeq: 0,
3382
+ token: options.token
3383
+ }));
3384
+ };
3385
+ ws.onmessage = (messageEvent) => {
3386
+ armWatchdog();
3387
+ handleFrame(messageEvent.data);
3388
+ };
3389
+ ws.onerror = () => {
3390
+ log$1.debug("fusor ws: socket error event");
3391
+ };
3392
+ ws.onclose = (closeEvent) => {
3393
+ if (closedByUs) {
3394
+ settle();
3395
+ return;
3396
+ }
3397
+ const detail = pendingError ? `${pendingError.code}${pendingError.reason ? `:${pendingError.reason}` : ""} — ${pendingError.message}` : closeEvent.reason || "connection closed";
3398
+ settle(new FusorWsError(`fusor websocket closed (${closeEvent.code}): ${detail}`, closeEvent.code, pendingError?.code ?? (closeEvent.reason || void 0)));
3399
+ };
3400
+ armWatchdog();
3401
+ return {
3402
+ done,
3403
+ close() {
3404
+ closedByUs = true;
3405
+ try {
3406
+ ws.close(1e3);
3407
+ } catch {}
3408
+ setTimeout(() => settle(), 2e3).unref?.();
3409
+ }
3410
+ };
3411
+ }
3412
+ //#endregion
3413
+ //#region src/fusor/core.ts
3414
+ const DEFAULT_FUSOR_WS_URL = "wss://fusor-ws.spectrum.photon.codes/v1/subscribe";
3415
+ const RECONNECT_BASE_MS = 1e3;
3416
+ const RECONNECT_MAX_MS = 3e4;
3417
+ const log = createLogger("spectrum.fusor");
3418
+ const errorText = (error) => error instanceof Error ? error.message : String(error);
3419
+ function toReplyBytes(body) {
3420
+ if (body === void 0) return new Uint8Array(0);
3421
+ if (typeof body === "string") return new TextEncoder().encode(body);
3422
+ return body;
3423
+ }
3424
+ function combineReplies(outcomes) {
3425
+ const successes = outcomes.filter((o) => o.ok);
3426
+ if (successes.length === 0) return {
3427
+ eventId: "",
3428
+ errorReason: outcomes[0]?.errorReason ?? "no handler succeeded",
3429
+ status: 0,
3430
+ headers: {},
3431
+ body: new Uint8Array(0)
3432
+ };
3433
+ let status = 0;
3434
+ const headers = {};
3435
+ let body = new Uint8Array(0);
3436
+ for (const outcome of successes) {
3437
+ const reply = outcome.reply;
3438
+ if (!reply) continue;
3439
+ if (reply.status !== void 0 && reply.status > status) status = reply.status;
3440
+ if (reply.headers) for (const [k, v] of Object.entries(reply.headers)) headers[k.toLowerCase()] = v;
3441
+ const candidate = toReplyBytes(reply.body);
3442
+ if (candidate.length > 0) body = candidate;
3443
+ }
3444
+ return {
3445
+ eventId: "",
3446
+ errorReason: "",
3447
+ status,
3448
+ headers,
3449
+ body
3450
+ };
3451
+ }
3452
+ function routeHandlerResult(result, handler, deliver) {
3453
+ if (result === void 0) return;
3454
+ const items = Array.isArray(result) ? result : [result];
3455
+ for (const item of items) {
3456
+ if (!isFusorEvent(item)) {
3457
+ deliver(item);
3458
+ continue;
3459
+ }
3460
+ if (item.name === "messages") deliver(item.data);
3461
+ else handler.pushEvent(item.name, item.data);
3462
+ }
3463
+ }
3464
+ function runHandlerOnce(handler, parsedRequest, deliver = handler.pushMessage) {
3465
+ return (async () => {
3466
+ try {
3467
+ const payload = await handler.verify(parsedRequest);
3468
+ let reply;
3469
+ let respondCalled = false;
3470
+ let returned = false;
3471
+ const respond = (next) => {
3472
+ if (returned) {
3473
+ log.warn("fusor.respond called after handler returned; ignoring");
3474
+ return;
3475
+ }
3476
+ if (respondCalled) log.debug("fusor.respond called more than once; last call wins");
3477
+ respondCalled = true;
3478
+ reply = next;
3479
+ };
3480
+ const result = await handler.messages({
3481
+ payload,
3482
+ respond
3483
+ });
3484
+ returned = true;
3485
+ routeHandlerResult(result, handler, deliver);
3486
+ return {
3487
+ ok: true,
3488
+ reply
3489
+ };
3490
+ } catch (error) {
3491
+ return {
3492
+ ok: false,
3493
+ errorReason: errorText(error)
3494
+ };
3495
+ }
3496
+ })();
3497
+ }
3498
+ var FusorCore = class {
3499
+ options;
3500
+ websocketEndpoint;
3501
+ handlers = /* @__PURE__ */ new Map();
3502
+ tokenProvider;
3503
+ wsSession;
3504
+ connectionLoop;
3505
+ started = false;
3506
+ stopped = false;
3507
+ stopResolve;
3508
+ stoppedPromise;
3509
+ reconnectTimer;
3510
+ reconnectResolve;
3511
+ constructor(options) {
3512
+ this.options = options;
3513
+ this.websocketEndpoint = options.websocketEndpoint ?? process.env.SPECTRUM_FUSOR_WS_URL ?? DEFAULT_FUSOR_WS_URL;
3514
+ this.stoppedPromise = new Promise((resolve) => {
3515
+ this.stopResolve = resolve;
3516
+ });
3517
+ }
3518
+ register(platform, handler) {
3519
+ const list = this.handlers.get(platform) ?? [];
3520
+ list.push(handler);
3521
+ this.handlers.set(platform, list);
3522
+ }
3523
+ async start() {
3524
+ if (!(this.options.projectId && this.options.projectSecret)) throw new Error("fusor: streaming via spectrum.messages requires projectId and projectSecret");
3525
+ if (this.started) return;
3526
+ this.started = true;
3527
+ this.tokenProvider = await createFusorTokenProvider(this.options.projectId, this.options.projectSecret);
3528
+ this.connectionLoop = this.runConnectionLoop().catch((error) => {
3529
+ log.error("fusor connection loop crashed", errorAttrs(error), error);
3530
+ });
3531
+ }
3532
+ async runConnectionLoop() {
3533
+ let attempt = 0;
3534
+ while (!this.stopped) {
3535
+ const wsRan = await this.tryWebsocketOnce();
3536
+ if (this.stopped) return;
3537
+ if (wsRan) {
3538
+ attempt = 0;
3539
+ continue;
3540
+ }
3541
+ attempt += 1;
3542
+ await this.backoffSleep(this.backoffMs(attempt));
3543
+ }
3544
+ }
3545
+ async tryWebsocketOnce() {
3546
+ try {
3547
+ await this.runWebsocketOnce();
3548
+ return true;
3549
+ } catch (error) {
3550
+ if (isWsAuthError(error)) this.tokenProvider?.invalidate();
3551
+ if (!this.stopped) log.warn("fusor websocket stream errored; reconnecting", errorAttrs(error), error);
3552
+ return false;
3553
+ }
3554
+ }
3555
+ backoffMs(attempt) {
3556
+ return Math.min(RECONNECT_BASE_MS * 2 ** (attempt - 1), RECONNECT_MAX_MS);
3557
+ }
3558
+ async backoffSleep(backoff) {
3559
+ await new Promise((resolve) => {
3560
+ this.reconnectResolve = resolve;
3561
+ const timer = setTimeout(resolve, backoff);
3562
+ timer.unref?.();
3563
+ this.reconnectTimer = timer;
3564
+ });
3565
+ this.reconnectTimer = void 0;
3566
+ this.reconnectResolve = void 0;
3567
+ }
3568
+ async runWebsocketOnce() {
3569
+ if (!this.tokenProvider) throw new Error("fusor: token not initialized");
3570
+ const token = await this.tokenProvider.getToken();
3571
+ const session = runFusorWsSession({
3572
+ url: this.websocketEndpoint,
3573
+ token,
3574
+ onEvent: async (event, sendReply) => {
3575
+ if (this.stopped) return;
3576
+ const reply = await this.processEvent(event);
3577
+ sendReply?.(reply);
3578
+ }
3579
+ });
3580
+ this.wsSession = session;
3581
+ try {
3582
+ await session.done;
3583
+ } finally {
3584
+ this.wsSession = void 0;
3585
+ }
3586
+ }
3587
+ async processEvent(event, deliver) {
3588
+ const handlers = this.handlers.get(event.platform) ?? [];
3589
+ if (handlers.length === 0) {
3590
+ const hint = officialProviderInstallHint(event.platform);
3591
+ log.warn(hint ? `fusor: no handler for platform — ${hint}` : "fusor: no handler for platform", {
3592
+ "spectrum.fusor.platform": event.platform,
3593
+ "spectrum.fusor.event_id": event.eventId
3594
+ });
3595
+ return {
3596
+ eventId: event.eventId,
3597
+ errorReason: `no handler for platform ${event.platform}`,
3598
+ status: 0,
3599
+ headers: {},
3600
+ body: new Uint8Array(0)
3601
+ };
3602
+ }
3603
+ let parsedRequest;
3604
+ try {
3605
+ parsedRequest = parseHttpRequest(event.rawRequest);
3606
+ } catch (error) {
3607
+ const errorReason = errorText(error);
3608
+ log.warn("fusor: failed to parse raw_request", {
3609
+ "spectrum.fusor.platform": event.platform,
3610
+ "spectrum.fusor.event_id": event.eventId,
3611
+ ...errorAttrs(error)
3612
+ });
3613
+ return {
3614
+ eventId: event.eventId,
3615
+ errorReason,
3616
+ status: 0,
3617
+ headers: {},
3618
+ body: new Uint8Array(0)
3619
+ };
3620
+ }
3621
+ const combined = combineReplies(await Promise.all(handlers.map((handler) => runHandlerOnce(handler, parsedRequest, deliver))));
3622
+ combined.eventId = event.eventId;
3623
+ return combined;
3624
+ }
3625
+ async close() {
3626
+ if (this.stopped) return;
3627
+ this.stopped = true;
3628
+ this.wsSession?.close();
3629
+ if (this.reconnectTimer) {
3630
+ clearTimeout(this.reconnectTimer);
3631
+ this.reconnectTimer = void 0;
3632
+ }
3633
+ this.reconnectResolve?.();
3634
+ this.reconnectResolve = void 0;
3635
+ if (this.tokenProvider) await this.tokenProvider.dispose();
3636
+ if (this.connectionLoop) await this.connectionLoop;
3637
+ this.stopResolve?.();
3638
+ }
3639
+ async waitStopped() {
3640
+ return this.stoppedPromise;
3641
+ }
3642
+ };
3643
+ //#endregion
3644
+ //#region src/utils/store.ts
3645
+ const isRecordObject = (value) => {
3646
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
3647
+ const prototype = Object.getPrototypeOf(value);
3648
+ return prototype === Object.prototype || prototype === null;
3649
+ };
3650
+ function createStore() {
3651
+ const data = /* @__PURE__ */ new Map();
3652
+ return {
3653
+ set(key, value) {
3654
+ data.set(key, value);
3655
+ },
3656
+ get(key) {
3657
+ return data.get(key);
3658
+ },
3659
+ has(key) {
3660
+ return data.has(key);
3661
+ },
3662
+ delete(key) {
3663
+ return data.delete(key);
3664
+ },
3665
+ clear() {
3666
+ data.clear();
3667
+ },
3668
+ keys() {
3669
+ return Array.from(data.keys());
3670
+ },
3671
+ string(key) {
3672
+ const v = data.get(key);
3673
+ return typeof v === "string" ? v : void 0;
3674
+ },
3675
+ number(key) {
3676
+ const v = data.get(key);
3677
+ return typeof v === "number" ? v : void 0;
3678
+ },
3679
+ bool(key) {
3680
+ const v = data.get(key);
3681
+ return typeof v === "boolean" ? v : void 0;
3682
+ },
3683
+ object(key) {
3684
+ const v = data.get(key);
3685
+ if (!isRecordObject(v)) return;
3686
+ return v;
3687
+ },
3688
+ array(key) {
3689
+ const v = data.get(key);
3690
+ return Array.isArray(v) ? v : void 0;
3691
+ }
3692
+ };
3693
+ }
3694
+ //#endregion
3695
+ //#region src/webhook/deserialize.ts
3696
+ /** The single event type that carries a message today. */
3697
+ const MESSAGES_EVENT = "messages";
3698
+ const DEFAULT_ATTACHMENT_NAME = "attachment";
3699
+ const DEFAULT_MIME_TYPE = "application/octet-stream";
3700
+ const isRecord = (value) => typeof value === "object" && value !== null;
3701
+ const asString = (value) => typeof value === "string" ? value : "";
3702
+ const asOptionalDate = (value) => typeof value === "string" ? new Date(value) : void 0;
3703
+ /**
3704
+ * Map a native Spectrum webhook envelope to a `ProviderMessageRecord` plus the
3705
+ * platform that owns it, ready for `resolveRecordToMessages`. Returns `null`
3706
+ * when the delivery carries nothing to route (an unknown `event` type, or a
3707
+ * message with no resolvable platform) — the caller acknowledges it (200)
3708
+ * rather than failing, since neither is fixed by a retry.
3709
+ *
3710
+ * Reaction targets and group items are emitted as **raw nested records**; the
3711
+ * `wrapProviderMessage`/`wrapNestedContent` pipeline turns them into fully-built
3712
+ * Messages, exactly as a provider's own `messages` handler would.
3713
+ */
3714
+ function deserializeSpectrumMessage(envelope, ctx) {
3715
+ if (envelope.event !== MESSAGES_EVENT) return null;
3716
+ const message = envelope.message;
3717
+ const platform = resolvePlatform(message);
3718
+ if (!platform) return null;
3719
+ const spaceRef = { ...message.space };
3720
+ return {
3721
+ platform,
3722
+ record: {
3723
+ id: message.id,
3724
+ direction: "inbound",
3725
+ content: deserializeContent(message.content, platform, spaceRef, ctx),
3726
+ space: spaceRef,
3727
+ sender: message.sender ? { ...message.sender } : void 0,
3728
+ timestamp: asOptionalDate(message.timestamp)
3729
+ }
3730
+ };
3731
+ }
3732
+ const resolvePlatform = (message) => message.platform ?? message.space.platform;
3733
+ const deserializeContent = (content, platform, spaceRef, ctx) => {
3734
+ try {
3735
+ return mapContent(content, platform, spaceRef, ctx);
3736
+ } catch {
3737
+ return asCustom(content);
3738
+ }
3739
+ };
3740
+ const mapContent = (content, platform, spaceRef, ctx) => {
3741
+ const raw = content;
3742
+ switch (content.type) {
3743
+ case "text": return {
3744
+ type: "text",
3745
+ text: asString(raw.text)
3746
+ };
3747
+ case "richlink": return asRichlink({ url: asString(raw.url) });
3748
+ case "contact": return deserializeContact(raw);
3749
+ case "reaction": return deserializeReaction(raw, spaceRef);
3750
+ case "group": return deserializeGroup(raw, platform, spaceRef, ctx);
3751
+ case "attachment": return deserializeAttachment(raw, platform, spaceRef, ctx);
3752
+ default: return asCustom(content);
3753
+ }
3754
+ };
3755
+ const deserializeAttachment = (raw, platform, spaceRef, ctx) => {
3756
+ const id = asString(raw.id);
3757
+ const bytes = ctx.resolveAttachment?.(platform, spaceRef, id);
3758
+ const unavailable = () => Promise.reject(UnsupportedError.action("getAttachment", platform, `attachment "${id}" arrived without bytes over the Spectrum webhook and "${platform}" exposes no getAttachment`));
3759
+ return asAttachment({
3760
+ id,
3761
+ name: asString(raw.name) || DEFAULT_ATTACHMENT_NAME,
3762
+ mimeType: asString(raw.mimeType) || DEFAULT_MIME_TYPE,
3763
+ size: typeof raw.size === "number" ? raw.size : void 0,
3764
+ read: bytes ? bytes.read : unavailable,
3765
+ stream: bytes?.stream
3766
+ });
3767
+ };
3768
+ const deserializeReaction = (raw, spaceRef) => ({
3769
+ type: "reaction",
3770
+ emoji: asString(raw.emoji),
3771
+ target: buildTargetRecord(raw.target, spaceRef)
3772
+ });
3773
+ const buildTargetRecord = (target, spaceRef) => {
3774
+ const ref = isRecord(target) ? target : {};
3775
+ return {
3776
+ id: asString(ref.id),
3777
+ content: {
3778
+ type: "text",
3779
+ text: asString(ref.contentPreview)
3780
+ },
3781
+ space: { ...spaceRef },
3782
+ sender: ref.sender ? { ...ref.sender } : void 0,
3783
+ timestamp: asOptionalDate(ref.timestamp)
3784
+ };
3785
+ };
3786
+ const deserializeGroup = (raw, platform, spaceRef, ctx) => {
3787
+ return {
3788
+ type: "group",
3789
+ items: (Array.isArray(raw.items) ? raw.items : []).map((item) => buildItemRecord(item, platform, spaceRef, ctx))
3790
+ };
3791
+ };
3792
+ const buildItemRecord = (item, platform, spaceRef, ctx) => {
3793
+ const record = isRecord(item) ? item : {};
3794
+ const itemSpace = isRecord(record.space) ? {
3795
+ ...record.space,
3796
+ id: asString(record.space.id) || spaceRef.id
3797
+ } : spaceRef;
3798
+ const content = isRecord(record.content) ? deserializeContent(record.content, platform, itemSpace, ctx) : asCustom(record.content);
3799
+ return {
3800
+ id: asString(record.id),
3801
+ content,
3802
+ space: itemSpace,
3803
+ sender: isRecord(record.sender) ? {
3804
+ ...record.sender,
3805
+ id: asString(record.sender.id)
3806
+ } : void 0,
3807
+ timestamp: asOptionalDate(record.timestamp)
3808
+ };
3809
+ };
3810
+ const deserializeContact = (raw) => {
3811
+ const input = {};
3812
+ const name = normalizeContactName(raw.name);
3813
+ if (name) input.name = name;
3814
+ const phones = normalizeContactPhones(raw.phones);
3815
+ if (phones) input.phones = phones;
3816
+ if (typeof raw.note === "string") input.note = raw.note;
3817
+ if (raw.raw !== void 0) input.raw = raw.raw;
3818
+ return asContact(input);
3819
+ };
3820
+ const CONTACT_NAME_KEYS = [
3821
+ "formatted",
3822
+ "first",
3823
+ "last",
3824
+ "middle",
3825
+ "prefix",
3826
+ "suffix"
3827
+ ];
3828
+ const normalizeContactName = (value) => {
3829
+ if (typeof value === "string") return { formatted: value };
3830
+ if (!isRecord(value)) return;
3831
+ const name = {};
3832
+ for (const key of CONTACT_NAME_KEYS) {
3833
+ const part = value[key];
3834
+ if (typeof part === "string") name[key] = part;
3835
+ }
3836
+ return Object.keys(name).length > 0 ? name : void 0;
3837
+ };
3838
+ const normalizeContactPhones = (value) => {
3839
+ if (!Array.isArray(value)) return;
3840
+ const phones = [];
3841
+ for (const entry of value) if (typeof entry === "string") phones.push({ value: entry });
3842
+ else if (isRecord(entry) && typeof entry.value === "string") phones.push({ value: entry.value });
3843
+ return phones.length > 0 ? phones : void 0;
3844
+ };
3845
+ //#endregion
3846
+ //#region src/webhook/types.ts
3847
+ /**
3848
+ * Wire schemas for the **native Spectrum webhook**
3849
+ * (https://photon.codes/docs/webhooks).
3850
+ *
3851
+ * Unlike the fusor webhook — which relays a raw provider request inside a
3852
+ * protobuf envelope — the native webhook delivers Spectrum's own message model
3853
+ * already normalized to slim JSON (methods and byte payloads stripped), signed
3854
+ * with an HMAC. These schemas validate the fields the deserializer depends on
3855
+ * while **preserving** unknown/extra fields (`z.looseObject`), so additive
3856
+ * changes — new platform-specific space fields, future content arms — never
3857
+ * break an older SDK. Content is discriminated by hand in `deserialize.ts`
3858
+ * rather than here, so an unknown content `type` survives parsing instead of
3859
+ * throwing.
3860
+ */
3861
+ const slimSenderSchema = z.looseObject({
3862
+ id: z.string(),
3863
+ platform: z.string().optional()
3864
+ });
3865
+ const slimSpaceSchema = z.looseObject({
3866
+ id: z.string(),
3867
+ platform: z.string().optional()
3868
+ });
3869
+ const slimContentSchema = z.looseObject({ type: z.string() });
3870
+ z.looseObject({
3871
+ id: z.string(),
3872
+ platform: z.string().optional(),
3873
+ timestamp: z.string().optional(),
3874
+ sender: slimSenderSchema.optional(),
3875
+ contentPreview: z.string().optional()
3876
+ });
3877
+ const slimMessageSchema = z.looseObject({
3878
+ id: z.string(),
3879
+ platform: z.string().optional(),
3880
+ direction: z.string().optional(),
3881
+ timestamp: z.string().optional(),
3882
+ sender: slimSenderSchema.optional(),
3883
+ space: slimSpaceSchema,
3884
+ content: slimContentSchema
3885
+ });
3886
+ const slimEnvelopeSchema = z.looseObject({
3887
+ event: z.string(),
3888
+ space: slimSpaceSchema.optional(),
3889
+ message: slimMessageSchema
3890
+ });
3891
+ //#endregion
3892
+ //#region src/webhook/verify.ts
3893
+ const SIGNATURE_HEADER = "x-spectrum-signature";
3894
+ const TIMESTAMP_HEADER = "x-spectrum-timestamp";
3895
+ const SIGNATURE_PREFIX = "v0=";
3896
+ const SIGNATURE_SCHEME = "v0";
3897
+ /**
3898
+ * Replay-protection window, in seconds. Spectrum signs each delivery with a
3899
+ * timestamp; a delivery whose timestamp is further than this from now (past or
3900
+ * future) is rejected, so a captured request cannot be replayed indefinitely.
3901
+ * Matches the documented 5-minute tolerance.
3902
+ */
3903
+ const REPLAY_TOLERANCE_SECONDS = 300;
3904
+ const MILLIS_PER_SECOND = 1e3;
3905
+ /**
3906
+ * Verify a native Spectrum webhook signature.
3907
+ *
3908
+ * The header is `X-Spectrum-Signature: v0=<lowercase-hex>` where the hex digest
3909
+ * is `HMAC-SHA256(secret, "v0:" + timestamp + ":" + rawBody)` and `timestamp`
3910
+ * is the `X-Spectrum-Timestamp` header (unix seconds). The base string is built
3911
+ * over the **exact body bytes**: never JSON-parse-then-restringify before
3912
+ * verifying, or the bytes (key order, whitespace) change and the MAC won't
3913
+ * match. The digest comparison is constant-time.
3914
+ */
3915
+ function verifySpectrumSignature(input) {
3916
+ const { rawBody, headers, secret, now = Date.now() } = input;
3917
+ const provided = headers[SIGNATURE_HEADER];
3918
+ const timestamp = headers[TIMESTAMP_HEADER];
3919
+ if (!(provided && timestamp)) return {
3920
+ ok: false,
3921
+ reason: "missing-headers"
3922
+ };
3923
+ const timestampSeconds = Number(timestamp);
3924
+ if (!Number.isFinite(timestampSeconds)) return {
3925
+ ok: false,
3926
+ reason: "missing-headers"
3927
+ };
3928
+ const nowSeconds = Math.floor(now / MILLIS_PER_SECOND);
3929
+ if (Math.abs(nowSeconds - timestampSeconds) > REPLAY_TOLERANCE_SECONDS) return {
3930
+ ok: false,
3931
+ reason: "expired"
3932
+ };
3933
+ const base = Buffer.concat([Buffer.from(`${SIGNATURE_SCHEME}:${timestamp}:`, "utf8"), Buffer.from(rawBody)]);
3934
+ const expected = createHmac("sha256", secret).update(base).digest();
3935
+ const providedHex = provided.startsWith(SIGNATURE_PREFIX) ? provided.slice(3) : provided;
3936
+ const providedBytes = Buffer.from(providedHex, "hex");
3937
+ if (providedBytes.length !== expected.length || !timingSafeEqual(providedBytes, expected)) return {
3938
+ ok: false,
3939
+ reason: "signature-mismatch"
3940
+ };
3941
+ return { ok: true };
3942
+ }
3943
+ //#endregion
3944
+ //#region src/spectrum.ts
3945
+ const PHOTON_OTEL_ENDPOINT = "https://otlp.photon.codes";
3946
+ const STREAM_CLOSE_TIMEOUT_MS = 5e3;
3947
+ const lifecycleLog = createLogger("spectrum.lifecycle");
3948
+ const ignoreCleanupError = () => void 0;
3949
+ const spectrumOptionsSchema = z.object({
3950
+ flattenGroups: z.boolean().optional(),
3951
+ logLevel: z.enum([
3952
+ "debug",
3953
+ "info",
3954
+ "warn",
3955
+ "error",
3956
+ "silent"
3957
+ ]).optional()
3958
+ }).optional();
3959
+ const spectrumConfigSchema = z.union([z.object({
3960
+ projectId: z.string().min(1),
3961
+ projectSecret: z.string().min(1),
3962
+ providers: z.array(z.custom()),
3963
+ options: spectrumOptionsSchema,
3964
+ telemetry: z.boolean().optional(),
3965
+ webhookSecret: z.string().min(1).optional()
3966
+ }), z.object({
3967
+ projectId: z.undefined().optional(),
3968
+ projectSecret: z.undefined().optional(),
3969
+ providers: z.array(z.custom()),
3970
+ options: spectrumOptionsSchema,
3971
+ telemetry: z.boolean().optional(),
3972
+ webhookSecret: z.string().min(1).optional()
3973
+ })]);
3974
+ function bootstrapTelemetry(opts) {
3975
+ const headers = {};
3976
+ if (opts.projectId && opts.projectSecret) {
3977
+ const credential = `${opts.projectId}:${opts.projectSecret}`;
3978
+ headers.Authorization = `Basic ${btoa(credential)}`;
3979
+ }
3980
+ const resourceAttributes = { "deployment.environment": process.env.DEPLOYMENT_ENV ?? "production" };
3981
+ if (opts.projectId) resourceAttributes["spectrum.project_id"] = opts.projectId;
3982
+ return setupOtel({
3983
+ serviceName: "spectrum-ts",
3984
+ serviceVersion: SPECTRUM_SDK_VERSION,
3985
+ endpoint: PHOTON_OTEL_ENDPOINT,
3986
+ headers,
3987
+ resourceAttributes
3988
+ });
3989
+ }
3990
+ function applyLogLevel(level) {
3991
+ if (level) setLogLevel(level);
3992
+ }
3993
+ async function Spectrum(options) {
3994
+ spectrumConfigSchema.parse(options);
3995
+ const { projectId, projectSecret, providers, options: runtimeOptions, telemetry, webhookSecret } = options;
3996
+ const flattenGroups = runtimeOptions?.flattenGroups ?? false;
3997
+ applyLogLevel(runtimeOptions?.logLevel);
3998
+ const resolvedWebhookSecret = webhookSecret ?? process.env.SPECTRUM_WEBHOOK_SECRET;
3999
+ const otelHandle = telemetry ? bootstrapTelemetry({
4000
+ projectId,
4001
+ projectSecret
4002
+ }) : void 0;
4003
+ const projectConfig = projectId !== void 0 && projectSecret !== void 0 ? await cloud.getProject(projectId, projectSecret) : void 0;
4004
+ const platformStates = /* @__PURE__ */ new Map();
4005
+ const fusorMessageSources = /* @__PURE__ */ new Map();
4006
+ const messageBroadcasters = /* @__PURE__ */ new Map();
4007
+ const fusorEventSources = /* @__PURE__ */ new Map();
4008
+ const eventBroadcasters = /* @__PURE__ */ new Map();
4009
+ const customEventStreams = /* @__PURE__ */ new Map();
4010
+ let stopped = false;
4011
+ const adaptIterable = (iterable, project) => stream((emit, end) => {
4012
+ const iterator = iterable[Symbol.asyncIterator]();
4013
+ const pump = (async () => {
4014
+ try {
4015
+ let result = await iterator.next();
4016
+ while (!result.done) {
4017
+ if (project) await project(result.value, emit);
4018
+ else await emit(result.value);
4019
+ result = await iterator.next();
4020
+ }
4021
+ end();
4022
+ } catch (error) {
4023
+ end(error);
4024
+ }
4025
+ })();
4026
+ return async () => {
4027
+ await iterator.return?.();
4028
+ await pump.catch(ignoreCleanupError);
4029
+ };
4030
+ });
4031
+ const resolveRecordToMessages = async (record, rt) => {
4032
+ const { client, config, definition, store } = rt;
4033
+ const { space, normalizedMessage } = await withSpan("spectrum.message.receive", {
4034
+ "spectrum.provider": definition.name,
4035
+ "spectrum.message.id": record.id,
4036
+ "spectrum.space.id": record.space?.id,
4037
+ ...contentAttrs(record.content),
4038
+ ...senderAttrs(record.sender)
4039
+ }, () => {
4040
+ const spaceRef = {
4041
+ ...record.space,
4042
+ __platform: definition.name
4043
+ };
4044
+ const space = buildSpace({
4045
+ spaceRef,
4046
+ extras: {},
4047
+ actionCtx: {
4048
+ space: spaceRef,
4049
+ client,
4050
+ config,
4051
+ store
4052
+ },
4053
+ definition,
4054
+ client,
4055
+ config,
4056
+ store
4057
+ });
4058
+ return {
4059
+ space,
4060
+ normalizedMessage: wrapProviderMessage(record, {
4061
+ client,
4062
+ config,
4063
+ definition,
4064
+ space,
4065
+ spaceRef,
4066
+ store
4067
+ }, "inbound")
4068
+ };
4069
+ });
4070
+ if (flattenGroups && normalizedMessage.content.type === "group") return normalizedMessage.content.items.map((item) => [space, item]);
4071
+ return [[space, normalizedMessage]];
4072
+ };
4073
+ const createProviderMessagesStream = (state) => {
4074
+ const { client, config, definition, store } = state;
4075
+ const fusorSource = fusorMessageSources.get(definition.name);
4076
+ return adaptIterable(fusorSource ? fusorSource.iterable : definition.messages({
4077
+ client,
4078
+ config,
4079
+ projectConfig,
4080
+ store
4081
+ }), async (record, emit) => {
4082
+ const tuples = await resolveRecordToMessages(record, {
4083
+ client,
4084
+ config,
4085
+ definition,
4086
+ store
4087
+ });
4088
+ for (const tuple of tuples) await emit(tuple);
4089
+ });
4090
+ };
4091
+ const getOrCreateMessageBroadcast = (state) => {
4092
+ if (stopped) throw new Error(`Spectrum instance has been stopped; cannot subscribe to "${state.definition.name}" messages`);
4093
+ const name = state.definition.name;
4094
+ let broadcaster = messageBroadcasters.get(name);
4095
+ if (!broadcaster) {
4096
+ broadcaster = broadcast(createProviderMessagesStream(state));
4097
+ messageBroadcasters.set(name, broadcaster);
4098
+ }
4099
+ return broadcaster;
4100
+ };
4101
+ const getOrCreateEventBroadcast = (platform, channel) => {
4102
+ const queue = fusorEventSources.get(platform)?.get(channel);
4103
+ if (!queue) return;
4104
+ if (stopped) throw new Error(`Spectrum instance has been stopped; cannot subscribe to "${platform}" event "${channel}"`);
4105
+ const key = `${platform}${channel}`;
4106
+ let broadcaster = eventBroadcasters.get(key);
4107
+ if (!broadcaster) {
4108
+ broadcaster = broadcast(adaptIterable(queue.iterable));
4109
+ eventBroadcasters.set(key, broadcaster);
4110
+ }
4111
+ return broadcaster;
4112
+ };
4113
+ await withSpan("spectrum.init", {
4114
+ "spectrum.provider_count": providers.length,
4115
+ "spectrum.flatten_groups": flattenGroups
4116
+ }, async () => {
4117
+ for (const provider of providers) {
4118
+ const providerConfig = provider;
4119
+ const def = providerConfig.__definition;
4120
+ const userConfig = def.config.parse(providerConfig.config);
4121
+ const store = createStore();
4122
+ const state = {
4123
+ client: await withSpan("spectrum.provider.create_client", { "spectrum.provider": def.name }, () => def.lifecycle.createClient({
4124
+ config: userConfig,
4125
+ projectId,
4126
+ projectSecret,
4127
+ projectConfig,
4128
+ store
4129
+ })),
4130
+ config: userConfig,
4131
+ definition: def,
4132
+ store
4133
+ };
4134
+ platformStates.set(def.name, {
4135
+ ...state,
4136
+ projectConfig,
4137
+ subscribeMessages: () => getOrCreateMessageBroadcast(state).subscribe(),
4138
+ subscribeEvent: (channel) => getOrCreateEventBroadcast(def.name, channel)?.subscribe()
4139
+ });
4140
+ }
4141
+ });
4142
+ let fusorCore;
4143
+ let fusorStartPromise;
4144
+ const fusorPlatforms = [];
4145
+ for (const [name, state] of platformStates) if (isFusorClient(state.client)) fusorPlatforms.push({
4146
+ name,
4147
+ client: state.client
4148
+ });
4149
+ if (fusorPlatforms.length > 0) {
4150
+ fusorCore = new FusorCore({
4151
+ projectId,
4152
+ projectSecret
4153
+ });
4154
+ for (const { name, client } of fusorPlatforms) {
4155
+ const queue = createAsyncQueue();
4156
+ fusorMessageSources.set(name, queue);
4157
+ const runtime = platformStates.get(name);
4158
+ if (!runtime) continue;
4159
+ const userMessages = runtime.definition.messages;
4160
+ const declaredEvents = runtime.definition.events ?? {};
4161
+ const eventQueues = /* @__PURE__ */ new Map();
4162
+ for (const channel of Object.keys(declaredEvents)) eventQueues.set(channel, createAsyncQueue());
4163
+ fusorEventSources.set(name, eventQueues);
4164
+ const handler = {
4165
+ verify: client.verify,
4166
+ messages: async (ctx) => userMessages({
4167
+ ...ctx,
4168
+ config: runtime.config,
4169
+ store: runtime.store,
4170
+ projectConfig: runtime.projectConfig
4171
+ }),
4172
+ pushMessage: (record) => queue.push(record),
4173
+ pushEvent: (channel, data) => {
4174
+ const eventQueue = eventQueues.get(channel);
4175
+ if (!eventQueue) {
4176
+ lifecycleLog.warn(`spectrum: fusorEvent("${channel}", …) names a channel not declared in "${name}".events; dropping`, {
4177
+ "spectrum.lifecycle.platform": name,
4178
+ "spectrum.lifecycle.channel": channel
4179
+ });
4180
+ return;
4181
+ }
4182
+ eventQueue.push(data);
4183
+ }
4184
+ };
4185
+ fusorCore.register(client.platform, handler);
4186
+ }
4187
+ }
4188
+ const ensureFusorStarted = () => {
4189
+ if (!fusorCore) return Promise.resolve();
4190
+ if (!fusorStartPromise) fusorStartPromise = fusorCore.start();
4191
+ return fusorStartPromise;
4192
+ };
4193
+ const providerNames = providers.map((p) => p.__definition.name).join(",");
4194
+ lifecycleLog.info("Spectrum started", {
4195
+ "spectrum.lifecycle.provider_count": providers.length,
4196
+ "spectrum.lifecycle.providers": providerNames,
4197
+ "spectrum.lifecycle.telemetry": telemetry === true
4198
+ });
4199
+ if (projectConfig && projectId !== void 0) {
4200
+ const registered = new Set(Array.from(platformStates.keys(), normalizePlatformKey));
4201
+ cloud.getPlatforms(projectId).then((platforms) => {
4202
+ for (const [platform, status] of Object.entries(platforms)) {
4203
+ if (!status.enabled || registered.has(normalizePlatformKey(platform))) continue;
4204
+ const hint = officialProviderInstallHint(platform);
4205
+ lifecycleLog.warn(hint ? `spectrum: project has "${platform}" enabled but no matching provider is registered — ${hint}` : `spectrum: project has "${platform}" enabled but no matching provider is registered`, { "spectrum.lifecycle.platform": platform });
4206
+ }
4207
+ }).catch(() => {});
4208
+ }
4209
+ const createMessagesStream = () => stream((emit, end) => {
4210
+ ensureFusorStarted().catch((error) => end(error));
4211
+ const merged = mergeStreams(Array.from(platformStates.values(), (runtime) => runtime.subscribeMessages()));
4212
+ const pump = (async () => {
4213
+ try {
4214
+ for await (const value of merged) await emit(value);
4215
+ end();
4216
+ } catch (error) {
4217
+ end(error);
4218
+ }
4219
+ })();
4220
+ return async () => {
4221
+ await merged.close();
4222
+ await pump.catch(ignoreCleanupError);
4223
+ };
4224
+ });
4225
+ const createCustomEventStream = (eventName) => stream((emit, end) => {
4226
+ const providerStreams = [];
4227
+ for (const state of platformStates.values()) {
4228
+ const { client, config, definition, store } = state;
4229
+ let source = state.subscribeEvent?.(eventName);
4230
+ if (!source) {
4231
+ const producer = definition.events?.[eventName];
4232
+ if (typeof producer !== "function") continue;
4233
+ source = producer({
4234
+ client,
4235
+ config,
4236
+ projectConfig,
4237
+ store
4238
+ });
4239
+ }
4240
+ const providerEvents = source;
4241
+ providerStreams.push(adaptIterable(providerEvents, async (value, emit) => {
4242
+ await emit(await withSpan("spectrum.event", {
4243
+ "spectrum.provider": definition.name,
4244
+ "spectrum.event.name": eventName
4245
+ }, () => typeof value === "object" && value !== null ? {
4246
+ ...value,
4247
+ platform: definition.name
4248
+ } : {
4249
+ platform: definition.name,
4250
+ payload: value
4251
+ }));
4252
+ }));
4253
+ }
4254
+ const merged = mergeStreams(providerStreams);
4255
+ const pump = (async () => {
4256
+ try {
4257
+ for await (const value of merged) await emit(value);
4258
+ end();
4259
+ } catch (error) {
4260
+ end(error);
4261
+ }
4262
+ })();
4263
+ return async () => {
4264
+ await merged.close();
4265
+ await pump.catch(ignoreCleanupError);
4266
+ };
4267
+ });
4268
+ const messagesStream = createMessagesStream();
4269
+ const closeFusorSources = () => {
4270
+ for (const queue of fusorMessageSources.values()) queue.close();
4271
+ fusorMessageSources.clear();
4272
+ for (const queues of fusorEventSources.values()) for (const queue of queues.values()) queue.close();
4273
+ fusorEventSources.clear();
4274
+ };
4275
+ const stopOnce = async () => {
4276
+ if (stopped) return;
4277
+ stopped = true;
4278
+ const streamShutdowns = [
4279
+ messagesStream.close(),
4280
+ ...Array.from(customEventStreams.values(), (eventStream) => eventStream.close()),
4281
+ ...Array.from(messageBroadcasters.values(), (broadcaster) => broadcaster.close()),
4282
+ ...Array.from(eventBroadcasters.values(), (broadcaster) => broadcaster.close())
4283
+ ];
4284
+ process.off("SIGINT", handleSignal);
4285
+ process.off("SIGTERM", handleSignal);
4286
+ const streamCloseStart = performance.now();
4287
+ const streamSettled = Promise.allSettled(streamShutdowns);
4288
+ let streamTimedOut = false;
4289
+ await Promise.race([streamSettled, new Promise((resolve) => {
4290
+ setTimeout(() => {
4291
+ streamTimedOut = true;
4292
+ resolve();
4293
+ }, STREAM_CLOSE_TIMEOUT_MS).unref();
4294
+ })]);
4295
+ if (streamTimedOut) lifecycleLog.warn("stream close timed out; proceeding to teardown", { "spectrum.lifecycle.stream_close_timeout_ms": STREAM_CLOSE_TIMEOUT_MS });
4296
+ let fusorCloseMs = 0;
4297
+ if (fusorCore) {
4298
+ const fusorCloseStart = performance.now();
4299
+ if (fusorStartPromise) await fusorStartPromise.catch(ignoreCleanupError);
4300
+ await fusorCore.close().catch((error) => {
4301
+ lifecycleLog.warn("fusor core close failed", errorAttrs(error), error);
4302
+ });
4303
+ fusorCloseMs = Math.round(performance.now() - fusorCloseStart);
4304
+ closeFusorSources();
4305
+ }
4306
+ const clientShutdowns = [];
4307
+ for (const state of platformStates.values()) {
4308
+ const destroy = state.definition.lifecycle.destroyClient;
4309
+ if (!destroy) continue;
4310
+ clientShutdowns.push(withSpan("spectrum.provider.destroy_client", { "spectrum.provider": state.definition.name }, () => destroy({
4311
+ client: state.client,
4312
+ store: state.store
4313
+ })));
4314
+ }
4315
+ const clientCloseStart = performance.now();
4316
+ await Promise.allSettled(clientShutdowns);
4317
+ const clientCloseMs = Math.round(performance.now() - clientCloseStart);
4318
+ await streamSettled.catch(() => void 0);
4319
+ const streamCloseMs = Math.round(performance.now() - streamCloseStart);
4320
+ customEventStreams.clear();
4321
+ messageBroadcasters.clear();
4322
+ eventBroadcasters.clear();
4323
+ platformStates.clear();
4324
+ lifecycleLog.info("Spectrum stopped", {
4325
+ "spectrum.lifecycle.providers": providerNames,
4326
+ "spectrum.lifecycle.stream_close_ms": streamCloseMs,
4327
+ "spectrum.lifecycle.fusor_close_ms": fusorCloseMs,
4328
+ "spectrum.lifecycle.client_close_ms": clientCloseMs
4329
+ });
4330
+ if (otelHandle) await otelHandle.shutdown();
4331
+ };
4332
+ const handleSignal = () => {
4333
+ setTimeout(() => process.exit(1), 3e3).unref();
4334
+ stopOnce().then(() => process.exit(0)).catch(() => process.exit(1));
4335
+ };
4336
+ process.on("SIGINT", handleSignal);
4337
+ process.on("SIGTERM", handleSignal);
4338
+ const messages = messagesStream;
4339
+ const customEventProxy = new Proxy({}, { get(_target, prop) {
4340
+ let eventStream = customEventStreams.get(prop);
4341
+ if (!eventStream) {
4342
+ eventStream = createCustomEventStream(prop);
4343
+ customEventStreams.set(prop, eventStream);
4344
+ }
4345
+ return eventStream;
4346
+ } });
4347
+ const encodeText = (s) => new TextEncoder().encode(s);
4348
+ const buildWebhookResult = (asWeb, result) => {
4349
+ if (asWeb) return new Response(result.body, {
4350
+ status: result.status,
4351
+ headers: result.headers
4352
+ });
4353
+ return result;
4354
+ };
4355
+ const readWebhookInput = async (request) => {
4356
+ if (typeof Request !== "undefined" && request instanceof Request) {
4357
+ const headers = {};
4358
+ for (const [key, value] of request.headers) headers[key.toLowerCase()] = value;
4359
+ return {
4360
+ asWeb: true,
4361
+ bodyBytes: new Uint8Array(await request.arrayBuffer()),
4362
+ headers
4363
+ };
4364
+ }
4365
+ const raw = request;
4366
+ const bodyBytes = raw.body instanceof ArrayBuffer ? new Uint8Array(raw.body) : raw.body;
4367
+ const headers = {};
4368
+ for (const [key, value] of Object.entries(raw.headers ?? {})) headers[key.toLowerCase()] = String(value);
4369
+ return {
4370
+ asWeb: false,
4371
+ bodyBytes,
4372
+ headers
4373
+ };
4374
+ };
4375
+ const deliverWebhookMessages = async (collected, runtime, handler, context) => {
4376
+ for (const record of collected) {
4377
+ const tuples = await resolveRecordToMessages(record, runtime);
4378
+ for (const [space, message] of tuples) try {
4379
+ await handler(space, message);
4380
+ } catch (error) {
4381
+ lifecycleLog.error("spectrum.webhook: handler threw (async)", {
4382
+ "spectrum.webhook.event_id": context.eventId,
4383
+ "spectrum.webhook.platform": context.platform,
4384
+ "spectrum.webhook.message_id": message.id,
4385
+ ...errorAttrs(error)
4386
+ }, error);
4387
+ }
4388
+ }
4389
+ };
4390
+ const decodeWebhookEvent = (bodyBytes) => {
4391
+ try {
4392
+ return RawInboundEvent.decode(bodyBytes);
4393
+ } catch (error) {
4394
+ lifecycleLog.warn("spectrum.webhook: undecodable RawInboundEvent body", errorAttrs(error), error);
4395
+ return null;
4396
+ }
4397
+ };
4398
+ const processWebhookEvent = async (core, event, handler) => {
4399
+ const collected = [];
4400
+ const reply = await core.processEvent(event, (record) => {
4401
+ collected.push(record);
4402
+ });
4403
+ if (reply.errorReason) return {
4404
+ status: 400,
4405
+ headers: reply.headers ?? {},
4406
+ body: encodeText(reply.errorReason)
4407
+ };
4408
+ const result = {
4409
+ status: reply.status === 0 ? 200 : reply.status,
4410
+ headers: reply.headers ?? {},
4411
+ body: reply.body ?? new Uint8Array(0)
4412
+ };
4413
+ const runtime = platformStates.get(event.platform);
4414
+ if (runtime && collected.length > 0) deliverWebhookMessages(collected, runtime, handler, event).catch((error) => {
4415
+ lifecycleLog.error("spectrum.webhook: delivery failed (async)", {
4416
+ "spectrum.webhook.event_id": event.eventId,
4417
+ "spectrum.webhook.platform": event.platform,
4418
+ ...errorAttrs(error)
4419
+ }, error);
4420
+ });
4421
+ return result;
4422
+ };
4423
+ const looksLikeNativePayload = (bodyBytes) => {
4424
+ for (const byte of bodyBytes) {
4425
+ if (byte === 32 || byte === 9 || byte === 10 || byte === 13) continue;
4426
+ return byte === 123;
4427
+ }
4428
+ return false;
4429
+ };
4430
+ const webhookText = (status, text) => ({
4431
+ status,
4432
+ headers: {},
4433
+ body: encodeText(text)
4434
+ });
4435
+ const resolveWebhookAttachment = (platform, spaceRef, attachmentId) => {
4436
+ const runtime = platformStates.get(platform);
4437
+ const action = (runtime?.definition)?.actions?.getAttachment;
4438
+ if (!runtime || typeof action !== "function") return;
4439
+ const getAttachment = action;
4440
+ const phone = typeof spaceRef.phone === "string" ? spaceRef.phone : void 0;
4441
+ let cached;
4442
+ const fetchOnce = () => {
4443
+ cached ??= getAttachment({
4444
+ client: runtime.client,
4445
+ config: runtime.config,
4446
+ store: runtime.store
4447
+ }, attachmentId, phone);
4448
+ return cached;
4449
+ };
4450
+ return {
4451
+ read: async () => {
4452
+ const found = await fetchOnce();
4453
+ if (!found) throw new Error(`Spectrum webhook attachment "${attachmentId}" not found on "${platform}"`);
4454
+ return found.read();
4455
+ },
4456
+ stream: async () => {
4457
+ const found = await fetchOnce();
4458
+ if (!found?.stream) throw new Error(`Spectrum webhook attachment "${attachmentId}" has no stream on "${platform}"`);
4459
+ return found.stream();
4460
+ }
4461
+ };
4462
+ };
4463
+ const handleSpectrumWebhook = async (bodyBytes, headers, handler) => {
4464
+ if (!resolvedWebhookSecret) {
4465
+ lifecycleLog.error("spectrum.webhook: received a signed Spectrum webhook but no webhookSecret is configured (set Spectrum({ webhookSecret }) or SPECTRUM_WEBHOOK_SECRET)", { "spectrum.webhook.reason": "missing-secret" });
4466
+ return webhookText(500, "webhook secret not configured");
4467
+ }
4468
+ const verification = verifySpectrumSignature({
4469
+ rawBody: bodyBytes,
4470
+ headers,
4471
+ secret: resolvedWebhookSecret
4472
+ });
4473
+ if (!verification.ok) return webhookText(verification.reason === "missing-headers" ? 400 : 401, verification.reason);
4474
+ let envelope;
4475
+ try {
4476
+ const parsed = JSON.parse(new TextDecoder().decode(bodyBytes));
4477
+ envelope = slimEnvelopeSchema.parse(parsed);
4478
+ } catch (error) {
4479
+ lifecycleLog.warn("spectrum.webhook: malformed Spectrum webhook payload", errorAttrs(error), error);
4480
+ return webhookText(400, "malformed payload");
4481
+ }
4482
+ const deserialized = deserializeSpectrumMessage(envelope, { resolveAttachment: resolveWebhookAttachment });
4483
+ if (!deserialized) return webhookText(200, "ok");
4484
+ const { platform, record } = deserialized;
4485
+ const runtime = platformStates.get(platform);
4486
+ if (!runtime) {
4487
+ lifecycleLog.warn(`spectrum.webhook: no provider configured for platform "${platform}"; acknowledging without delivery`, { "spectrum.webhook.platform": platform });
4488
+ return webhookText(200, "ok");
4489
+ }
4490
+ deliverWebhookMessages([record], runtime, handler, { platform }).catch((error) => {
4491
+ lifecycleLog.error("spectrum.webhook: Spectrum delivery failed (async)", {
4492
+ "spectrum.webhook.platform": platform,
4493
+ "spectrum.webhook.message_id": record.id,
4494
+ ...errorAttrs(error)
4495
+ }, error);
4496
+ });
4497
+ return webhookText(200, "ok");
4498
+ };
4499
+ const handleWebhook = async (request, handler) => {
4500
+ const { asWeb, bodyBytes, headers } = await readWebhookInput(request);
4501
+ if (looksLikeNativePayload(bodyBytes)) return buildWebhookResult(asWeb, await handleSpectrumWebhook(bodyBytes, headers, handler));
4502
+ if (!fusorCore) throw new Error("spectrum.webhook() received a non-Spectrum (fusor) request but no fusor provider is configured");
4503
+ const event = decodeWebhookEvent(bodyBytes);
4504
+ if (!event) return buildWebhookResult(asWeb, {
4505
+ status: 400,
4506
+ headers: {},
4507
+ body: new Uint8Array(0)
4508
+ });
4509
+ return buildWebhookResult(asWeb, await processWebhookEvent(fusorCore, event, handler));
4510
+ };
4511
+ return new Proxy({
4512
+ __providers: providers,
4513
+ __internal: { platforms: platformStates },
4514
+ config: projectConfig,
4515
+ messages,
4516
+ stop: stopOnce,
4517
+ webhook: handleWebhook,
4518
+ send: (async (space, ...content) => content.length === 1 ? await space.send(content[0]) : await space.send(...content)),
4519
+ edit: async (message, newContent) => {
4520
+ await message.edit(newContent);
4521
+ },
4522
+ responding: async (space, fn) => space.responding(fn)
4523
+ }, { get(target, prop, receiver) {
4524
+ if (prop in target) return Reflect.get(target, prop, receiver);
4525
+ if (typeof prop === "string") return customEventProxy[prop];
4526
+ } });
4527
+ }
4528
+ //#endregion
4529
+ export { Emoji, Spectrum, SpectrumCloudError, UnsupportedError, app, appLayoutSchema, attachment, avatar, broadcast, cloud, contact, custom, definePlatform, edit, fromVCard, fusor, fusorEvent, group, isFusorClient, isFusorEvent, markdown, mergeStreams, option, poll, reaction, read, rename, reply, resolveContents, richlink, stream, text, toVCard, typing, unsend, voice };