easy-ping 0.7.0 → 0.8.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 +4 -0
- package/dist/{adapter-BJ8Iejvs.d.cts → adapter-CV5Yt2c6.d.cts} +1 -1
- package/dist/{adapter-BJ8Iejvs.d.ts → adapter-CV5Yt2c6.d.ts} +1 -1
- package/dist/adapters/drizzle.d.cts +1 -1
- package/dist/adapters/drizzle.d.ts +1 -1
- package/dist/adapters/mongodb.d.cts +2 -2
- package/dist/adapters/mongodb.d.ts +2 -2
- package/dist/adapters/mysql.d.cts +2 -2
- package/dist/adapters/mysql.d.ts +2 -2
- package/dist/adapters/postgres.d.cts +2 -2
- package/dist/adapters/postgres.d.ts +2 -2
- package/dist/adapters/sqlite.d.cts +2 -2
- package/dist/adapters/sqlite.d.ts +2 -2
- package/dist/client.cjs +7 -2
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +7 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/{plugin-Wwp1nvo0.d.cts → plugin-6S8tpH0n.d.cts} +1 -1
- package/dist/{plugin-CSLKKgXT.d.ts → plugin-C4fikLl7.d.ts} +1 -1
- package/dist/plugins/digests.d.cts +2 -2
- package/dist/plugins/digests.d.ts +2 -2
- package/dist/plugins/mobile-push.cjs +307 -0
- package/dist/plugins/mobile-push.cjs.map +1 -0
- package/dist/plugins/mobile-push.d.cts +117 -0
- package/dist/plugins/mobile-push.d.ts +117 -0
- package/dist/plugins/mobile-push.js +261 -0
- package/dist/plugins/mobile-push.js.map +1 -0
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- package/dist/plugins/preferences-react.d.cts +1 -1
- package/dist/plugins/preferences-react.d.ts +1 -1
- package/dist/plugins/preferences.cjs +9 -1
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +2 -2
- package/dist/plugins/preferences.d.ts +2 -2
- package/dist/plugins/preferences.js +9 -1
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.d.cts +2 -2
- package/dist/plugins/push.d.ts +2 -2
- package/dist/plugins/telegram.d.cts +2 -2
- package/dist/plugins/telegram.d.ts +2 -2
- package/dist/providers/expo-push.cjs +114 -0
- package/dist/providers/expo-push.cjs.map +1 -0
- package/dist/providers/expo-push.d.cts +61 -0
- package/dist/providers/expo-push.d.ts +61 -0
- package/dist/providers/expo-push.js +109 -0
- package/dist/providers/expo-push.js.map +1 -0
- package/dist/providers/web-push.d.cts +2 -2
- package/dist/providers/web-push.d.ts +2 -2
- package/dist/react-native.cjs +637 -0
- package/dist/react-native.cjs.map +1 -0
- package/dist/react-native.d.cts +145 -0
- package/dist/react-native.d.ts +145 -0
- package/dist/react-native.js +609 -0
- package/dist/react-native.js.map +1 -0
- package/dist/react.cjs +7 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +7 -2
- package/dist/react.js.map +1 -1
- package/dist/schema.d.cts +2 -2
- package/dist/schema.d.ts +2 -2
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/package.json +26 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/preferences/index.ts"],"names":[],"mappings":"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","file":"preferences.js","sourcesContent":["import { readJsonBody } from \"../../core/handler\";\nimport type {\n ChannelDecision,\n EasyPingPlugin,\n PluginInitContext,\n PrepareContext,\n ResolveChannelsContext,\n} from \"../../core/plugin\";\nimport { DEFAULT_TOKEN_TTL_SECONDS, expiresIn, signToken } from \"../../core/tokens\";\nimport type { Channel, Frequency } from \"../../core/types\";\n\nexport const UNSUBSCRIBE_PURPOSE = \"unsubscribe\";\n\nconst TABLE = \"notification_preference\";\n\nconst CHANNELS: readonly Channel[] = [\"inApp\", \"email\", \"push\", \"telegram\", \"sms\", \"slack\"];\nconst FREQUENCIES: readonly Frequency[] = [\"instant\", \"daily\", \"weekly\", \"off\"];\n\nconst isChannel = (value: unknown): value is Channel => CHANNELS.includes(value as Channel);\nconst isFrequency = (value: unknown): value is Frequency =>\n FREQUENCIES.includes(value as Frequency);\n\nexport type PreferenceRow = {\n userId: string;\n type: string;\n channel: Channel;\n enabled: boolean;\n frequency: Frequency;\n /** Last explicit change. An unsubscribe token issued before it is refused. */\n updatedAt?: Date;\n};\n\nexport type PreferencesOptions = {\n /** What an absent row means. Flip to false for opt-in-only notifications. */\n defaultEnabled?: boolean;\n /** Types that ignore preferences entirely: receipts, security alerts. */\n alwaysSend?: readonly string[];\n unsubscribeTtlSeconds?: number;\n};\n\nconst key = (userId: string, type: string, channel: string) => `${userId} ${type} ${channel}`;\n\ntype PreparedPreferences = Map<string, PreferenceRow>;\n\nexport function buildUnsubscribeToken(\n secret: string,\n userId: string,\n type: string,\n channel: Channel,\n ttlSeconds = DEFAULT_TOKEN_TTL_SECONDS,\n): Promise<string> {\n return signToken(secret, {\n uid: userId,\n purpose: UNSUBSCRIBE_PURPOSE,\n exp: expiresIn(ttlSeconds),\n data: { type, channel },\n });\n}\n\nexport function preferences(options: PreferencesOptions = {}): EasyPingPlugin<\"preferences\"> {\n const defaultEnabled = options.defaultEnabled ?? true;\n const alwaysSend = new Set(options.alwaysSend ?? []);\n\n // Assigned by init(), which easyPing() runs before any hook or route.\n let ctx: PluginInitContext;\n\n const isAllowed = (userId: string, type: string, channel: Channel, rows: PreparedPreferences) => {\n const row = rows.get(key(userId, type, channel));\n return row ? row.enabled && row.frequency !== \"off\" : defaultEnabled;\n };\n\n const load = (userIds: readonly string[]) =>\n ctx.store.find<PreferenceRow>(TABLE, { userId: { in: [...userIds] } });\n\n const save = (row: PreferenceRow) =>\n ctx.store.upsert(TABLE, [{ ...row, updatedAt: new Date() }], {\n onConflict: [\"userId\", \"type\", \"channel\"],\n });\n\n return {\n id: \"preferences\",\n\n init: (context) => {\n ctx = context;\n },\n\n schema: {\n notificationPreference: {\n tableName: TABLE,\n fields: {\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n enabled: { type: \"boolean\", required: true, default: true },\n frequency: { type: \"string\", required: true, default: \"instant\" },\n updatedAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"userId\", \"type\", \"channel\"],\n },\n },\n\n hooks: {\n /** One query per send; doing this per recipient would be an N+1. */\n prepare: async (context: PrepareContext): Promise<PreparedPreferences> => {\n if (alwaysSend.has(context.type)) return new Map();\n\n const rows = await load(context.recipients.map((recipient) => recipient.userId));\n return new Map(rows.map((row) => [key(row.userId, row.type, row.channel), row]));\n },\n\n resolveChannels: (context: ResolveChannelsContext) => {\n if (alwaysSend.has(context.type)) return context.decisions;\n\n // prepare() failed: fail closed rather than assume everyone opted in.\n if (!(context.prepared instanceof Map)) {\n return context.decisions.map((decision) => ({ ...decision, action: \"skip\" as const }));\n }\n\n const rows = context.prepared as PreparedPreferences;\n\n return context.decisions.map<ChannelDecision>((decision) =>\n isAllowed(context.recipient.userId, context.type, decision.channel, rows)\n ? decision\n : { ...decision, action: \"skip\" },\n );\n },\n },\n\n routes: [\n {\n path: \"/preferences\",\n method: \"GET\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const rows = await load([userId ?? \"\"]);\n return Response.json({ preferences: rows, defaultEnabled });\n },\n },\n\n {\n path: \"/preferences\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n const body = parsed.body;\n\n const bad = (error: string) => Response.json({ error }, { status: 400 });\n\n // Only configured types, or every invented string becomes a row.\n if (typeof body.type !== \"string\" || !ctx.notificationTypes.includes(body.type)) {\n return bad(\"type must be a configured notification type\");\n }\n if (!isChannel(body.channel)) return bad(`channel must be one of ${CHANNELS.join(\", \")}`);\n if (body.enabled !== undefined && typeof body.enabled !== \"boolean\") {\n return bad(\"enabled must be a boolean\");\n }\n if (body.frequency !== undefined && !isFrequency(body.frequency)) {\n return bad(`frequency must be one of ${FREQUENCIES.join(\", \")}`);\n }\n\n // userId comes from the resolved session, never from the body.\n await save({\n userId: userId ?? \"\",\n type: body.type,\n channel: body.channel,\n enabled: body.enabled ?? true,\n frequency: body.frequency ?? \"instant\",\n });\n\n return Response.json({ ok: true });\n },\n },\n\n {\n // No session in a mail client; the link carries the proof.\n path: \"/unsubscribe\",\n method: \"POST\",\n scope: { type: \"signed\", purpose: UNSUBSCRIBE_PURPOSE },\n handler: async ({ claims }) => {\n const type = claims?.data?.type;\n const channel = claims?.data?.channel;\n if (!claims || !type || !isChannel(channel)) {\n return Response.json({ error: \"invalid token\" }, { status: 400 });\n }\n\n const [current] = await ctx.store.find<PreferenceRow>(\n TABLE,\n { userId: claims.uid, type, channel },\n { limit: 1 },\n );\n\n // Already off: a second click, or a mail client retrying, is fine.\n if (current && !current.enabled && current.frequency === \"off\") {\n return Response.json({ ok: true, unsubscribed: { type, channel } });\n }\n\n // Changed after the email went out: an old link must not undo a newer decision.\n if (\n current?.updatedAt &&\n claims.iat !== undefined &&\n current.updatedAt.getTime() > (claims.iat + 1) * 1000\n ) {\n return Response.json({ error: \"invalid token\" }, { status: 400 });\n }\n\n await save({\n userId: claims.uid,\n type,\n channel,\n enabled: false,\n frequency: \"off\",\n });\n\n // One-click (Gmail, Yahoo) requires the POST itself to unsubscribe.\n return Response.json({ ok: true, unsubscribed: { type, channel } });\n },\n },\n ],\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/preferences/index.ts"],"names":[],"mappings":"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","file":"preferences.js","sourcesContent":["import { readJsonBody } from \"../../core/handler\";\nimport type {\n ChannelDecision,\n EasyPingPlugin,\n PluginInitContext,\n PrepareContext,\n ResolveChannelsContext,\n} from \"../../core/plugin\";\nimport { DEFAULT_TOKEN_TTL_SECONDS, expiresIn, signToken } from \"../../core/tokens\";\nimport type { Channel, Frequency } from \"../../core/types\";\n\nexport const UNSUBSCRIBE_PURPOSE = \"unsubscribe\";\n\nconst TABLE = \"notification_preference\";\n\nconst CHANNELS: readonly Channel[] = [\n \"inApp\",\n \"email\",\n \"push\",\n \"mobilePush\",\n \"telegram\",\n \"sms\",\n \"slack\",\n];\nconst FREQUENCIES: readonly Frequency[] = [\"instant\", \"daily\", \"weekly\", \"off\"];\n\nconst isChannel = (value: unknown): value is Channel => CHANNELS.includes(value as Channel);\nconst isFrequency = (value: unknown): value is Frequency =>\n FREQUENCIES.includes(value as Frequency);\n\nexport type PreferenceRow = {\n userId: string;\n type: string;\n channel: Channel;\n enabled: boolean;\n frequency: Frequency;\n /** Last explicit change. An unsubscribe token issued before it is refused. */\n updatedAt?: Date;\n};\n\nexport type PreferencesOptions = {\n /** What an absent row means. Flip to false for opt-in-only notifications. */\n defaultEnabled?: boolean;\n /** Types that ignore preferences entirely: receipts, security alerts. */\n alwaysSend?: readonly string[];\n unsubscribeTtlSeconds?: number;\n};\n\nconst key = (userId: string, type: string, channel: string) => `${userId} ${type} ${channel}`;\n\ntype PreparedPreferences = Map<string, PreferenceRow>;\n\nexport function buildUnsubscribeToken(\n secret: string,\n userId: string,\n type: string,\n channel: Channel,\n ttlSeconds = DEFAULT_TOKEN_TTL_SECONDS,\n): Promise<string> {\n return signToken(secret, {\n uid: userId,\n purpose: UNSUBSCRIBE_PURPOSE,\n exp: expiresIn(ttlSeconds),\n data: { type, channel },\n });\n}\n\nexport function preferences(options: PreferencesOptions = {}): EasyPingPlugin<\"preferences\"> {\n const defaultEnabled = options.defaultEnabled ?? true;\n const alwaysSend = new Set(options.alwaysSend ?? []);\n\n // Assigned by init(), which easyPing() runs before any hook or route.\n let ctx: PluginInitContext;\n\n const isAllowed = (userId: string, type: string, channel: Channel, rows: PreparedPreferences) => {\n const row = rows.get(key(userId, type, channel));\n return row ? row.enabled && row.frequency !== \"off\" : defaultEnabled;\n };\n\n const load = (userIds: readonly string[]) =>\n ctx.store.find<PreferenceRow>(TABLE, { userId: { in: [...userIds] } });\n\n const save = (row: PreferenceRow) =>\n ctx.store.upsert(TABLE, [{ ...row, updatedAt: new Date() }], {\n onConflict: [\"userId\", \"type\", \"channel\"],\n });\n\n return {\n id: \"preferences\",\n\n init: (context) => {\n ctx = context;\n },\n\n schema: {\n notificationPreference: {\n tableName: TABLE,\n fields: {\n userId: { type: \"string\", required: true },\n type: { type: \"string\", required: true },\n channel: { type: \"string\", required: true },\n enabled: { type: \"boolean\", required: true, default: true },\n frequency: { type: \"string\", required: true, default: \"instant\" },\n updatedAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"userId\", \"type\", \"channel\"],\n },\n },\n\n hooks: {\n /** One query per send; doing this per recipient would be an N+1. */\n prepare: async (context: PrepareContext): Promise<PreparedPreferences> => {\n if (alwaysSend.has(context.type)) return new Map();\n\n const rows = await load(context.recipients.map((recipient) => recipient.userId));\n return new Map(rows.map((row) => [key(row.userId, row.type, row.channel), row]));\n },\n\n resolveChannels: (context: ResolveChannelsContext) => {\n if (alwaysSend.has(context.type)) return context.decisions;\n\n // prepare() failed: fail closed rather than assume everyone opted in.\n if (!(context.prepared instanceof Map)) {\n return context.decisions.map((decision) => ({ ...decision, action: \"skip\" as const }));\n }\n\n const rows = context.prepared as PreparedPreferences;\n\n return context.decisions.map<ChannelDecision>((decision) =>\n isAllowed(context.recipient.userId, context.type, decision.channel, rows)\n ? decision\n : { ...decision, action: \"skip\" },\n );\n },\n },\n\n routes: [\n {\n path: \"/preferences\",\n method: \"GET\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const rows = await load([userId ?? \"\"]);\n return Response.json({ preferences: rows, defaultEnabled });\n },\n },\n\n {\n path: \"/preferences\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n const body = parsed.body;\n\n const bad = (error: string) => Response.json({ error }, { status: 400 });\n\n // Only configured types, or every invented string becomes a row.\n if (typeof body.type !== \"string\" || !ctx.notificationTypes.includes(body.type)) {\n return bad(\"type must be a configured notification type\");\n }\n if (!isChannel(body.channel)) return bad(`channel must be one of ${CHANNELS.join(\", \")}`);\n if (body.enabled !== undefined && typeof body.enabled !== \"boolean\") {\n return bad(\"enabled must be a boolean\");\n }\n if (body.frequency !== undefined && !isFrequency(body.frequency)) {\n return bad(`frequency must be one of ${FREQUENCIES.join(\", \")}`);\n }\n\n // userId comes from the resolved session, never from the body.\n await save({\n userId: userId ?? \"\",\n type: body.type,\n channel: body.channel,\n enabled: body.enabled ?? true,\n frequency: body.frequency ?? \"instant\",\n });\n\n return Response.json({ ok: true });\n },\n },\n\n {\n // No session in a mail client; the link carries the proof.\n path: \"/unsubscribe\",\n method: \"POST\",\n scope: { type: \"signed\", purpose: UNSUBSCRIBE_PURPOSE },\n handler: async ({ claims }) => {\n const type = claims?.data?.type;\n const channel = claims?.data?.channel;\n if (!claims || !type || !isChannel(channel)) {\n return Response.json({ error: \"invalid token\" }, { status: 400 });\n }\n\n const [current] = await ctx.store.find<PreferenceRow>(\n TABLE,\n { userId: claims.uid, type, channel },\n { limit: 1 },\n );\n\n // Already off: a second click, or a mail client retrying, is fine.\n if (current && !current.enabled && current.frequency === \"off\") {\n return Response.json({ ok: true, unsubscribed: { type, channel } });\n }\n\n // Changed after the email went out: an old link must not undo a newer decision.\n if (\n current?.updatedAt &&\n claims.iat !== undefined &&\n current.updatedAt.getTime() > (claims.iat + 1) * 1000\n ) {\n return Response.json({ error: \"invalid token\" }, { status: 400 });\n }\n\n await save({\n userId: claims.uid,\n type,\n channel,\n enabled: false,\n frequency: \"off\",\n });\n\n // One-click (Gmail, Yahoo) requires the POST itself to unsubscribe.\n return Response.json({ ok: true, unsubscribed: { type, channel } });\n },\n },\n ],\n };\n}\n"]}
|
package/dist/plugins/push.d.cts
CHANGED
package/dist/plugins/push.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { h as EasyPingPlugin, L as Logger } from '../plugin-
|
|
1
|
+
import { h as EasyPingPlugin, L as Logger } from '../plugin-6S8tpH0n.cjs';
|
|
2
2
|
import { TelegramProvider, TelegramUpdate } from '../providers/telegram.cjs';
|
|
3
|
-
import '../adapter-
|
|
3
|
+
import '../adapter-CV5Yt2c6.cjs';
|
|
4
4
|
|
|
5
5
|
type TelegramChat = {
|
|
6
6
|
id: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { h as EasyPingPlugin, L as Logger } from '../plugin-
|
|
1
|
+
import { h as EasyPingPlugin, L as Logger } from '../plugin-C4fikLl7.js';
|
|
2
2
|
import { TelegramProvider, TelegramUpdate } from '../providers/telegram.js';
|
|
3
|
-
import '../adapter-
|
|
3
|
+
import '../adapter-CV5Yt2c6.js';
|
|
4
4
|
|
|
5
5
|
type TelegramChat = {
|
|
6
6
|
id: string;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/providers/expo-push/index.ts
|
|
4
|
+
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,}\]$/.test(token);
|
|
5
|
+
var EXPO_SEND_BATCH = 100;
|
|
6
|
+
var EXPO_RECEIPT_BATCH = 1e3;
|
|
7
|
+
var GONE = /* @__PURE__ */ new Set(["DeviceNotRegistered"]);
|
|
8
|
+
var RETRYABLE = /* @__PURE__ */ new Set(["MessageRateExceeded", "ProviderError", "ExpoError"]);
|
|
9
|
+
var classify = (code, message) => ({
|
|
10
|
+
error: code ? `${code}: ${message ?? ""}`.trim() : message ?? "expo push failed",
|
|
11
|
+
gone: code !== void 0 && GONE.has(code),
|
|
12
|
+
retryable: code !== void 0 && RETRYABLE.has(code)
|
|
13
|
+
});
|
|
14
|
+
function expoPush(options = {}) {
|
|
15
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
16
|
+
const base = (options.apiBase ?? "https://exp.host").replace(/\/$/, "");
|
|
17
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
18
|
+
const redact = (text) => options.accessToken ? text.split(options.accessToken).join("<token>") : text;
|
|
19
|
+
async function post(path, body, signal) {
|
|
20
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
21
|
+
const headers = {
|
|
22
|
+
accept: "application/json",
|
|
23
|
+
"content-type": "application/json"
|
|
24
|
+
};
|
|
25
|
+
if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;
|
|
26
|
+
const response = await doFetch(`${base}${path}`, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers,
|
|
29
|
+
body: JSON.stringify(body),
|
|
30
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const text = redact((await response.text().catch(() => "")).slice(0, 200));
|
|
34
|
+
const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());
|
|
35
|
+
error.status = response.status;
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
return await response.json();
|
|
39
|
+
}
|
|
40
|
+
const transportFailure = (error) => {
|
|
41
|
+
const status = error?.status;
|
|
42
|
+
const message = redact(error instanceof Error ? error.message : String(error));
|
|
43
|
+
const retryable = status === void 0 || status === 429 || status >= 500;
|
|
44
|
+
return { ok: false, error: message, gone: false, retryable };
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
name: "expo-push",
|
|
48
|
+
isValidToken: isExpoPushToken,
|
|
49
|
+
async send(messages, signal) {
|
|
50
|
+
const results = [];
|
|
51
|
+
for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {
|
|
52
|
+
const batch = messages.slice(start, start + EXPO_SEND_BATCH);
|
|
53
|
+
let tickets;
|
|
54
|
+
try {
|
|
55
|
+
const response = await post(
|
|
56
|
+
"/--/api/v2/push/send",
|
|
57
|
+
batch.map((message) => ({
|
|
58
|
+
to: message.token,
|
|
59
|
+
title: message.title,
|
|
60
|
+
body: message.body,
|
|
61
|
+
...message.data ? { data: message.data } : {},
|
|
62
|
+
...message.badge === void 0 ? {} : { badge: message.badge },
|
|
63
|
+
...message.sound === void 0 ? { sound: "default" } : { sound: message.sound },
|
|
64
|
+
...message.channelId ? { channelId: message.channelId } : {}
|
|
65
|
+
})),
|
|
66
|
+
signal
|
|
67
|
+
);
|
|
68
|
+
tickets = response.data;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const failure = transportFailure(error);
|
|
71
|
+
for (const _ of batch) results.push(failure);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
batch.forEach((_, index) => {
|
|
75
|
+
const ticket = tickets[index];
|
|
76
|
+
if (!ticket) {
|
|
77
|
+
results.push({ ok: false, error: "no ticket returned", gone: false, retryable: true });
|
|
78
|
+
} else if (ticket.status === "ok") {
|
|
79
|
+
results.push({ ok: true, ticketId: ticket.id ?? null });
|
|
80
|
+
} else {
|
|
81
|
+
results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
},
|
|
87
|
+
async receipts(ticketIds, signal) {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {
|
|
90
|
+
const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);
|
|
91
|
+
const response = await post(
|
|
92
|
+
"/--/api/v2/push/getReceipts",
|
|
93
|
+
{ ids },
|
|
94
|
+
signal
|
|
95
|
+
);
|
|
96
|
+
for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {
|
|
97
|
+
if (receipt.status === "ok") out.push({ ticketId, ok: true });
|
|
98
|
+
else {
|
|
99
|
+
const { error, gone } = classify(receipt.details?.error, receipt.message);
|
|
100
|
+
out.push({ ticketId, ok: false, error, gone });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
exports.EXPO_RECEIPT_BATCH = EXPO_RECEIPT_BATCH;
|
|
110
|
+
exports.EXPO_SEND_BATCH = EXPO_SEND_BATCH;
|
|
111
|
+
exports.expoPush = expoPush;
|
|
112
|
+
exports.isExpoPushToken = isExpoPushToken;
|
|
113
|
+
//# sourceMappingURL=expo-push.cjs.map
|
|
114
|
+
//# sourceMappingURL=expo-push.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/expo-push/index.ts"],"names":[],"mappings":"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","file":"expo-push.cjs","sourcesContent":["export type MobilePushMessage = {\n /** The device's push token as the app registered it. */\n token: string;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n badge?: number | undefined;\n /** \"default\" plays the platform sound; null is silent. */\n sound?: \"default\" | null | undefined;\n /** Android notification channel id. */\n channelId?: string | undefined;\n};\n\nexport type MobilePushTicket =\n | { ok: true; ticketId: string | null }\n | {\n ok: false;\n error: string;\n /** The token will never work again (app uninstalled, token rotated). Prune it. */\n gone: boolean;\n retryable: boolean;\n };\n\nexport type MobilePushReceipt =\n | { ticketId: string; ok: true }\n | { ticketId: string; ok: false; error: string; gone: boolean };\n\nexport type MobilePushProvider = {\n name: string;\n /** True for a token this provider can deliver to. Rejected at registration otherwise. */\n isValidToken(token: string): boolean;\n /** One result per message, in order. Never throws for a per-token failure. */\n send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;\n /**\n * Some services (Expo) accept a message, then learn from APNs/FCM later\n * that the device is gone. Receipts carry that late verdict.\n */\n receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;\n};\n\nexport type ExpoPushOptions = {\n /** Expo \"enhanced security\" access token. Optional; when set, requests without it are refused by Expo. */\n accessToken?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://exp.host. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ExpoTicket =\n | { status: \"ok\"; id: string }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\ntype ExpoReceipt =\n | { status: \"ok\" }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\n/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */\nexport const isExpoPushToken = (token: string): boolean =>\n /^Expo(nent)?PushToken\\[[A-Za-z0-9_-]{8,}\\]$/.test(token);\n\n/** Expo's send endpoint takes at most this many messages per request. */\nexport const EXPO_SEND_BATCH = 100;\n/** And its receipts endpoint at most this many ids. */\nexport const EXPO_RECEIPT_BATCH = 1000;\n\n// Expo's error codes, per https://docs.expo.dev/push-notifications/sending-notifications/#individual-errors\nconst GONE = new Set([\"DeviceNotRegistered\"]);\nconst RETRYABLE = new Set([\"MessageRateExceeded\", \"ProviderError\", \"ExpoError\"]);\n\nconst classify = (\n code: string | undefined,\n message: string | undefined,\n): { error: string; gone: boolean; retryable: boolean } => ({\n error: code ? `${code}: ${message ?? \"\"}`.trim() : (message ?? \"expo push failed\"),\n gone: code !== undefined && GONE.has(code),\n retryable: code !== undefined && RETRYABLE.has(code),\n});\n\n/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */\nexport function expoPush(options: ExpoPushOptions = {}): MobilePushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = (options.apiBase ?? \"https://exp.host\").replace(/\\/$/, \"\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) =>\n options.accessToken ? text.split(options.accessToken).join(\"<token>\") : text;\n\n async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const timeout = AbortSignal.timeout(timeoutMs);\n const headers: Record<string, string> = {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n };\n if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;\n const response = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n });\n if (!response.ok) {\n const text = redact((await response.text().catch(() => \"\")).slice(0, 200));\n const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());\n (error as { status?: number }).status = response.status;\n throw error;\n }\n return (await response.json()) as T;\n }\n\n const transportFailure = (error: unknown): MobilePushTicket => {\n const status = (error as { status?: number })?.status;\n const message = redact(error instanceof Error ? error.message : String(error));\n // 4xx from the service itself (bad access token, malformed request) cannot be fixed by retrying.\n const retryable = status === undefined || status === 429 || status >= 500;\n return { ok: false, error: message, gone: false, retryable };\n };\n\n return {\n name: \"expo-push\",\n isValidToken: isExpoPushToken,\n\n async send(messages, signal) {\n const results: MobilePushTicket[] = [];\n for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {\n const batch = messages.slice(start, start + EXPO_SEND_BATCH);\n let tickets: ExpoTicket[];\n try {\n const response = await post<{ data: ExpoTicket[] }>(\n \"/--/api/v2/push/send\",\n batch.map((message) => ({\n to: message.token,\n title: message.title,\n body: message.body,\n ...(message.data ? { data: message.data } : {}),\n ...(message.badge === undefined ? {} : { badge: message.badge }),\n ...(message.sound === undefined ? { sound: \"default\" } : { sound: message.sound }),\n ...(message.channelId ? { channelId: message.channelId } : {}),\n })),\n signal,\n );\n tickets = response.data;\n } catch (error) {\n const failure = transportFailure(error);\n for (const _ of batch) results.push(failure);\n continue;\n }\n batch.forEach((_, index) => {\n const ticket = tickets[index];\n if (!ticket) {\n results.push({ ok: false, error: \"no ticket returned\", gone: false, retryable: true });\n } else if (ticket.status === \"ok\") {\n results.push({ ok: true, ticketId: ticket.id ?? null });\n } else {\n results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });\n }\n });\n }\n return results;\n },\n\n async receipts(ticketIds, signal) {\n const out: MobilePushReceipt[] = [];\n for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {\n const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);\n const response = await post<{ data: Record<string, ExpoReceipt> }>(\n \"/--/api/v2/push/getReceipts\",\n { ids },\n signal,\n );\n for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {\n if (receipt.status === \"ok\") out.push({ ticketId, ok: true });\n else {\n const { error, gone } = classify(receipt.details?.error, receipt.message);\n out.push({ ticketId, ok: false, error, gone });\n }\n }\n }\n return out;\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type MobilePushMessage = {
|
|
2
|
+
/** The device's push token as the app registered it. */
|
|
3
|
+
token: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
data?: Record<string, unknown> | undefined;
|
|
7
|
+
badge?: number | undefined;
|
|
8
|
+
/** "default" plays the platform sound; null is silent. */
|
|
9
|
+
sound?: "default" | null | undefined;
|
|
10
|
+
/** Android notification channel id. */
|
|
11
|
+
channelId?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
type MobilePushTicket = {
|
|
14
|
+
ok: true;
|
|
15
|
+
ticketId: string | null;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
/** The token will never work again (app uninstalled, token rotated). Prune it. */
|
|
20
|
+
gone: boolean;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
};
|
|
23
|
+
type MobilePushReceipt = {
|
|
24
|
+
ticketId: string;
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ticketId: string;
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
gone: boolean;
|
|
31
|
+
};
|
|
32
|
+
type MobilePushProvider = {
|
|
33
|
+
name: string;
|
|
34
|
+
/** True for a token this provider can deliver to. Rejected at registration otherwise. */
|
|
35
|
+
isValidToken(token: string): boolean;
|
|
36
|
+
/** One result per message, in order. Never throws for a per-token failure. */
|
|
37
|
+
send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Some services (Expo) accept a message, then learn from APNs/FCM later
|
|
40
|
+
* that the device is gone. Receipts carry that late verdict.
|
|
41
|
+
*/
|
|
42
|
+
receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;
|
|
43
|
+
};
|
|
44
|
+
type ExpoPushOptions = {
|
|
45
|
+
/** Expo "enhanced security" access token. Optional; when set, requests without it are refused by Expo. */
|
|
46
|
+
accessToken?: string | undefined;
|
|
47
|
+
fetch?: typeof globalThis.fetch | undefined;
|
|
48
|
+
/** Default https://exp.host. A local fake for tests. */
|
|
49
|
+
apiBase?: string | undefined;
|
|
50
|
+
timeoutMs?: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */
|
|
53
|
+
declare const isExpoPushToken: (token: string) => boolean;
|
|
54
|
+
/** Expo's send endpoint takes at most this many messages per request. */
|
|
55
|
+
declare const EXPO_SEND_BATCH = 100;
|
|
56
|
+
/** And its receipts endpoint at most this many ids. */
|
|
57
|
+
declare const EXPO_RECEIPT_BATCH = 1000;
|
|
58
|
+
/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */
|
|
59
|
+
declare function expoPush(options?: ExpoPushOptions): MobilePushProvider;
|
|
60
|
+
|
|
61
|
+
export { EXPO_RECEIPT_BATCH, EXPO_SEND_BATCH, type ExpoPushOptions, type MobilePushMessage, type MobilePushProvider, type MobilePushReceipt, type MobilePushTicket, expoPush, isExpoPushToken };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
type MobilePushMessage = {
|
|
2
|
+
/** The device's push token as the app registered it. */
|
|
3
|
+
token: string;
|
|
4
|
+
title: string;
|
|
5
|
+
body: string;
|
|
6
|
+
data?: Record<string, unknown> | undefined;
|
|
7
|
+
badge?: number | undefined;
|
|
8
|
+
/** "default" plays the platform sound; null is silent. */
|
|
9
|
+
sound?: "default" | null | undefined;
|
|
10
|
+
/** Android notification channel id. */
|
|
11
|
+
channelId?: string | undefined;
|
|
12
|
+
};
|
|
13
|
+
type MobilePushTicket = {
|
|
14
|
+
ok: true;
|
|
15
|
+
ticketId: string | null;
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: string;
|
|
19
|
+
/** The token will never work again (app uninstalled, token rotated). Prune it. */
|
|
20
|
+
gone: boolean;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
};
|
|
23
|
+
type MobilePushReceipt = {
|
|
24
|
+
ticketId: string;
|
|
25
|
+
ok: true;
|
|
26
|
+
} | {
|
|
27
|
+
ticketId: string;
|
|
28
|
+
ok: false;
|
|
29
|
+
error: string;
|
|
30
|
+
gone: boolean;
|
|
31
|
+
};
|
|
32
|
+
type MobilePushProvider = {
|
|
33
|
+
name: string;
|
|
34
|
+
/** True for a token this provider can deliver to. Rejected at registration otherwise. */
|
|
35
|
+
isValidToken(token: string): boolean;
|
|
36
|
+
/** One result per message, in order. Never throws for a per-token failure. */
|
|
37
|
+
send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Some services (Expo) accept a message, then learn from APNs/FCM later
|
|
40
|
+
* that the device is gone. Receipts carry that late verdict.
|
|
41
|
+
*/
|
|
42
|
+
receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;
|
|
43
|
+
};
|
|
44
|
+
type ExpoPushOptions = {
|
|
45
|
+
/** Expo "enhanced security" access token. Optional; when set, requests without it are refused by Expo. */
|
|
46
|
+
accessToken?: string | undefined;
|
|
47
|
+
fetch?: typeof globalThis.fetch | undefined;
|
|
48
|
+
/** Default https://exp.host. A local fake for tests. */
|
|
49
|
+
apiBase?: string | undefined;
|
|
50
|
+
timeoutMs?: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */
|
|
53
|
+
declare const isExpoPushToken: (token: string) => boolean;
|
|
54
|
+
/** Expo's send endpoint takes at most this many messages per request. */
|
|
55
|
+
declare const EXPO_SEND_BATCH = 100;
|
|
56
|
+
/** And its receipts endpoint at most this many ids. */
|
|
57
|
+
declare const EXPO_RECEIPT_BATCH = 1000;
|
|
58
|
+
/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */
|
|
59
|
+
declare function expoPush(options?: ExpoPushOptions): MobilePushProvider;
|
|
60
|
+
|
|
61
|
+
export { EXPO_RECEIPT_BATCH, EXPO_SEND_BATCH, type ExpoPushOptions, type MobilePushMessage, type MobilePushProvider, type MobilePushReceipt, type MobilePushTicket, expoPush, isExpoPushToken };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/providers/expo-push/index.ts
|
|
2
|
+
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,}\]$/.test(token);
|
|
3
|
+
var EXPO_SEND_BATCH = 100;
|
|
4
|
+
var EXPO_RECEIPT_BATCH = 1e3;
|
|
5
|
+
var GONE = /* @__PURE__ */ new Set(["DeviceNotRegistered"]);
|
|
6
|
+
var RETRYABLE = /* @__PURE__ */ new Set(["MessageRateExceeded", "ProviderError", "ExpoError"]);
|
|
7
|
+
var classify = (code, message) => ({
|
|
8
|
+
error: code ? `${code}: ${message ?? ""}`.trim() : message ?? "expo push failed",
|
|
9
|
+
gone: code !== void 0 && GONE.has(code),
|
|
10
|
+
retryable: code !== void 0 && RETRYABLE.has(code)
|
|
11
|
+
});
|
|
12
|
+
function expoPush(options = {}) {
|
|
13
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
14
|
+
const base = (options.apiBase ?? "https://exp.host").replace(/\/$/, "");
|
|
15
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
16
|
+
const redact = (text) => options.accessToken ? text.split(options.accessToken).join("<token>") : text;
|
|
17
|
+
async function post(path, body, signal) {
|
|
18
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
19
|
+
const headers = {
|
|
20
|
+
accept: "application/json",
|
|
21
|
+
"content-type": "application/json"
|
|
22
|
+
};
|
|
23
|
+
if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;
|
|
24
|
+
const response = await doFetch(`${base}${path}`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers,
|
|
27
|
+
body: JSON.stringify(body),
|
|
28
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const text = redact((await response.text().catch(() => "")).slice(0, 200));
|
|
32
|
+
const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());
|
|
33
|
+
error.status = response.status;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
return await response.json();
|
|
37
|
+
}
|
|
38
|
+
const transportFailure = (error) => {
|
|
39
|
+
const status = error?.status;
|
|
40
|
+
const message = redact(error instanceof Error ? error.message : String(error));
|
|
41
|
+
const retryable = status === void 0 || status === 429 || status >= 500;
|
|
42
|
+
return { ok: false, error: message, gone: false, retryable };
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
name: "expo-push",
|
|
46
|
+
isValidToken: isExpoPushToken,
|
|
47
|
+
async send(messages, signal) {
|
|
48
|
+
const results = [];
|
|
49
|
+
for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {
|
|
50
|
+
const batch = messages.slice(start, start + EXPO_SEND_BATCH);
|
|
51
|
+
let tickets;
|
|
52
|
+
try {
|
|
53
|
+
const response = await post(
|
|
54
|
+
"/--/api/v2/push/send",
|
|
55
|
+
batch.map((message) => ({
|
|
56
|
+
to: message.token,
|
|
57
|
+
title: message.title,
|
|
58
|
+
body: message.body,
|
|
59
|
+
...message.data ? { data: message.data } : {},
|
|
60
|
+
...message.badge === void 0 ? {} : { badge: message.badge },
|
|
61
|
+
...message.sound === void 0 ? { sound: "default" } : { sound: message.sound },
|
|
62
|
+
...message.channelId ? { channelId: message.channelId } : {}
|
|
63
|
+
})),
|
|
64
|
+
signal
|
|
65
|
+
);
|
|
66
|
+
tickets = response.data;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const failure = transportFailure(error);
|
|
69
|
+
for (const _ of batch) results.push(failure);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
batch.forEach((_, index) => {
|
|
73
|
+
const ticket = tickets[index];
|
|
74
|
+
if (!ticket) {
|
|
75
|
+
results.push({ ok: false, error: "no ticket returned", gone: false, retryable: true });
|
|
76
|
+
} else if (ticket.status === "ok") {
|
|
77
|
+
results.push({ ok: true, ticketId: ticket.id ?? null });
|
|
78
|
+
} else {
|
|
79
|
+
results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
},
|
|
85
|
+
async receipts(ticketIds, signal) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {
|
|
88
|
+
const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);
|
|
89
|
+
const response = await post(
|
|
90
|
+
"/--/api/v2/push/getReceipts",
|
|
91
|
+
{ ids },
|
|
92
|
+
signal
|
|
93
|
+
);
|
|
94
|
+
for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {
|
|
95
|
+
if (receipt.status === "ok") out.push({ ticketId, ok: true });
|
|
96
|
+
else {
|
|
97
|
+
const { error, gone } = classify(receipt.details?.error, receipt.message);
|
|
98
|
+
out.push({ ticketId, ok: false, error, gone });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export { EXPO_RECEIPT_BATCH, EXPO_SEND_BATCH, expoPush, isExpoPushToken };
|
|
108
|
+
//# sourceMappingURL=expo-push.js.map
|
|
109
|
+
//# sourceMappingURL=expo-push.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/providers/expo-push/index.ts"],"names":[],"mappings":"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","file":"expo-push.js","sourcesContent":["export type MobilePushMessage = {\n /** The device's push token as the app registered it. */\n token: string;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n badge?: number | undefined;\n /** \"default\" plays the platform sound; null is silent. */\n sound?: \"default\" | null | undefined;\n /** Android notification channel id. */\n channelId?: string | undefined;\n};\n\nexport type MobilePushTicket =\n | { ok: true; ticketId: string | null }\n | {\n ok: false;\n error: string;\n /** The token will never work again (app uninstalled, token rotated). Prune it. */\n gone: boolean;\n retryable: boolean;\n };\n\nexport type MobilePushReceipt =\n | { ticketId: string; ok: true }\n | { ticketId: string; ok: false; error: string; gone: boolean };\n\nexport type MobilePushProvider = {\n name: string;\n /** True for a token this provider can deliver to. Rejected at registration otherwise. */\n isValidToken(token: string): boolean;\n /** One result per message, in order. Never throws for a per-token failure. */\n send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;\n /**\n * Some services (Expo) accept a message, then learn from APNs/FCM later\n * that the device is gone. Receipts carry that late verdict.\n */\n receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;\n};\n\nexport type ExpoPushOptions = {\n /** Expo \"enhanced security\" access token. Optional; when set, requests without it are refused by Expo. */\n accessToken?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://exp.host. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ExpoTicket =\n | { status: \"ok\"; id: string }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\ntype ExpoReceipt =\n | { status: \"ok\" }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\n/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */\nexport const isExpoPushToken = (token: string): boolean =>\n /^Expo(nent)?PushToken\\[[A-Za-z0-9_-]{8,}\\]$/.test(token);\n\n/** Expo's send endpoint takes at most this many messages per request. */\nexport const EXPO_SEND_BATCH = 100;\n/** And its receipts endpoint at most this many ids. */\nexport const EXPO_RECEIPT_BATCH = 1000;\n\n// Expo's error codes, per https://docs.expo.dev/push-notifications/sending-notifications/#individual-errors\nconst GONE = new Set([\"DeviceNotRegistered\"]);\nconst RETRYABLE = new Set([\"MessageRateExceeded\", \"ProviderError\", \"ExpoError\"]);\n\nconst classify = (\n code: string | undefined,\n message: string | undefined,\n): { error: string; gone: boolean; retryable: boolean } => ({\n error: code ? `${code}: ${message ?? \"\"}`.trim() : (message ?? \"expo push failed\"),\n gone: code !== undefined && GONE.has(code),\n retryable: code !== undefined && RETRYABLE.has(code),\n});\n\n/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */\nexport function expoPush(options: ExpoPushOptions = {}): MobilePushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = (options.apiBase ?? \"https://exp.host\").replace(/\\/$/, \"\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) =>\n options.accessToken ? text.split(options.accessToken).join(\"<token>\") : text;\n\n async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const timeout = AbortSignal.timeout(timeoutMs);\n const headers: Record<string, string> = {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n };\n if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;\n const response = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n });\n if (!response.ok) {\n const text = redact((await response.text().catch(() => \"\")).slice(0, 200));\n const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());\n (error as { status?: number }).status = response.status;\n throw error;\n }\n return (await response.json()) as T;\n }\n\n const transportFailure = (error: unknown): MobilePushTicket => {\n const status = (error as { status?: number })?.status;\n const message = redact(error instanceof Error ? error.message : String(error));\n // 4xx from the service itself (bad access token, malformed request) cannot be fixed by retrying.\n const retryable = status === undefined || status === 429 || status >= 500;\n return { ok: false, error: message, gone: false, retryable };\n };\n\n return {\n name: \"expo-push\",\n isValidToken: isExpoPushToken,\n\n async send(messages, signal) {\n const results: MobilePushTicket[] = [];\n for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {\n const batch = messages.slice(start, start + EXPO_SEND_BATCH);\n let tickets: ExpoTicket[];\n try {\n const response = await post<{ data: ExpoTicket[] }>(\n \"/--/api/v2/push/send\",\n batch.map((message) => ({\n to: message.token,\n title: message.title,\n body: message.body,\n ...(message.data ? { data: message.data } : {}),\n ...(message.badge === undefined ? {} : { badge: message.badge }),\n ...(message.sound === undefined ? { sound: \"default\" } : { sound: message.sound }),\n ...(message.channelId ? { channelId: message.channelId } : {}),\n })),\n signal,\n );\n tickets = response.data;\n } catch (error) {\n const failure = transportFailure(error);\n for (const _ of batch) results.push(failure);\n continue;\n }\n batch.forEach((_, index) => {\n const ticket = tickets[index];\n if (!ticket) {\n results.push({ ok: false, error: \"no ticket returned\", gone: false, retryable: true });\n } else if (ticket.status === \"ok\") {\n results.push({ ok: true, ticketId: ticket.id ?? null });\n } else {\n results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });\n }\n });\n }\n return results;\n },\n\n async receipts(ticketIds, signal) {\n const out: MobilePushReceipt[] = [];\n for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {\n const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);\n const response = await post<{ data: Record<string, ExpoReceipt> }>(\n \"/--/api/v2/push/getReceipts\",\n { ids },\n signal,\n );\n for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {\n if (receipt.status === \"ok\") out.push({ ticketId, ok: true });\n else {\n const { error, gone } = classify(receipt.details?.error, receipt.message);\n out.push({ ticketId, ok: false, error, gone });\n }\n }\n }\n return out;\n },\n };\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PushProvider } from '../plugins/push.cjs';
|
|
2
|
-
import '../plugin-
|
|
3
|
-
import '../adapter-
|
|
2
|
+
import '../plugin-6S8tpH0n.cjs';
|
|
3
|
+
import '../adapter-CV5Yt2c6.cjs';
|
|
4
4
|
|
|
5
5
|
/** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */
|
|
6
6
|
declare class InvalidSubscriptionError extends Error {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PushProvider } from '../plugins/push.js';
|
|
2
|
-
import '../plugin-
|
|
3
|
-
import '../adapter-
|
|
2
|
+
import '../plugin-C4fikLl7.js';
|
|
3
|
+
import '../adapter-CV5Yt2c6.js';
|
|
4
4
|
|
|
5
5
|
/** The subscription itself is unusable. Distinct from a transport failure so the plugin prunes rather than retries. */
|
|
6
6
|
declare class InvalidSubscriptionError extends Error {
|