spectrum-ts 4.0.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.
Files changed (35) hide show
  1. package/dist/{attachment-CEpGtZLm.d.ts → attachment-CnivEhr6.d.ts} +1 -1
  2. package/dist/{authoring-CP3vRza8.d.ts → authoring-b9AhXgPI.d.ts} +2 -2
  3. package/dist/authoring.d.ts +3 -3
  4. package/dist/authoring.js +3 -3
  5. package/dist/{chunk-PV4AVMNN.js → chunk-ARL2NOBO.js} +4 -4
  6. package/dist/{chunk-OGTHPDG7.js → chunk-B52VPQO3.js} +69 -29
  7. package/dist/{chunk-5VCWWPFW.js → chunk-DMPDLSFU.js} +8 -4
  8. package/dist/{chunk-57NECZQZ.js → chunk-N6THJDZV.js} +6 -4
  9. package/dist/{chunk-W5HNZ7YT.js → chunk-NLMQ75LH.js} +33 -76
  10. package/dist/{chunk-VEF6FUE7.js → chunk-WXLQNANA.js} +4 -1
  11. package/dist/elysia.d.ts +94 -0
  12. package/dist/elysia.js +15 -0
  13. package/dist/express.d.ts +62 -0
  14. package/dist/express.js +19 -0
  15. package/dist/hono.d.ts +64 -0
  16. package/dist/hono.js +11 -0
  17. package/dist/index.d.ts +33 -18
  18. package/dist/index.js +428 -36
  19. package/dist/providers/imessage/index.d.ts +10 -32
  20. package/dist/providers/imessage/index.js +7 -6
  21. package/dist/providers/index.d.ts +4 -4
  22. package/dist/providers/index.js +16 -16
  23. package/dist/providers/slack/index.d.ts +1 -1
  24. package/dist/providers/slack/index.js +2 -2
  25. package/dist/providers/telegram/index.d.ts +2 -2
  26. package/dist/providers/telegram/index.js +3 -3
  27. package/dist/providers/terminal/index.d.ts +1 -1
  28. package/dist/providers/terminal/index.js +3 -3
  29. package/dist/providers/whatsapp-business/index.d.ts +1 -1
  30. package/dist/providers/whatsapp-business/index.js +4 -4
  31. package/dist/read-C4uvozGX.d.ts +53 -0
  32. package/dist/{types-Be0T6E0e.d.ts → types-CyfLJXgu.d.ts} +26 -1
  33. package/dist/{types-CDYXH2R7.d.ts → types-ZgFTj5hJ.d.ts} +11 -6
  34. package/package.json +25 -1
  35. package/dist/photo-content-BJKnqgN-.d.ts +0 -13
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,
@@ -44,14 +46,17 @@ import {
44
46
  definePlatform,
45
47
  edit,
46
48
  markdown,
49
+ read,
47
50
  rename,
48
51
  reply,
49
52
  senderAttrs,
50
53
  typing,
51
54
  unsend,
52
55
  wrapProviderMessage
53
- } from "./chunk-OGTHPDG7.js";
56
+ } from "./chunk-B52VPQO3.js";
54
57
  import {
58
+ asAttachment,
59
+ asCustom,
55
60
  attachment,
56
61
  custom,
57
62
  reaction,
@@ -1995,7 +2000,7 @@ import {
1995
2000
  withSpan
1996
2001
  } from "@photon-ai/otel";
1997
2002
  import { RawInboundEvent } from "@photon-ai/proto/photon/fusor/v1/inbound";
1998
- import z from "zod";
2003
+ import z2 from "zod";
1999
2004
 
2000
2005
  // src/build-env.ts
2001
2006
  var SPECTRUM_SDK_VERSION = "local";
@@ -2706,28 +2711,284 @@ function createStore() {
2706
2711
  };
2707
2712
  }
2708
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
+
2709
2968
  // src/spectrum.ts
2710
2969
  var PHOTON_OTEL_ENDPOINT = "https://otlp.photon.codes";
2711
2970
  var STREAM_CLOSE_TIMEOUT_MS = 5e3;
2712
2971
  var lifecycleLog = createLogger3("spectrum.lifecycle");
2713
2972
  var ignoreCleanupError = () => void 0;
