@spectrum-ts/whatsapp-business 12.0.0 → 12.1.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.d.ts CHANGED
@@ -16,7 +16,7 @@ type WhatsAppMessage = SchemaMessage<typeof userSchema, typeof spaceSchema>;
16
16
  * WhatsApp-only template message content. Lives entirely under the WhatsApp
17
17
  * Business provider — never enters the universal `Content` discriminated
18
18
  * union. The framework recognizes it via the generic content-level platform
19
- * contract: `__platform: "WhatsApp Business"` lets
19
+ * contract: `__platform: "whatsapp_business"` lets
20
20
  * `findUnsupportedPlatformContent` warn-and-skip when a different platform
21
21
  * receives it.
22
22
  *
@@ -29,7 +29,7 @@ type WhatsAppMessage = SchemaMessage<typeof userSchema, typeof spaceSchema>;
29
29
  */
30
30
  declare const whatsappTemplateSchema: z.ZodObject<{
31
31
  type: z.ZodLiteral<"whatsapp-template">;
32
- __platform: z.ZodLiteral<"WhatsApp Business">;
32
+ __platform: z.ZodLiteral<"whatsapp_business">;
33
33
  name: z.ZodString;
34
34
  languageCode: z.ZodString;
35
35
  bodyParams: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -53,7 +53,7 @@ declare class WhatsAppPartialSendError extends Error {
53
53
  }
54
54
  //#endregion
55
55
  //#region src/index.d.ts
56
- declare const whatsappBusiness: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"WhatsApp Business", import("zod").ZodUnion<readonly [import("zod").ZodObject<{
56
+ declare const whatsappBusiness: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"whatsapp_business", import("zod").ZodUnion<readonly [import("zod").ZodObject<{
57
57
  accessToken: import("zod").ZodString;
58
58
  appSecret: import("zod").ZodOptional<import("zod").ZodString>;
59
59
  phoneNumberId: import("zod").ZodString;
package/dist/index.js CHANGED
@@ -1,24 +1,15 @@
1
1
  import { TypedEventStream, button, buttons, createClient, list } from "@photon-ai/whatsapp-business";
2
2
  import { UnsupportedError, cloud, definePlatform, mergeStreams, stream } from "@spectrum-ts/core";
3
- import { asAttachment, asContact, asCustom, asGroup, asPollOption, asReaction, asReply, asText, asUnsend, asVoice, createLogger, errorAttrs, tracedFetch } from "@spectrum-ts/core/authoring";
3
+ import { asAttachment, asContact, asCustom, asGroup, asPollOption, asReaction, asReply, asText, asUnsend, asVoice, createLogger, createTokenRenewal, errorAttrs, tracedFetch } from "@spectrum-ts/core/authoring";
4
4
  import { extension } from "mime-types";
5
5
  import z from "zod";
6
6
  //#region src/auth.ts
7
- const log = createLogger("spectrum.whatsapp.auth");
8
7
  const streamLog$1 = createLogger("spectrum.whatsapp.stream");
9
- const RENEWAL_RATIO = .8;
10
- const EXPIRY_BUFFER_MS = 3e4;
11
- const RETRY_DELAY_MS = 3e4;
12
8
  const RESUBSCRIBE_BACKOFF_MS = 500;
13
9
  const ignoreCleanupError = () => void 0;
14
10
  const cloudAuthState = /* @__PURE__ */ new WeakMap();
15
11
  async function createCloudClients(projectId, projectSecret) {
16
12
  let tokenData = await cloud.issueWhatsappBusinessTokens(projectId, projectSecret);
17
- let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
18
- let disposed = false;
19
- let renewalTimer;
20
- let refreshFailures = 0;
21
- let refreshInFlight;
22
13
  const lines = /* @__PURE__ */ new Map();
23
14
  const buildRawClient = (phoneNumberId) => {
24
15
  const accessToken = tokenData.auth[phoneNumberId];
@@ -31,7 +22,6 @@ async function createCloudClients(projectId, projectSecret) {
31
22
  };
32
23
  const refreshTokens = async () => {
33
24
  tokenData = await cloud.issueWhatsappBusinessTokens(projectId, projectSecret);
34
- tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
35
25
  for (const [phoneNumberId, state] of lines) {
36
26
  if (!tokenData.auth[phoneNumberId]) continue;
37
27
  const old = state.current;
@@ -40,69 +30,21 @@ async function createCloudClients(projectId, projectSecret) {
40
30
  await old.close().catch(() => void 0);
41
31
  }
42
32
  };
43
- const onRefreshSuccess = () => {
44
- if (refreshFailures > 0) {
45
- log.info("whatsapp token refresh recovered", { "spectrum.whatsapp.auth.attempt": refreshFailures });
46
- refreshFailures = 0;
47
- }
48
- };
49
- const onRefreshFailure = (error) => {
50
- refreshFailures += 1;
51
- log.warn("whatsapp token refresh failed; retrying", {
52
- "spectrum.whatsapp.auth.attempt": refreshFailures,
53
- "spectrum.whatsapp.auth.retry_in_ms": RETRY_DELAY_MS,
54
- ...errorAttrs(error)
55
- }, error);
56
- };
57
- const clearRenewalTimer = () => {
58
- if (renewalTimer !== void 0) {
59
- clearTimeout(renewalTimer);
60
- renewalTimer = void 0;
61
- }
62
- };
63
- const refreshNow = async () => {
64
- await refreshTokens();
65
- onRefreshSuccess();
66
- scheduleRenewal();
67
- };
68
- const coalescedRefresh = () => {
69
- if (!refreshInFlight) refreshInFlight = refreshNow().finally(() => {
70
- refreshInFlight = void 0;
71
- });
72
- return refreshInFlight;
73
- };
74
- const scheduleRenewal = () => {
75
- if (disposed) return;
76
- clearRenewalTimer();
77
- const ttlMs = tokenData.expiresIn * 1e3;
78
- const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
79
- const runScheduledRefresh = () => {
80
- coalescedRefresh().catch((err) => {
81
- onRefreshFailure(err);
82
- if (disposed) return;
83
- renewalTimer = setTimeout(runScheduledRefresh, RETRY_DELAY_MS);
84
- renewalTimer?.unref?.();
85
- });
86
- };
87
- renewalTimer = setTimeout(runScheduledRefresh, renewInMs);
88
- renewalTimer?.unref?.();
89
- };
90
- const refreshIfNeeded = async () => {
91
- if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) return;
92
- await coalescedRefresh();
93
- };
94
- scheduleRenewal();
33
+ const renewal = createTokenRenewal({
34
+ expiresInSeconds: () => tokenData.expiresIn,
35
+ name: "whatsapp",
36
+ refresh: refreshTokens
37
+ });
95
38
  const clients = Object.keys(tokenData.auth).map((phoneNumberId) => {
96
39
  const state = {
97
40
  current: buildRawClient(phoneNumberId),
98
41
  subscriptions: /* @__PURE__ */ new Set()
99
42
  };
100
43
  lines.set(phoneNumberId, state);
101
- return buildClientProxy(state, refreshIfNeeded);
44
+ return buildClientProxy(state, renewal.refreshIfNeeded);
102
45
  });
103
46
  cloudAuthState.set(clients, { dispose: async () => {
104
- disposed = true;
105
- clearRenewalTimer();
47
+ renewal.dispose();
106
48
  for (const state of lines.values()) for (const sub of state.subscriptions) sub.close();
107
49
  await Promise.allSettled(Array.from(lines.values()).map((s) => s.current.close()));
108
50
  lines.clear();
@@ -256,7 +198,7 @@ const resubscribableStream = (state, options) => {
256
198
  * WhatsApp-only template message content. Lives entirely under the WhatsApp
257
199
  * Business provider — never enters the universal `Content` discriminated
258
200
  * union. The framework recognizes it via the generic content-level platform
259
- * contract: `__platform: "WhatsApp Business"` lets
201
+ * contract: `__platform: "whatsapp_business"` lets
260
202
  * `findUnsupportedPlatformContent` warn-and-skip when a different platform
261
203
  * receives it.
262
204
  *
@@ -269,7 +211,7 @@ const resubscribableStream = (state, options) => {
269
211
  */
270
212
  const whatsappTemplateSchema = z.object({
271
213
  type: z.literal("whatsapp-template"),
272
- __platform: z.literal("WhatsApp Business"),
214
+ __platform: z.literal("whatsapp_business"),
273
215
  name: z.string().min(1),
274
216
  languageCode: z.string().min(1),
275
217
  bodyParams: z.array(z.string()).optional()
@@ -277,7 +219,7 @@ const whatsappTemplateSchema = z.object({
277
219
  const isWhatsAppTemplate = (value) => whatsappTemplateSchema.safeParse(value).success;
278
220
  const asWhatsAppTemplate = (input) => whatsappTemplateSchema.parse({
279
221
  type: "whatsapp-template",
280
- __platform: "WhatsApp Business",
222
+ __platform: "whatsapp_business",
281
223
  ...input
282
224
  });
283
225
  function whatsappTemplate(input) {
@@ -948,9 +890,11 @@ const cloudConfig = z.object({}).strict();
948
890
  const configSchema = z.union([directConfig, cloudConfig]);
949
891
  const isCloudConfig = (config) => !("accessToken" in config);
950
892
  z.object({});
893
+ const spaceSchema = z.object({ id: z.string() });
951
894
  //#endregion
952
895
  //#region src/index.ts
953
- const whatsappBusiness = definePlatform("WhatsApp Business", {
896
+ const PLATFORM_ID = "whatsapp_business";
897
+ const whatsappBusiness = definePlatform(PLATFORM_ID, {
954
898
  config: configSchema,
955
899
  lifecycle: {
956
900
  createClient: async ({ config, projectId, projectSecret }) => {
@@ -969,10 +913,10 @@ const whatsappBusiness = definePlatform("WhatsApp Business", {
969
913
  },
970
914
  user: { resolve: async ({ input }) => ({ id: input.userID }) },
971
915
  space: {
972
- schema: z.object({ id: z.string() }),
916
+ schema: spaceSchema,
973
917
  create: async ({ input }) => {
974
918
  if (input.users.length === 0) throw new Error("WhatsApp space creation requires at least one user");
975
- if (input.users.length > 1) throw UnsupportedError.action("space.create", "WhatsApp Business", "only 1:1 conversations are supported");
919
+ if (input.users.length > 1) throw UnsupportedError.action("space.create", PLATFORM_ID, "only 1:1 conversations are supported");
976
920
  const user = input.users[0];
977
921
  if (!user) throw new Error("WhatsApp space creation requires a user");
978
922
  return { id: user.id };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/whatsapp-business",
3
- "version": "12.0.0",
3
+ "version": "12.1.0",
4
4
  "description": "WhatsApp Business provider for spectrum-ts.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,9 +26,9 @@
26
26
  "access": "public"
27
27
  },
28
28
  "spectrum": {
29
- "key": "whatsapp-business",
29
+ "key": "whatsapp_business",
30
30
  "import": "whatsappBusiness",
31
- "label": "WhatsApp Business"
31
+ "label": "whatsapp_business"
32
32
  },
33
33
  "dependencies": {
34
34
  "@photon-ai/whatsapp-business": "^0.2.0",