spectrum-ts 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/authoring.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
+ import {
3
+ asVoice
4
+ } from "./chunk-FAIFTUV2.js";
2
5
  import {
3
6
  asRichlink
4
7
  } from "./chunk-ZR3TKZMT.js";
5
8
  import {
6
9
  asGroup
7
10
  } from "./chunk-LZXPLXZF.js";
8
- import {
9
- asVoice
10
- } from "./chunk-FAIFTUV2.js";
11
11
  import {
12
12
  asPoll,
13
13
  asPollOption
@@ -0,0 +1,94 @@
1
+ import { Elysia } from 'elysia';
2
+ import { W as WebhookHandler } from './types-ZgFTj5hJ.js';
3
+ export { M as Message, S as Space } from './types-CyfLJXgu.js';
4
+ import 'hotscript';
5
+ import 'zod';
6
+
7
+ /**
8
+ * The minimal structural surface of a Spectrum instance the plugin needs. Kept
9
+ * structural (rather than importing the generic `SpectrumInstance<Providers>`)
10
+ * so the plugin stays decoupled from provider typing; a real instance is
11
+ * assignable via its Web `Request` webhook overload.
12
+ */
13
+ interface WebhookReceiver {
14
+ webhook(request: Request, handler: WebhookHandler): Promise<Response>;
15
+ }
16
+ interface SpectrumPluginOptions {
17
+ /** The Spectrum instance returned by `await Spectrum({...})`. */
18
+ app: WebhookReceiver;
19
+ /**
20
+ * Invoked once per inbound message, fire-and-forget after the response — the
21
+ * same `(space, message)` contract as `app.webhook(request, handler)`. Covers
22
+ * both native Spectrum webhooks and fusor webhooks identically.
23
+ */
24
+ onMessage: WebhookHandler;
25
+ /**
26
+ * Route the webhook is mounted on.
27
+ *
28
+ * @default "/spectrum/webhook"
29
+ */
30
+ path?: string;
31
+ }
32
+ /**
33
+ * Mount a Spectrum webhook endpoint on an Elysia app.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { Elysia } from "elysia";
38
+ * import { Spectrum } from "spectrum-ts";
39
+ * import { spectrum } from "spectrum-ts/elysia";
40
+ *
41
+ * const app = await Spectrum({ ..., webhookSecret: process.env.SPECTRUM_WEBHOOK_SECRET });
42
+ *
43
+ * new Elysia()
44
+ * .use(spectrum({
45
+ * app,
46
+ * onMessage: async (space, message) => {
47
+ * if (message.content.type === "text") await space.send(`echo: ${message.content.text}`);
48
+ * },
49
+ * }))
50
+ * .listen(3000);
51
+ * ```
52
+ */
53
+ declare function spectrum(options: SpectrumPluginOptions): Elysia<"", {
54
+ decorator: {};
55
+ store: {};
56
+ derive: {};
57
+ resolve: {};
58
+ }, {
59
+ typebox: {};
60
+ error: {};
61
+ }, {
62
+ schema: {};
63
+ standaloneSchema: {};
64
+ macro: {};
65
+ macroFn: {};
66
+ parser: {};
67
+ response: {};
68
+ }, {
69
+ [x: string]: {
70
+ post: {
71
+ body: unknown;
72
+ params: {};
73
+ query: unknown;
74
+ headers: unknown;
75
+ response: {
76
+ 200: Response;
77
+ };
78
+ };
79
+ };
80
+ }, {
81
+ derive: {};
82
+ resolve: {};
83
+ schema: {};
84
+ standaloneSchema: {};
85
+ response: {};
86
+ }, {
87
+ derive: {};
88
+ resolve: {};
89
+ schema: {};
90
+ standaloneSchema: {};
91
+ response: {};
92
+ }>;
93
+
94
+ export { type SpectrumPluginOptions, WebhookHandler, spectrum };
package/dist/elysia.js ADDED
@@ -0,0 +1,15 @@
1
+ import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
+
3
+ // src/elysia.ts
4
+ import { Elysia } from "elysia";
5
+ function spectrum(options) {
6
+ const { app, onMessage, path = "/spectrum/webhook" } = options;
7
+ return new Elysia({ name: "spectrum-webhook", seed: path }).post(
8
+ path,
9
+ ({ request }) => app.webhook(request, onMessage),
10
+ { parse: "none" }
11
+ );
12
+ }
13
+ export {
14
+ spectrum
15
+ };
@@ -0,0 +1,62 @@
1
+ import { Router } from 'express';
2
+ import { W as WebhookHandler } from './types-ZgFTj5hJ.js';
3
+ export { M as Message, S as Space } from './types-CyfLJXgu.js';
4
+ import 'hotscript';
5
+ import 'zod';
6
+
7
+ /**
8
+ * The minimal structural surface of a Spectrum instance the plugin needs. Kept
9
+ * structural (rather than importing the generic `SpectrumInstance<Providers>`)
10
+ * so the plugin stays decoupled from provider typing; a real instance is
11
+ * assignable via its raw (`{ body, headers }`) webhook overload.
12
+ */
13
+ interface WebhookReceiver {
14
+ webhook(request: {
15
+ body: Uint8Array | ArrayBuffer;
16
+ headers?: Record<string, string>;
17
+ }, handler: WebhookHandler): Promise<{
18
+ body: Uint8Array;
19
+ headers: Record<string, string>;
20
+ status: number;
21
+ }>;
22
+ }
23
+ interface SpectrumPluginOptions {
24
+ /** The Spectrum instance returned by `await Spectrum({...})`. */
25
+ app: WebhookReceiver;
26
+ /**
27
+ * Invoked once per inbound message, fire-and-forget after the response — the
28
+ * same `(space, message)` contract as `app.webhook(request, handler)`. Covers
29
+ * both native Spectrum webhooks and fusor webhooks identically.
30
+ */
31
+ onMessage: WebhookHandler;
32
+ /**
33
+ * Route the webhook is mounted on.
34
+ *
35
+ * @default "/spectrum/webhook"
36
+ */
37
+ path?: string;
38
+ }
39
+ /**
40
+ * Mount a Spectrum webhook endpoint on an Express app.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import express from "express";
45
+ * import { Spectrum } from "spectrum-ts";
46
+ * import { spectrum } from "spectrum-ts/express";
47
+ *
48
+ * const app = await Spectrum({ ..., webhookSecret: process.env.SPECTRUM_WEBHOOK_SECRET });
49
+ *
50
+ * const server = express();
51
+ * server.use(spectrum({ // mount before any global express.json()
52
+ * app,
53
+ * onMessage: async (space, message) => {
54
+ * if (message.content.type === "text") await space.send(`echo: ${message.content.text}`);
55
+ * },
56
+ * }));
57
+ * server.listen(3000);
58
+ * ```
59
+ */
60
+ declare function spectrum(options: SpectrumPluginOptions): Router;
61
+
62
+ export { type SpectrumPluginOptions, WebhookHandler, spectrum };
@@ -0,0 +1,19 @@
1
+ import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
+
3
+ // src/express.ts
4
+ import express from "express";
5
+ function spectrum(options) {
6
+ const { app, onMessage, path = "/spectrum/webhook" } = options;
7
+ const router = express.Router();
8
+ router.post(path, express.raw({ type: "*/*" }), async (req, res) => {
9
+ const result = await app.webhook(
10
+ { body: req.body, headers: req.headers },
11
+ onMessage
12
+ );
13
+ res.status(result.status).set(result.headers).send(Buffer.from(result.body));
14
+ });
15
+ return router;
16
+ }
17
+ export {
18
+ spectrum
19
+ };
package/dist/hono.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ import * as hono_hono_base from 'hono/hono-base';
2
+ import * as hono_utils_http_status from 'hono/utils/http-status';
3
+ import * as hono_types from 'hono/types';
4
+ import { W as WebhookHandler } from './types-ZgFTj5hJ.js';
5
+ export { M as Message, S as Space } from './types-CyfLJXgu.js';
6
+ import 'hotscript';
7
+ import 'zod';
8
+
9
+ /**
10
+ * The minimal structural surface of a Spectrum instance the plugin needs. Kept
11
+ * structural (rather than importing the generic `SpectrumInstance<Providers>`)
12
+ * so the plugin stays decoupled from provider typing; a real instance is
13
+ * assignable via its Web `Request` webhook overload.
14
+ */
15
+ interface WebhookReceiver {
16
+ webhook(request: Request, handler: WebhookHandler): Promise<Response>;
17
+ }
18
+ interface SpectrumPluginOptions {
19
+ /** The Spectrum instance returned by `await Spectrum({...})`. */
20
+ app: WebhookReceiver;
21
+ /**
22
+ * Invoked once per inbound message, fire-and-forget after the response — the
23
+ * same `(space, message)` contract as `app.webhook(request, handler)`. Covers
24
+ * both native Spectrum webhooks and fusor webhooks identically.
25
+ */
26
+ onMessage: WebhookHandler;
27
+ /**
28
+ * Route the webhook is mounted on.
29
+ *
30
+ * @default "/spectrum/webhook"
31
+ */
32
+ path?: string;
33
+ }
34
+ /**
35
+ * Mount a Spectrum webhook endpoint on a Hono app.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { Hono } from "hono";
40
+ * import { Spectrum } from "spectrum-ts";
41
+ * import { spectrum } from "spectrum-ts/hono";
42
+ *
43
+ * const app = await Spectrum({ ..., webhookSecret: process.env.SPECTRUM_WEBHOOK_SECRET });
44
+ *
45
+ * const server = new Hono().route("/", spectrum({
46
+ * app,
47
+ * onMessage: async (space, message) => {
48
+ * if (message.content.type === "text") await space.send(`echo: ${message.content.text}`);
49
+ * },
50
+ * }));
51
+ * ```
52
+ */
53
+ declare function spectrum(options: SpectrumPluginOptions): hono_hono_base.HonoBase<hono_types.BlankEnv, {
54
+ [x: string]: {
55
+ $post: {
56
+ input: {};
57
+ output: {};
58
+ outputFormat: string;
59
+ status: hono_utils_http_status.StatusCode;
60
+ };
61
+ };
62
+ }, "/", string>;
63
+
64
+ export { type SpectrumPluginOptions, WebhookHandler, spectrum };
package/dist/hono.js ADDED
@@ -0,0 +1,11 @@
1
+ import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
+
3
+ // src/hono.ts
4
+ import { Hono } from "hono";
5
+ function spectrum(options) {
6
+ const { app, onMessage, path = "/spectrum/webhook" } = options;
7
+ return new Hono().post(path, (c) => app.webhook(c.req.raw, onMessage));
8
+ }
9
+ export {
10
+ spectrum
11
+ };
package/dist/index.d.ts CHANGED
@@ -6,8 +6,8 @@ import { C as ContentBuilder, M as Message, U as User, S as Space, h as ContentI
6
6
  export { p as AnyPlatformDef, B as Broadcaster, q as CloudPlatform, D as DedicatedTokenData, F as FusorTokenData, r as ImessageInfoData, s as ManagedStream, t as PlatformInstance, u as PlatformMessage, v as PlatformRuntime, w as PlatformSpace, x as PlatformStatus, y as PlatformUser, z as PlatformsData, G as ProjectProfile, R as Reaction, H as ReactionBuilder, a as SchemaMessage, J as SharedTokenData, K as SlackTeamMeta, L as SlackTokenData, N as SpaceNamespace, O as SpectrumCloudError, Q as SubscriptionData, T as SubscriptionStatus, V as TokenData, W as broadcast, X as cloud, Y as mergeStreams, Z as reaction, _ as stream } from './types-CyfLJXgu.js';
7
7
  import { S as StreamTextSource, T as TextStreamOptions, C as ContactInput, a as Contact } from './authoring-b9AhXgPI.js';
8
8
  export { b as ContactAddress, c as ContactDetails, d as ContactEmail, e as ContactName, f as ContactOrg, g as ContactPhone, D as DeltaExtractor, G as Group, P as Poll, h as PollChoice, i as PollChoiceInput, j as PollOption, R as Richlink, k as StreamText, V as Voice, l as contact, m as custom, n as group, o as option, p as poll, r as richlink, t as text, v as voice } from './authoring-b9AhXgPI.js';
9
- import { a as FusorVerify, F as FusorClient, b as FusorMessages, W as WebhookHandler, c as WebhookRawRequest, d as WebhookRawResult } from './types-BIta6Kxi.js';
10
- export { e as FusorEvent, f as FusorMessagesCtx, g as FusorMessagesReturn, h as FusorReply, i as FusorRespond, j as FusorVerifyRequest, k as fusorEvent, l as isFusorEvent } from './types-BIta6Kxi.js';
9
+ import { a as FusorVerify, F as FusorClient, b as FusorMessages, W as WebhookHandler, c as WebhookRawRequest, d as WebhookRawResult } from './types-ZgFTj5hJ.js';
10
+ export { e as FusorEvent, f as FusorMessagesCtx, g as FusorMessagesReturn, h as FusorReply, i as FusorRespond, j as FusorVerifyRequest, k as fusorEvent, l as isFusorEvent } from './types-ZgFTj5hJ.js';
11
11
  import 'hotscript';
12
12
  import 'vcf';
13
13
 
@@ -2757,12 +2757,23 @@ type SpectrumInstance<Providers extends PlatformProviderConfig[] = PlatformProvi
2757
2757
  edit(message: Message, newContent: ContentInput): Promise<void>;
2758
2758
  responding<T>(space: Space, fn: () => T | Promise<T>): Promise<T>;
2759
2759
  /**
2760
- * Handle one inbound fusor webhook delivery. Call this from your HTTP
2761
- * server's POST route — it verifies, decodes, routes to the matching
2762
- * provider's verify + message pipeline, and returns the HTTP response fusor
2763
- * relays back to the platform: the platform's `respond()` reply (including
2764
- * protocol echoes like Slack `url_verification`), computed synchronously and
2765
- * returned immediately.
2760
+ * Handle one inbound webhook delivery. Call this from your HTTP server's
2761
+ * POST route — it auto-detects which of the two Spectrum webhook formats the
2762
+ * request carries and routes accordingly:
2763
+ *
2764
+ * - **Native Spectrum webhook** (the body is normalized JSON): Spectrum Cloud
2765
+ * POSTs already-normalized, HMAC-signed JSON. The signature is verified
2766
+ * against `Spectrum({ webhookSecret })` (a bad signature → 401), the slim
2767
+ * payload is deserialized into `[space, message]`, and a `200` is returned.
2768
+ * Works without any fusor provider configured.
2769
+ * - **Fusor webhook** (the body is a protobuf envelope): a protobuf wrapping a
2770
+ * raw provider request is decoded and routed to the matching provider's
2771
+ * verify + message pipeline; the HTTP response is that platform's
2772
+ * `respond()` reply (including protocol echoes like Slack
2773
+ * `url_verification`), computed synchronously and returned immediately.
2774
+ *
2775
+ * Detection is by payload shape, not headers — Spectrum signs both kinds with
2776
+ * `X-Spectrum-Signature`, so the header can't discriminate.
2766
2777
  *
2767
2778
  * `handler` is invoked once per resolved message **fire-and-forget** — it is
2768
2779
  * dispatched after the response is computed and is NOT awaited, so its
@@ -2772,8 +2783,9 @@ type SpectrumInstance<Providers extends PlatformProviderConfig[] = PlatformProvi
2772
2783
  * and process in a separate worker).
2773
2784
  *
2774
2785
  * Stateless and request-scoped: it does NOT feed `spectrum.messages`, and it
2775
- * never opens the gRPC stream. fusor delivers at-least-once, so `handler`
2776
- * should dedupe on `message`/the event id for exactly-once side effects.
2786
+ * never opens the streaming connection. Both formats deliver at-least-once,
2787
+ * so `handler` should dedupe on `message`/the event id for exactly-once side
2788
+ * effects.
2777
2789
  */
2778
2790
  webhook(request: Request, handler: WebhookHandler): Promise<Response>;
2779
2791
  webhook(request: WebhookRawRequest, handler: WebhookHandler): Promise<WebhookRawResult>;
@@ -2800,6 +2812,7 @@ declare function Spectrum<const Providers extends PlatformProviderConfig[]>(opti
2800
2812
  providers: [...Providers];
2801
2813
  options?: SpectrumOptions;
2802
2814
  telemetry?: boolean;
2815
+ webhookSecret?: string;
2803
2816
  }): Promise<SpectrumInstance<Providers> & {
2804
2817
  readonly config: ProjectData;
2805
2818
  }>;
@@ -2809,6 +2822,7 @@ declare function Spectrum<const Providers extends PlatformProviderConfig[]>(opti
2809
2822
  providers: [...Providers];
2810
2823
  options?: SpectrumOptions;
2811
2824
  telemetry?: boolean;
2825
+ webhookSecret?: string;
2812
2826
  }): Promise<SpectrumInstance<Providers>>;
