easy-ping 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +15 -4
  2. package/dist/{adapter-f0OR2DhY.d.cts → adapter-BfLe2J0t.d.cts} +10 -0
  3. package/dist/{adapter-f0OR2DhY.d.ts → adapter-BfLe2J0t.d.ts} +10 -0
  4. package/dist/adapters/drizzle.cjs +9 -4
  5. package/dist/adapters/drizzle.cjs.map +1 -1
  6. package/dist/adapters/drizzle.d.cts +35 -1
  7. package/dist/adapters/drizzle.d.ts +35 -1
  8. package/dist/adapters/drizzle.js +10 -5
  9. package/dist/adapters/drizzle.js.map +1 -1
  10. package/dist/adapters/mongodb.cjs +9 -4
  11. package/dist/adapters/mongodb.cjs.map +1 -1
  12. package/dist/adapters/mongodb.d.cts +2 -2
  13. package/dist/adapters/mongodb.d.ts +2 -2
  14. package/dist/adapters/mongodb.js +8 -4
  15. package/dist/adapters/mongodb.js.map +1 -1
  16. package/dist/adapters/postgres.cjs +136 -3
  17. package/dist/adapters/postgres.cjs.map +1 -1
  18. package/dist/adapters/postgres.d.cts +45 -2
  19. package/dist/adapters/postgres.d.ts +45 -2
  20. package/dist/adapters/postgres.js +38 -5
  21. package/dist/adapters/postgres.js.map +1 -1
  22. package/dist/{chunk-OZSOIX23.js → chunk-3Y2MBUAW.js} +53 -12
  23. package/dist/chunk-3Y2MBUAW.js.map +1 -0
  24. package/dist/chunk-CYKAM7LE.js +226 -0
  25. package/dist/chunk-CYKAM7LE.js.map +1 -0
  26. package/dist/chunk-DG3ZL5NP.js +315 -0
  27. package/dist/chunk-DG3ZL5NP.js.map +1 -0
  28. package/dist/{chunk-NYDVNMKG.js → chunk-PSJHXZPR.js} +4 -3
  29. package/dist/chunk-PSJHXZPR.js.map +1 -0
  30. package/dist/{chunk-BV3U542P.js → chunk-W6I3B2CI.js} +2 -2
  31. package/dist/chunk-W6I3B2CI.js.map +1 -0
  32. package/dist/chunk-YEMRRMJQ.js +199 -0
  33. package/dist/chunk-YEMRRMJQ.js.map +1 -0
  34. package/dist/client.cjs +3 -5
  35. package/dist/client.cjs.map +1 -1
  36. package/dist/client.js +3 -5
  37. package/dist/client.js.map +1 -1
  38. package/dist/index.cjs +377 -73
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.cts +88 -22
  41. package/dist/index.d.ts +88 -22
  42. package/dist/index.js +119 -351
  43. package/dist/index.js.map +1 -1
  44. package/dist/node.cjs +27 -6
  45. package/dist/node.cjs.map +1 -1
  46. package/dist/node.d.cts +8 -2
  47. package/dist/node.d.ts +8 -2
  48. package/dist/node.js +26 -6
  49. package/dist/node.js.map +1 -1
  50. package/dist/{plugin-Bqj9xAOS.d.cts → plugin-C9Hoxus2.d.ts} +38 -10
  51. package/dist/{plugin-bTJKmnlq.d.ts → plugin-CiMsGlQe.d.cts} +38 -10
  52. package/dist/plugins/digests.d.cts +2 -2
  53. package/dist/plugins/digests.d.ts +2 -2
  54. package/dist/plugins/preferences-client.cjs.map +1 -1
  55. package/dist/plugins/preferences-client.d.cts +2 -2
  56. package/dist/plugins/preferences-client.d.ts +2 -2
  57. package/dist/plugins/preferences-client.js +1 -1
  58. package/dist/plugins/preferences.cjs +100 -15
  59. package/dist/plugins/preferences.cjs.map +1 -1
  60. package/dist/plugins/preferences.d.cts +4 -2
  61. package/dist/plugins/preferences.d.ts +4 -2
  62. package/dist/plugins/preferences.js +35 -7
  63. package/dist/plugins/preferences.js.map +1 -1
  64. package/dist/plugins/push.cjs +222 -61
  65. package/dist/plugins/push.cjs.map +1 -1
  66. package/dist/plugins/push.d.cts +81 -3
  67. package/dist/plugins/push.d.ts +81 -3
  68. package/dist/plugins/push.js +164 -62
  69. package/dist/plugins/push.js.map +1 -1
  70. package/dist/providers/web-push.cjs +37 -11
  71. package/dist/providers/web-push.cjs.map +1 -1
  72. package/dist/providers/web-push.d.cts +9 -3
  73. package/dist/providers/web-push.d.ts +9 -3
  74. package/dist/providers/web-push.js +37 -12
  75. package/dist/providers/web-push.js.map +1 -1
  76. package/dist/react.cjs +3 -5
  77. package/dist/react.cjs.map +1 -1
  78. package/dist/react.js +3 -5
  79. package/dist/react.js.map +1 -1
  80. package/dist/schema.cjs +2 -1
  81. package/dist/schema.cjs.map +1 -1
  82. package/dist/schema.d.cts +7 -2
  83. package/dist/schema.d.ts +7 -2
  84. package/dist/schema.js +2 -198
  85. package/dist/schema.js.map +1 -1
  86. package/dist/testing.cjs +29 -0
  87. package/dist/testing.cjs.map +1 -1
  88. package/dist/testing.d.cts +1 -1
  89. package/dist/testing.d.ts +1 -1
  90. package/dist/testing.js +29 -0
  91. package/dist/testing.js.map +1 -1
  92. package/package.json +1 -1
  93. package/dist/chunk-BV3U542P.js.map +0 -1
  94. package/dist/chunk-NYDVNMKG.js.map +0 -1
  95. package/dist/chunk-OZSOIX23.js.map +0 -1
  96. package/dist/chunk-W3BMEPPQ.js +0 -50
  97. package/dist/chunk-W3BMEPPQ.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,361 +1,60 @@
