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.
- package/README.md +15 -4
- package/dist/{adapter-f0OR2DhY.d.cts → adapter-BfLe2J0t.d.cts} +10 -0
- package/dist/{adapter-f0OR2DhY.d.ts → adapter-BfLe2J0t.d.ts} +10 -0
- package/dist/adapters/drizzle.cjs +9 -4
- package/dist/adapters/drizzle.cjs.map +1 -1
- package/dist/adapters/drizzle.d.cts +35 -1
- package/dist/adapters/drizzle.d.ts +35 -1
- package/dist/adapters/drizzle.js +10 -5
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/mongodb.cjs +9 -4
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +2 -2
- package/dist/adapters/mongodb.d.ts +2 -2
- package/dist/adapters/mongodb.js +8 -4
- package/dist/adapters/mongodb.js.map +1 -1
- package/dist/adapters/postgres.cjs +136 -3
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +45 -2
- package/dist/adapters/postgres.d.ts +45 -2
- package/dist/adapters/postgres.js +38 -5
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/{chunk-OZSOIX23.js → chunk-3Y2MBUAW.js} +53 -12
- package/dist/chunk-3Y2MBUAW.js.map +1 -0
- package/dist/chunk-CYKAM7LE.js +226 -0
- package/dist/chunk-CYKAM7LE.js.map +1 -0
- package/dist/chunk-DG3ZL5NP.js +315 -0
- package/dist/chunk-DG3ZL5NP.js.map +1 -0
- package/dist/{chunk-NYDVNMKG.js → chunk-PSJHXZPR.js} +4 -3
- package/dist/chunk-PSJHXZPR.js.map +1 -0
- package/dist/{chunk-BV3U542P.js → chunk-W6I3B2CI.js} +2 -2
- package/dist/chunk-W6I3B2CI.js.map +1 -0
- package/dist/chunk-YEMRRMJQ.js +199 -0
- package/dist/chunk-YEMRRMJQ.js.map +1 -0
- package/dist/client.cjs +3 -5
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +3 -5
- package/dist/client.js.map +1 -1
- package/dist/index.cjs +377 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -22
- package/dist/index.d.ts +88 -22
- package/dist/index.js +119 -351
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +27 -6
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +8 -2
- package/dist/node.d.ts +8 -2
- package/dist/node.js +26 -6
- package/dist/node.js.map +1 -1
- package/dist/{plugin-Bqj9xAOS.d.cts → plugin-C9Hoxus2.d.ts} +38 -10
- package/dist/{plugin-bTJKmnlq.d.ts → plugin-CiMsGlQe.d.cts} +38 -10
- package/dist/plugins/digests.d.cts +2 -2
- package/dist/plugins/digests.d.ts +2 -2
- package/dist/plugins/preferences-client.cjs.map +1 -1
- package/dist/plugins/preferences-client.d.cts +2 -2
- package/dist/plugins/preferences-client.d.ts +2 -2
- package/dist/plugins/preferences-client.js +1 -1
- package/dist/plugins/preferences.cjs +100 -15
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +4 -2
- package/dist/plugins/preferences.d.ts +4 -2
- package/dist/plugins/preferences.js +35 -7
- package/dist/plugins/preferences.js.map +1 -1
- package/dist/plugins/push.cjs +222 -61
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.d.cts +81 -3
- package/dist/plugins/push.d.ts +81 -3
- package/dist/plugins/push.js +164 -62
- package/dist/plugins/push.js.map +1 -1
- package/dist/providers/web-push.cjs +37 -11
- package/dist/providers/web-push.cjs.map +1 -1
- package/dist/providers/web-push.d.cts +9 -3
- package/dist/providers/web-push.d.ts +9 -3
- package/dist/providers/web-push.js +37 -12
- package/dist/providers/web-push.js.map +1 -1
- package/dist/react.cjs +3 -5
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +3 -5
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +2 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +7 -2
- package/dist/schema.d.ts +7 -2
- package/dist/schema.js +2 -198
- package/dist/schema.js.map +1 -1
- package/dist/testing.cjs +29 -0
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +29 -0
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-BV3U542P.js.map +0 -1
- package/dist/chunk-NYDVNMKG.js.map +0 -1
- package/dist/chunk-OZSOIX23.js.map +0 -1
- package/dist/chunk-W3BMEPPQ.js +0 -50
- package/dist/chunk-W3BMEPPQ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -53,18 +53,27 @@ var consoleLogger = {
|
|
|
53
53
|
|
|
54
54
|
// src/core/tokens.ts
|
|
55
55
|
var encoder = new TextEncoder();
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
var KEY_SALT = encoder.encode("easy-ping/tokens/v1");
|
|
57
|
+
async function hmacKey(secret, purpose) {
|
|
58
|
+
const master = await crypto.subtle.importKey("raw", encoder.encode(secret), "HKDF", false, [
|
|
59
|
+
"deriveKey"
|
|
60
|
+
]);
|
|
61
|
+
return crypto.subtle.deriveKey(
|
|
62
|
+
{ name: "HKDF", hash: "SHA-256", salt: KEY_SALT, info: encoder.encode(purpose) },
|
|
63
|
+
master,
|
|
64
|
+
{ name: "HMAC", hash: "SHA-256", length: 256 },
|
|
61
65
|
false,
|
|
62
66
|
["sign", "verify"]
|
|
63
67
|
);
|
|
64
68
|
}
|
|
65
|
-
async function signToken(secret, claims) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
69
|
+
async function signToken(secret, claims, now = /* @__PURE__ */ new Date()) {
|
|
70
|
+
const stamped = { ...claims, iat: claims.iat ?? Math.floor(now.getTime() / 1e3) };
|
|
71
|
+
const body = encodeBase64Url(JSON.stringify(stamped));
|
|
72
|
+
const signature = await crypto.subtle.sign(
|
|
73
|
+
"HMAC",
|
|
74
|
+
await hmacKey(secret, claims.purpose),
|
|
75
|
+
encoder.encode(body)
|
|
76
|
+
);
|
|
68
77
|
return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;
|
|
69
78
|
}
|
|
70
79
|
async function verifyToken(secret, token, purpose, now = /* @__PURE__ */ new Date()) {
|
|
@@ -75,7 +84,7 @@ async function verifyToken(secret, token, purpose, now = /* @__PURE__ */ new Dat
|
|
|
75
84
|
if (!signature) return null;
|
|
76
85
|
const valid = await crypto.subtle.verify(
|
|
77
86
|
"HMAC",
|
|
78
|
-
await hmacKey(secret),
|
|
87
|
+
await hmacKey(secret, purpose),
|
|
79
88
|
signature,
|
|
80
89
|
encoder.encode(body)
|
|
81
90
|
);
|
|
@@ -91,43 +100,138 @@ async function verifyToken(secret, token, purpose, now = /* @__PURE__ */ new Dat
|
|
|
91
100
|
if (typeof claims?.uid !== "string" || typeof claims.exp !== "number") return null;
|
|
92
101
|
if (claims.purpose !== purpose) return null;
|
|
93
102
|
if (claims.exp * 1e3 <= now.getTime()) return null;
|
|
103
|
+
if (claims.iat !== void 0 && typeof claims.iat !== "number") return null;
|
|
104
|
+
if (claims.data !== void 0) {
|
|
105
|
+
if (typeof claims.data !== "object" || claims.data === null || Array.isArray(claims.data)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
for (const value of Object.values(claims.data)) {
|
|
109
|
+
if (typeof value !== "string") return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
94
112
|
return claims;
|
|
95
113
|
}
|
|
96
114
|
var DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
97
|
-
var
|
|
115
|
+
var MAX_TOKEN_TTL_SECONDS = 90 * 24 * 60 * 60;
|
|
116
|
+
var expiresIn = (seconds, now = /* @__PURE__ */ new Date()) => Math.floor(now.getTime() / 1e3) + Math.min(Math.max(seconds, 0), MAX_TOKEN_TTL_SECONDS);
|
|
117
|
+
function createScopedSigner(secret, pluginId, purposes, onViolation) {
|
|
118
|
+
return (request) => {
|
|
119
|
+
if (!purposes.has(request.purpose)) {
|
|
120
|
+
throw onViolation(
|
|
121
|
+
`plugin "${pluginId}" tried to sign a token for purpose "${request.purpose}", which none of its signed routes declare.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return signToken(secret, {
|
|
125
|
+
uid: request.uid,
|
|
126
|
+
purpose: request.purpose,
|
|
127
|
+
exp: expiresIn(request.ttlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS),
|
|
128
|
+
...request.data ? { data: request.data } : {}
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
}
|
|
98
132
|
|
|
99
133
|
// src/core/handler.ts
|
|
134
|
+
var MAX_BODY_BYTES = 64 * 1024;
|
|
135
|
+
var MAX_READ_IDS = 200;
|
|
136
|
+
var PRIVATE_HEADERS = {
|
|
137
|
+
"cache-control": "private, no-store",
|
|
138
|
+
"x-content-type-options": "nosniff"
|
|
139
|
+
};
|
|
100
140
|
var json = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
101
141
|
status,
|
|
102
|
-
headers: { "content-type": "application/json" }
|
|
142
|
+
headers: { "content-type": "application/json", ...PRIVATE_HEADERS }
|
|
103
143
|
});
|
|
104
|
-
var empty = (status) => new Response(null, { status });
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const right = encoder2.encode(b);
|
|
110
|
-
let diff = left.length ^ right.length;
|
|
111
|
-
const length = Math.max(left.length, right.length);
|
|
112
|
-
for (let i = 0; i < length; i += 1) {
|
|
113
|
-
diff |= (left[i] ?? 0) ^ (right[i] ?? 0);
|
|
144
|
+
var empty = (status) => new Response(null, { status, headers: PRIVATE_HEADERS });
|
|
145
|
+
function harden(response) {
|
|
146
|
+
const headers = new Headers(response.headers);
|
|
147
|
+
for (const [name, value] of Object.entries(PRIVATE_HEADERS)) {
|
|
148
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
114
149
|
}
|
|
150
|
+
return new Response(response.body, {
|
|
151
|
+
status: response.status,
|
|
152
|
+
statusText: response.statusText,
|
|
153
|
+
headers
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
var encoder2 = new TextEncoder();
|
|
157
|
+
async function timingSafeEqual(a, b) {
|
|
158
|
+
const [left, right] = await Promise.all(
|
|
159
|
+
[a, b].map(
|
|
160
|
+
async (value) => new Uint8Array(await crypto.subtle.digest("SHA-256", encoder2.encode(value)))
|
|
161
|
+
)
|
|
162
|
+
);
|
|
163
|
+
let diff = 0;
|
|
164
|
+
for (let i = 0; i < 32; i += 1) diff |= (left?.[i] ?? 0) ^ (right?.[i] ?? 0);
|
|
115
165
|
return diff === 0;
|
|
116
166
|
}
|
|
117
167
|
function routePath(url, basePath) {
|
|
118
|
-
const
|
|
119
|
-
if (
|
|
120
|
-
return
|
|
168
|
+
const { pathname } = url;
|
|
169
|
+
if (pathname === basePath) return "/";
|
|
170
|
+
if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length);
|
|
171
|
+
return pathname;
|
|
121
172
|
}
|
|
122
|
-
async function
|
|
173
|
+
async function readJsonBody(request, maxBytes = MAX_BODY_BYTES) {
|
|
174
|
+
const tooLarge = () => ({ error: json({ error: `body exceeds ${maxBytes} bytes` }, 413) });
|
|
175
|
+
const declared = Number(request.headers.get("content-length"));
|
|
176
|
+
if (Number.isFinite(declared) && declared > maxBytes) return tooLarge();
|
|
177
|
+
if (!request.body) return { body: {} };
|
|
178
|
+
const reader = request.body.getReader();
|
|
179
|
+
const chunks = [];
|
|
180
|
+
let total = 0;
|
|
181
|
+
for (; ; ) {
|
|
182
|
+
const { done, value } = await reader.read();
|
|
183
|
+
if (done) break;
|
|
184
|
+
total += value.byteLength;
|
|
185
|
+
if (total > maxBytes) {
|
|
186
|
+
await reader.cancel();
|
|
187
|
+
return tooLarge();
|
|
188
|
+
}
|
|
189
|
+
chunks.push(value);
|
|
190
|
+
}
|
|
191
|
+
const bytes = new Uint8Array(total);
|
|
192
|
+
let offset = 0;
|
|
193
|
+
for (const chunk of chunks) {
|
|
194
|
+
bytes.set(chunk, offset);
|
|
195
|
+
offset += chunk.byteLength;
|
|
196
|
+
}
|
|
197
|
+
const text = new TextDecoder().decode(bytes);
|
|
198
|
+
if (text.trim() === "") return { body: {} };
|
|
123
199
|
try {
|
|
124
|
-
const
|
|
125
|
-
return
|
|
200
|
+
const parsed = JSON.parse(text);
|
|
201
|
+
return {
|
|
202
|
+
body: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
|
|
203
|
+
};
|
|
204
|
+
} catch {
|
|
205
|
+
return { error: json({ error: "body must be a JSON object" }, 400) };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
var isJsonContentType = (request) => /^application\/json\s*(;|$)/i.test((request.headers.get("content-type") ?? "").trim());
|
|
209
|
+
function originAllowed(origin, url, request, trusted) {
|
|
210
|
+
let host;
|
|
211
|
+
try {
|
|
212
|
+
host = new URL(origin).host;
|
|
126
213
|
} catch {
|
|
127
|
-
return
|
|
214
|
+
return false;
|
|
128
215
|
}
|
|
216
|
+
if (host === url.host) return true;
|
|
217
|
+
const forwarded = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
218
|
+
if (forwarded && host === forwarded) return true;
|
|
219
|
+
return trusted.some(
|
|
220
|
+
(entry) => entry.startsWith("*.") ? host.endsWith(entry.slice(1)) : entry === origin
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function csrfCheck(request, url, trusted) {
|
|
224
|
+
if (!isJsonContentType(request)) {
|
|
225
|
+
return json({ error: "content-type must be application/json" }, 415);
|
|
226
|
+
}
|
|
227
|
+
const origin = request.headers.get("origin");
|
|
228
|
+
if (origin === null) return null;
|
|
229
|
+
if (origin === "null" || !originAllowed(origin, url, request, trusted)) return empty(403);
|
|
230
|
+
return null;
|
|
129
231
|
}
|
|
130
232
|
function createHandler(deps) {
|
|
233
|
+
const trustedOrigins = deps.trustedOrigins ?? [];
|
|
234
|
+
const maxBodyBytes = deps.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
131
235
|
async function resolveUserId(request) {
|
|
132
236
|
try {
|
|
133
237
|
return await deps.session.getUserId(request);
|
|
@@ -136,47 +240,67 @@ function createHandler(deps) {
|
|
|
136
240
|
return "error";
|
|
137
241
|
}
|
|
138
242
|
}
|
|
139
|
-
async function
|
|
140
|
-
if (!
|
|
243
|
+
async function bearerMatches(request, secret) {
|
|
244
|
+
if (!secret) return false;
|
|
141
245
|
const header = request.headers.get("authorization") ?? "";
|
|
142
246
|
const presented = header.startsWith("Bearer ") ? header.slice(7) : header;
|
|
143
|
-
|
|
144
|
-
|
|
247
|
+
return timingSafeEqual(presented, secret);
|
|
248
|
+
}
|
|
249
|
+
async function handleCron(request) {
|
|
250
|
+
if (!deps.cronSecret) return empty(404);
|
|
251
|
+
if (!await bearerMatches(request, deps.cronSecret)) return empty(401);
|
|
252
|
+
const result = await deps.runner.drain(
|
|
253
|
+
deps.cronMaxSweeps === void 0 ? {} : { maxSweeps: deps.cronMaxSweeps }
|
|
254
|
+
);
|
|
145
255
|
return json(result);
|
|
146
256
|
}
|
|
147
|
-
async function handlePluginRoute(
|
|
148
|
-
if (
|
|
257
|
+
async function handlePluginRoute(route2, request, url) {
|
|
258
|
+
if (route2.scope.type === "user") {
|
|
259
|
+
if (request.method === "POST") {
|
|
260
|
+
const rejected = csrfCheck(request, url, trustedOrigins);
|
|
261
|
+
if (rejected) return rejected;
|
|
262
|
+
}
|
|
149
263
|
const userId = await resolveUserId(request);
|
|
150
264
|
if (userId === "error") return empty(500);
|
|
151
265
|
if (userId === null) return empty(401);
|
|
152
|
-
return
|
|
266
|
+
return harden(await route2.handler({ request, userId, claims: null, params: {} }));
|
|
153
267
|
}
|
|
154
|
-
if (
|
|
155
|
-
if (!deps.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!timingSafeEqual(presented, deps.cronSecret)) return empty(401);
|
|
159
|
-
return route.handler({ request, userId: null, claims: null, params: {} });
|
|
268
|
+
if (route2.scope.type === "machine") {
|
|
269
|
+
if (!deps.machineSecret) return empty(404);
|
|
270
|
+
if (!await bearerMatches(request, deps.machineSecret)) return empty(401);
|
|
271
|
+
return harden(await route2.handler({ request, userId: null, claims: null, params: {} }));
|
|
160
272
|
}
|
|
161
|
-
if (
|
|
273
|
+
if (route2.scope.type === "signed") {
|
|
162
274
|
const token = url.searchParams.get("token") ?? (request.headers.get("authorization") ?? "").replace(/^Bearer /, "");
|
|
163
|
-
const claims = await verifyToken(deps.secret, token,
|
|
275
|
+
const claims = await verifyToken(deps.secret, token, route2.scope.purpose);
|
|
164
276
|
if (!claims) return empty(400);
|
|
165
|
-
return
|
|
277
|
+
return harden(await route2.handler({ request, userId: claims.uid, claims, params: {} }));
|
|
166
278
|
}
|
|
167
|
-
return
|
|
279
|
+
return harden(await route2.handler({ request, userId: null, claims: null, params: {} }));
|
|
168
280
|
}
|
|
169
|
-
async function
|
|
281
|
+
async function route(request) {
|
|
170
282
|
const url = new URL(request.url);
|
|
171
283
|
const path = routePath(url, deps.basePath);
|
|
284
|
+
if (deps.onRequest) {
|
|
285
|
+
const early = await deps.onRequest(request);
|
|
286
|
+
if (early instanceof Response) return early;
|
|
287
|
+
}
|
|
288
|
+
if (deps.rateLimit) {
|
|
289
|
+
const limited = deps.rateLimit(request);
|
|
290
|
+
if (limited) return limited;
|
|
291
|
+
}
|
|
172
292
|
if (path === "/cron") {
|
|
173
293
|
if (request.method !== "POST") return empty(405);
|
|
174
294
|
return handleCron(request);
|
|
175
295
|
}
|
|
176
296
|
const pluginRoute = deps.pluginRoutes?.find(
|
|
177
|
-
(
|
|
297
|
+
(candidate) => candidate.path === path && candidate.method === request.method
|
|
178
298
|
);
|
|
179
299
|
if (pluginRoute) return handlePluginRoute(pluginRoute, request, url);
|
|
300
|
+
if (request.method === "POST") {
|
|
301
|
+
const rejected = csrfCheck(request, url, trustedOrigins);
|
|
302
|
+
if (rejected) return rejected;
|
|
303
|
+
}
|
|
180
304
|
const userId = await resolveUserId(request);
|
|
181
305
|
if (userId === "error") return empty(500);
|
|
182
306
|
if (userId === null) return empty(401);
|
|
@@ -184,27 +308,32 @@ function createHandler(deps) {
|
|
|
184
308
|
const limitParam = Number(url.searchParams.get("limit") ?? 20);
|
|
185
309
|
const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;
|
|
186
310
|
const cursor = url.searchParams.get("cursor");
|
|
187
|
-
const page = await
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
311
|
+
const [page, unseenCount] = await Promise.all([
|
|
312
|
+
deps.adapter.listNotifications({
|
|
313
|
+
userId,
|
|
314
|
+
limit,
|
|
315
|
+
...cursor ? { cursor } : {},
|
|
316
|
+
...url.searchParams.get("unreadOnly") === "true" ? { unreadOnly: true } : {}
|
|
317
|
+
}),
|
|
318
|
+
cursor ? Promise.resolve(null) : deps.adapter.countUnseen(userId)
|
|
319
|
+
]);
|
|
320
|
+
return json(unseenCount === null ? page : { ...page, unseenCount });
|
|
194
321
|
}
|
|
195
322
|
if (request.method === "GET" && path === "/count") {
|
|
196
323
|
return json({ unseen: await deps.adapter.countUnseen(userId) });
|
|
197
324
|
}
|
|
198
325
|
if (request.method === "POST" && path === "/seen") {
|
|
199
|
-
const
|
|
200
|
-
|
|
326
|
+
const parsed = await readJsonBody(request, maxBodyBytes);
|
|
327
|
+
if ("error" in parsed) return parsed.error;
|
|
328
|
+
const before = typeof parsed.body.before === "string" ? new Date(parsed.body.before) : null;
|
|
201
329
|
const cutoff = before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : /* @__PURE__ */ new Date();
|
|
202
330
|
await deps.adapter.markSeen(userId, cutoff);
|
|
203
331
|
return json({ ok: true });
|
|
204
332
|
}
|
|
205
333
|
if (request.method === "POST" && path === "/read") {
|
|
206
|
-
const
|
|
207
|
-
|
|
334
|
+
const parsed = await readJsonBody(request, maxBodyBytes);
|
|
335
|
+
if ("error" in parsed) return parsed.error;
|
|
336
|
+
const ids = Array.isArray(parsed.body.ids) ? parsed.body.ids.filter((id) => typeof id === "string") : [];
|
|
208
337
|
if (ids.length === 0) return json({ error: "ids must be a non-empty string array" }, 400);
|
|
209
338
|
if (ids.length > MAX_READ_IDS) {
|
|
210
339
|
return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);
|
|
@@ -217,6 +346,14 @@ function createHandler(deps) {
|
|
|
217
346
|
}
|
|
218
347
|
return empty(404);
|
|
219
348
|
}
|
|
349
|
+
async function handle(request) {
|
|
350
|
+
try {
|
|
351
|
+
return await route(request);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
deps.logger.error("request failed", { url: request.url, error });
|
|
354
|
+
return empty(500);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
220
357
|
return {
|
|
221
358
|
handle,
|
|
222
359
|
GET: handle,
|
|
@@ -224,6 +361,50 @@ function createHandler(deps) {
|
|
|
224
361
|
};
|
|
225
362
|
}
|
|
226
363
|
|
|
364
|
+
// src/core/html.ts
|
|
365
|
+
var ESCAPES = {
|
|
366
|
+
"&": "&",
|
|
367
|
+
"<": "<",
|
|
368
|
+
">": ">",
|
|
369
|
+
'"': """,
|
|
370
|
+
"'": "'"
|
|
371
|
+
};
|
|
372
|
+
var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
|
|
373
|
+
|
|
374
|
+
// src/core/rate-limit.ts
|
|
375
|
+
var clientAddress = (request) => {
|
|
376
|
+
const headers = request.headers;
|
|
377
|
+
return headers.get("cf-connecting-ip") ?? headers.get("x-real-ip") ?? headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? null;
|
|
378
|
+
};
|
|
379
|
+
function createRateLimiter(config) {
|
|
380
|
+
const keyOf = config.key ?? clientAddress;
|
|
381
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
382
|
+
let lastSweep = 0;
|
|
383
|
+
return (request) => {
|
|
384
|
+
const key = keyOf(request);
|
|
385
|
+
if (key === null) return null;
|
|
386
|
+
const now = Date.now();
|
|
387
|
+
if (now - lastSweep > config.windowMs) {
|
|
388
|
+
for (const [id, bucket2] of buckets) if (bucket2.resetAt <= now) buckets.delete(id);
|
|
389
|
+
lastSweep = now;
|
|
390
|
+
}
|
|
391
|
+
let bucket = buckets.get(key);
|
|
392
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
393
|
+
bucket = { count: 0, resetAt: now + config.windowMs };
|
|
394
|
+
buckets.set(key, bucket);
|
|
395
|
+
}
|
|
396
|
+
bucket.count += 1;
|
|
397
|
+
if (bucket.count <= config.max) return null;
|
|
398
|
+
return new Response(null, {
|
|
399
|
+
status: 429,
|
|
400
|
+
headers: {
|
|
401
|
+
"retry-after": String(Math.max(1, Math.ceil((bucket.resetAt - now) / 1e3))),
|
|
402
|
+
"cache-control": "private, no-store"
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
227
408
|
// src/core/backoff.ts
|
|
228
409
|
var SCHEDULE_MS = [3e4, 12e4, 48e4, 192e4];
|
|
229
410
|
function backoffMs(attempt, strategy = "exponential") {
|
|
@@ -251,7 +432,8 @@ async function withTimeout(run, ms, label) {
|
|
|
251
432
|
clearTimeout(timer);
|
|
252
433
|
}
|
|
253
434
|
}
|
|
254
|
-
var
|
|
435
|
+
var redactErrorMessage = (text) => text.replace(/[^\s"'<>()[\]{},;]+@[^\s"'<>()[\]{},;]+/g, "[redacted-email]");
|
|
436
|
+
var message = (error) => redactErrorMessage(error instanceof Error ? error.message : String(error));
|
|
255
437
|
function createRunner(deps) {
|
|
256
438
|
async function deliverOne(delivery, recipient) {
|
|
257
439
|
if (!recipient) {
|
|
@@ -337,7 +519,7 @@ function createRunner(deps) {
|
|
|
337
519
|
}
|
|
338
520
|
}
|
|
339
521
|
async function notifyAfterDeliver(delivery, outcome) {
|
|
340
|
-
const isTerminal = outcome.result
|
|
522
|
+
const isTerminal = outcome.result !== "failed" || !outcome.retryable || delivery.attempts + 1 >= delivery.maxAttempts;
|
|
341
523
|
for (const plugin of deps.plugins) {
|
|
342
524
|
const hook = plugin.hooks?.afterDeliver;
|
|
343
525
|
if (!hook) continue;
|
|
@@ -368,16 +550,18 @@ function createRunner(deps) {
|
|
|
368
550
|
...options.channels ? { channels: options.channels } : {},
|
|
369
551
|
...options.ids ? { ids: options.ids } : {}
|
|
370
552
|
});
|
|
371
|
-
if (claimed.length === 0) return { claimed: 0, sent: 0, failed: 0 };
|
|
553
|
+
if (claimed.length === 0) return { claimed: 0, sent: 0, failed: 0, skipped: 0 };
|
|
372
554
|
const userIds = [...new Set(claimed.map((row) => row.notification.userId))];
|
|
373
555
|
const recipients = await deps.getRecipients(userIds);
|
|
374
556
|
const byUserId = new Map(recipients.map((recipient) => [recipient.userId, recipient]));
|
|
375
557
|
const releases = [];
|
|
376
558
|
let sent = 0;
|
|
377
559
|
let failed = 0;
|
|
560
|
+
let skipped = 0;
|
|
378
561
|
for (const delivery of claimed) {
|
|
379
562
|
const outcome = await deliverOne(delivery, byUserId.get(delivery.notification.userId));
|
|
380
563
|
if (outcome.result === "sent") sent += 1;
|
|
564
|
+
else if (outcome.result === "skipped") skipped += 1;
|
|
381
565
|
else failed += 1;
|
|
382
566
|
releases.push({
|
|
383
567
|
id: delivery.id,
|
|
@@ -388,16 +572,17 @@ function createRunner(deps) {
|
|
|
388
572
|
await notifyAfterDeliver(delivery, outcome);
|
|
389
573
|
}
|
|
390
574
|
await deps.adapter.releaseDeliveries(releases);
|
|
391
|
-
return { claimed: claimed.length, sent, failed };
|
|
575
|
+
return { claimed: claimed.length, sent, failed, skipped };
|
|
392
576
|
}
|
|
393
577
|
async function drain(options = {}) {
|
|
394
578
|
const maxSweeps = options.maxSweeps ?? 50;
|
|
395
|
-
const total = { claimed: 0, sent: 0, failed: 0 };
|
|
579
|
+
const total = { claimed: 0, sent: 0, failed: 0, skipped: 0 };
|
|
396
580
|
for (let sweep = 0; sweep < maxSweeps; sweep += 1) {
|
|
397
581
|
const result = await runOnce(options.ids ? { ids: options.ids } : {});
|
|
398
582
|
total.claimed += result.claimed;
|
|
399
583
|
total.sent += result.sent;
|
|
400
584
|
total.failed += result.failed;
|
|
585
|
+
total.skipped += result.skipped;
|
|
401
586
|
if (result.claimed === 0) break;
|
|
402
587
|
}
|
|
403
588
|
return total;
|
|
@@ -637,6 +822,33 @@ async function runSend(type, input, deps) {
|
|
|
637
822
|
var toSnakeCase = (value) => value.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
638
823
|
|
|
639
824
|
// src/core/store.ts
|
|
825
|
+
var OPERATORS = ["in", "lt", "lte", "gt", "gte", "not"];
|
|
826
|
+
var isOperator = (value) => typeof value === "object" && value !== null && !(value instanceof Date) && OPERATORS.some((op) => Object.hasOwn(value, op));
|
|
827
|
+
var isScalar = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
|
|
828
|
+
function assertCondition(pluginId, field, condition) {
|
|
829
|
+
if (isScalar(condition)) return;
|
|
830
|
+
if (isOperator(condition)) {
|
|
831
|
+
const operator = condition;
|
|
832
|
+
const keys = Object.keys(operator);
|
|
833
|
+
const [op] = keys;
|
|
834
|
+
if (keys.length !== 1 || op === void 0) {
|
|
835
|
+
throw new ConfigError(`plugin "${pluginId}" gave field "${field}" more than one operator.`);
|
|
836
|
+
}
|
|
837
|
+
const payload = operator[op];
|
|
838
|
+
const valid = op === "in" ? Array.isArray(payload) && payload.every((item) => typeof item === "string" || typeof item === "number") : isScalar(payload);
|
|
839
|
+
if (valid) return;
|
|
840
|
+
}
|
|
841
|
+
throw new ConfigError(
|
|
842
|
+
`plugin "${pluginId}" passed a non-scalar value for field "${field}"; validate request bodies before querying with them.`
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
function assertValue(pluginId, field, spec, value) {
|
|
846
|
+
if (value === void 0 || isScalar(value)) return;
|
|
847
|
+
if (spec.type === "json" && typeof value === "object") return;
|
|
848
|
+
throw new ConfigError(
|
|
849
|
+
`plugin "${pluginId}" passed a non-scalar value for ${spec.type} field "${field}".`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
640
852
|
function createPluginStore(pluginId, schema, storage, prefix) {
|
|
641
853
|
const byTableName = /* @__PURE__ */ new Map();
|
|
642
854
|
for (const declaration of Object.values(schema ?? {})) {
|
|
@@ -653,7 +865,7 @@ function createPluginStore(pluginId, schema, storage, prefix) {
|
|
|
653
865
|
}
|
|
654
866
|
function checkFields(table, fields, context) {
|
|
655
867
|
for (const field of fields) {
|
|
656
|
-
const spec = table.fields[field];
|
|
868
|
+
const spec = Object.hasOwn(table.fields, field) ? table.fields[field] : void 0;
|
|
657
869
|
if (!spec) {
|
|
658
870
|
throw new ConfigError(
|
|
659
871
|
`plugin "${pluginId}" used unknown field "${field}" on "${table.tableName}" (${context}).`
|
|
@@ -666,6 +878,19 @@ function createPluginStore(pluginId, schema, storage, prefix) {
|
|
|
666
878
|
}
|
|
667
879
|
}
|
|
668
880
|
}
|
|
881
|
+
function checkWhere(table, where) {
|
|
882
|
+
checkFields(table, Object.keys(where), "where");
|
|
883
|
+
for (const [field, condition] of Object.entries(where)) {
|
|
884
|
+
assertCondition(pluginId, field, condition);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
function checkRow(table, row, context) {
|
|
888
|
+
checkFields(table, Object.keys(row), context);
|
|
889
|
+
for (const [field, value] of Object.entries(row)) {
|
|
890
|
+
const spec = table.fields[field];
|
|
891
|
+
if (spec) assertValue(pluginId, field, spec, value);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
669
894
|
const qualified = (table) => prefix + table.tableName;
|
|
670
895
|
const column = (field) => storage.naming === "preserve" ? field : toSnakeCase(field);
|
|
671
896
|
const serialize = (table, row) => {
|
|
@@ -679,21 +904,24 @@ function createPluginStore(pluginId, schema, storage, prefix) {
|
|
|
679
904
|
return {
|
|
680
905
|
async find(table, where = {}, options = {}) {
|
|
681
906
|
const declaration = resolve(table);
|
|
682
|
-
|
|
907
|
+
checkWhere(declaration, where);
|
|
683
908
|
if (options.orderBy) checkFields(declaration, [options.orderBy.field], "orderBy");
|
|
684
909
|
const rows = await storage.queryTable(qualified(declaration), where, options);
|
|
685
910
|
return rows.map((row) => {
|
|
686
911
|
const mapped = {};
|
|
687
912
|
for (const field of Object.keys(declaration.fields)) {
|
|
688
913
|
const value = row[column(field)];
|
|
689
|
-
|
|
914
|
+
const type = declaration.fields[field]?.type;
|
|
915
|
+
if (type === "json" && typeof value === "string") mapped[field] = JSON.parse(value);
|
|
916
|
+
else if (type === "date" && typeof value === "string") mapped[field] = new Date(value);
|
|
917
|
+
else mapped[field] = value;
|
|
690
918
|
}
|
|
691
919
|
return mapped;
|
|
692
920
|
});
|
|
693
921
|
},
|
|
694
922
|
async insert(table, rows) {
|
|
695
923
|
const declaration = resolve(table);
|
|
696
|
-
for (const row of rows)
|
|
924
|
+
for (const row of rows) checkRow(declaration, row, "insert");
|
|
697
925
|
return storage.insertRows(
|
|
698
926
|
qualified(declaration),
|
|
699
927
|
rows.map((row) => serialize(declaration, row))
|
|
@@ -701,7 +929,7 @@ function createPluginStore(pluginId, schema, storage, prefix) {
|
|
|
701
929
|
},
|
|
702
930
|
async upsert(table, rows, options) {
|
|
703
931
|
const declaration = resolve(table);
|
|
704
|
-
for (const row of rows)
|
|
932
|
+
for (const row of rows) checkRow(declaration, row, "upsert");
|
|
705
933
|
checkFields(declaration, options.onConflict, "onConflict");
|
|
706
934
|
return storage.insertRows(
|
|
707
935
|
qualified(declaration),
|
|
@@ -711,19 +939,27 @@ function createPluginStore(pluginId, schema, storage, prefix) {
|
|
|
711
939
|
},
|
|
712
940
|
async update(table, where, set) {
|
|
713
941
|
const declaration = resolve(table);
|
|
714
|
-
|
|
715
|
-
|
|
942
|
+
checkWhere(declaration, where);
|
|
943
|
+
checkRow(declaration, set, "update");
|
|
716
944
|
return storage.updateRows(qualified(declaration), where, serialize(declaration, set));
|
|
717
945
|
},
|
|
718
946
|
async remove(table, where) {
|
|
719
947
|
const declaration = resolve(table);
|
|
720
|
-
|
|
948
|
+
checkWhere(declaration, where);
|
|
721
949
|
return storage.deleteRows(qualified(declaration), where);
|
|
722
950
|
}
|
|
723
951
|
};
|
|
724
952
|
}
|
|
725
953
|
|
|
726
954
|
// src/core/instance.ts
|
|
955
|
+
var CORE_ROUTES = [
|
|
956
|
+
{ method: "GET", path: "/", scope: { type: "user" }, owner: "core" },
|
|
957
|
+
{ method: "GET", path: "/count", scope: { type: "user" }, owner: "core" },
|
|
958
|
+
{ method: "POST", path: "/seen", scope: { type: "user" }, owner: "core" },
|
|
959
|
+
{ method: "POST", path: "/read", scope: { type: "user" }, owner: "core" },
|
|
960
|
+
{ method: "POST", path: "/read-all", scope: { type: "user" }, owner: "core" },
|
|
961
|
+
{ method: "POST", path: "/cron", scope: { type: "machine" }, owner: "core" }
|
|
962
|
+
];
|
|
727
963
|
var DEFAULTS = {
|
|
728
964
|
maxAttempts: 5,
|
|
729
965
|
leaseMs: 6e4,
|
|
@@ -731,6 +967,24 @@ var DEFAULTS = {
|
|
|
731
967
|
basePath: "/api/notifications",
|
|
732
968
|
mode: "cron"
|
|
733
969
|
};
|
|
970
|
+
var MIN_SECRET_LENGTH = 16;
|
|
971
|
+
var PLACEHOLDER_SECRETS = /* @__PURE__ */ new Set([
|
|
972
|
+
"secret",
|
|
973
|
+
"changeme",
|
|
974
|
+
"change-me",
|
|
975
|
+
"replace-me",
|
|
976
|
+
"replace-me-too",
|
|
977
|
+
"password",
|
|
978
|
+
"demo-signing-secret",
|
|
979
|
+
"demo-cron-secret"
|
|
980
|
+
]);
|
|
981
|
+
function checkSecretStrength(name, value) {
|
|
982
|
+
if (value.length < MIN_SECRET_LENGTH || PLACEHOLDER_SECRETS.has(value.toLowerCase())) {
|
|
983
|
+
throw new ConfigError(
|
|
984
|
+
`\`${name}\` must be at least ${MIN_SECRET_LENGTH} characters and not a placeholder. Generate one with \`openssl rand -base64 32\`.`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
734
988
|
function validate(config, plugins) {
|
|
735
989
|
if (!config.database) throw new ConfigError("`database` is required.");
|
|
736
990
|
if (!config.notifications) throw new ConfigError("`notifications` is required.");
|
|
@@ -744,6 +998,12 @@ function validate(config, plugins) {
|
|
|
744
998
|
"`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."
|
|
745
999
|
);
|
|
746
1000
|
}
|
|
1001
|
+
checkSecretStrength("secret", config.secret);
|
|
1002
|
+
if (config.cron?.secret) checkSecretStrength("cron.secret", config.cron.secret);
|
|
1003
|
+
if (config.machineSecret) checkSecretStrength("machineSecret", config.machineSecret);
|
|
1004
|
+
if (config.rateLimit && (config.rateLimit.max < 1 || config.rateLimit.windowMs < 1)) {
|
|
1005
|
+
throw new ConfigError("`rateLimit.max` and `rateLimit.windowMs` must both be positive.");
|
|
1006
|
+
}
|
|
747
1007
|
if (typeof config.getRecipients !== "function") {
|
|
748
1008
|
throw new ConfigError("`getRecipients` is required to resolve emails and timezones.");
|
|
749
1009
|
}
|
|
@@ -812,6 +1072,15 @@ function easyPing(config) {
|
|
|
812
1072
|
"delivery.mode is 'deferred' but no delivery.waitUntil was supplied; delivery will wait for the cron sweep."
|
|
813
1073
|
);
|
|
814
1074
|
}
|
|
1075
|
+
for (const plugin of plugins) {
|
|
1076
|
+
for (const route of plugin.routes ?? []) {
|
|
1077
|
+
if (route.scope.type === "custom") {
|
|
1078
|
+
warnings.push(
|
|
1079
|
+
`plugin "${plugin.id}" mounts ${route.method} ${route.path} with custom auth: ` + route.scope.justification
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
815
1084
|
for (const warning of warnings) logger.warn(warning);
|
|
816
1085
|
const runner = createRunner({
|
|
817
1086
|
adapter: config.database,
|
|
@@ -833,7 +1102,13 @@ function easyPing(config) {
|
|
|
833
1102
|
secret: config.secret,
|
|
834
1103
|
basePath: config.basePath ?? DEFAULTS.basePath,
|
|
835
1104
|
pluginRoutes,
|
|
836
|
-
|
|
1105
|
+
trustedOrigins: config.trustedOrigins,
|
|
1106
|
+
onRequest: config.onRequest,
|
|
1107
|
+
maxBodyBytes: config.maxBodyBytes,
|
|
1108
|
+
rateLimit: config.rateLimit ? createRateLimiter(config.rateLimit) : void 0,
|
|
1109
|
+
cronMaxSweeps: config.cron?.maxSweeps,
|
|
1110
|
+
...config.cron?.secret ? { cronSecret: config.cron.secret } : {},
|
|
1111
|
+
...config.machineSecret ?? config.cron?.secret ? { machineSecret: config.machineSecret ?? config.cron?.secret } : {}
|
|
837
1112
|
});
|
|
838
1113
|
async function send(type, args) {
|
|
839
1114
|
const result = await runSend(
|
|
@@ -865,10 +1140,21 @@ function easyPing(config) {
|
|
|
865
1140
|
return result;
|
|
866
1141
|
}
|
|
867
1142
|
for (const plugin of plugins) {
|
|
1143
|
+
const purposes = new Set(
|
|
1144
|
+
(plugin.routes ?? []).flatMap(
|
|
1145
|
+
(route) => route.scope.type === "signed" ? [route.scope.purpose] : []
|
|
1146
|
+
)
|
|
1147
|
+
);
|
|
868
1148
|
plugin.init?.({
|
|
869
1149
|
store: createPluginStore(plugin.id, plugin.schema, config.database, tablePrefix),
|
|
870
|
-
|
|
1150
|
+
sign: createScopedSigner(
|
|
1151
|
+
config.secret,
|
|
1152
|
+
plugin.id,
|
|
1153
|
+
purposes,
|
|
1154
|
+
(message2) => new ConfigError(message2)
|
|
1155
|
+
),
|
|
871
1156
|
logger,
|
|
1157
|
+
notificationTypes: Object.keys(config.notifications),
|
|
872
1158
|
getRecipients: config.getRecipients,
|
|
873
1159
|
send: (type, args) => send(type, args)
|
|
874
1160
|
});
|
|
@@ -877,6 +1163,17 @@ function easyPing(config) {
|
|
|
877
1163
|
send,
|
|
878
1164
|
handler,
|
|
879
1165
|
startWorker: (options) => runner.startWorker(options ?? {}),
|
|
1166
|
+
listRoutes: () => [
|
|
1167
|
+
...CORE_ROUTES.filter((route) => route.path !== "/cron" || Boolean(config.cron?.secret)),
|
|
1168
|
+
...plugins.flatMap(
|
|
1169
|
+
(plugin) => (plugin.routes ?? []).map((route) => ({
|
|
1170
|
+
method: route.method,
|
|
1171
|
+
path: route.path,
|
|
1172
|
+
scope: route.scope,
|
|
1173
|
+
owner: plugin.id
|
|
1174
|
+
}))
|
|
1175
|
+
)
|
|
1176
|
+
],
|
|
880
1177
|
getFailedDeliveries: (options = {}) => config.database.getFailedDeliveries({
|
|
881
1178
|
since: options.since ?? new Date(Date.now() - 24 * 36e5),
|
|
882
1179
|
limit: Math.min(options.limit ?? 100, 1e3)
|
|
@@ -904,8 +1201,12 @@ function defineClientPlugin(plugin) {
|
|
|
904
1201
|
exports.ConfigError = ConfigError;
|
|
905
1202
|
exports.DEFAULT_TOKEN_TTL_SECONDS = DEFAULT_TOKEN_TTL_SECONDS;
|
|
906
1203
|
exports.EasyPingError = EasyPingError;
|
|
1204
|
+
exports.MAX_BODY_BYTES = MAX_BODY_BYTES;
|
|
1205
|
+
exports.MAX_TOKEN_TTL_SECONDS = MAX_TOKEN_TTL_SECONDS;
|
|
1206
|
+
exports.MIN_SECRET_LENGTH = MIN_SECRET_LENGTH;
|
|
907
1207
|
exports.ValidationError = ValidationError;
|
|
908
1208
|
exports.consoleLogger = consoleLogger;
|
|
1209
|
+
exports.createRateLimiter = createRateLimiter;
|
|
909
1210
|
exports.createRunner = createRunner;
|
|
910
1211
|
exports.decodeBase64Url = decodeBase64Url;
|
|
911
1212
|
exports.defineClientPlugin = defineClientPlugin;
|
|
@@ -913,7 +1214,10 @@ exports.defineNotification = defineNotification;
|
|
|
913
1214
|
exports.definePlugin = definePlugin;
|
|
914
1215
|
exports.easyPing = easyPing;
|
|
915
1216
|
exports.encodeBase64Url = encodeBase64Url;
|
|
1217
|
+
exports.escapeHtml = escapeHtml;
|
|
916
1218
|
exports.expiresIn = expiresIn;
|
|
1219
|
+
exports.readJsonBody = readJsonBody;
|
|
1220
|
+
exports.redactErrorMessage = redactErrorMessage;
|
|
917
1221
|
exports.signToken = signToken;
|
|
918
1222
|
exports.verifyToken = verifyToken;
|
|
919
1223
|
//# sourceMappingURL=index.cjs.map
|