@zaisma/game-server 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (349) hide show
  1. package/README.md +2 -0
  2. package/bench/brand/.assetsignore +4 -0
  3. package/bench/brand/README.md +51 -0
  4. package/bench/brand/_headers +13 -0
  5. package/bench/brand/app-icon-1024.png +0 -0
  6. package/bench/brand/app-icon-192.png +0 -0
  7. package/bench/brand/app-icon-512.png +0 -0
  8. package/bench/brand/apple-touch-icon.png +0 -0
  9. package/bench/brand/cover-guide/coin-cascade-busy-fine.webp +0 -0
  10. package/bench/brand/cover-guide/gem-grid-cover.webp +0 -0
  11. package/bench/brand/cover-guide/gem-grid-screenshot.webp +0 -0
  12. package/bench/brand/cover-guide/good-arena.webp +0 -0
  13. package/bench/brand/cover-guide/good-four-in-a-row.webp +0 -0
  14. package/bench/brand/cover-guide/lava-leap-extra-text.webp +0 -0
  15. package/bench/brand/cover-guide/lava-leap-fake-reward.webp +0 -0
  16. package/bench/brand/cover-guide/lava-leap-fine.webp +0 -0
  17. package/bench/brand/cover-guide/lava-leap-overlay.webp +0 -0
  18. package/bench/brand/cover-guide/screenshot-fine.webp +0 -0
  19. package/bench/brand/cover-guide/screenshot-not-fine.webp +0 -0
  20. package/bench/brand/design/covers/ashen-crown.webp +0 -0
  21. package/bench/brand/design/covers/frostline.webp +0 -0
  22. package/bench/brand/design/covers/glasswing.webp +0 -0
  23. package/bench/brand/design/covers/hollow-tide.webp +0 -0
  24. package/bench/brand/design/covers/iron-vigil.webp +0 -0
  25. package/bench/brand/design/covers/kestrel.webp +0 -0
  26. package/bench/brand/design/covers/neon-drift.webp +0 -0
  27. package/bench/brand/design/covers/verdant.webp +0 -0
  28. package/bench/brand/favicon-32.png +0 -0
  29. package/bench/brand/favicon.ico +0 -0
  30. package/bench/brand/favicon.svg +1 -0
  31. package/bench/brand/flags/LICENSE +21 -0
  32. package/bench/brand/flags/ad.png +0 -0
  33. package/bench/brand/flags/ae.png +0 -0
  34. package/bench/brand/flags/af.png +0 -0
  35. package/bench/brand/flags/ag.png +0 -0
  36. package/bench/brand/flags/ai.png +0 -0
  37. package/bench/brand/flags/al.png +0 -0
  38. package/bench/brand/flags/am.png +0 -0
  39. package/bench/brand/flags/ao.png +0 -0
  40. package/bench/brand/flags/aq.png +0 -0
  41. package/bench/brand/flags/ar.png +0 -0
  42. package/bench/brand/flags/as.png +0 -0
  43. package/bench/brand/flags/at.png +0 -0
  44. package/bench/brand/flags/au.png +0 -0
  45. package/bench/brand/flags/aw.png +0 -0
  46. package/bench/brand/flags/ax.png +0 -0
  47. package/bench/brand/flags/az.png +0 -0
  48. package/bench/brand/flags/ba.png +0 -0
  49. package/bench/brand/flags/bb.png +0 -0
  50. package/bench/brand/flags/bd.png +0 -0
  51. package/bench/brand/flags/be.png +0 -0
  52. package/bench/brand/flags/bf.png +0 -0
  53. package/bench/brand/flags/bg.png +0 -0
  54. package/bench/brand/flags/bh.png +0 -0
  55. package/bench/brand/flags/bi.png +0 -0
  56. package/bench/brand/flags/bj.png +0 -0
  57. package/bench/brand/flags/bl.png +0 -0
  58. package/bench/brand/flags/bm.png +0 -0
  59. package/bench/brand/flags/bn.png +0 -0
  60. package/bench/brand/flags/bo.png +0 -0
  61. package/bench/brand/flags/bq.png +0 -0
  62. package/bench/brand/flags/br.png +0 -0
  63. package/bench/brand/flags/bs.png +0 -0
  64. package/bench/brand/flags/bt.png +0 -0
  65. package/bench/brand/flags/bv.png +0 -0
  66. package/bench/brand/flags/bw.png +0 -0
  67. package/bench/brand/flags/by.png +0 -0
  68. package/bench/brand/flags/bz.png +0 -0
  69. package/bench/brand/flags/ca.png +0 -0
  70. package/bench/brand/flags/cc.png +0 -0
  71. package/bench/brand/flags/cd.png +0 -0
  72. package/bench/brand/flags/cf.png +0 -0
  73. package/bench/brand/flags/cg.png +0 -0
  74. package/bench/brand/flags/ch.png +0 -0
  75. package/bench/brand/flags/ci.png +0 -0
  76. package/bench/brand/flags/ck.png +0 -0
  77. package/bench/brand/flags/cl.png +0 -0
  78. package/bench/brand/flags/cm.png +0 -0
  79. package/bench/brand/flags/cn.png +0 -0
  80. package/bench/brand/flags/co.png +0 -0
  81. package/bench/brand/flags/cp.png +0 -0
  82. package/bench/brand/flags/cr.png +0 -0
  83. package/bench/brand/flags/cu.png +0 -0
  84. package/bench/brand/flags/cv.png +0 -0
  85. package/bench/brand/flags/cw.png +0 -0
  86. package/bench/brand/flags/cx.png +0 -0
  87. package/bench/brand/flags/cy.png +0 -0
  88. package/bench/brand/flags/cz.png +0 -0
  89. package/bench/brand/flags/de.png +0 -0
  90. package/bench/brand/flags/dg.png +0 -0
  91. package/bench/brand/flags/dj.png +0 -0
  92. package/bench/brand/flags/dk.png +0 -0
  93. package/bench/brand/flags/dm.png +0 -0
  94. package/bench/brand/flags/do.png +0 -0
  95. package/bench/brand/flags/dz.png +0 -0
  96. package/bench/brand/flags/ec.png +0 -0
  97. package/bench/brand/flags/ee.png +0 -0
  98. package/bench/brand/flags/eg.png +0 -0
  99. package/bench/brand/flags/eh.png +0 -0
  100. package/bench/brand/flags/er.png +0 -0
  101. package/bench/brand/flags/es.png +0 -0
  102. package/bench/brand/flags/et.png +0 -0
  103. package/bench/brand/flags/eu.png +0 -0
  104. package/bench/brand/flags/fi.png +0 -0
  105. package/bench/brand/flags/fj.png +0 -0
  106. package/bench/brand/flags/fk.png +0 -0
  107. package/bench/brand/flags/fm.png +0 -0
  108. package/bench/brand/flags/fo.png +0 -0
  109. package/bench/brand/flags/fr.png +0 -0
  110. package/bench/brand/flags/ga.png +0 -0
  111. package/bench/brand/flags/gb.png +0 -0
  112. package/bench/brand/flags/gd.png +0 -0
  113. package/bench/brand/flags/ge.png +0 -0
  114. package/bench/brand/flags/gf.png +0 -0
  115. package/bench/brand/flags/gg.png +0 -0
  116. package/bench/brand/flags/gh.png +0 -0
  117. package/bench/brand/flags/gi.png +0 -0
  118. package/bench/brand/flags/gl.png +0 -0
  119. package/bench/brand/flags/gm.png +0 -0
  120. package/bench/brand/flags/gn.png +0 -0
  121. package/bench/brand/flags/gp.png +0 -0
  122. package/bench/brand/flags/gq.png +0 -0
  123. package/bench/brand/flags/gr.png +0 -0
  124. package/bench/brand/flags/gs.png +0 -0
  125. package/bench/brand/flags/gt.png +0 -0
  126. package/bench/brand/flags/gu.png +0 -0
  127. package/bench/brand/flags/gw.png +0 -0
  128. package/bench/brand/flags/gy.png +0 -0
  129. package/bench/brand/flags/hk.png +0 -0
  130. package/bench/brand/flags/hm.png +0 -0
  131. package/bench/brand/flags/hn.png +0 -0
  132. package/bench/brand/flags/hr.png +0 -0
  133. package/bench/brand/flags/ht.png +0 -0
  134. package/bench/brand/flags/hu.png +0 -0
  135. package/bench/brand/flags/ic.png +0 -0
  136. package/bench/brand/flags/id.png +0 -0
  137. package/bench/brand/flags/ie.png +0 -0
  138. package/bench/brand/flags/il.png +0 -0
  139. package/bench/brand/flags/im.png +0 -0
  140. package/bench/brand/flags/in.png +0 -0
  141. package/bench/brand/flags/io.png +0 -0
  142. package/bench/brand/flags/iq.png +0 -0
  143. package/bench/brand/flags/ir.png +0 -0
  144. package/bench/brand/flags/is.png +0 -0
  145. package/bench/brand/flags/it.png +0 -0
  146. package/bench/brand/flags/je.png +0 -0
  147. package/bench/brand/flags/jm.png +0 -0
  148. package/bench/brand/flags/jo.png +0 -0
  149. package/bench/brand/flags/jp.png +0 -0
  150. package/bench/brand/flags/ke.png +0 -0
  151. package/bench/brand/flags/kg.png +0 -0
  152. package/bench/brand/flags/kh.png +0 -0
  153. package/bench/brand/flags/ki.png +0 -0
  154. package/bench/brand/flags/km.png +0 -0
  155. package/bench/brand/flags/kn.png +0 -0
  156. package/bench/brand/flags/kp.png +0 -0
  157. package/bench/brand/flags/kr.png +0 -0
  158. package/bench/brand/flags/kw.png +0 -0
  159. package/bench/brand/flags/ky.png +0 -0
  160. package/bench/brand/flags/kz.png +0 -0
  161. package/bench/brand/flags/la.png +0 -0
  162. package/bench/brand/flags/lb.png +0 -0
  163. package/bench/brand/flags/lc.png +0 -0
  164. package/bench/brand/flags/li.png +0 -0
  165. package/bench/brand/flags/lk.png +0 -0
  166. package/bench/brand/flags/lr.png +0 -0
  167. package/bench/brand/flags/ls.png +0 -0
  168. package/bench/brand/flags/lt.png +0 -0
  169. package/bench/brand/flags/lu.png +0 -0
  170. package/bench/brand/flags/lv.png +0 -0
  171. package/bench/brand/flags/ly.png +0 -0
  172. package/bench/brand/flags/ma.png +0 -0
  173. package/bench/brand/flags/mc.png +0 -0
  174. package/bench/brand/flags/md.png +0 -0
  175. package/bench/brand/flags/me.png +0 -0
  176. package/bench/brand/flags/mf.png +0 -0
  177. package/bench/brand/flags/mg.png +0 -0
  178. package/bench/brand/flags/mh.png +0 -0
  179. package/bench/brand/flags/mk.png +0 -0
  180. package/bench/brand/flags/ml.png +0 -0
  181. package/bench/brand/flags/mm.png +0 -0
  182. package/bench/brand/flags/mn.png +0 -0
  183. package/bench/brand/flags/mo.png +0 -0
  184. package/bench/brand/flags/mp.png +0 -0
  185. package/bench/brand/flags/mq.png +0 -0
  186. package/bench/brand/flags/mr.png +0 -0
  187. package/bench/brand/flags/ms.png +0 -0
  188. package/bench/brand/flags/mt.png +0 -0
  189. package/bench/brand/flags/mu.png +0 -0
  190. package/bench/brand/flags/mv.png +0 -0
  191. package/bench/brand/flags/mw.png +0 -0
  192. package/bench/brand/flags/mx.png +0 -0
  193. package/bench/brand/flags/my.png +0 -0
  194. package/bench/brand/flags/mz.png +0 -0
  195. package/bench/brand/flags/na.png +0 -0
  196. package/bench/brand/flags/nc.png +0 -0
  197. package/bench/brand/flags/ne.png +0 -0
  198. package/bench/brand/flags/nf.png +0 -0
  199. package/bench/brand/flags/ng.png +0 -0
  200. package/bench/brand/flags/ni.png +0 -0
  201. package/bench/brand/flags/nl.png +0 -0
  202. package/bench/brand/flags/no.png +0 -0
  203. package/bench/brand/flags/np.png +0 -0
  204. package/bench/brand/flags/nr.png +0 -0
  205. package/bench/brand/flags/nu.png +0 -0
  206. package/bench/brand/flags/nz.png +0 -0
  207. package/bench/brand/flags/om.png +0 -0
  208. package/bench/brand/flags/pa.png +0 -0
  209. package/bench/brand/flags/pc.png +0 -0
  210. package/bench/brand/flags/pe.png +0 -0
  211. package/bench/brand/flags/pf.png +0 -0
  212. package/bench/brand/flags/pg.png +0 -0
  213. package/bench/brand/flags/ph.png +0 -0
  214. package/bench/brand/flags/pk.png +0 -0
  215. package/bench/brand/flags/pl.png +0 -0
  216. package/bench/brand/flags/pm.png +0 -0
  217. package/bench/brand/flags/pn.png +0 -0
  218. package/bench/brand/flags/pr.png +0 -0
  219. package/bench/brand/flags/ps.png +0 -0
  220. package/bench/brand/flags/pt.png +0 -0
  221. package/bench/brand/flags/pw.png +0 -0
  222. package/bench/brand/flags/py.png +0 -0
  223. package/bench/brand/flags/qa.png +0 -0
  224. package/bench/brand/flags/re.png +0 -0
  225. package/bench/brand/flags/ro.png +0 -0
  226. package/bench/brand/flags/rs.png +0 -0
  227. package/bench/brand/flags/ru.png +0 -0
  228. package/bench/brand/flags/rw.png +0 -0
  229. package/bench/brand/flags/sa.png +0 -0
  230. package/bench/brand/flags/sb.png +0 -0
  231. package/bench/brand/flags/sc.png +0 -0
  232. package/bench/brand/flags/sd.png +0 -0
  233. package/bench/brand/flags/se.png +0 -0
  234. package/bench/brand/flags/sg.png +0 -0
  235. package/bench/brand/flags/sh.png +0 -0
  236. package/bench/brand/flags/si.png +0 -0
  237. package/bench/brand/flags/sj.png +0 -0
  238. package/bench/brand/flags/sk.png +0 -0
  239. package/bench/brand/flags/sl.png +0 -0
  240. package/bench/brand/flags/sm.png +0 -0
  241. package/bench/brand/flags/sn.png +0 -0
  242. package/bench/brand/flags/so.png +0 -0
  243. package/bench/brand/flags/sr.png +0 -0
  244. package/bench/brand/flags/ss.png +0 -0
  245. package/bench/brand/flags/st.png +0 -0
  246. package/bench/brand/flags/sv.png +0 -0
  247. package/bench/brand/flags/sx.png +0 -0
  248. package/bench/brand/flags/sy.png +0 -0
  249. package/bench/brand/flags/sz.png +0 -0
  250. package/bench/brand/flags/tc.png +0 -0
  251. package/bench/brand/flags/td.png +0 -0
  252. package/bench/brand/flags/tf.png +0 -0
  253. package/bench/brand/flags/tg.png +0 -0
  254. package/bench/brand/flags/th.png +0 -0
  255. package/bench/brand/flags/tj.png +0 -0
  256. package/bench/brand/flags/tk.png +0 -0
  257. package/bench/brand/flags/tl.png +0 -0
  258. package/bench/brand/flags/tm.png +0 -0
  259. package/bench/brand/flags/tn.png +0 -0
  260. package/bench/brand/flags/to.png +0 -0
  261. package/bench/brand/flags/tr.png +0 -0
  262. package/bench/brand/flags/tt.png +0 -0
  263. package/bench/brand/flags/tv.png +0 -0
  264. package/bench/brand/flags/tw.png +0 -0
  265. package/bench/brand/flags/tz.png +0 -0
  266. package/bench/brand/flags/ua.png +0 -0
  267. package/bench/brand/flags/ug.png +0 -0
  268. package/bench/brand/flags/um.png +0 -0
  269. package/bench/brand/flags/un.png +0 -0
  270. package/bench/brand/flags/us.png +0 -0
  271. package/bench/brand/flags/uy.png +0 -0
  272. package/bench/brand/flags/uz.png +0 -0
  273. package/bench/brand/flags/va.png +0 -0
  274. package/bench/brand/flags/vc.png +0 -0
  275. package/bench/brand/flags/ve.png +0 -0
  276. package/bench/brand/flags/vg.png +0 -0
  277. package/bench/brand/flags/vi.png +0 -0
  278. package/bench/brand/flags/vn.png +0 -0
  279. package/bench/brand/flags/vu.png +0 -0
  280. package/bench/brand/flags/wf.png +0 -0
  281. package/bench/brand/flags/ws.png +0 -0
  282. package/bench/brand/flags/xk.png +0 -0
  283. package/bench/brand/flags/xx.png +0 -0
  284. package/bench/brand/flags/ye.png +0 -0
  285. package/bench/brand/flags/yt.png +0 -0
  286. package/bench/brand/flags/za.png +0 -0
  287. package/bench/brand/flags/zm.png +0 -0
  288. package/bench/brand/flags/zw.png +0 -0
  289. package/bench/brand/fonts/OFL.txt +93 -0
  290. package/bench/brand/fonts/jost.7726a5cd.woff2 +0 -0
  291. package/bench/brand/loader/gif/zaisma-loader-black-128.gif +0 -0
  292. package/bench/brand/loader/gif/zaisma-loader-black-512.gif +0 -0
  293. package/bench/brand/loader/gif/zaisma-loader-white-128.gif +0 -0
  294. package/bench/brand/loader/gif/zaisma-loader-white-512.gif +0 -0
  295. package/bench/brand/loader/png/zaisma-loader-black-128.png +0 -0
  296. package/bench/brand/loader/png/zaisma-loader-black-512.png +0 -0
  297. package/bench/brand/loader/png/zaisma-loader-white-128.png +0 -0
  298. package/bench/brand/loader/png/zaisma-loader-white-512.png +0 -0
  299. package/bench/brand/loader/webp/zaisma-loader-black-128.webp +0 -0
  300. package/bench/brand/loader/webp/zaisma-loader-black-512.webp +0 -0
  301. package/bench/brand/loader/webp/zaisma-loader-white-128.webp +0 -0
  302. package/bench/brand/loader/webp/zaisma-loader-white-512.webp +0 -0
  303. package/bench/brand/loader/zaisma-loader-black.svg +1 -0
  304. package/bench/brand/loader/zaisma-loader-white.svg +1 -0
  305. package/bench/brand/loader/zaisma-loader.svg +1 -0
  306. package/bench/brand/sign-in/city-day-phone.webp +0 -0
  307. package/bench/brand/sign-in/city-day.webp +0 -0
  308. package/bench/brand/sign-in/city-night-phone.webp +0 -0
  309. package/bench/brand/sign-in/city-night.webp +0 -0
  310. package/bench/brand/zaisma-app-icon.svg +1 -0
  311. package/bench/brand/zaisma-mark-black-2048.png +0 -0
  312. package/bench/brand/zaisma-mark-black.svg +1 -0
  313. package/bench/brand/zaisma-mark-metal-2048.png +0 -0
  314. package/bench/brand/zaisma-mark-metal.svg +1 -0
  315. package/bench/brand/zaisma-mark-small-black.svg +1 -0
  316. package/bench/brand/zaisma-mark-small-white.svg +1 -0
  317. package/bench/brand/zaisma-mark-tricolor.svg +1 -0
  318. package/bench/brand/zaisma-mark-white-2048.png +0 -0
  319. package/bench/brand/zaisma-mark-white.svg +1 -0
  320. package/bench/brand/zaisma-wordmark-black-2400.png +0 -0
  321. package/bench/brand/zaisma-wordmark-black.svg +1 -0
  322. package/bench/brand/zaisma-wordmark-metal-2400.png +0 -0
  323. package/bench/brand/zaisma-wordmark-metal.svg +1 -0
  324. package/bench/brand/zaisma-wordmark-white-2400.png +0 -0
  325. package/bench/brand/zaisma-wordmark-white.svg +1 -0
  326. package/bench/glass.js +46486 -0
  327. package/bin/local-play-bench.d.ts +8 -0
  328. package/bin/local-play-bench.js +218 -0
  329. package/bin/local-play-certificate.js +99 -0
  330. package/bin/local-play-runner.js +43 -0
  331. package/bin/local-play.d.ts +41 -0
  332. package/bin/local-play.js +1573 -0
  333. package/bin/node.js +99 -5
  334. package/package.json +1 -1
  335. package/sdk/picture.js +97 -0
  336. package/sdk/relay.js +269 -0
  337. package/sdk/saves.js +884 -0
  338. package/sdk/zaisma.d.ts +1810 -0
  339. package/sdk/zaisma.js +2459 -0
  340. package/src/chat.d.ts +23 -8
  341. package/src/chat.js +21 -11
  342. package/src/index.d.ts +2 -2
  343. package/src/index.js +1 -1
  344. package/src/pass.d.ts +9 -8
  345. package/src/pass.js +10 -8
  346. package/src/qr-code.d.ts +20 -0
  347. package/src/qr-code.js +319 -0
  348. package/src/room.d.ts +148 -3
  349. package/src/room.js +204 -3