1
- import { verifyToken } from './chunk-W3BMEPPQ.js';
2
- export { DEFAULT_TOKEN_TTL_SECONDS, expiresIn, signToken, verifyToken } from './chunk-W3BMEPPQ.js';
3
- export { defineClientPlugin, definePlugin } from './chunk-BV3U542P.js';
4
- import { consoleLogger, createPluginStore, ConfigError, ValidationError } from './chunk-OZSOIX23.js';
5
- export { ConfigError, EasyPingError, ValidationError, consoleLogger } from './chunk-OZSOIX23.js';
1
+ import { createRunner } from './chunk-CYKAM7LE.js';
2
+ export { createRunner, redactErrorMessage } from './chunk-CYKAM7LE.js';
3
+ export { defineClientPlugin, definePlugin } from './chunk-W6I3B2CI.js';
4
+ import { createHandler, createScopedSigner } from './chunk-DG3ZL5NP.js';
5
+ export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, expiresIn, readJsonBody, signToken, verifyToken } from './chunk-DG3ZL5NP.js';
6
+ import { consoleLogger, ConfigError, createPluginStore, ValidationError } from './chunk-3Y2MBUAW.js';
7
+ export { ConfigError, EasyPingError, ValidationError, consoleLogger } from './chunk-3Y2MBUAW.js';
6
8
  export { decodeBase64Url, encodeBase64Url } from './chunk-PTGHMCCG.js';
7
- import './chunk-NYDVNMKG.js';
9
+ import './chunk-PSJHXZPR.js';
8
10
 
9
11
  // src/core/definition.ts
10
12
  function defineNotification(definition) {
11
13
  return definition;
12
14
  }
13
15
 