2813
2827
 
2814
2828
  type UnsupportedKind = "content" | "action";
package/dist/index.js CHANGED
@@ -1,7 +1,4 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
- import {
3
- richlink
4
- } from "./chunk-ZR3TKZMT.js";
5
2
  import {
6
3
  FUSOR_MESSAGES_CHANNEL,
7
4
  fusor,
@@ -9,33 +6,38 @@ import {
9
6
  isFusorClient,
10
7
  isFusorEvent
11
8
  } from "./chunk-34FQGGD7.js";
12
- import {
13
- group
14
- } from "./chunk-LZXPLXZF.js";
15
9
  import {
16
10
  voice
17
11
  } from "./chunk-FAIFTUV2.js";
12
+ import {
13
+ asRichlink,
14
+ richlink
15
+ } from "./chunk-ZR3TKZMT.js";
16
+ import {
17
+ group
18
+ } from "./chunk-LZXPLXZF.js";
18
19
  import {
19
20
  option,
20
21
  poll
21
22
  } from "./chunk-2D27WW5B.js";
23
+ import {
24
+ asContact,
25
+ contact
26
+ } from "./chunk-A37PM5N2.js";
27
+ import {
28
+ fromVCard,
29
+ toVCard
30
+ } from "./chunk-6UZFVXQF.js";
22
31
  import {
23
32
  SpectrumCloudError,
24
33
  cloud
25
34
  } from "./chunk-3GEJYGZK.js";
26
- import {
27
- contact
28
- } from "./chunk-A37PM5N2.js";
29
35
  import {
30
36
  broadcast,
31
37
  createAsyncQueue,
32
38
  mergeStreams,
33
39
  stream
34
40
  } from "./chunk-5XEFJBN2.js";
35
- import {
36
- fromVCard,
37
- toVCard
38
- } from "./chunk-6UZFVXQF.js";
39
41
  import {
40
42
  UnsupportedError,
41
43
  avatar,
@@ -53,6 +55,8 @@ import {
53
55
  wrapProviderMessage
54
56
  } from "./chunk-B52VPQO3.js";
55
57
  import {
58
+ asAttachment,
59
+ asCustom,
56
60
  attachment,
57
61
  custom,
58
62
  reaction,
@@ -1996,7 +2000,7 @@ import {
1996
2000
  withSpan
1997
2001
  } from "@photon-ai/otel";
1998
2002
  import { RawInboundEvent } from "@photon-ai/proto/photon/fusor/v1/inbound";
1999
- import z from "zod";
2003
+ import z2 from "zod";
2000
2004
 
2001
2005
  // src/build-env.ts
2002
2006
  var SPECTRUM_SDK_VERSION = "local";
@@ -2707,28 +2711,284 @@ function createStore() {
2707
2711
  };
2708
2712
  }
2709
2713
 
2714
+ // src/webhook/deserialize.ts
2715
+ var MESSAGES_EVENT = "messages";
2716
+ var DEFAULT_ATTACHMENT_NAME = "attachment";
2717
+ var DEFAULT_MIME_TYPE = "application/octet-stream";
2718
+ var isRecord = (value) => typeof value === "object" && value !== null;
2719
+ var asString = (value) => typeof value === "string" ? value : "";
2720
+ var asOptionalDate = (value) => typeof value === "string" ? new Date(value) : void 0;
2721
+ function deserializeSpectrumMessage(envelope, ctx) {
2722
+ if (envelope.event !== MESSAGES_EVENT) {
2723
+ return null;
2724
+ }
2725
+ const message = envelope.message;
2726
+ const platform = resolvePlatform(message);
2727
+ if (!platform) {
2728
+ return null;
2729
+ }
2730
+ const spaceRef = { ...message.space };
2731
+ return {
2732
+ platform,
2733
+ record: {
2734
+ id: message.id,
2735
+ direction: "inbound",
2736
+ content: deserializeContent(message.content, platform, spaceRef, ctx),
2737
+ space: spaceRef,
2738
+ sender: message.sender ? { ...message.sender } : void 0,
2739
+ timestamp: asOptionalDate(message.timestamp)
2740
+ }
2741
+ };
2742
+ }
2743
+ var resolvePlatform = (message) => message.platform ?? message.space.platform;
2744
+ var deserializeContent = (content, platform, spaceRef, ctx) => {
2745
+ try {
2746
+ return mapContent(content, platform, spaceRef, ctx);
2747
+ } catch {
2748
+ return asCustom(content);
2749
+ }
2750
+ };
2751
+ var mapContent = (content, platform, spaceRef, ctx) => {
2752
+ const raw = content;
2753
+ switch (content.type) {
2754
+ case "text":
2755
+ return { type: "text", text: asString(raw.text) };
2756
+ case "richlink":
2757
+ return asRichlink({ url: asString(raw.url) });
2758
+ case "contact":
2759
+ return deserializeContact(raw);
2760
+ case "reaction":
2761
+ return deserializeReaction(raw, spaceRef);
2762
+ case "group":
2763
+ return deserializeGroup(raw, platform, spaceRef, ctx);
2764
+ case "attachment":
2765
+ return deserializeAttachment(raw, platform, spaceRef, ctx);
2766
+ default:
2767
+ return asCustom(content);
2768
+ }
2769
+ };
2770
+ var deserializeAttachment = (raw, platform, spaceRef, ctx) => {
2771
+ const id = asString(raw.id);
2772
+ const bytes = ctx.resolveAttachment?.(platform, spaceRef, id);
2773
+ const unavailable = () => Promise.reject(
2774
+ UnsupportedError.action(
2775
+ "getAttachment",
2776
+ platform,
2777
+ `attachment "${id}" arrived without bytes over the Spectrum webhook and "${platform}" exposes no getAttachment`
2778
+ )
2779
+ );
2780
+ return asAttachment({
2781
+ id,
2782
+ name: asString(raw.name) || DEFAULT_ATTACHMENT_NAME,
2783
+ mimeType: asString(raw.mimeType) || DEFAULT_MIME_TYPE,
2784
+ size: typeof raw.size === "number" ? raw.size : void 0,
2785
+ read: bytes ? bytes.read : unavailable,
2786
+ stream: bytes?.stream
2787
+ });
2788
+ };
2789
+ var deserializeReaction = (raw, spaceRef) => (
2790
+ // `target` is a raw record; `wrapNestedContent` wraps it into a Message.
2791
+ {
2792
+ type: "reaction",
2793
+ emoji: asString(raw.emoji),
2794
+ target: buildTargetRecord(raw.target, spaceRef)
2795
+ }
2796
+ );
2797
+ var buildTargetRecord = (target, spaceRef) => {
2798
+ const ref = isRecord(target) ? target : {};
2799
+ return {
2800
+ id: asString(ref.id),
2801
+ // The target's full content is not delivered; the 80-char `contentPreview`
2802
+ // (text targets only) is the best available stand-in.
2803
+ content: { type: "text", text: asString(ref.contentPreview) },
2804
+ space: { ...spaceRef },
2805
+ sender: ref.sender ? { ...ref.sender } : void 0,
2806
+ timestamp: asOptionalDate(ref.timestamp)
2807
+ };
2808
+ };
2809
+ var deserializeGroup = (raw, platform, spaceRef, ctx) => {
2810
+ const rawItems = Array.isArray(raw.items) ? raw.items : [];
2811
+ return {
2812
+ type: "group",
2813
+ items: rawItems.map(
2814
+ (item) => buildItemRecord(item, platform, spaceRef, ctx)
2815
+ )
2816
+ };
2817
+ };
2818
+ var buildItemRecord = (item, platform, spaceRef, ctx) => {
2819
+ const record = isRecord(item) ? item : {};
2820
+ const itemSpace = isRecord(record.space) ? {
2821
+ ...record.space,
2822
+ id: asString(record.space.id) || spaceRef.id
2823
+ } : spaceRef;
2824
+ const content = isRecord(record.content) ? deserializeContent(
2825
+ record.content,
2826
+ platform,
2827
+ itemSpace,
2828
+ ctx
2829
+ ) : asCustom(record.content);
2830
+ return {
2831
+ id: asString(record.id),
2832
+ content,
2833
+ space: itemSpace,
2834
+ sender: isRecord(record.sender) ? { ...record.sender, id: asString(record.sender.id) } : void 0,
2835
+ timestamp: asOptionalDate(record.timestamp)
2836
+ };
2837
+ };
2838
+ var deserializeContact = (raw) => {
2839
+ const input = {};
2840
+ const name = normalizeContactName(raw.name);
2841
+ if (name) {
2842
+ input.name = name;
2843
+ }
2844
+ const phones = normalizeContactPhones(raw.phones);
2845
+ if (phones) {
2846
+ input.phones = phones;
2847
+ }
2848
+ if (typeof raw.note === "string") {
2849
+ input.note = raw.note;
2850
+ }
2851
+ if (raw.raw !== void 0) {
2852
+ input.raw = raw.raw;
2853
+ }
2854
+ return asContact(input);
2855
+ };
2856
+ var CONTACT_NAME_KEYS = [
2857
+ "formatted",
2858
+ "first",
2859
+ "last",
2860
+ "middle",
2861
+ "prefix",
2862
+ "suffix"
2863
+ ];
2864
+ var normalizeContactName = (value) => {
2865
+ if (typeof value === "string") {
2866
+ return { formatted: value };
2867
+ }
2868
+ if (!isRecord(value)) {
2869
+ return;
2870
+ }
2871
+ const name = {};
2872
+ for (const key of CONTACT_NAME_KEYS) {
2873
+ const part = value[key];
2874
+ if (typeof part === "string") {
2875
+ name[key] = part;
2876
+ }
2877
+ }
2878
+ return Object.keys(name).length > 0 ? name : void 0;
2879
+ };
2880
+ var normalizeContactPhones = (value) => {
2881
+ if (!Array.isArray(value)) {
2882
+ return;
2883
+ }
2884
+ const phones = [];
2885
+ for (const entry of value) {
2886
+ if (typeof entry === "string") {
2887
+ phones.push({ value: entry });
2888
+ } else if (isRecord(entry) && typeof entry.value === "string") {
2889
+ phones.push({ value: entry.value });
2890
+ }
2891
+ }
2892
+ return phones.length > 0 ? phones : void 0;
2893
+ };
2894
+
2895
+ // src/webhook/types.ts
2896
+ import z from "zod";
2897
+ var slimSenderSchema = z.looseObject({
2898
+ id: z.string(),
2899
+ platform: z.string().optional()
2900
+ });
2901
+ var slimSpaceSchema = z.looseObject({
2902
+ id: z.string(),
2903
+ platform: z.string().optional()
2904
+ });
2905
+ var slimContentSchema = z.looseObject({
2906
+ type: z.string()
2907
+ });
2908
+ var slimMessageRefSchema = z.looseObject({
2909
+ id: z.string(),
2910
+ platform: z.string().optional(),
2911
+ timestamp: z.string().optional(),
2912
+ sender: slimSenderSchema.optional(),
2913
+ contentPreview: z.string().optional()
2914
+ });
2915
+ var slimMessageSchema = z.looseObject({
2916
+ id: z.string(),
2917
+ platform: z.string().optional(),
2918
+ // Webhooks are inbound-only; `direction` is left loose (not a `"inbound"`
2919
+ // literal) so a future direction value cannot fail an older SDK's parse.
2920
+ direction: z.string().optional(),
2921
+ timestamp: z.string().optional(),
2922
+ sender: slimSenderSchema.optional(),
2923
+ space: slimSpaceSchema,
2924
+ content: slimContentSchema
2925
+ });
2926
+ var slimEnvelopeSchema = z.looseObject({
2927
+ event: z.string(),
2928
+ space: slimSpaceSchema.optional(),
2929
+ message: slimMessageSchema
2930
+ });
2931
+
2932
+ // src/webhook/verify.ts
2933
+ import { createHmac, timingSafeEqual } from "crypto";
2934
+ var SIGNATURE_HEADER = "x-spectrum-signature";
2935
+ var TIMESTAMP_HEADER = "x-spectrum-timestamp";
2936
+ var SIGNATURE_PREFIX = "v0=";
2937
+ var SIGNATURE_SCHEME = "v0";
2938
+ var REPLAY_TOLERANCE_SECONDS = 300;
2939
+ var MILLIS_PER_SECOND = 1e3;
2940
+ function verifySpectrumSignature(input) {
2941
+ const { rawBody, headers, secret, now = Date.now() } = input;
2942
+ const provided = headers[SIGNATURE_HEADER];
2943
+ const timestamp = headers[TIMESTAMP_HEADER];
2944
+ if (!(provided && timestamp)) {
2945
+ return { ok: false, reason: "missing-headers" };
2946
+ }
2947
+ const timestampSeconds = Number(timestamp);
2948
+ if (!Number.isFinite(timestampSeconds)) {
2949
+ return { ok: false, reason: "missing-headers" };
2950
+ }
2951
+ const nowSeconds = Math.floor(now / MILLIS_PER_SECOND);
2952
+ if (Math.abs(nowSeconds - timestampSeconds) > REPLAY_TOLERANCE_SECONDS) {
2953
+ return { ok: false, reason: "expired" };
2954
+ }
2955
+ const base = Buffer.concat([
2956
+ Buffer.from(`${SIGNATURE_SCHEME}:${timestamp}:`, "utf8"),
2957
+ Buffer.from(rawBody)
2958
+ ]);
2959
+ const expected = createHmac("sha256", secret).update(base).digest();
2960
+ const providedHex = provided.startsWith(SIGNATURE_PREFIX) ? provided.slice(SIGNATURE_PREFIX.length) : provided;
2961
+ const providedBytes = Buffer.from(providedHex, "hex");
2962
+ if (providedBytes.length !== expected.length || !timingSafeEqual(providedBytes, expected)) {
2963
+ return { ok: false, reason: "signature-mismatch" };
2964
+ }
2965
+ return { ok: true };
2966
+ }
2967
+
2710
2968
  // src/spectrum.ts
2711
2969
  var PHOTON_OTEL_ENDPOINT = "https://otlp.photon.codes";
2712
2970
  var STREAM_CLOSE_TIMEOUT_MS = 5e3;
2713
2971
  var lifecycleLog = createLogger3("spectrum.lifecycle");
2714
2972
  var ignoreCleanupError = () => void 0;
2715
- var spectrumOptionsSchema = z.object({
2716
- flattenGroups: z.boolean().optional()
2973
+ var spectrumOptionsSchema = z2.object({
2974
+ flattenGroups: z2.boolean().optional()
2717
2975
  }).optional();
2718
- var spectrumConfigSchema = z.union([
2719
- z.object({
2720
- projectId: z.string().min(1),
2721
- projectSecret: z.string().min(1),
2722
- providers: z.array(z.custom()),
2976
+ var spectrumConfigSchema = z2.union([
2977
+ z2.object({
2978
+ projectId: z2.string().min(1),
2979
+ projectSecret: z2.string().min(1),
2980
+ providers: z2.array(z2.custom()),
2723
2981
  options: spectrumOptionsSchema,
2724
- telemetry: z.boolean().optional()
2982
+ telemetry: z2.boolean().optional(),
2983
+ webhookSecret: z2.string().min(1).optional()
2725
2984
  }),
2726
- z.object({
2727
- projectId: z.undefined().optional(),
2728
- projectSecret: z.undefined().optional(),
2729
- providers: z.array(z.custom()),
2985
+ z2.object({
2986
+ projectId: z2.undefined().optional(),
2987
+ projectSecret: z2.undefined().optional(),
2988
+ providers: z2.array(z2.custom()),
2730
2989
  options: spectrumOptionsSchema,
2731
- telemetry: z.boolean().optional()
2990
+ telemetry: z2.boolean().optional(),
2991
+ webhookSecret: z2.string().min(1).optional()
2732
2992
  })
2733
2993
  ]);
2734
2994
  function bootstrapTelemetry(opts) {
@@ -2758,9 +3018,11 @@ async function Spectrum(options) {
2758
3018
  projectSecret,
2759
3019
  providers,
2760
3020
  options: runtimeOptions,
2761
- telemetry
3021
+ telemetry,
3022
+ webhookSecret
2762
3023
  } = options;
2763
3024
  const flattenGroups = runtimeOptions?.flattenGroups ?? false;
3025
+ const resolvedWebhookSecret = webhookSecret ?? process.env.SPECTRUM_WEBHOOK_SECRET;
2764
3026
  const otelHandle = telemetry ? bootstrapTelemetry({ projectId, projectSecret }) : void 0;
2765
3027
  const projectConfig = projectId !== void 0 && projectSecret !== void 0 ? await cloud.getProject(projectId, projectSecret) : void 0;
2766
3028
  const platformStates = /* @__PURE__ */ new Map();
@@ -3217,16 +3479,25 @@ async function Spectrum(options) {
3217
3479
  };
3218
3480
  const readWebhookInput = async (request) => {
3219
3481
  if (typeof Request !== "undefined" && request instanceof Request) {
3482
+ const headers2 = {};
3483
+ for (const [key, value] of request.headers) {
3484
+ headers2[key.toLowerCase()] = value;
3485
+ }
3220
3486
  return {
3221
3487
  asWeb: true,
3222
- bodyBytes: new Uint8Array(await request.arrayBuffer())
3488
+ bodyBytes: new Uint8Array(await request.arrayBuffer()),
3489
+ headers: headers2
3223
3490
  };
3224
3491
  }
3225
3492
  const raw = request;
3226
3493
  const bodyBytes = raw.body instanceof ArrayBuffer ? new Uint8Array(raw.body) : raw.body;
3227
- return { asWeb: false, bodyBytes };
3494
+ const headers = {};
3495
+ for (const [key, value] of Object.entries(raw.headers ?? {})) {
3496
+ headers[key.toLowerCase()] = String(value);
3497
+ }
3498
+ return { asWeb: false, bodyBytes, headers };
3228
3499
  };
3229
- const deliverWebhookMessages = async (collected, runtime, handler, event) => {
3500
+ const deliverWebhookMessages = async (collected, runtime, handler, context) => {
3230
3501
  for (const record of collected) {
3231
3502
  const tuples = await resolveRecordToMessages(record, runtime);
3232
3503
  for (const [space, message] of tuples) {
@@ -3236,8 +3507,8 @@ async function Spectrum(options) {
3236
3507
  lifecycleLog.error(
3237
3508
  `spectrum.webhook: handler threw (async), ${error}`,
3238
3509
  {
3239
- eventId: event.eventId,
3240
- platform: event.platform,
3510
+ eventId: context.eventId,
3511
+ platform: context.platform,
3241
3512
  messageId: message.id,
3242
3513
  error: error instanceof Error ? error.message : String(error)
3243
3514
  }
@@ -3290,13 +3561,132 @@ async function Spectrum(options) {
3290
3561
  }
3291
3562
  return result;
3292
3563
  };
3564
+ const looksLikeNativePayload = (bodyBytes) => {
3565
+ for (const byte of bodyBytes) {
3566
+ if (byte === 32 || byte === 9 || byte === 10 || byte === 13) {
3567
+ continue;
3568
+ }
3569
+ return byte === 123;
3570
+ }
3571
+ return false;
3572
+ };
3573
+ const webhookText = (status, text2) => ({
3574
+ status,
3575
+ headers: {},
3576
+ body: encodeText(text2)
3577
+ });
3578
+ const resolveWebhookAttachment = (platform, spaceRef, attachmentId) => {
3579
+ const runtime = platformStates.get(platform);
3580
+ const action = runtime?.definition?.actions?.getAttachment;
3581
+ if (!runtime || typeof action !== "function") {
3582
+ return;
3583
+ }
3584
+ const getAttachment = action;
3585
+ const phone = typeof spaceRef.phone === "string" ? spaceRef.phone : void 0;
3586
+ let cached;
3587
+ const fetchOnce = () => {
3588
+ cached ??= getAttachment(
3589
+ {
3590
+ client: runtime.client,
3591
+ config: runtime.config,
3592
+ store: runtime.store
3593
+ },
3594
+ attachmentId,
3595
+ phone
3596
+ );
3597
+ return cached;
3598
+ };
3599
+ return {
3600
+ read: async () => {
3601
+ const found = await fetchOnce();
3602
+ if (!found) {
3603
+ throw new Error(
3604
+ `Spectrum webhook attachment "${attachmentId}" not found on "${platform}"`
3605
+ );
3606
+ }
3607
+ return found.read();
3608
+ },
3609
+ stream: async () => {
3610
+ const found = await fetchOnce();
3611
+ if (!found?.stream) {
3612
+ throw new Error(
3613
+ `Spectrum webhook attachment "${attachmentId}" has no stream on "${platform}"`
3614
+ );
3615
+ }
3616
+ return found.stream();
3617
+ }
3618
+ };
3619
+ };
3620
+ const handleSpectrumWebhook = async (bodyBytes, headers, handler) => {
3621
+ if (!resolvedWebhookSecret) {
3622
+ lifecycleLog.error(
3623
+ "spectrum.webhook: received a signed Spectrum webhook but no webhookSecret is configured (set Spectrum({ webhookSecret }) or SPECTRUM_WEBHOOK_SECRET)"
3624
+ );
3625
+ return webhookText(500, "webhook secret not configured");
3626
+ }
3627
+ const verification = verifySpectrumSignature({
3628
+ rawBody: bodyBytes,
3629
+ headers,
3630
+ secret: resolvedWebhookSecret
3631
+ });
3632
+ if (!verification.ok) {
3633
+ const status = verification.reason === "missing-headers" ? 400 : 401;
3634
+ return webhookText(status, verification.reason);
3635
+ }
3636
+ let envelope;
3637
+ try {
3638
+ const parsed = JSON.parse(new TextDecoder().decode(bodyBytes));
3639
+ envelope = slimEnvelopeSchema.parse(parsed);
3640
+ } catch (error) {
3641
+ lifecycleLog.warn(
3642
+ `spectrum.webhook: malformed Spectrum webhook payload, ${error}`
3643
+ );
3644
+ return webhookText(400, "malformed payload");
3645
+ }
3646
+ const deserialized = deserializeSpectrumMessage(envelope, {
3647
+ resolveAttachment: resolveWebhookAttachment
3648
+ });
3649
+ if (!deserialized) {
3650
+ return webhookText(200, "ok");
3651
+ }
3652
+ const { platform, record } = deserialized;
3653
+ const runtime = platformStates.get(platform);
3654
+ if (!runtime) {
3655
+ lifecycleLog.warn(
3656
+ `spectrum.webhook: no provider configured for platform "${platform}"; acknowledging without delivery`,
3657
+ { platform }
3658
+ );
3659
+ return webhookText(200, "ok");
3660
+ }
3661
+ deliverWebhookMessages([record], runtime, handler, { platform }).catch(
3662
+ (error) => {
3663
+ lifecycleLog.error(
3664
+ `spectrum.webhook: Spectrum delivery failed (async), ${error}`,
3665
+ {
3666
+ platform,
3667
+ messageId: record.id,
3668
+ error: error instanceof Error ? error.message : String(error)
3669
+ }
3670
+ );
3671
+ }
3672
+ );
3673
+ return webhookText(200, "ok");
3674
+ };
3293
3675
  const handleWebhook = async (request, handler) => {
3676
+ const { asWeb, bodyBytes, headers } = await readWebhookInput(request);
3677
+ if (looksLikeNativePayload(bodyBytes)) {
3678
+ const spectrumResult = await handleSpectrumWebhook(
3679
+ bodyBytes,
3680
+ headers,
3681
+ handler
3682
+ );
3683
+ return buildWebhookResult(asWeb, spectrumResult);
3684
+ }
3294
3685
  if (!fusorCore) {
3295
3686
  throw new Error(
3296
- "spectrum.webhook() requires at least one fusor provider; none are configured"
3687
+ "spectrum.webhook() received a non-Spectrum (fusor) request but no fusor provider is configured"
3297
3688
  );
3298
3689
  }
3299
- const { asWeb, bodyBytes } = await readWebhookInput(request);
3300
3690
  const event = decodeWebhookEvent(bodyBytes);
3301
3691
  if (!event) {
3302
3692
  return buildWebhookResult(asWeb, {
@@ -124,8 +124,8 @@ declare const userSchema: z__default.ZodObject<{}, z__default.core.$strip>;
124
124
  declare const spaceSchema: z__default.ZodObject<{
125
125
  id: z__default.ZodString;
126
126
  type: z__default.ZodEnum<{
127
- group: "group";
128
127
  dm: "dm";
128
+ group: "group";
129
129
  }>;
130
130
  phone: z__default.ZodString;
131
131
  }, z__default.core.$strip>;
@@ -151,8 +151,8 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
151
151
  }, zod_v4_core.$strip>]>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, z.ZodObject<{
152
152
  id: z.ZodString;
153
153
  type: z.ZodEnum<{
154
- group: "group";
155
154
  dm: "dm";
155
+ group: "group";
156
156
  }>;
157
157
  phone: z.ZodString;
158
158
  }, zod_v4_core.$strip>, z.ZodObject<{
@@ -161,7 +161,7 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
161
161
  id: string;
162
162
  }, {
163
163
  id: string;
164
- type: "group" | "dm";
164
+ type: "dm" | "group";
165
165
  phone: string;
166
166
  }, z.ZodObject<{
167
167
  partIndex: z.ZodOptional<z.ZodNumber>;
@@ -190,7 +190,7 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
190
190
  store: Store;
191
191
  }, space: {
192
192
  id: string;
193
- type: "group" | "dm";
193
+ type: "dm" | "group";
194
194
  phone: string;
195
195
  } & {
196
196
  id: string;
@@ -4,14 +4,14 @@ import {
4
4
  customizedMiniApp,
5
5
  effect,
6
6
  imessage
7
- } from "../../chunk-DMT6BFJV.js";
7
+ } from "../../chunk-NLMQ75LH.js";
8
8
  import "../../chunk-ZR3TKZMT.js";
9
9
  import "../../chunk-LZXPLXZF.js";
10
10
  import "../../chunk-2D27WW5B.js";
11
- import "../../chunk-3GEJYGZK.js";
12
11
  import "../../chunk-A37PM5N2.js";
13
- import "../../chunk-5XEFJBN2.js";
14
12
  import "../../chunk-6UZFVXQF.js";
13
+ import "../../chunk-3GEJYGZK.js";
14
+ import "../../chunk-5XEFJBN2.js";
15
15
  import {
16
16
  read
17
17
  } from "../../chunk-B52VPQO3.js";
@@ -13,7 +13,7 @@ import '@photon-ai/advanced-imessage';
13
13
  import '@photon-ai/imessage-kit';
14
14
  import '@photon-ai/slack';
15
15
  import '@photon-ai/telegram-ts';
16
- import '../types-BIta6Kxi.js';
16
+ import '../types-ZgFTj5hJ.js';
17
17
  import 'node:child_process';
18
18
  import 'node:net';
19
19
  import '@photon-ai/whatsapp-business';
@@ -1,28 +1,28 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
- import {
3
- imessage
4
- } from "../chunk-DMT6BFJV.js";
5
- import "../chunk-ZR3TKZMT.js";
6
- import {
7
- slack
8
- } from "../chunk-WXLQNANA.js";
9
2
  import {
10
3
  telegram
11
- } from "../chunk-U3QQ56YZ.js";
12
- import "../chunk-34FQGGD7.js";
13
- import "../chunk-LZXPLXZF.js";
4
+ } from "../chunk-N6THJDZV.js";
14
5
  import {
15
6
  terminal
16
- } from "../chunk-IM5ADDZS.js";
17
- import "../chunk-FAIFTUV2.js";
7
+ } from "../chunk-ARL2NOBO.js";
18
8
  import {
19
9
  whatsappBusiness
20
- } from "../chunk-3KWFP4L2.js";
10
+ } from "../chunk-DMPDLSFU.js";
11
+ import "../chunk-34FQGGD7.js";
12
+ import "../chunk-FAIFTUV2.js";
13
+ import {
14
+ imessage
15
+ } from "../chunk-NLMQ75LH.js";
16
+ import "../chunk-ZR3TKZMT.js";
17
+ import "../chunk-LZXPLXZF.js";
21
18
  import "../chunk-2D27WW5B.js";
22
- import "../chunk-3GEJYGZK.js";
23
19
  import "../chunk-A37PM5N2.js";
24
- import "../chunk-5XEFJBN2.js";
25
20
  import "../chunk-6UZFVXQF.js";
21
+ import {
22
+ slack
23
+ } from "../chunk-WXLQNANA.js";
24
+ import "../chunk-3GEJYGZK.js";
25
+ import "../chunk-5XEFJBN2.js";
26
26
  import "../chunk-B52VPQO3.js";
27
27
  import "../chunk-UXAKIXVM.js";
28
28
  export {
@@ -3,7 +3,7 @@ import * as _photon_ai_telegram_ts from '@photon-ai/telegram-ts';
3
3
  import * as zod_v4_core from 'zod/v4/core';
4
4
  import * as z from 'zod';
5
5
  import z__default from 'zod';
6
- import { F as FusorClient } from '../../types-BIta6Kxi.js';
6
+ import { F as FusorClient } from '../../types-ZgFTj5hJ.js';
7
7
  import 'hotscript';
8
8
 
9
9
  interface TelegramSpace {
@@ -1,10 +1,10 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
2
  import {
3
3
  telegram
4
- } from "../../chunk-U3QQ56YZ.js";
4
+ } from "../../chunk-N6THJDZV.js";
5
5
  import "../../chunk-34FQGGD7.js";
6
- import "../../chunk-LZXPLXZF.js";
7
6
  import "../../chunk-FAIFTUV2.js";
7
+ import "../../chunk-LZXPLXZF.js";
8
8
  import "../../chunk-6UZFVXQF.js";
9
9
  import "../../chunk-B52VPQO3.js";
10
10
  import "../../chunk-UXAKIXVM.js";
@@ -1,11 +1,11 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
2
  import {
3
3
  terminal
4
- } from "../../chunk-IM5ADDZS.js";
4
+ } from "../../chunk-ARL2NOBO.js";
5
5
  import "../../chunk-FAIFTUV2.js";
6
6
  import "../../chunk-A37PM5N2.js";
7
- import "../../chunk-5XEFJBN2.js";
8
7
  import "../../chunk-6UZFVXQF.js";
8
+ import "../../chunk-5XEFJBN2.js";
9
9
  import "../../chunk-B52VPQO3.js";
10
10
  import "../../chunk-UXAKIXVM.js";
11
11
  export {
@@ -1,12 +1,12 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
2
  import {
3
3
  whatsappBusiness
4
- } from "../../chunk-3KWFP4L2.js";
4
+ } from "../../chunk-DMPDLSFU.js";
5
5
  import "../../chunk-2D27WW5B.js";
6
- import "../../chunk-3GEJYGZK.js";
7
6
  import "../../chunk-A37PM5N2.js";
8
- import "../../chunk-5XEFJBN2.js";
9
7
  import "../../chunk-6UZFVXQF.js";
8
+ import "../../chunk-3GEJYGZK.js";
9
+ import "../../chunk-5XEFJBN2.js";
10
10
  import "../../chunk-B52VPQO3.js";
11
11
  import "../../chunk-UXAKIXVM.js";
12
12
  export {
@@ -62,11 +62,16 @@ interface FusorClient<TPayload = unknown> {
62
62
  type WebhookHandler = (space: Space, message: Message) => void | Promise<void>;
63
63
  /**
64
64
  * Raw webhook input for HTTP servers without Web `Request`/`Response` (Express,
65
- * raw Node). `body` MUST be the exact bytes fusor POSTed — never a re-encoded
66
- * JSON/text body — so the protobuf decode works. `headers` are accepted (so the
67
- * natural `{ headers: req.headers, body: req.body }` shape keeps working) but are
68
- * not read: inbound authenticity is established by the per-platform `verify()`,
69
- * which reads the inner request reconstructed from the envelope.
65
+ * raw Node). `body` MUST be the exact bytes POSTed — never a re-encoded
66
+ * JSON/text body — so both the protobuf decode (fusor) and the HMAC
67
+ * verification (native Spectrum webhook) work.
68
+ *
69
+ * `headers` ARE read for **native Spectrum webhooks**: `X-Spectrum-Signature` /
70
+ * `X-Spectrum-Timestamp` carry the HMAC verified against
71
+ * `Spectrum({ webhookSecret })`, and the signature header also selects the
72
+ * native path. For **fusor** envelopes they are ignored (authenticity is the
73
+ * per-platform `verify()` reading the inner reconstructed request). The natural
74
+ * `{ headers: req.headers, body: req.body }` shape works for both.
70
75
  */
71
76
  interface WebhookRawRequest {
72
77
  body: Uint8Array | ArrayBuffer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-ts",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "Bring agents to any interface — unified messaging SDK for TypeScript.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,6 +27,18 @@
27
27
  "types": "./dist/authoring.d.ts",
28
28
  "default": "./dist/authoring.js"
29
29
  },
30
+ "./elysia": {
31
+ "types": "./dist/elysia.d.ts",
32
+ "default": "./dist/elysia.js"
33
+ },
34
+ "./express": {
35
+ "types": "./dist/express.d.ts",
36
+ "default": "./dist/express.js"
37
+ },
38
+ "./hono": {
39
+ "types": "./dist/hono.d.ts",
40
+ "default": "./dist/hono.js"
41
+ },
30
42
  "./providers": {
31
43
  "types": "./dist/providers/index.d.ts",
32
44
  "default": "./dist/providers/index.js"
@@ -55,12 +67,24 @@
55
67
  "zod": "^4.2.1"
56
68
  },
57
69
  "peerDependencies": {
70
+ "elysia": "^1",
71
+ "express": "^4 || ^5",
58
72
  "ffmpeg-static": "^5",
73
+ "hono": "^4",
59
74
  "typescript": "^5 || ^6.0.0"
60
75
  },
61
76
  "peerDependenciesMeta": {
77
+ "elysia": {
78
+ "optional": true
79
+ },
80
+ "express": {
81
+ "optional": true
82
+ },
62
83
  "ffmpeg-static": {
63
84
  "optional": true
85
+ },
86
+ "hono": {
87
+ "optional": true
64
88
  }
65
89
  },
66
90
  "license": "MIT"
@@ -5,13 +5,13 @@ import {
5
5
  import {
6
6
  asContact
7
7
  } from "./chunk-A37PM5N2.js";
8
- import {
9
- stream
10
- } from "./chunk-5XEFJBN2.js";
11
8
  import {
12
9
  fromVCard,
13
10
  toVCard
14
11
  } from "./chunk-6UZFVXQF.js";
12
+ import {
13
+ stream
14
+ } from "./chunk-5XEFJBN2.js";
15
15
  import {
16
16
  UnsupportedError,
17
17
  definePlatform
@@ -2,12 +2,12 @@ import { createRequire as __spectrumCreateRequire } from "node:module"; const re
2
2
  import {
3
3
  asPollOption
4
4
  } from "./chunk-2D27WW5B.js";
5
- import {
6
- cloud
7
- } from "./chunk-3GEJYGZK.js";
8
5
  import {
9
6
  asContact
10
7
  } from "./chunk-A37PM5N2.js";
8
+ import {
9
+ cloud
10
+ } from "./chunk-3GEJYGZK.js";
11
11
  import {
12
12
  mergeStreams,
13
13
  stream
@@ -2,12 +2,12 @@ import { createRequire as __spectrumCreateRequire } from "node:module"; const re
2
2
  import {
3
3
  fusor
4
4
  } from "./chunk-34FQGGD7.js";
5
- import {
6
- asGroup
7
- } from "./chunk-LZXPLXZF.js";
8
5
  import {
9
6
  asVoice
10
7
  } from "./chunk-FAIFTUV2.js";
8
+ import {
9
+ asGroup
10
+ } from "./chunk-LZXPLXZF.js";
11
11
  import {
12
12
  toVCard
13
13
  } from "./chunk-6UZFVXQF.js";
@@ -10,20 +10,20 @@ import {
10
10
  asPoll,
11
11
  asPollOption
12
12
  } from "./chunk-2D27WW5B.js";
13
- import {
14
- cloud
15
- } from "./chunk-3GEJYGZK.js";
16
13
  import {
17
14
  asContact
18
15
  } from "./chunk-A37PM5N2.js";
19
- import {
20
- mergeStreams,
21
- stream
22
- } from "./chunk-5XEFJBN2.js";
23
16
  import {
24
17
  fromVCard,
25
18
  toVCard
26
19
  } from "./chunk-6UZFVXQF.js";
20
+ import {
21
+ cloud
22
+ } from "./chunk-3GEJYGZK.js";
23
+ import {
24
+ mergeStreams,
25
+ stream
26
+ } from "./chunk-5XEFJBN2.js";
27
27
  import {
28
28
  UnsupportedError,
29
29
  buildPhotoAction,