package/src/chat.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
3
3
  * leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
4
- * contact details in any disguise, moving to another app or meeting up, personal details, slurs and
5
- * sexual content are stopped. Swearing is only flagged (hasSwearing): blocking it is your call. The answer
6
- * is one verdict for the whole message, never per receiver, so it never says who's under 18.
4
+ * contact details in any disguise, moving to another app or meeting up, personal details and sexual
5
+ * content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
6
+ * call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
7
+ * The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
7
8
  *
8
9
  * checkChat sends the players' passes (as your server received them) and the sender's last few messages to
9
10
  * Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
@@ -11,14 +12,17 @@
11
12
  * It's for your own server only. In Zaisma rooms the room filters the chat it delivers itself, so a game
12
13
  * server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
13
14
  */
14
- /** Why a message was stopped. The same reasons as Zaisma.checkChat, plus chat_paused and not_allowed_to_chat. */
15
+ /**
16
+ * Why a message was stopped. The same reasons as Zaisma.checkChat, plus chat_paused, not_allowed_to_chat and
17
+ * unchecked (Zaisma couldn't be reached). slur is v1's and no longer given: slurs are flagged (hasSlur), never stopped.
18
+ */
15
19
  export type ChatCheckReason = "contact_details" | "personal_details" | "slur" | "sexual" | "chat_paused" | "unchecked" | "too_long" | "not_allowed_to_chat";