14
- // src/core/handler.ts
15
- var json = (body, status = 200) => new Response(JSON.stringify(body), {
16
- status,
17
- headers: { "content-type": "application/json" }
18
- });
19
- var empty = (status) => new Response(null, { status });
20
- var MAX_READ_IDS = 200;
21
- function timingSafeEqual(a, b) {
22
- const encoder = new TextEncoder();
23
- const left = encoder.encode(a);
24
- const right = encoder.encode(b);
25
- let diff = left.length ^ right.length;
26
- const length = Math.max(left.length, right.length);
27
- for (let i = 0; i < length; i += 1) {
28
- diff |= (left[i] ?? 0) ^ (right[i] ?? 0);
29
- }
30
- return diff === 0;
31
- }
32
- function routePath(url, basePath) {
33
- const index = url.pathname.indexOf(basePath);
34
- if (index < 0) return url.pathname;
35
- return url.pathname.slice(index + basePath.length) || "/";
36
- }
37
- async function readJson(request) {
38
- try {
39
- const body = await request.json();
40
- return body && typeof body === "object" ? body : {};
41
- } catch {
42
- return {};
43
- }
44
- }
45
- function createHandler(deps) {
46
- async function resolveUserId(request) {
47
- try {
48
- return await deps.session.getUserId(request);
49
- } catch (error) {
50
- deps.logger.error("session.getUserId threw", { error });
51
- return "error";
52
- }
53
- }
54
- async function handleCron(request) {
55
- if (!deps.cronSecret) return empty(404);
56
- const header = request.headers.get("authorization") ?? "";
57
- const presented = header.startsWith("Bearer ") ? header.slice(7) : header;
58
- if (!timingSafeEqual(presented, deps.cronSecret)) return empty(401);
59
- const result = await deps.runner.drain();
60
- return json(result);
61
- }
62
- async function handlePluginRoute(route, request, url) {
63
- if (route.scope.type === "user") {
64
- const userId = await resolveUserId(request);
65
- if (userId === "error") return empty(500);
66
- if (userId === null) return empty(401);
67
- return route.handler({ request, userId, claims: null, params: {} });
68
- }
69
- if (route.scope.type === "machine") {
70
- if (!deps.cronSecret) return empty(404);
71
- const header = request.headers.get("authorization") ?? "";
72
- const presented = header.startsWith("Bearer ") ? header.slice(7) : header;
73
- if (!timingSafeEqual(presented, deps.cronSecret)) return empty(401);
74
- return route.handler({ request, userId: null, claims: null, params: {} });
75
- }
76
- if (route.scope.type === "signed") {
77
- const token = url.searchParams.get("token") ?? (request.headers.get("authorization") ?? "").replace(/^Bearer /, "");
78
- const claims = await verifyToken(deps.secret, token, route.scope.purpose);
79
- if (!claims) return empty(400);
80
- return route.handler({ request, userId: claims.uid, claims, params: {} });
81
- }
82
- return route.handler({ request, userId: null, claims: null, params: {} });
83
- }
84
- async function handle(request) {
85
- const url = new URL(request.url);
86
- const path = routePath(url, deps.basePath);
87
- if (path === "/cron") {
88
- if (request.method !== "POST") return empty(405);
89
- return handleCron(request);
90
- }
91
- const pluginRoute = deps.pluginRoutes?.find(
92
- (route) => route.path === path && route.method === request.method
93
- );
94
- if (pluginRoute) return handlePluginRoute(pluginRoute, request, url);
95
- const userId = await resolveUserId(request);
96
- if (userId === "error") return empty(500);
97
- if (userId === null) return empty(401);
98
- if (request.method === "GET" && path === "/") {
99
- const limitParam = Number(url.searchParams.get("limit") ?? 20);
100
- const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;
101
- const cursor = url.searchParams.get("cursor");
102
- const page = await deps.adapter.listNotifications({
103
- userId,
104
- limit,
105
- ...cursor ? { cursor } : {},
106
- ...url.searchParams.get("unreadOnly") === "true" ? { unreadOnly: true } : {}
107
- });
108
- return json(page);
109
- }
110
- if (request.method === "GET" && path === "/count") {
111
- return json({ unseen: await deps.adapter.countUnseen(userId) });
112
- }
113
- if (request.method === "POST" && path === "/seen") {
114
- const body = await readJson(request);
115
- const before = typeof body.before === "string" ? new Date(body.before) : null;
116
- const cutoff = before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : /* @__PURE__ */ new Date();
117
- await deps.adapter.markSeen(userId, cutoff);
118
- return json({ ok: true });
119
- }
120
- if (request.method === "POST" && path === "/read") {
121
- const body = await readJson(request);
122
- const ids = Array.isArray(body.ids) ? body.ids.filter((id) => typeof id === "string") : [];
123
- if (ids.length === 0) return json({ error: "ids must be a non-empty string array" }, 400);
124
- if (ids.length > MAX_READ_IDS) {
125
- return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);
126
- }
127
- const updated = await deps.adapter.markRead(userId, ids);
128
- return updated === 0 ? empty(404) : json({ updated });
129
- }
130
- if (request.method === "POST" && path === "/read-all") {
131
- return json({ updated: await deps.adapter.markAllRead(userId) });
132
- }
133
- return empty(404);
134
- }
135
- return {
136
- handle,
137
- GET: handle,
138
- POST: handle
139
- };
140
- }
141
-
142
- // src/core/backoff.ts
143
- var SCHEDULE_MS = [3e4, 12e4, 48e4, 192e4];
144
- function backoffMs(attempt, strategy = "exponential") {
145
- if (typeof strategy === "function") return Math.max(0, strategy(attempt));
146
- const index = Math.min(Math.max(attempt, 1), SCHEDULE_MS.length) - 1;
147
- const base = SCHEDULE_MS[index] ?? SCHEDULE_MS[SCHEDULE_MS.length - 1] ?? 3e4;
148
- return Math.round(base * (0.8 + Math.random() * 0.4));
149
- }
150
- var nextAttemptAt = (attempt, strategy, now = /* @__PURE__ */ new Date()) => new Date(now.getTime() + backoffMs(attempt, strategy));
16
+ // src/core/html.ts
17
+ var ESCAPES = {
18
+ "&": "&amp;",
19
+ "<": "&lt;",
20
+ ">": "&gt;",
21
+ '"': "&quot;",
22
+ "'": "&#39;"
23
+ };
24
+ var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
151
25
 
