easy-ping 0.6.0 → 0.7.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 (78) hide show
  1. package/README.md +21 -5
  2. package/dist/{adapter-D3lTy-4E.d.cts → adapter-BJ8Iejvs.d.cts} +1 -1
  3. package/dist/{adapter-D3lTy-4E.d.ts → adapter-BJ8Iejvs.d.ts} +1 -1
  4. package/dist/adapters/drizzle.d.cts +1 -1
  5. package/dist/adapters/drizzle.d.ts +1 -1
  6. package/dist/adapters/mongodb.d.cts +2 -2
  7. package/dist/adapters/mongodb.d.ts +2 -2
  8. package/dist/adapters/mysql.d.cts +2 -2
  9. package/dist/adapters/mysql.d.ts +2 -2
  10. package/dist/adapters/postgres.d.cts +2 -2
  11. package/dist/adapters/postgres.d.ts +2 -2
  12. package/dist/adapters/sqlite.d.cts +2 -2
  13. package/dist/adapters/sqlite.d.ts +2 -2
  14. package/dist/{chunk-USQ6CQTJ.js → chunk-JOVIZXO6.js} +105 -54
  15. package/dist/chunk-JOVIZXO6.js.map +1 -0
  16. package/dist/chunk-R3BE67L7.js +13 -0
  17. package/dist/chunk-R3BE67L7.js.map +1 -0
  18. package/dist/chunk-SSHVZHSJ.js +100 -0
  19. package/dist/chunk-SSHVZHSJ.js.map +1 -0
  20. package/dist/client.cjs +12 -3
  21. package/dist/client.cjs.map +1 -1
  22. package/dist/client.d.cts +6 -0
  23. package/dist/client.d.ts +6 -0
  24. package/dist/client.js +12 -3
  25. package/dist/client.js.map +1 -1
  26. package/dist/index.cjs +106 -53
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +8 -4
  29. package/dist/index.d.ts +8 -4
  30. package/dist/index.js +7 -14
  31. package/dist/index.js.map +1 -1
  32. package/dist/node.cjs.map +1 -1
  33. package/dist/node.js +1 -1
  34. package/dist/{plugin-Cg9usuUi.d.ts → plugin-CSLKKgXT.d.ts} +1 -1
  35. package/dist/{plugin-CHd2ZiGC.d.cts → plugin-Wwp1nvo0.d.cts} +1 -1
  36. package/dist/plugins/digests.d.cts +2 -2
  37. package/dist/plugins/digests.d.ts +2 -2
  38. package/dist/plugins/preferences-client.d.cts +2 -2
  39. package/dist/plugins/preferences-client.d.ts +2 -2
  40. package/dist/plugins/preferences-react.d.cts +1 -1
  41. package/dist/plugins/preferences-react.d.ts +1 -1
  42. package/dist/plugins/preferences.cjs +3 -3
  43. package/dist/plugins/preferences.cjs.map +1 -1
  44. package/dist/plugins/preferences.d.cts +2 -2
  45. package/dist/plugins/preferences.d.ts +2 -2
  46. package/dist/plugins/preferences.js +2 -2
  47. package/dist/plugins/preferences.js.map +1 -1
  48. package/dist/plugins/push.cjs +2 -2
  49. package/dist/plugins/push.cjs.map +1 -1
  50. package/dist/plugins/push.d.cts +2 -2
  51. package/dist/plugins/push.d.ts +2 -2
  52. package/dist/plugins/push.js +1 -1
  53. package/dist/plugins/telegram.cjs +379 -0
  54. package/dist/plugins/telegram.cjs.map +1 -0
  55. package/dist/plugins/telegram.d.cts +153 -0
  56. package/dist/plugins/telegram.d.ts +153 -0
  57. package/dist/plugins/telegram.js +313 -0
  58. package/dist/plugins/telegram.js.map +1 -0
  59. package/dist/providers/telegram.cjs +103 -0
  60. package/dist/providers/telegram.cjs.map +1 -0
  61. package/dist/providers/telegram.d.cts +83 -0
  62. package/dist/providers/telegram.d.ts +83 -0
  63. package/dist/providers/telegram.js +3 -0
  64. package/dist/providers/telegram.js.map +1 -0
  65. package/dist/providers/web-push.d.cts +2 -2
  66. package/dist/providers/web-push.d.ts +2 -2
  67. package/dist/react.cjs +12 -3
  68. package/dist/react.cjs.map +1 -1
  69. package/dist/react.d.cts +6 -0
  70. package/dist/react.d.ts +6 -0
  71. package/dist/react.js +12 -3
  72. package/dist/react.js.map +1 -1
  73. package/dist/schema.d.cts +2 -2
  74. package/dist/schema.d.ts +2 -2
  75. package/dist/testing.d.cts +1 -1
  76. package/dist/testing.d.ts +1 -1
  77. package/package.json +16 -4
  78. package/dist/chunk-USQ6CQTJ.js.map +0 -1
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  # easy-ping
4
4
 
5
5
  **Own your notifications.**