16
20
  export interface ChatCheck {
17
21
  /** Whether to deliver it. */
18
22
  isAllowed: boolean;
19
23
  /**
20
- * Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked: Zaisma
21
- * couldn't check it, so don't deliver it (try again in a moment). too_long: over 2000 characters.
24
+ * Why not; null when it's allowed. chat_paused: the sender's chat is paused or removed. unchecked: your
25
+ * server couldn't reach Zaisma, so don't deliver it (try again in a moment). too_long: over 2000 characters.
22
26
  * not_allowed_to_chat: canChat says the sender and a receiver may not chat (Zaisma checks it too).
23
27
  */
24
28
  reason: ChatCheckReason | null;
@@ -26,6 +30,8 @@ export interface ChatCheck {
26
30
  message: string | null;
27
31
  /** Whether it swears. Zaisma allows it; block it yourself if your game doesn't. */
28
32
  hasSwearing: boolean;
33
+ /** Whether it has a slur. Zaisma allows it too, so your game and its moderators decide. */
34
+ hasSlur: boolean;
29
35
  }
30
36
  export interface ChatCheckOptions {
31
37
  /** The message. */
@@ -34,7 +40,15 @@ export interface ChatCheckOptions {
34
40
  senderPass: string;
35
41
  /** The passes of everyone it would reach, up to 50. */
36
42
  receiverPasses: string[];
43
+ /** Check chat between adults too, as for under-18s; each message Zaisma's AI looks at counts on your hosting meter. False by default. */
44
+ isAdultFilterEnabled?: boolean;
37
45
  }
46
+ /** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
47
+ export declare const CHAT_ROOM_LIMITS: {
48
+ /** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
49
+ readonly maximumChatMessageCount: 20;
50
+ readonly chatWindowDurationMs: 10000;
51
+ };
38
52
  export declare const CHAT_CHECK_LIMITS: {
39
53
  /** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
40
54
  readonly maximumMessageCharacterCount: 2000;
@@ -56,8 +70,9 @@ export declare class ChatCheckError extends Error {
56
70
  /**
57
71
  * Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
58
72
  * canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
59
- * too, so a message to anyone who may not chat is stopped). It's free. When Zaisma can't be
60
- * reached it answers unchecked: don't deliver the message.
73
+ * too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
74
+ * adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
75
+ * The runner (zaisma-game-server) does all this for room.chat in its rooms.
61
76
  *
62
77
  * const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
63
78
  * if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
package/src/chat.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Zaisma's chat filter for your own server (docs/DECISIONS.md, "The text filter catches what matters and
3
3
  * leaves the rest alone"). Every chat message to or from an under-18 account is checked on Zaisma's side:
4
- * contact details in any disguise, moving to another app or meeting up, personal details, slurs and
5
- * sexual content are stopped. Swearing is only flagged (hasSwearing): blocking it is your call. The answer
6
- * is one verdict for the whole message, never per receiver, so it never says who's under 18.
4
+ * contact details in any disguise, moving to another app or meeting up, personal details and sexual
5
+ * content are stopped. Swearing and slurs are only flagged (hasSwearing, hasSlur): blocking them is your
6
+ * call. Between adults it only flags, unless you turn the filter on for them too (isAdultFilterEnabled).
7
+ * The answer is one verdict for the whole message, never per receiver, so it never says who's under 18.
7
8
  *
8
9
  * checkChat sends the players' passes (as your server received them) and the sender's last few messages to
9
10
  * Zaisma (POST <issuer>/v1/chat/check), so a phone number sent a few digits at a time is caught too.
@@ -12,6 +13,12 @@
12
13
  * server running there never sees passes, and checkChat throws a ChatCheckError (own_server_only).
13
14
  */
14
15
  import { OutsideCallError } from "./outside-calls.js";
16
+ /** Chat in rooms (room.chat), for both Zaisma's rooms and your own server's runner: past any human's typing, so it only stops floods. */
17
+ export const CHAT_ROOM_LIMITS = {
18
+ /** Chat messages one player may send in any chatWindowDurationMs; past it, chat_too_fast. */
19
+ maximumChatMessageCount: 20,
20
+ chatWindowDurationMs: 10_000,
21
+ };
15
22
  export const CHAT_CHECK_LIMITS = {
16
23
  /** The longest message checked, in characters: the whole text is checked, and a longer one is refused (too_long), never cut. */
17
24
  maximumMessageCharacterCount: 2000,
@@ -34,9 +41,9 @@ export class ChatCheckError extends Error {
34
41
  this.fix = fix;
35
42
  }
36
43
  }
37
- const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false };
38
- const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false };
39
- const REASONS = ["contact_details", "personal_details", "slur", "sexual", "chat_paused", "unchecked", "too_long", "not_allowed_to_chat"];
44
+ const UNCHECKED = { isAllowed: false, reason: "unchecked", message: "This message couldn't be checked. Try again in a moment.", hasSwearing: false, hasSlur: false };
45
+ const TOO_LONG = { isAllowed: false, reason: "too_long", message: "That message is too long to send.", hasSwearing: false, hasSlur: false };
46
+ const REASONS = ["contact_details", "personal_details", "sexual", "chat_paused", "unchecked", "too_long", "not_allowed_to_chat"];
40
47
  /** Each sender's last messages, by the sender's pass's player ID: only in this process's memory. */