152
- // src/core/runner.ts
153
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
154
- async function withTimeout(run, ms, label) {
155
- const controller = new AbortController();
156
- let timer;
157
- const timeout = new Promise((_, reject) => {
158
- timer = setTimeout(() => {
159
- controller.abort();
160
- reject(new Error(`${label} timed out after ${ms}ms`));
161
- }, ms);
162
- });
163
- try {
164
- return await Promise.race([run(controller.signal), timeout]);
165
- } finally {
166
- clearTimeout(timer);
167
- }
168
- }
169
- var message = (error) => error instanceof Error ? error.message : String(error);
170
- function createRunner(deps) {
171
- async function deliverOne(delivery, recipient) {
172
- if (!recipient) {
173
- return { result: "failed", error: "recipient could not be resolved", retryable: false };
174
- }
175
- if (delivery.channel === "inApp") {
176
- return { result: "sent" };
177
- }
178
- if (delivery.channel === "email") {
179
- const provider = deps.channels.email?.provider;
180
- if (!provider) {
181
- return { result: "failed", error: "no email provider configured", retryable: false };
182
- }
183
- const to = recipient.email;
184
- if (!to) {
185
- return { result: "failed", error: "recipient has no email address", retryable: false };
186
- }
187
- const email = deps.definitions[delivery.notification.type]?.email;
188
- if (!email) {
189
- return {
190
- result: "failed",
191
- error: `notification type "${delivery.notification.type}" has no email template`,
192
- retryable: false
193
- };
194
- }
195
- try {
196
- const payload = delivery.notification.payload;
197
- const html = await email.template(payload);
198
- await withTimeout(
199
- (signal) => provider.send({
200
- to,
201
- subject: email.subject(payload),
202
- html,
203
- // Delivery is at-least-once; the provider dedupes on this.
204
- idempotencyKey: delivery.id,
205
- signal
206
- }),
207
- provider.timeoutMs ?? 3e4,
208
- `${provider.name} send`
209
- );
210
- return { result: "sent" };
211
- } catch (error) {
212
- return {
213
- result: "failed",
214
- error: message(error),
215
- retryable: provider.isRetryable(error)
216
- };
217
- }
218
- }
219
- const owner = deps.plugins.find(
220
- (plugin) => plugin.channels?.includes(delivery.channel) && plugin.hooks?.deliver
221
- );
222
- if (!owner?.hooks?.deliver) {
223
- return {
224
- result: "failed",
225
- error: `no plugin delivers channel "${delivery.channel}"`,
226
- retryable: false
227
- };
228
- }
229
- try {
230
- return await withTimeout(
231
- (signal) => Promise.resolve(
232
- // biome-ignore lint/style/noNonNullAssertion: guarded directly above
233
- owner.hooks.deliver({
234
- channel: delivery.channel,
235
- deliveryId: delivery.id,
236
- attempt: delivery.attempts + 1,
237
- recipient,
238
- notification: {
239
- id: delivery.notificationId,
240
- userId: delivery.notification.userId,
241
- type: delivery.notification.type,
242
- payload: delivery.notification.payload
243
- },
244
- signal
245
- })
246
- ),
247
- 3e4,
248
- `plugin "${owner.id}" deliver`
249
- );
250
- } catch (error) {
251
- return { result: "failed", error: message(error), retryable: true };
252
- }
253
- }
254
- async function notifyAfterDeliver(delivery, outcome) {
255
- const isTerminal = outcome.result === "sent" || !outcome.retryable || delivery.attempts + 1 >= delivery.maxAttempts;
256
- for (const plugin of deps.plugins) {
257
- const hook = plugin.hooks?.afterDeliver;
258
- if (!hook) continue;
259
- try {
260
- await hook({
261
- deliveryId: delivery.id,
262
- notificationId: delivery.notificationId,
263
- userId: delivery.notification.userId,
264
- channel: delivery.channel,
265
- outcome,
266
- attempt: delivery.attempts + 1,
267
- isTerminal
268
- });
269
- } catch (error) {
270
- deps.logger.error(`plugin "${plugin.id}" afterDeliver threw; ignoring`, {
271
- deliveryId: delivery.id,
272
- error
273
- });
26
+ // src/core/rate-limit.ts
27
+ var clientAddress = (request) => {
28
+ const headers = request.headers;
29
+ return headers.get("cf-connecting-ip") ?? headers.get("x-real-ip") ?? headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? null;
30
+ };
31
+ function createRateLimiter(config) {
32
+ const keyOf = config.key ?? clientAddress;
33
+ const buckets = /* @__PURE__ */ new Map();
34
+ let lastSweep = 0;
35
+ return (request) => {
36
+ const key = keyOf(request);
37
+ if (key === null) return null;
38
+ const now = Date.now();
39
+ if (now - lastSweep > config.windowMs) {
40
+ for (const [id, bucket2] of buckets) if (bucket2.resetAt <= now) buckets.delete(id);
41
+ lastSweep = now;
42
+ }
43
+ let bucket = buckets.get(key);
44
+ if (!bucket || bucket.resetAt <= now) {
45
+ bucket = { count: 0, resetAt: now + config.windowMs };
46
+ buckets.set(key, bucket);
47
+ }
48
+ bucket.count += 1;
49
+ if (bucket.count <= config.max) return null;
50
+ return new Response(null, {
51
+ status: 429,
52
+ headers: {
53
+ "retry-after": String(Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3))),
54
+ "cache-control": "private, no-store"
274
55
  }
275
- }
276
- }
277
- async function runOnce(options = {}) {
278
- const claimToken = crypto.randomUUID();
279
- const claimed = await deps.adapter.claimPendingDeliveries({
280
- limit: options.limit ?? deps.batchSize,
281
- leaseMs: deps.leaseMs,
282
- claimToken,
283
- ...options.channels ? { channels: options.channels } : {},
284
- ...options.ids ? { ids: options.ids } : {}
285
56
  });
286
- if (claimed.length === 0) return { claimed: 0, sent: 0, failed: 0 };
287
- const userIds = [...new Set(claimed.map((row) => row.notification.userId))];
288
- const recipients = await deps.getRecipients(userIds);
289
- const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));
290
- const releases = [];
291
- let sent = 0;
292
- let failed = 0;
293
- for (const delivery of claimed) {
294
- const outcome = await deliverOne(delivery, byUserId.get(delivery.notification.userId));
295
- if (outcome.result === "sent") sent += 1;
296
- else failed += 1;
297
- releases.push({
298
- id: delivery.id,
299
- claimToken,
300
- outcome,
301
- ...outcome.result === "failed" && outcome.retryable ? { nextAttemptAt: nextAttemptAt(delivery.attempts + 1, deps.backoff) } : {}
302
- });
303
- await notifyAfterDeliver(delivery, outcome);
304
- }
305
- await deps.adapter.releaseDeliveries(releases);
306
- return { claimed: claimed.length, sent, failed };
307
- }
308
- async function drain(options = {}) {
309
- const maxSweeps = options.maxSweeps ?? 50;
310
- const total = { claimed: 0, sent: 0, failed: 0 };
311
- for (let sweep = 0; sweep < maxSweeps; sweep += 1) {
312
- const result = await runOnce(options.ids ? { ids: options.ids } : {});
313
- total.claimed += result.claimed;
314
- total.sent += result.sent;
315
- total.failed += result.failed;
316
- if (result.claimed === 0) break;
317
- }
318
- return total;
319
- }
320
- let warnedMissingWaitUntil = false;
321
- function scheduleDeferred(waitUntil, ids) {
322
- if (waitUntil) {
323
- waitUntil(
324
- drain({ maxSweeps: 10, ...ids ? { ids } : {} }).catch(
325
- (error) => deps.logger.error("deferred sweep failed", { error })
326
- )
327
- );
328
- return;
329
- }
330
- if (!warnedMissingWaitUntil) {
331
- warnedMissingWaitUntil = true;
332
- deps.logger.warn(
333
- "delivery.mode is 'deferred' but no delivery.waitUntil was supplied \u2014 falling back to the cron sweep. Notifications are not lost, but they will not be delivered until POST /cron runs."
334
- );
335
- }
336
- }
337
- function startWorker(options = {}) {
338
- const intervalMs = options.intervalMs ?? 1e3;
339
- let stopped = false;
340
- const loop = (async () => {
341
- while (!stopped) {
342
- try {
343
- const result = await runOnce({ limit: options.batchSize ?? deps.batchSize });
344
- if (result.claimed === 0) await sleep(intervalMs);
345
- } catch (error) {
346
- deps.logger.error("worker sweep failed", { error });
347
- await sleep(intervalMs);
348
- }
349
- }
350
- })();
351
- return {
352
- stop: async () => {
353
- stopped = true;
354
- await loop;
355
- }
356
- };
357
- }
358
- return { runOnce, drain, scheduleDeferred, startWorker };
57
+ };
359
58
  }