2714
- var spectrumOptionsSchema = z.object({
2715
- flattenGroups: z.boolean().optional()
2973
+ var spectrumOptionsSchema = z2.object({
2974
+ flattenGroups: z2.boolean().optional()
2716
2975
  }).optional();
2717
- var spectrumConfigSchema = z.union([
2718
- z.object({
2719
- projectId: z.string().min(1),
2720
- projectSecret: z.string().min(1),
2721
- 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()),
2722
2981
  options: spectrumOptionsSchema,
2723
- telemetry: z.boolean().optional()
2982
+ telemetry: z2.boolean().optional(),
2983
+ webhookSecret: z2.string().min(1).optional()
2724
2984
  }),
2725
- z.object({
2726
- projectId: z.undefined().optional(),
2727
- projectSecret: z.undefined().optional(),
2728
- providers: z.array(z.custom()),
2985
+ z2.object({
2986
+ projectId: z2.undefined().optional(),
2987
+ projectSecret: z2.undefined().optional(),
2988
+ providers: z2.array(z2.custom()),
2729
2989
  options: spectrumOptionsSchema,
2730
- telemetry: z.boolean().optional()
2990
+ telemetry: z2.boolean().optional(),
2991
+ webhookSecret: z2.string().min(1).optional()
2731
2992
  })
2732
2993
  ]);
2733
2994
  function bootstrapTelemetry(opts) {
@@ -2757,9 +3018,11 @@ async function Spectrum(options) {
2757
3018
  projectSecret,
2758
3019
  providers,
2759
3020
  options: runtimeOptions,
2760
- telemetry
3021
+ telemetry,
3022
+ webhookSecret
2761
3023
  } = options;
2762
3024
  const flattenGroups = runtimeOptions?.flattenGroups ?? false;
3025
+ const resolvedWebhookSecret = webhookSecret ?? process.env.SPECTRUM_WEBHOOK_SECRET;
2763
3026
  const otelHandle = telemetry ? bootstrapTelemetry({ projectId, projectSecret }) : void 0;
2764
3027
  const projectConfig = projectId !== void 0 && projectSecret !== void 0 ? await cloud.getProject(projectId, projectSecret) : void 0;
2765
3028
  const platformStates = /* @__PURE__ */ new Map();
@@ -3216,16 +3479,25 @@ async function Spectrum(options) {
3216
3479
  };
3217
3480
  const readWebhookInput = async (request) => {
3218
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
+ }
3219
3486
  return {
3220
3487
  asWeb: true,
3221
- bodyBytes: new Uint8Array(await request.arrayBuffer())
3488
+ bodyBytes: new Uint8Array(await request.arrayBuffer()),
3489
+ headers: headers2
3222
3490
  };
3223
3491
  }
3224
3492
  const raw = request;
3225
3493
  const bodyBytes = raw.body instanceof ArrayBuffer ? new Uint8Array(raw.body) : raw.body;
3226
- 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 };
3227
3499
  };
3228
- const deliverWebhookMessages = async (collected, runtime, handler, event) => {
3500
+ const deliverWebhookMessages = async (collected, runtime, handler, context) => {
3229
3501
  for (const record of collected) {
3230
3502
  const tuples = await resolveRecordToMessages(record, runtime);
3231
3503
  for (const [space, message] of tuples) {
@@ -3235,8 +3507,8 @@ async function Spectrum(options) {
3235
3507
  lifecycleLog.error(
3236
3508
  `spectrum.webhook: handler threw (async), ${error}`,
3237
3509
  {
3238
- eventId: event.eventId,
3239
- platform: event.platform,
3510
+ eventId: context.eventId,
3511
+ platform: context.platform,
3240
3512
  messageId: message.id,
3241
3513
  error: error instanceof Error ? error.message : String(error)
3242
3514
  }
@@ -3289,13 +3561,132 @@ async function Spectrum(options) {
3289
3561
  }
3290
3562
  return result;
3291
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
+ };
3292
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
+ }
3293
3685
  if (!fusorCore) {
3294
3686
  throw new Error(
3295
- "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"
3296
3688
  );
3297
3689
  }
3298
- const { asWeb, bodyBytes } = await readWebhookInput(request);
3299
3690
  const event = decodeWebhookEvent(bodyBytes);
3300
3691
  if (!event) {
3301
3692
  return buildWebhookResult(asWeb, {
@@ -3358,6 +3749,7 @@ export {
3358
3749
  option,
3359
3750
  poll,
3360
3751
  reaction,
3752
+ read,
3361
3753
  rename,
3362
3754
  reply,
3363
3755
  resolveContents,
@@ -1,9 +1,10 @@
1
- import { C as ContentBuilder, h as ContentInput, M as Message, S as SchemaMessage, P as Platform, a as PlatformDef, e as Space, c as Store } from '../../types-Be0T6E0e.js';
1
+ import { C as ContentBuilder, h as ContentInput, a as SchemaMessage, P as Platform, b as PlatformDef, S as Space, d as Store } from '../../types-CyfLJXgu.js';
2
2
  import * as zod_v4_core from 'zod/v4/core';
3
3
  import * as z from 'zod';
4
4
  import z__default from 'zod';
5
- import { A as Attachment } from '../../attachment-CEpGtZLm.js';
6
- import { P as PhotoInput } from '../../photo-content-BJKnqgN-.js';
5
+ import { A as Attachment } from '../../attachment-CnivEhr6.js';
6
+ import { P as PhotoInput } from '../../read-C4uvozGX.js';
7
+ export { r as read } from '../../read-C4uvozGX.js';
7
8
  import { MessageEffect, AdvancedIMessage } from '@photon-ai/advanced-imessage';
8
9
  import { IMessageSDK } from '@photon-ai/imessage-kit';
9
10
  import 'hotscript';
@@ -114,26 +115,6 @@ declare function customizedMiniApp(input: CustomizedMiniAppInput): ContentBuilde
114
115
  type IMessageMessageEffect = MessageEffect;
115
116
  declare function effect(input: ContentInput, messageEffect: IMessageMessageEffect): ContentBuilder;
116
117
 
117
- /**
118
- * Mark the chat containing `target` as read. iMessage-only, remote-only.
119
- *
120
- * Implemented via `chats.markRead(chatGuid)`, which marks **every unread
121
- * message in the chat** as read — there is no per-message read receipt in
122
- * the SDK. `target` is used only to identify the chat (and to give
123
- * `message.read()` something to pass), so passing any message from a chat
124
- * marks the whole chat as read.
125
- *
126
- * `space.send(read(message))` is the canonical form; `space.read(message)`
127
- * and `message.read()` are sugar attached via the iMessage platform's
128
- * `space.actions` / `message.actions` slots.
129
- *
130
- * `Read` is intentionally not a member of the universal `Content` union —
131
- * the `as unknown as Content` cast keeps the builder shape compatible with
132
- * the framework's `ContentBuilder.build(): Promise<Content>` signature. The
133
- * framework treats it as a fire-and-forget control signal at runtime.
134
- */
135
- declare function read(target: Message): ContentBuilder;
136
-
137
118
  interface RemoteClient {
138
119
  client: AdvancedIMessage;
139
120
  phone: string;
@@ -143,8 +124,8 @@ declare const userSchema: z__default.ZodObject<{}, z__default.core.$strip>;
143
124
  declare const spaceSchema: z__default.ZodObject<{
144
125
  id: z__default.ZodString;
145
126
  type: z__default.ZodEnum<{
146
- group: "group";
147
127
  dm: "dm";
128
+ group: "group";
148
129
  }>;
149
130
  phone: z__default.ZodString;
150
131
  }, z__default.core.$strip>;
@@ -170,8 +151,8 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
170
151
  }, zod_v4_core.$strip>]>, z.ZodType<object, unknown, zod_v4_core.$ZodTypeInternals<object, unknown>> | undefined, z.ZodObject<{
171
152
  id: z.ZodString;
172
153
  type: z.ZodEnum<{
173
- group: "group";
174
154
  dm: "dm";
155
+ group: "group";
175
156
  }>;
176
157
  phone: z.ZodString;
177
158
  }, zod_v4_core.$strip>, z.ZodObject<{
@@ -180,7 +161,7 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
180
161
  id: string;
181
162
  }, {
182
163
  id: string;
183
- type: "group" | "dm";
164
+ type: "dm" | "group";
184
165
  phone: string;
185
166
  }, z.ZodObject<{
186
167
  partIndex: z.ZodOptional<z.ZodNumber>;
@@ -189,10 +170,7 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
189
170
  background: (space: Space, input: BackgroundInput, opts?: {
190
171
  mimeType?: string;
191
172
  }) => Promise<void>;
192
- read: (space: Space, message: Message) => Promise<void>;
193
- }, {
194
- read: (message: Message) => Promise<void>;
195
- }, {
173
+ }, Record<never, never>, {
196
174
  getMessage: ({ client }: {
197
175
  client: IMessageClient;
198
176
  config: {
@@ -212,7 +190,7 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
212
190
  store: Store;
213
191
  }, space: {
214
192
  id: string;
215
- type: "group" | "dm";
193
+ type: "dm" | "group";
216
194
  phone: string;
217
195
  } & {
218
196
  id: string;
@@ -241,4 +219,4 @@ declare const imessage: Platform<PlatformDef<"iMessage", z.ZodUnion<readonly [z.
241
219
  };
242
220
  }>;
243
221
 
244
- export { type BackgroundInput, type CustomizedMiniApp, type CustomizedMiniAppInput, type CustomizedMiniAppLayout, type IMessageMessageEffect, background, customizedMiniApp, effect, imessage, read };
222
+ export { type BackgroundInput, type CustomizedMiniApp, type CustomizedMiniAppInput, type CustomizedMiniAppLayout, type IMessageMessageEffect, background, customizedMiniApp, effect, imessage };
@@ -3,17 +3,18 @@ import {
3
3
  background,
4
4
  customizedMiniApp,
5
5
  effect,
6
- imessage,
7
- read
8
- } from "../../chunk-W5HNZ7YT.js";
6
+ imessage
7
+ } from "../../chunk-NLMQ75LH.js";
9
8
  import "../../chunk-ZR3TKZMT.js";
10
9
  import "../../chunk-LZXPLXZF.js";
11
10
  import "../../chunk-2D27WW5B.js";
12
- import "../../chunk-3GEJYGZK.js";
13
11
  import "../../chunk-A37PM5N2.js";
14
- import "../../chunk-5XEFJBN2.js";
15
12
  import "../../chunk-6UZFVXQF.js";
16
- import "../../chunk-OGTHPDG7.js";
13
+ import "../../chunk-3GEJYGZK.js";
14
+ import "../../chunk-5XEFJBN2.js";
15
+ import {
16
+ read
17
+ } from "../../chunk-B52VPQO3.js";
17
18
  import "../../chunk-UXAKIXVM.js";
18
19
  export {
19
20
  background,