41
48
  const recentBySender = new Map();
42
49
  /** The claims of a pass, unchecked: Zaisma checks the signature. Only to find whose it is and where it's from. */
@@ -67,13 +74,16 @@ function rememberText(senderPlayerId, text, nowMs) {
67
74
  /**
68
75
  * Check one chat message with Zaisma's filter. Call it on every chat message your server delivers, after
69
76
  * canChat says the players may chat, and deliver it only when isAllowed (Zaisma applies canChat's rule
70
- * too, so a message to anyone who may not chat is stopped). It's free. When Zaisma can't be
71
- * reached it answers unchecked: don't deliver the message.
77
+ * too, so a message to anyone who may not chat is stopped). It's free, except the checks of chat between
78
+ * adults when isAdultFilterEnabled. When Zaisma can't be reached it answers unchecked: don't deliver the message.
79
+ * The runner (zaisma-game-server) does all this for room.chat in its rooms.
72
80
  *
73
81
  * const check = await checkChat({ text, senderPass, receiverPasses: [receiverPass] });
74
82
  * if (!check.isAllowed || check.hasSwearing) return reply(sender, check.message ?? "No swearing here.");
75
83
  */
76
84
  export async function checkChat({ text, senderPass, receiverPasses }) {
85
+ // Read apart from the destructuring, which is v1's signature as it's locked (test/contracts).
86
+ const isAdultFilterEnabled = arguments[0]?.isAdultFilterEnabled === true;
77
87
  const claims = readClaims(String(senderPass ?? ""));
78
88
  if (typeof claims.sub !== "string" || !isZaismaIssuer(claims.iss) || !Array.isArray(receiverPasses))
79
89
  return UNCHECKED;
@@ -87,7 +97,7 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
87
97
  const response = await fetch(`${claims.iss}/v1/chat/check`, {
88
98
  method: "POST",
89
99
  headers: { "Content-Type": "application/json" },
90
- body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts }),
100
+ body: JSON.stringify({ gameSlug: claims.aud, senderPass, receiverPasses: receiverPasses.slice(0, CHAT_CHECK_LIMITS.maximumReceiverCount), text: message, recentTexts, isAdultFilterEnabled }),
91
101
  signal: AbortSignal.timeout(CHAT_CHECK_LIMITS.checkTimeoutMs),
92
102
  });
93
103
  if (!response.ok)
@@ -96,12 +106,12 @@ export async function checkChat({ text, senderPass, receiverPasses }) {
96
106
  if (typeof answer.isAllowed !== "boolean")
97
107
  return UNCHECKED;
98
108
  const reason = typeof answer.reason === "string" && REASONS.includes(answer.reason) ? answer.reason : null;
99
- return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true };
109
+ return { isAllowed: answer.isAllowed, reason, message: typeof answer.message === "string" ? answer.message : null, hasSwearing: answer.hasSwearing === true, hasSlur: answer.hasSlur === true };
100
110
  }
101
111
  catch (error) {
102
112
  // In Zaisma rooms the gateway refuses the call to Zaisma's own address: checkChat is for a game server on your own server.
103
113
  if (error instanceof OutsideCallError && ["unreachable_host", "not_https", "host_not_allowed"].includes(error.code)) {
104
- throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "Send chat through Zaisma's chat hosting in rooms once it's out; until then, check what a player types in the game with Zaisma.checkChat, or offer preset phrases and emotes.");
114
+ throw new ChatCheckError("checkChat is for a game server on your own server. In Zaisma rooms the room runs Zaisma's chat filter on the chat it delivers.", "In Zaisma rooms, send chat with room.chat in the game; the room filters it.");
105
115
  }