360
59
 
361
60
  // src/core/send.ts
@@ -549,6 +248,14 @@ async function runSend(type, input, deps) {
549
248
  }
550
249
 
551
250
  // src/core/instance.ts
251
+ var CORE_ROUTES = [
252
+ { method: "GET", path: "/", scope: { type: "user" }, owner: "core" },
253
+ { method: "GET", path: "/count", scope: { type: "user" }, owner: "core" },
254
+ { method: "POST", path: "/seen", scope: { type: "user" }, owner: "core" },
255
+ { method: "POST", path: "/read", scope: { type: "user" }, owner: "core" },
256
+ { method: "POST", path: "/read-all", scope: { type: "user" }, owner: "core" },
257
+ { method: "POST", path: "/cron", scope: { type: "machine" }, owner: "core" }
258
+ ];
552
259
  var DEFAULTS = {
553
260
  maxAttempts: 5,
554
261
  leaseMs: 6e4,
@@ -556,6 +263,24 @@ var DEFAULTS = {
556
263
  basePath: "/api/notifications",
557
264
  mode: "cron"
558
265
  };
266
+ var MIN_SECRET_LENGTH = 16;
267
+ var PLACEHOLDER_SECRETS = /* @__PURE__ */ new Set([
268
+ "secret",
269
+ "changeme",
270
+ "change-me",
271
+ "replace-me",
272
+ "replace-me-too",
273
+ "password",
274
+ "demo-signing-secret",
275
+ "demo-cron-secret"
276
+ ]);
277
+ function checkSecretStrength(name, value) {
278
+ if (value.length < MIN_SECRET_LENGTH || PLACEHOLDER_SECRETS.has(value.toLowerCase())) {
279
+ throw new ConfigError(
280
+ `\`${name}\` must be at least ${MIN_SECRET_LENGTH} characters and not a placeholder. Generate one with \`openssl rand -base64 32\`.`
281
+ );
282
+ }
283
+ }
559
284
  function validate(config, plugins) {
560
285
  if (!config.database) throw new ConfigError("`database` is required.");
561
286
  if (!config.notifications) throw new ConfigError("`notifications` is required.");
@@ -569,6 +294,12 @@ function validate(config, plugins) {
569
294
  "`secret` is required \u2014 it signs unsubscribe and other session-less links. Adding it later is a breaking change, so it is mandatory from the start."
570
295
  );
571
296
  }
297
+ checkSecretStrength("secret", config.secret);
298
+ if (config.cron?.secret) checkSecretStrength("cron.secret", config.cron.secret);
299
+ if (config.machineSecret) checkSecretStrength("machineSecret", config.machineSecret);
300
+ if (config.rateLimit && (config.rateLimit.max < 1 || config.rateLimit.windowMs < 1)) {
301
+ throw new ConfigError("`rateLimit.max` and `rateLimit.windowMs` must both be positive.");
302
+ }
572
303
  if (typeof config.getRecipients !== "function") {
573
304
  throw new ConfigError("`getRecipients` is required to resolve emails and timezones.");
574
305
  }
@@ -637,6 +368,15 @@ function easyPing(config) {
637
368
  "delivery.mode is 'deferred' but no delivery.waitUntil was supplied; delivery will wait for the cron sweep."
638
369
  );
639
370
  }
