@yyyu-star/arcade-kit 0.0.0-stage → 0.10.2

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 (69) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/LICENSE +21 -0
  3. package/README.md +441 -2
  4. package/dist/auth.d.ts +66 -0
  5. package/dist/auth.d.ts.map +1 -0
  6. package/dist/auth.js +86 -0
  7. package/dist/auth.js.map +1 -0
  8. package/dist/client.d.ts +194 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +379 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/fun-art.d.ts +41 -0
  13. package/dist/fun-art.d.ts.map +1 -0
  14. package/dist/fun-art.js +141 -0
  15. package/dist/fun-art.js.map +1 -0
  16. package/dist/fun.d.ts +87 -0
  17. package/dist/fun.d.ts.map +1 -0
  18. package/dist/fun.js +512 -0
  19. package/dist/fun.js.map +1 -0
  20. package/dist/game.d.ts +160 -0
  21. package/dist/game.d.ts.map +1 -0
  22. package/dist/game.js +41 -0
  23. package/dist/game.js.map +1 -0
  24. package/dist/guest.d.ts +60 -0
  25. package/dist/guest.d.ts.map +1 -0
  26. package/dist/guest.js +87 -0
  27. package/dist/guest.js.map +1 -0
  28. package/dist/internal.d.ts +33 -0
  29. package/dist/internal.d.ts.map +1 -0
  30. package/dist/internal.js +53 -0
  31. package/dist/internal.js.map +1 -0
  32. package/dist/meter.d.ts +102 -0
  33. package/dist/meter.d.ts.map +1 -0
  34. package/dist/meter.js +207 -0
  35. package/dist/meter.js.map +1 -0
  36. package/dist/protocol.d.ts +227 -0
  37. package/dist/protocol.d.ts.map +1 -0
  38. package/dist/protocol.js +65 -0
  39. package/dist/protocol.js.map +1 -0
  40. package/dist/rng.d.ts +40 -0
  41. package/dist/rng.d.ts.map +1 -0
  42. package/dist/rng.js +119 -0
  43. package/dist/rng.js.map +1 -0
  44. package/dist/room.d.ts +352 -0
  45. package/dist/room.d.ts.map +1 -0
  46. package/dist/room.js +1563 -0
  47. package/dist/room.js.map +1 -0
  48. package/dist/testing.d.ts +125 -0
  49. package/dist/testing.d.ts.map +1 -0
  50. package/dist/testing.js +272 -0
  51. package/dist/testing.js.map +1 -0
  52. package/dist/worker.d.ts +92 -0
  53. package/dist/worker.d.ts.map +1 -0
  54. package/dist/worker.js +184 -0
  55. package/dist/worker.js.map +1 -0
  56. package/package.json +107 -4
  57. package/src/auth.ts +100 -0
  58. package/src/client.ts +438 -0
  59. package/src/fun-art.ts +184 -0
  60. package/src/fun.ts +626 -0
  61. package/src/game.ts +200 -0
  62. package/src/guest.ts +122 -0
  63. package/src/internal.ts +64 -0
  64. package/src/meter.ts +255 -0
  65. package/src/protocol.ts +243 -0
  66. package/src/rng.ts +131 -0
  67. package/src/room.ts +1677 -0
  68. package/src/testing.ts +300 -0
  69. package/src/worker.ts +234 -0