106
116
  return UNCHECKED;
107
117
  }
package/src/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type JoinInfo, type KickOptions, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type SendOptions, type VersionUpdate, } from "./room.ts";
1
+ export { BUSY_CLOSE_CODE, ENDED_CLOSE_CODE, GAME_SERVER_OPTIONS, GameServerError, NO_ANSWER_CLOSE_CODE, REMOVED_CLOSE_CODE, ROOM_LIMITS, SEAT_GONE_CLOSE_CODE, VERSION_UPDATE_CLOSE_CODE, defineGameServer, type Audience, type AwayInfo, type AwayMode, type AwayReason, type BroadcastOptions, type Delivery, type GameServer, type GameServerErrorCode, type HandlerResult, type IdleCheck, type JoinAnswer, type ChatAnswer, type ChatInfo, type JoinInfo, type KickOptions, type LeaveInfo, type LeaveReason, type MoveOptions, type MoveResult, type Player, type Purchase, type Refusal, type ReserveOptions, type RestoreInfo, type Room, type RoomErrorCode, type RoomInfo, type SendOptions, type VersionUpdate, } from "./room.ts";
2
2
  export { REQUEST_LIMITS, type RequestErrorCode, type Server } from "./requests.ts";
3
3
  export { SCHEDULE_LIMITS, type Schedule, type ScheduledRun } from "./schedules.ts";
4
4
  export { WEBHOOK_LIMITS, verifyWebhook, type WebhookError, type WebhookErrorCode, type WebhookEvent, type WebhookEventKind, type WebhookHeaders } from "./webhooks.ts";
5
5
  export { DATABASE_LIMITS, DatabaseError, type DatabaseErrorCode, type DatabaseRow, type DatabaseStatement, type DatabaseValue, type GameDatabase } from "./database.ts";
6
6
  export { OUTSIDE_CALL_LIMITS, OutsideCallError, type OutsideCallErrorCode } from "./outside-calls.ts";
7
7
  export { PASS_ISSUER_URL, PASS_TOKEN_TYPE, PASS_VERSION_NUMBER, PassError, RECEIPT_ISSUER_URL, RECEIPT_TOKEN_TYPE, RECEIPT_VERSION_NUMBER, ReceiptError, canChat, passesAreFriends, passesHaveBlockBetween, verifyPass, verifyReceipt, type PassClaims, type PassErrorCode, type PassExpectations, type PlayerKind, type PublicKey, type ReceiptClaims, type ReceiptErrorCode, type ReceiptExpectations, type WhoCanChat } from "./pass.ts";
