@lunora/notify 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +29 -0
- package/dist/index.d.mts +300 -16
- package/dist/index.d.ts +300 -16
- package/dist/index.mjs +1 -8
- package/dist/packem_shared/FCM_ENV_KEYS-BbhPScGH.mjs +1 -0
- package/dist/packem_shared/buildEngine-Cl_c6i9_.mjs +1 -0
- package/dist/packem_shared/createNotify-GgvLs28y.mjs +1 -0
- package/dist/packem_shared/d1SubscriptionStore-D5VVGBxm.mjs +1 -0
- package/dist/packem_shared/defineNotify-CUi2k7pP.mjs +1 -0
- package/dist/packem_shared/enqueuePushBroadcast-tf1LqgzA.mjs +1 -0
- package/dist/packem_shared/fcmId-ggkSOUHd.mjs +1 -0
- package/dist/packem_shared/memorySubscriptionStore-B60C_I7v.mjs +1 -0
- package/dist/packem_shared/ssrf-host-BCpHorGa.mjs +1 -0
- package/dist/web.mjs +1 -44
- package/package.json +3 -3
- package/dist/packem_shared/FCM_ENV_KEYS-DY4-A717.mjs +0 -32
- package/dist/packem_shared/buildEngine-DlmjvnNk.mjs +0 -65
- package/dist/packem_shared/createNotify-bDWs4qBm.mjs +0 -191
- package/dist/packem_shared/d1SubscriptionStore-Dv3VPMI_.mjs +0 -107
- package/dist/packem_shared/defineNotify-B6S_47C2.mjs +0 -18
- package/dist/packem_shared/enqueuePushBroadcast-DiK7Hyja.mjs +0 -4
- package/dist/packem_shared/fcmId-B-YPgHi7.mjs +0 -68
- package/dist/packem_shared/memorySubscriptionStore-DhS-YnLe.mjs +0 -46
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { createNotification } from '@visulima/notification';
|
|
2
|
-
import { retryMiddleware, circuitBreakerMiddleware } from '@visulima/notification/middleware';
|
|
3
|
-
import { fcmProvider } from '@visulima/notification/providers/fcm';
|
|
4
|
-
import { webPushProvider } from '@visulima/notification/providers/web-push';
|
|
5
|
-
|
|
6
|
-
const isWebPushTarget = (target) => {
|
|
7
|
-
if (typeof target !== "string") {
|
|
8
|
-
return typeof target === "object" && target !== null && "endpoint" in target;
|
|
9
|
-
}
|
|
10
|
-
if (!target.startsWith("{")) {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
try {
|
|
14
|
-
const parsed = JSON.parse(target);
|
|
15
|
-
return typeof parsed.endpoint === "string" && typeof parsed.keys === "object";
|
|
16
|
-
} catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
const routingPushProvider = (options) => {
|
|
21
|
-
const pick = (target) => {
|
|
22
|
-
const provider = isWebPushTarget(target) ? options.webPush : options.fcm;
|
|
23
|
-
if (provider === void 0) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
isWebPushTarget(target) ? "@lunora/notify: received a web-push target but no `webPush` channel is configured" : "@lunora/notify: received an FCM token target but no `fcm` channel is configured"
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
return provider;
|
|
29
|
-
};
|
|
30
|
-
return {
|
|
31
|
-
channel: "push",
|
|
32
|
-
id: "lunora-push-router",
|
|
33
|
-
initialize: async () => {
|
|
34
|
-
await options.webPush?.initialize();
|
|
35
|
-
await options.fcm?.initialize();
|
|
36
|
-
},
|
|
37
|
-
isAvailable: () => (options.webPush ?? options.fcm) !== void 0,
|
|
38
|
-
send: (payload) => {
|
|
39
|
-
const target = Array.isArray(payload.to) ? payload.to[0] : payload.to;
|
|
40
|
-
return pick(target).send(payload);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
};
|
|
44
|
-
const buildEngine = (resolved) => {
|
|
45
|
-
const webPush = resolved.webPush === void 0 ? void 0 : webPushProvider(resolved.webPush);
|
|
46
|
-
const fcm = resolved.fcm === void 0 ? void 0 : fcmProvider(resolved.fcm);
|
|
47
|
-
const providers = {};
|
|
48
|
-
if (webPush !== void 0 || fcm !== void 0) {
|
|
49
|
-
providers.push = routingPushProvider({ fcm, webPush });
|
|
50
|
-
}
|
|
51
|
-
if (resolved.chat !== void 0) {
|
|
52
|
-
providers.chat = resolved.chat;
|
|
53
|
-
}
|
|
54
|
-
if (resolved.inApp !== void 0) {
|
|
55
|
-
providers.inapp = resolved.inApp;
|
|
56
|
-
}
|
|
57
|
-
if (resolved.webhook !== void 0) {
|
|
58
|
-
providers.webhook = resolved.webhook;
|
|
59
|
-
}
|
|
60
|
-
const engine = createNotification(providers);
|
|
61
|
-
engine.use(retryMiddleware()).use(circuitBreakerMiddleware());
|
|
62
|
-
return engine;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export { buildEngine, routingPushProvider };
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
import { buildEngine } from './buildEngine-DlmjvnNk.mjs';
|
|
3
|
-
import { memorySubscriptionStore } from './memorySubscriptionStore-DhS-YnLe.mjs';
|
|
4
|
-
import { normalizeRegisterInput, targetOf, isGoneError } from './fcmId-B-YPgHi7.mjs';
|
|
5
|
-
|
|
6
|
-
const resolveMaybeFactory = (value, env) => typeof value === "function" ? value(env) : value;
|
|
7
|
-
const receiptError = (receipt) => receipt.successful ? void 0 : receipt.errorMessages.join("; ");
|
|
8
|
-
const pushDeliveryStatus = (receipt, error) => {
|
|
9
|
-
if (receipt.successful) {
|
|
10
|
-
return "accepted";
|
|
11
|
-
}
|
|
12
|
-
return isGoneError(error) ? "gone" : "failed";
|
|
13
|
-
};
|
|
14
|
-
const mapWithConcurrency = async (items, limit, task) => {
|
|
15
|
-
const results = Array.from({ length: items.length });
|
|
16
|
-
let cursor = 0;
|
|
17
|
-
const worker = async () => {
|
|
18
|
-
while (cursor < items.length) {
|
|
19
|
-
const index = cursor;
|
|
20
|
-
cursor += 1;
|
|
21
|
-
results[index] = await task(items[index]);
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
25
|
-
return results;
|
|
26
|
-
};
|
|
27
|
-
const resolveProviders = (definition, env) => {
|
|
28
|
-
return {
|
|
29
|
-
chat: resolveMaybeFactory(definition.chat, env),
|
|
30
|
-
fcm: resolveMaybeFactory(definition.fcm, env),
|
|
31
|
-
inApp: resolveMaybeFactory(definition.inApp, env),
|
|
32
|
-
webhook: resolveMaybeFactory(definition.webhook, env),
|
|
33
|
-
webPush: resolveMaybeFactory(definition.webPush, env)
|
|
34
|
-
};
|
|
35
|
-
};
|
|
36
|
-
const runtimeCache = /* @__PURE__ */ new WeakMap();
|
|
37
|
-
const runtimeFor = (definition, env) => {
|
|
38
|
-
let byEnv = runtimeCache.get(definition);
|
|
39
|
-
if (byEnv === void 0) {
|
|
40
|
-
byEnv = /* @__PURE__ */ new WeakMap();
|
|
41
|
-
runtimeCache.set(definition, byEnv);
|
|
42
|
-
}
|
|
43
|
-
let runtime = byEnv.get(env);
|
|
44
|
-
if (runtime === void 0) {
|
|
45
|
-
runtime = { warnedNoStore: false };
|
|
46
|
-
byEnv.set(env, runtime);
|
|
47
|
-
}
|
|
48
|
-
return runtime;
|
|
49
|
-
};
|
|
50
|
-
const createNotify = (definition, env, options = {}) => {
|
|
51
|
-
const runtime = runtimeFor(definition, env);
|
|
52
|
-
let engine;
|
|
53
|
-
if (options.engine === void 0) {
|
|
54
|
-
runtime.engine ??= buildEngine(resolveProviders(definition, env));
|
|
55
|
-
engine = runtime.engine;
|
|
56
|
-
} else {
|
|
57
|
-
engine = options.engine;
|
|
58
|
-
}
|
|
59
|
-
let store = definition.store?.(env);
|
|
60
|
-
if (store === void 0) {
|
|
61
|
-
runtime.fallbackStore ??= memorySubscriptionStore();
|
|
62
|
-
if (!options.silent && !runtime.warnedNoStore) {
|
|
63
|
-
runtime.warnedNoStore = true;
|
|
64
|
-
console.warn(
|
|
65
|
-
"@lunora/notify: no `store` configured — using a non-durable in-memory subscription store. Configure `store: (env) => d1SubscriptionStore(env.DB)` for production."
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
store = runtime.fallbackStore;
|
|
69
|
-
}
|
|
70
|
-
const subscriptionStore = store;
|
|
71
|
-
const concurrency = Math.max(1, options.concurrency ?? 10);
|
|
72
|
-
const { log, metrics } = options;
|
|
73
|
-
const countSend = (channel, provider, status, count = 1) => {
|
|
74
|
-
metrics?.count("notify.send", count, { channel, provider: provider ?? channel, status });
|
|
75
|
-
};
|
|
76
|
-
const warnFailedSend = (channel, provider, fields) => {
|
|
77
|
-
log?.warn(`notify ${channel} delivery failed`, { channel, provider: provider ?? channel, status: "failed", ...fields });
|
|
78
|
-
};
|
|
79
|
-
const observeSkip = (channel, reason) => {
|
|
80
|
-
metrics?.count("notify.skipped", 1, { channel, reason });
|
|
81
|
-
};
|
|
82
|
-
const resolveSubscription = async (target) => {
|
|
83
|
-
if (typeof target !== "string") {
|
|
84
|
-
return target;
|
|
85
|
-
}
|
|
86
|
-
const found = await subscriptionStore.get(target);
|
|
87
|
-
if (found === void 0) {
|
|
88
|
-
throw new LunoraError("BAD_REQUEST", `@lunora/notify: no registered subscription with id "${target}"`);
|
|
89
|
-
}
|
|
90
|
-
return found;
|
|
91
|
-
};
|
|
92
|
-
const deliver = async (subscription, payload, countInline) => {
|
|
93
|
-
const receipt = await engine.sendToChannel("push", { ...payload, to: targetOf(subscription) });
|
|
94
|
-
const error = receiptError(receipt);
|
|
95
|
-
const status = pushDeliveryStatus(receipt, error);
|
|
96
|
-
if (status === "accepted") {
|
|
97
|
-
await subscriptionStore.markStatus(subscription.id, "ok");
|
|
98
|
-
} else if (status === "gone") {
|
|
99
|
-
await subscriptionStore.delete(subscription.id);
|
|
100
|
-
} else {
|
|
101
|
-
await subscriptionStore.markStatus(subscription.id, "failed", error);
|
|
102
|
-
}
|
|
103
|
-
if (status === "failed") {
|
|
104
|
-
warnFailedSend("push", subscription.kind, { error, subscriptionId: subscription.id, userId: subscription.userId ?? null });
|
|
105
|
-
}
|
|
106
|
-
if (countInline) {
|
|
107
|
-
countSend("push", subscription.kind, status);
|
|
108
|
-
}
|
|
109
|
-
return { receipt, status };
|
|
110
|
-
};
|
|
111
|
-
const push = {
|
|
112
|
-
broadcast: async (payload, filter) => {
|
|
113
|
-
const subscriptions = await subscriptionStore.list(filter);
|
|
114
|
-
if (subscriptions.length === 0) {
|
|
115
|
-
observeSkip("push", "no-subscriptions-matched");
|
|
116
|
-
}
|
|
117
|
-
const rows = await mapWithConcurrency(subscriptions, concurrency, async (subscription) => {
|
|
118
|
-
const { receipt, status } = await deliver(subscription, payload, false);
|
|
119
|
-
return { kind: subscription.kind, receipt, status, subscription };
|
|
120
|
-
});
|
|
121
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
122
|
-
for (const { kind, status } of rows) {
|
|
123
|
-
const key = `${kind}\0${status}`;
|
|
124
|
-
const bucket = buckets.get(key);
|
|
125
|
-
if (bucket === void 0) {
|
|
126
|
-
buckets.set(key, { count: 1, kind, status });
|
|
127
|
-
} else {
|
|
128
|
-
bucket.count += 1;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
for (const { count, kind, status } of buckets.values()) {
|
|
132
|
-
countSend("push", kind, status, count);
|
|
133
|
-
}
|
|
134
|
-
const outcomes = rows.map(({ receipt, status, subscription }) => {
|
|
135
|
-
if (status === "accepted") {
|
|
136
|
-
return { id: subscription.id, status: "ok" };
|
|
137
|
-
}
|
|
138
|
-
const error = receiptError(receipt);
|
|
139
|
-
return status === "gone" ? { error, id: subscription.id, status: "expired" } : { error, id: subscription.id, status: "failed" };
|
|
140
|
-
});
|
|
141
|
-
return {
|
|
142
|
-
failed: outcomes.filter((outcome) => outcome.status === "failed").length,
|
|
143
|
-
outcomes,
|
|
144
|
-
pruned: outcomes.filter((outcome) => outcome.status === "expired").length,
|
|
145
|
-
sent: outcomes.filter((outcome) => outcome.status === "ok").length,
|
|
146
|
-
total: outcomes.length
|
|
147
|
-
};
|
|
148
|
-
},
|
|
149
|
-
list: (filter) => subscriptionStore.list(filter),
|
|
150
|
-
register: (input) => subscriptionStore.put(normalizeRegisterInput(input)),
|
|
151
|
-
send: async (target, payload) => {
|
|
152
|
-
const { receipt } = await deliver(await resolveSubscription(target), payload, true);
|
|
153
|
-
return receipt;
|
|
154
|
-
},
|
|
155
|
-
unregister: (id) => subscriptionStore.delete(id)
|
|
156
|
-
};
|
|
157
|
-
const sendToChannel = async (channel, payload) => {
|
|
158
|
-
if (engine.getProvider(channel) === void 0) {
|
|
159
|
-
observeSkip(channel, "channel-not-configured");
|
|
160
|
-
throw new LunoraError("BAD_REQUEST", `@lunora/notify: the "${channel}" channel is not configured in defineNotify(...)`);
|
|
161
|
-
}
|
|
162
|
-
const receipt = await engine.sendToChannel(channel, payload);
|
|
163
|
-
const status = receipt.successful ? "accepted" : "failed";
|
|
164
|
-
countSend(channel, receipt.provider, status);
|
|
165
|
-
if (status === "failed") {
|
|
166
|
-
warnFailedSend(channel, receipt.provider, { error: receiptError(receipt) });
|
|
167
|
-
}
|
|
168
|
-
return receipt;
|
|
169
|
-
};
|
|
170
|
-
const notify = {
|
|
171
|
-
chat: (payload) => sendToChannel("chat", payload),
|
|
172
|
-
inApp: (payload) => sendToChannel("inapp", payload),
|
|
173
|
-
push,
|
|
174
|
-
send: async (message) => {
|
|
175
|
-
const receipts = await engine.send(message);
|
|
176
|
-
for (const receipt of receipts) {
|
|
177
|
-
const channel = receipt.channel ?? "unknown";
|
|
178
|
-
const status = receipt.successful ? "accepted" : "failed";
|
|
179
|
-
countSend(channel, receipt.provider, status);
|
|
180
|
-
if (status === "failed") {
|
|
181
|
-
warnFailedSend(channel, receipt.provider, { error: receiptError(receipt) });
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
return receipts;
|
|
185
|
-
},
|
|
186
|
-
webhook: (payload) => sendToChannel("webhook", payload)
|
|
187
|
-
};
|
|
188
|
-
return { notify, push };
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
export { createNotify };
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const rowToSubscription = (row) => {
|
|
4
|
-
const subscription = {
|
|
5
|
-
createdAt: row.created_at,
|
|
6
|
-
id: row.id,
|
|
7
|
-
kind: row.kind,
|
|
8
|
-
lastSeenAt: row.last_seen_at,
|
|
9
|
-
userId: row.user_id
|
|
10
|
-
};
|
|
11
|
-
if (row.endpoint !== null) {
|
|
12
|
-
subscription.endpoint = row.endpoint;
|
|
13
|
-
}
|
|
14
|
-
if (row.p256dh !== null && row.auth !== null) {
|
|
15
|
-
subscription.keys = { auth: row.auth, p256dh: row.p256dh };
|
|
16
|
-
}
|
|
17
|
-
if (row.token !== null) {
|
|
18
|
-
subscription.token = row.token;
|
|
19
|
-
}
|
|
20
|
-
if (row.last_status !== null) {
|
|
21
|
-
subscription.lastStatus = row.last_status;
|
|
22
|
-
}
|
|
23
|
-
if (row.last_error !== null) {
|
|
24
|
-
subscription.lastError = row.last_error;
|
|
25
|
-
}
|
|
26
|
-
if (row.metadata !== null) {
|
|
27
|
-
try {
|
|
28
|
-
subscription.metadata = JSON.parse(row.metadata);
|
|
29
|
-
} catch {
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return subscription;
|
|
33
|
-
};
|
|
34
|
-
const IDENTIFIER_PATTERN = /^[A-Za-z_]\w*$/u;
|
|
35
|
-
const d1SubscriptionStore = (database, options = {}) => {
|
|
36
|
-
const table = options.tableName ?? "lunora_push_subscriptions";
|
|
37
|
-
if (!IDENTIFIER_PATTERN.test(table)) {
|
|
38
|
-
throw new LunoraError("BAD_REQUEST", `@lunora/notify: d1SubscriptionStore tableName "${table}" is not a bare SQL identifier`);
|
|
39
|
-
}
|
|
40
|
-
let schemaReady;
|
|
41
|
-
const ensureSchema = () => {
|
|
42
|
-
if (schemaReady === void 0) {
|
|
43
|
-
schemaReady = database.prepare(
|
|
44
|
-
`CREATE TABLE IF NOT EXISTS ${table} (id TEXT PRIMARY KEY, kind TEXT NOT NULL, endpoint TEXT, p256dh TEXT, auth TEXT, token TEXT, user_id TEXT, metadata TEXT, created_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL, last_status TEXT, last_error TEXT)`
|
|
45
|
-
).run().then(() => void 0);
|
|
46
|
-
schemaReady.catch(() => {
|
|
47
|
-
schemaReady = void 0;
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
return schemaReady;
|
|
51
|
-
};
|
|
52
|
-
const put = async (subscription) => {
|
|
53
|
-
await ensureSchema();
|
|
54
|
-
await database.prepare(
|
|
55
|
-
`INSERT INTO ${table} (id, kind, endpoint, p256dh, auth, token, user_id, metadata, created_at, last_seen_at, last_status, last_error) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12) ON CONFLICT(id) DO UPDATE SET kind = ?2, endpoint = ?3, p256dh = ?4, auth = ?5, token = ?6, user_id = ?7, metadata = ?8, last_seen_at = ?10, last_status = ?11, last_error = ?12`
|
|
56
|
-
).bind(
|
|
57
|
-
subscription.id,
|
|
58
|
-
subscription.kind,
|
|
59
|
-
subscription.endpoint ?? null,
|
|
60
|
-
subscription.keys?.p256dh ?? null,
|
|
61
|
-
subscription.keys?.auth ?? null,
|
|
62
|
-
subscription.token ?? null,
|
|
63
|
-
subscription.userId ?? null,
|
|
64
|
-
subscription.metadata === void 0 ? null : JSON.stringify(subscription.metadata),
|
|
65
|
-
subscription.createdAt,
|
|
66
|
-
subscription.lastSeenAt,
|
|
67
|
-
subscription.lastStatus ?? null,
|
|
68
|
-
subscription.lastError ?? null
|
|
69
|
-
).run();
|
|
70
|
-
return subscription;
|
|
71
|
-
};
|
|
72
|
-
const get = async (id) => {
|
|
73
|
-
await ensureSchema();
|
|
74
|
-
const row = await database.prepare(`SELECT * FROM ${table} WHERE id = ?1`).bind(id).first();
|
|
75
|
-
return row === null ? void 0 : rowToSubscription(row);
|
|
76
|
-
};
|
|
77
|
-
const remove = async (id) => {
|
|
78
|
-
await ensureSchema();
|
|
79
|
-
await database.prepare(`DELETE FROM ${table} WHERE id = ?1`).bind(id).run();
|
|
80
|
-
};
|
|
81
|
-
const list = async (filter) => {
|
|
82
|
-
await ensureSchema();
|
|
83
|
-
const clauses = [];
|
|
84
|
-
const bindings = [];
|
|
85
|
-
if (filter?.kind !== void 0) {
|
|
86
|
-
bindings.push(filter.kind);
|
|
87
|
-
clauses.push(`kind = ?${bindings.length.toString()}`);
|
|
88
|
-
}
|
|
89
|
-
if (filter?.userId !== void 0) {
|
|
90
|
-
bindings.push(filter.userId);
|
|
91
|
-
clauses.push(filter.userId === null ? "user_id IS NULL" : `user_id = ?${bindings.length.toString()}`);
|
|
92
|
-
if (filter.userId === null) {
|
|
93
|
-
bindings.pop();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const where = clauses.length === 0 ? "" : ` WHERE ${clauses.join(" AND ")}`;
|
|
97
|
-
const { results } = await database.prepare(`SELECT * FROM ${table}${where}`).bind(...bindings).all();
|
|
98
|
-
return results.map((row) => rowToSubscription(row));
|
|
99
|
-
};
|
|
100
|
-
const markStatus = async (id, status, error) => {
|
|
101
|
-
await ensureSchema();
|
|
102
|
-
await database.prepare(`UPDATE ${table} SET last_status = ?2, last_error = ?3, last_seen_at = ?4 WHERE id = ?1`).bind(id, status, error ?? null, Date.now()).run();
|
|
103
|
-
};
|
|
104
|
-
return { delete: remove, get, list, markStatus, put };
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export { d1SubscriptionStore };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
const defineNotify = (config) => {
|
|
2
|
-
if (config.webPush !== void 0 && typeof config.webPush !== "function" && typeof config.webPush !== "object") {
|
|
3
|
-
throw new TypeError("defineNotify: `webPush` must be a WebPushConfig object or an `(env) => WebPushConfig` function");
|
|
4
|
-
}
|
|
5
|
-
if (config.fcm !== void 0 && typeof config.fcm !== "function" && typeof config.fcm !== "object") {
|
|
6
|
-
throw new TypeError("defineNotify: `fcm` must be an FcmConfig object or an `(env) => FcmConfig` function");
|
|
7
|
-
}
|
|
8
|
-
if (config.store !== void 0 && typeof config.store !== "function") {
|
|
9
|
-
throw new TypeError("defineNotify: `store` must be a function `(env) => SubscriptionStore` when provided");
|
|
10
|
-
}
|
|
11
|
-
if (config.webPush === void 0 && config.fcm === void 0) {
|
|
12
|
-
throw new TypeError("defineNotify: configure at least one push channel — `webPush` and/or `fcm`");
|
|
13
|
-
}
|
|
14
|
-
return { ...config, isLunoraNotify: true };
|
|
15
|
-
};
|
|
16
|
-
const isNotifyDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraNotify === true;
|
|
17
|
-
|
|
18
|
-
export { defineNotify, isNotifyDefinition };
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const fnv1a = (input) => {
|
|
4
|
-
let hash = 2166136261;
|
|
5
|
-
for (let index = 0; index < input.length; index += 1) {
|
|
6
|
-
hash ^= input.codePointAt(index) ?? 0;
|
|
7
|
-
hash = Math.imul(hash, 16777619);
|
|
8
|
-
}
|
|
9
|
-
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
10
|
-
};
|
|
11
|
-
const webPushId = (endpoint) => `wp_${fnv1a(endpoint)}`;
|
|
12
|
-
const fcmId = (token) => `fcm_${fnv1a(token)}`;
|
|
13
|
-
const parseSubscription = (subscription) => {
|
|
14
|
-
if (typeof subscription !== "string") {
|
|
15
|
-
return subscription ?? {};
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
return JSON.parse(subscription);
|
|
19
|
-
} catch (error) {
|
|
20
|
-
throw new LunoraError(
|
|
21
|
-
"BAD_REQUEST",
|
|
22
|
-
`@lunora/notify: register() web-push subscription is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
const normalizeRegisterInput = (input, now = Date.now()) => {
|
|
27
|
-
if ("token" in input) {
|
|
28
|
-
const { token } = input;
|
|
29
|
-
if (typeof token !== "string" || token === "") {
|
|
30
|
-
throw new LunoraError("BAD_REQUEST", "@lunora/notify: register() fcm input requires a non-empty `token`");
|
|
31
|
-
}
|
|
32
|
-
return { createdAt: now, id: fcmId(token), kind: "fcm", lastSeenAt: now, metadata: input.metadata, token, userId: input.userId ?? null };
|
|
33
|
-
}
|
|
34
|
-
const subscription = parseSubscription(input.subscription);
|
|
35
|
-
const { endpoint } = subscription;
|
|
36
|
-
const p256dh = subscription.keys?.p256dh;
|
|
37
|
-
const auth = subscription.keys?.auth;
|
|
38
|
-
if (typeof endpoint !== "string" || endpoint === "" || typeof p256dh !== "string" || typeof auth !== "string") {
|
|
39
|
-
throw new LunoraError("BAD_REQUEST", "@lunora/notify: register() web-push subscription requires `endpoint` and `keys.{p256dh, auth}`");
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
createdAt: now,
|
|
43
|
-
endpoint,
|
|
44
|
-
id: webPushId(endpoint),
|
|
45
|
-
keys: { auth, p256dh },
|
|
46
|
-
kind: "web-push",
|
|
47
|
-
lastSeenAt: now,
|
|
48
|
-
metadata: input.metadata,
|
|
49
|
-
userId: input.userId ?? null
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
const targetOf = (subscription) => {
|
|
53
|
-
if (subscription.kind === "fcm") {
|
|
54
|
-
return subscription.token ?? "";
|
|
55
|
-
}
|
|
56
|
-
return JSON.stringify({ endpoint: subscription.endpoint, keys: subscription.keys });
|
|
57
|
-
};
|
|
58
|
-
const WEB_PUSH_GONE_PATTERN = /\bhttp\s*4(?:04|10)\b/iu;
|
|
59
|
-
const FCM_GONE_PATTERN = /\b(?:unregistered|not[\s-]?registered|registration-token-not-registered)\b/iu;
|
|
60
|
-
const GONE_TEXT_FALLBACK = /\bsubscription (?:is )?(?:gone|expired|no longer valid)\b/iu;
|
|
61
|
-
const isGoneError = (message) => {
|
|
62
|
-
if (message === void 0) {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
return WEB_PUSH_GONE_PATTERN.test(message) || FCM_GONE_PATTERN.test(message) || GONE_TEXT_FALLBACK.test(message);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export { fcmId, isGoneError, normalizeRegisterInput, targetOf, webPushId };
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const matches = (subscription, filter) => {
|
|
2
|
-
if (filter === void 0) {
|
|
3
|
-
return true;
|
|
4
|
-
}
|
|
5
|
-
if (filter.kind !== void 0 && subscription.kind !== filter.kind) {
|
|
6
|
-
return false;
|
|
7
|
-
}
|
|
8
|
-
if (filter.userId !== void 0 && (subscription.userId ?? null) !== filter.userId) {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
return true;
|
|
12
|
-
};
|
|
13
|
-
const memorySubscriptionStore = () => {
|
|
14
|
-
const map = /* @__PURE__ */ new Map();
|
|
15
|
-
return {
|
|
16
|
-
delete: (id) => {
|
|
17
|
-
map.delete(id);
|
|
18
|
-
return Promise.resolve();
|
|
19
|
-
},
|
|
20
|
-
get: (id) => Promise.resolve(map.get(id)),
|
|
21
|
-
list: (filter) => {
|
|
22
|
-
const result = [];
|
|
23
|
-
for (const subscription of map.values()) {
|
|
24
|
-
if (matches(subscription, filter)) {
|
|
25
|
-
result.push(subscription);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return Promise.resolve(result);
|
|
29
|
-
},
|
|
30
|
-
markStatus: (id, status, error) => {
|
|
31
|
-
const existing = map.get(id);
|
|
32
|
-
if (existing !== void 0) {
|
|
33
|
-
map.set(id, { ...existing, lastError: error, lastSeenAt: Date.now(), lastStatus: status });
|
|
34
|
-
}
|
|
35
|
-
return Promise.resolve();
|
|
36
|
-
},
|
|
37
|
-
put: (subscription) => {
|
|
38
|
-
const existing = map.get(subscription.id);
|
|
39
|
-
const merged = existing === void 0 ? subscription : { ...existing, ...subscription, createdAt: existing.createdAt };
|
|
40
|
-
map.set(merged.id, merged);
|
|
41
|
-
return Promise.resolve(merged);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
export { memorySubscriptionStore };
|