371
+ for (const plugin of plugins) {
372
+ for (const route of plugin.routes ?? []) {
373
+ if (route.scope.type === "custom") {
374
+ warnings.push(
375
+ `plugin "${plugin.id}" mounts ${route.method} ${route.path} with custom auth: ` + route.scope.justification
376
+ );
377
+ }
378
+ }
379
+ }
640
380
  for (const warning of warnings) logger.warn(warning);
641
381
  const runner = createRunner({
642
382
  adapter: config.database,
@@ -658,7 +398,13 @@ function easyPing(config) {
658
398
  secret: config.secret,
659
399
  basePath: config.basePath ?? DEFAULTS.basePath,
660
400
  pluginRoutes,
661
- ...config.cron?.secret ? { cronSecret: config.cron.secret } : {}
401
+ trustedOrigins: config.trustedOrigins,
402
+ onRequest: config.onRequest,
403
+ maxBodyBytes: config.maxBodyBytes,
404
+ rateLimit: config.rateLimit ? createRateLimiter(config.rateLimit) : void 0,
405
+ cronMaxSweeps: config.cron?.maxSweeps,
406
+ ...config.cron?.secret ? { cronSecret: config.cron.secret } : {},
407
+ ...config.machineSecret ?? config.cron?.secret ? { machineSecret: config.machineSecret ?? config.cron?.secret } : {}
662
408
  });
663
409
  async function send(type, args) {
664
410
  const result = await runSend(
@@ -690,10 +436,21 @@ function easyPing(config) {
690
436
  return result;
691
437
  }
692
438
  for (const plugin of plugins) {
439
+ const purposes = new Set(
440
+ (plugin.routes ?? []).flatMap(
441
+ (route) => route.scope.type === "signed" ? [route.scope.purpose] : []
442
+ )
443
+ );
693
444
  plugin.init?.({
694
445
  store: createPluginStore(plugin.id, plugin.schema, config.database, tablePrefix),
695
- secret: config.secret,
446
+ sign: createScopedSigner(
447
+ config.secret,
448
+ plugin.id,
449
+ purposes,
450
+ (message) => new ConfigError(message)
451
+ ),
696
452
  logger,
453
+ notificationTypes: Object.keys(config.notifications),
697
454
  getRecipients: config.getRecipients,
698
455
  send: (type, args) => send(type, args)
699
456
  });
@@ -702,6 +459,17 @@ function easyPing(config) {
702
459
  send,
703
460
  handler,
704
461
  startWorker: (options) => runner.startWorker(options ?? {}),
462
+ listRoutes: () => [
463
+ ...CORE_ROUTES.filter((route) => route.path !== "/cron" || Boolean(config.cron?.secret)),
464
+ ...plugins.flatMap(
465
+ (plugin) => (plugin.routes ?? []).map((route) => ({
466
+ method: route.method,
467
+ path: route.path,
468
+ scope: route.scope,
469
+ owner: plugin.id
470
+ }))
471
+ )
472
+ ],
705
473
  getFailedDeliveries: (options = {}) => config.database.getFailedDeliveries({
706
474
  since: options.since ?? new Date(Date.now() - 24 * 36e5),
707
475
  limit: Math.min(options.limit ?? 100, 1e3)
@@ -718,6 +486,6 @@ function easyPing(config) {
718
486
  };
719
487
  }
720
488
 
721
- export { createRunner, defineNotification, easyPing };
489
+ export { MIN_SECRET_LENGTH, createRateLimiter, defineNotification, easyPing, escapeHtml };
722
490
  //# sourceMappingURL=index.js.map
723
491
  //# sourceMappingURL=index.js.map