8
- export { CHAT_CHECK_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
8
+ export { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS, ChatCheckError, checkChat, type ChatCheck, type ChatCheckOptions, type ChatCheckReason } from "./chat.ts";
9
9
  export { guestName, isRoomCode } from "./names.ts";
10
10
  export { relay } from "./relay.ts";
package/src/index.js CHANGED
@@ -7,6 +7,6 @@ export { WEBHOOK_LIMITS, verifyWebhook } from "./webhooks.js";
7
7
  export { DATABASE_LIMITS, DatabaseError } from "./database.js";
8
8
  export { OUTSIDE_CALL_LIMITS, OutsideCallError } from "./outside-calls.js";
9
9
  export { PASS_ISSUER_URL, PASS_TOKEN_TYPE, PASS_VERSION_NUMBER, PassError, RECEIPT_ISSUER_URL, RECEIPT_TOKEN_TYPE, RECEIPT_VERSION_NUMBER, ReceiptError, canChat, passesAreFriends, passesHaveBlockBetween, verifyPass, verifyReceipt } from "./pass.js";
10
- export { CHAT_CHECK_LIMITS, ChatCheckError, checkChat } from "./chat.js";
10
+ export { CHAT_CHECK_LIMITS, CHAT_ROOM_LIMITS, ChatCheckError, checkChat } from "./chat.js";
11
11
  export { guestName, isRoomCode } from "./names.js";
12
12
  export { relay } from "./relay.js";
package/src/pass.d.ts CHANGED
@@ -28,9 +28,9 @@ export interface PassClaims {
28
28
  */
29
29
  who_can_chat?: WhoCanChat | (string & {});
30
30
  /**
31
- * For accounts that chat with anyone, the group they can chat in (chat_group_…): two such players
32
- * chat when their groups match (canChat). Opaque and different in every game, so it never says an
33
- * age; a game could guess which group is the younger one from its size.
31
+ * For accounts that chat at all (friends or anyone), the group they can chat in (chat_group_…): two
32
+ * players chat only when their groups match (canChat), friends included. Opaque and different in every
33
+ * game, so it never says an age; a game could guess which group is the younger one from its size.
34
34
  */
35
35
  chat_group_id?: string;
36
36
  /**
@@ -88,11 +88,12 @@ export declare function passesHaveBlockBetween(first: PassClaims, second: PassCl
88
88
  */
89
89
  export declare function passesAreFriends(first: PassClaims, second: PassClaims): boolean;
90
90
  /**
91
- * Whether two players may chat, by the passes your server checked (verifyPass): both accounts, no block
92
- * between them, neither letting nobody chat, and either friends or both letting anyone chat, in the same
93
- * chat group. Guests and accounts set to nobody never chat; accounts set to friends chat only with
94
- * friends (passesAreFriends). Use it on every message your own server delivers, text or voice; Zaisma's
95
- * chat check (checkChat) applies the same rule, and Zaisma rooms will do the same for you.
91
+ * Whether two players may chat, by the passes your server checked (verifyPass): both accounts in the same
92
+ * chat group, no block between them, neither letting nobody chat, and either friends or both letting
93
+ * anyone chat. Guests and accounts set to nobody never chat; accounts set to friends chat only with
94
+ * friends (passesAreFriends); friends in different chat groups don't chat. Use it on every message your
95
+ * own server delivers, text or voice; Zaisma's chat check (checkChat) applies the same rule, and so do
96
+ * rooms (room.chat), on Zaisma and in the runner.
96
97
  */
97
98
  export declare function canChat(first: PassClaims, second: PassClaims): boolean;
98
99
  export type PublicKey = JsonWebKey & {
package/src/pass.js CHANGED
@@ -44,11 +44,12 @@ export function passesAreFriends(first, second) {
44
44
  return lists(first, second) && lists(second, first);
45
45
  }
46
46
  /**
47
- * Whether two players may chat, by the passes your server checked (verifyPass): both accounts, no block
48
- * between them, neither letting nobody chat, and either friends or both letting anyone chat, in the same
49
- * chat group. Guests and accounts set to nobody never chat; accounts set to friends chat only with
50
- * friends (passesAreFriends). Use it on every message your own server delivers, text or voice; Zaisma's
51
- * chat check (checkChat) applies the same rule, and Zaisma rooms will do the same for you.
47
+ * Whether two players may chat, by the passes your server checked (verifyPass): both accounts in the same
48
+ * chat group, no block between them, neither letting nobody chat, and either friends or both letting
49
+ * anyone chat. Guests and accounts set to nobody never chat; accounts set to friends chat only with
50
+ * friends (passesAreFriends); friends in different chat groups don't chat. Use it on every message your
51
+ * own server delivers, text or voice; Zaisma's chat check (checkChat) applies the same rule, and so do
52
+ * rooms (room.chat), on Zaisma and in the runner.
52
53
  */
53
54
  export function canChat(first, second) {
54
55
  mustBePasses(first, second, "canChat");
@@ -59,9 +60,10 @@ export function canChat(first, second) {
59
60
  const isOpenTo = (whoCanChat) => whoCanChat === "friends" || whoCanChat === "anyone";
60
61
  if (!isOpenTo(first.who_can_chat) || !isOpenTo(second.who_can_chat))
61
62
  return false;
62
- if (passesAreFriends(first, second))
63
- return true;
64
- return first.who_can_chat === "anyone" && second.who_can_chat === "anyone" && typeof first.chat_group_id === "string" && first.chat_group_id === second.chat_group_id;
63
+ // Chat never leaves a chat group, friends included.
64
+ if (typeof first.chat_group_id !== "string" || first.chat_group_id !== second.chat_group_id)
65
+ return false;
66
+ return passesAreFriends(first, second) || (first.who_can_chat === "anyone" && second.who_can_chat === "anyone");
65
67
  }
66
68
  // Plain class fields (no TypeScript-only syntax) so Node can run this file directly.
67
69
  export class PassError extends Error {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * QR codes for game and room links (docs/DECISIONS.md, "QR codes"): players get one behind Share,
3
+ * developers on the dashboard and through the API and MCP. Byte mode, error correction level M,
4
+ * versions 1 to 10 (up to 213 bytes, far more than a link needs). Written here rather than taken from a
5
+ * package so the site's page and the API draw the same code with no dependency; it follows ISO/IEC
6
+ * 18004 the way Project Nayuki's reference encoder does.
7
+ */
8
+ export declare const QR_CODE_LIMITS: {
9
+ /** Version 10 at level M holds 213 bytes. */
10
+ readonly maximumTextBytes: 213;
11
+ /** Light modules around the code, as the standard asks. */
12
+ readonly quietZoneModuleCount: 4;
13
+ };
14
+ /**
15
+ * The code's modules for `text`, as rows of dark (true) and light (false), with no quiet zone. Throws
16
+ * a RangeError when the text is over QR_CODE_LIMITS.maximumTextBytes in UTF-8.
17
+ */
18
+ export declare function qrCodeModules(text: string): boolean[][];
19
+ /** The code for `text` as an SVG document: dark modules on white, with the quiet zone, one unit a module. */
20
+ export declare function qrCodeSvg(text: string): string;
package/src/qr-code.js ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * QR codes for game and room links (docs/DECISIONS.md, "QR codes"): players get one behind Share,
3
+ * developers on the dashboard and through the API and MCP. Byte mode, error correction level M,
4
+ * versions 1 to 10 (up to 213 bytes, far more than a link needs). Written here rather than taken from a
5
+ * package so the site's page and the API draw the same code with no dependency; it follows ISO/IEC
6
+ * 18004 the way Project Nayuki's reference encoder does.
7
+ */
8
+ export const QR_CODE_LIMITS = {
9
+ /** Version 10 at level M holds 213 bytes. */
10
+ maximumTextBytes: 213,
11
+ /** Light modules around the code, as the standard asks. */
12
+ quietZoneModuleCount: 4,
13
+ };
14
+ // Level M: error correction codewords per block and blocks, by version (index 0 unused).
15
+ const ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK = [0, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26];
16
+ const ERROR_CORRECTION_BLOCK_COUNTS = [0, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5];
17
+ /** Error correction level M (medium)'s two bits in the format information. */
18
+ const MEDIUM_CORRECTION_FORMAT_NUMBER = 0;
19
+ const HIGHEST_VERSION_NUMBER = 10;
20
+ const isBitSet = (bitsNumber, bitIndex) => ((bitsNumber >>> bitIndex) & 1) !== 0;
21
+ function rawDataModuleCount(versionNumber) {
22
+ let moduleCount = (16 * versionNumber + 128) * versionNumber + 64;
23
+ if (versionNumber >= 2) {
24
+ const alignmentCount = Math.floor(versionNumber / 7) + 2;
25
+ moduleCount -= (25 * alignmentCount - 10) * alignmentCount - 55;
26
+ if (versionNumber >= 7)
27
+ moduleCount -= 36;
28
+ }
29
+ return moduleCount;
30
+ }
31
+ const dataCodewordCount = (versionNumber) => Math.floor(rawDataModuleCount(versionNumber) / 8) - ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK[versionNumber] * ERROR_CORRECTION_BLOCK_COUNTS[versionNumber];
32
+ // Reed-Solomon over GF(256) with the polynomial 0x11D.
33
+ function multiplied(leftNumber, rightNumber) {
34
+ let productNumber = 0;
35
+ for (let bitIndex = 7; bitIndex >= 0; bitIndex--) {
36
+ productNumber = (productNumber << 1) ^ ((productNumber >>> 7) * 0x11d);
37
+ productNumber ^= ((rightNumber >>> bitIndex) & 1) * leftNumber;
38
+ }
39
+ return productNumber;
40
+ }
41
+ function divisorOf(degreeCount) {
42
+ const coefficients = new Array(degreeCount).fill(0);
43
+ coefficients[degreeCount - 1] = 1;
44
+ let rootNumber = 1;
45
+ for (let stepIndex = 0; stepIndex < degreeCount; stepIndex++) {
46
+ for (let index = 0; index < coefficients.length; index++) {
47
+ coefficients[index] = multiplied(coefficients[index], rootNumber);
48
+ if (index + 1 < coefficients.length)
49
+ coefficients[index] = coefficients[index] ^ coefficients[index + 1];
50
+ }
51
+ rootNumber = multiplied(rootNumber, 0x02);
52
+ }
53
+ return coefficients;
54
+ }
55
+ function remainderOf(codewords, divisor) {
56
+ const remainders = divisor.map(() => 0);
57
+ for (const codewordNumber of codewords) {
58
+ const factorNumber = codewordNumber ^ remainders.shift();
59
+ remainders.push(0);
60
+ divisor.forEach((coefficientNumber, index) => (remainders[index] = remainders[index] ^ multiplied(coefficientNumber, factorNumber)));
61
+ }
62
+ return remainders;
63
+ }
64
+ /** The data codewords with their error correction, split into blocks and interleaved. */
65
+ function withErrorCorrection(dataCodewords, versionNumber) {
66
+ const blockCount = ERROR_CORRECTION_BLOCK_COUNTS[versionNumber];
67
+ const blockCorrectionCount = ERROR_CORRECTION_CODEWORD_COUNTS_PER_BLOCK[versionNumber];
68
+ const rawCodewordCount = Math.floor(rawDataModuleCount(versionNumber) / 8);
69
+ const shortBlockCount = blockCount - (rawCodewordCount % blockCount);
70
+ const shortBlockCodewordCount = Math.floor(rawCodewordCount / blockCount);
71
+ const divisor = divisorOf(blockCorrectionCount);
72
+ const blocks = [];
73
+ for (let blockIndex = 0, startIndex = 0; blockIndex < blockCount; blockIndex++) {
74
+ const block = dataCodewords.slice(startIndex, startIndex + shortBlockCodewordCount - blockCorrectionCount + (blockIndex < shortBlockCount ? 0 : 1));
75
+ startIndex += block.length;
76
+ const correction = remainderOf(block, divisor);
77
+ if (blockIndex < shortBlockCount)
78
+ block.push(0);
79
+ blocks.push(block.concat(correction));
80
+ }
81
+ const interleaved = [];
82
+ for (let codewordIndex = 0; codewordIndex < blocks[0].length; codewordIndex++) {
83
+ blocks.forEach((block, blockIndex) => {
84
+ if (codewordIndex !== shortBlockCodewordCount - blockCorrectionCount || blockIndex >= shortBlockCount)
85
+ interleaved.push(block[codewordIndex]);
86
+ });
87
+ }
88
+ return interleaved;
89
+ }
90
+ function alignmentIndexes(versionNumber, sideModuleCount) {
91
+ if (versionNumber === 1)
92
+ return [];
93
+ const alignmentCount = Math.floor(versionNumber / 7) + 2;
94
+ const stepModuleCount = Math.ceil((versionNumber * 4 + 4) / (alignmentCount * 2 - 2)) * 2;
95
+ const indexes = [6];
96
+ for (let moduleIndex = sideModuleCount - 7; indexes.length < alignmentCount; moduleIndex -= stepModuleCount)
97
+ indexes.splice(1, 0, moduleIndex);
98
+ return indexes;
99
+ }
100
+ /** The standard's eight masks, by column and row. */
101
+ const MASKS = [
102
+ (columnIndex, rowIndex) => (columnIndex + rowIndex) % 2 === 0,
103
+ (...indexes) => indexes[1] % 2 === 0,
104
+ (columnIndex) => columnIndex % 3 === 0,
105
+ (columnIndex, rowIndex) => (columnIndex + rowIndex) % 3 === 0,
106
+ (columnIndex, rowIndex) => (Math.floor(columnIndex / 3) + Math.floor(rowIndex / 2)) % 2 === 0,
107
+ (columnIndex, rowIndex) => ((columnIndex * rowIndex) % 2) + ((columnIndex * rowIndex) % 3) === 0,
108
+ (columnIndex, rowIndex) => (((columnIndex * rowIndex) % 2) + ((columnIndex * rowIndex) % 3)) % 2 === 0,
109
+ (columnIndex, rowIndex) => (((columnIndex + rowIndex) % 2) + ((columnIndex * rowIndex) % 3)) % 2 === 0,
110
+ ];
111
+ /** A score for how hard a masked code is to read (lower is better), from the standard's rules. */
112
+ function penaltyPointCount(modules) {
113
+ const sideModuleCount = modules.length;
114
+ let pointCount = 0;
115
+ const scoreLines = (isDarkAt) => {
116
+ for (let lineIndex = 0; lineIndex < sideModuleCount; lineIndex++) {
117
+ let runModuleCount = 0;
118
+ let isRunDark = false;
119
+ const line = [];
120
+ for (let alongIndex = 0; alongIndex < sideModuleCount; alongIndex++) {
121
+ const isDark = isDarkAt(lineIndex, alongIndex);
122
+ line.push(isDark);
123
+ if (alongIndex > 0 && isDark === isRunDark) {
124
+ runModuleCount++;
125
+ if (runModuleCount === 5)
126
+ pointCount += 3;
127
+ else if (runModuleCount > 5)
128
+ pointCount++;
129
+ }
130
+ else {
131
+ isRunDark = isDark;
132
+ runModuleCount = 1;
133
+ }
134
+ }
135
+ // Finder-like runs: dark-light-dark-dark-dark-light-dark with four light modules on a side.
136
+ const padded = [false, false, false, false, ...line, false, false, false, false];
137
+ for (let startIndex = 0; startIndex + 11 <= padded.length; startIndex++) {
138
+ const isDarkAtOffset = (offsetIndex) => padded[startIndex + offsetIndex];
139
+ if (!(isDarkAtOffset(4) && !isDarkAtOffset(5) && isDarkAtOffset(6) && isDarkAtOffset(7) && isDarkAtOffset(8) && !isDarkAtOffset(9) && isDarkAtOffset(10)))
140
+ continue;
141
+ const isLightBefore = !isDarkAtOffset(0) && !isDarkAtOffset(1) && !isDarkAtOffset(2) && !isDarkAtOffset(3);
142
+ const isLightAfter = startIndex + 15 <= padded.length && !isDarkAtOffset(11) && !isDarkAtOffset(12) && !isDarkAtOffset(13) && !isDarkAtOffset(14);
143
+ if (isLightBefore || isLightAfter)
144
+ pointCount += 40;
145
+ }
146
+ }
147
+ };
148
+ scoreLines((rowIndex, columnIndex) => modules[rowIndex][columnIndex]);
149
+ scoreLines((columnIndex, rowIndex) => modules[rowIndex][columnIndex]);
150
+ for (let rowIndex = 0; rowIndex + 1 < sideModuleCount; rowIndex++) {
151
+ for (let columnIndex = 0; columnIndex + 1 < sideModuleCount; columnIndex++) {
152
+ const isDark = modules[rowIndex][columnIndex];
153
+ if (isDark === modules[rowIndex][columnIndex + 1] && isDark === modules[rowIndex + 1][columnIndex] && isDark === modules[rowIndex + 1][columnIndex + 1])
154
+ pointCount += 3;
155
+ }
156
+ }
157
+ const darkModuleCount = modules.flat().filter(Boolean).length;
158
+ const moduleCount = sideModuleCount * sideModuleCount;
159
+ pointCount += (Math.ceil(Math.abs(darkModuleCount * 20 - moduleCount * 10) / moduleCount) - 1) * 10;
160
+ return pointCount;
161
+ }
162
+ /**
163
+ * The code's modules for `text`, as rows of dark (true) and light (false), with no quiet zone. Throws
164
+ * a RangeError when the text is over QR_CODE_LIMITS.maximumTextBytes in UTF-8.
165
+ */
166
+ export function qrCodeModules(text) {
167
+ const textBytes = [...new TextEncoder().encode(text)];
168
+ const neededBitCount = (candidateNumber) => 4 + (candidateNumber < 10 ? 8 : 16) + textBytes.length * 8;
169
+ let versionNumber = 1;
170
+ while (versionNumber <= HIGHEST_VERSION_NUMBER && neededBitCount(versionNumber) > dataCodewordCount(versionNumber) * 8)
171
+ versionNumber++;
172
+ if (versionNumber > HIGHEST_VERSION_NUMBER)
173
+ throw new RangeError(`The text is ${textBytes.length} bytes; a QR code here holds at most ${QR_CODE_LIMITS.maximumTextBytes}.`);
174
+ // The bit stream: byte mode, the length, the bytes, a terminator, then padding.
175
+ const capacityBitCount = dataCodewordCount(versionNumber) * 8;
176
+ const bits = [];
177
+ const append = (bitsNumber, bitCount) => {
178
+ for (let bitIndex = bitCount - 1; bitIndex >= 0; bitIndex--)
179
+ bits.push((bitsNumber >>> bitIndex) & 1);
180
+ };
181
+ append(0b0100, 4);
182
+ append(textBytes.length, versionNumber < 10 ? 8 : 16);
183
+ for (const textByte of textBytes)
184
+ append(textByte, 8);
185
+ append(0, Math.min(4, capacityBitCount - bits.length));
186
+ append(0, (8 - (bits.length % 8)) % 8);
187
+ for (let padByte = 0xec; bits.length < capacityBitCount; padByte ^= 0xec ^ 0x11)
188
+ append(padByte, 8);
189
+ const dataCodewords = [];
190
+ for (let bitIndex = 0; bitIndex < bits.length; bitIndex += 8)
191
+ dataCodewords.push(bits.slice(bitIndex, bitIndex + 8).reduce((byteNumber, bitNumber) => (byteNumber << 1) | bitNumber, 0));
192
+ const codewords = withErrorCorrection(dataCodewords, versionNumber);
193
+ const sideModuleCount = versionNumber * 4 + 17;
194
+ const modules = Array.from({ length: sideModuleCount }, () => new Array(sideModuleCount).fill(false));
195
+ const isFunction = Array.from({ length: sideModuleCount }, () => new Array(sideModuleCount).fill(false));
196
+ const setFunction = (columnIndex, rowIndex, isDark) => {
197
+ modules[rowIndex][columnIndex] = isDark;
198
+ isFunction[rowIndex][columnIndex] = true;
199
+ };
200
+ for (let moduleIndex = 0; moduleIndex < sideModuleCount; moduleIndex++) {
201
+ setFunction(6, moduleIndex, moduleIndex % 2 === 0);
202
+ setFunction(moduleIndex, 6, moduleIndex % 2 === 0);
203
+ }
204
+ const finderCenters = [[3, 3], [sideModuleCount - 4, 3], [3, sideModuleCount - 4]];
205
+ for (const [centerColumnIndex, centerRowIndex] of finderCenters) {
206
+ for (let rowOffsetIndex = -4; rowOffsetIndex <= 4; rowOffsetIndex++) {
207
+ for (let columnOffsetIndex = -4; columnOffsetIndex <= 4; columnOffsetIndex++) {
208
+ const columnIndex = centerColumnIndex + columnOffsetIndex;
209
+ const rowIndex = centerRowIndex + rowOffsetIndex;
210
+ const ringIndex = Math.max(Math.abs(columnOffsetIndex), Math.abs(rowOffsetIndex));
211
+ if (columnIndex >= 0 && columnIndex < sideModuleCount && rowIndex >= 0 && rowIndex < sideModuleCount)
212
+ setFunction(columnIndex, rowIndex, ringIndex !== 2 && ringIndex !== 4);
213
+ }
214
+ }
215
+ }
216
+ const alignments = alignmentIndexes(versionNumber, sideModuleCount);
217
+ const lastAlignmentIndex = alignments.length - 1;
218
+ alignments.forEach((centerColumnIndex, alignmentColumnIndex) => alignments.forEach((centerRowIndex, alignmentRowIndex) => {
219
+ const isCorner = (alignmentColumnIndex === 0 && alignmentRowIndex === 0) || (alignmentColumnIndex === 0 && alignmentRowIndex === lastAlignmentIndex) || (alignmentColumnIndex === lastAlignmentIndex && alignmentRowIndex === 0);
220
+ if (isCorner)
221
+ return;
222
+ for (let rowOffsetIndex = -2; rowOffsetIndex <= 2; rowOffsetIndex++) {
223
+ for (let columnOffsetIndex = -2; columnOffsetIndex <= 2; columnOffsetIndex++) {
224
+ setFunction(centerColumnIndex + columnOffsetIndex, centerRowIndex + rowOffsetIndex, Math.max(Math.abs(columnOffsetIndex), Math.abs(rowOffsetIndex)) !== 1);
225
+ }
226
+ }
227
+ }));
228
+ const drawFormat = (maskIndex) => {
229
+ const formatNumber = (MEDIUM_CORRECTION_FORMAT_NUMBER << 3) | maskIndex;
230
+ let remainderNumber = formatNumber;
231
+ for (let stepIndex = 0; stepIndex < 10; stepIndex++)
232
+ remainderNumber = (remainderNumber << 1) ^ ((remainderNumber >>> 9) * 0x537);
233
+ const formatBitsNumber = ((formatNumber << 10) | remainderNumber) ^ 0x5412;
234
+ for (let bitIndex = 0; bitIndex <= 5; bitIndex++)
235
+ setFunction(8, bitIndex, isBitSet(formatBitsNumber, bitIndex));
236
+ setFunction(8, 7, isBitSet(formatBitsNumber, 6));
237
+ setFunction(8, 8, isBitSet(formatBitsNumber, 7));
238
+ setFunction(7, 8, isBitSet(formatBitsNumber, 8));
239
+ for (let bitIndex = 9; bitIndex < 15; bitIndex++)
240
+ setFunction(14 - bitIndex, 8, isBitSet(formatBitsNumber, bitIndex));
241
+ for (let bitIndex = 0; bitIndex < 8; bitIndex++)
242
+ setFunction(sideModuleCount - 1 - bitIndex, 8, isBitSet(formatBitsNumber, bitIndex));
243
+ for (let bitIndex = 8; bitIndex < 15; bitIndex++)
244
+ setFunction(8, sideModuleCount - 15 + bitIndex, isBitSet(formatBitsNumber, bitIndex));
245
+ setFunction(8, sideModuleCount - 8, true);
246
+ };
247
+ drawFormat(0);
248
+ if (versionNumber >= 7) {
249
+ let remainderNumber = versionNumber;
250
+ for (let stepIndex = 0; stepIndex < 12; stepIndex++)
251
+ remainderNumber = (remainderNumber << 1) ^ ((remainderNumber >>> 11) * 0x1f25);
252
+ const versionBitsNumber = (versionNumber << 12) | remainderNumber;
253
+ for (let bitIndex = 0; bitIndex < 18; bitIndex++) {
254
+ const farIndex = sideModuleCount - 11 + (bitIndex % 3);
255
+ const nearIndex = Math.floor(bitIndex / 3);
256
+ setFunction(farIndex, nearIndex, isBitSet(versionBitsNumber, bitIndex));
257
+ setFunction(nearIndex, farIndex, isBitSet(versionBitsNumber, bitIndex));
258
+ }
259
+ }
260
+ // The codewords, zigzagging up and down two columns at a time from the bottom right.
261
+ let codewordBitIndex = 0;
262
+ for (let rightColumnIndex = sideModuleCount - 1; rightColumnIndex >= 1; rightColumnIndex -= 2) {
263
+ if (rightColumnIndex === 6)
264
+ rightColumnIndex = 5;
265
+ for (let stepIndex = 0; stepIndex < sideModuleCount; stepIndex++) {
266
+ for (let pairIndex = 0; pairIndex < 2; pairIndex++) {
267
+ const columnIndex = rightColumnIndex - pairIndex;
268
+ const rowIndex = ((rightColumnIndex + 1) & 2) === 0 ? sideModuleCount - 1 - stepIndex : stepIndex;
269
+ if (!isFunction[rowIndex][columnIndex] && codewordBitIndex < codewords.length * 8) {
270
+ modules[rowIndex][columnIndex] = isBitSet(codewords[codewordBitIndex >>> 3], 7 - (codewordBitIndex & 7));
271
+ codewordBitIndex++;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ // The mask that reads best.
277
+ const masked = (maskIndex) => {
278
+ const copy = modules.map((row) => row.slice());
279
+ for (let rowIndex = 0; rowIndex < sideModuleCount; rowIndex++) {
280
+ for (let columnIndex = 0; columnIndex < sideModuleCount; columnIndex++) {
281
+ if (!isFunction[rowIndex][columnIndex] && MASKS[maskIndex](columnIndex, rowIndex))
282
+ copy[rowIndex][columnIndex] = !copy[rowIndex][columnIndex];
283
+ }
284
+ }
285
+ return copy;
286
+ };
287
+ let bestMaskIndex = 0;
288
+ let bestPointCount = Infinity;
289
+ for (let maskIndex = 0; maskIndex < MASKS.length; maskIndex++) {
290
+ drawFormat(maskIndex);
291
+ const pointCount = penaltyPointCount(masked(maskIndex));
292
+ if (pointCount < bestPointCount) {
293
+ bestMaskIndex = maskIndex;
294
+ bestPointCount = pointCount;
295
+ }
296
+ }
297
+ drawFormat(bestMaskIndex);
298
+ return masked(bestMaskIndex);
299
+ }
300
+ /** The code for `text` as an SVG document: dark modules on white, with the quiet zone, one unit a module. */
301
+ export function qrCodeSvg(text) {
302
+ const modules = qrCodeModules(text);
303
+ const marginModuleCount = QR_CODE_LIMITS.quietZoneModuleCount;
304
+ const sideModuleCount = modules.length + marginModuleCount * 2;
305
+ let path = "";
306
+ modules.forEach((row, rowIndex) => {
307
+ for (let columnIndex = 0; columnIndex < row.length; columnIndex++) {
308
+ if (!row[columnIndex])
309
+ continue;
310
+ let runEndIndex = columnIndex;
311
+ while (runEndIndex + 1 < row.length && row[runEndIndex + 1])
312
+ runEndIndex++;
313
+ const runModuleCount = runEndIndex - columnIndex + 1;
314
+ path += `M${columnIndex + marginModuleCount} ${rowIndex + marginModuleCount}h${runModuleCount}v1h-${runModuleCount}z`;
315
+ columnIndex = runEndIndex;
316
+ }
317
+ });
318
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${sideModuleCount} ${sideModuleCount}" shape-rendering="crispEdges"><rect width="${sideModuleCount}" height="${sideModuleCount}" fill="#fff"/><path d="${path}" fill="#000"/></svg>`;
319
+ }