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.d.cts CHANGED
@@ -1,8 +1,8 @@
1
- import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-f0OR2DhY.cjs';
2
- export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-f0OR2DhY.cjs';
1
+ import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-BfLe2J0t.cjs';
2
+ export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-BfLe2J0t.cjs';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
- import { A as AnyPlugin, L as Logger } from './plugin-Bqj9xAOS.cjs';
5
- export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, P as PluginHooks, j as PrepareContext, k as Promisable, R as ResolveChannelsContext, l as RouteContext, m as RouteDefinition, n as RouteScope, S as SchemaDeclaration, T as TableDeclaration, U as UnionToIntersection, V as ValidationError, o as consoleLogger, p as defineClientPlugin, q as definePlugin } from './plugin-Bqj9xAOS.cjs';
4
+ import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-CiMsGlQe.cjs';
5
+ export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-CiMsGlQe.cjs';
6
6
  import { E as EmailProvider } from './provider-CA45WVqt.cjs';
7
7
  export { a as EmailMessage, P as ProviderSendResult } from './provider-CA45WVqt.cjs';
8
8
 
@@ -38,6 +38,25 @@ type PayloadOf<TDef> = TDef extends {
38
38
  schema: infer TSchema extends StandardSchemaV1;
39
39
  } ? StandardSchemaV1.InferOutput<TSchema> : Record<string, unknown>;
40
40
 
