easy-ping 0.9.0 → 0.10.1
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 +2 -2
- package/dist/adapters/drizzle.cjs +6 -4
- package/dist/adapters/drizzle.cjs.map +1 -1
- package/dist/adapters/drizzle.js +7 -5
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/mongodb.cjs.map +1 -1
- package/dist/adapters/mongodb.d.cts +1 -1
- package/dist/adapters/mongodb.d.ts +1 -1
- package/dist/adapters/mongodb.js +1 -1
- package/dist/adapters/mysql.cjs.map +1 -1
- package/dist/adapters/mysql.d.cts +1 -1
- package/dist/adapters/mysql.d.ts +1 -1
- package/dist/adapters/mysql.js +2 -2
- package/dist/adapters/postgres.cjs +3 -3
- package/dist/adapters/postgres.cjs.map +1 -1
- package/dist/adapters/postgres.d.cts +2 -2
- package/dist/adapters/postgres.d.ts +2 -2
- package/dist/adapters/postgres.js +5 -5
- package/dist/adapters/postgres.js.map +1 -1
- package/dist/adapters/sqlite.cjs.map +1 -1
- package/dist/adapters/sqlite.d.cts +1 -1
- package/dist/adapters/sqlite.d.ts +1 -1
- package/dist/adapters/sqlite.js +2 -2
- package/dist/{chunk-EROWLUL4.js → chunk-A4HX5YTG.js} +5 -3
- package/dist/chunk-A4HX5YTG.js.map +1 -0
- package/dist/{chunk-JOVIZXO6.js → chunk-CFLVAF2R.js} +20 -6
- package/dist/chunk-CFLVAF2R.js.map +1 -0
- package/dist/{chunk-2EYKPF5A.js → chunk-CMWOIYVQ.js} +3 -3
- package/dist/{chunk-2EYKPF5A.js.map → chunk-CMWOIYVQ.js.map} +1 -1
- package/dist/{chunk-E4IZ2CQC.js → chunk-JN3UCQBP.js} +2 -2
- package/dist/chunk-JN3UCQBP.js.map +1 -0
- package/dist/{chunk-3VOQOGDR.js → chunk-O2PBBL5X.js} +24 -6
- package/dist/chunk-O2PBBL5X.js.map +1 -0
- package/dist/index.cjs +74 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +39 -7
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +14 -2
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +15 -3
- package/dist/node.js.map +1 -1
- package/dist/{plugin-C4fikLl7.d.ts → plugin-Bp68CCnd.d.ts} +1 -0
- package/dist/{plugin-6S8tpH0n.d.cts → plugin-p4HS8yjS.d.cts} +1 -0
- package/dist/plugins/digests.cjs +1 -1
- package/dist/plugins/digests.cjs.map +1 -1
- package/dist/plugins/digests.d.cts +1 -1
- package/dist/plugins/digests.d.ts +1 -1
- package/dist/plugins/digests.js +1 -1
- package/dist/plugins/digests.js.map +1 -1
- package/dist/plugins/mobile-push.cjs +2 -1
- package/dist/plugins/mobile-push.cjs.map +1 -1
- package/dist/plugins/mobile-push.d.cts +1 -1
- package/dist/plugins/mobile-push.d.ts +1 -1
- package/dist/plugins/mobile-push.js +3 -2
- package/dist/plugins/mobile-push.js.map +1 -1
- package/dist/plugins/preferences-client.cjs.map +1 -1
- package/dist/plugins/preferences-client.d.cts +1 -1
- package/dist/plugins/preferences-client.d.ts +1 -1
- package/dist/plugins/preferences-client.js +1 -1
- package/dist/plugins/preferences.cjs.map +1 -1
- package/dist/plugins/preferences.d.cts +1 -1
- package/dist/plugins/preferences.d.ts +1 -1
- package/dist/plugins/preferences.js +1 -1
- package/dist/plugins/push.cjs.map +1 -1
- package/dist/plugins/push.d.cts +1 -1
- package/dist/plugins/push.d.ts +1 -1
- package/dist/plugins/push.js +1 -1
- package/dist/plugins/telegram.cjs +22 -1
- package/dist/plugins/telegram.cjs.map +1 -1
- package/dist/plugins/telegram.d.cts +1 -1
- package/dist/plugins/telegram.d.ts +1 -1
- package/dist/plugins/telegram.js +23 -2
- package/dist/plugins/telegram.js.map +1 -1
- package/dist/providers/expo-push.cjs +1 -1
- package/dist/providers/expo-push.cjs.map +1 -1
- package/dist/providers/expo-push.js +1 -1
- package/dist/providers/expo-push.js.map +1 -1
- package/dist/providers/web-push.cjs +3 -7
- package/dist/providers/web-push.cjs.map +1 -1
- package/dist/providers/web-push.d.cts +1 -1
- package/dist/providers/web-push.d.ts +1 -1
- package/dist/providers/web-push.js +3 -7
- package/dist/providers/web-push.js.map +1 -1
- package/dist/react-native.cjs +20 -2
- package/dist/react-native.cjs.map +1 -1
- package/dist/react-native.js +20 -2
- package/dist/react-native.js.map +1 -1
- package/dist/react.cjs +20 -2
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +20 -2
- package/dist/react.js.map +1 -1
- package/dist/schema.cjs +3 -1
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +2 -1
- package/dist/schema.d.ts +2 -1
- package/dist/schema.js +1 -1
- package/package.json +8 -3
- package/dist/chunk-3VOQOGDR.js.map +0 -1
- package/dist/chunk-E4IZ2CQC.js.map +0 -1
- package/dist/chunk-EROWLUL4.js.map +0 -1
- package/dist/chunk-JOVIZXO6.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/base64url.ts","../../src/core/handler.ts","../../src/core/html.ts","../../src/providers/telegram/index.ts","../../src/plugins/telegram/index.ts"],"names":["encoder"],"mappings":";;;AAEO,SAAS,gBAAgB,KAAA,EAAoC;AAClE,EAAA,MAAM,KAAA,GAAQ,OAAO,KAAA,KAAU,QAAA,GAAW,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA,GAAI,KAAA;AAC5E,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,EAAO,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAC5D,EAAA,OAAO,IAAA,CAAK,MAAM,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC/E;;;ACwCO,IAAM,iBAAiB,EAAA,GAAK,IAAA;AAKnC,IAAM,eAAA,GAA0C;AAAA,EAC9C,eAAA,EAAiB,mBAAA;AAAA,EACjB,wBAAA,EAA0B;AAC5B,CAAA;AAEA,IAAM,IAAA,GAAO,CAAC,IAAA,EAAe,MAAA,GAAS,GAAA,EAAK,OAAA,GAAkC,EAAC,KAC5E,IAAI,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAG;AAAA,EACjC,MAAA;AAAA,EACA,SAAS,EAAE,cAAA,EAAgB,oBAAoB,GAAG,eAAA,EAAiB,GAAG,OAAA;AACxE,CAAC,CAAA;AAiBa,IAAI,WAAA;AAyBpB,eAAsB,YAAA,CACpB,OAAA,EACA,QAAA,GAAmB,cAAA,EACA;AACnB,EAAA,MAAM,QAAA,GAAW,OAAO,EAAE,KAAA,EAAO,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA,aAAA,EAAgB,QAAQ,CAAA,MAAA,CAAA,EAAS,EAAG,GAAG,CAAA,EAAE,CAAA;AAExF,EAAA,MAAM,WAAW,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAC,CAAA;AAC7D,EAAA,IAAI,OAAO,QAAA,CAAS,QAAQ,KAAK,QAAA,GAAW,QAAA,SAAiB,QAAA,EAAS;AACtE,EAAA,IAAI,CAAC,OAAA,CAAQ,IAAA,SAAa,EAAE,IAAA,EAAM,EAAC,EAAE;AAErC,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,IAAA,CAAK,SAAA,EAAU;AACtC,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,IAAI,KAAA,GAAQ,CAAA;AAEZ,EAAA,WAAS;AACP,IAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,KAAA,IAAS,KAAA,CAAM,UAAA;AACf,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,OAAO,MAAA,EAAO;AACpB,MAAA,OAAO,QAAA,EAAS;AAAA,IAClB;AACA,IAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,EACnB;AAEA,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,KAAK,CAAA;AAClC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,KAAA,CAAM,GAAA,CAAI,OAAO,MAAM,CAAA;AACvB,IAAA,MAAA,IAAU,KAAA,CAAM,UAAA;AAAA,EAClB;AAEA,EAAA,MAAM,IAAA,GAAO,IAAI,WAAA,EAAY,CAAE,OAAO,KAAK,CAAA;AAC3C,EAAA,IAAI,IAAA,CAAK,MAAK,KAAM,EAAA,SAAW,EAAE,IAAA,EAAM,EAAC,EAAE;AAE1C,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACvC,IAAA,OAAO;AAAA,MACL,IAAA,EACE,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GACxD,MAAA,GACD;AAAC,KACT;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAE,OAAO,IAAA,CAAK,EAAE,OAAO,4BAAA,EAA6B,EAAG,GAAG,CAAA,EAAE;AAAA,EACrE;AACF;;;ACrJA,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,CAAA;;;ACuDlE,IAAM,iBAAA,GAAoB,CAAC,WAAA,EAAqB,IAAA,KACrD,CAAA,aAAA,EAAgB,WAAA,CAAY,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAC,CAAA,OAAA,EAAU,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;;;AChDjF,IAAM,IAAA,GAAO,4BAAA;AACb,IAAM,IAAA,GAAO,4BAAA;AA+Cb,IAAM,gBAAA,GAAmB;AAAA,EACvB,SAAA,EAAW,iDAAA;AAAA,EACX,YAAA,EAAc,iEAAA;AAAA,EACd,OAAA,EAAS,iEAAA;AAAA,EACT,KAAA,EAAO;AACT,CAAA;AAGO,IAAM,cAAA,GAAiB;AAAA,EAC5B,YAAA,EAAc;AAAA,IACZ,SAAA,EAAW,IAAA;AAAA,IACX,MAAA,EAAQ;AAAA,MACN,EAAA,EAAI,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACrC,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,QAAA,EAAU,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MAC3C,QAAA,EAAU,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC3B,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MACxB,UAAU,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA,EAAK;AAAA,MAC3D,YAAY,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA;AAAK,KAC/D;AAAA,IACA,UAAA,EAAY,CAAC,IAAI,CAAA;AAAA,IACjB,OAAA,EAAS;AAAA,MACP,EAAE,EAAA,EAAI,CAAC,QAAQ,CAAA,EAAG,MAAM,wBAAA,EAAyB;AAAA;AAAA,MAEjD,EAAE,IAAI,CAAC,QAAQ,GAAG,MAAA,EAAQ,IAAA,EAAM,MAAM,wBAAA;AAAyB;AACjE,GACF;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,SAAA,EAAW,IAAA;AAAA,IACX,MAAA,EAAQ;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACvC,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,WAAW,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA,EAAK;AAAA,MAC5D,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,UAAU,IAAA;AAAK,KAC5C;AAAA,IACA,UAAA,EAAY,CAAC,MAAM,CAAA;AAAA,IACnB,OAAA,EAAS,CAAC,EAAE,EAAA,EAAI,CAAC,QAAQ,CAAA,EAAG,IAAA,EAAM,wBAAA,EAA0B;AAAA;AAEhE;AAEA,IAAM,UAAA,GAAa,MAAM,eAAA,CAAgB,MAAA,CAAO,gBAAgB,IAAI,UAAA,CAAW,EAAE,CAAC,CAAC,CAAA;AAG5E,SAAS,mBAAmB,KAAA,EAAwB;AACzD,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,KAAK,CAAA;AAAA,EACrB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,GAAA,CAAI,aAAa,QAAA,IAAY,GAAA,CAAI,aAAa,OAAA,EAAS,OAAO,IAAI,QAAA,KAAa,KAAA;AACnF,EAAA,MAAM,IAAA,GAAO,GAAA,CAAI,QAAA,CAAS,WAAA,EAAY;AACtC,EAAA,OAAO,EACL,IAAA,KAAS,WAAA,IACT,mCAAA,CAAoC,IAAA,CAAK,IAAI,CAAA,IAC7C,sBAAA,CAAuB,IAAA,CAAK,IAAI,CAAA,IAChC,IAAA,CAAK,WAAW,GAAG,CAAA,CAAA;AAEvB;AAEA,IAAMA,QAAAA,GAAU,IAAI,WAAA,EAAY;AAEhC,eAAe,eAAA,CAAgB,GAAW,CAAA,EAA6B;AACrE,EAAA,MAAM,CAAC,IAAA,EAAM,KAAK,CAAA,GAAI,MAAM,OAAA,CAAQ,GAAA;AAAA,IAClC,CAAC,CAAA,EAAG,CAAC,CAAA,CAAE,GAAA;AAAA,MACL,OAAO,KAAA,KAAU,IAAI,UAAA,CAAW,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,SAAA,EAAWA,QAAAA,CAAQ,MAAA,CAAO,KAAK,CAAC,CAAC;AAAA;AAC9F,GACF;AACA,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,EAAI,CAAA,IAAK,CAAA,EAAG,IAAA,IAAA,CAAS,IAAA,GAAO,CAAC,CAAA,IAAK,CAAA,KAAM,KAAA,GAAQ,CAAC,CAAA,IAAK,CAAA,CAAA;AAC1E,EAAA,OAAO,IAAA,KAAS,CAAA;AAClB;AAcO,SAAS,SAAS,OAAA,EAA0C;AACjE,EAAA,IAAI,GAAA;AACJ,EAAA,MAAM,WAAW,EAAE,GAAG,gBAAA,EAAkB,GAAG,QAAQ,QAAA,EAAS;AAC5D,EAAA,MAAM,KAAA,GAAA,CAAS,OAAA,CAAQ,cAAA,IAAkB,EAAA,IAAM,GAAA;AAC/C,EAAA,MAAM,WAAW,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAA,CAAQ,mBAAmB,CAAC,CAAA;AACzD,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,CAAY,OAAA,CAAQ,MAAM,EAAE,CAAA;AAExD,EAAA,MAAM,KAAA,GAAQ,OAAO,MAAA,EAAgB,IAAA,KAAiB;AACpD,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,QAAA,CAAS,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,UAAA,CAAW,IAAI,CAAA,EAAG,CAAA;AAC7E,IAAA,IAAI,CAAC,OAAO,EAAA,EAAI,GAAA,CAAI,OAAO,IAAA,CAAK,CAAA,+BAAA,EAAkC,MAAA,CAAO,KAAK,CAAA,CAAE,CAAA;AAAA,EAClF,CAAA;AAEA,EAAA,eAAe,IAAA,CAAK,MAAc,IAAA,EAAsD;AACtF,IAAA,MAAM,CAAC,OAAO,CAAA,GAAI,MAAM,IAAI,KAAA,CAAM,IAAA,CAAmB,IAAA,EAAM,EAAE,IAAA,EAAK,EAAG,EAAE,KAAA,EAAO,GAAG,CAAA;AACjF,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA;AAE7B,IAAA,IAAI,CAAC,WAAW,OAAA,CAAQ,SAAA,CAAU,SAAQ,GAAI,IAAA,CAAK,KAAI,EAAG;AACxD,MAAA,IAAI,OAAA,QAAe,GAAA,CAAI,KAAA,CAAM,OAAO,IAAA,EAAM,EAAE,MAAM,CAAA;AAClD,MAAA,MAAM,KAAA,CAAM,MAAA,EAAQ,QAAA,CAAS,OAAO,CAAA;AACpC,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,EAAE,MAAM,CAAA;AAErC,IAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,IAAA,MAAM,OAAA,GAAU;AAAA,MACd,UAAU,IAAA,CAAK,IAAA;AAAA,MACf,QAAA,EAAU,KAAK,QAAA,IAAY,IAAA;AAAA,MAC3B,KAAA,EAAO,KAAK,KAAA,IAAS,IAAA;AAAA,MACrB,UAAA,EAAY;AAAA,KACd;AACA,IAAA,MAAM,CAAC,QAAQ,CAAA,GAAI,MAAM,IAAI,KAAA,CAAM,IAAA,CAAmB,IAAA,EAAM,EAAE,MAAA,EAAO,EAAG,EAAE,KAAA,EAAO,GAAG,CAAA;AAEpF,IAAA,IAAI,QAAA,EAAU;AAGZ,MAAA,MAAM,IAAI,KAAA,CAAM,MAAA;AAAA,QACd,IAAA;AAAA,QACA,EAAE,EAAA,EAAI,QAAA,CAAS,EAAA,EAAG;AAAA,QAClB,EAAE,MAAA,EAAQ,OAAA,CAAQ,QAAQ,QAAA,EAAU,GAAA,EAAK,GAAG,OAAA;AAAQ,OACtD;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,KAAA,CAAM,IAAA;AAAA,QAC5B,IAAA;AAAA,QACA,EAAE,MAAA,EAAQ,OAAA,CAAQ,MAAA,EAAO;AAAA,QACzB,EAAE,OAAA,EAAS,EAAE,OAAO,YAAA,EAAc,SAAA,EAAW,OAAM;AAAE,OACvD;AACA,MAAA,MAAM,MAAA,GAAS,KAAA,CAAM,MAAA,GAAS,QAAA,GAAW,CAAA;AACzC,MAAA,IAAI,SAAS,CAAA,EAAG;AACd,QAAA,MAAM,GAAA,CAAI,MAAM,MAAA,CAAO,IAAA,EAAM,EAAE,EAAA,EAAI,EAAE,IAAI,KAAA,CAAM,KAAA,CAAM,GAAG,MAAM,CAAA,CAAE,IAAI,CAAC,GAAA,KAAQ,IAAI,EAAE,CAAA,IAAK,CAAA;AAAA,MAC1F;AACA,MAAA,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM;AAAA,QAC3B,EAAE,EAAA,EAAI,MAAA,CAAO,UAAA,EAAW,EAAG,MAAA,EAAQ,OAAA,CAAQ,MAAA,EAAQ,MAAA,EAAQ,QAAA,EAAU,GAAA,EAAK,GAAG,OAAA;AAAQ,OACtF,CAAA;AAAA,IACH;AACA,IAAA,MAAM,KAAA,CAAM,MAAA,EAAQ,QAAA,CAAS,SAAS,CAAA;AAAA,EACxC;AAEA,EAAA,eAAe,aAAa,MAAA,EAAuC;AACjE,IAAA,MAAM,SAAS,MAAA,CAAO,cAAA;AACtB,IAAA,IAAI,MAAA,IAAU,CAAC,QAAA,EAAU,MAAM,EAAE,QAAA,CAAS,MAAA,CAAO,eAAA,CAAgB,MAAM,CAAA,EAAG;AACxE,MAAA,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,EAAE,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,EAAG,CAAA;AAC/D,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,UAAU,MAAA,CAAO,OAAA;AACvB,IAAA,IAAI,CAAC,SAAS,IAAA,EAAM;AACpB,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,EAAE,CAAA;AACrC,IAAA,MAAM,CAAC,OAAA,EAAS,QAAQ,CAAA,GAAI,OAAA,CAAQ,KAAK,IAAA,EAAK,CAAE,KAAA,CAAM,KAAA,EAAO,CAAC,CAAA;AAE9D,IAAA,MAAM,IAAA,GAAO,OAAA,EAAS,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AAElC,IAAA,IAAI,SAAS,QAAA,EAAU;AACrB,MAAA,IAAI,QAAA,EAAU,MAAM,IAAA,CAAK,QAAA,EAAU,QAAQ,IAAI,CAAA;AAAA,WAC1C,MAAM,KAAA,CAAM,MAAA,EAAQ,QAAA,CAAS,KAAK,CAAA;AACvC,MAAA;AAAA,IACF;AACA,IAAA,IAAI,SAAS,OAAA,EAAS;AACpB,MAAA,MAAM,OAAA,GAAU,MAAM,GAAA,CAAI,KAAA,CAAM,OAAO,IAAA,EAAM,EAAE,QAAQ,CAAA;AACvD,MAAA,IAAI,UAAU,CAAA,EAAG,MAAM,KAAA,CAAM,MAAA,EAAQ,SAAS,YAAY,CAAA;AAAA,IAC5D;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAA4B;AAAA,IAChC;AAAA,MACE,IAAA,EAAM,gBAAA;AAAA,MACN,MAAA,EAAQ,MAAA;AAAA,MACR,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACtB,OAAA,EAAS,OAAO,EAAE,MAAA,EAAO,KAAM;AAC7B,QAAA,MAAM,QAAQ,MAAA,IAAU,EAAA;AACxB,QAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AAErB,QAAA,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAA,EAAO,SAAA,EAAW,EAAE,IAAI,IAAI,IAAA,CAAK,GAAG,CAAA,IAAK,CAAA;AAEhF,QAAA,MAAM,OAAO,UAAA,EAAW;AACxB,QAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,GAAA,GAAM,KAAK,CAAA;AACtC,QAAA,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM;AAAA,UAC3B,EAAE,MAAM,MAAA,EAAQ,KAAA,EAAO,WAAW,IAAI,IAAA,CAAK,GAAG,CAAA,EAAG,SAAA;AAAU,SAC5D,CAAA;AACD,QAAA,OAAO,SAAS,IAAA,CAAK;AAAA,UACnB,GAAA,EAAK,iBAAA,CAAkB,WAAA,EAAa,IAAI,CAAA;AAAA,UACxC,IAAA;AAAA,UACA,SAAA,EAAW,UAAU,WAAA;AAAY,SAClC,CAAA;AAAA,MACH;AAAA,KACF;AAAA,IACA;AAAA,MACE,IAAA,EAAM,iBAAA;AAAA,MACN,MAAA,EAAQ,KAAA;AAAA,MACR,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACtB,OAAA,EAAS,OAAO,EAAE,MAAA,EAAO,KAAM;AAC7B,QAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,KAAA,CAAM,IAAA;AAAA,UAC5B,IAAA;AAAA,UACA,EAAE,MAAA,EAAQ,MAAA,IAAU,EAAA,EAAG;AAAA,UACvB,EAAE,OAAA,EAAS,EAAE,OAAO,UAAA,EAAY,SAAA,EAAW,QAAO;AAAE,SACtD;AACA,QAAA,OAAO,SAAS,IAAA,CAAK;AAAA,UACnB,KAAA,EAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,MAAU;AAAA,YAC1B,QAAQ,IAAA,CAAK,MAAA;AAAA,YACb,UAAU,IAAA,CAAK,QAAA;AAAA,YACf,UAAU,IAAA,CAAK,QAAA;AAAA,YACf,OAAO,IAAA,CAAK,KAAA;AAAA,YACZ,QAAA,EAAU,IAAA,CAAK,QAAA,CAAS,WAAA;AAAY,WACtC,CAAE;AAAA,SACH,CAAA;AAAA,MACH;AAAA,KACF;AAAA,IACA;AAAA,MACE,IAAA,EAAM,kBAAA;AAAA,MACN,MAAA,EAAQ,MAAA;AAAA,MACR,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACtB,OAAA,EAAS,OAAO,EAAE,OAAA,EAAS,QAAO,KAAM;AACtC,QAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,OAAO,CAAA;AACzC,QAAA,IAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,MAAA,CAAO,KAAA;AACrC,QAAA,MAAM,EAAE,MAAA,EAAO,GAAI,MAAA,CAAO,IAAA;AAC1B,QAAA,MAAM,QAAQ,MAAA,IAAU,EAAA;AAExB,QAAA,MAAM,OAAA,GAAU,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA;AAAA,UAC9B,IAAA;AAAA,UACA,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,EAAA,GACrC,EAAE,MAAA,EAAQ,KAAA,EAAO,MAAA,EAAO,GACxB,EAAE,MAAA,EAAQ,KAAA;AAAM,SACtB;AACA,QAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,OAAA,EAAS,CAAA;AAAA,MAClC;AAAA;AACF,GACF;AAEA,EAAA,IAAI,QAAQ,aAAA,EAAe;AACzB,IAAA,MAAM,SAAS,OAAA,CAAQ,aAAA;AACvB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,mBAAA;AAAA,MACN,MAAA,EAAQ,MAAA;AAAA,MACR,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,QAAA;AAAA,QACN,aAAA,EACE;AAAA,OACJ;AAAA,MACA,OAAA,EAAS,OAAO,EAAE,OAAA,EAAQ,KAAM;AAC9B,QAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,iCAAiC,CAAA,IAAK,EAAA;AAC5E,QAAA,IAAI,CAAE,MAAM,eAAA,CAAgB,SAAA,EAAW,MAAM,CAAA,EAAI,OAAO,IAAI,QAAA,CAAS,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAK,CAAA;AAE1F,QAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,OAAO,CAAA;AACzC,QAAA,IAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,MAAA,CAAO,KAAA;AACrC,QAAA,IAAI;AACF,UAAA,MAAM,YAAA,CAAa,OAAO,IAAiC,CAAA;AAAA,QAC7D,SAAS,KAAA,EAAO;AAEd,UAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,wBAAA,EAA0B,EAAE,OAAO,CAAA;AAAA,QACtD;AACA,QAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,EAAA,EAAI,MAAM,CAAA;AAAA,MACnC;AAAA,KACD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,UAAA;AAAA,IACJ,QAAA,EAAU,CAAC,UAAU,CAAA;AAAA,IACrB,MAAA,EAAQ,cAAA;AAAA,IAER,IAAA,EAAM,CAAC,OAAA,KAAY;AACjB,MAAA,GAAA,GAAM,OAAA;AAAA,IACR,CAAA;AAAA,IAEA,KAAA,EAAO;AAAA,MACL,OAAA,EAAS,OAAO,OAAA,KAAsD;AACpE,QAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,KAAA,CAAM,KAAmB,IAAA,EAAM;AAAA,UACrD,MAAA,EAAQ,QAAQ,YAAA,CAAa;AAAA,SAC9B,CAAA;AACD,QAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG,OAAO,EAAE,MAAA,EAAQ,SAAA,EAAW,QAAQ,yBAAA,EAA0B;AAEtF,QAAA,MAAM,QAAA,GAAW,QAAQ,MAAA,CAAO;AAAA,UAC9B,IAAA,EAAM,QAAQ,YAAA,CAAa,IAAA;AAAA,UAC3B,OAAA,EAAS,QAAQ,YAAA,CAAa;AAAA,SAC/B,CAAA;AACD,QAAA,IAAI,SAAS,QAAA,CAAS,MAAA;AACtB,QAAA,IAAI,MAAA,IAAU,CAAC,kBAAA,CAAmB,MAAA,CAAO,GAAG,CAAA,EAAG;AAC7C,UAAA,GAAA,CAAI,MAAA,CAAO,IAAA;AAAA,YACT,CAAA,0BAAA,EAA6B,OAAO,GAAG,CAAA,6CAAA;AAAA,WACzC;AACA,UAAA,MAAA,GAAS,MAAA;AAAA,QACX;AAEA,QAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,GAAA;AAAA,UAC5B,KAAA,CAAM,GAAA;AAAA,YAAI,CAAC,IAAA,KACT,OAAA,CAAQ,QAAA,CAAS,IAAA,CAAK;AAAA,cACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,cACb,MAAM,QAAA,CAAS,IAAA;AAAA,cACf,OAAA,EAAS,MAAA,GAAS,CAAC,MAAM,CAAA,GAAI,MAAA;AAAA,cAC7B,QAAQ,OAAA,CAAQ;AAAA,aACjB;AAAA;AACH,SACF;AAEA,QAAA,MAAM,QAAkB,EAAC;AACzB,QAAA,IAAI,SAAA,GAAY,CAAA;AAChB,QAAA,IAAI,SAAA,GAAY,KAAA;AAChB,QAAA,IAAI,SAAA,GAAY,EAAA;AAChB,QAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,MAAA,EAAQ,KAAA,KAAU;AACjC,UAAA,MAAM,IAAA,GAAO,MAAM,KAAK,CAAA;AACxB,UAAA,IAAI,CAAC,IAAA,EAAM;AACX,UAAA,IAAI,OAAO,EAAA,EAAI;AACb,YAAA,SAAA,IAAa,CAAA;AACb,YAAA;AAAA,UACF;AACA,UAAA,SAAA,GAAY,MAAA,CAAO,KAAA;AACnB,UAAA,IAAI,MAAA,CAAO,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,KAAK,EAAE,CAAA;AAAA,eAAA,IAC1B,MAAA,CAAO,WAAW,SAAA,GAAY,IAAA;AAAA,QACzC,CAAC,CAAA;AAED,QAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,EAAE,EAAA,EAAI,EAAE,EAAA,EAAI,KAAA,IAAS,CAAA;AACxE,QAAA,IAAI,SAAA,GAAY,CAAA,EAAG,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC3C,QAAA,IAAI,KAAA,CAAM,MAAA,KAAW,KAAA,CAAM,MAAA,EAAQ;AACjC,UAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,2BAAA,EAA6B,WAAW,KAAA,EAAM;AAAA,QAClF;AACA,QAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,SAAA,IAAa,gCAAgC,SAAA,EAAU;AAAA,MAC3F;AAAA,KACF;AAAA,IAEA,MAAA;AAAA,IACA,YAAA;AAAA,IAEA,IAAA,CAAK,WAAA,GAAc,EAAC,EAAG;AACrB,MAAA,MAAM,MAAA,GAAS,WAAA,CAAY,MAAA,IAAU,GAAA,CAAI,MAAA;AACzC,MAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,MAAA,IAAI,MAAA;AAEJ,MAAA,KAAA,CAAM,YAAY;AAChB,QAAA,OAAO,CAAC,UAAA,CAAW,MAAA,CAAO,OAAA,EAAS;AACjC,UAAA,IAAI;AACF,YAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,QAAA,CAAS,UAAA,CAAW;AAAA,cAChD,MAAA;AAAA,cACA,QAAQ,UAAA,CAAW;AAAA,aACpB,CAAA;AACD,YAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,cAAA,MAAA,GAAS,OAAO,SAAA,GAAY,CAAA;AAC5B,cAAA,MAAM,YAAA,CAAa,MAAM,CAAA,CAAE,KAAA;AAAA,gBAAM,CAAC,KAAA,KAChC,MAAA,CAAO,MAAM,wBAAA,EAA0B,EAAE,OAAO;AAAA,eAClD;AAAA,YACF;AAAA,UACF,SAAS,KAAA,EAAO;AACd,YAAA,IAAI,UAAA,CAAW,OAAO,OAAA,EAAS;AAC/B,YAAA,MAAA,CAAO,KAAA,CAAM,yBAAA,EAA2B,EAAE,KAAA,EAAO,CAAA;AACjD,YAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,GAAK,CAAC,CAAA;AAAA,UAC3D;AAAA,QACF;AAAA,MACF,CAAA,GAAG;AAEH,MAAA,OAAO,EAAE,IAAA,EAAM,MAAM,UAAA,CAAW,OAAM,EAAE;AAAA,IAC1C;AAAA,GACF;AACF","file":"telegram.cjs","sourcesContent":["// Web APIs, not Buffer: this runs on Workers and Vercel Edge.\n\nexport function encodeBase64Url(input: string | Uint8Array): string {\n const bytes = typeof input === \"string\" ? new TextEncoder().encode(input) : input;\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n// Uint8Array<ArrayBuffer>: bare Uint8Array widens and is not a BufferSource.\nexport function decodeBase64UrlBytes(value: string): Uint8Array<ArrayBuffer> | null {\n try {\n const binary = atob(value.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n return Uint8Array.from(binary, (char) => char.charCodeAt(0));\n } catch {\n return null;\n }\n}\n\nexport function decodeBase64Url(value: string): string | null {\n const bytes = decodeBase64UrlBytes(value);\n return bytes ? new TextDecoder().decode(bytes) : null;\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","const ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\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","export type TelegramButton = { text: string; url: string };\n\nexport type TelegramMessage = {\n chatId: string;\n /** Telegram HTML: <b>, <i>, <a href>, <code>, <pre>. Escape user text with escapeHtml(). */\n text: string;\n buttons?: readonly TelegramButton[] | undefined;\n signal?: AbortSignal | undefined;\n};\n\nexport type TelegramSendResult =\n | { ok: true; messageId: number }\n | {\n ok: false;\n error: string;\n /** Worth another attempt: rate limit, 5xx, network. */\n retryable: boolean;\n /** The chat can never receive again (user blocked the bot, chat deleted). Prune it. */\n gone: boolean;\n retryAfterSeconds?: number | undefined;\n };\n\n/** The slice of a Bot API update the plugin reads. Everything else passes through untouched. */\nexport type TelegramUpdate = {\n update_id: number;\n message?: {\n text?: string;\n chat: { id: number | string; type: string; username?: string; title?: string };\n from?: { id: number; username?: string; first_name?: string };\n };\n my_chat_member?: {\n chat: { id: number | string; type: string; username?: string; title?: string };\n new_chat_member: { status: string };\n };\n};\n\nexport type TelegramProvider = {\n name: string;\n send(message: TelegramMessage): Promise<TelegramSendResult>;\n /** Long-polls for updates. For development or hosts without a public URL. */\n getUpdates(options: {\n offset?: number | undefined;\n timeoutSeconds?: number | undefined;\n signal?: AbortSignal | undefined;\n }): Promise<TelegramUpdate[]>;\n /** Points Telegram at your webhook route. Run once per deployment URL. */\n setWebhook(url: string, secretToken: string): Promise<void>;\n deleteWebhook(): Promise<void>;\n getMe(): Promise<{ id: number; username: string }>;\n};\n\nexport type TelegramBotOptions = {\n /** From @BotFather. Never appears in errors or logs. */\n token: string;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://api.telegram.org. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ApiResponse<T> =\n | { ok: true; result: T }\n | { ok: false; description?: string; error_code?: number; parameters?: { retry_after?: number } };\n\n/** Deep link that opens the bot with a start parameter. Codes are [A-Za-z0-9_-], at most 64 chars. */\nexport const telegramStartLink = (botUsername: string, code: string) =>\n `https://t.me/${botUsername.replace(/^@/, \"\")}?start=${encodeURIComponent(code)}`;\n\n/** Bot API over fetch, so it runs on edge runtimes. Failures are classified, never thrown, on send. */\nexport function telegramBot(options: TelegramBotOptions): TelegramProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = `${(options.apiBase ?? \"https://api.telegram.org\").replace(/\\/$/, \"\")}/bot${options.token}`;\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) => text.split(options.token).join(\"<token>\");\n\n async function call<T>(\n method: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n budgetMs = timeoutMs,\n ): Promise<{ status: number; data: ApiResponse<T> }> {\n const timeout = AbortSignal.timeout(budgetMs);\n const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;\n const response = await doFetch(`${base}/${method}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: merged,\n });\n const data = (await response.json().catch(() => ({ ok: false }))) as ApiResponse<T>;\n return { status: response.status, data };\n }\n\n async function must<T>(method: string, body: Record<string, unknown>): Promise<T> {\n const { data } = await call<T>(method, body);\n if (!data.ok)\n throw new Error(redact(`telegram ${method} failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n }\n\n return {\n name: \"telegram\",\n\n async send(message) {\n const body: Record<string, unknown> = {\n chat_id: message.chatId,\n text: message.text,\n parse_mode: \"HTML\",\n link_preview_options: { is_disabled: true },\n };\n if (message.buttons?.length) {\n body.reply_markup = {\n inline_keyboard: [\n message.buttons.map((button) => ({ text: button.text, url: button.url })),\n ],\n };\n }\n\n let status: number;\n let data: ApiResponse<{ message_id: number }>;\n try {\n ({ status, data } = await call<{ message_id: number }>(\n \"sendMessage\",\n body,\n message.signal,\n ));\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error);\n return { ok: false, error: redact(text), retryable: true, gone: false };\n }\n\n if (data.ok) return { ok: true, messageId: data.result.message_id };\n\n const description = redact(data.description ?? `HTTP ${status}`);\n const code = data.error_code ?? status;\n if (code === 429) {\n return {\n ok: false,\n error: description,\n retryable: true,\n gone: false,\n retryAfterSeconds: data.parameters?.retry_after,\n };\n }\n // 403: the user blocked the bot or was deactivated. 400 \"chat not found\": deleted or never existed.\n const gone =\n code === 403 ||\n (code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description));\n return { ok: false, error: description, retryable: code >= 500, gone };\n },\n\n async getUpdates({ offset, timeoutSeconds = 25, signal }) {\n // Telegram holds the request open for `timeout` seconds; the budget must outlive it.\n const { data } = await call<TelegramUpdate[]>(\n \"getUpdates\",\n {\n ...(offset === undefined ? {} : { offset }),\n timeout: timeoutSeconds,\n allowed_updates: [\"message\", \"my_chat_member\"],\n },\n signal,\n (timeoutSeconds + 10) * 1000,\n );\n if (!data.ok)\n throw new Error(redact(`telegram getUpdates failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n },\n\n setWebhook: async (url, secretToken) => {\n await must(\"setWebhook\", {\n url,\n secret_token: secretToken,\n allowed_updates: [\"message\", \"my_chat_member\"],\n });\n },\n deleteWebhook: async () => {\n await must(\"deleteWebhook\", {});\n },\n getMe: () => must(\"getMe\", {}),\n };\n}\n","import type { DeliveryOutcome } from \"../../core/adapter\";\nimport { encodeBase64Url } from \"../../core/base64url\";\nimport type { Logger } from \"../../core/errors\";\nimport { readJsonBody } from \"../../core/handler\";\nimport { escapeHtml } from \"../../core/html\";\nimport type {\n DeliverContext,\n EasyPingPlugin,\n PluginInitContext,\n RouteDefinition,\n SchemaDeclaration,\n} from \"../../core/plugin\";\nimport {\n type TelegramProvider,\n type TelegramUpdate,\n telegramStartLink,\n} from \"../../providers/telegram\";\n\nconst CHAT = \"notification_telegram_chat\";\nconst LINK = \"notification_telegram_link\";\n\nexport type TelegramChat = {\n id: string;\n userId: string;\n chatId: string;\n /** \"private\", \"group\", \"supergroup\" or \"channel\". */\n chatType: string;\n username: string | null;\n title: string | null;\n linkedAt: Date;\n lastSeenAt: Date;\n};\n\nexport type TelegramLink = {\n code: string;\n userId: string;\n createdAt: Date;\n expiresAt: Date;\n};\n\nexport type TelegramRendered = {\n /** Telegram HTML. Anything user-typed inside it goes through escapeHtml(). */\n text: string;\n /** Adds one inline button under the message. */\n button?: { text: string; url: string } | undefined;\n};\n\nexport type TelegramOptions = {\n provider: TelegramProvider;\n /** Without the @. Builds the https://t.me/<bot>?start=<code> link. */\n botUsername: string;\n render: (input: { type: string; payload: unknown }) => TelegramRendered;\n /**\n * Shared with Telegram through setWebhook; Telegram echoes it in\n * X-Telegram-Bot-Api-Secret-Token and the webhook route accepts nothing else.\n * Unset, the route is not mounted and updates must come through poll().\n */\n webhookSecret?: string | undefined;\n /** How long a \"Connect Telegram\" link stays valid. Default 10 minutes. */\n linkTtlMinutes?: number | undefined;\n /** Linking past this evicts the user's least recently seen chat. Default 5. */\n maxChatsPerUser?: number | undefined;\n /** Bot replies, if the defaults are not your voice. */\n messages?: Partial<typeof DEFAULT_MESSAGES> | undefined;\n};\n\nconst DEFAULT_MESSAGES = {\n connected: \"Connected. You will receive notifications here.\",\n disconnected: \"Disconnected. You will not receive notifications here any more.\",\n expired: \"That link has expired. Open the app and connect Telegram again.\",\n start: \"Open the app and use “Connect Telegram” to link this chat.\",\n};\n\n/** The tables this plugin owns, exported so DDL needs no plugin instance. */\nexport const telegramSchema = {\n telegramChat: {\n tableName: CHAT,\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n chatId: { type: \"string\", required: true },\n chatType: { type: \"string\", required: true },\n username: { type: \"string\" },\n title: { type: \"string\" },\n linkedAt: { type: \"date\", required: true, defaultNow: true },\n lastSeenAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\"], name: \"telegram_chat_user_idx\" },\n // A chat belongs to one account at a time; re-linking moves it.\n { on: [\"chatId\"], unique: true, name: \"telegram_chat_chat_idx\" },\n ],\n },\n telegramLink: {\n tableName: LINK,\n fields: {\n code: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n expiresAt: { type: \"date\", required: true },\n },\n primaryKey: [\"code\"],\n indexes: [{ on: [\"userId\"], name: \"telegram_link_user_idx\" }],\n },\n} as const satisfies SchemaDeclaration;\n\nconst randomCode = () => encodeBase64Url(crypto.getRandomValues(new Uint8Array(24)));\n\n/** Telegram refuses buttons to localhost, private hosts and non-http schemes with a 400 for the whole message. */\nexport function isButtonUrlAllowed(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return url.protocol === \"tg:\";\n const host = url.hostname.toLowerCase();\n return !(\n host === \"localhost\" ||\n /\\.(localhost|local|internal|lan)$/.test(host) ||\n /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"[\")\n );\n}\n\nconst encoder = new TextEncoder();\n\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\nexport type TelegramPlugin = EasyPingPlugin<\"telegram\", typeof telegramSchema> & {\n /** Feeds one Bot API update through the linking logic. The webhook route and poll() both call it. */\n handleUpdate(update: TelegramUpdate): Promise<void>;\n /**\n * Long-polls getUpdates instead of a webhook: for development, or a host\n * with no public URL. One poller per bot token, or Telegram splits updates\n * between them. Call stop() on shutdown.\n */\n poll(options?: { logger?: Logger | undefined }): { stop(): void };\n};\n\n/** Telegram over a bot the user connects with one tap. Fans out per linked chat; blocked chats are pruned. */\nexport function telegram(options: TelegramOptions): TelegramPlugin {\n let ctx: PluginInitContext;\n const messages = { ...DEFAULT_MESSAGES, ...options.messages };\n const ttlMs = (options.linkTtlMinutes ?? 10) * 60_000;\n const maxChats = Math.max(1, options.maxChatsPerUser ?? 5);\n const botUsername = options.botUsername.replace(/^@/, \"\");\n\n const reply = async (chatId: string, text: string) => {\n const result = await options.provider.send({ chatId, text: escapeHtml(text) });\n if (!result.ok) ctx.logger.warn(`telegram reply to chat failed: ${result.error}`);\n };\n\n async function link(code: string, chat: NonNullable<TelegramUpdate[\"message\"]>[\"chat\"]) {\n const [pending] = await ctx.store.find<TelegramLink>(LINK, { code }, { limit: 1 });\n const chatId = String(chat.id);\n\n if (!pending || pending.expiresAt.getTime() < Date.now()) {\n if (pending) await ctx.store.remove(LINK, { code });\n await reply(chatId, messages.expired);\n return;\n }\n // One-time: the code is spent whether or not the rest succeeds.\n await ctx.store.remove(LINK, { code });\n\n const now = new Date();\n const details = {\n chatType: chat.type,\n username: chat.username ?? null,\n title: chat.title ?? null,\n lastSeenAt: now,\n };\n const [existing] = await ctx.store.find<TelegramChat>(CHAT, { chatId }, { limit: 1 });\n\n if (existing) {\n // Re-linking moves the chat to whoever holds the fresh code: the Telegram\n // user tapped it, and the app session that minted it is theirs.\n await ctx.store.update(\n CHAT,\n { id: existing.id },\n { userId: pending.userId, linkedAt: now, ...details },\n );\n } else {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: pending.userId },\n { orderBy: { field: \"lastSeenAt\", direction: \"asc\" } },\n );\n const excess = chats.length - maxChats + 1;\n if (excess > 0) {\n await ctx.store.remove(CHAT, { id: { in: chats.slice(0, excess).map((row) => row.id) } });\n }\n await ctx.store.insert(CHAT, [\n { id: crypto.randomUUID(), userId: pending.userId, chatId, linkedAt: now, ...details },\n ]);\n }\n await reply(chatId, messages.connected);\n }\n\n async function handleUpdate(update: TelegramUpdate): Promise<void> {\n const member = update.my_chat_member;\n if (member && [\"kicked\", \"left\"].includes(member.new_chat_member.status)) {\n await ctx.store.remove(CHAT, { chatId: String(member.chat.id) });\n return;\n }\n\n const message = update.message;\n if (!message?.text) return;\n const chatId = String(message.chat.id);\n const [command, argument] = message.text.trim().split(/\\s+/, 2);\n // \"/start@botname\" arrives in groups.\n const name = command?.split(\"@\")[0];\n\n if (name === \"/start\") {\n if (argument) await link(argument, message.chat);\n else await reply(chatId, messages.start);\n return;\n }\n if (name === \"/stop\") {\n const removed = await ctx.store.remove(CHAT, { chatId });\n if (removed > 0) await reply(chatId, messages.disconnected);\n }\n }\n\n const routes: RouteDefinition[] = [\n {\n path: \"/telegram/link\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const owner = userId ?? \"\";\n const now = Date.now();\n // A user's stale codes go, so the table cannot fill with abandoned links.\n await ctx.store.remove(LINK, { userId: owner, expiresAt: { lt: new Date(now) } });\n\n const code = randomCode();\n const expiresAt = new Date(now + ttlMs);\n await ctx.store.insert(LINK, [\n { code, userId: owner, createdAt: new Date(now), expiresAt },\n ]);\n return Response.json({\n url: telegramStartLink(botUsername, code),\n code,\n expiresAt: expiresAt.toISOString(),\n });\n },\n },\n {\n path: \"/telegram/chats\",\n method: \"GET\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: userId ?? \"\" },\n { orderBy: { field: \"linkedAt\", direction: \"desc\" } },\n );\n return Response.json({\n chats: chats.map((chat) => ({\n chatId: chat.chatId,\n chatType: chat.chatType,\n username: chat.username,\n title: chat.title,\n linkedAt: chat.linkedAt.toISOString(),\n })),\n });\n },\n },\n {\n path: \"/telegram/unlink\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n const { chatId } = parsed.body;\n const owner = userId ?? \"\";\n // Scoped by userId too: a chat id is not a secret.\n const removed = await ctx.store.remove(\n CHAT,\n typeof chatId === \"string\" && chatId !== \"\"\n ? { userId: owner, chatId }\n : { userId: owner },\n );\n return Response.json({ removed });\n },\n },\n ];\n\n if (options.webhookSecret) {\n const secret = options.webhookSecret;\n routes.push({\n path: \"/telegram/webhook\",\n method: \"POST\",\n scope: {\n type: \"custom\",\n justification:\n \"called by Telegram, authenticated by the X-Telegram-Bot-Api-Secret-Token header set through setWebhook\",\n },\n handler: async ({ request }) => {\n const presented = request.headers.get(\"x-telegram-bot-api-secret-token\") ?? \"\";\n if (!(await timingSafeEqual(presented, secret))) return new Response(null, { status: 401 });\n\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n try {\n await handleUpdate(parsed.body as unknown as TelegramUpdate);\n } catch (error) {\n // Telegram retries non-2xx for hours; a bad update is logged and acknowledged.\n ctx.logger.error(\"telegram update failed\", { error });\n }\n return Response.json({ ok: true });\n },\n });\n }\n\n return {\n id: \"telegram\",\n channels: [\"telegram\"],\n schema: telegramSchema,\n\n init: (context) => {\n ctx = context;\n },\n\n hooks: {\n deliver: async (context: DeliverContext): Promise<DeliveryOutcome> => {\n const chats = await ctx.store.find<TelegramChat>(CHAT, {\n userId: context.notification.userId,\n });\n if (chats.length === 0) return { result: \"skipped\", reason: \"no linked telegram chat\" };\n\n const rendered = options.render({\n type: context.notification.type,\n payload: context.notification.payload,\n });\n let button = rendered.button;\n if (button && !isButtonUrlAllowed(button.url)) {\n ctx.logger.warn(\n `telegram: dropped button \"${button.url}\"; Telegram accepts only public http(s) links`,\n );\n button = undefined;\n }\n\n const results = await Promise.all(\n chats.map((chat) =>\n options.provider.send({\n chatId: chat.chatId,\n text: rendered.text,\n buttons: button ? [button] : undefined,\n signal: context.signal,\n }),\n ),\n );\n\n const prune: string[] = [];\n let delivered = 0;\n let retryable = false;\n let lastError = \"\";\n results.forEach((result, index) => {\n const chat = chats[index];\n if (!chat) return;\n if (result.ok) {\n delivered += 1;\n return;\n }\n lastError = result.error;\n if (result.gone) prune.push(chat.id);\n else if (result.retryable) retryable = true;\n });\n\n if (prune.length > 0) await ctx.store.remove(CHAT, { id: { in: prune } });\n if (delivered > 0) return { result: \"sent\" };\n if (prune.length === chats.length) {\n return { result: \"failed\", error: \"every linked chat is gone\", retryable: false };\n }\n return { result: \"failed\", error: lastError || \"telegram refused the message\", retryable };\n },\n },\n\n routes,\n handleUpdate,\n\n poll(pollOptions = {}) {\n const logger = pollOptions.logger ?? ctx.logger;\n const controller = new AbortController();\n let offset: number | undefined;\n\n void (async () => {\n while (!controller.signal.aborted) {\n try {\n const updates = await options.provider.getUpdates({\n offset,\n signal: controller.signal,\n });\n for (const update of updates) {\n offset = update.update_id + 1;\n await handleUpdate(update).catch((error) =>\n logger.error(\"telegram update failed\", { error }),\n );\n }\n } catch (error) {\n if (controller.signal.aborted) return;\n logger.error(\"telegram polling failed\", { error });\n await new Promise((resolve) => setTimeout(resolve, 3_000));\n }\n }\n })();\n\n return { stop: () => controller.abort() };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/core/base64url.ts","../../src/core/handler.ts","../../src/core/html.ts","../../src/providers/telegram/index.ts","../../src/plugins/telegram/index.ts"],"names":["encoder"],"mappings":";;;AAEO,SAAS,gBAAgB,KAAA,EAAoC;AAClE,EAAA,MAAM,KAAA,GAAQ,OAAO,KAAA,KAAU,QAAA,GAAW,IAAI,WAAA,EAAY,CAAE,MAAA,CAAO,KAAK,CAAA,GAAI,KAAA;AAC5E,EAAA,IAAI,MAAA,GAAS,EAAA;AACb,EAAA,KAAA,MAAW,IAAA,IAAQ,KAAA,EAAO,MAAA,IAAU,MAAA,CAAO,aAAa,IAAI,CAAA;AAC5D,EAAA,OAAO,IAAA,CAAK,MAAM,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA,CAAE,OAAA,CAAQ,OAAO,EAAE,CAAA;AAC/E;;;ACwCO,IAAM,iBAAiB,EAAA,GAAK,IAAA;AAOnC,IAAM,eAAA,GAA0C;AAAA,EAC9C,eAAA,EAAiB,mBAAA;AAAA,EACjB,wBAAA,EAA0B;AAC5B,CAAA;AAEA,IAAM,IAAA,GAAO,CAAC,IAAA,EAAe,MAAA,GAAS,GAAA,EAAK,OAAA,GAAkC,EAAC,KAC5E,IAAI,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAG;AAAA,EACjC,MAAA;AAAA,EACA,SAAS,EAAE,cAAA,EAAgB,oBAAoB,GAAG,eAAA,EAAiB,GAAG,OAAA;AACxE,CAAC,CAAA;AAiBa,IAAI,WAAA;AAyBpB,eAAsB,YAAA,CACpB,OAAA,EACA,QAAA,GAAmB,cAAA,EACA;AACnB,EAAA,MAAM,QAAA,GAAW,OAAO,EAAE,KAAA,EAAO,IAAA,CAAK,EAAE,KAAA,EAAO,CAAA,aAAA,EAAgB,QAAQ,CAAA,MAAA,CAAA,EAAS,EAAG,GAAG,CAAA,EAAE,CAAA;AAExF,EAAA,MAAM,WAAW,MAAA,CAAO,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,gBAAgB,CAAC,CAAA;AAC7D,EAAA,IAAI,OAAO,QAAA,CAAS,QAAQ,KAAK,QAAA,GAAW,QAAA,SAAiB,QAAA,EAAS;AACtE,EAAA,IAAI,CAAC,OAAA,CAAQ,IAAA,SAAa,EAAE,IAAA,EAAM,EAAC,EAAE;AAErC,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,IAAA,CAAK,SAAA,EAAU;AACtC,EAAA,MAAM,SAAuB,EAAC;AAC9B,EAAA,IAAI,KAAA,GAAQ,CAAA;AAEZ,EAAA,WAAS;AACP,IAAA,MAAM,EAAE,IAAA,EAAM,KAAA,EAAM,GAAI,MAAM,OAAO,IAAA,EAAK;AAC1C,IAAA,IAAI,IAAA,EAAM;AACV,IAAA,KAAA,IAAS,KAAA,CAAM,UAAA;AACf,IAAA,IAAI,QAAQ,QAAA,EAAU;AACpB,MAAA,MAAM,OAAO,MAAA,EAAO;AACpB,MAAA,OAAO,QAAA,EAAS;AAAA,IAClB;AACA,IAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,EACnB;AAEA,EAAA,MAAM,KAAA,GAAQ,IAAI,UAAA,CAAW,KAAK,CAAA;AAClC,EAAA,IAAI,MAAA,GAAS,CAAA;AACb,EAAA,KAAA,MAAW,SAAS,MAAA,EAAQ;AAC1B,IAAA,KAAA,CAAM,GAAA,CAAI,OAAO,MAAM,CAAA;AACvB,IAAA,MAAA,IAAU,KAAA,CAAM,UAAA;AAAA,EAClB;AAEA,EAAA,MAAM,IAAA,GAAO,IAAI,WAAA,EAAY,CAAE,OAAO,KAAK,CAAA;AAC3C,EAAA,IAAI,IAAA,CAAK,MAAK,KAAM,EAAA,SAAW,EAAE,IAAA,EAAM,EAAC,EAAE;AAE1C,EAAA,IAAI;AACF,IAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,IAAI,CAAA;AACvC,IAAA,OAAO;AAAA,MACL,IAAA,EACE,MAAA,IAAU,OAAO,MAAA,KAAW,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GACxD,MAAA,GACD;AAAC,KACT;AAAA,EACF,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,EAAE,OAAO,IAAA,CAAK,EAAE,OAAO,4BAAA,EAA6B,EAAG,GAAG,CAAA,EAAE;AAAA,EACrE;AACF;;;ACvJA,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,CAAA;;;ACuDlE,IAAM,iBAAA,GAAoB,CAAC,WAAA,EAAqB,IAAA,KACrD,CAAA,aAAA,EAAgB,WAAA,CAAY,OAAA,CAAQ,IAAA,EAAM,EAAE,CAAC,CAAA,OAAA,EAAU,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;;;AChDjF,IAAM,IAAA,GAAO,4BAAA;AACb,IAAM,IAAA,GAAO,4BAAA;AA+Cb,IAAM,gBAAA,GAAmB;AAAA,EACvB,SAAA,EAAW,iDAAA;AAAA,EACX,YAAA,EAAc,iEAAA;AAAA,EACd,OAAA,EAAS,iEAAA;AAAA,EACT,KAAA,EAAO;AACT,CAAA;AAGO,IAAM,cAAA,GAAiB;AAAA,EAC5B,YAAA,EAAc;AAAA,IACZ,SAAA,EAAW,IAAA;AAAA,IACX,MAAA,EAAQ;AAAA,MACN,EAAA,EAAI,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACrC,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,QAAA,EAAU,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MAC3C,QAAA,EAAU,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MAC3B,KAAA,EAAO,EAAE,IAAA,EAAM,QAAA,EAAS;AAAA,MACxB,UAAU,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA,EAAK;AAAA,MAC3D,YAAY,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA;AAAK,KAC/D;AAAA,IACA,UAAA,EAAY,CAAC,IAAI,CAAA;AAAA,IACjB,OAAA,EAAS;AAAA,MACP,EAAE,EAAA,EAAI,CAAC,QAAQ,CAAA,EAAG,MAAM,wBAAA,EAAyB;AAAA;AAAA,MAEjD,EAAE,IAAI,CAAC,QAAQ,GAAG,MAAA,EAAQ,IAAA,EAAM,MAAM,wBAAA;AAAyB;AACjE,GACF;AAAA,EACA,YAAA,EAAc;AAAA,IACZ,SAAA,EAAW,IAAA;AAAA,IACX,MAAA,EAAQ;AAAA,MACN,IAAA,EAAM,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACvC,MAAA,EAAQ,EAAE,IAAA,EAAM,QAAA,EAAU,UAAU,IAAA,EAAK;AAAA,MACzC,WAAW,EAAE,IAAA,EAAM,QAAQ,QAAA,EAAU,IAAA,EAAM,YAAY,IAAA,EAAK;AAAA,MAC5D,SAAA,EAAW,EAAE,IAAA,EAAM,MAAA,EAAQ,UAAU,IAAA;AAAK,KAC5C;AAAA,IACA,UAAA,EAAY,CAAC,MAAM,CAAA;AAAA,IACnB,OAAA,EAAS,CAAC,EAAE,EAAA,EAAI,CAAC,QAAQ,CAAA,EAAG,IAAA,EAAM,wBAAA,EAA0B;AAAA;AAEhE;AAEA,IAAM,QAAA,GAAW,CAAC,KAAA,KACf,OAAO,UAAU,QAAA,IAAY,MAAA,CAAO,aAAA,CAAc,KAAK,KACvD,OAAO,KAAA,KAAU,QAAA,IAAY,cAAA,CAAe,KAAK,KAAK,CAAA;AAEzD,IAAM,MAAA,GAAS,CAAC,KAAA,KAA4B;AAC1C,EAAA,MAAM,IAAA,GAAO,KAAA;AAMb,EAAA,OACE,OAAO,IAAA,KAAS,QAAA,IAChB,IAAA,KAAS,IAAA,IACT,SAAS,IAAA,CAAK,EAAE,CAAA,IAChB,OAAO,IAAA,CAAK,IAAA,KAAS,aACpB,IAAA,CAAK,QAAA,KAAa,MAAA,IAAa,OAAO,IAAA,CAAK,QAAA,KAAa,QAAA,CAAA,KACxD,IAAA,CAAK,KAAA,KAAU,MAAA,IAAa,OAAO,IAAA,CAAK,KAAA,KAAU,QAAA,CAAA;AAEvD,CAAA;AAEA,SAAS,aAAa,MAAA,EAA2C;AAC/D,EAAA,IAAI,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,MAAM,OAAO,KAAA;AAC1D,EAAA,MAAM,EAAE,OAAA,EAAS,cAAA,EAAgB,MAAA,EAAO,GAAI,MAAA;AAC5C,EAAA,IAAI,YAAY,MAAA,EAAW;AACzB,IAAA,MAAM,CAAA,GAAI,OAAA;AACV,IAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,CAAC,MAAA,CAAO,CAAA,CAAE,IAAI,CAAA,EAAG,OAAO,KAAA;AACnE,IAAA,IAAI,EAAE,IAAA,KAAS,MAAA,IAAa,OAAO,CAAA,CAAE,IAAA,KAAS,UAAU,OAAO,KAAA;AAAA,EACjE;AACA,EAAA,IAAI,WAAW,MAAA,EAAW;AACxB,IAAA,MAAM,CAAA,GAAI,MAAA;AACV,IAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,KAAM,IAAA,IAAQ,CAAC,MAAA,CAAO,CAAA,CAAE,IAAI,CAAA,EAAG,OAAO,KAAA;AACnE,IAAA,IAAI,OAAO,CAAA,CAAE,eAAA,EAAiB,MAAA,KAAW,UAAU,OAAO,KAAA;AAAA,EAC5D;AACA,EAAA,OAAO,IAAA;AACT;AAEA,IAAM,UAAA,GAAa,MAAM,eAAA,CAAgB,MAAA,CAAO,gBAAgB,IAAI,UAAA,CAAW,EAAE,CAAC,CAAC,CAAA;AAG5E,SAAS,mBAAmB,KAAA,EAAwB;AACzD,EAAA,IAAI,GAAA;AACJ,EAAA,IAAI;AACF,IAAA,GAAA,GAAM,IAAI,IAAI,KAAK,CAAA;AAAA,EACrB,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,KAAA;AAAA,EACT;AACA,EAAA,IAAI,GAAA,CAAI,aAAa,QAAA,IAAY,GAAA,CAAI,aAAa,OAAA,EAAS,OAAO,IAAI,QAAA,KAAa,KAAA;AACnF,EAAA,MAAM,IAAA,GAAO,GAAA,CAAI,QAAA,CAAS,WAAA,EAAY;AACtC,EAAA,OAAO,EACL,IAAA,KAAS,WAAA,IACT,mCAAA,CAAoC,IAAA,CAAK,IAAI,CAAA,IAC7C,sBAAA,CAAuB,IAAA,CAAK,IAAI,CAAA,IAChC,IAAA,CAAK,WAAW,GAAG,CAAA,CAAA;AAEvB;AAEA,IAAMA,QAAAA,GAAU,IAAI,WAAA,EAAY;AAEhC,eAAe,eAAA,CAAgB,GAAW,CAAA,EAA6B;AACrE,EAAA,MAAM,CAAC,IAAA,EAAM,KAAK,CAAA,GAAI,MAAM,OAAA,CAAQ,GAAA;AAAA,IAClC,CAAC,CAAA,EAAG,CAAC,CAAA,CAAE,GAAA;AAAA,MACL,OAAO,KAAA,KAAU,IAAI,UAAA,CAAW,MAAM,MAAA,CAAO,MAAA,CAAO,MAAA,CAAO,SAAA,EAAWA,QAAAA,CAAQ,MAAA,CAAO,KAAK,CAAC,CAAC;AAAA;AAC9F,GACF;AACA,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,EAAA,EAAI,CAAA,IAAK,CAAA,EAAG,IAAA,IAAA,CAAS,IAAA,GAAO,CAAC,CAAA,IAAK,CAAA,KAAM,KAAA,GAAQ,CAAC,CAAA,IAAK,CAAA,CAAA;AAC1E,EAAA,OAAO,IAAA,KAAS,CAAA;AAClB;AAcO,SAAS,SAAS,OAAA,EAA0C;AACjE,EAAA,IAAI,GAAA;AACJ,EAAA,MAAM,WAAW,EAAE,GAAG,gBAAA,EAAkB,GAAG,QAAQ,QAAA,EAAS;AAC5D,EAAA,MAAM,KAAA,GAAA,CAAS,OAAA,CAAQ,cAAA,IAAkB,EAAA,IAAM,GAAA;AAC/C,EAAA,MAAM,WAAW,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,OAAA,CAAQ,mBAAmB,CAAC,CAAA;AACzD,EAAA,MAAM,WAAA,GAAc,OAAA,CAAQ,WAAA,CAAY,OAAA,CAAQ,MAAM,EAAE,CAAA;AAExD,EAAA,MAAM,KAAA,GAAQ,OAAO,MAAA,EAAgB,IAAA,KAAiB;AACpD,IAAA,MAAM,MAAA,GAAS,MAAM,OAAA,CAAQ,QAAA,CAAS,IAAA,CAAK,EAAE,MAAA,EAAQ,IAAA,EAAM,UAAA,CAAW,IAAI,CAAA,EAAG,CAAA;AAC7E,IAAA,IAAI,CAAC,OAAO,EAAA,EAAI,GAAA,CAAI,OAAO,IAAA,CAAK,CAAA,+BAAA,EAAkC,MAAA,CAAO,KAAK,CAAA,CAAE,CAAA;AAAA,EAClF,CAAA;AAEA,EAAA,eAAe,IAAA,CAAK,MAAc,IAAA,EAAsD;AACtF,IAAA,MAAM,CAAC,OAAO,CAAA,GAAI,MAAM,IAAI,KAAA,CAAM,IAAA,CAAmB,IAAA,EAAM,EAAE,IAAA,EAAK,EAAG,EAAE,KAAA,EAAO,GAAG,CAAA;AACjF,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA;AAE7B,IAAA,IAAI,CAAC,WAAW,OAAA,CAAQ,SAAA,CAAU,SAAQ,GAAI,IAAA,CAAK,KAAI,EAAG;AACxD,MAAA,IAAI,OAAA,QAAe,GAAA,CAAI,KAAA,CAAM,OAAO,IAAA,EAAM,EAAE,MAAM,CAAA;AAClD,MAAA,MAAM,KAAA,CAAM,MAAA,EAAQ,QAAA,CAAS,OAAO,CAAA;AACpC,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,EAAE,MAAM,CAAA;AAErC,IAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,IAAA,MAAM,OAAA,GAAU;AAAA,MACd,UAAU,IAAA,CAAK,IAAA;AAAA,MACf,QAAA,EAAU,KAAK,QAAA,IAAY,IAAA;AAAA,MAC3B,KAAA,EAAO,KAAK,KAAA,IAAS,IAAA;AAAA,MACrB,UAAA,EAAY;AAAA,KACd;AACA,IAAA,MAAM,CAAC,QAAQ,CAAA,GAAI,MAAM,IAAI,KAAA,CAAM,IAAA,CAAmB,IAAA,EAAM,EAAE,MAAA,EAAO,EAAG,EAAE,KAAA,EAAO,GAAG,CAAA;AAEpF,IAAA,IAAI,QAAA,EAAU;AAGZ,MAAA,MAAM,IAAI,KAAA,CAAM,MAAA;AAAA,QACd,IAAA;AAAA,QACA,EAAE,EAAA,EAAI,QAAA,CAAS,EAAA,EAAG;AAAA,QAClB,EAAE,MAAA,EAAQ,OAAA,CAAQ,QAAQ,QAAA,EAAU,GAAA,EAAK,GAAG,OAAA;AAAQ,OACtD;AAAA,IACF,CAAA,MAAO;AACL,MAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,KAAA,CAAM,IAAA;AAAA,QAC5B,IAAA;AAAA,QACA,EAAE,MAAA,EAAQ,OAAA,CAAQ,MAAA,EAAO;AAAA,QACzB,EAAE,OAAA,EAAS,EAAE,OAAO,YAAA,EAAc,SAAA,EAAW,OAAM;AAAE,OACvD;AACA,MAAA,MAAM,MAAA,GAAS,KAAA,CAAM,MAAA,GAAS,QAAA,GAAW,CAAA;AACzC,MAAA,IAAI,SAAS,CAAA,EAAG;AACd,QAAA,MAAM,GAAA,CAAI,MAAM,MAAA,CAAO,IAAA,EAAM,EAAE,EAAA,EAAI,EAAE,IAAI,KAAA,CAAM,KAAA,CAAM,GAAG,MAAM,CAAA,CAAE,IAAI,CAAC,GAAA,KAAQ,IAAI,EAAE,CAAA,IAAK,CAAA;AAAA,MAC1F;AACA,MAAA,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM;AAAA,QAC3B,EAAE,EAAA,EAAI,MAAA,CAAO,UAAA,EAAW,EAAG,MAAA,EAAQ,OAAA,CAAQ,MAAA,EAAQ,MAAA,EAAQ,QAAA,EAAU,GAAA,EAAK,GAAG,OAAA;AAAQ,OACtF,CAAA;AAAA,IACH;AACA,IAAA,MAAM,KAAA,CAAM,MAAA,EAAQ,QAAA,CAAS,SAAS,CAAA;AAAA,EACxC;AAEA,EAAA,eAAe,aAAa,MAAA,EAAuC;AACjE,IAAA,IAAI,CAAC,YAAA,CAAa,MAAM,CAAA,EAAG;AAC3B,IAAA,MAAM,SAAS,MAAA,CAAO,cAAA;AACtB,IAAA,IAAI,MAAA,IAAU,CAAC,QAAA,EAAU,MAAM,EAAE,QAAA,CAAS,MAAA,CAAO,eAAA,CAAgB,MAAM,CAAA,EAAG;AACxE,MAAA,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,EAAE,MAAA,EAAQ,MAAA,CAAO,MAAA,CAAO,IAAA,CAAK,EAAE,CAAA,EAAG,CAAA;AAC/D,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,UAAU,MAAA,CAAO,OAAA;AACvB,IAAA,IAAI,CAAC,SAAS,IAAA,EAAM;AACpB,IAAA,MAAM,MAAA,GAAS,MAAA,CAAO,OAAA,CAAQ,IAAA,CAAK,EAAE,CAAA;AACrC,IAAA,MAAM,CAAC,OAAA,EAAS,QAAQ,CAAA,GAAI,OAAA,CAAQ,KAAK,IAAA,EAAK,CAAE,KAAA,CAAM,KAAA,EAAO,CAAC,CAAA;AAE9D,IAAA,MAAM,IAAA,GAAO,OAAA,EAAS,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AAElC,IAAA,IAAI,SAAS,QAAA,EAAU;AACrB,MAAA,IAAI,QAAA,EAAU,MAAM,IAAA,CAAK,QAAA,EAAU,QAAQ,IAAI,CAAA;AAAA,WAC1C,MAAM,KAAA,CAAM,MAAA,EAAQ,QAAA,CAAS,KAAK,CAAA;AACvC,MAAA;AAAA,IACF;AACA,IAAA,IAAI,SAAS,OAAA,EAAS;AACpB,MAAA,MAAM,OAAA,GAAU,MAAM,GAAA,CAAI,KAAA,CAAM,OAAO,IAAA,EAAM,EAAE,QAAQ,CAAA;AACvD,MAAA,IAAI,UAAU,CAAA,EAAG,MAAM,KAAA,CAAM,MAAA,EAAQ,SAAS,YAAY,CAAA;AAAA,IAC5D;AAAA,EACF;AAEA,EAAA,MAAM,MAAA,GAA4B;AAAA,IAChC;AAAA,MACE,IAAA,EAAM,gBAAA;AAAA,MACN,MAAA,EAAQ,MAAA;AAAA,MACR,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACtB,OAAA,EAAS,OAAO,EAAE,MAAA,EAAO,KAAM;AAC7B,QAAA,MAAM,QAAQ,MAAA,IAAU,EAAA;AACxB,QAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,QAAA,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,MAAM,EAAE,MAAA,EAAQ,OAAO,CAAA;AAE9C,QAAA,MAAM,OAAO,UAAA,EAAW;AACxB,QAAA,MAAM,SAAA,GAAY,IAAI,IAAA,CAAK,GAAA,GAAM,KAAK,CAAA;AACtC,QAAA,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM;AAAA,UAC3B,EAAE,MAAM,MAAA,EAAQ,KAAA,EAAO,WAAW,IAAI,IAAA,CAAK,GAAG,CAAA,EAAG,SAAA;AAAU,SAC5D,CAAA;AACD,QAAA,OAAO,SAAS,IAAA,CAAK;AAAA,UACnB,GAAA,EAAK,iBAAA,CAAkB,WAAA,EAAa,IAAI,CAAA;AAAA,UACxC,IAAA;AAAA,UACA,SAAA,EAAW,UAAU,WAAA;AAAY,SAClC,CAAA;AAAA,MACH;AAAA,KACF;AAAA,IACA;AAAA,MACE,IAAA,EAAM,iBAAA;AAAA,MACN,MAAA,EAAQ,KAAA;AAAA,MACR,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACtB,OAAA,EAAS,OAAO,EAAE,MAAA,EAAO,KAAM;AAC7B,QAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,KAAA,CAAM,IAAA;AAAA,UAC5B,IAAA;AAAA,UACA,EAAE,MAAA,EAAQ,MAAA,IAAU,EAAA,EAAG;AAAA,UACvB,EAAE,OAAA,EAAS,EAAE,OAAO,UAAA,EAAY,SAAA,EAAW,QAAO;AAAE,SACtD;AACA,QAAA,OAAO,SAAS,IAAA,CAAK;AAAA,UACnB,KAAA,EAAO,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,MAAU;AAAA,YAC1B,QAAQ,IAAA,CAAK,MAAA;AAAA,YACb,UAAU,IAAA,CAAK,QAAA;AAAA,YACf,UAAU,IAAA,CAAK,QAAA;AAAA,YACf,OAAO,IAAA,CAAK,KAAA;AAAA,YACZ,QAAA,EAAU,IAAA,CAAK,QAAA,CAAS,WAAA;AAAY,WACtC,CAAE;AAAA,SACH,CAAA;AAAA,MACH;AAAA,KACF;AAAA,IACA;AAAA,MACE,IAAA,EAAM,kBAAA;AAAA,MACN,MAAA,EAAQ,MAAA;AAAA,MACR,KAAA,EAAO,EAAE,IAAA,EAAM,MAAA,EAAO;AAAA,MACtB,OAAA,EAAS,OAAO,EAAE,OAAA,EAAS,QAAO,KAAM;AACtC,QAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,OAAO,CAAA;AACzC,QAAA,IAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,MAAA,CAAO,KAAA;AACrC,QAAA,MAAM,EAAE,MAAA,EAAO,GAAI,MAAA,CAAO,IAAA;AAC1B,QAAA,MAAM,QAAQ,MAAA,IAAU,EAAA;AAExB,QAAA,MAAM,OAAA,GAAU,MAAM,GAAA,CAAI,KAAA,CAAM,MAAA;AAAA,UAC9B,IAAA;AAAA,UACA,OAAO,MAAA,KAAW,QAAA,IAAY,MAAA,KAAW,EAAA,GACrC,EAAE,MAAA,EAAQ,KAAA,EAAO,MAAA,EAAO,GACxB,EAAE,MAAA,EAAQ,KAAA;AAAM,SACtB;AACA,QAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,OAAA,EAAS,CAAA;AAAA,MAClC;AAAA;AACF,GACF;AAEA,EAAA,IAAI,QAAQ,aAAA,EAAe;AACzB,IAAA,MAAM,SAAS,OAAA,CAAQ,aAAA;AACvB,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,IAAA,EAAM,mBAAA;AAAA,MACN,MAAA,EAAQ,MAAA;AAAA,MACR,KAAA,EAAO;AAAA,QACL,IAAA,EAAM,QAAA;AAAA,QACN,aAAA,EACE;AAAA,OACJ;AAAA,MACA,OAAA,EAAS,OAAO,EAAE,OAAA,EAAQ,KAAM;AAC9B,QAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,iCAAiC,CAAA,IAAK,EAAA;AAC5E,QAAA,IAAI,CAAE,MAAM,eAAA,CAAgB,SAAA,EAAW,MAAM,CAAA,EAAI,OAAO,IAAI,QAAA,CAAS,IAAA,EAAM,EAAE,MAAA,EAAQ,KAAK,CAAA;AAE1F,QAAA,MAAM,MAAA,GAAS,MAAM,YAAA,CAAa,OAAO,CAAA;AACzC,QAAA,IAAI,OAAA,IAAW,MAAA,EAAQ,OAAO,MAAA,CAAO,KAAA;AACrC,QAAA,IAAI;AACF,UAAA,MAAM,YAAA,CAAa,OAAO,IAAiC,CAAA;AAAA,QAC7D,SAAS,KAAA,EAAO;AAEd,UAAA,GAAA,CAAI,MAAA,CAAO,KAAA,CAAM,wBAAA,EAA0B,EAAE,OAAO,CAAA;AAAA,QACtD;AACA,QAAA,OAAO,QAAA,CAAS,IAAA,CAAK,EAAE,EAAA,EAAI,MAAM,CAAA;AAAA,MACnC;AAAA,KACD,CAAA;AAAA,EACH;AAEA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,UAAA;AAAA,IACJ,QAAA,EAAU,CAAC,UAAU,CAAA;AAAA,IACrB,MAAA,EAAQ,cAAA;AAAA,IAER,IAAA,EAAM,CAAC,OAAA,KAAY;AACjB,MAAA,GAAA,GAAM,OAAA;AAAA,IACR,CAAA;AAAA,IAEA,KAAA,EAAO;AAAA,MACL,OAAA,EAAS,OAAO,OAAA,KAAsD;AACpE,QAAA,MAAM,KAAA,GAAQ,MAAM,GAAA,CAAI,KAAA,CAAM,KAAmB,IAAA,EAAM;AAAA,UACrD,MAAA,EAAQ,QAAQ,YAAA,CAAa;AAAA,SAC9B,CAAA;AACD,QAAA,IAAI,KAAA,CAAM,WAAW,CAAA,EAAG,OAAO,EAAE,MAAA,EAAQ,SAAA,EAAW,QAAQ,yBAAA,EAA0B;AAEtF,QAAA,MAAM,QAAA,GAAW,QAAQ,MAAA,CAAO;AAAA,UAC9B,IAAA,EAAM,QAAQ,YAAA,CAAa,IAAA;AAAA,UAC3B,OAAA,EAAS,QAAQ,YAAA,CAAa;AAAA,SAC/B,CAAA;AACD,QAAA,IAAI,SAAS,QAAA,CAAS,MAAA;AACtB,QAAA,IAAI,MAAA,IAAU,CAAC,kBAAA,CAAmB,MAAA,CAAO,GAAG,CAAA,EAAG;AAC7C,UAAA,GAAA,CAAI,MAAA,CAAO,IAAA;AAAA,YACT,CAAA,0BAAA,EAA6B,OAAO,GAAG,CAAA,6CAAA;AAAA,WACzC;AACA,UAAA,MAAA,GAAS,MAAA;AAAA,QACX;AAEA,QAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,GAAA;AAAA,UAC5B,KAAA,CAAM,GAAA;AAAA,YAAI,CAAC,IAAA,KACT,OAAA,CAAQ,QAAA,CAAS,IAAA,CAAK;AAAA,cACpB,QAAQ,IAAA,CAAK,MAAA;AAAA,cACb,MAAM,QAAA,CAAS,IAAA;AAAA,cACf,OAAA,EAAS,MAAA,GAAS,CAAC,MAAM,CAAA,GAAI,MAAA;AAAA,cAC7B,QAAQ,OAAA,CAAQ;AAAA,aACjB;AAAA;AACH,SACF;AAEA,QAAA,MAAM,QAAkB,EAAC;AACzB,QAAA,IAAI,SAAA,GAAY,CAAA;AAChB,QAAA,IAAI,SAAA,GAAY,KAAA;AAChB,QAAA,IAAI,SAAA,GAAY,EAAA;AAChB,QAAA,OAAA,CAAQ,OAAA,CAAQ,CAAC,MAAA,EAAQ,KAAA,KAAU;AACjC,UAAA,MAAM,IAAA,GAAO,MAAM,KAAK,CAAA;AACxB,UAAA,IAAI,CAAC,IAAA,EAAM;AACX,UAAA,IAAI,OAAO,EAAA,EAAI;AACb,YAAA,SAAA,IAAa,CAAA;AACb,YAAA;AAAA,UACF;AACA,UAAA,SAAA,GAAY,MAAA,CAAO,KAAA;AACnB,UAAA,IAAI,MAAA,CAAO,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,KAAK,EAAE,CAAA;AAAA,eAAA,IAC1B,MAAA,CAAO,WAAW,SAAA,GAAY,IAAA;AAAA,QACzC,CAAC,CAAA;AAED,QAAA,IAAI,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,MAAA,CAAO,IAAA,EAAM,EAAE,EAAA,EAAI,EAAE,EAAA,EAAI,KAAA,IAAS,CAAA;AACxE,QAAA,IAAI,SAAA,GAAY,CAAA,EAAG,OAAO,EAAE,QAAQ,MAAA,EAAO;AAC3C,QAAA,IAAI,KAAA,CAAM,MAAA,KAAW,KAAA,CAAM,MAAA,EAAQ;AACjC,UAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,2BAAA,EAA6B,WAAW,KAAA,EAAM;AAAA,QAClF;AACA,QAAA,OAAO,EAAE,MAAA,EAAQ,QAAA,EAAU,KAAA,EAAO,SAAA,IAAa,gCAAgC,SAAA,EAAU;AAAA,MAC3F;AAAA,KACF;AAAA,IAEA,MAAA;AAAA,IACA,YAAA;AAAA,IAEA,IAAA,CAAK,WAAA,GAAc,EAAC,EAAG;AACrB,MAAA,MAAM,MAAA,GAAS,WAAA,CAAY,MAAA,IAAU,GAAA,CAAI,MAAA;AACzC,MAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,MAAA,IAAI,MAAA;AAEJ,MAAA,KAAA,CAAM,YAAY;AAChB,QAAA,OAAO,CAAC,UAAA,CAAW,MAAA,CAAO,OAAA,EAAS;AACjC,UAAA,IAAI;AACF,YAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,QAAA,CAAS,UAAA,CAAW;AAAA,cAChD,MAAA;AAAA,cACA,QAAQ,UAAA,CAAW;AAAA,aACpB,CAAA;AACD,YAAA,KAAA,MAAW,UAAU,OAAA,EAAS;AAC5B,cAAA,MAAA,GAAS,OAAO,SAAA,GAAY,CAAA;AAC5B,cAAA,MAAM,YAAA,CAAa,MAAM,CAAA,CAAE,KAAA;AAAA,gBAAM,CAAC,KAAA,KAChC,MAAA,CAAO,MAAM,wBAAA,EAA0B,EAAE,OAAO;AAAA,eAClD;AAAA,YACF;AAAA,UACF,SAAS,KAAA,EAAO;AACd,YAAA,IAAI,UAAA,CAAW,OAAO,OAAA,EAAS;AAC/B,YAAA,MAAA,CAAO,KAAA,CAAM,yBAAA,EAA2B,EAAE,KAAA,EAAO,CAAA;AACjD,YAAA,MAAM,IAAI,OAAA,CAAQ,CAAC,YAAY,UAAA,CAAW,OAAA,EAAS,GAAK,CAAC,CAAA;AAAA,UAC3D;AAAA,QACF;AAAA,MACF,CAAA,GAAG;AAEH,MAAA,OAAO,EAAE,IAAA,EAAM,MAAM,UAAA,CAAW,OAAM,EAAE;AAAA,IAC1C;AAAA,GACF;AACF","file":"telegram.cjs","sourcesContent":["// Web APIs, not Buffer: this runs on Workers and Vercel Edge.\n\nexport function encodeBase64Url(input: string | Uint8Array): string {\n const bytes = typeof input === \"string\" ? new TextEncoder().encode(input) : input;\n let binary = \"\";\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\n// Uint8Array<ArrayBuffer>: bare Uint8Array widens and is not a BufferSource.\nexport function decodeBase64UrlBytes(value: string): Uint8Array<ArrayBuffer> | null {\n try {\n const binary = atob(value.replace(/-/g, \"+\").replace(/_/g, \"/\"));\n return Uint8Array.from(binary, (char) => char.charCodeAt(0));\n } catch {\n return null;\n }\n}\n\nexport function decodeBase64Url(value: string): string | null {\n const bytes = decodeBase64UrlBytes(value);\n return bytes ? new TextDecoder().decode(bytes) : null;\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\nconst SESSION_ERROR: unique symbol = Symbol(\"session-error\");\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 let warnedBadUserId = false;\n\n async function resolveUserId(request: Request): Promise<string | null | typeof SESSION_ERROR> {\n let id: unknown;\n try {\n id = 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 SESSION_ERROR;\n }\n if (id === null || id === undefined) return null;\n if (typeof id !== \"string\" || id === \"\") {\n if (!warnedBadUserId) {\n warnedBadUserId = true;\n deps.logger.warn(\n `session.getUserId returned ${id === \"\" ? \"an empty string\" : typeof id}; treating the ` +\n \"request as unauthenticated. Return a non-empty string or null.\",\n );\n }\n return null;\n }\n return id;\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 === SESSION_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 === SESSION_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","const ESCAPES: Record<string, string> = {\n \"&\": \"&\",\n \"<\": \"<\",\n \">\": \">\",\n '\"': \""\",\n \"'\": \"'\",\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","export type TelegramButton = { text: string; url: string };\n\nexport type TelegramMessage = {\n chatId: string;\n /** Telegram HTML: <b>, <i>, <a href>, <code>, <pre>. Escape user text with escapeHtml(). */\n text: string;\n buttons?: readonly TelegramButton[] | undefined;\n signal?: AbortSignal | undefined;\n};\n\nexport type TelegramSendResult =\n | { ok: true; messageId: number }\n | {\n ok: false;\n error: string;\n /** Worth another attempt: rate limit, 5xx, network. */\n retryable: boolean;\n /** The chat can never receive again (user blocked the bot, chat deleted). Prune it. */\n gone: boolean;\n retryAfterSeconds?: number | undefined;\n };\n\n/** The slice of a Bot API update the plugin reads. Everything else passes through untouched. */\nexport type TelegramUpdate = {\n update_id: number;\n message?: {\n text?: string;\n chat: { id: number | string; type: string; username?: string; title?: string };\n from?: { id: number; username?: string; first_name?: string };\n };\n my_chat_member?: {\n chat: { id: number | string; type: string; username?: string; title?: string };\n new_chat_member: { status: string };\n };\n};\n\nexport type TelegramProvider = {\n name: string;\n send(message: TelegramMessage): Promise<TelegramSendResult>;\n /** Long-polls for updates. For development or hosts without a public URL. */\n getUpdates(options: {\n offset?: number | undefined;\n timeoutSeconds?: number | undefined;\n signal?: AbortSignal | undefined;\n }): Promise<TelegramUpdate[]>;\n /** Points Telegram at your webhook route. Run once per deployment URL. */\n setWebhook(url: string, secretToken: string): Promise<void>;\n deleteWebhook(): Promise<void>;\n getMe(): Promise<{ id: number; username: string }>;\n};\n\nexport type TelegramBotOptions = {\n /** From @BotFather. Never appears in errors or logs. */\n token: string;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://api.telegram.org. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ApiResponse<T> =\n | { ok: true; result: T }\n | { ok: false; description?: string; error_code?: number; parameters?: { retry_after?: number } };\n\n/** Deep link that opens the bot with a start parameter. Codes are [A-Za-z0-9_-], at most 64 chars. */\nexport const telegramStartLink = (botUsername: string, code: string) =>\n `https://t.me/${botUsername.replace(/^@/, \"\")}?start=${encodeURIComponent(code)}`;\n\n/** Bot API over fetch, so it runs on edge runtimes. Failures are classified, never thrown, on send. */\nexport function telegramBot(options: TelegramBotOptions): TelegramProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = `${(options.apiBase ?? \"https://api.telegram.org\").replace(/\\/$/, \"\")}/bot${options.token}`;\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) => text.split(options.token).join(\"<token>\");\n\n async function call<T>(\n method: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n budgetMs = timeoutMs,\n ): Promise<{ status: number; data: ApiResponse<T> }> {\n const timeout = AbortSignal.timeout(budgetMs);\n const merged = signal ? AbortSignal.any([signal, timeout]) : timeout;\n const response = await doFetch(`${base}/${method}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: merged,\n });\n const data = (await response.json().catch(() => ({ ok: false }))) as ApiResponse<T>;\n return { status: response.status, data };\n }\n\n async function must<T>(method: string, body: Record<string, unknown>): Promise<T> {\n const { data } = await call<T>(method, body);\n if (!data.ok)\n throw new Error(redact(`telegram ${method} failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n }\n\n return {\n name: \"telegram\",\n\n async send(message) {\n const body: Record<string, unknown> = {\n chat_id: message.chatId,\n text: message.text,\n parse_mode: \"HTML\",\n link_preview_options: { is_disabled: true },\n };\n if (message.buttons?.length) {\n body.reply_markup = {\n inline_keyboard: [\n message.buttons.map((button) => ({ text: button.text, url: button.url })),\n ],\n };\n }\n\n let status: number;\n let data: ApiResponse<{ message_id: number }>;\n try {\n ({ status, data } = await call<{ message_id: number }>(\n \"sendMessage\",\n body,\n message.signal,\n ));\n } catch (error) {\n const text = error instanceof Error ? error.message : String(error);\n return { ok: false, error: redact(text), retryable: true, gone: false };\n }\n\n if (data.ok) return { ok: true, messageId: data.result.message_id };\n\n const description = redact(data.description ?? `HTTP ${status}`);\n const code = data.error_code ?? status;\n if (code === 429) {\n return {\n ok: false,\n error: description,\n retryable: true,\n gone: false,\n retryAfterSeconds: data.parameters?.retry_after,\n };\n }\n // 403: the user blocked the bot or was deactivated. 400 \"chat not found\": deleted or never existed.\n const gone =\n code === 403 ||\n (code === 400 && /chat not found|user is deactivated|bot was kicked/i.test(description));\n return { ok: false, error: description, retryable: code >= 500, gone };\n },\n\n async getUpdates({ offset, timeoutSeconds = 25, signal }) {\n // Telegram holds the request open for `timeout` seconds; the budget must outlive it.\n const { data } = await call<TelegramUpdate[]>(\n \"getUpdates\",\n {\n ...(offset === undefined ? {} : { offset }),\n timeout: timeoutSeconds,\n allowed_updates: [\"message\", \"my_chat_member\"],\n },\n signal,\n (timeoutSeconds + 10) * 1000,\n );\n if (!data.ok)\n throw new Error(redact(`telegram getUpdates failed: ${data.description ?? \"unknown\"}`));\n return data.result;\n },\n\n setWebhook: async (url, secretToken) => {\n await must(\"setWebhook\", {\n url,\n secret_token: secretToken,\n allowed_updates: [\"message\", \"my_chat_member\"],\n });\n },\n deleteWebhook: async () => {\n await must(\"deleteWebhook\", {});\n },\n getMe: () => must(\"getMe\", {}),\n };\n}\n","import type { DeliveryOutcome } from \"../../core/adapter\";\nimport { encodeBase64Url } from \"../../core/base64url\";\nimport type { Logger } from \"../../core/errors\";\nimport { readJsonBody } from \"../../core/handler\";\nimport { escapeHtml } from \"../../core/html\";\nimport type {\n DeliverContext,\n EasyPingPlugin,\n PluginInitContext,\n RouteDefinition,\n SchemaDeclaration,\n} from \"../../core/plugin\";\nimport {\n type TelegramProvider,\n type TelegramUpdate,\n telegramStartLink,\n} from \"../../providers/telegram\";\n\nconst CHAT = \"notification_telegram_chat\";\nconst LINK = \"notification_telegram_link\";\n\nexport type TelegramChat = {\n id: string;\n userId: string;\n chatId: string;\n /** \"private\", \"group\", \"supergroup\" or \"channel\". */\n chatType: string;\n username: string | null;\n title: string | null;\n linkedAt: Date;\n lastSeenAt: Date;\n};\n\nexport type TelegramLink = {\n code: string;\n userId: string;\n createdAt: Date;\n expiresAt: Date;\n};\n\nexport type TelegramRendered = {\n /** Telegram HTML. Anything user-typed inside it goes through escapeHtml(). */\n text: string;\n /** Adds one inline button under the message. */\n button?: { text: string; url: string } | undefined;\n};\n\nexport type TelegramOptions = {\n provider: TelegramProvider;\n /** Without the @. Builds the https://t.me/<bot>?start=<code> link. */\n botUsername: string;\n render: (input: { type: string; payload: unknown }) => TelegramRendered;\n /**\n * Shared with Telegram through setWebhook; Telegram echoes it in\n * X-Telegram-Bot-Api-Secret-Token and the webhook route accepts nothing else.\n * Unset, the route is not mounted and updates must come through poll().\n */\n webhookSecret?: string | undefined;\n /** How long a \"Connect Telegram\" link stays valid. Default 10 minutes. */\n linkTtlMinutes?: number | undefined;\n /** Linking past this evicts the user's least recently seen chat. Default 5. */\n maxChatsPerUser?: number | undefined;\n /** Bot replies, if the defaults are not your voice. */\n messages?: Partial<typeof DEFAULT_MESSAGES> | undefined;\n};\n\nconst DEFAULT_MESSAGES = {\n connected: \"Connected. You will receive notifications here.\",\n disconnected: \"Disconnected. You will not receive notifications here any more.\",\n expired: \"That link has expired. Open the app and connect Telegram again.\",\n start: \"Open the app and use “Connect Telegram” to link this chat.\",\n};\n\n/** The tables this plugin owns, exported so DDL needs no plugin instance. */\nexport const telegramSchema = {\n telegramChat: {\n tableName: CHAT,\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n chatId: { type: \"string\", required: true },\n chatType: { type: \"string\", required: true },\n username: { type: \"string\" },\n title: { type: \"string\" },\n linkedAt: { type: \"date\", required: true, defaultNow: true },\n lastSeenAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\"], name: \"telegram_chat_user_idx\" },\n // A chat belongs to one account at a time; re-linking moves it.\n { on: [\"chatId\"], unique: true, name: \"telegram_chat_chat_idx\" },\n ],\n },\n telegramLink: {\n tableName: LINK,\n fields: {\n code: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n expiresAt: { type: \"date\", required: true },\n },\n primaryKey: [\"code\"],\n indexes: [{ on: [\"userId\"], name: \"telegram_link_user_idx\" }],\n },\n} as const satisfies SchemaDeclaration;\n\nconst isChatId = (value: unknown) =>\n (typeof value === \"number\" && Number.isSafeInteger(value)) ||\n (typeof value === \"string\" && /^-?\\d{1,20}$/.test(value));\n\nconst isChat = (value: unknown): boolean => {\n const chat = value as {\n id?: unknown;\n type?: unknown;\n username?: unknown;\n title?: unknown;\n } | null;\n return (\n typeof chat === \"object\" &&\n chat !== null &&\n isChatId(chat.id) &&\n typeof chat.type === \"string\" &&\n (chat.username === undefined || typeof chat.username === \"string\") &&\n (chat.title === undefined || typeof chat.title === \"string\")\n );\n};\n\nfunction isWellFormed(update: unknown): update is TelegramUpdate {\n if (typeof update !== \"object\" || update === null) return false;\n const { message, my_chat_member: member } = update as Record<string, unknown>;\n if (message !== undefined) {\n const m = message as { text?: unknown; chat?: unknown } | null;\n if (typeof m !== \"object\" || m === null || !isChat(m.chat)) return false;\n if (m.text !== undefined && typeof m.text !== \"string\") return false;\n }\n if (member !== undefined) {\n const c = member as { chat?: unknown; new_chat_member?: { status?: unknown } } | null;\n if (typeof c !== \"object\" || c === null || !isChat(c.chat)) return false;\n if (typeof c.new_chat_member?.status !== \"string\") return false;\n }\n return true;\n}\n\nconst randomCode = () => encodeBase64Url(crypto.getRandomValues(new Uint8Array(24)));\n\n/** Telegram refuses buttons to localhost, private hosts and non-http schemes with a 400 for the whole message. */\nexport function isButtonUrlAllowed(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return url.protocol === \"tg:\";\n const host = url.hostname.toLowerCase();\n return !(\n host === \"localhost\" ||\n /\\.(localhost|local|internal|lan)$/.test(host) ||\n /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"[\")\n );\n}\n\nconst encoder = new TextEncoder();\n\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\nexport type TelegramPlugin = EasyPingPlugin<\"telegram\", typeof telegramSchema> & {\n /** Feeds one Bot API update through the linking logic. The webhook route and poll() both call it. */\n handleUpdate(update: TelegramUpdate): Promise<void>;\n /**\n * Long-polls getUpdates instead of a webhook: for development, or a host\n * with no public URL. One poller per bot token, or Telegram splits updates\n * between them. Call stop() on shutdown.\n */\n poll(options?: { logger?: Logger | undefined }): { stop(): void };\n};\n\n/** Telegram over a bot the user connects with one tap. Fans out per linked chat; blocked chats are pruned. */\nexport function telegram(options: TelegramOptions): TelegramPlugin {\n let ctx: PluginInitContext;\n const messages = { ...DEFAULT_MESSAGES, ...options.messages };\n const ttlMs = (options.linkTtlMinutes ?? 10) * 60_000;\n const maxChats = Math.max(1, options.maxChatsPerUser ?? 5);\n const botUsername = options.botUsername.replace(/^@/, \"\");\n\n const reply = async (chatId: string, text: string) => {\n const result = await options.provider.send({ chatId, text: escapeHtml(text) });\n if (!result.ok) ctx.logger.warn(`telegram reply to chat failed: ${result.error}`);\n };\n\n async function link(code: string, chat: NonNullable<TelegramUpdate[\"message\"]>[\"chat\"]) {\n const [pending] = await ctx.store.find<TelegramLink>(LINK, { code }, { limit: 1 });\n const chatId = String(chat.id);\n\n if (!pending || pending.expiresAt.getTime() < Date.now()) {\n if (pending) await ctx.store.remove(LINK, { code });\n await reply(chatId, messages.expired);\n return;\n }\n // One-time: the code is spent whether or not the rest succeeds.\n await ctx.store.remove(LINK, { code });\n\n const now = new Date();\n const details = {\n chatType: chat.type,\n username: chat.username ?? null,\n title: chat.title ?? null,\n lastSeenAt: now,\n };\n const [existing] = await ctx.store.find<TelegramChat>(CHAT, { chatId }, { limit: 1 });\n\n if (existing) {\n // Re-linking moves the chat to whoever holds the fresh code: the Telegram\n // user tapped it, and the app session that minted it is theirs.\n await ctx.store.update(\n CHAT,\n { id: existing.id },\n { userId: pending.userId, linkedAt: now, ...details },\n );\n } else {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: pending.userId },\n { orderBy: { field: \"lastSeenAt\", direction: \"asc\" } },\n );\n const excess = chats.length - maxChats + 1;\n if (excess > 0) {\n await ctx.store.remove(CHAT, { id: { in: chats.slice(0, excess).map((row) => row.id) } });\n }\n await ctx.store.insert(CHAT, [\n { id: crypto.randomUUID(), userId: pending.userId, chatId, linkedAt: now, ...details },\n ]);\n }\n await reply(chatId, messages.connected);\n }\n\n async function handleUpdate(update: TelegramUpdate): Promise<void> {\n if (!isWellFormed(update)) return;\n const member = update.my_chat_member;\n if (member && [\"kicked\", \"left\"].includes(member.new_chat_member.status)) {\n await ctx.store.remove(CHAT, { chatId: String(member.chat.id) });\n return;\n }\n\n const message = update.message;\n if (!message?.text) return;\n const chatId = String(message.chat.id);\n const [command, argument] = message.text.trim().split(/\\s+/, 2);\n // \"/start@botname\" arrives in groups.\n const name = command?.split(\"@\")[0];\n\n if (name === \"/start\") {\n if (argument) await link(argument, message.chat);\n else await reply(chatId, messages.start);\n return;\n }\n if (name === \"/stop\") {\n const removed = await ctx.store.remove(CHAT, { chatId });\n if (removed > 0) await reply(chatId, messages.disconnected);\n }\n }\n\n const routes: RouteDefinition[] = [\n {\n path: \"/telegram/link\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const owner = userId ?? \"\";\n const now = Date.now();\n await ctx.store.remove(LINK, { userId: owner });\n\n const code = randomCode();\n const expiresAt = new Date(now + ttlMs);\n await ctx.store.insert(LINK, [\n { code, userId: owner, createdAt: new Date(now), expiresAt },\n ]);\n return Response.json({\n url: telegramStartLink(botUsername, code),\n code,\n expiresAt: expiresAt.toISOString(),\n });\n },\n },\n {\n path: \"/telegram/chats\",\n method: \"GET\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: userId ?? \"\" },\n { orderBy: { field: \"linkedAt\", direction: \"desc\" } },\n );\n return Response.json({\n chats: chats.map((chat) => ({\n chatId: chat.chatId,\n chatType: chat.chatType,\n username: chat.username,\n title: chat.title,\n linkedAt: chat.linkedAt.toISOString(),\n })),\n });\n },\n },\n {\n path: \"/telegram/unlink\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n const { chatId } = parsed.body;\n const owner = userId ?? \"\";\n // Scoped by userId too: a chat id is not a secret.\n const removed = await ctx.store.remove(\n CHAT,\n typeof chatId === \"string\" && chatId !== \"\"\n ? { userId: owner, chatId }\n : { userId: owner },\n );\n return Response.json({ removed });\n },\n },\n ];\n\n if (options.webhookSecret) {\n const secret = options.webhookSecret;\n routes.push({\n path: \"/telegram/webhook\",\n method: \"POST\",\n scope: {\n type: \"custom\",\n justification:\n \"called by Telegram, authenticated by the X-Telegram-Bot-Api-Secret-Token header set through setWebhook\",\n },\n handler: async ({ request }) => {\n const presented = request.headers.get(\"x-telegram-bot-api-secret-token\") ?? \"\";\n if (!(await timingSafeEqual(presented, secret))) return new Response(null, { status: 401 });\n\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n try {\n await handleUpdate(parsed.body as unknown as TelegramUpdate);\n } catch (error) {\n // Telegram retries non-2xx for hours; a bad update is logged and acknowledged.\n ctx.logger.error(\"telegram update failed\", { error });\n }\n return Response.json({ ok: true });\n },\n });\n }\n\n return {\n id: \"telegram\",\n channels: [\"telegram\"],\n schema: telegramSchema,\n\n init: (context) => {\n ctx = context;\n },\n\n hooks: {\n deliver: async (context: DeliverContext): Promise<DeliveryOutcome> => {\n const chats = await ctx.store.find<TelegramChat>(CHAT, {\n userId: context.notification.userId,\n });\n if (chats.length === 0) return { result: \"skipped\", reason: \"no linked telegram chat\" };\n\n const rendered = options.render({\n type: context.notification.type,\n payload: context.notification.payload,\n });\n let button = rendered.button;\n if (button && !isButtonUrlAllowed(button.url)) {\n ctx.logger.warn(\n `telegram: dropped button \"${button.url}\"; Telegram accepts only public http(s) links`,\n );\n button = undefined;\n }\n\n const results = await Promise.all(\n chats.map((chat) =>\n options.provider.send({\n chatId: chat.chatId,\n text: rendered.text,\n buttons: button ? [button] : undefined,\n signal: context.signal,\n }),\n ),\n );\n\n const prune: string[] = [];\n let delivered = 0;\n let retryable = false;\n let lastError = \"\";\n results.forEach((result, index) => {\n const chat = chats[index];\n if (!chat) return;\n if (result.ok) {\n delivered += 1;\n return;\n }\n lastError = result.error;\n if (result.gone) prune.push(chat.id);\n else if (result.retryable) retryable = true;\n });\n\n if (prune.length > 0) await ctx.store.remove(CHAT, { id: { in: prune } });\n if (delivered > 0) return { result: \"sent\" };\n if (prune.length === chats.length) {\n return { result: \"failed\", error: \"every linked chat is gone\", retryable: false };\n }\n return { result: \"failed\", error: lastError || \"telegram refused the message\", retryable };\n },\n },\n\n routes,\n handleUpdate,\n\n poll(pollOptions = {}) {\n const logger = pollOptions.logger ?? ctx.logger;\n const controller = new AbortController();\n let offset: number | undefined;\n\n void (async () => {\n while (!controller.signal.aborted) {\n try {\n const updates = await options.provider.getUpdates({\n offset,\n signal: controller.signal,\n });\n for (const update of updates) {\n offset = update.update_id + 1;\n await handleUpdate(update).catch((error) =>\n logger.error(\"telegram update failed\", { error }),\n );\n }\n } catch (error) {\n if (controller.signal.aborted) return;\n logger.error(\"telegram polling failed\", { error });\n await new Promise((resolve) => setTimeout(resolve, 3_000));\n }\n }\n })();\n\n return { stop: () => controller.abort() };\n },\n };\n}\n"]}
|
package/dist/plugins/telegram.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { telegramStartLink } from '../chunk-SSHVZHSJ.js';
|
|
2
2
|
import { escapeHtml } from '../chunk-R3BE67L7.js';
|
|
3
|
-
import { readJsonBody } from '../chunk-
|
|
3
|
+
import { readJsonBody } from '../chunk-CFLVAF2R.js';
|
|
4
4
|
import '../chunk-ORZKH224.js';
|
|
5
5
|
import { encodeBase64Url } from '../chunk-PTGHMCCG.js';
|
|
6
6
|
|
|
@@ -45,6 +45,26 @@ var telegramSchema = {
|
|
|
45
45
|
indexes: [{ on: ["userId"], name: "telegram_link_user_idx" }]
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
+
var isChatId = (value) => typeof value === "number" && Number.isSafeInteger(value) || typeof value === "string" && /^-?\d{1,20}$/.test(value);
|
|
49
|
+
var isChat = (value) => {
|
|
50
|
+
const chat = value;
|
|
51
|
+
return typeof chat === "object" && chat !== null && isChatId(chat.id) && typeof chat.type === "string" && (chat.username === void 0 || typeof chat.username === "string") && (chat.title === void 0 || typeof chat.title === "string");
|
|
52
|
+
};
|
|
53
|
+
function isWellFormed(update) {
|
|
54
|
+
if (typeof update !== "object" || update === null) return false;
|
|
55
|
+
const { message, my_chat_member: member } = update;
|
|
56
|
+
if (message !== void 0) {
|
|
57
|
+
const m = message;
|
|
58
|
+
if (typeof m !== "object" || m === null || !isChat(m.chat)) return false;
|
|
59
|
+
if (m.text !== void 0 && typeof m.text !== "string") return false;
|
|
60
|
+
}
|
|
61
|
+
if (member !== void 0) {
|
|
62
|
+
const c = member;
|
|
63
|
+
if (typeof c !== "object" || c === null || !isChat(c.chat)) return false;
|
|
64
|
+
if (typeof c.new_chat_member?.status !== "string") return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
48
68
|
var randomCode = () => encodeBase64Url(crypto.getRandomValues(new Uint8Array(24)));
|
|
49
69
|
function isButtonUrlAllowed(value) {
|
|
50
70
|
let url;
|
|
@@ -118,6 +138,7 @@ function telegram(options) {
|
|
|
118
138
|
await reply(chatId, messages.connected);
|
|
119
139
|
}
|
|
120
140
|
async function handleUpdate(update) {
|
|
141
|
+
if (!isWellFormed(update)) return;
|
|
121
142
|
const member = update.my_chat_member;
|
|
122
143
|
if (member && ["kicked", "left"].includes(member.new_chat_member.status)) {
|
|
123
144
|
await ctx.store.remove(CHAT, { chatId: String(member.chat.id) });
|
|
@@ -146,7 +167,7 @@ function telegram(options) {
|
|
|
146
167
|
handler: async ({ userId }) => {
|
|
147
168
|
const owner = userId ?? "";
|
|
148
169
|
const now = Date.now();
|
|
149
|
-
await ctx.store.remove(LINK, { userId: owner
|
|
170
|
+
await ctx.store.remove(LINK, { userId: owner });
|
|
150
171
|
const code = randomCode();
|
|
151
172
|
const expiresAt = new Date(now + ttlMs);
|
|
152
173
|
await ctx.store.insert(LINK, [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/telegram/index.ts"],"names":[],"mappings":"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","file":"telegram.js","sourcesContent":["import type { DeliveryOutcome } from \"../../core/adapter\";\nimport { encodeBase64Url } from \"../../core/base64url\";\nimport type { Logger } from \"../../core/errors\";\nimport { readJsonBody } from \"../../core/handler\";\nimport { escapeHtml } from \"../../core/html\";\nimport type {\n DeliverContext,\n EasyPingPlugin,\n PluginInitContext,\n RouteDefinition,\n SchemaDeclaration,\n} from \"../../core/plugin\";\nimport {\n type TelegramProvider,\n type TelegramUpdate,\n telegramStartLink,\n} from \"../../providers/telegram\";\n\nconst CHAT = \"notification_telegram_chat\";\nconst LINK = \"notification_telegram_link\";\n\nexport type TelegramChat = {\n id: string;\n userId: string;\n chatId: string;\n /** \"private\", \"group\", \"supergroup\" or \"channel\". */\n chatType: string;\n username: string | null;\n title: string | null;\n linkedAt: Date;\n lastSeenAt: Date;\n};\n\nexport type TelegramLink = {\n code: string;\n userId: string;\n createdAt: Date;\n expiresAt: Date;\n};\n\nexport type TelegramRendered = {\n /** Telegram HTML. Anything user-typed inside it goes through escapeHtml(). */\n text: string;\n /** Adds one inline button under the message. */\n button?: { text: string; url: string } | undefined;\n};\n\nexport type TelegramOptions = {\n provider: TelegramProvider;\n /** Without the @. Builds the https://t.me/<bot>?start=<code> link. */\n botUsername: string;\n render: (input: { type: string; payload: unknown }) => TelegramRendered;\n /**\n * Shared with Telegram through setWebhook; Telegram echoes it in\n * X-Telegram-Bot-Api-Secret-Token and the webhook route accepts nothing else.\n * Unset, the route is not mounted and updates must come through poll().\n */\n webhookSecret?: string | undefined;\n /** How long a \"Connect Telegram\" link stays valid. Default 10 minutes. */\n linkTtlMinutes?: number | undefined;\n /** Linking past this evicts the user's least recently seen chat. Default 5. */\n maxChatsPerUser?: number | undefined;\n /** Bot replies, if the defaults are not your voice. */\n messages?: Partial<typeof DEFAULT_MESSAGES> | undefined;\n};\n\nconst DEFAULT_MESSAGES = {\n connected: \"Connected. You will receive notifications here.\",\n disconnected: \"Disconnected. You will not receive notifications here any more.\",\n expired: \"That link has expired. Open the app and connect Telegram again.\",\n start: \"Open the app and use “Connect Telegram” to link this chat.\",\n};\n\n/** The tables this plugin owns, exported so DDL needs no plugin instance. */\nexport const telegramSchema = {\n telegramChat: {\n tableName: CHAT,\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n chatId: { type: \"string\", required: true },\n chatType: { type: \"string\", required: true },\n username: { type: \"string\" },\n title: { type: \"string\" },\n linkedAt: { type: \"date\", required: true, defaultNow: true },\n lastSeenAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\"], name: \"telegram_chat_user_idx\" },\n // A chat belongs to one account at a time; re-linking moves it.\n { on: [\"chatId\"], unique: true, name: \"telegram_chat_chat_idx\" },\n ],\n },\n telegramLink: {\n tableName: LINK,\n fields: {\n code: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n expiresAt: { type: \"date\", required: true },\n },\n primaryKey: [\"code\"],\n indexes: [{ on: [\"userId\"], name: \"telegram_link_user_idx\" }],\n },\n} as const satisfies SchemaDeclaration;\n\nconst randomCode = () => encodeBase64Url(crypto.getRandomValues(new Uint8Array(24)));\n\n/** Telegram refuses buttons to localhost, private hosts and non-http schemes with a 400 for the whole message. */\nexport function isButtonUrlAllowed(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return url.protocol === \"tg:\";\n const host = url.hostname.toLowerCase();\n return !(\n host === \"localhost\" ||\n /\\.(localhost|local|internal|lan)$/.test(host) ||\n /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"[\")\n );\n}\n\nconst encoder = new TextEncoder();\n\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\nexport type TelegramPlugin = EasyPingPlugin<\"telegram\", typeof telegramSchema> & {\n /** Feeds one Bot API update through the linking logic. The webhook route and poll() both call it. */\n handleUpdate(update: TelegramUpdate): Promise<void>;\n /**\n * Long-polls getUpdates instead of a webhook: for development, or a host\n * with no public URL. One poller per bot token, or Telegram splits updates\n * between them. Call stop() on shutdown.\n */\n poll(options?: { logger?: Logger | undefined }): { stop(): void };\n};\n\n/** Telegram over a bot the user connects with one tap. Fans out per linked chat; blocked chats are pruned. */\nexport function telegram(options: TelegramOptions): TelegramPlugin {\n let ctx: PluginInitContext;\n const messages = { ...DEFAULT_MESSAGES, ...options.messages };\n const ttlMs = (options.linkTtlMinutes ?? 10) * 60_000;\n const maxChats = Math.max(1, options.maxChatsPerUser ?? 5);\n const botUsername = options.botUsername.replace(/^@/, \"\");\n\n const reply = async (chatId: string, text: string) => {\n const result = await options.provider.send({ chatId, text: escapeHtml(text) });\n if (!result.ok) ctx.logger.warn(`telegram reply to chat failed: ${result.error}`);\n };\n\n async function link(code: string, chat: NonNullable<TelegramUpdate[\"message\"]>[\"chat\"]) {\n const [pending] = await ctx.store.find<TelegramLink>(LINK, { code }, { limit: 1 });\n const chatId = String(chat.id);\n\n if (!pending || pending.expiresAt.getTime() < Date.now()) {\n if (pending) await ctx.store.remove(LINK, { code });\n await reply(chatId, messages.expired);\n return;\n }\n // One-time: the code is spent whether or not the rest succeeds.\n await ctx.store.remove(LINK, { code });\n\n const now = new Date();\n const details = {\n chatType: chat.type,\n username: chat.username ?? null,\n title: chat.title ?? null,\n lastSeenAt: now,\n };\n const [existing] = await ctx.store.find<TelegramChat>(CHAT, { chatId }, { limit: 1 });\n\n if (existing) {\n // Re-linking moves the chat to whoever holds the fresh code: the Telegram\n // user tapped it, and the app session that minted it is theirs.\n await ctx.store.update(\n CHAT,\n { id: existing.id },\n { userId: pending.userId, linkedAt: now, ...details },\n );\n } else {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: pending.userId },\n { orderBy: { field: \"lastSeenAt\", direction: \"asc\" } },\n );\n const excess = chats.length - maxChats + 1;\n if (excess > 0) {\n await ctx.store.remove(CHAT, { id: { in: chats.slice(0, excess).map((row) => row.id) } });\n }\n await ctx.store.insert(CHAT, [\n { id: crypto.randomUUID(), userId: pending.userId, chatId, linkedAt: now, ...details },\n ]);\n }\n await reply(chatId, messages.connected);\n }\n\n async function handleUpdate(update: TelegramUpdate): Promise<void> {\n const member = update.my_chat_member;\n if (member && [\"kicked\", \"left\"].includes(member.new_chat_member.status)) {\n await ctx.store.remove(CHAT, { chatId: String(member.chat.id) });\n return;\n }\n\n const message = update.message;\n if (!message?.text) return;\n const chatId = String(message.chat.id);\n const [command, argument] = message.text.trim().split(/\\s+/, 2);\n // \"/start@botname\" arrives in groups.\n const name = command?.split(\"@\")[0];\n\n if (name === \"/start\") {\n if (argument) await link(argument, message.chat);\n else await reply(chatId, messages.start);\n return;\n }\n if (name === \"/stop\") {\n const removed = await ctx.store.remove(CHAT, { chatId });\n if (removed > 0) await reply(chatId, messages.disconnected);\n }\n }\n\n const routes: RouteDefinition[] = [\n {\n path: \"/telegram/link\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const owner = userId ?? \"\";\n const now = Date.now();\n // A user's stale codes go, so the table cannot fill with abandoned links.\n await ctx.store.remove(LINK, { userId: owner, expiresAt: { lt: new Date(now) } });\n\n const code = randomCode();\n const expiresAt = new Date(now + ttlMs);\n await ctx.store.insert(LINK, [\n { code, userId: owner, createdAt: new Date(now), expiresAt },\n ]);\n return Response.json({\n url: telegramStartLink(botUsername, code),\n code,\n expiresAt: expiresAt.toISOString(),\n });\n },\n },\n {\n path: \"/telegram/chats\",\n method: \"GET\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: userId ?? \"\" },\n { orderBy: { field: \"linkedAt\", direction: \"desc\" } },\n );\n return Response.json({\n chats: chats.map((chat) => ({\n chatId: chat.chatId,\n chatType: chat.chatType,\n username: chat.username,\n title: chat.title,\n linkedAt: chat.linkedAt.toISOString(),\n })),\n });\n },\n },\n {\n path: \"/telegram/unlink\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n const { chatId } = parsed.body;\n const owner = userId ?? \"\";\n // Scoped by userId too: a chat id is not a secret.\n const removed = await ctx.store.remove(\n CHAT,\n typeof chatId === \"string\" && chatId !== \"\"\n ? { userId: owner, chatId }\n : { userId: owner },\n );\n return Response.json({ removed });\n },\n },\n ];\n\n if (options.webhookSecret) {\n const secret = options.webhookSecret;\n routes.push({\n path: \"/telegram/webhook\",\n method: \"POST\",\n scope: {\n type: \"custom\",\n justification:\n \"called by Telegram, authenticated by the X-Telegram-Bot-Api-Secret-Token header set through setWebhook\",\n },\n handler: async ({ request }) => {\n const presented = request.headers.get(\"x-telegram-bot-api-secret-token\") ?? \"\";\n if (!(await timingSafeEqual(presented, secret))) return new Response(null, { status: 401 });\n\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n try {\n await handleUpdate(parsed.body as unknown as TelegramUpdate);\n } catch (error) {\n // Telegram retries non-2xx for hours; a bad update is logged and acknowledged.\n ctx.logger.error(\"telegram update failed\", { error });\n }\n return Response.json({ ok: true });\n },\n });\n }\n\n return {\n id: \"telegram\",\n channels: [\"telegram\"],\n schema: telegramSchema,\n\n init: (context) => {\n ctx = context;\n },\n\n hooks: {\n deliver: async (context: DeliverContext): Promise<DeliveryOutcome> => {\n const chats = await ctx.store.find<TelegramChat>(CHAT, {\n userId: context.notification.userId,\n });\n if (chats.length === 0) return { result: \"skipped\", reason: \"no linked telegram chat\" };\n\n const rendered = options.render({\n type: context.notification.type,\n payload: context.notification.payload,\n });\n let button = rendered.button;\n if (button && !isButtonUrlAllowed(button.url)) {\n ctx.logger.warn(\n `telegram: dropped button \"${button.url}\"; Telegram accepts only public http(s) links`,\n );\n button = undefined;\n }\n\n const results = await Promise.all(\n chats.map((chat) =>\n options.provider.send({\n chatId: chat.chatId,\n text: rendered.text,\n buttons: button ? [button] : undefined,\n signal: context.signal,\n }),\n ),\n );\n\n const prune: string[] = [];\n let delivered = 0;\n let retryable = false;\n let lastError = \"\";\n results.forEach((result, index) => {\n const chat = chats[index];\n if (!chat) return;\n if (result.ok) {\n delivered += 1;\n return;\n }\n lastError = result.error;\n if (result.gone) prune.push(chat.id);\n else if (result.retryable) retryable = true;\n });\n\n if (prune.length > 0) await ctx.store.remove(CHAT, { id: { in: prune } });\n if (delivered > 0) return { result: \"sent\" };\n if (prune.length === chats.length) {\n return { result: \"failed\", error: \"every linked chat is gone\", retryable: false };\n }\n return { result: \"failed\", error: lastError || \"telegram refused the message\", retryable };\n },\n },\n\n routes,\n handleUpdate,\n\n poll(pollOptions = {}) {\n const logger = pollOptions.logger ?? ctx.logger;\n const controller = new AbortController();\n let offset: number | undefined;\n\n void (async () => {\n while (!controller.signal.aborted) {\n try {\n const updates = await options.provider.getUpdates({\n offset,\n signal: controller.signal,\n });\n for (const update of updates) {\n offset = update.update_id + 1;\n await handleUpdate(update).catch((error) =>\n logger.error(\"telegram update failed\", { error }),\n );\n }\n } catch (error) {\n if (controller.signal.aborted) return;\n logger.error(\"telegram polling failed\", { error });\n await new Promise((resolve) => setTimeout(resolve, 3_000));\n }\n }\n })();\n\n return { stop: () => controller.abort() };\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/telegram/index.ts"],"names":[],"mappings":"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","file":"telegram.js","sourcesContent":["import type { DeliveryOutcome } from \"../../core/adapter\";\nimport { encodeBase64Url } from \"../../core/base64url\";\nimport type { Logger } from \"../../core/errors\";\nimport { readJsonBody } from \"../../core/handler\";\nimport { escapeHtml } from \"../../core/html\";\nimport type {\n DeliverContext,\n EasyPingPlugin,\n PluginInitContext,\n RouteDefinition,\n SchemaDeclaration,\n} from \"../../core/plugin\";\nimport {\n type TelegramProvider,\n type TelegramUpdate,\n telegramStartLink,\n} from \"../../providers/telegram\";\n\nconst CHAT = \"notification_telegram_chat\";\nconst LINK = \"notification_telegram_link\";\n\nexport type TelegramChat = {\n id: string;\n userId: string;\n chatId: string;\n /** \"private\", \"group\", \"supergroup\" or \"channel\". */\n chatType: string;\n username: string | null;\n title: string | null;\n linkedAt: Date;\n lastSeenAt: Date;\n};\n\nexport type TelegramLink = {\n code: string;\n userId: string;\n createdAt: Date;\n expiresAt: Date;\n};\n\nexport type TelegramRendered = {\n /** Telegram HTML. Anything user-typed inside it goes through escapeHtml(). */\n text: string;\n /** Adds one inline button under the message. */\n button?: { text: string; url: string } | undefined;\n};\n\nexport type TelegramOptions = {\n provider: TelegramProvider;\n /** Without the @. Builds the https://t.me/<bot>?start=<code> link. */\n botUsername: string;\n render: (input: { type: string; payload: unknown }) => TelegramRendered;\n /**\n * Shared with Telegram through setWebhook; Telegram echoes it in\n * X-Telegram-Bot-Api-Secret-Token and the webhook route accepts nothing else.\n * Unset, the route is not mounted and updates must come through poll().\n */\n webhookSecret?: string | undefined;\n /** How long a \"Connect Telegram\" link stays valid. Default 10 minutes. */\n linkTtlMinutes?: number | undefined;\n /** Linking past this evicts the user's least recently seen chat. Default 5. */\n maxChatsPerUser?: number | undefined;\n /** Bot replies, if the defaults are not your voice. */\n messages?: Partial<typeof DEFAULT_MESSAGES> | undefined;\n};\n\nconst DEFAULT_MESSAGES = {\n connected: \"Connected. You will receive notifications here.\",\n disconnected: \"Disconnected. You will not receive notifications here any more.\",\n expired: \"That link has expired. Open the app and connect Telegram again.\",\n start: \"Open the app and use “Connect Telegram” to link this chat.\",\n};\n\n/** The tables this plugin owns, exported so DDL needs no plugin instance. */\nexport const telegramSchema = {\n telegramChat: {\n tableName: CHAT,\n fields: {\n id: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n chatId: { type: \"string\", required: true },\n chatType: { type: \"string\", required: true },\n username: { type: \"string\" },\n title: { type: \"string\" },\n linkedAt: { type: \"date\", required: true, defaultNow: true },\n lastSeenAt: { type: \"date\", required: true, defaultNow: true },\n },\n primaryKey: [\"id\"],\n indexes: [\n { on: [\"userId\"], name: \"telegram_chat_user_idx\" },\n // A chat belongs to one account at a time; re-linking moves it.\n { on: [\"chatId\"], unique: true, name: \"telegram_chat_chat_idx\" },\n ],\n },\n telegramLink: {\n tableName: LINK,\n fields: {\n code: { type: \"string\", required: true },\n userId: { type: \"string\", required: true },\n createdAt: { type: \"date\", required: true, defaultNow: true },\n expiresAt: { type: \"date\", required: true },\n },\n primaryKey: [\"code\"],\n indexes: [{ on: [\"userId\"], name: \"telegram_link_user_idx\" }],\n },\n} as const satisfies SchemaDeclaration;\n\nconst isChatId = (value: unknown) =>\n (typeof value === \"number\" && Number.isSafeInteger(value)) ||\n (typeof value === \"string\" && /^-?\\d{1,20}$/.test(value));\n\nconst isChat = (value: unknown): boolean => {\n const chat = value as {\n id?: unknown;\n type?: unknown;\n username?: unknown;\n title?: unknown;\n } | null;\n return (\n typeof chat === \"object\" &&\n chat !== null &&\n isChatId(chat.id) &&\n typeof chat.type === \"string\" &&\n (chat.username === undefined || typeof chat.username === \"string\") &&\n (chat.title === undefined || typeof chat.title === \"string\")\n );\n};\n\nfunction isWellFormed(update: unknown): update is TelegramUpdate {\n if (typeof update !== \"object\" || update === null) return false;\n const { message, my_chat_member: member } = update as Record<string, unknown>;\n if (message !== undefined) {\n const m = message as { text?: unknown; chat?: unknown } | null;\n if (typeof m !== \"object\" || m === null || !isChat(m.chat)) return false;\n if (m.text !== undefined && typeof m.text !== \"string\") return false;\n }\n if (member !== undefined) {\n const c = member as { chat?: unknown; new_chat_member?: { status?: unknown } } | null;\n if (typeof c !== \"object\" || c === null || !isChat(c.chat)) return false;\n if (typeof c.new_chat_member?.status !== \"string\") return false;\n }\n return true;\n}\n\nconst randomCode = () => encodeBase64Url(crypto.getRandomValues(new Uint8Array(24)));\n\n/** Telegram refuses buttons to localhost, private hosts and non-http schemes with a 400 for the whole message. */\nexport function isButtonUrlAllowed(value: string): boolean {\n let url: URL;\n try {\n url = new URL(value);\n } catch {\n return false;\n }\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") return url.protocol === \"tg:\";\n const host = url.hostname.toLowerCase();\n return !(\n host === \"localhost\" ||\n /\\.(localhost|local|internal|lan)$/.test(host) ||\n /^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host) ||\n host.startsWith(\"[\")\n );\n}\n\nconst encoder = new TextEncoder();\n\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\nexport type TelegramPlugin = EasyPingPlugin<\"telegram\", typeof telegramSchema> & {\n /** Feeds one Bot API update through the linking logic. The webhook route and poll() both call it. */\n handleUpdate(update: TelegramUpdate): Promise<void>;\n /**\n * Long-polls getUpdates instead of a webhook: for development, or a host\n * with no public URL. One poller per bot token, or Telegram splits updates\n * between them. Call stop() on shutdown.\n */\n poll(options?: { logger?: Logger | undefined }): { stop(): void };\n};\n\n/** Telegram over a bot the user connects with one tap. Fans out per linked chat; blocked chats are pruned. */\nexport function telegram(options: TelegramOptions): TelegramPlugin {\n let ctx: PluginInitContext;\n const messages = { ...DEFAULT_MESSAGES, ...options.messages };\n const ttlMs = (options.linkTtlMinutes ?? 10) * 60_000;\n const maxChats = Math.max(1, options.maxChatsPerUser ?? 5);\n const botUsername = options.botUsername.replace(/^@/, \"\");\n\n const reply = async (chatId: string, text: string) => {\n const result = await options.provider.send({ chatId, text: escapeHtml(text) });\n if (!result.ok) ctx.logger.warn(`telegram reply to chat failed: ${result.error}`);\n };\n\n async function link(code: string, chat: NonNullable<TelegramUpdate[\"message\"]>[\"chat\"]) {\n const [pending] = await ctx.store.find<TelegramLink>(LINK, { code }, { limit: 1 });\n const chatId = String(chat.id);\n\n if (!pending || pending.expiresAt.getTime() < Date.now()) {\n if (pending) await ctx.store.remove(LINK, { code });\n await reply(chatId, messages.expired);\n return;\n }\n // One-time: the code is spent whether or not the rest succeeds.\n await ctx.store.remove(LINK, { code });\n\n const now = new Date();\n const details = {\n chatType: chat.type,\n username: chat.username ?? null,\n title: chat.title ?? null,\n lastSeenAt: now,\n };\n const [existing] = await ctx.store.find<TelegramChat>(CHAT, { chatId }, { limit: 1 });\n\n if (existing) {\n // Re-linking moves the chat to whoever holds the fresh code: the Telegram\n // user tapped it, and the app session that minted it is theirs.\n await ctx.store.update(\n CHAT,\n { id: existing.id },\n { userId: pending.userId, linkedAt: now, ...details },\n );\n } else {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: pending.userId },\n { orderBy: { field: \"lastSeenAt\", direction: \"asc\" } },\n );\n const excess = chats.length - maxChats + 1;\n if (excess > 0) {\n await ctx.store.remove(CHAT, { id: { in: chats.slice(0, excess).map((row) => row.id) } });\n }\n await ctx.store.insert(CHAT, [\n { id: crypto.randomUUID(), userId: pending.userId, chatId, linkedAt: now, ...details },\n ]);\n }\n await reply(chatId, messages.connected);\n }\n\n async function handleUpdate(update: TelegramUpdate): Promise<void> {\n if (!isWellFormed(update)) return;\n const member = update.my_chat_member;\n if (member && [\"kicked\", \"left\"].includes(member.new_chat_member.status)) {\n await ctx.store.remove(CHAT, { chatId: String(member.chat.id) });\n return;\n }\n\n const message = update.message;\n if (!message?.text) return;\n const chatId = String(message.chat.id);\n const [command, argument] = message.text.trim().split(/\\s+/, 2);\n // \"/start@botname\" arrives in groups.\n const name = command?.split(\"@\")[0];\n\n if (name === \"/start\") {\n if (argument) await link(argument, message.chat);\n else await reply(chatId, messages.start);\n return;\n }\n if (name === \"/stop\") {\n const removed = await ctx.store.remove(CHAT, { chatId });\n if (removed > 0) await reply(chatId, messages.disconnected);\n }\n }\n\n const routes: RouteDefinition[] = [\n {\n path: \"/telegram/link\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const owner = userId ?? \"\";\n const now = Date.now();\n await ctx.store.remove(LINK, { userId: owner });\n\n const code = randomCode();\n const expiresAt = new Date(now + ttlMs);\n await ctx.store.insert(LINK, [\n { code, userId: owner, createdAt: new Date(now), expiresAt },\n ]);\n return Response.json({\n url: telegramStartLink(botUsername, code),\n code,\n expiresAt: expiresAt.toISOString(),\n });\n },\n },\n {\n path: \"/telegram/chats\",\n method: \"GET\",\n scope: { type: \"user\" },\n handler: async ({ userId }) => {\n const chats = await ctx.store.find<TelegramChat>(\n CHAT,\n { userId: userId ?? \"\" },\n { orderBy: { field: \"linkedAt\", direction: \"desc\" } },\n );\n return Response.json({\n chats: chats.map((chat) => ({\n chatId: chat.chatId,\n chatType: chat.chatType,\n username: chat.username,\n title: chat.title,\n linkedAt: chat.linkedAt.toISOString(),\n })),\n });\n },\n },\n {\n path: \"/telegram/unlink\",\n method: \"POST\",\n scope: { type: \"user\" },\n handler: async ({ request, userId }) => {\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n const { chatId } = parsed.body;\n const owner = userId ?? \"\";\n // Scoped by userId too: a chat id is not a secret.\n const removed = await ctx.store.remove(\n CHAT,\n typeof chatId === \"string\" && chatId !== \"\"\n ? { userId: owner, chatId }\n : { userId: owner },\n );\n return Response.json({ removed });\n },\n },\n ];\n\n if (options.webhookSecret) {\n const secret = options.webhookSecret;\n routes.push({\n path: \"/telegram/webhook\",\n method: \"POST\",\n scope: {\n type: \"custom\",\n justification:\n \"called by Telegram, authenticated by the X-Telegram-Bot-Api-Secret-Token header set through setWebhook\",\n },\n handler: async ({ request }) => {\n const presented = request.headers.get(\"x-telegram-bot-api-secret-token\") ?? \"\";\n if (!(await timingSafeEqual(presented, secret))) return new Response(null, { status: 401 });\n\n const parsed = await readJsonBody(request);\n if (\"error\" in parsed) return parsed.error;\n try {\n await handleUpdate(parsed.body as unknown as TelegramUpdate);\n } catch (error) {\n // Telegram retries non-2xx for hours; a bad update is logged and acknowledged.\n ctx.logger.error(\"telegram update failed\", { error });\n }\n return Response.json({ ok: true });\n },\n });\n }\n\n return {\n id: \"telegram\",\n channels: [\"telegram\"],\n schema: telegramSchema,\n\n init: (context) => {\n ctx = context;\n },\n\n hooks: {\n deliver: async (context: DeliverContext): Promise<DeliveryOutcome> => {\n const chats = await ctx.store.find<TelegramChat>(CHAT, {\n userId: context.notification.userId,\n });\n if (chats.length === 0) return { result: \"skipped\", reason: \"no linked telegram chat\" };\n\n const rendered = options.render({\n type: context.notification.type,\n payload: context.notification.payload,\n });\n let button = rendered.button;\n if (button && !isButtonUrlAllowed(button.url)) {\n ctx.logger.warn(\n `telegram: dropped button \"${button.url}\"; Telegram accepts only public http(s) links`,\n );\n button = undefined;\n }\n\n const results = await Promise.all(\n chats.map((chat) =>\n options.provider.send({\n chatId: chat.chatId,\n text: rendered.text,\n buttons: button ? [button] : undefined,\n signal: context.signal,\n }),\n ),\n );\n\n const prune: string[] = [];\n let delivered = 0;\n let retryable = false;\n let lastError = \"\";\n results.forEach((result, index) => {\n const chat = chats[index];\n if (!chat) return;\n if (result.ok) {\n delivered += 1;\n return;\n }\n lastError = result.error;\n if (result.gone) prune.push(chat.id);\n else if (result.retryable) retryable = true;\n });\n\n if (prune.length > 0) await ctx.store.remove(CHAT, { id: { in: prune } });\n if (delivered > 0) return { result: \"sent\" };\n if (prune.length === chats.length) {\n return { result: \"failed\", error: \"every linked chat is gone\", retryable: false };\n }\n return { result: \"failed\", error: lastError || \"telegram refused the message\", retryable };\n },\n },\n\n routes,\n handleUpdate,\n\n poll(pollOptions = {}) {\n const logger = pollOptions.logger ?? ctx.logger;\n const controller = new AbortController();\n let offset: number | undefined;\n\n void (async () => {\n while (!controller.signal.aborted) {\n try {\n const updates = await options.provider.getUpdates({\n offset,\n signal: controller.signal,\n });\n for (const update of updates) {\n offset = update.update_id + 1;\n await handleUpdate(update).catch((error) =>\n logger.error(\"telegram update failed\", { error }),\n );\n }\n } catch (error) {\n if (controller.signal.aborted) return;\n logger.error(\"telegram polling failed\", { error });\n await new Promise((resolve) => setTimeout(resolve, 3_000));\n }\n }\n })();\n\n return { stop: () => controller.abort() };\n },\n };\n}\n"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// src/providers/expo-push/index.ts
|
|
4
|
-
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,}\]$/.test(token);
|
|
4
|
+
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,64}\]$/.test(token);
|
|
5
5
|
var EXPO_SEND_BATCH = 100;
|
|
6
6
|
var EXPO_RECEIPT_BATCH = 1e3;
|
|
7
7
|
var GONE = /* @__PURE__ */ new Set(["DeviceNotRegistered"]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/providers/expo-push/index.ts"],"names":[],"mappings":"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","file":"expo-push.cjs","sourcesContent":["export type MobilePushMessage = {\n /** The device's push token as the app registered it. */\n token: string;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n badge?: number | undefined;\n /** \"default\" plays the platform sound; null is silent. */\n sound?: \"default\" | null | undefined;\n /** Android notification channel id. */\n channelId?: string | undefined;\n};\n\nexport type MobilePushTicket =\n | { ok: true; ticketId: string | null }\n | {\n ok: false;\n error: string;\n /** The token will never work again (app uninstalled, token rotated). Prune it. */\n gone: boolean;\n retryable: boolean;\n };\n\nexport type MobilePushReceipt =\n | { ticketId: string; ok: true }\n | { ticketId: string; ok: false; error: string; gone: boolean };\n\nexport type MobilePushProvider = {\n name: string;\n /** True for a token this provider can deliver to. Rejected at registration otherwise. */\n isValidToken(token: string): boolean;\n /** One result per message, in order. Never throws for a per-token failure. */\n send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;\n /**\n * Some services (Expo) accept a message, then learn from APNs/FCM later\n * that the device is gone. Receipts carry that late verdict.\n */\n receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;\n};\n\nexport type ExpoPushOptions = {\n /** Expo \"enhanced security\" access token. Optional; when set, requests without it are refused by Expo. */\n accessToken?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://exp.host. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ExpoTicket =\n | { status: \"ok\"; id: string }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\ntype ExpoReceipt =\n | { status: \"ok\" }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\n/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */\nexport const isExpoPushToken = (token: string): boolean =>\n /^Expo(nent)?PushToken\\[[A-Za-z0-9_-]{8,}\\]$/.test(token);\n\n/** Expo's send endpoint takes at most this many messages per request. */\nexport const EXPO_SEND_BATCH = 100;\n/** And its receipts endpoint at most this many ids. */\nexport const EXPO_RECEIPT_BATCH = 1000;\n\n// Expo's error codes, per https://docs.expo.dev/push-notifications/sending-notifications/#individual-errors\nconst GONE = new Set([\"DeviceNotRegistered\"]);\nconst RETRYABLE = new Set([\"MessageRateExceeded\", \"ProviderError\", \"ExpoError\"]);\n\nconst classify = (\n code: string | undefined,\n message: string | undefined,\n): { error: string; gone: boolean; retryable: boolean } => ({\n error: code ? `${code}: ${message ?? \"\"}`.trim() : (message ?? \"expo push failed\"),\n gone: code !== undefined && GONE.has(code),\n retryable: code !== undefined && RETRYABLE.has(code),\n});\n\n/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */\nexport function expoPush(options: ExpoPushOptions = {}): MobilePushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = (options.apiBase ?? \"https://exp.host\").replace(/\\/$/, \"\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) =>\n options.accessToken ? text.split(options.accessToken).join(\"<token>\") : text;\n\n async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const timeout = AbortSignal.timeout(timeoutMs);\n const headers: Record<string, string> = {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n };\n if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;\n const response = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n });\n if (!response.ok) {\n const text = redact((await response.text().catch(() => \"\")).slice(0, 200));\n const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());\n (error as { status?: number }).status = response.status;\n throw error;\n }\n return (await response.json()) as T;\n }\n\n const transportFailure = (error: unknown): MobilePushTicket => {\n const status = (error as { status?: number })?.status;\n const message = redact(error instanceof Error ? error.message : String(error));\n // 4xx from the service itself (bad access token, malformed request) cannot be fixed by retrying.\n const retryable = status === undefined || status === 429 || status >= 500;\n return { ok: false, error: message, gone: false, retryable };\n };\n\n return {\n name: \"expo-push\",\n isValidToken: isExpoPushToken,\n\n async send(messages, signal) {\n const results: MobilePushTicket[] = [];\n for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {\n const batch = messages.slice(start, start + EXPO_SEND_BATCH);\n let tickets: ExpoTicket[];\n try {\n const response = await post<{ data: ExpoTicket[] }>(\n \"/--/api/v2/push/send\",\n batch.map((message) => ({\n to: message.token,\n title: message.title,\n body: message.body,\n ...(message.data ? { data: message.data } : {}),\n ...(message.badge === undefined ? {} : { badge: message.badge }),\n ...(message.sound === undefined ? { sound: \"default\" } : { sound: message.sound }),\n ...(message.channelId ? { channelId: message.channelId } : {}),\n })),\n signal,\n );\n tickets = response.data;\n } catch (error) {\n const failure = transportFailure(error);\n for (const _ of batch) results.push(failure);\n continue;\n }\n batch.forEach((_, index) => {\n const ticket = tickets[index];\n if (!ticket) {\n results.push({ ok: false, error: \"no ticket returned\", gone: false, retryable: true });\n } else if (ticket.status === \"ok\") {\n results.push({ ok: true, ticketId: ticket.id ?? null });\n } else {\n results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });\n }\n });\n }\n return results;\n },\n\n async receipts(ticketIds, signal) {\n const out: MobilePushReceipt[] = [];\n for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {\n const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);\n const response = await post<{ data: Record<string, ExpoReceipt> }>(\n \"/--/api/v2/push/getReceipts\",\n { ids },\n signal,\n );\n for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {\n if (receipt.status === \"ok\") out.push({ ticketId, ok: true });\n else {\n const { error, gone } = classify(receipt.details?.error, receipt.message);\n out.push({ ticketId, ok: false, error, gone });\n }\n }\n }\n return out;\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/providers/expo-push/index.ts"],"names":[],"mappings":"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","file":"expo-push.cjs","sourcesContent":["export type MobilePushMessage = {\n /** The device's push token as the app registered it. */\n token: string;\n title: string;\n body: string;\n data?: Record<string, unknown> | undefined;\n badge?: number | undefined;\n /** \"default\" plays the platform sound; null is silent. */\n sound?: \"default\" | null | undefined;\n /** Android notification channel id. */\n channelId?: string | undefined;\n};\n\nexport type MobilePushTicket =\n | { ok: true; ticketId: string | null }\n | {\n ok: false;\n error: string;\n /** The token will never work again (app uninstalled, token rotated). Prune it. */\n gone: boolean;\n retryable: boolean;\n };\n\nexport type MobilePushReceipt =\n | { ticketId: string; ok: true }\n | { ticketId: string; ok: false; error: string; gone: boolean };\n\nexport type MobilePushProvider = {\n name: string;\n /** True for a token this provider can deliver to. Rejected at registration otherwise. */\n isValidToken(token: string): boolean;\n /** One result per message, in order. Never throws for a per-token failure. */\n send(messages: readonly MobilePushMessage[], signal?: AbortSignal): Promise<MobilePushTicket[]>;\n /**\n * Some services (Expo) accept a message, then learn from APNs/FCM later\n * that the device is gone. Receipts carry that late verdict.\n */\n receipts?(ticketIds: readonly string[], signal?: AbortSignal): Promise<MobilePushReceipt[]>;\n};\n\nexport type ExpoPushOptions = {\n /** Expo \"enhanced security\" access token. Optional; when set, requests without it are refused by Expo. */\n accessToken?: string | undefined;\n fetch?: typeof globalThis.fetch | undefined;\n /** Default https://exp.host. A local fake for tests. */\n apiBase?: string | undefined;\n timeoutMs?: number | undefined;\n};\n\ntype ExpoTicket =\n | { status: \"ok\"; id: string }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\ntype ExpoReceipt =\n | { status: \"ok\" }\n | { status: \"error\"; message?: string; details?: { error?: string } };\n\n/** Expo tokens look like ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx] or ExpoPushToken[...]. */\nexport const isExpoPushToken = (token: string): boolean =>\n /^Expo(nent)?PushToken\\[[A-Za-z0-9_-]{8,64}\\]$/.test(token);\n\n/** Expo's send endpoint takes at most this many messages per request. */\nexport const EXPO_SEND_BATCH = 100;\n/** And its receipts endpoint at most this many ids. */\nexport const EXPO_RECEIPT_BATCH = 1000;\n\n// Expo's error codes, per https://docs.expo.dev/push-notifications/sending-notifications/#individual-errors\nconst GONE = new Set([\"DeviceNotRegistered\"]);\nconst RETRYABLE = new Set([\"MessageRateExceeded\", \"ProviderError\", \"ExpoError\"]);\n\nconst classify = (\n code: string | undefined,\n message: string | undefined,\n): { error: string; gone: boolean; retryable: boolean } => ({\n error: code ? `${code}: ${message ?? \"\"}`.trim() : (message ?? \"expo push failed\"),\n gone: code !== undefined && GONE.has(code),\n retryable: code !== undefined && RETRYABLE.has(code),\n});\n\n/** Expo Push over fetch, so it runs on edge runtimes. Batches, classifies, never throws per token. */\nexport function expoPush(options: ExpoPushOptions = {}): MobilePushProvider {\n const doFetch = options.fetch ?? globalThis.fetch;\n const base = (options.apiBase ?? \"https://exp.host\").replace(/\\/$/, \"\");\n const timeoutMs = options.timeoutMs ?? 10_000;\n const redact = (text: string) =>\n options.accessToken ? text.split(options.accessToken).join(\"<token>\") : text;\n\n async function post<T>(path: string, body: unknown, signal?: AbortSignal): Promise<T> {\n const timeout = AbortSignal.timeout(timeoutMs);\n const headers: Record<string, string> = {\n accept: \"application/json\",\n \"content-type\": \"application/json\",\n };\n if (options.accessToken) headers.authorization = `Bearer ${options.accessToken}`;\n const response = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers,\n body: JSON.stringify(body),\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n });\n if (!response.ok) {\n const text = redact((await response.text().catch(() => \"\")).slice(0, 200));\n const error = new Error(`expo push ${path} failed: HTTP ${response.status} ${text}`.trim());\n (error as { status?: number }).status = response.status;\n throw error;\n }\n return (await response.json()) as T;\n }\n\n const transportFailure = (error: unknown): MobilePushTicket => {\n const status = (error as { status?: number })?.status;\n const message = redact(error instanceof Error ? error.message : String(error));\n // 4xx from the service itself (bad access token, malformed request) cannot be fixed by retrying.\n const retryable = status === undefined || status === 429 || status >= 500;\n return { ok: false, error: message, gone: false, retryable };\n };\n\n return {\n name: \"expo-push\",\n isValidToken: isExpoPushToken,\n\n async send(messages, signal) {\n const results: MobilePushTicket[] = [];\n for (let start = 0; start < messages.length; start += EXPO_SEND_BATCH) {\n const batch = messages.slice(start, start + EXPO_SEND_BATCH);\n let tickets: ExpoTicket[];\n try {\n const response = await post<{ data: ExpoTicket[] }>(\n \"/--/api/v2/push/send\",\n batch.map((message) => ({\n to: message.token,\n title: message.title,\n body: message.body,\n ...(message.data ? { data: message.data } : {}),\n ...(message.badge === undefined ? {} : { badge: message.badge }),\n ...(message.sound === undefined ? { sound: \"default\" } : { sound: message.sound }),\n ...(message.channelId ? { channelId: message.channelId } : {}),\n })),\n signal,\n );\n tickets = response.data;\n } catch (error) {\n const failure = transportFailure(error);\n for (const _ of batch) results.push(failure);\n continue;\n }\n batch.forEach((_, index) => {\n const ticket = tickets[index];\n if (!ticket) {\n results.push({ ok: false, error: \"no ticket returned\", gone: false, retryable: true });\n } else if (ticket.status === \"ok\") {\n results.push({ ok: true, ticketId: ticket.id ?? null });\n } else {\n results.push({ ok: false, ...classify(ticket.details?.error, ticket.message) });\n }\n });\n }\n return results;\n },\n\n async receipts(ticketIds, signal) {\n const out: MobilePushReceipt[] = [];\n for (let start = 0; start < ticketIds.length; start += EXPO_RECEIPT_BATCH) {\n const ids = ticketIds.slice(start, start + EXPO_RECEIPT_BATCH);\n const response = await post<{ data: Record<string, ExpoReceipt> }>(\n \"/--/api/v2/push/getReceipts\",\n { ids },\n signal,\n );\n for (const [ticketId, receipt] of Object.entries(response.data ?? {})) {\n if (receipt.status === \"ok\") out.push({ ticketId, ok: true });\n else {\n const { error, gone } = classify(receipt.details?.error, receipt.message);\n out.push({ ticketId, ok: false, error, gone });\n }\n }\n }\n return out;\n },\n };\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/providers/expo-push/index.ts
|
|
2
|
-
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,}\]$/.test(token);
|
|
2
|
+
var isExpoPushToken = (token) => /^Expo(nent)?PushToken\[[A-Za-z0-9_-]{8,64}\]$/.test(token);
|
|
3
3
|
var EXPO_SEND_BATCH = 100;
|
|
4
4
|
var EXPO_RECEIPT_BATCH = 1e3;
|
|
5
5
|
var GONE = /* @__PURE__ */ new Set(["DeviceNotRegistered"]);
|