6
- In-app inbox, transactional email and web push — running inside your app, stored in your
6
+ In-app inbox, transactional email, web push and Telegram — running inside your app, stored in your
7
7
  database, with no per-notification bill.
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/easy-ping?color=%23e0362a&label=npm)](https://www.npmjs.com/package/easy-ping)
@@ -11,7 +11,7 @@ database, with no per-notification bill.
11
11
  [![node](https://img.shields.io/node/v/easy-ping)](https://www.npmjs.com/package/easy-ping)
12
12
  [![license](https://img.shields.io/npm/l/easy-ping?color=blue)](./LICENSE)
13
13
 
14
- [Documentation](https://easy-notify-website.vercel.app) · [Quickstart](https://easy-notify-website.vercel.app/docs/quickstart) · [Changelog](https://easy-notify-website.vercel.app/docs/changelog) · [AI context](https://easy-notify-website.vercel.app/docs/ai-assistant)
14
+ [Documentation](https://easy-ping.teklumoges.dev) · [Quickstart](https://easy-ping.teklumoges.dev/docs/quickstart) · [Changelog](https://easy-ping.teklumoges.dev/docs/changelog) · [AI context](https://easy-ping.teklumoges.dev/docs/ai-assistant)
15
15
 
16
16
  </div>
17
17
 
@@ -47,7 +47,7 @@ hand-rolling it badly.
47
47
  | **Cost per notification** | none | none, you pay for servers | metered per send |
48
48
  | **License** | MIT, all of it | MIT core, commercial modules | proprietary |
49
49
  | **Visual workflow editor** | **no**, a notification is code | yes | yes |
50
- | **Channels out of the box** | **in-app, email, web push** | dozens | dozens |
50
+ | **Channels out of the box** | **in-app, email, web push, Telegram** | dozens | dozens |
51
51
 
52
52
  The last two rows are the trade. If you need a workflow editor a non-developer can edit, use one
53
53
  of the others — they are good tools solving a bigger problem.
@@ -56,7 +56,7 @@ of the others — they are good tools solving a bigger problem.
56
56
  Every "no queue required" decision below follows from that.
57
57
 
58
58
  > **Status: published, pre-1.0.** The core pipeline, all three adapters, the React client and the
59
- > preferences, digests and push plugins are covered by tests against real databases. Web push is
59
+ > preferences, digests, push and telegram plugins are covered by tests against real databases. Web push is
60
60
  > verified end to end against Mozilla's production push service and cross-checked against
61
61
  > `http_ece`. The bell updates over a server-sent event stream with polling as the fallback;
62
62
  > batching is not built, and the Resend provider has only been exercised against a stub. Minor
@@ -379,10 +379,25 @@ single replica.
379
379
  | `inApp` | ✅ built in, on by default, needs no provider |
380
380
  | `email` | ✅ Resend provider; the interface is open for others |
381
381
  | `push` | ✅ push plugin + `webPush()` — VAPID and aes128gcm on Web Crypto, so it runs on edge too |
382
+ | `telegram` | ✅ telegram plugin + `telegramBot()` — one-tap linking through the bot, webhook or long-poll |
382
383
  | `sms` | ⬜ not implemented |
383
384
  | `slack` | ⬜ not implemented |
384
385
 
385
- A channel is usable when core carries it (`inApp`, `email`) or a plugin declares it and can `deliver` it. That is how push works, and how sms and slack will.
386
+ A channel is usable when core carries it (`inApp`, `email`) or a plugin declares it and can `deliver` it. That is how push and telegram work, and how sms and slack will.
387
+
388
+ ```ts
389
+ import { telegram } from "easy-ping/plugins/telegram";
390
+ import { telegramBot } from "easy-ping/providers/telegram";
391
+
392
+ telegram({
393
+ provider: telegramBot({ token: process.env.TELEGRAM_BOT_TOKEN! }),
394
+ botUsername: "your_bot",
395
+ webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET, // or plugin.poll() in development
396
+ render: ({ payload }) => ({ text: `<b>${escapeHtml(payload.authorName)}</b> replied` }),
397
+ });
398
+ ```
399
+
400
+ `POST /telegram/link` returns a `t.me/<bot>?start=<code>` link; the user taps it, the bot stores the chat, and every `send()` with `telegram` in its channels reaches it. Blocked chats prune themselves.
386
401
 
387
402
  Declaring a channel nothing can carry **warns at startup** and reports `skipped: "channel-unavailable"` — deliberately distinct from `"no-channels"`, so a missing provider never looks like a user opt-out.
388
403
 
@@ -475,6 +490,7 @@ Defaults to the last 24 hours, capped at 1000 rows. Wire it to an admin page or
475
490
  | ✅ digests plugin, timezone-aware | |
476
491
  | ✅ push plugin + web-push provider | VAPID + RFC 8291, no node:crypto |
477
492
  | ✅ push verified against a live push service | Mozilla autopush, plus a cross-check against `http_ece` |
493
+ | ✅ telegram plugin + bot provider | one-tap linking, webhook or long-poll, blocked chats pruned |
478
494
  | ✅ scoped plugin storage, so plugins own their tables | |
479
495
  | ✅ additive schema migrations for the raw-SQL path | `planPostgresMigration()`; MySQL and SQLite are bootstrap-only for now |
480
496
  | ✅ failed deliveries reachable from the instance | `notify.getFailedDeliveries()` |
@@ -1,4 +1,4 @@
1
- type Channel = "inApp" | "email" | "push" | "sms" | "slack";
1
+ type Channel = "inApp" | "email" | "push" | "telegram" | "sms" | "slack";
2
2
  type DeliveryStatus = "pending" | "claimed" | "sent" | "failed" | "skipped";
3
3
  type DeliveryMode = "inline" | "deferred" | "worker" | "cron";
4
4
  type Frequency = "instant" | "daily" | "weekly" | "off";
@@ -1,4 +1,4 @@
1
- type Channel = "inApp" | "email" | "push" | "sms" | "slack";
1
+ type Channel = "inApp" | "email" | "push" | "telegram" | "sms" | "slack";
2
2
  type DeliveryStatus = "pending" | "claimed" | "sent" | "failed" | "skipped";
3
3
  type DeliveryMode = "inline" | "deferred" | "worker" | "cron";
4
4
  type Frequency = "instant" | "daily" | "weekly" | "off";
@@ -1,4 +1,4 @@
1
- import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-D3lTy-4E.cjs';
1
+ import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-BJ8Iejvs.cjs';
2
2
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
3
3
 
4
4
  type DrizzleAdapterOptions = {
@@ -1,4 +1,4 @@
1
- import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-D3lTy-4E.js';
1
+ import { D as DatabaseAdapter, C as Channel, a as DeliveryStatus, F as Frequency } from '../adapter-BJ8Iejvs.js';
2
2
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
3
3
 
4
4
  type DrizzleAdapterOptions = {
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
2
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
2
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.cjs';
4
4
 
5
5
  type MongoAdapterOptions = {
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
2
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
2
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.js';
4
4
 
5
5
  type MongoAdapterOptions = {
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.cjs';
2
2
  export { b as MysqlResult } from '../adapter-B7R4rXtJ.cjs';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
4
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
4
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
5
5
 
6
6
  type MysqlAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.js';
2
2
  export { b as MysqlResult } from '../adapter-B7R4rXtJ.js';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
4
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
4
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
5
5
 
6
6
  type MysqlAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
2
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
2
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.cjs';
4
4
 
5
5
  type Row = Record<string, unknown>;
@@ -1,5 +1,5 @@
1
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
2
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
1
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
2
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
3
3
  import { S as Signals } from '../signals-kPmOlKDX.js';
4
4
 
5
5
  type Row = Record<string, unknown>;
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.cjs';
2
2
  export { b as SqliteResult } from '../adapter-B7R4rXtJ.cjs';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.cjs';
4
- import { S as SchemaDeclaration } from '../plugin-CHd2ZiGC.cjs';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.cjs';
4
+ import { S as SchemaDeclaration } from '../plugin-Wwp1nvo0.cjs';
5
5
 
6
6
  type SqliteAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -1,7 +1,7 @@
1
1
  import { S as SqlAdapterOptions, a as SqlExec } from '../adapter-B7R4rXtJ.js';
2
2
  export { b as SqliteResult } from '../adapter-B7R4rXtJ.js';
3
- import { D as DatabaseAdapter } from '../adapter-D3lTy-4E.js';
4
- import { S as SchemaDeclaration } from '../plugin-Cg9usuUi.js';
3
+ import { D as DatabaseAdapter } from '../adapter-BJ8Iejvs.js';
4
+ import { S as SchemaDeclaration } from '../plugin-CSLKKgXT.js';
5
5
 
6
6
  type SqliteAdapterOptions = SqlAdapterOptions;
7
7
  /**
@@ -81,15 +81,16 @@ function createScopedSigner(secret, pluginId, purposes, onViolation) {
81
81
  }
82
82
 
83
83
  // src/core/handler.ts
84
+ var STREAM_HIGH_WATER_MARK = 256;
84
85
  var MAX_BODY_BYTES = 64 * 1024;
85
86
  var MAX_READ_IDS = 200;
86
87
  var PRIVATE_HEADERS = {
87
88
  "cache-control": "private, no-store",
88
89
  "x-content-type-options": "nosniff"
89
90
  };
90
- var json = (body, status = 200) => new Response(JSON.stringify(body), {
91
+ var json = (body, status = 200, headers = {}) => new Response(JSON.stringify(body), {
91
92
  status,
92
- headers: { "content-type": "application/json", ...PRIVATE_HEADERS }
93
+ headers: { "content-type": "application/json", ...PRIVATE_HEADERS, ...headers }
93
94
  });
94
95
  var empty = (status) => new Response(null, { status, headers: PRIVATE_HEADERS });
95
96
  function harden(response) {
@@ -255,7 +256,14 @@ function createHandler(deps) {
255
256
  if (userId === "error") return empty(500);
256
257
  if (userId === null) return empty(401);
257
258
  if (request.method === "GET" && path === "/events") {
258
- return deps.events ? events(request, userId, deps.events) : empty(404);
259
+ if (!deps.events) return empty(404);
260
+ if ((openStreams.get(userId) ?? 0) >= deps.events.maxStreamsPerUser) {
261
+ return json({ error: "too many open event streams" }, 429, { "retry-after": "30" });
262
+ }
263
+ if (totalStreams >= deps.events.maxStreams) {
264
+ return json({ error: "event streams unavailable" }, 503, { "retry-after": "30" });
265
+ }
266
+ return events(request, userId, deps.events);
259
267
  }
260
268
  if (request.method === "GET" && path === "/") {
261
269
  const limitParam = Number(url.searchParams.get("limit") ?? 20);
@@ -303,6 +311,18 @@ function createHandler(deps) {
303
311
  }
304
312
  return empty(404);
305
313
  }
314
+ const openStreams = /* @__PURE__ */ new Map();
315
+ let totalStreams = 0;
316
+ const trackStream = (userId) => {
317
+ openStreams.set(userId, (openStreams.get(userId) ?? 0) + 1);
318
+ totalStreams += 1;
319
+ return () => {
320
+ const remaining = (openStreams.get(userId) ?? 1) - 1;
321
+ if (remaining > 0) openStreams.set(userId, remaining);
322
+ else openStreams.delete(userId);
323
+ totalStreams -= 1;
324
+ };
325
+ };
306
326
  async function fingerprint(userId) {
307
327
  const [page, unseen] = await Promise.all([
308
328
  deps.adapter.listNotifications({ userId, limit: 1 }),
@@ -311,63 +331,92 @@ function createHandler(deps) {
311
331
  const newest = page.notifications[0];
312
332
  return `${newest?.id ?? ""}|${newest?.readAt ? 1 : 0}|${unseen}`;
313
333
  }
334
+ const probes = /* @__PURE__ */ new Map();
335
+ function subscribeProbe(userId, onChanged, intervalMs) {
336
+ let probe = probes.get(userId);
337
+ if (!probe) {
338
+ const created = {
339
+ listeners: /* @__PURE__ */ new Set(),
340
+ timer: setInterval(() => {
341
+ fingerprint(userId).then((value) => {
342
+ if (created.last !== void 0 && value !== created.last) {
343
+ for (const listener of created.listeners) listener();
344
+ }
345
+ created.last = value;
346
+ }).catch((error) => deps.logger.error("events probe failed", { error }));
347
+ }, intervalMs)
348
+ };
349
+ probe = created;
350
+ probes.set(userId, created);
351
+ fingerprint(userId).then((value) => {
352
+ if (created.last === void 0) created.last = value;
353
+ }).catch(() => {
354
+ });
355
+ }
356
+ probe.listeners.add(onChanged);
357
+ return () => {
358
+ const current = probes.get(userId);
359
+ if (!current) return;
360
+ current.listeners.delete(onChanged);
361
+ if (current.listeners.size === 0) {
362
+ clearInterval(current.timer);
363
+ probes.delete(userId);
364
+ }
365
+ };
366
+ }
314
367
  function events(request, userId, settings) {
315
368
  const encoder3 = new TextEncoder();
316
369
  const timers = [];
317
- let unsubscribe;
370
+ const unsubscribes = [];
318
371
  let closed = false;
319
372
  let cleanup = () => {
320
373
  };
321
- const stream = new ReadableStream({
322
- start(controller) {
323
- const write = (text) => {
324
- if (closed) return;
325
- try {
326
- controller.enqueue(encoder3.encode(text));
327
- } catch {
328
- cleanup();
374
+ const stream = new ReadableStream(
375
+ {
376
+ start(controller) {
377
+ const write = (text) => {
378
+ if (closed) return;
379
+ if (controller.desiredSize !== null && controller.desiredSize <= 0) {
380
+ cleanup();
381
+ return;
382
+ }
383
+ try {
384
+ controller.enqueue(encoder3.encode(text));
385
+ } catch {
386
+ cleanup();
387
+ }
388
+ };
389
+ cleanup = () => {
390
+ if (closed) return;
391
+ closed = true;
392
+ for (const unsubscribe of unsubscribes) unsubscribe();
393
+ for (const timer of timers) clearInterval(timer);
394
+ try {
395
+ controller.close();
396
+ } catch {
397
+ }
398
+ };
399
+ const changed = () => write("event: changed\ndata: {}\n\n");
400
+ unsubscribes.push(trackStream(userId));
401
+ write("retry: 3000\n\nevent: ready\ndata: {}\n\n");
402
+ unsubscribes.push(deps.signals.subscribe(inboxChannel(userId), changed));
403
+ timers.push(setInterval(() => write(": keepalive\n\n"), settings.heartbeatMs));
404
+ if (!deps.signals.crossProcess && settings.probeIntervalMs > 0) {
405
+ unsubscribes.push(subscribeProbe(userId, changed, settings.probeIntervalMs));
329
406
  }
330
- };
331
- cleanup = () => {
332
- if (closed) return;
333
- closed = true;
334
- unsubscribe?.();
335
- for (const timer of timers) clearInterval(timer);
336
- try {
337
- controller.close();
338
- } catch {
407
+ if (settings.maxDurationMs > 0) {
408
+ timers.push(
409
+ setTimeout(cleanup, settings.maxDurationMs)
410
+ );
339
411
  }
340
- };
341
- const changed = () => write("event: changed\ndata: {}\n\n");
342
- write("retry: 3000\n\nevent: ready\ndata: {}\n\n");
343
- unsubscribe = deps.signals.subscribe(inboxChannel(userId), changed);
344
- timers.push(setInterval(() => write(": keepalive\n\n"), settings.heartbeatMs));
345
- if (!deps.signals.crossProcess && settings.probeIntervalMs > 0) {
346
- let last;
347
- fingerprint(userId).then((value) => {
348
- last = value;
349
- }).catch(() => {
350
- });
351
- timers.push(
352
- setInterval(() => {
353
- fingerprint(userId).then((value) => {
354
- if (last !== void 0 && value !== last) changed();
355
- last = value;
356
- }).catch((error) => deps.logger.error("events probe failed", { error }));
357
- }, settings.probeIntervalMs)
358
- );
359
- }
360
- if (settings.maxDurationMs > 0) {
361
- timers.push(
362
- setTimeout(cleanup, settings.maxDurationMs)
363
- );
412
+ request.signal?.addEventListener("abort", cleanup, { once: true });
413
+ },
414
+ cancel() {
415
+ cleanup();
364
416
  }
365
- request.signal?.addEventListener("abort", cleanup, { once: true });
366
417
  },
367
- cancel() {
368
- cleanup();
369
- }
370
- });
418
+ { highWaterMark: STREAM_HIGH_WATER_MARK }
419
+ );
371
420
  return new Response(stream, {
372
421
  headers: {
373
422
  "content-type": "text/event-stream; charset=utf-8",
@@ -379,13 +428,15 @@ function createHandler(deps) {
379
428
  });
380
429
  }
381
430
  async function handle(request) {
431
+ let response;
382
432
  try {
383
- return await route(request);
433
+ response = await route(request);
434
+ return response;
384
435
  } catch (error) {
385
436
  deps.logger.error("request failed", { url: request.url, error });
386
437
  return empty(500);
387
438
  } finally {
388
- deps.sweepOnRequest?.();
439
+ if (response && response.status < 400) deps.sweepOnRequest?.();
389
440
  }
390
441
  }
391
442
  return {
@@ -396,5 +447,5 @@ function createHandler(deps) {
396
447
  }
397
448
 
398
449
  export { DEFAULT_TOKEN_TTL_SECONDS, MAX_BODY_BYTES, MAX_TOKEN_TTL_SECONDS, createHandler, createScopedSigner, expiresIn, readJsonBody, signToken, verifyToken };
399
- //# sourceMappingURL=chunk-USQ6CQTJ.js.map
400
- //# sourceMappingURL=chunk-USQ6CQTJ.js.map
450
+ //# sourceMappingURL=chunk-JOVIZXO6.js.map
451
+ //# sourceMappingURL=chunk-JOVIZXO6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/tokens.ts","../src/core/handler.ts"],"names":["encoder","route"],"mappings":"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","file":"chunk-JOVIZXO6.js","sourcesContent":["import { decodeBase64Url, decodeBase64UrlBytes, encodeBase64Url } from \"./base64url\";\n\nexport type TokenClaims = {\n /** Subject — the user the token acts for. */\n uid: string;\n /** What the token is allowed to do. Verification requires an exact match. */\n purpose: string;\n /** Expiry, epoch seconds. */\n exp: number;\n /** Issued at, epoch seconds. Set by signToken; lets a route refuse a token older than a later change. */\n iat?: number;\n /** Free-form, purpose-specific. */\n data?: Record<string, string>;\n};\n\nconst encoder = new TextEncoder();\n\n// Changing this invalidates every outstanding token. Bump it on purpose only.\nconst KEY_SALT = encoder.encode(\"easy-ping/tokens/v1\");\n\n/** One HMAC key per purpose, derived with HKDF, so an unsubscribe key signs nothing else. */\nasync function hmacKey(secret: string, purpose: string): Promise<CryptoKey> {\n const master = await crypto.subtle.importKey(\"raw\", encoder.encode(secret), \"HKDF\", false, [\n \"deriveKey\",\n ]);\n return crypto.subtle.deriveKey(\n { name: \"HKDF\", hash: \"SHA-256\", salt: KEY_SALT, info: encoder.encode(purpose) },\n master,\n { name: \"HMAC\", hash: \"SHA-256\", length: 256 },\n false,\n [\"sign\", \"verify\"],\n );\n}\n\nexport async function signToken(\n secret: string,\n claims: TokenClaims,\n now: Date = new Date(),\n): Promise<string> {\n const stamped: TokenClaims = { ...claims, iat: claims.iat ?? Math.floor(now.getTime() / 1000) };\n const body = encodeBase64Url(JSON.stringify(stamped));\n const signature = await crypto.subtle.sign(\n \"HMAC\",\n await hmacKey(secret, claims.purpose),\n encoder.encode(body),\n );\n return `${body}.${encodeBase64Url(new Uint8Array(signature))}`;\n}\n\n/** null for every failure mode. Distinguishing them tells an attacker which part to fix. */\nexport async function verifyToken(\n secret: string,\n token: string,\n purpose: string,\n now: Date = new Date(),\n): Promise<TokenClaims | null> {\n const separator = token.lastIndexOf(\".\");\n if (separator <= 0) return null;\n\n const body = token.slice(0, separator);\n const signature = decodeBase64UrlBytes(token.slice(separator + 1));\n if (!signature) return null;\n\n // Verified under the expected purpose's key before the body is parsed. subtle.verify is constant-time.\n const valid = await crypto.subtle.verify(\n \"HMAC\",\n await hmacKey(secret, purpose),\n signature,\n encoder.encode(body),\n );\n if (!valid) return null;\n\n const decoded = decodeBase64Url(body);\n if (!decoded) return null;\n\n let claims: TokenClaims;\n try {\n claims = JSON.parse(decoded) as TokenClaims;\n } catch {\n return null;\n }\n\n if (typeof claims?.uid !== \"string\" || typeof claims.exp !== \"number\") return null;\n if (claims.purpose !== purpose) return null;\n if (claims.exp * 1000 <= now.getTime()) return null;\n if (claims.iat !== undefined && typeof claims.iat !== \"number\") return null;\n\n if (claims.data !== undefined) {\n if (typeof claims.data !== \"object\" || claims.data === null || Array.isArray(claims.data)) {\n return null;\n }\n for (const value of Object.values(claims.data)) {\n if (typeof value !== \"string\") return null;\n }\n }\n\n return claims;\n}\n\nexport const DEFAULT_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;\n/** Ninety days. A link that must outlive this should be re-issued, not made immortal. */\nexport const MAX_TOKEN_TTL_SECONDS = 90 * 24 * 60 * 60;\n\nexport const expiresIn = (seconds: number, now: Date = new Date()): number =>\n Math.floor(now.getTime() / 1000) + Math.min(Math.max(seconds, 0), MAX_TOKEN_TTL_SECONDS);\n\nexport type SignRequest = {\n uid: string;\n purpose: string;\n data?: Record<string, string>;\n /** Defaults to 30 days, capped at 90. */\n ttlSeconds?: number;\n};\n\n/** What a plugin gets instead of the secret: signs only its own declared purposes. */\nexport function createScopedSigner(\n secret: string,\n pluginId: string,\n purposes: ReadonlySet<string>,\n onViolation: (message: string) => Error,\n): (request: SignRequest) => Promise<string> {\n return (request) => {\n if (!purposes.has(request.purpose)) {\n throw onViolation(\n `plugin \"${pluginId}\" tried to sign a token for purpose \"${request.purpose}\", ` +\n \"which none of its signed routes declare.\",\n );\n }\n return signToken(secret, {\n uid: request.uid,\n purpose: request.purpose,\n exp: expiresIn(request.ttlSeconds ?? DEFAULT_TOKEN_TTL_SECONDS),\n ...(request.data ? { data: request.data } : {}),\n });\n };\n}\n","import type { DatabaseAdapter } from \"./adapter\";\nimport type { SessionConfig } from \"./config\";\nimport type { Logger } from \"./errors\";\nimport type { Promisable, RouteDefinition } from \"./plugin\";\nimport type { Runner } from \"./runner\";\nimport { inboxChannel, type Signals } from \"./signals\";\nimport { verifyToken } from \"./tokens\";\n\nexport type EventsSettings = {\n heartbeatMs: number;\n probeIntervalMs: number;\n maxDurationMs: number;\n maxStreamsPerUser: number;\n maxStreams: number;\n};\n\n/** Unread chunks a stream may hold before it is closed on a consumer that stopped reading. */\nconst STREAM_HIGH_WATER_MARK = 256;\n\nexport type HandlerDeps = {\n adapter: DatabaseAdapter;\n session: SessionConfig;\n runner: Runner;\n logger: Logger;\n basePath: string;\n secret: string;\n signals: Signals;\n /** `false` unmounts GET /events. */\n events: EventsSettings | false;\n /** Fire-and-forget delivery pass after a request. Rate-limited by the instance. */\n sweepOnRequest?: (() => void) | undefined;\n cronSecret?: string | undefined;\n /** Guards plugin machine routes. The instance defaults it to cronSecret. */\n machineSecret?: string | undefined;\n cronMaxSweeps?: number | undefined;\n pluginRoutes?: readonly RouteDefinition[] | undefined;\n /** Returns a 429 to send, or null to proceed. Runs after onRequest, before routing. */\n rateLimit?: ((request: Request) => Response | null) | undefined;\n /** Origins other than the request's own host that may POST. `*.example.com` allowed. */\n trustedOrigins?: readonly string[] | undefined;\n /** Runs before routing. Returning a Response short-circuits; use it for rate limiting. */\n // biome-ignore lint/suspicious/noConfusingVoidType: a hook that returns nothing is the common case\n onRequest?: ((request: Request) => Promisable<Response | undefined | void>) | undefined;\n maxBodyBytes?: number | undefined;\n};\n\n/** Every JSON body is capped here. Nothing legitimate the routes accept is bigger. */\nexport const MAX_BODY_BYTES = 64 * 1024;\n\nconst MAX_READ_IDS = 200;\n\n// Per-user data: no shared caching, no sniffing.\nconst PRIVATE_HEADERS: Record<string, string> = {\n \"cache-control\": \"private, no-store\",\n \"x-content-type-options\": \"nosniff\",\n};\n\nconst json = (body: unknown, status = 200, headers: Record<string, string> = {}) =>\n new Response(JSON.stringify(body), {\n status,\n headers: { \"content-type\": \"application/json\", ...PRIVATE_HEADERS, ...headers },\n });\n\nconst empty = (status: number) => new Response(null, { status, headers: PRIVATE_HEADERS });\n\n/** Plugin responses get the same headers, without touching their body. */\nfunction harden(response: Response): Response {\n const headers = new Headers(response.headers);\n for (const [name, value] of Object.entries(PRIVATE_HEADERS)) {\n if (!headers.has(name)) headers.set(name, value);\n }\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n\nconst encoder = new TextEncoder();\n\n/** Constant time over SHA-256 digests, so neither content nor length leaks. Works on edge runtimes. */\nasync function timingSafeEqual(a: string, b: string): Promise<boolean> {\n const [left, right] = await Promise.all(\n [a, b].map(\n async (value) => new Uint8Array(await crypto.subtle.digest(\"SHA-256\", encoder.encode(value))),\n ),\n );\n let diff = 0;\n for (let i = 0; i < 32; i += 1) diff |= (left?.[i] ?? 0) ^ (right?.[i] ?? 0);\n return diff === 0;\n}\n\n/** Anchored at a segment boundary: `/foo/api/notifications/cron` is not a cron request. */\nfunction routePath(url: URL, basePath: string): string {\n const { pathname } = url;\n if (pathname === basePath) return \"/\";\n if (pathname.startsWith(`${basePath}/`)) return pathname.slice(basePath.length);\n return pathname;\n}\n\nexport type JsonBody = { body: Record<string, unknown> } | { error: Response };\n\n/** Reads a JSON object body with a streaming byte cap. Malformed JSON is 400; an empty body is `{}`. */\nexport async function readJsonBody(\n request: Request,\n maxBytes: number = MAX_BODY_BYTES,\n): Promise<JsonBody> {\n const tooLarge = () => ({ error: json({ error: `body exceeds ${maxBytes} bytes` }, 413) });\n\n const declared = Number(request.headers.get(\"content-length\"));\n if (Number.isFinite(declared) && declared > maxBytes) return tooLarge();\n if (!request.body) return { body: {} };\n\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > maxBytes) {\n await reader.cancel();\n return tooLarge();\n }\n chunks.push(value);\n }\n\n const bytes = new Uint8Array(total);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n const text = new TextDecoder().decode(bytes);\n if (text.trim() === \"\") return { body: {} };\n\n try {\n const parsed: unknown = JSON.parse(text);\n return {\n body:\n parsed && typeof parsed === \"object\" && !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {},\n };\n } catch {\n return { error: json({ error: \"body must be a JSON object\" }, 400) };\n }\n}\n\nconst isJsonContentType = (request: Request) =>\n /^application\\/json\\s*(;|$)/i.test((request.headers.get(\"content-type\") ?? \"\").trim());\n\nfunction originAllowed(origin: string, url: URL, request: Request, trusted: readonly string[]) {\n let host: string;\n try {\n host = new URL(origin).host;\n } catch {\n return false;\n }\n\n if (host === url.host) return true;\n // Behind a proxy that rewrites Host, the public host arrives here instead.\n const forwarded = request.headers.get(\"x-forwarded-host\")?.split(\",\")[0]?.trim();\n if (forwarded && host === forwarded) return true;\n\n return trusted.some((entry) =>\n entry.startsWith(\"*.\") ? host.endsWith(entry.slice(1)) : entry === origin,\n );\n}\n\n/** CSRF: a cross-site form can neither send `application/json` without a preflight nor forge `Origin`. */\nfunction csrfCheck(request: Request, url: URL, trusted: readonly string[]): Response | null {\n if (!isJsonContentType(request)) {\n return json({ error: \"content-type must be application/json\" }, 415);\n }\n\n const origin = request.headers.get(\"origin\");\n if (origin === null) return null;\n if (origin === \"null\" || !originAllowed(origin, url, request, trusted)) return empty(403);\n\n return null;\n}\n\nexport function createHandler(deps: HandlerDeps) {\n const trustedOrigins = deps.trustedOrigins ?? [];\n const maxBodyBytes = deps.maxBodyBytes ?? MAX_BODY_BYTES;\n\n async function resolveUserId(request: Request): Promise<string | null | \"error\"> {\n try {\n return await deps.session.getUserId(request);\n } catch (error) {\n // A broken session lookup is a 500, never a 401. RFC 0002 §1.\n deps.logger.error(\"session.getUserId threw\", { error });\n return \"error\";\n }\n }\n\n async function bearerMatches(request: Request, secret: string | undefined): Promise<boolean> {\n if (!secret) return false;\n const header = request.headers.get(\"authorization\") ?? \"\";\n const presented = header.startsWith(\"Bearer \") ? header.slice(7) : header;\n return timingSafeEqual(presented, secret);\n }\n\n async function handleCron(request: Request): Promise<Response> {\n if (!deps.cronSecret) return empty(404);\n if (!(await bearerMatches(request, deps.cronSecret))) return empty(401);\n\n // Bounded so an overlapping scheduler does not hold a request open for the whole backlog.\n const result = await deps.runner.drain(\n deps.cronMaxSweeps === undefined ? {} : { maxSweeps: deps.cronMaxSweeps },\n );\n return json(result);\n }\n\n /** Plugin routes first; auth is enforced here so a plugin cannot forget it. */\n async function handlePluginRoute(\n route: RouteDefinition,\n request: Request,\n url: URL,\n ): Promise<Response> {\n if (route.scope.type === \"user\") {\n if (request.method === \"POST\") {\n const rejected = csrfCheck(request, url, trustedOrigins);\n if (rejected) return rejected;\n }\n\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n return harden(await route.handler({ request, userId, claims: null, params: {} }));\n }\n\n if (route.scope.type === \"machine\") {\n if (!deps.machineSecret) return empty(404);\n if (!(await bearerMatches(request, deps.machineSecret))) return empty(401);\n return harden(await route.handler({ request, userId: null, claims: null, params: {} }));\n }\n\n if (route.scope.type === \"signed\") {\n const token =\n url.searchParams.get(\"token\") ??\n (request.headers.get(\"authorization\") ?? \"\").replace(/^Bearer /, \"\");\n\n const claims = await verifyToken(deps.secret, token, route.scope.purpose);\n // One response for every failure mode. RFC 0002 §5.\n if (!claims) return empty(400);\n\n return harden(await route.handler({ request, userId: claims.uid, claims, params: {} }));\n }\n\n return harden(await route.handler({ request, userId: null, claims: null, params: {} }));\n }\n\n async function route(request: Request): Promise<Response> {\n const url = new URL(request.url);\n const path = routePath(url, deps.basePath);\n\n if (deps.onRequest) {\n const early = await deps.onRequest(request);\n if (early instanceof Response) return early;\n }\n\n if (deps.rateLimit) {\n const limited = deps.rateLimit(request);\n if (limited) return limited;\n }\n\n if (path === \"/cron\") {\n if (request.method !== \"POST\") return empty(405);\n return handleCron(request);\n }\n\n const pluginRoute = deps.pluginRoutes?.find(\n (candidate) => candidate.path === path && candidate.method === request.method,\n );\n if (pluginRoute) return handlePluginRoute(pluginRoute, request, url);\n\n if (request.method === \"POST\") {\n const rejected = csrfCheck(request, url, trustedOrigins);\n if (rejected) return rejected;\n }\n\n const userId = await resolveUserId(request);\n if (userId === \"error\") return empty(500);\n if (userId === null) return empty(401);\n\n if (request.method === \"GET\" && path === \"/events\") {\n if (!deps.events) return empty(404);\n // A session can open streams for free; without a cap one account can hold every socket.\n if ((openStreams.get(userId) ?? 0) >= deps.events.maxStreamsPerUser) {\n return json({ error: \"too many open event streams\" }, 429, { \"retry-after\": \"30\" });\n }\n if (totalStreams >= deps.events.maxStreams) {\n return json({ error: \"event streams unavailable\" }, 503, { \"retry-after\": \"30\" });\n }\n return events(request, userId, deps.events);\n }\n\n if (request.method === \"GET\" && path === \"/\") {\n const limitParam = Number(url.searchParams.get(\"limit\") ?? 20);\n const limit = Number.isFinite(limitParam) ? Math.min(Math.max(limitParam, 1), 100) : 20;\n const cursor = url.searchParams.get(\"cursor\");\n\n // unseenCount rides on the first page only: a cursor request is scrollback, not a poll.\n const [page, unseenCount] = await Promise.all([\n deps.adapter.listNotifications({\n userId,\n limit,\n ...(cursor ? { cursor } : {}),\n ...(url.searchParams.get(\"unreadOnly\") === \"true\" ? { unreadOnly: true } : {}),\n }),\n cursor ? Promise.resolve(null) : deps.adapter.countUnseen(userId),\n ]);\n\n return json(unseenCount === null ? page : { ...page, unseenCount });\n }\n\n if (request.method === \"GET\" && path === \"/count\") {\n return json({ unseen: await deps.adapter.countUnseen(userId) });\n }\n\n if (request.method === \"POST\" && path === \"/seen\") {\n const parsed = await readJsonBody(request, maxBodyBytes);\n if (\"error\" in parsed) return parsed.error;\n const before = typeof parsed.body.before === \"string\" ? new Date(parsed.body.before) : null;\n\n // +1ms: Postgres keeps microseconds, an ISO string milliseconds; without it markSeen misses the newest row.\n const cutoff =\n before && !Number.isNaN(before.getTime()) ? new Date(before.getTime() + 1) : new Date();\n\n await deps.adapter.markSeen(userId, cutoff);\n deps.signals.publish(inboxChannel(userId));\n return json({ ok: true });\n }\n\n if (request.method === \"POST\" && path === \"/read\") {\n const parsed = await readJsonBody(request, maxBodyBytes);\n if (\"error\" in parsed) return parsed.error;\n const ids = Array.isArray(parsed.body.ids)\n ? parsed.body.ids.filter((id): id is string => typeof id === \"string\")\n : [];\n if (ids.length === 0) return json({ error: \"ids must be a non-empty string array\" }, 400);\n\n if (ids.length > MAX_READ_IDS) {\n return json({ error: `at most ${MAX_READ_IDS} ids per request` }, 400);\n }\n\n // 404, not 403: a 403 confirms the row exists. RFC 0002 §2.\n const updated = await deps.adapter.markRead(userId, ids);\n if (updated > 0) deps.signals.publish(inboxChannel(userId));\n return updated === 0 ? empty(404) : json({ updated });\n }\n\n if (request.method === \"POST\" && path === \"/read-all\") {\n const updated = await deps.adapter.markAllRead(userId);\n if (updated > 0) deps.signals.publish(inboxChannel(userId));\n return json({ updated });\n }\n\n return empty(404);\n }\n\n const openStreams = new Map<string, number>();\n let totalStreams = 0;\n\n const trackStream = (userId: string) => {\n openStreams.set(userId, (openStreams.get(userId) ?? 0) + 1);\n totalStreams += 1;\n return () => {\n const remaining = (openStreams.get(userId) ?? 1) - 1;\n if (remaining > 0) openStreams.set(userId, remaining);\n else openStreams.delete(userId);\n totalStreams -= 1;\n };\n };\n\n /** A change fingerprint cheap enough to take every probe interval. */\n async function fingerprint(userId: string): Promise<string> {\n const [page, unseen] = await Promise.all([\n deps.adapter.listNotifications({ userId, limit: 1 }),\n deps.adapter.countUnseen(userId),\n ]);\n const newest = page.notifications[0];\n return `${newest?.id ?? \"\"}|${newest?.readAt ? 1 : 0}|${unseen}`;\n }\n\n // One probe per user, however many streams that user holds: N tabs must not mean N queries.\n type Probe = { listeners: Set<() => void>; timer: ReturnType<typeof setInterval>; last?: string };\n const probes = new Map<string, Probe>();\n\n function subscribeProbe(userId: string, onChanged: () => void, intervalMs: number) {\n let probe = probes.get(userId);\n if (!probe) {\n const created: Probe = {\n listeners: new Set(),\n timer: setInterval(() => {\n fingerprint(userId)\n .then((value) => {\n if (created.last !== undefined && value !== created.last) {\n for (const listener of created.listeners) listener();\n }\n created.last = value;\n })\n .catch((error) => deps.logger.error(\"events probe failed\", { error }));\n }, intervalMs),\n };\n probe = created;\n probes.set(userId, created);\n fingerprint(userId)\n .then((value) => {\n if (created.last === undefined) created.last = value;\n })\n .catch(() => {});\n }\n probe.listeners.add(onChanged);\n return () => {\n const current = probes.get(userId);\n if (!current) return;\n current.listeners.delete(onChanged);\n if (current.listeners.size === 0) {\n clearInterval(current.timer);\n probes.delete(userId);\n }\n };\n }\n\n /**\n * Server-Sent Events: `ready` once, `changed` whenever this user's inbox\n * moves, comments as keepalive. No payload ever travels on the stream; the\n * client fetches the feed on `changed`. RFC 0006 §4D.\n */\n function events(request: Request, userId: string, settings: EventsSettings): Response {\n const encoder = new TextEncoder();\n const timers: ReturnType<typeof setInterval>[] = [];\n const unsubscribes: (() => void)[] = [];\n let closed = false;\n let cleanup = () => {};\n\n const stream = new ReadableStream<Uint8Array>(\n {\n start(controller) {\n const write = (text: string) => {\n if (closed) return;\n // A consumer that stopped reading would make this queue grow without bound.\n if (controller.desiredSize !== null && controller.desiredSize <= 0) {\n cleanup();\n return;\n }\n try {\n controller.enqueue(encoder.encode(text));\n } catch {\n cleanup();\n }\n };\n cleanup = () => {\n if (closed) return;\n closed = true;\n for (const unsubscribe of unsubscribes) unsubscribe();\n for (const timer of timers) clearInterval(timer);\n try {\n controller.close();\n } catch {\n // already closed by the consumer\n }\n };\n const changed = () => write(\"event: changed\\ndata: {}\\n\\n\");\n\n unsubscribes.push(trackStream(userId));\n write(\"retry: 3000\\n\\nevent: ready\\ndata: {}\\n\\n\");\n unsubscribes.push(deps.signals.subscribe(inboxChannel(userId), changed));\n timers.push(setInterval(() => write(\": keepalive\\n\\n\"), settings.heartbeatMs));\n\n if (!deps.signals.crossProcess && settings.probeIntervalMs > 0) {\n unsubscribes.push(subscribeProbe(userId, changed, settings.probeIntervalMs));\n }\n\n if (settings.maxDurationMs > 0) {\n timers.push(\n setTimeout(cleanup, settings.maxDurationMs) as ReturnType<typeof setInterval>,\n );\n }\n request.signal?.addEventListener(\"abort\", cleanup, { once: true });\n },\n cancel() {\n cleanup();\n },\n },\n { highWaterMark: STREAM_HIGH_WATER_MARK },\n );\n\n return new Response(stream, {\n headers: {\n \"content-type\": \"text/event-stream; charset=utf-8\",\n \"cache-control\": \"no-cache, no-store\",\n connection: \"keep-alive\",\n \"x-accel-buffering\": \"no\",\n \"x-content-type-options\": \"nosniff\",\n },\n });\n }\n\n async function handle(request: Request): Promise<Response> {\n let response: Response | undefined;\n try {\n response = await route(request);\n return response;\n } catch (error) {\n // A thrown driver error must not escape: unhandled rejection on Node, stack trace elsewhere.\n deps.logger.error(\"request failed\", { url: request.url, error });\n return empty(500);\n } finally {\n // Only a served request earns a sweep; anonymous 401s must not drive database work.\n if (response && response.status < 400) deps.sweepOnRequest?.();\n }\n }\n\n return {\n handle,\n GET: handle,\n POST: handle,\n };\n}\n"]}
@@ -0,0 +1,13 @@
1
+ // src/core/html.ts
2
+ var ESCAPES = {
3
+ "&": "&amp;",
4
+ "<": "&lt;",
5
+ ">": "&gt;",
6
+ '"': "&quot;",
7
+ "'": "&#39;"
8
+ };
9
+ var escapeHtml = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESCAPES[char] ?? char);
10
+
11
+ export { escapeHtml };
12
+ //# sourceMappingURL=chunk-R3BE67L7.js.map
13
+ //# sourceMappingURL=chunk-R3BE67L7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/html.ts"],"names":[],"mappings":";AAAA,IAAM,OAAA,GAAkC;AAAA,EACtC,GAAA,EAAK,OAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,MAAA;AAAA,EACL,GAAA,EAAK,QAAA;AAAA,EACL,GAAA,EAAK;AACP,CAAA;AAGO,IAAM,UAAA,GAAa,CAAC,KAAA,KACzB,MAAA,CAAO,SAAS,EAAE,CAAA,CAAE,OAAA,CAAQ,UAAA,EAAY,CAAC,IAAA,KAAS,OAAA,CAAQ,IAAI,KAAK,IAAI","file":"chunk-R3BE67L7.js","sourcesContent":["const ESCAPES: Record<string, string> = {\n \"&\": \"&amp;\",\n \"<\": \"&lt;\",\n \">\": \"&gt;\",\n '\"': \"&quot;\",\n \"'\": \"&#39;\",\n};\n\n/** For template literals: anything a user typed goes through this, or their HTML ships from your domain. */\nexport const escapeHtml = (value: unknown): string =>\n String(value ?? \"\").replace(/[&<>\"']/g, (char) => ESCAPES[char] ?? char);\n"]}
@@ -0,0 +1,100 @@
1
+ // src/providers/telegram/index.ts
2
+ var telegramStartLink = (botUsername, code) => `https://t.me/${botUsername.replace(/^@/, "")}?start=${encodeURIComponent(code)}`;
3
+ function telegramBot(options) {
4
+ const doFetch = options.fetch ?? globalThis.fetch;
5
+ const base = `${(options.apiBase ?? "https://api.telegram.org").replace(/\/$/, "")}/bot${options.token}`;
6
+ const timeoutMs = options.timeoutMs ?? 1e4;
7
+ const redact = (text) => text.split(options.token).join("<token>");
8
+ async function call(method, body, signal, budgetMs = timeoutMs) {
9
+ const timeout = AbortSignal.timeout(budgetMs);
10
+ const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;
11
+ const response = await doFetch(`${base}/${method}`, {
12
+ method: "POST",
13
+ headers: { "content-type": "application/json" },
14
+ body: JSON.stringify(body),
15
+ signal: merged
16
+ });
17
+ const data = await response.json().catch(() => ({ ok: false }));
18
+ return { status: response.status, data };
19
+ }
20
+ async function must(method, body) {
21
+ const { data } = await call(method, body);
22
+ if (!data.ok)
23
+ throw new Error(redact(`telegram ${method} failed: ${data.description ?? "unknown"}`));
24
+ return data.result;
25
+ }
26
+ return {
27
+ name: "telegram",
28
+ async send(message) {
29
+ const body = {
30
+ chat_id: message.chatId,
31
+ text: message.text,
32
+ parse_mode: "HTML",
33
+ link_preview_options: { is_disabled: true }
34
+ };
35
+ if (message.buttons?.length) {
36
+ body.reply_markup = {
37
+ inline_keyboard: [
38
+ message.buttons.map((button) => ({ text: button.text, url: button.url }))
39
+ ]
40
+ };
41
+ }
42
+ let status;
43
+ let data;
44
+ try {
45
+ ({ status, data } = await call(
46
+ "sendMessage",
47
+ body,
48
+ message.signal
49
+ ));
50
+ } catch (error) {
51
+ const text = error instanceof Error ? error.message : String(error);
52
+ return { ok: false, error: redact(text), retryable: true, gone: false };
53
+ }
54
+ if (data.ok) return { ok: true, messageId: data.result.message_id };
55
+ const description = redact(data.description ?? `HTTP ${status}`);
56
+ const code = data.error_code ?? status;
57
+ if (code === 429) {
58
+ return {
59
+ ok: false,
60
+ error: description,
61
+ retryable: true,
62
+ gone: false,
63
+ retryAfterSeconds: data.parameters?.retry_after
64
+ };
65
+ }
66
+ const gone = code === 403 || code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description);
67
+ return { ok: false, error: description, retryable: code >= 500, gone };
68
+ },
69
+ async getUpdates({ offset, timeoutSeconds = 25, signal }) {
70
+ const { data } = await call(
71
+ "getUpdates",
72
+ {
73
+ ...offset === void 0 ? {} : { offset },
74
+ timeout: timeoutSeconds,
75
+ allowed_updates: ["message", "my_chat_member"]
76
+ },
77
+ signal,
78
+ (timeoutSeconds + 10) * 1e3
79
+ );
80
+ if (!data.ok)
81
+ throw new Error(redact(`telegram getUpdates failed: ${data.description ?? "unknown"}`));
82
+ return data.result;
83
+ },
84
+ setWebhook: async (url, secretToken) => {
85
+ await must("setWebhook", {
86
+ url,
87
+ secret_token: secretToken,
88
+ allowed_updates: ["message", "my_chat_member"]
89
+ });
90
+ },
91
+ deleteWebhook: async () => {
92
+ await must("deleteWebhook", {});
93
+ },
94
+ getMe: () => must("getMe", {})
95
+ };
96
+ }
97
+
98
+ export { telegramBot, telegramStartLink };
99
+ //# sourceMappingURL=chunk-SSHVZHSJ.js.map
100
+ //# sourceMappingURL=chunk-SSHVZHSJ.js.map