@spectrum-ts/imessage 11.2.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/README.md CHANGED
@@ -15,7 +15,7 @@ import { Spectrum } from "spectrum-ts";
15
15
  import { imessage } from "@spectrum-ts/imessage";
16
16
 
17
17
  const spectrum = Spectrum({
18
- platforms: [imessage.config()],
18
+ providers: [imessage.config()],
19
19
  });
20
20
  ```
21
21
 
package/dist/index.d.ts CHANGED
@@ -64,7 +64,7 @@ declare function background(input: string | Buffer | URL, options?: {
64
64
  * enters the universal `Content` discriminated union. The framework recognizes
65
65
  * it via two generic content-level contracts:
66
66
  *
67
- * 1. `__platform: "iMessage"` — `findUnsupportedPlatformContent` in
67
+ * 1. `__platform: "imessage"` — `findUnsupportedPlatformContent` in
68
68
  * `platform/build.ts` reads this tag and warns-and-skips when a different
69
69
  * platform receives it.
70
70
  * 2. `__fireAndForget: true` — `dispatchSend`'s fire-and-forget check treats
@@ -76,7 +76,7 @@ declare function background(input: string | Buffer | URL, options?: {
76
76
  */
77
77
  declare const contactCardSchema: z.ZodObject<{
78
78
  type: z.ZodLiteral<"contactCard">;
79
- __platform: z.ZodLiteral<"iMessage">;
79
+ __platform: z.ZodLiteral<"imessage">;
80
80
  __fireAndForget: z.ZodLiteral<true>;
81
81
  }, z.core.$strip>;
82
82
  type ContactCard = z.infer<typeof contactCardSchema>;
@@ -116,7 +116,7 @@ declare const layoutSchema: z.ZodObject<{
116
116
  * provider — never enters the universal `Content` discriminated union. The
117
117
  * framework recognizes it via the generic content-level platform contract:
118
118
  *
119
- * - `__platform: "iMessage"` — `findUnsupportedPlatformContent` reads this tag
119
+ * - `__platform: "imessage"` — `findUnsupportedPlatformContent` reads this tag
120
120
  * and warns-and-skips when a different platform receives it.
121
121
  *
122
122
  * Unlike `background` / `read`, this content is **not** `__fireAndForget`: it
@@ -126,7 +126,7 @@ declare const layoutSchema: z.ZodObject<{
126
126
  */
127
127
  declare const customizedMiniAppSchema: z.ZodObject<{
128
128
  type: z.ZodLiteral<"customized-mini-app">;
129
- __platform: z.ZodLiteral<"iMessage">;
129
+ __platform: z.ZodLiteral<"imessage">;
130
130
  appName: z.ZodString;
131
131
  appStoreId: z.ZodOptional<z.ZodNumber>;
132
132
  extensionBundleId: z.ZodString;
@@ -195,8 +195,8 @@ declare const userSchema: z.ZodObject<{
195
195
  address: z.ZodOptional<z.ZodString>;
196
196
  country: z.ZodOptional<z.ZodString>;
197
197
  service: z.ZodOptional<z.ZodEnum<{
198
- iMessage: "iMessage";
199
198
  unknown: "unknown";
199
+ iMessage: "iMessage";
200
200
  SMS: "SMS";
201
201
  RCS: "RCS";
202
202
  }>>;
@@ -233,7 +233,7 @@ type IMessageMessage = SchemaMessage<typeof userSchema, typeof spaceSchema> & {
233
233
  };
234
234
  //#endregion
235
235
  //#region src/index.d.ts
236
- declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"iMessage", import("zod").ZodObject<{
236
+ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-ts/core").PlatformDef<"imessage", import("zod").ZodObject<{
237
237
  clients: import("zod").ZodOptional<import("zod").ZodUnion<[import("zod").ZodObject<{
238
238
  address: import("zod").ZodString;
239
239
  token: import("zod").ZodString;
@@ -247,8 +247,8 @@ declare const imessage: import("@spectrum-ts/core").Platform<import("@spectrum-t
247
247
  address: import("zod").ZodOptional<import("zod").ZodString>;
248
248
  country: import("zod").ZodOptional<import("zod").ZodString>;
249
249
  service: import("zod").ZodOptional<import("zod").ZodEnum<{
250
- iMessage: "iMessage";
251
250
  unknown: "unknown";
251
+ iMessage: "iMessage";
252
252
  SMS: "SMS";
253
253
  RCS: "RCS";
254
254
  }>>;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { NotFoundError, ValidationError, createClient } from "@photon-ai/advanced-imessage";
2
2
  import { sanitizePhone, withSpan } from "@photon-ai/otel";
3
3
  import { UnsupportedError, appLayoutSchema, cloud, definePlatform, fromVCard, mergeStreams, read, text, toVCard } from "@spectrum-ts/core";
4
- import { addMemberSchema, asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asReply, asText, asVoice, avatarSchema, buildPhotoAction, createLogger, ensureM4a, errorAttrs, groupSchema, leaveSpaceSchema, messageEffectSchema, photoActionSchema, reactionSchema, removeMemberSchema, renameSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
4
+ import { addMemberSchema, asAttachment, asContact, asCustom, asGroup, asPoll, asPollOption, asReply, asText, asVoice, avatarSchema, buildPhotoAction, createLogger, createTokenRenewal, ensureM4a, errorAttrs, groupSchema, leaveSpaceSchema, messageEffectSchema, photoActionSchema, reactionSchema, removeMemberSchema, renameSchema, resumableOrderedStream, sanitizeErrorMessage } from "@spectrum-ts/core/authoring";
5
5
  import z from "zod";
6
6
  import { Marked } from "marked";
7
7
  import { LRUCache } from "lru-cache";
@@ -11,7 +11,7 @@ import { LRUCache } from "lru-cache";
11
11
  * provider — never enters the universal `Content` discriminated union. The
12
12
  * framework recognizes it via two generic content-level contracts:
13
13
  *
14
- * 1. `__platform: "iMessage"` — `findUnsupportedPlatformContent` in
14
+ * 1. `__platform: "imessage"` — `findUnsupportedPlatformContent` in
15
15
  * `platform/build.ts` reads this tag and warns-and-skips when a different
16
16
  * platform receives it.
17
17
  * 2. `__fireAndForget: true` — `dispatchSend`'s fire-and-forget check
@@ -23,7 +23,7 @@ import { LRUCache } from "lru-cache";
23
23
  */
24
24
  const backgroundSchema = z.object({
25
25
  type: z.literal("background"),
26
- __platform: z.literal("iMessage"),
26
+ __platform: z.literal("imessage"),
27
27
  __fireAndForget: z.literal(true),
28
28
  action: photoActionSchema
29
29
  });
@@ -32,7 +32,7 @@ function background(input, options) {
32
32
  const action = buildPhotoAction(input, options, "background");
33
33
  return { build: async () => backgroundSchema.parse({
34
34
  type: "background",
35
- __platform: "iMessage",
35
+ __platform: "imessage",
36
36
  __fireAndForget: true,
37
37
  action
38
38
  }) };
@@ -53,7 +53,7 @@ function background(input, options) {
53
53
  * enters the universal `Content` discriminated union. The framework recognizes
54
54
  * it via two generic content-level contracts:
55
55
  *
56
- * 1. `__platform: "iMessage"` — `findUnsupportedPlatformContent` in
56
+ * 1. `__platform: "imessage"` — `findUnsupportedPlatformContent` in
57
57
  * `platform/build.ts` reads this tag and warns-and-skips when a different
58
58
  * platform receives it.
59
59
  * 2. `__fireAndForget: true` — `dispatchSend`'s fire-and-forget check treats
@@ -65,7 +65,7 @@ function background(input, options) {
65
65
  */
66
66
  const contactCardSchema = z.object({
67
67
  type: z.literal("contactCard"),
68
- __platform: z.literal("iMessage"),
68
+ __platform: z.literal("imessage"),
69
69
  __fireAndForget: z.literal(true)
70
70
  });
71
71
  const isContactCard = (v) => contactCardSchema.safeParse(v).success;
@@ -90,7 +90,7 @@ const isContactCard = (v) => contactCardSchema.safeParse(v).success;
90
90
  function nativeContactCard() {
91
91
  return { build: async () => contactCardSchema.parse({
92
92
  type: "contactCard",
93
- __platform: "iMessage",
93
+ __platform: "imessage",
94
94
  __fireAndForget: true
95
95
  }) };
96
96
  }
@@ -102,7 +102,7 @@ const layoutSchema = appLayoutSchema;
102
102
  * provider — never enters the universal `Content` discriminated union. The
103
103
  * framework recognizes it via the generic content-level platform contract:
104
104
  *
105
- * - `__platform: "iMessage"` — `findUnsupportedPlatformContent` reads this tag
105
+ * - `__platform: "imessage"` — `findUnsupportedPlatformContent` reads this tag
106
106
  * and warns-and-skips when a different platform receives it.
107
107
  *
108
108
  * Unlike `background` / `read`, this content is **not** `__fireAndForget`: it
@@ -112,7 +112,7 @@ const layoutSchema = appLayoutSchema;
112
112
  */
113
113
  const customizedMiniAppSchema = z.object({
114
114
  type: z.literal("customized-mini-app"),
115
- __platform: z.literal("iMessage"),
115
+ __platform: z.literal("imessage"),
116
116
  appName: z.string().nonempty(),
117
117
  appStoreId: z.number().int().positive().optional(),
118
118
  extensionBundleId: z.string().nonempty(),
@@ -124,7 +124,7 @@ const customizedMiniAppSchema = z.object({
124
124
  const isCustomizedMiniApp = (v) => customizedMiniAppSchema.safeParse(v).success;
125
125
  const asCustomizedMiniApp = (input) => customizedMiniAppSchema.parse({
126
126
  type: "customized-mini-app",
127
- __platform: "iMessage",
127
+ __platform: "imessage",
128
128
  ...input
129
129
  });
130
130
  /**
@@ -231,10 +231,6 @@ const messageSchema = z.object({
231
231
  });
232
232
  //#endregion
233
233
  //#region src/auth.ts
234
- const log$4 = createLogger("spectrum.imessage.auth");
235
- const RENEWAL_RATIO = .8;
236
- const EXPIRY_BUFFER_MS = 3e4;
237
- const RETRY_DELAY_MS = 3e4;
238
234
  const FORCE_REFRESH_MIN_INTERVAL_MS = 5e3;
239
235
  const cloudAuthState = /* @__PURE__ */ new WeakMap();
240
236
  const requirePhone = (data, instanceId) => {
@@ -244,81 +240,26 @@ const requirePhone = (data, instanceId) => {
244
240
  };
245
241
  async function createCloudClients(projectId, projectSecret) {
246
242
  let tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
247
- let tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
248
- let disposed = false;
249
- let renewalTimer;
250
- let refreshFailures = 0;
251
- let refreshInFlight;
252
243
  let lastRefreshAt = Date.now();
253
244
  const records = [];
254
245
  const syncPhones = (data) => {
255
246
  for (const { entry, instanceId } of records) entry.phone = requirePhone(data, instanceId);
256
247
  };
257
- const onRefreshSuccess = () => {
258
- if (refreshFailures > 0) {
259
- log$4.info("imessage token refresh recovered", { "spectrum.imessage.auth.attempt": refreshFailures });
260
- refreshFailures = 0;
248
+ const renewal = createTokenRenewal({
249
+ expiresInSeconds: () => tokenData.expiresIn,
250
+ name: "imessage",
251
+ refresh: async () => {
252
+ tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
253
+ lastRefreshAt = Date.now();
254
+ if (tokenData.type === "dedicated") syncPhones(tokenData);
261
255
  }
262
- };
263
- const onRefreshFailure = (error) => {
264
- refreshFailures += 1;
265
- log$4.warn("imessage token refresh failed; retrying", {
266
- "spectrum.imessage.auth.attempt": refreshFailures,
267
- "spectrum.imessage.auth.retry_in_ms": RETRY_DELAY_MS,
268
- ...errorAttrs(error)
269
- }, error);
270
- };
271
- const refreshNow = async () => {
272
- tokenData = await cloud.issueImessageTokens(projectId, projectSecret);
273
- tokenExpiresAt = Date.now() + tokenData.expiresIn * 1e3;
274
- lastRefreshAt = Date.now();
275
- if (tokenData.type === "dedicated") syncPhones(tokenData);
276
- onRefreshSuccess();
277
- scheduleRenewal();
278
- };
279
- const coalescedRefresh = () => {
280
- if (!refreshInFlight) refreshInFlight = refreshNow().finally(() => {
281
- refreshInFlight = void 0;
282
- });
283
- return refreshInFlight;
284
- };
285
- const scheduleRenewal = () => {
286
- if (disposed) return;
287
- if (renewalTimer !== void 0) {
288
- clearTimeout(renewalTimer);
289
- renewalTimer = void 0;
290
- }
291
- const ttlMs = tokenData.expiresIn * 1e3;
292
- const renewInMs = Math.max(ttlMs * RENEWAL_RATIO, 5e3);
293
- const runScheduledRefresh = () => {
294
- coalescedRefresh().catch((error) => {
295
- onRefreshFailure(error);
296
- if (disposed) return;
297
- renewalTimer = setTimeout(runScheduledRefresh, RETRY_DELAY_MS);
298
- renewalTimer?.unref?.();
299
- });
300
- };
301
- renewalTimer = setTimeout(runScheduledRefresh, renewInMs);
302
- renewalTimer?.unref?.();
303
- };
304
- scheduleRenewal();
305
- const refreshIfNeeded = async () => {
306
- if (Date.now() < tokenExpiresAt - EXPIRY_BUFFER_MS) return;
307
- await coalescedRefresh();
308
- };
256
+ });
309
257
  const forceRefresh = async () => {
310
258
  if (Date.now() - lastRefreshAt < FORCE_REFRESH_MIN_INTERVAL_MS) return;
311
- await coalescedRefresh();
312
- };
313
- const dispose = () => {
314
- disposed = true;
315
- if (renewalTimer !== void 0) {
316
- clearTimeout(renewalTimer);
317
- renewalTimer = void 0;
318
- }
259
+ await renewal.forceRefresh();
319
260
  };
320
261
  const cloudAuth = {
321
- dispose,
262
+ dispose: renewal.dispose,
322
263
  forceRefresh
323
264
  };
324
265
  if (tokenData.type === "shared") {
@@ -330,7 +271,7 @@ async function createCloudClients(projectId, projectSecret) {
330
271
  retry: true,
331
272
  tls: true,
332
273
  token: async () => {
333
- await refreshIfNeeded();
274
+ await renewal.refreshIfNeeded();
334
275
  return tokenData.token;
335
276
  }
336
277
  })
@@ -348,7 +289,7 @@ async function createCloudClients(projectId, projectSecret) {
348
289
  retry: true,
349
290
  tls: true,
350
291
  token: async () => {
351
- await refreshIfNeeded();
292
+ await renewal.refreshIfNeeded();
352
293
  return tokenData.auth[instanceId] ?? token;
353
294
  }
354
295
  })
@@ -461,6 +402,9 @@ const getPollCache = (owner) => {
461
402
  return cache;
462
403
  };
463
404
  //#endregion
405
+ //#region src/platform.ts
406
+ const IMESSAGE_PLATFORM = "imessage";
407
+ //#endregion
464
408
  //#region src/remote/ids.ts
465
409
  const PART_PREFIX = /^p:(\d+)\//;
466
410
  const dmChatGuid = (address) => `any;-;${address}`;
@@ -1104,7 +1048,6 @@ const getDisplayName$1 = async (remote, spaceId) => {
1104
1048
  };
1105
1049
  //#endregion
1106
1050
  //#region src/shared/errors.ts
1107
- const IMESSAGE_PLATFORM = "iMessage";
1108
1051
  const unsupportedRemoteContent = (type, detail) => UnsupportedError.content(type, IMESSAGE_PLATFORM, detail);
1109
1052
  //#endregion
1110
1053
  //#region src/remote/markdown.ts
@@ -2135,7 +2078,7 @@ const handleEdit = async (client, space, content) => {
2135
2078
  if (nextSession) content.target.miniAppCardSession = nextSession;
2136
2079
  };
2137
2080
  if (content.content.type === "app") {
2138
- if (!miniAppCardSession) throw UnsupportedError.content("edit", "iMessage", "mini app card edits require a miniAppCardSession from the original send");
2081
+ if (!miniAppCardSession) throw UnsupportedError.content("edit", IMESSAGE_PLATFORM, "mini app card edits require a miniAppCardSession from the original send");
2139
2082
  const url = await content.content.url();
2140
2083
  const layout = await content.content.layout();
2141
2084
  const remote = clientForPhone(client, space.phone);
@@ -2143,16 +2086,16 @@ const handleEdit = async (client, space, content) => {
2143
2086
  return;
2144
2087
  }
2145
2088
  if (isCustomizedMiniApp(content.content)) {
2146
- if (!miniAppCardSession) throw UnsupportedError.content("edit", "iMessage", "customized mini app card edits require a miniAppCardSession from the original send");
2089
+ if (!miniAppCardSession) throw UnsupportedError.content("edit", IMESSAGE_PLATFORM, "customized mini app card edits require a miniAppCardSession from the original send");
2147
2090
  const remote = clientForPhone(client, space.phone);
2148
2091
  updateMiniAppCardSession(cacheRemoteOutbound(remote, space, await updateCustomizedMiniApp(remote, space.id, miniAppCardSession, content.content)));
2149
2092
  return;
2150
2093
  }
2151
- if (content.content.type !== "text") throw UnsupportedError.content("edit", "iMessage", `only text content can be edited (got "${content.content.type}")`);
2094
+ if (content.content.type !== "text") throw UnsupportedError.content("edit", IMESSAGE_PLATFORM, `only text content can be edited (got "${content.content.type}")`);
2152
2095
  await editMessage(clientForPhone(client, space.phone), space.id, content.target.id, content.content);
2153
2096
  };
2154
2097
  const handleUnsend = async (client, space, content) => {
2155
- if (isPollContent(content.target.content)) throw UnsupportedError.action("unsend", "iMessage", "iMessage polls cannot be unsent");
2098
+ if (isPollContent(content.target.content)) throw UnsupportedError.action("unsend", IMESSAGE_PLATFORM, "iMessage polls cannot be unsent");
2156
2099
  const remote = clientForPhone(client, space.phone);
2157
2100
  const targetContent = content.target.content;
2158
2101
  if (targetContent.type === "reaction") {
@@ -2195,11 +2138,11 @@ const handleTyping = async (client, space, state) => {
2195
2138
  else await stopTyping(remote, space.id);
2196
2139
  };
2197
2140
  const handleRename = async (client, space, content) => {
2198
- if (space.type !== "group") throw UnsupportedError.action("rename", "iMessage", "only group chats can be renamed (this space is a DM)");
2141
+ if (space.type !== "group") throw UnsupportedError.action("rename", IMESSAGE_PLATFORM, "only group chats can be renamed (this space is a DM)");
2199
2142
  await setDisplayName(clientForPhone(client, space.phone), space.id, content);
2200
2143
  };
2201
2144
  const handleAvatar = async (client, space, content) => {
2202
- if (space.type !== "group") throw UnsupportedError.action("avatar", "iMessage", "only group chats have avatars (this space is a DM)");
2145
+ if (space.type !== "group") throw UnsupportedError.action("avatar", IMESSAGE_PLATFORM, "only group chats have avatars (this space is a DM)");
2203
2146
  await setIcon(clientForPhone(client, space.phone), space.id, content);
2204
2147
  };
2205
2148
  /**
@@ -2208,7 +2151,7 @@ const handleAvatar = async (client, space, content) => {
2208
2151
  * guard sequence.
2209
2152
  */
2210
2153
  const remoteGroupClient = (client, space, action, detail) => {
2211
- if (space.type !== "group") throw UnsupportedError.action(action, "iMessage", detail);
2154
+ if (space.type !== "group") throw UnsupportedError.action(action, IMESSAGE_PLATFORM, detail);
2212
2155
  return clientForPhone(client, space.phone);
2213
2156
  };
2214
2157
  const handleAddMember = async (client, space, content) => {
@@ -2244,10 +2187,10 @@ const handleProviderControlSignal = async (client, space, content) => {
2244
2187
  * poll-unsupported message.
2245
2188
  */
2246
2189
  const remoteForMessageTarget = (client, space, target, action, pollNoun) => {
2247
- if (isPollContent(target.content)) throw UnsupportedError.action(action, "iMessage", `iMessage polls do not support ${pollNoun}`);
2190
+ if (isPollContent(target.content)) throw UnsupportedError.action(action, IMESSAGE_PLATFORM, `iMessage polls do not support ${pollNoun}`);
2248
2191
  return clientForPhone(client, space.phone);
2249
2192
  };
2250
- const imessage = definePlatform("iMessage", {
2193
+ const imessage = definePlatform(IMESSAGE_PLATFORM, {
2251
2194
  config: configSchema,
2252
2195
  static: { effect: { message: messageEffects } },
2253
2196
  lifecycle: {
@@ -2262,7 +2205,7 @@ const imessage = definePlatform("iMessage", {
2262
2205
  token: e.token
2263
2206
  })
2264
2207
  }));
2265
- if (!(projectId && projectSecret)) throw new Error("Cloud iMessage requires projectId and projectSecret. Pass credentials to Spectrum() or provide explicit clients with imessage.config({ clients: [...] }). For local Messages access, install @spectrum-ts/imessage-local and use its imessage.config().");
2208
+ if (!(projectId && projectSecret)) throw new Error("Cloud iMessage requires projectId and projectSecret. Pass credentials to Spectrum() or provide explicit clients with imessage.config({ clients: [...] }). For local Messages access, install @spectrum-ts/imessage-local and use localIMessage.config().");
2266
2209
  return await createCloudClients(projectId, projectSecret);
2267
2210
  },
2268
2211
  destroyClient: async ({ client }) => {
@@ -2393,7 +2336,7 @@ const imessage = definePlatform("iMessage", {
2393
2336
  })();
2394
2337
  const remote = clientForPhone(client, routedPhone);
2395
2338
  return withSpan("spectrum.imessage.getAttachment", {
2396
- "spectrum.provider": "iMessage",
2339
+ "spectrum.provider": IMESSAGE_PLATFORM,
2397
2340
  "spectrum.imessage.attachment.guid": guid,
2398
2341
  "spectrum.imessage.phone": routedPhone
2399
2342
  }, () => getRemoteAttachment(remote, guid));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spectrum-ts/imessage",
3
- "version": "11.2.0",
3
+ "version": "12.1.0",
4
4
  "description": "iMessage provider for spectrum-ts.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,7 @@
28
28
  "spectrum": {
29
29
  "key": "imessage",
30
30
  "import": "imessage",
31
- "label": "iMessage"
31
+ "label": "imessage"
32
32
  },
33
33
  "dependencies": {
34
34
  "@photon-ai/advanced-imessage": "^1.0.0",
@@ -38,7 +38,7 @@
38
38
  "zod": "^4.2.1"
39
39
  },
40
40
  "peerDependencies": {
41
- "@spectrum-ts/core": "^11.0.0",
41
+ "@spectrum-ts/core": "^12.0.0",
42
42
  "typescript": "^5 || ^6.0.0"
43
43
  },
44
44
  "license": "MIT"