@@ -0,0 +1,92 @@
1
+ import { type GuestOptions, isStandalone, type ModeEnv } from './guest.js';
2
+ import type { MeterEnv } from './meter.js';
3
+ /**
4
+ * 游戏 Worker 的绑定名固定:房间 ROOM,平台 ARCADE(登录闸门已经用这个名字),计量 METER、门槛 KIT_QUOTA(0.2.0,meter.ts)。
5
+ * METER 是房间自己用的(开房占名额、报写入),这里列出来是为了游戏的 Env 一眼看全
6
+ */
7
+ export interface KitEnv extends MeterEnv {
8
+ ROOM: DurableObjectNamespace;
9
+ ARCADE?: Fetcher;
10
+ /** "login" 表示这次部署必须接平台(0.10.0):没绑 ARCADE 时按平台不可用拒绝,不退成游客模式 */
11
+ GATE?: string;
12
+ }
13
+ export interface GateOptions {
14
+ /**
15
+ * Origin 只认这个站点(比如 https://<你的站点>)。不给就认请求自己的 origin(0.9.0):浏览器从哪个站点打开页面、连的就是哪个站点,
16
+ * 别的网站的页面带来的 Origin 对不上。游戏可以把部署时注入的 SITE_ORIGIN 变量传进来,设了就按它
17
+ */
18
+ siteOrigin?: string;
19
+ /** 默认是请求所在站点的 /api/me */
20
+ meUrl?: string;
21
+ /** 独立模式的游客设置(cookie 名、游戏电脑人物的名字当保留昵称,0.10.0);平台模式不看 */
22
+ guest?: GuestOptions;
23
+ }
24
+ export interface CreateOptions extends GateOptions {
25
+ /**
26
+ * 开房时就定下的开局选项(0.4.0,可选):游戏 Worker 从开房请求里取出来原样传进来,房间用 RoomGame.options 校验,
27
+ * 不合格回 400 bad_options、不占名额也不建房间。定下以后不能再改:进房的人都看得到(RoomInfo.opts),开打时 start 带的 opts 不看。
28
+ * 不带就和以前一样,由房主开打时的 start 定
29
+ */
30
+ options?: unknown;
31
+ }
32
+ /**
33
+ * 开房:POST。成功 201 { code };失败回 JSON { error, code? }(protocol.ts 的 CreateError):
34
+ * 别的网站 403 forbidden;没登录 401 login;这个人已经有开着的房间 409 has_room(带那个房间的码);今天开房次数用完 429 daily_limit;
35
+ * 今天联机的人太多 503 busy;开房选项不合格 400 bad_options(0.4.0);平台或计量连不上、房间码连撞 5 次、房间出错 503 unavailable。
36
+ * 额度由房间开房时向计量(env.METER,meter.ts)去占;没绑计量就不限额。
37
+ * 独立模式下没有游客 cookie 就现发一个(任何结果的回话都带上这条 Set-Cookie)。
38
+ */
39
+ export declare function acceptCreate(request: Request, env: KitEnv, opts?: CreateOptions): Promise<Response>;
40
+ /**
41
+ * 进房:WebSocket 升级。认不出人、别的网站、码不对、平台不可用,都是"接受后马上用关闭码关掉"。
42
+ * 独立模式下进房不发游客 cookie(101 回话上的 Set-Cookie 浏览器不一定收):没有就 4401,页面刷新一下首页(loginGate 会发)再来
43
+ */
44
+ export declare function acceptJoin(request: Request, env: KitEnv, code: string, opts?: GateOptions): Promise<Response>;
45
+ /**
46
+ * 登录闸门要的绑定:页面的静态资源 ASSETS、平台 ARCADE;GATE 写 "off" 时不拦(本地开发试页面用)。
47
+ * 没有 ARCADE、GATE 也不是 "login" 是独立模式(0.10.0):不拦,补发游客 cookie
48
+ */
49
+ export interface GateEnv {
50
+ ASSETS: Fetcher;
51
+ ARCADE?: Fetcher;
52
+ GATE?: string;
53
+ }
54
+ export interface LoginGateOptions {
55
+ /** 默认是请求所在站点的 /api/me */
56
+ meUrl?: string;
57
+ /** 平台连不上时回的整页 HTML(503);默认一个不带游戏名的通用页,游戏可以换成自己的标题和配色 */
58
+ downPage?: string;
59
+ /** 独立模式的游客设置(0.10.0):cookie 名要和开房、进房传的一样 */
60
+ guest?: GuestOptions;
61
+ }
62
+ /** 平台连不上时的默认页:不写游戏名,回大厅的链接指向平台首页 */
63
+ export declare const DOWN_PAGE = "<!doctype html>\n<html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<title>\u5E73\u53F0\u6682\u65F6\u8FDE\u4E0D\u4E0A</title>\n<style>body{margin:0;min-height:100vh;min-height:100dvh;display:grid;place-items:center;background:#1b1f2a;color:#eef0f4;font:16px/1.6 system-ui,sans-serif;text-align:center}\na{color:#f3c86b}</style></head>\n<body><main><h1>\u5E73\u53F0\u6682\u65F6\u8FDE\u4E0D\u4E0A</h1><p>\u7A0D\u7B49\u4E00\u4F1A\u513F\u518D\u5237\u65B0\u8BD5\u8BD5\u3002</p><p><a href=\"/\">\u56DE\u5927\u5385</a></p></main></body></html>";
64
+ /**
65
+ * 游戏首页的登录闸门(0.8.0,docs/platform-backend.md 5.8、15.8):游戏 Worker 把首页(`/games/<id>/`、`index.html`)交给它。
66
+ * - 登录了:放行,回 ASSETS 的页面,带上平台续期的 Set-Cookie,不缓存;
67
+ * - 没登录:302 到 `/login?next=<原路径和查询参数>`(平台 0.10.1 的 next,登录完整页跳回来,好友房链接的 ?room= 原样带回),同样带上 Set-Cookie;
68
+ * - 平台不可用:503 整页(`downPage`),`Retry-After: 30`;
69
+ * - `GATE` 是 "off":不问平台,直接回页面;
70
+ * - 独立模式(0.10.0,没有 ARCADE、GATE 不是 "login"):不跳登录,回页面;没有游客 cookie 就补发一个(开房、进房靠它认人)。
71
+ */
72
+ export declare function loginGate(request: Request, env: GateEnv, opts?: LoginGateOptions): Promise<Response>;
73
+ /**
74
+ * 独立部署时站点根上本该归平台的两个路径(0.10.0;`/api/me` 0.10.2)。只在独立模式下回话,其余回 null,游戏接着走自己的路由:
75
+ * - `/`:游戏挂在 `/games/<id>/`,单独部署到 workers.dev 时有人直接打开 `/`,302 到游戏首页(查询参数原样带上);
76
+ * - `/api/me`:页面的 `detectPlatform` 开局问一次。回 200 `{ standalone: true }`(没有 `user`,认作没有平台),
77
+ * 不让它落到静态资源回 404:404 在浏览器控制台每次都是一条红的资源加载错误。
78
+ * 两个路径都要先过 Worker:把 "/"、"/api/me" 加进 wrangler.jsonc 的 `assets.run_worker_first`。
79
+ * 本站部署时它们由平台的路由接,到不了游戏 Worker;就算到了,有平台绑定时这里也回 null
80
+ */
81
+ export declare function standaloneRoot(request: Request, env: ModeEnv, gameId: string): Response | null;
82
+ export { isStandalone };
83
+ /** 带上平台的 Set-Cookie(whoIs 给的 setCookie);按人回的内容一律不缓存 */
84
+ export declare function withCookies(res: Response, setCookie: readonly string[]): Response;
85
+ /** JSON 回话,带上 Set-Cookie、不缓存 */
86
+ export declare function json(body: unknown, status: number, setCookie?: readonly string[]): Response;
87
+ /**
88
+ * 读 JSON 请求体,读不了回 null。游戏自己的接口读请求体往往在认人之前:必须带 content-length 且 1 到 maxBytes 字节才读
89
+ * (分块传的大请求体不带长度,没登录的人也能让这里解析一大段)。浏览器 fetch 发字符串请求体总带 content-length
90
+ */
91
+ export declare function readJson(request: Request, maxBytes: number): Promise<unknown>;
92
+ //# sourceMappingURL=worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,KAAK,OAAO,EAAE,MAAM,YAAY,CAAC;AAG3E,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAG3C;;;GAGG;AACH,MAAM,WAAW,MAAO,SAAQ,QAAQ;IACtC,IAAI,EAAE,sBAAsB,CAAC;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAYD,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAU7G;AAgCD;;;GAGG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAYvH;AASD;;;GAGG;AACH,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED,oCAAoC;AACpC,eAAO,MAAM,SAAS,2nBAKwE,CAAC;AAE/F;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,GAAE,gBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAiB9G;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAM9F;AAED,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,CAKjF;AAED,gCAAgC;AAChC,wBAAgB,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAE,SAAS,MAAM,EAAO,GAAG,QAAQ,CAE/F;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASnF"}
package/dist/worker.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * 游戏 Worker 里开房、进房的两个入口:校验房间码、查 Origin、经平台 /api/me 认人、再找房间,打包在一起。
3
+ * 游戏只要把路由接到这两个函数上,不用自己碰浏览器的原始请求,也就没有机会把它(连同浏览器自己带的头)原样转给房间。
4
+ *
5
+ * 进房被拒时不回 4xx:浏览器的 WebSocket 拿不到 HTTP 状态码,只会看到"连不上",然后一直重连。
6
+ * 这里先接受升级、马上用关闭码关掉,客户端认得这些码:4401 没登录、4403 别的网站、4004 没有这个房间都不再重连
7
+ * (房间自己还会回 4503:今天联机额度快满了,不进新人);
8
+ * 平台暂时不可用是 1013(稍后再试),客户端按退避重连。
9
+ * 独立模式(0.10.0,guest.ts):没有 ARCADE 绑定、GATE 也不是 login 时认游客 cookie(开房时没有就现发一个),
10
+ * 浏览器可以在开房、进房的地址上带 ?name=<昵称>;有 ARCADE 绑定时和以前完全一样。
11
+ * 这是普通 Worker,不是房间,没有 ctx.acceptWebSocket,只能用 server.accept();连接接受了马上关,不留着。
12
+ */
13
+ import { identify, sameOrigin } from './auth.js';
14
+ import { isStandalone } from './guest.js';
15
+ import { CLOSE, ROOM_CODE_RE } from './protocol.js';
16
+ import { identityHeader, KIT_HOST, newRoomCode, roomStub } from './internal.js';
17
+ /** 认人:按部署方式(identify),昵称取地址里的 ?name= */
18
+ function who(request, env, opts, mint) {
19
+ return identify(request, env, {
20
+ ...(opts.meUrl ? { meUrl: opts.meUrl } : {}),
21
+ ...(opts.guest ? { guest: opts.guest } : {}),
22
+ mint,
23
+ nickname: new URL(request.url).searchParams.get('name'),
24
+ });
25
+ }
26
+ /**
27
+ * 开房:POST。成功 201 { code };失败回 JSON { error, code? }(protocol.ts 的 CreateError):
28
+ * 别的网站 403 forbidden;没登录 401 login;这个人已经有开着的房间 409 has_room(带那个房间的码);今天开房次数用完 429 daily_limit;
29
+ * 今天联机的人太多 503 busy;开房选项不合格 400 bad_options(0.4.0);平台或计量连不上、房间码连撞 5 次、房间出错 503 unavailable。
30
+ * 额度由房间开房时向计量(env.METER,meter.ts)去占;没绑计量就不限额。
31
+ * 独立模式下没有游客 cookie 就现发一个(任何结果的回话都带上这条 Set-Cookie)。
32
+ */
33
+ export async function acceptCreate(request, env, opts = {}) {
34
+ if (request.method !== 'POST')
35
+ return new Response('method not allowed', { status: 405 });
36
+ if (!sameOrigin(request, opts.siteOrigin))
37
+ return fail(403, 'forbidden');
38
+ const w = await who(request, env, opts, true);
39
+ if ('error' in w)
40
+ return w.error === 'anonymous' ? fail(401, 'login') : fail(503, 'unavailable');
41
+ const me = w.user;
42
+ // 平台模式不转 Set-Cookie(和以前一样);独立模式现发的游客 cookie 要带回去,不然下次进房认不出这个人
43
+ const cookies = isStandalone(env) ? w.setCookie : [];
44
+ const res = await create(env, me, opts);
45
+ return cookies.length ? withCookies(res, cookies) : res;
46
+ }
47
+ async function create(env, me, opts) {
48
+ for (let i = 0; i < 5; i++) {
49
+ const code = newRoomCode();
50
+ // 走 RPC,不走 fetch:身份是参数,不在任何请求头里
51
+ const stub = roomStub(env.ROOM, code);
52
+ const out = await stub.kitCreate(code, me, opts.options);
53
+ switch (out.r) {
54
+ case 'created':
55
+ return Response.json({ code }, { status: 201 });
56
+ case 'exists':
57
+ continue;
58
+ case 'has_room':
59
+ return fail(409, 'has_room', out.code);
60
+ case 'daily_limit':
61
+ return fail(429, 'daily_limit');
62
+ case 'bad_options':
63
+ return fail(400, 'bad_options');
64
+ case 'busy':
65
+ return fail(503, 'busy');
66
+ default:
67
+ return fail(503, 'unavailable');
68
+ }
69
+ }
70
+ return fail(503, 'unavailable');
71
+ }
72
+ function fail(status, error, code) {
73
+ return Response.json(code ? { error, code } : { error }, { status });
74
+ }
75
+ /**
76
+ * 进房:WebSocket 升级。认不出人、别的网站、码不对、平台不可用,都是"接受后马上用关闭码关掉"。
77
+ * 独立模式下进房不发游客 cookie(101 回话上的 Set-Cookie 浏览器不一定收):没有就 4401,页面刷新一下首页(loginGate 会发)再来
78
+ */
79
+ export async function acceptJoin(request, env, code, opts = {}) {
80
+ if (request.headers.get('upgrade')?.toLowerCase() !== 'websocket')
81
+ return new Response('websocket only', { status: 426 });
82
+ if (!ROOM_CODE_RE.test(code))
83
+ return refuse(CLOSE.noRoom, 'no room');
84
+ if (!sameOrigin(request, opts.siteOrigin))
85
+ return refuse(CLOSE.forbidden, 'bad origin');
86
+ const w = await who(request, env, opts, false);
87
+ if ('error' in w)
88
+ return w.error === 'anonymous' ? refuse(CLOSE.unauthorized, 'login first') : refuse(CLOSE.tryLater, 'try later');
89
+ const me = w.user;
90
+ // 新建请求,只带认好的身份和客户端版本;浏览器带来的 x-kit-user 之类的头一律不转
91
+ const cv = new URL(request.url).searchParams.get('cv') ?? '0';
92
+ return roomStub(env.ROOM, code).fetch(`https://${KIT_HOST}/ws?cv=${encodeURIComponent(cv)}`, {
93
+ headers: { upgrade: 'websocket', 'x-kit-user': identityHeader(me) },
94
+ });
95
+ }
96
+ function refuse(code, reason) {
97
+ const pair = new WebSocketPair();
98
+ pair[1].accept();
99
+ pair[1].close(code, reason);
100
+ return new Response(null, { status: 101, webSocket: pair[0] });
101
+ }
102
+ /** 平台连不上时的默认页:不写游戏名,回大厅的链接指向平台首页 */
103
+ export const DOWN_PAGE = `<!doctype html>
104
+ <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
105
+ <title>平台暂时连不上</title>
106
+ <style>body{margin:0;min-height:100vh;min-height:100dvh;display:grid;place-items:center;background:#1b1f2a;color:#eef0f4;font:16px/1.6 system-ui,sans-serif;text-align:center}
107
+ a{color:#f3c86b}</style></head>
108
+ <body><main><h1>平台暂时连不上</h1><p>稍等一会儿再刷新试试。</p><p><a href="/">回大厅</a></p></main></body></html>`;
109
+ /**
110
+ * 游戏首页的登录闸门(0.8.0,docs/platform-backend.md 5.8、15.8):游戏 Worker 把首页(`/games/<id>/`、`index.html`)交给它。
111
+ * - 登录了:放行,回 ASSETS 的页面,带上平台续期的 Set-Cookie,不缓存;
112
+ * - 没登录:302 到 `/login?next=<原路径和查询参数>`(平台 0.10.1 的 next,登录完整页跳回来,好友房链接的 ?room= 原样带回),同样带上 Set-Cookie;
113
+ * - 平台不可用:503 整页(`downPage`),`Retry-After: 30`;
114
+ * - `GATE` 是 "off":不问平台,直接回页面;
115
+ * - 独立模式(0.10.0,没有 ARCADE、GATE 不是 "login"):不跳登录,回页面;没有游客 cookie 就补发一个(开房、进房靠它认人)。
116
+ */
117
+ export async function loginGate(request, env, opts = {}) {
118
+ if (isStandalone(env)) {
119
+ const g = await identify(request, env, { ...(opts.guest ? { guest: opts.guest } : {}), mint: true });
120
+ return withCookies(await env.ASSETS.fetch(request), 'user' in g ? g.setCookie : []);
121
+ }
122
+ if (env.GATE === 'off')
123
+ return env.ASSETS.fetch(request);
124
+ const who = await identify(request, env, opts.meUrl ? { meUrl: opts.meUrl } : {});
125
+ if ('user' in who)
126
+ return withCookies(await env.ASSETS.fetch(request), who.setCookie);
127
+ if (who.error === 'anonymous') {
128
+ const url = new URL(request.url);
129
+ const next = encodeURIComponent(url.pathname + url.search);
130
+ return withCookies(new Response(null, { status: 302, headers: { location: `/login?next=${next}` } }), who.setCookie);
131
+ }
132
+ return new Response(opts.downPage ?? DOWN_PAGE, {
133
+ status: 503,
134
+ headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store', 'retry-after': '30' },
135
+ });
136
+ }
137
+ /**
138
+ * 独立部署时站点根上本该归平台的两个路径(0.10.0;`/api/me` 0.10.2)。只在独立模式下回话,其余回 null,游戏接着走自己的路由:
139
+ * - `/`:游戏挂在 `/games/<id>/`,单独部署到 workers.dev 时有人直接打开 `/`,302 到游戏首页(查询参数原样带上);
140
+ * - `/api/me`:页面的 `detectPlatform` 开局问一次。回 200 `{ standalone: true }`(没有 `user`,认作没有平台),
141
+ * 不让它落到静态资源回 404:404 在浏览器控制台每次都是一条红的资源加载错误。
142
+ * 两个路径都要先过 Worker:把 "/"、"/api/me" 加进 wrangler.jsonc 的 `assets.run_worker_first`。
143
+ * 本站部署时它们由平台的路由接,到不了游戏 Worker;就算到了,有平台绑定时这里也回 null
144
+ */
145
+ export function standaloneRoot(request, env, gameId) {
146
+ const url = new URL(request.url);
147
+ if (!isStandalone(env) || !/^[a-z0-9-]+$/.test(gameId))
148
+ return null;
149
+ if (url.pathname === '/api/me')
150
+ return Response.json({ standalone: true }, { headers: { 'cache-control': 'no-store' } });
151
+ if (url.pathname !== '/')
152
+ return null;
153
+ return new Response(null, { status: 302, headers: { location: `/games/${gameId}/${url.search}`, 'cache-control': 'no-store' } });
154
+ }
155
+ export { isStandalone };
156
+ /** 带上平台的 Set-Cookie(whoIs 给的 setCookie);按人回的内容一律不缓存 */
157
+ export function withCookies(res, setCookie) {
158
+ const out = new Response(res.body, res);
159
+ out.headers.set('cache-control', 'private, no-store');
160
+ for (const c of setCookie)
161
+ out.headers.append('set-cookie', c);
162
+ return out;
163
+ }
164
+ /** JSON 回话,带上 Set-Cookie、不缓存 */
165
+ export function json(body, status, setCookie = []) {
166
+ return withCookies(Response.json(body, { status }), setCookie);
167
+ }
168
+ /**
169
+ * 读 JSON 请求体,读不了回 null。游戏自己的接口读请求体往往在认人之前:必须带 content-length 且 1 到 maxBytes 字节才读
170
+ * (分块传的大请求体不带长度,没登录的人也能让这里解析一大段)。浏览器 fetch 发字符串请求体总带 content-length
171
+ */
172
+ export async function readJson(request, maxBytes) {
173
+ const len = request.headers.get('content-length');
174
+ const n = len === null ? NaN : Number(len);
175
+ if (!(n > 0 && n <= maxBytes))
176
+ return null;
177
+ try {
178
+ return await request.json();
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ }
184
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker.js","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,109 @@
1
1
  {
2
2
  "name": "@yyyu-star/arcade-kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.10.2",
4
+ "description": "Multiplayer toolkit for small Cloudflare Workers games: Durable Object game rooms, room create/join entry points, browser client, daily write quota, ChaCha20 RNG, platform or standalone (guest) identity. 小游戏共用的联机底座",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "keywords": [
8
+ "cloudflare",
9
+ "workers",
10
+ "durable-objects",
11
+ "websocket",
12
+ "multiplayer",
13
+ "game",
14
+ "room",
15
+ "turn-based",
16
+ "chacha20"
17
+ ],
18
+ "homepage": "https://github.com/yyyu-star/arcade/tree/main/packages/arcade-kit#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/yyyu-star/arcade/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/yyyu-star/arcade.git",
25
+ "directory": "packages/arcade-kit"
26
+ },
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "sideEffects": false,
31
+ "exports": {
32
+ "./room": {
33
+ "types": "./dist/room.d.ts",
34
+ "default": "./dist/room.js"
35
+ },
36
+ "./worker": {
37
+ "types": "./dist/worker.d.ts",
38
+ "default": "./dist/worker.js"
39
+ },
40
+ "./game": {
41
+ "types": "./dist/game.d.ts",
42
+ "default": "./dist/game.js"
43
+ },
44
+ "./client": {
45
+ "types": "./dist/client.d.ts",
46
+ "default": "./dist/client.js"
47
+ },
48
+ "./protocol": {
49
+ "types": "./dist/protocol.d.ts",
50
+ "default": "./dist/protocol.js"
51
+ },
52
+ "./meter": {
53
+ "types": "./dist/meter.d.ts",
54
+ "default": "./dist/meter.js"
55
+ },
56
+ "./rng": {
57
+ "types": "./dist/rng.d.ts",
58
+ "default": "./dist/rng.js"
59
+ },
60
+ "./auth": {
61
+ "types": "./dist/auth.d.ts",
62
+ "default": "./dist/auth.js"
63
+ },
64
+ "./fun": {
65
+ "types": "./dist/fun.d.ts",
66
+ "default": "./dist/fun.js"
67
+ },
68
+ "./testing": {
69
+ "types": "./dist/testing.d.ts",
70
+ "default": "./dist/testing.js"
71
+ },
72
+ "./guest": {
73
+ "types": "./dist/guest.d.ts",
74
+ "default": "./dist/guest.js"
75
+ }
76
+ },
77
+ "files": [
78
+ "dist",
79
+ "src",
80
+ "!src/**/*.test.ts",
81
+ "CHANGELOG.md"
82
+ ],
83
+ "publishConfig": {
84
+ "access": "public"
85
+ },
86
+ "scripts": {
87
+ "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
88
+ "typecheck": "tsc --noEmit && tsc --noEmit -p test",
89
+ "check:dist": "npm run build && node scripts/manifest.mjs check",
90
+ "test": "npm run typecheck && vitest run && npm run check:dist",
91
+ "prepack": "npm run build && node scripts/manifest.mjs pack",
92
+ "postpack": "node scripts/manifest.mjs restore"
93
+ },
94
+ "peerDependencies": {
95
+ "@cloudflare/workers-types": ">=4"
96
+ },
97
+ "peerDependenciesMeta": {
98
+ "@cloudflare/workers-types": {
99
+ "optional": true
100
+ }
101
+ },
102
+ "devDependencies": {
103
+ "@cloudflare/vitest-plugin": "^1.4.1",
104
+ "@cloudflare/workers-types": "^5.20261014.1",
105
+ "typescript": "^7.0.2",
106
+ "vitest": "^5.0.4",
107
+ "wrangler": "^4.150.0"
108
+ }
109
+ }
package/src/auth.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * 进房认人(游戏 Worker 里用):WebSocket 升级请求带着平台的登录 cookie,游戏 Worker 经服务绑定 ARCADE 问一次平台 GET /api/me,
3
+ * 和游戏登录闸门一个做法(docs/platform-backend.md 15.8):
4
+ * - 请求里没有平台的会话 cookie(SESSION_COOKIE)就直接算"没登录",不问平台:没登录的脚本刷进房地址,不占平台的计量(0.1.1);
5
+ * - 只转发 Cookie 和 cf-connecting-ip 两个头,3 秒超时;
6
+ * - 200 且 user 不为 null 才认,拿 user.id 和昵称交给房间;200 且 user 为 null 是"没登录";
7
+ * - 其他状态码、不是 JSON、缺字段、超时、绑定不存在都是"平台暂时不可用":同样拒绝(fail-closed),但和"没登录"分开,
8
+ * 进房时回 1013(稍后再试,客户端会重连),不回 4401(不重连),平台抖一下不至于把正在打牌的人踢走;
9
+ * - 地址按请求自己的 origin 拼(`<请求的 origin>/api/me`,0.9.0):游戏和平台挂在同一个站点下,平台按主机名认是不是本站;
10
+ * kit 里不写死站点地址,测试或特殊部署可以用 meUrl 覆盖;
11
+ * - 平台续期或清掉失效会话时回的 Set-Cookie 原样带回来(setCookie),游戏的登录闸门、自己的接口在放行和"没登录"时转给浏览器,
12
+ * 只玩游戏、不回平台页的人 cookie 也能续上(15.8)。进房用不上,不转。
13
+ *
14
+ * 0.10.0 起有独立模式(guest.ts):游戏不带平台单独部署时认游客,按部署方式选哪种认法的是 identify;whoIs 本身不变,只问平台。
15
+ *
16
+ * 0.3.1 起作为 `arcade-kit/auth` 导出:游戏自己的接口用同一份,不再各抄一份;登录闸门 0.8.0 起直接用 `arcade-kit/worker` 的 `loginGate`。
17
+ *
18
+ * 第一版不做 Ed25519 入场票(5.3):好友房都是熟人小范围开房,进房时多问一次 /api/me(算平台 1 次调用,带 cookie 时读 D1 一两行)
19
+ * 换来不用管签名密钥、不用改平台;等开房要走平台登记和配额、或者进房量大到这一次调用显得贵了,再换成入场票,房间这边只是换一种认人的方式。
20
+ */
21
+ import { cleanNickname, guestIdentity, type GuestOptions, isStandalone, mintGuestCookie, type ModeEnv, readGuestCookie } from './guest.js';
22
+ import { hasSessionCookie, type Identity } from './internal.js';
23
+
24
+ /** 平台"我是谁"接口的路径;完整地址按请求的 origin 拼(meUrlOf) */
25
+ export const ME_PATH = '/api/me';
26
+
27
+ /** 这个请求所在站点的 /api/me 地址 */
28
+ export function meUrlOf(request: Request): string {
29
+ return new URL(ME_PATH, request.url).href;
30
+ }
31
+
32
+ export interface WhoIsOptions {
33
+ /** 默认是请求所在站点的 /api/me(meUrlOf) */
34
+ meUrl?: string;
35
+ timeoutMs?: number;
36
+ }
37
+
38
+ export type WhoIs = { user: Identity; setCookie: string[] } | { error: 'anonymous'; setCookie: string[] } | { error: 'unavailable' };
39
+
40
+ const UNAVAILABLE: WhoIs = { error: 'unavailable' };
41
+
42
+ export type { Identity };
43
+
44
+ /** 认人。认不出来时分"没登录"(anonymous)和"平台暂时不可用"(unavailable),两种调用方都要拒绝。 */
45
+ export async function whoIs(request: Request, arcade: Fetcher | undefined, opts: WhoIsOptions = {}): Promise<WhoIs> {
46
+ if (!hasSessionCookie(request.headers.get('cookie'))) return { error: 'anonymous', setCookie: [] };
47
+ if (!arcade) return UNAVAILABLE;
48
+ try {
49
+ const res = await arcade.fetch(opts.meUrl ?? meUrlOf(request), {
50
+ headers: {
51
+ cookie: request.headers.get('cookie') ?? '',
52
+ 'cf-connecting-ip': request.headers.get('cf-connecting-ip') ?? '',
53
+ },
54
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 3_000),
55
+ });
56
+ if (res.status !== 200) return UNAVAILABLE;
57
+ const body = (await res.json()) as { user?: { id?: unknown; displayName?: unknown } | null };
58
+ if (!body || typeof body !== 'object' || !('user' in body)) return UNAVAILABLE;
59
+ const setCookie = res.headers.getSetCookie();
60
+ const u = body.user;
61
+ if (u === null) return { error: 'anonymous', setCookie };
62
+ if (!u || typeof u.id !== 'string' || !u.id || typeof u.displayName !== 'string') return UNAVAILABLE;
63
+ return { user: { userId: u.id, name: u.displayName }, setCookie };
64
+ } catch {
65
+ return UNAVAILABLE;
66
+ }
67
+ }
68
+
69
+ export interface IdentifyOptions extends WhoIsOptions {
70
+ /** 独立模式的游客设置(cookie 名、游戏的保留昵称) */
71
+ guest?: GuestOptions;
72
+ /** 独立模式下没有游客 cookie 时现发一个(放进 setCookie,调用方要转给浏览器);不给就算"没登录" */
73
+ mint?: boolean;
74
+ /** 独立模式下浏览器带来的昵称(没清洗过的原样),不合格就用默认的"游客1234";平台模式不看 */
75
+ nickname?: string | null;
76
+ }
77
+
78
+ /**
79
+ * 按部署方式认人(0.10.0):
80
+ * - 平台模式(有 ARCADE 绑定,或者 GATE=login):就是 whoIs,游客 cookie 一律不看;GATE=login 却没绑 ARCADE 时 whoIs 回 unavailable(fail-closed);
81
+ * - 独立模式(guest.ts 的 isStandalone):认游客 cookie;没有时 mint 为 true 就现发一个,否则算 anonymous。
82
+ */
83
+ export async function identify(request: Request, env: ModeEnv & { ARCADE?: Fetcher }, opts: IdentifyOptions = {}): Promise<WhoIs> {
84
+ if (!isStandalone(env)) return whoIs(request, env.ARCADE, opts.meUrl ? { meUrl: opts.meUrl } : {});
85
+ const nickname = opts.nickname == null ? null : cleanNickname(opts.nickname, opts.guest?.reserved);
86
+ const have = readGuestCookie(request, opts.guest);
87
+ if (have) return { user: await guestIdentity(have, nickname), setCookie: [] };
88
+ if (!opts.mint) return { error: 'anonymous', setCookie: [] };
89
+ const fresh = mintGuestCookie(request, opts.guest);
90
+ return { user: await guestIdentity(fresh.value, nickname), setCookie: [fresh.setCookie] };
91
+ }
92
+
93
+ /**
94
+ * WebSocket 不受同源策略管,别的网站的页面也能往这里连(浏览器会带上能带的 cookie):升级请求的 Origin 必须是本站。
95
+ * 没带 Origin 的(不是浏览器)也拒绝。
96
+ * 本站默认就是请求自己的 origin(0.9.0:kit 不写死站点地址);部署时想更严,可以传 siteOrigin(比如由 SITE_ORIGIN 变量注入)
97
+ */
98
+ export function sameOrigin(request: Request, siteOrigin?: string): boolean {
99
+ return request.headers.get('origin') === (siteOrigin ?? new URL(request.url).origin);
100
+ }