41
+ type RateLimitConfig = {
42
+ /** Requests allowed per key per window. */
43
+ max: number;
44
+ windowMs: number;
45
+ /**
46
+ * Which bucket a request lands in. Defaults to the client address as the
47
+ * usual proxy headers report it; returning null exempts the request. Behind
48
+ * no proxy those headers are absent, so supply your own key there.
49
+ */
50
+ key?: (request: Request) => string | null;
51
+ };
52
+ /**
53
+ * Fixed-window counter held in process memory. Per isolate on edge runtimes
54
+ * and per replica anywhere else, so it bounds one client's cost to one
55
+ * process rather than enforcing a global quota. That is enough to blunt
56
+ * secret guessing and poll storms, which is what it is for.
57
+ */
58
+ declare function createRateLimiter(config: RateLimitConfig): (request: Request) => Response | null;
59
+
41
60
  type SessionConfig = {
42
61
  /** null yields 401; throwing yields 500. Deliberately distinct. */
43
62
  getUserId: (request: Request) => string | null | Promise<string | null>;
@@ -77,10 +96,33 @@ type EasyPingConfig<TDefs extends NotificationDefinitions> = {
77
96
  /** Required by `cron` and `deferred` modes. See RFC 0002 §4. */
78
97
  cron?: {
79
98
  secret: string;
99
+ /** Sweeps one POST /cron may run before returning. Defaults to 50. */
100
+ maxSweeps?: number;
80
101
  };
102
+ /**
103
+ * Bearer secret for plugins' machine routes (`/push/prune`, `/digests/cron`).
104
+ * Falls back to `cron.secret`; set it when the scheduler that hits /cron
105
+ * should not also be able to prune devices or fire digests.
106
+ */
107
+ machineSecret?: string;
108
+ /**
109
+ * In-process fixed-window limiter, applied before routing. Keyed by client
110
+ * address from the usual proxy headers unless `key` says otherwise.
111
+ */
112
+ rateLimit?: RateLimitConfig;
81
113
  plugins?: readonly AnyPlugin[];
82
114
  /** Where the handler is mounted, used to strip the prefix off incoming URLs. */
83
115
  basePath?: string;
116
+ /**
117
+ * Origins allowed to POST besides the request's own host. Cross-origin
118
+ * POSTs are otherwise refused with 403, and every POST must be
119
+ * `application/json`. `"*.example.com"` matches subdomains.
120
+ */
121
+ trustedOrigins?: readonly string[];
122
+ /** Runs before routing. Return a Response to short-circuit — a rate limiter's 429, say. */
123
+ onRequest?: (request: Request) => Promisable<Response | undefined | void>;
124
+ /** Defaults to 64 KiB. */
125
+ maxBodyBytes?: number;
84
126
  /** Table-name prefix; must match the one given to the adapter. */
85
127
  tablePrefix?: string;
86
128
  logger?: Logger;
@@ -111,6 +153,13 @@ type SendResult = {
111
153
  type Worker = {
112
154
  stop: () => Promise<void>;
113
155
  };
156
+ type MountedRoute = {
157
+ method: "GET" | "POST";
158
+ path: string;
159
+ scope: RouteScope;
160
+ /** "core" for the built-in routes, otherwise the plugin id. */
161
+ owner: string;
162
+ };
114
163
  type HealthReport = {
115
164
  mode: DeliveryMode;
116
165
  cronMounted: boolean;
@@ -131,6 +180,8 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
131
180
  batchSize?: number;
132
181
  }): Worker;
133
182
  healthCheck(): Promise<HealthReport>;
183
+ /** Every mounted route with its auth scope, so `custom`-scoped ones stay visible. RFC 0002 §3. */
184
+ listRoutes(): readonly MountedRoute[];
134
185
  /**
135
186
  * Deliveries that exhausted their attempts, newest first.
136
187
  *
@@ -146,8 +197,6 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
146
197
  }): Promise<readonly DeliveryRecord[]>;
147
198
  };
148
199
 
149
- declare function easyPing<TDefs extends NotificationDefinitions>(config: EasyPingConfig<TDefs>): EasyPing<TDefs>;
150
-
151
200
  type Backoff = "exponential" | ((attempt: number) => number);
152
201
 
153
202
  type RunnerDeps = {
@@ -165,7 +214,15 @@ type SweepResult = {
165
214
  claimed: number;
166
215
  sent: number;
167
216
  failed: number;
217
+ /** Deliveries that had nothing to do. Not errors; see DeliveryOutcome. */
218
+ skipped: number;
168
219
  };
220
+ /**
221
+ * What gets persisted to `last_error`. Provider responses quote the offending
222
+ * field, which for a bad recipient is the address itself; that column outlives
223
+ * the notification and is read by admin tooling, so addresses do not go in.
224
+ */
225
+ declare const redactErrorMessage: (text: string) => string;
169
226
  declare function createRunner(deps: RunnerDeps): {
170
227
  runOnce: (options?: {
171
228
  limit?: number;
@@ -184,20 +241,29 @@ declare function createRunner(deps: RunnerDeps): {
184
241
  };
185
242
  type Runner = ReturnType<typeof createRunner>;
186
243
 
187
- type TokenClaims = {
188
- /** Subject — the user the token acts for. */
189
- uid: string;
190
- /** What the token is allowed to do. Verification requires an exact match. */
191
- purpose: string;
192
- /** Expiry, epoch seconds. */
193
- exp: number;
194
- /** Free-form, purpose-specific. */
195
- data?: Record<string, string>;
196
- };
197
- declare function signToken(secret: string, claims: TokenClaims): Promise<string>;
198
- /** null for every failure mode. Distinguishing them tells an attacker which part to fix. */
199
- declare function verifyToken(secret: string, token: string, purpose: string, now?: Date): Promise<TokenClaims | null>;
200
- declare const DEFAULT_TOKEN_TTL_SECONDS: number;
201
- declare const expiresIn: (seconds: number, now?: Date) => number;
244
+ /** Every JSON body is capped here. Nothing legitimate the routes accept is bigger. */
245
+ declare const MAX_BODY_BYTES: number;
246
+ type JsonBody = {
247
+ body: Record<string, unknown>;
248
+ } | {
249
+ error: Response;
250
+ };
251
+ /**
252
+ * Reads a JSON object body with a byte cap enforced while streaming, so an
253
+ * oversized body is refused before it is buffered. Malformed JSON is 400; an
254
+ * empty body is `{}`. Plugin routes should use this rather than
255
+ * `request.json()`, which has no limit.
256
+ */
257
+ declare function readJsonBody(request: Request, maxBytes?: number): Promise<JsonBody>;
258
+
259
+ /**
260
+ * For email templates built with template literals. Anything in a payload
261
+ * that a user typed — a display name, a comment excerpt — goes through this
262
+ * or the recipient gets that user's HTML delivered from your sending domain.
263
+ */
264
+ declare const escapeHtml: (value: unknown) => string;
265
+
266
+ declare const MIN_SECRET_LENGTH = 16;
267
+ declare function easyPing<TDefs extends NotificationDefinitions>(config: EasyPingConfig<TDefs>): EasyPing<TDefs>;
202
268
 
203
- export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DEFAULT_TOKEN_TTL_SECONDS, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type HealthReport, Logger, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Recipient, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, SkipReason, type SweepResult, type TokenClaims, type Worker, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, expiresIn, signToken, verifyToken };
269
+ export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type HealthReport, type JsonBody, Logger, MAX_BODY_BYTES, MIN_SECRET_LENGTH, type MountedRoute, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Promisable, type RateLimitConfig, Recipient, RouteScope, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, SkipReason, type SweepResult, type Worker, createRateLimiter, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, escapeHtml, readJsonBody, redactErrorMessage };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-f0OR2DhY.js';
2
- export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-f0OR2DhY.js';
1
+ import { C as Channel, b as DeliveryMode, S as SkipReason, c as DeliveryRecord, D as DatabaseAdapter, R as Recipient } from './adapter-BfLe2J0t.js';
2
+ export { d as ClaimArgs, e as ClaimedDelivery, f as DeliveryOutcome, g as DeliveryRelease, a as DeliveryStatus, h as FeedPage, i as FeedQuery, F as Frequency, I as InsertDelivery, j as InsertNotification, N as NotificationRecord, P as PreferenceRecord } from './adapter-BfLe2J0t.js';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
- import { A as AnyPlugin, L as Logger } from './plugin-bTJKmnlq.js';
5
- export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, P as PluginHooks, j as PrepareContext, k as Promisable, R as ResolveChannelsContext, l as RouteContext, m as RouteDefinition, n as RouteScope, S as SchemaDeclaration, T as TableDeclaration, U as UnionToIntersection, V as ValidationError, o as consoleLogger, p as defineClientPlugin, q as definePlugin } from './plugin-bTJKmnlq.js';
4
+ import { R as RouteScope, A as AnyPlugin, P as Promisable, L as Logger } from './plugin-C9Hoxus2.js';
5
+ export { a as ActionsOf, b as AfterDeliverContext, c as AnyClientPlugin, B as BeforeSendContext, d as BeforeSendResult, C as ChannelDecision, e as ClientFetch, f as ClientPlugin, g as ConfigError, D as DEFAULT_TOKEN_TTL_SECONDS, E as EasyPingError, h as EasyPingPlugin, F as FieldDeclaration, i as FieldType, M as MAX_TOKEN_TTL_SECONDS, j as PluginHooks, k as PrepareContext, l as ResolveChannelsContext, m as RouteContext, n as RouteDefinition, S as SchemaDeclaration, o as SignRequest, T as TableDeclaration, p as TokenClaims, U as UnionToIntersection, V as ValidationError, q as consoleLogger, r as defineClientPlugin, s as definePlugin, t as expiresIn, u as signToken, v as verifyToken } from './plugin-C9Hoxus2.js';
6
6
  import { E as EmailProvider } from './provider-CA45WVqt.js';
7
7
  export { a as EmailMessage, P as ProviderSendResult } from './provider-CA45WVqt.js';
8
8
 
@@ -38,6 +38,25 @@ type PayloadOf<TDef> = TDef extends {
38
38
  schema: infer TSchema extends StandardSchemaV1;
39
39
  } ? StandardSchemaV1.InferOutput<TSchema> : Record<string, unknown>;
40
40
 
41
+ type RateLimitConfig = {
42
+ /** Requests allowed per key per window. */
43
+ max: number;
44
+ windowMs: number;
45
+ /**
46
+ * Which bucket a request lands in. Defaults to the client address as the
47
+ * usual proxy headers report it; returning null exempts the request. Behind
48
+ * no proxy those headers are absent, so supply your own key there.
49
+ */
50
+ key?: (request: Request) => string | null;
51
+ };
52
+ /**
53
+ * Fixed-window counter held in process memory. Per isolate on edge runtimes
54
+ * and per replica anywhere else, so it bounds one client's cost to one
55
+ * process rather than enforcing a global quota. That is enough to blunt
56
+ * secret guessing and poll storms, which is what it is for.
57
+ */
58
+ declare function createRateLimiter(config: RateLimitConfig): (request: Request) => Response | null;
59
+
41
60
  type SessionConfig = {
42
61
  /** null yields 401; throwing yields 500. Deliberately distinct. */
43
62
  getUserId: (request: Request) => string | null | Promise<string | null>;
@@ -77,10 +96,33 @@ type EasyPingConfig<TDefs extends NotificationDefinitions> = {
77
96
  /** Required by `cron` and `deferred` modes. See RFC 0002 §4. */
78
97
  cron?: {
79
98
  secret: string;
99
+ /** Sweeps one POST /cron may run before returning. Defaults to 50. */
100
+ maxSweeps?: number;
80
101
  };
102
+ /**
103
+ * Bearer secret for plugins' machine routes (`/push/prune`, `/digests/cron`).
104
+ * Falls back to `cron.secret`; set it when the scheduler that hits /cron
105
+ * should not also be able to prune devices or fire digests.
106
+ */
107
+ machineSecret?: string;
108
+ /**
109
+ * In-process fixed-window limiter, applied before routing. Keyed by client
110
+ * address from the usual proxy headers unless `key` says otherwise.
111
+ */
112
+ rateLimit?: RateLimitConfig;
81
113
  plugins?: readonly AnyPlugin[];
82
114
  /** Where the handler is mounted, used to strip the prefix off incoming URLs. */
83
115
  basePath?: string;
116
+ /**
117
+ * Origins allowed to POST besides the request's own host. Cross-origin
118
+ * POSTs are otherwise refused with 403, and every POST must be
119
+ * `application/json`. `"*.example.com"` matches subdomains.
120
+ */
121
+ trustedOrigins?: readonly string[];
122
+ /** Runs before routing. Return a Response to short-circuit — a rate limiter's 429, say. */
123
+ onRequest?: (request: Request) => Promisable<Response | undefined | void>;
124
+ /** Defaults to 64 KiB. */
125
+ maxBodyBytes?: number;
84
126
  /** Table-name prefix; must match the one given to the adapter. */
85
127
  tablePrefix?: string;
86
128
  logger?: Logger;
@@ -111,6 +153,13 @@ type SendResult = {
111
153
  type Worker = {
112
154
  stop: () => Promise<void>;
113
155
  };
156
+ type MountedRoute = {
157
+ method: "GET" | "POST";
158
+ path: string;
159
+ scope: RouteScope;
160
+ /** "core" for the built-in routes, otherwise the plugin id. */
161
+ owner: string;
162
+ };
114
163
  type HealthReport = {
115
164
  mode: DeliveryMode;
116
165
  cronMounted: boolean;
@@ -131,6 +180,8 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
131
180
  batchSize?: number;
132
181
  }): Worker;
133
182
  healthCheck(): Promise<HealthReport>;
183
+ /** Every mounted route with its auth scope, so `custom`-scoped ones stay visible. RFC 0002 §3. */
184
+ listRoutes(): readonly MountedRoute[];
134
185
  /**
135
186
  * Deliveries that exhausted their attempts, newest first.
136
187
  *
@@ -146,8 +197,6 @@ type EasyPing<TDefs extends NotificationDefinitions> = {
146
197
  }): Promise<readonly DeliveryRecord[]>;
147
198
  };
148
199
 
149
- declare function easyPing<TDefs extends NotificationDefinitions>(config: EasyPingConfig<TDefs>): EasyPing<TDefs>;
150
-
151
200
  type Backoff = "exponential" | ((attempt: number) => number);
152
201
 
153
202
  type RunnerDeps = {
@@ -165,7 +214,15 @@ type SweepResult = {
165
214
  claimed: number;
166
215
  sent: number;
167
216
  failed: number;
217
+ /** Deliveries that had nothing to do. Not errors; see DeliveryOutcome. */
218
+ skipped: number;
168
219
  };
220
+ /**
221
+ * What gets persisted to `last_error`. Provider responses quote the offending
222
+ * field, which for a bad recipient is the address itself; that column outlives
223
+ * the notification and is read by admin tooling, so addresses do not go in.
224
+ */
225
+ declare const redactErrorMessage: (text: string) => string;
169
226
  declare function createRunner(deps: RunnerDeps): {
170
227
  runOnce: (options?: {
171
228
  limit?: number;
@@ -184,20 +241,29 @@ declare function createRunner(deps: RunnerDeps): {
184
241
  };
185
242
  type Runner = ReturnType<typeof createRunner>;
186
243
 
187
- type TokenClaims = {
188
- /** Subject — the user the token acts for. */
189
- uid: string;
190
- /** What the token is allowed to do. Verification requires an exact match. */
191
- purpose: string;
192
- /** Expiry, epoch seconds. */
193
- exp: number;
194
- /** Free-form, purpose-specific. */
195
- data?: Record<string, string>;
196
- };
197
- declare function signToken(secret: string, claims: TokenClaims): Promise<string>;
198
- /** null for every failure mode. Distinguishing them tells an attacker which part to fix. */
199
- declare function verifyToken(secret: string, token: string, purpose: string, now?: Date): Promise<TokenClaims | null>;
200
- declare const DEFAULT_TOKEN_TTL_SECONDS: number;
201
- declare const expiresIn: (seconds: number, now?: Date) => number;
244
+ /** Every JSON body is capped here. Nothing legitimate the routes accept is bigger. */
245
+ declare const MAX_BODY_BYTES: number;
246
+ type JsonBody = {
247
+ body: Record<string, unknown>;
248
+ } | {
249
+ error: Response;
250
+ };
251
+ /**
252
+ * Reads a JSON object body with a byte cap enforced while streaming, so an
253
+ * oversized body is refused before it is buffered. Malformed JSON is 400; an
254
+ * empty body is `{}`. Plugin routes should use this rather than
255
+ * `request.json()`, which has no limit.
256
+ */
257
+ declare function readJsonBody(request: Request, maxBytes?: number): Promise<JsonBody>;
258
+
259
+ /**
260
+ * For email templates built with template literals. Anything in a payload
261
+ * that a user typed — a display name, a comment excerpt — goes through this
262
+ * or the recipient gets that user's HTML delivered from your sending domain.
263
+ */
264
+ declare const escapeHtml: (value: unknown) => string;
265
+
266
+ declare const MIN_SECRET_LENGTH = 16;
267
+ declare function easyPing<TDefs extends NotificationDefinitions>(config: EasyPingConfig<TDefs>): EasyPing<TDefs>;
202
268
 
203
- export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DEFAULT_TOKEN_TTL_SECONDS, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type HealthReport, Logger, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Recipient, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, SkipReason, type SweepResult, type TokenClaims, type Worker, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, expiresIn, signToken, verifyToken };
269
+ export { type AnyNotificationDefinition, AnyPlugin, Channel, type ChannelsConfig, DatabaseAdapter, type DeliveryConfig, DeliveryMode, DeliveryRecord, type EasyPing, type EasyPingConfig, EmailProvider, type EmailTemplate, type HealthReport, type JsonBody, Logger, MAX_BODY_BYTES, MIN_SECRET_LENGTH, type MountedRoute, type NotificationDefinition, type NotificationDefinitions, type PayloadFromSchema, type PayloadOf, Promisable, type RateLimitConfig, Recipient, RouteScope, type Runner, type RunnerDeps, type SendArgs, type SendResult, type SessionConfig, SkipReason, type SweepResult, type Worker, createRateLimiter, createRunner, decodeBase64Url, defineNotification, easyPing, encodeBase64Url, escapeHtml, readJsonBody, redactErrorMessage };