@artenx/dshb 0.0.3 → 0.1.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 (166) hide show
  1. package/cordis.patch.yml +8 -49
  2. package/lib/auth/credentials.d.ts +30 -0
  3. package/lib/auth/credentials.js +121 -0
  4. package/lib/auth/credentials.js.map +1 -0
  5. package/lib/auth/decorate-webserver.d.ts +7 -0
  6. package/lib/auth/decorate-webserver.js +75 -0
  7. package/lib/auth/decorate-webserver.js.map +1 -0
  8. package/lib/auth/gate.d.ts +14 -0
  9. package/lib/auth/gate.js +45 -0
  10. package/lib/auth/gate.js.map +1 -0
  11. package/lib/auth/index.d.ts +8 -0
  12. package/lib/auth/index.js +51 -0
  13. package/lib/auth/index.js.map +1 -0
  14. package/lib/auth/login-page.d.ts +5 -0
  15. package/lib/auth/login-page.js +87 -0
  16. package/lib/auth/login-page.js.map +1 -0
  17. package/lib/auth/loopback-compat.d.ts +6 -0
  18. package/lib/auth/loopback-compat.js +94 -0
  19. package/lib/auth/loopback-compat.js.map +1 -0
  20. package/lib/auth/loopback.d.ts +7 -0
  21. package/lib/auth/loopback.js +24 -0
  22. package/lib/auth/loopback.js.map +1 -0
  23. package/lib/auth/ratelimit.d.ts +11 -0
  24. package/lib/auth/ratelimit.js +37 -0
  25. package/lib/auth/ratelimit.js.map +1 -0
  26. package/lib/auth/routes.d.ts +8 -0
  27. package/lib/auth/routes.js +224 -0
  28. package/lib/auth/routes.js.map +1 -0
  29. package/lib/auth/session.d.ts +11 -0
  30. package/lib/auth/session.js +52 -0
  31. package/lib/auth/session.js.map +1 -0
  32. package/lib/auth/startup.d.ts +15 -0
  33. package/lib/auth/startup.js +68 -0
  34. package/lib/auth/startup.js.map +1 -0
  35. package/lib/auth/webserver.d.ts +18 -0
  36. package/lib/auth/webserver.js +60 -0
  37. package/lib/auth/webserver.js.map +1 -0
  38. package/lib/client.js +2770 -0
  39. package/lib/client.js.map +1 -0
  40. package/lib/connection-sIX25pkS.d.ts +46 -0
  41. package/lib/core/audit.d.ts +16 -0
  42. package/lib/core/audit.js +38 -0
  43. package/lib/core/audit.js.map +1 -0
  44. package/lib/core/index.d.ts +8 -0
  45. package/lib/core/index.js +77 -0
  46. package/lib/core/index.js.map +1 -0
  47. package/lib/core/known-hosts.d.ts +23 -0
  48. package/lib/core/known-hosts.js +76 -0
  49. package/lib/core/known-hosts.js.map +1 -0
  50. package/lib/core/node-registry.d.ts +108 -0
  51. package/lib/core/node-registry.js +191 -0
  52. package/lib/core/node-registry.js.map +1 -0
  53. package/lib/core/routes.d.ts +9 -0
  54. package/lib/core/routes.js +404 -0
  55. package/lib/core/routes.js.map +1 -0
  56. package/lib/core/ssh-config.d.ts +23 -0
  57. package/lib/core/ssh-config.js +70 -0
  58. package/lib/core/ssh-config.js.map +1 -0
  59. package/lib/core/test.d.ts +43 -0
  60. package/lib/core/test.js +116 -0
  61. package/lib/core/test.js.map +1 -0
  62. package/lib/core/workspace-bindings.d.ts +28 -0
  63. package/lib/core/workspace-bindings.js +86 -0
  64. package/lib/core/workspace-bindings.js.map +1 -0
  65. package/lib/core/workspaces.d.ts +9 -0
  66. package/lib/core/workspaces.js +283 -0
  67. package/lib/core/workspaces.js.map +1 -0
  68. package/lib/exec-docker/daemon-runner.d.ts +21 -0
  69. package/lib/exec-docker/daemon-runner.js +326 -0
  70. package/lib/exec-docker/daemon-runner.js.map +1 -0
  71. package/lib/exec-docker/docker-backend.d.ts +48 -0
  72. package/lib/exec-docker/docker-backend.js +1 -0
  73. package/lib/exec-docker/docker-client.d.ts +46 -0
  74. package/lib/exec-docker/docker-client.js +302 -0
  75. package/lib/exec-docker/docker-client.js.map +1 -0
  76. package/lib/exec-docker/docker-routes.d.ts +6 -0
  77. package/lib/exec-docker/docker-routes.js +215 -0
  78. package/lib/exec-docker/docker-routes.js.map +1 -0
  79. package/lib/exec-docker/fs-provider.d.ts +50 -0
  80. package/lib/exec-docker/fs-provider.js +92 -0
  81. package/lib/exec-docker/fs-provider.js.map +1 -0
  82. package/lib/exec-docker/host-runner.d.ts +29 -0
  83. package/lib/exec-docker/host-runner.js +17 -0
  84. package/lib/exec-docker/host-runner.js.map +1 -0
  85. package/lib/exec-docker/index.d.ts +21 -0
  86. package/lib/exec-docker/index.js +137 -0
  87. package/lib/exec-docker/index.js.map +1 -0
  88. package/lib/exec-docker/process-provider.d.ts +20 -0
  89. package/lib/exec-docker/process-provider.js +75 -0
  90. package/lib/exec-docker/process-provider.js.map +1 -0
  91. package/lib/exec-docker/provision.d.ts +24 -0
  92. package/lib/exec-docker/provision.js +103 -0
  93. package/lib/exec-docker/provision.js.map +1 -0
  94. package/lib/exec-docker/remote-docker-cli.d.ts +52 -0
  95. package/lib/exec-docker/remote-docker-cli.js +155 -0
  96. package/lib/exec-docker/remote-docker-cli.js.map +1 -0
  97. package/lib/exec-docker/world.d.ts +37 -0
  98. package/lib/exec-docker/world.js +62 -0
  99. package/lib/exec-docker/world.js.map +1 -0
  100. package/lib/exec-ssh/connection-pool.d.ts +25 -0
  101. package/lib/exec-ssh/connection-pool.js +54 -0
  102. package/lib/exec-ssh/connection-pool.js.map +1 -0
  103. package/lib/exec-ssh/connection.d.ts +2 -0
  104. package/lib/exec-ssh/connection.js +125 -0
  105. package/lib/exec-ssh/connection.js.map +1 -0
  106. package/lib/exec-ssh/env-scrub.d.ts +7 -0
  107. package/lib/exec-ssh/env-scrub.js +40 -0
  108. package/lib/exec-ssh/env-scrub.js.map +1 -0
  109. package/lib/exec-ssh/executor.d.ts +46 -0
  110. package/lib/exec-ssh/executor.js +212 -0
  111. package/lib/exec-ssh/executor.js.map +1 -0
  112. package/lib/exec-ssh/fs-provider.d.ts +50 -0
  113. package/lib/exec-ssh/fs-provider.js +92 -0
  114. package/lib/exec-ssh/fs-provider.js.map +1 -0
  115. package/lib/exec-ssh/index.d.ts +16 -0
  116. package/lib/exec-ssh/index.js +30 -0
  117. package/lib/exec-ssh/index.js.map +1 -0
  118. package/lib/exec-ssh/process-provider.d.ts +20 -0
  119. package/lib/exec-ssh/process-provider.js +75 -0
  120. package/lib/exec-ssh/process-provider.js.map +1 -0
  121. package/lib/exec-ssh/runner.d.ts +13 -0
  122. package/lib/exec-ssh/runner.js +62 -0
  123. package/lib/exec-ssh/runner.js.map +1 -0
  124. package/lib/exec-ssh/world.d.ts +79 -0
  125. package/lib/exec-ssh/world.js +95 -0
  126. package/lib/exec-ssh/world.js.map +1 -0
  127. package/lib/index.d.ts +2 -0
  128. package/lib/index.js +2 -0
  129. package/lib/router/decorate-fs.d.ts +6 -0
  130. package/lib/router/decorate-fs.js +78 -0
  131. package/lib/router/decorate-fs.js.map +1 -0
  132. package/lib/router/decorate-shell.d.ts +6 -0
  133. package/lib/router/decorate-shell.js +35 -0
  134. package/lib/router/decorate-shell.js.map +1 -0
  135. package/lib/router/decorate-subprocess.d.ts +6 -0
  136. package/lib/router/decorate-subprocess.js +35 -0
  137. package/lib/router/decorate-subprocess.js.map +1 -0
  138. package/lib/router/index.d.ts +10 -0
  139. package/lib/router/index.js +21 -0
  140. package/lib/router/index.js.map +1 -0
  141. package/lib/router/resolve.d.ts +22 -0
  142. package/lib/router/resolve.js +45 -0
  143. package/lib/router/resolve.js.map +1 -0
  144. package/lib/router/router-fs.d.ts +25 -0
  145. package/lib/router/router-fs.js +64 -0
  146. package/lib/router/router-fs.js.map +1 -0
  147. package/lib/router/router-shell.d.ts +14 -0
  148. package/lib/router/router-shell.js +36 -0
  149. package/lib/router/router-shell.js.map +1 -0
  150. package/lib/router/router-subprocess.d.ts +13 -0
  151. package/lib/router/router-subprocess.js +33 -0
  152. package/lib/router/router-subprocess.js.map +1 -0
  153. package/lib/router/types.d.ts +2 -0
  154. package/lib/router/types.js +1 -0
  155. package/lib/types-C0TtEO7z.d.ts +67 -0
  156. package/lib/ui/decorate-picker.d.ts +6 -0
  157. package/lib/ui/decorate-picker.js +58 -0
  158. package/lib/ui/decorate-picker.js.map +1 -0
  159. package/lib/ui/index.d.ts +8 -0
  160. package/lib/ui/index.js +11 -0
  161. package/lib/ui/index.js.map +1 -0
  162. package/lib/ui/picker.d.ts +10 -0
  163. package/lib/ui/picker.js +58 -0
  164. package/lib/ui/picker.js.map +1 -0
  165. package/package.json +158 -9
  166. package/LICENSE +0 -21
@@ -0,0 +1,94 @@
1
+ //#region src/auth/loopback-compat.ts
2
+ const MOBILE_CSS = `
3
+ <style id="dshb-mobile-fix">
4
+ @media (max-width:1023px),(display-mode:standalone){
5
+ html{-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}
6
+ [class*="_command_1gdtu_"]{min-width:max-content!important;max-width:none!important;overflow:visible!important;white-space:pre!important;text-overflow:clip!important;max-height:999999px!important}
7
+ [class*="_prompt_1gdtu_"],[class*="_promptLine_1gdtu_"]{min-width:max-content!important;max-height:999999px!important}
8
+ [class*="_header_1gdtu_"]{min-width:0!important;max-width:100%!important;overflow-x:auto!important;overscroll-behavior-x:contain;touch-action:pan-x pan-y!important}
9
+ [class*="_output_1gdtu_"]{overflow-x:auto!important;overflow-y:auto!important;-webkit-overflow-scrolling:touch;touch-action:pan-x pan-y!important}
10
+ [class*="_line_1gdtu_"]{min-width:max-content!important;overflow-x:visible!important;max-height:999999px!important}
11
+ [class*="_copyButton_1gdtu_"]{position:sticky!important;right:0}
12
+ [class*="7KE1Ra_modelName"],[class*="7KE1Ra_description"],[class*="modelName"]{white-space:normal!important;overflow-wrap:break-word;text-overflow:clip!important;overflow:visible!important}
13
+ [class*="7KE1Ra_menu"],[class*="_menu"]:has([class*="modelName"]){left:0!important;right:auto!important;width:calc(100vw - 32px)!important;min-width:0!important;max-width:calc(100vw - 32px)!important}
14
+ [class*="_copyButton_"]{touch-action:manipulation!important}
15
+ [class*="_copyAnchor_"],[class*="_body_1ye18_"],[class*="_root_4qrvp_"]{touch-action:pan-x pan-y!important}
16
+ [class*="_root_4qrvp_1"]{width:100%!important;min-width:0!important;overflow-x:auto!important}
17
+ [class*="_container_4qrvp_"]{width:auto!important;min-width:100%!important;max-width:none!important}
18
+ [class*="_expandedTopLevel_4qrvp_"]{width:auto!important;min-width:100%!important;max-width:none!important}
19
+ [class*="_body_biesw_"],[class*="_body_srovd_"],[class*="_body_s66q0_"]{overflow-x:auto!important;-webkit-overflow-scrolling:touch}
20
+ [class*="_line_biesw_"],[class*="_line_srovd_"],[class*="_line_s66q0_"]{min-width:max-content!important}
21
+ [class*="_block_1gdtu_"]{padding-left:0!important;overflow-x:auto!important}
22
+ }
23
+ </style>`;
24
+ function loopbackCompatScript() {
25
+ return `<script>
26
+ ;(function () {
27
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function' && typeof crypto.getRandomValues === 'function') {
28
+ crypto.randomUUID = function () {
29
+ var b = crypto.getRandomValues(new Uint8Array(16))
30
+ b[6] = (b[6] & 0x0f) | 0x40
31
+ b[8] = (b[8] & 0x3f) | 0x80
32
+ var h = ''
33
+ for (var i = 0; i < 16; i++) {
34
+ if (i === 4 || i === 6 || i === 8 || i === 10) h += '-'
35
+ var hex = b[i].toString(16)
36
+ if (hex.length < 2) hex = '0' + hex
37
+ h += hex
38
+ }
39
+ return h
40
+ }
41
+ }
42
+ function installIsLoopbackOverride() {
43
+ var loader = window.__ModuleLoader__
44
+ if (!loader || loader.__dshbIsLoopbackHooked) return false
45
+ if (loader.mode !== 'live') return false
46
+ loader.__dshbIsLoopbackHooked = true
47
+ var origLoad = loader.load.bind(loader)
48
+ loader.load = function (handoff) {
49
+ var factory = handoff && handoff.factory
50
+ if (typeof factory === 'function') {
51
+ handoff.factory = function (require) {
52
+ var exports = factory(require)
53
+ var apply = exports && exports.apply
54
+ if (typeof apply === 'function') {
55
+ exports.apply = function (ctx) {
56
+ var result = apply(ctx)
57
+ try {
58
+ var connection = ctx && ctx.get && ctx.get('connection')
59
+ if (connection) {
60
+ Object.defineProperty(connection, 'isLoopback', { configurable: true, get: function () { return true } })
61
+ }
62
+ } catch (e) {}
63
+ return result
64
+ }
65
+ }
66
+ return exports
67
+ }
68
+ }
69
+ return origLoad(handoff)
70
+ }
71
+ return true
72
+ }
73
+ function tryInstall() { if (!installIsLoopbackOverride()) setTimeout(tryInstall, 0) }
74
+ tryInstall()
75
+ })()
76
+ ;(async function () {
77
+ try {
78
+ var res = await fetch('/api/auth/status')
79
+ var data = await res.json()
80
+ if (window.location.pathname !== '/login' && !data.authenticated) {
81
+ window.location.replace('/login')
82
+ return
83
+ }
84
+ } catch (e) {}
85
+ })()
86
+ <\/script>`;
87
+ }
88
+ function installLoopbackCompat(html) {
89
+ return html.replace("</head>", `${MOBILE_CSS}${loopbackCompatScript()}</head>`);
90
+ }
91
+ //#endregion
92
+ export { installLoopbackCompat, loopbackCompatScript };
93
+
94
+ //# sourceMappingURL=loopback-compat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback-compat.js","names":[],"sources":["../../src/auth/loopback-compat.ts"],"sourcesContent":["const MOBILE_CSS = `\n<style id=\"dshb-mobile-fix\">\n@media (max-width:1023px),(display-mode:standalone){\nhtml{-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}\n[class*=\"_command_1gdtu_\"]{min-width:max-content!important;max-width:none!important;overflow:visible!important;white-space:pre!important;text-overflow:clip!important;max-height:999999px!important}\n[class*=\"_prompt_1gdtu_\"],[class*=\"_promptLine_1gdtu_\"]{min-width:max-content!important;max-height:999999px!important}\n[class*=\"_header_1gdtu_\"]{min-width:0!important;max-width:100%!important;overflow-x:auto!important;overscroll-behavior-x:contain;touch-action:pan-x pan-y!important}\n[class*=\"_output_1gdtu_\"]{overflow-x:auto!important;overflow-y:auto!important;-webkit-overflow-scrolling:touch;touch-action:pan-x pan-y!important}\n[class*=\"_line_1gdtu_\"]{min-width:max-content!important;overflow-x:visible!important;max-height:999999px!important}\n[class*=\"_copyButton_1gdtu_\"]{position:sticky!important;right:0}\n[class*=\"7KE1Ra_modelName\"],[class*=\"7KE1Ra_description\"],[class*=\"modelName\"]{white-space:normal!important;overflow-wrap:break-word;text-overflow:clip!important;overflow:visible!important}\n[class*=\"7KE1Ra_menu\"],[class*=\"_menu\"]:has([class*=\"modelName\"]){left:0!important;right:auto!important;width:calc(100vw - 32px)!important;min-width:0!important;max-width:calc(100vw - 32px)!important}\n[class*=\"_copyButton_\"]{touch-action:manipulation!important}\n[class*=\"_copyAnchor_\"],[class*=\"_body_1ye18_\"],[class*=\"_root_4qrvp_\"]{touch-action:pan-x pan-y!important}\n[class*=\"_root_4qrvp_1\"]{width:100%!important;min-width:0!important;overflow-x:auto!important}\n[class*=\"_container_4qrvp_\"]{width:auto!important;min-width:100%!important;max-width:none!important}\n[class*=\"_expandedTopLevel_4qrvp_\"]{width:auto!important;min-width:100%!important;max-width:none!important}\n[class*=\"_body_biesw_\"],[class*=\"_body_srovd_\"],[class*=\"_body_s66q0_\"]{overflow-x:auto!important;-webkit-overflow-scrolling:touch}\n[class*=\"_line_biesw_\"],[class*=\"_line_srovd_\"],[class*=\"_line_s66q0_\"]{min-width:max-content!important}\n[class*=\"_block_1gdtu_\"]{padding-left:0!important;overflow-x:auto!important}\n}\n</style>`\n\nexport function loopbackCompatScript(): string {\n return `<script>\n;(function () {\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID !== 'function' && typeof crypto.getRandomValues === 'function') {\n crypto.randomUUID = function () {\n var b = crypto.getRandomValues(new Uint8Array(16))\n b[6] = (b[6] & 0x0f) | 0x40\n b[8] = (b[8] & 0x3f) | 0x80\n var h = ''\n for (var i = 0; i < 16; i++) {\n if (i === 4 || i === 6 || i === 8 || i === 10) h += '-'\n var hex = b[i].toString(16)\n if (hex.length < 2) hex = '0' + hex\n h += hex\n }\n return h\n }\n }\n function installIsLoopbackOverride() {\n var loader = window.__ModuleLoader__\n if (!loader || loader.__dshbIsLoopbackHooked) return false\n if (loader.mode !== 'live') return false\n loader.__dshbIsLoopbackHooked = true\n var origLoad = loader.load.bind(loader)\n loader.load = function (handoff) {\n var factory = handoff && handoff.factory\n if (typeof factory === 'function') {\n handoff.factory = function (require) {\n var exports = factory(require)\n var apply = exports && exports.apply\n if (typeof apply === 'function') {\n exports.apply = function (ctx) {\n var result = apply(ctx)\n try {\n var connection = ctx && ctx.get && ctx.get('connection')\n if (connection) {\n Object.defineProperty(connection, 'isLoopback', { configurable: true, get: function () { return true } })\n }\n } catch (e) {}\n return result\n }\n }\n return exports\n }\n }\n return origLoad(handoff)\n }\n return true\n }\n function tryInstall() { if (!installIsLoopbackOverride()) setTimeout(tryInstall, 0) }\n tryInstall()\n})()\n;(async function () {\n try {\n var res = await fetch('/api/auth/status')\n var data = await res.json()\n if (window.location.pathname !== '/login' && !data.authenticated) {\n window.location.replace('/login')\n return\n }\n } catch (e) {}\n})()\n</script>`\n}\n\nexport function installLoopbackCompat(html: string): string {\n return html.replace('</head>', `${MOBILE_CSS}${loopbackCompatScript()}</head>`)\n}\n"],"mappings":";AAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;AAuBnB,SAAgB,uBAA+B;CAC7C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8DT;AAEA,SAAgB,sBAAsB,MAAsB;CAC1D,OAAO,KAAK,QAAQ,WAAW,GAAG,aAAa,qBAAqB,EAAE,QAAQ;AAChF"}
@@ -0,0 +1,7 @@
1
+ //#region src/auth/loopback.d.ts
2
+ declare function isLoopbackAddress(address: string | undefined | null): boolean;
3
+ declare function isLoopbackHost(hostHeader: string | undefined | null): boolean;
4
+ declare function isLoopbackRequest(remoteAddress: string | undefined | null, hostHeader: string | undefined | null): boolean;
5
+ //#endregion
6
+ export { isLoopbackAddress, isLoopbackHost, isLoopbackRequest };
7
+ //# sourceMappingURL=loopback.d.ts.map
@@ -0,0 +1,24 @@
1
+ //#region src/auth/loopback.ts
2
+ function isLoopbackAddress(address) {
3
+ if (!address) return false;
4
+ let addr = address.trim().toLowerCase();
5
+ const v4mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/.exec(addr);
6
+ if (v4mapped) addr = v4mapped[1];
7
+ if (addr === "::1" || addr === "::") return true;
8
+ return /^127\./.test(addr);
9
+ }
10
+ function isLoopbackHost(hostHeader) {
11
+ if (!hostHeader) return false;
12
+ const authority = hostHeader.trim().toLowerCase();
13
+ if (!authority) return false;
14
+ const host = authority.startsWith("[") ? authority.slice(1, authority.indexOf("]")) : authority.split(":")[0];
15
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
16
+ return isLoopbackAddress(host);
17
+ }
18
+ function isLoopbackRequest(remoteAddress, hostHeader) {
19
+ return isLoopbackAddress(remoteAddress) && isLoopbackHost(hostHeader);
20
+ }
21
+ //#endregion
22
+ export { isLoopbackAddress, isLoopbackHost, isLoopbackRequest };
23
+
24
+ //# sourceMappingURL=loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.js","names":[],"sources":["../../src/auth/loopback.ts"],"sourcesContent":["export function isLoopbackAddress(address: string | undefined | null): boolean {\n if (!address) return false\n let addr = address.trim().toLowerCase()\n const v4mapped = /^::ffff:(\\d{1,3}(?:\\.\\d{1,3}){3})$/.exec(addr)\n if (v4mapped) addr = v4mapped[1]\n if (addr === '::1' || addr === '::') return true\n return /^127\\./.test(addr)\n}\n\nexport function isLoopbackHost(hostHeader: string | undefined | null): boolean {\n if (!hostHeader) return false\n const authority = hostHeader.trim().toLowerCase()\n if (!authority) return false\n const host = authority.startsWith('[')\n ? authority.slice(1, authority.indexOf(']'))\n : authority.split(':')[0]\n if (host === 'localhost' || host.endsWith('.localhost')) return true\n return isLoopbackAddress(host)\n}\n\nexport function isLoopbackRequest(\n remoteAddress: string | undefined | null,\n hostHeader: string | undefined | null,\n): boolean {\n return isLoopbackAddress(remoteAddress) && isLoopbackHost(hostHeader)\n}\n"],"mappings":";AAAA,SAAgB,kBAAkB,SAA6C;CAC7E,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,OAAO,QAAQ,KAAK,CAAC,CAAC,YAAY;CACtC,MAAM,WAAW,qCAAqC,KAAK,IAAI;CAC/D,IAAI,UAAU,OAAO,SAAS;CAC9B,IAAI,SAAS,SAAS,SAAS,MAAM,OAAO;CAC5C,OAAO,SAAS,KAAK,IAAI;AAC3B;AAEA,SAAgB,eAAe,YAAgD;CAC7E,IAAI,CAAC,YAAY,OAAO;CACxB,MAAM,YAAY,WAAW,KAAK,CAAC,CAAC,YAAY;CAChD,IAAI,CAAC,WAAW,OAAO;CACvB,MAAM,OAAO,UAAU,WAAW,GAAG,IACjC,UAAU,MAAM,GAAG,UAAU,QAAQ,GAAG,CAAC,IACzC,UAAU,MAAM,GAAG,CAAC,CAAC;CACzB,IAAI,SAAS,eAAe,KAAK,SAAS,YAAY,GAAG,OAAO;CAChE,OAAO,kBAAkB,IAAI;AAC/B;AAEA,SAAgB,kBACd,eACA,YACS;CACT,OAAO,kBAAkB,aAAa,KAAK,eAAe,UAAU;AACtE"}
@@ -0,0 +1,11 @@
1
+ //#region src/auth/ratelimit.d.ts
2
+ declare class LoginRateLimiter {
3
+ private readonly entries;
4
+ isLocked(ip: string, now?: number): boolean;
5
+ recordFailure(ip: string, now?: number): void;
6
+ recordSuccess(ip: string): void;
7
+ private sweep;
8
+ }
9
+ //#endregion
10
+ export { LoginRateLimiter };
11
+ //# sourceMappingURL=ratelimit.d.ts.map
@@ -0,0 +1,37 @@
1
+ //#region src/auth/ratelimit.ts
2
+ const WINDOW_MS = 3e4;
3
+ const MAX_FAILURES = 5;
4
+ var LoginRateLimiter = class {
5
+ entries = /* @__PURE__ */ new Map();
6
+ isLocked(ip, now = Date.now()) {
7
+ const entry = this.entries.get(ip);
8
+ if (!entry) return false;
9
+ if (entry.lockedUntil > now) return true;
10
+ if (entry.lockedUntil !== 0 && entry.lockedUntil <= now) this.entries.delete(ip);
11
+ return false;
12
+ }
13
+ recordFailure(ip, now = Date.now()) {
14
+ const entry = this.entries.get(ip) ?? {
15
+ failures: 0,
16
+ lockedUntil: 0
17
+ };
18
+ entry.failures += 1;
19
+ if (entry.failures >= MAX_FAILURES) {
20
+ entry.lockedUntil = now + WINDOW_MS;
21
+ entry.failures = 0;
22
+ }
23
+ this.entries.set(ip, entry);
24
+ this.sweep(now);
25
+ }
26
+ recordSuccess(ip) {
27
+ this.entries.delete(ip);
28
+ }
29
+ sweep(now) {
30
+ if (this.entries.size < 1024) return;
31
+ for (const [ip, entry] of this.entries) if (entry.lockedUntil <= now) this.entries.delete(ip);
32
+ }
33
+ };
34
+ //#endregion
35
+ export { LoginRateLimiter };
36
+
37
+ //# sourceMappingURL=ratelimit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ratelimit.js","names":[],"sources":["../../src/auth/ratelimit.ts"],"sourcesContent":["const WINDOW_MS = 30_000\nconst MAX_FAILURES = 5\n\ninterface Entry {\n failures: number\n lockedUntil: number\n}\n\nexport class LoginRateLimiter {\n private readonly entries = new Map<string, Entry>()\n\n isLocked(ip: string, now = Date.now()): boolean {\n const entry = this.entries.get(ip)\n if (!entry) return false\n if (entry.lockedUntil > now) return true\n if (entry.lockedUntil !== 0 && entry.lockedUntil <= now) this.entries.delete(ip)\n return false\n }\n\n recordFailure(ip: string, now = Date.now()): void {\n const entry = this.entries.get(ip) ?? { failures: 0, lockedUntil: 0 }\n entry.failures += 1\n if (entry.failures >= MAX_FAILURES) {\n entry.lockedUntil = now + WINDOW_MS\n entry.failures = 0\n }\n this.entries.set(ip, entry)\n this.sweep(now)\n }\n\n recordSuccess(ip: string): void {\n this.entries.delete(ip)\n }\n\n private sweep(now: number): void {\n if (this.entries.size < 1024) return\n for (const [ip, entry] of this.entries) {\n if (entry.lockedUntil <= now) this.entries.delete(ip)\n }\n }\n}\n"],"mappings":";AAAA,MAAM,YAAY;AAClB,MAAM,eAAe;AAOrB,IAAa,mBAAb,MAA8B;CAC5B,0BAA2B,IAAI,IAAmB;CAElD,SAAS,IAAY,MAAM,KAAK,IAAI,GAAY;EAC9C,MAAM,QAAQ,KAAK,QAAQ,IAAI,EAAE;EACjC,IAAI,CAAC,OAAO,OAAO;EACnB,IAAI,MAAM,cAAc,KAAK,OAAO;EACpC,IAAI,MAAM,gBAAgB,KAAK,MAAM,eAAe,KAAK,KAAK,QAAQ,OAAO,EAAE;EAC/E,OAAO;CACT;CAEA,cAAc,IAAY,MAAM,KAAK,IAAI,GAAS;EAChD,MAAM,QAAQ,KAAK,QAAQ,IAAI,EAAE,KAAK;GAAE,UAAU;GAAG,aAAa;EAAE;EACpE,MAAM,YAAY;EAClB,IAAI,MAAM,YAAY,cAAc;GAClC,MAAM,cAAc,MAAM;GAC1B,MAAM,WAAW;EACnB;EACA,KAAK,QAAQ,IAAI,IAAI,KAAK;EAC1B,KAAK,MAAM,GAAG;CAChB;CAEA,cAAc,IAAkB;EAC9B,KAAK,QAAQ,OAAO,EAAE;CACxB;CAEA,MAAc,KAAmB;EAC/B,IAAI,KAAK,QAAQ,OAAO,MAAM;EAC9B,KAAK,MAAM,CAAC,IAAI,UAAU,KAAK,SAC7B,IAAI,MAAM,eAAe,KAAK,KAAK,QAAQ,OAAO,EAAE;CAExD;AACF"}
@@ -0,0 +1,8 @@
1
+ import { CredentialStore } from "./credentials.js";
2
+ import { LoginRateLimiter } from "./ratelimit.js";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+ //#region src/auth/routes.d.ts
5
+ declare function registerAuthRoutes(ctx: Context, store: CredentialStore, limiter: LoginRateLimiter): void;
6
+ //#endregion
7
+ export { registerAuthRoutes };
8
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1,224 @@
1
+ import { sanitizeUsername } from "./credentials.js";
2
+ import { isLoopbackRequest } from "./loopback.js";
3
+ import { SESSION_COOKIE, clearCookieHeader, issueSession, readCookie, sessionCookieHeader, verifySession } from "./session.js";
4
+ import { loginPageHtml } from "./login-page.js";
5
+ //#region src/auth/routes.ts
6
+ const MAX_BODY = 65536;
7
+ function sendJson(res, status, body, headers = {}) {
8
+ res.writeHead(status, {
9
+ "content-type": "application/json",
10
+ ...headers
11
+ });
12
+ res.end(JSON.stringify(body));
13
+ }
14
+ function readBody(req) {
15
+ return new Promise((resolve) => {
16
+ if ((req.headers["content-type"] ?? "") !== "application/json") {
17
+ resolve(null);
18
+ return;
19
+ }
20
+ const chunks = [];
21
+ let size = 0;
22
+ req.on("data", (chunk) => {
23
+ size += chunk.length;
24
+ if (size > MAX_BODY) {
25
+ resolve(null);
26
+ req.destroy();
27
+ return;
28
+ }
29
+ chunks.push(chunk);
30
+ });
31
+ req.on("end", () => {
32
+ try {
33
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
34
+ } catch {
35
+ resolve(null);
36
+ }
37
+ });
38
+ req.on("error", () => resolve(null));
39
+ });
40
+ }
41
+ function clientIp(req) {
42
+ return req.socket.remoteAddress ?? "unknown";
43
+ }
44
+ function registerAuthRoutes(ctx, store, limiter) {
45
+ const webServer = ctx.webServer;
46
+ ctx.effect(() => webServer.register({
47
+ kind: "exact",
48
+ path: "/login",
49
+ handler: (_req, res) => {
50
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
51
+ res.end(loginPageHtml());
52
+ }
53
+ }));
54
+ ctx.effect(() => webServer.register({
55
+ kind: "prefix",
56
+ path: "/api/auth",
57
+ handler: async (req, res) => {
58
+ const path = new URL(req.url ?? "/", "http://x").pathname;
59
+ if (path === "/api/auth/status" && req.method === "GET") {
60
+ const loopback = isLoopbackRequest(req.socket.remoteAddress, req.headers.host);
61
+ let authenticated = loopback;
62
+ let username;
63
+ if (!loopback) {
64
+ const secret = store.sessionSecret;
65
+ const currentName = store.username;
66
+ if (secret && currentName) {
67
+ const cookie = readCookie(req.headers.cookie, SESSION_COOKIE);
68
+ if (verifySession(cookie, secret, currentName)) {
69
+ authenticated = true;
70
+ username = currentName;
71
+ }
72
+ }
73
+ }
74
+ sendJson(res, 200, {
75
+ ok: true,
76
+ authenticated,
77
+ initialized: store.isInitialized(),
78
+ username
79
+ });
80
+ return;
81
+ }
82
+ if (req.method !== "POST") {
83
+ sendJson(res, 405, {
84
+ ok: false,
85
+ error: "method not allowed"
86
+ });
87
+ return;
88
+ }
89
+ const body = await readBody(req);
90
+ if (!body) {
91
+ sendJson(res, 400, {
92
+ ok: false,
93
+ error: "请求体必须是 JSON"
94
+ });
95
+ return;
96
+ }
97
+ if (path === "/api/auth/register") {
98
+ if (store.isInitialized()) {
99
+ sendJson(res, 409, {
100
+ ok: false,
101
+ error: "管理员已存在"
102
+ });
103
+ return;
104
+ }
105
+ const username = sanitizeUsername(String(body.username ?? ""));
106
+ const password = String(body.password ?? "");
107
+ if (!username) {
108
+ sendJson(res, 400, {
109
+ ok: false,
110
+ error: "用户名不能为空"
111
+ });
112
+ return;
113
+ }
114
+ if (password.length < 8) {
115
+ sendJson(res, 400, {
116
+ ok: false,
117
+ error: "密码至少 8 个字符"
118
+ });
119
+ return;
120
+ }
121
+ store.register(username, password);
122
+ const cookie = issueSession(username, store.sessionSecret);
123
+ sendJson(res, 200, { ok: true }, { "set-cookie": sessionCookieHeader(cookie) });
124
+ return;
125
+ }
126
+ if (path === "/api/auth/login") {
127
+ const ip = clientIp(req);
128
+ if (limiter.isLocked(ip)) {
129
+ sendJson(res, 429, {
130
+ ok: false,
131
+ error: "尝试次数过多,请 30 秒后再试"
132
+ });
133
+ return;
134
+ }
135
+ if (!store.isInitialized()) {
136
+ sendJson(res, 409, {
137
+ ok: false,
138
+ error: "尚未初始化管理员"
139
+ });
140
+ return;
141
+ }
142
+ const username = sanitizeUsername(String(body.username ?? ""));
143
+ const password = String(body.password ?? "");
144
+ if (username !== store.username || !store.verify(password)) {
145
+ limiter.recordFailure(ip);
146
+ sendJson(res, 401, {
147
+ ok: false,
148
+ error: "用户名或密码错误"
149
+ });
150
+ return;
151
+ }
152
+ limiter.recordSuccess(ip);
153
+ const cookie = issueSession(username, store.sessionSecret);
154
+ sendJson(res, 200, { ok: true }, { "set-cookie": sessionCookieHeader(cookie) });
155
+ return;
156
+ }
157
+ if (path === "/api/auth/logout") {
158
+ sendJson(res, 200, { ok: true }, { "set-cookie": clearCookieHeader() });
159
+ return;
160
+ }
161
+ if (path === "/api/auth/change-password" || path === "/api/auth/change-username") {
162
+ const secret = store.sessionSecret;
163
+ const currentName = store.username;
164
+ const cookie = secret && currentName ? readCookie(req.headers.cookie, SESSION_COOKIE) : void 0;
165
+ if (!secret || !currentName || !cookie || !verifySession(cookie, secret, currentName)) {
166
+ sendJson(res, 401, {
167
+ ok: false,
168
+ error: "unauthorized"
169
+ });
170
+ return;
171
+ }
172
+ const ip = clientIp(req);
173
+ if (limiter.isLocked(ip)) {
174
+ sendJson(res, 429, {
175
+ ok: false,
176
+ error: "尝试次数过多,请 30 秒后再试"
177
+ });
178
+ return;
179
+ }
180
+ if (!store.verify(String(body.currentPassword ?? ""))) {
181
+ limiter.recordFailure(ip);
182
+ sendJson(res, 401, {
183
+ ok: false,
184
+ error: "当前密码错误"
185
+ });
186
+ return;
187
+ }
188
+ limiter.recordSuccess(ip);
189
+ if (path === "/api/auth/change-password") {
190
+ const password = String(body.password ?? "");
191
+ if (password.length < 8) {
192
+ sendJson(res, 400, {
193
+ ok: false,
194
+ error: "密码至少 8 个字符"
195
+ });
196
+ return;
197
+ }
198
+ store.changePassword(currentName, password);
199
+ } else {
200
+ const username = sanitizeUsername(String(body.username ?? ""));
201
+ if (!username) {
202
+ sendJson(res, 400, {
203
+ ok: false,
204
+ error: "用户名不能为空"
205
+ });
206
+ return;
207
+ }
208
+ store.changeUsername(username);
209
+ }
210
+ const fresh = issueSession(store.username, store.sessionSecret);
211
+ sendJson(res, 200, { ok: true }, { "set-cookie": sessionCookieHeader(fresh) });
212
+ return;
213
+ }
214
+ sendJson(res, 404, {
215
+ ok: false,
216
+ error: "not found"
217
+ });
218
+ }
219
+ }));
220
+ }
221
+ //#endregion
222
+ export { registerAuthRoutes };
223
+
224
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","names":[],"sources":["../../src/auth/routes.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sanitizeUsername, type CredentialStore } from './credentials.js'\nimport { isLoopbackRequest } from './loopback.js'\nimport { clearCookieHeader, issueSession, readCookie, SESSION_COOKIE, sessionCookieHeader, verifySession } from './session.js'\nimport type { LoginRateLimiter } from './ratelimit.js'\nimport { loginPageHtml } from './login-page.js'\n\nconst MAX_BODY = 64 * 1024\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown, headers: Record<string, string> = {}): void {\n res.writeHead(status, { 'content-type': 'application/json', ...headers })\n res.end(JSON.stringify(body))\n}\n\nfunction readBody(req: IncomingMessage): Promise<Record<string, unknown> | null> {\n return new Promise((resolve) => {\n if ((req.headers['content-type'] ?? '') !== 'application/json') {\n resolve(null)\n return\n }\n const chunks: Buffer[] = []\n let size = 0\n req.on('data', (chunk: Buffer) => {\n size += chunk.length\n if (size > MAX_BODY) {\n resolve(null)\n req.destroy()\n return\n }\n chunks.push(chunk)\n })\n req.on('end', () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>)\n } catch {\n resolve(null)\n }\n })\n req.on('error', () => resolve(null))\n })\n}\n\nfunction clientIp(req: IncomingMessage): string {\n return req.socket.remoteAddress ?? 'unknown'\n}\n\nexport function registerAuthRoutes(ctx: Context, store: CredentialStore, limiter: LoginRateLimiter): void {\n const webServer = ctx.webServer\n\n ctx.effect(() =>\n webServer.register({\n kind: 'exact',\n path: '/login',\n handler: (_req, res) => {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })\n res.end(loginPageHtml())\n },\n }),\n )\n\n ctx.effect(() =>\n webServer.register({\n kind: 'prefix',\n path: '/api/auth',\n handler: async (req, res) => {\n const path = new URL(req.url ?? '/', 'http://x').pathname\n\n if (path === '/api/auth/status' && req.method === 'GET') {\n const loopback = isLoopbackRequest(req.socket.remoteAddress, req.headers.host)\n let authenticated = loopback\n let username: string | undefined\n if (!loopback) {\n const secret = store.sessionSecret\n const currentName = store.username\n if (secret && currentName) {\n const cookie = readCookie(req.headers.cookie, SESSION_COOKIE)\n if (verifySession(cookie, secret, currentName)) {\n authenticated = true\n username = currentName\n }\n }\n }\n sendJson(res, 200, { ok: true, authenticated, initialized: store.isInitialized(), username })\n return\n }\n\n if (req.method !== 'POST') {\n sendJson(res, 405, { ok: false, error: 'method not allowed' })\n return\n }\n\n const body = await readBody(req)\n if (!body) {\n sendJson(res, 400, { ok: false, error: '请求体必须是 JSON' })\n return\n }\n\n if (path === '/api/auth/register') {\n if (store.isInitialized()) {\n sendJson(res, 409, { ok: false, error: '管理员已存在' })\n return\n }\n const username = sanitizeUsername(String(body.username ?? ''))\n const password = String(body.password ?? '')\n if (!username) {\n sendJson(res, 400, { ok: false, error: '用户名不能为空' })\n return\n }\n if (password.length < 8) {\n sendJson(res, 400, { ok: false, error: '密码至少 8 个字符' })\n return\n }\n store.register(username, password)\n const cookie = issueSession(username, store.sessionSecret!)\n sendJson(res, 200, { ok: true }, { 'set-cookie': sessionCookieHeader(cookie) })\n return\n }\n\n if (path === '/api/auth/login') {\n const ip = clientIp(req)\n if (limiter.isLocked(ip)) {\n sendJson(res, 429, { ok: false, error: '尝试次数过多,请 30 秒后再试' })\n return\n }\n if (!store.isInitialized()) {\n sendJson(res, 409, { ok: false, error: '尚未初始化管理员' })\n return\n }\n const username = sanitizeUsername(String(body.username ?? ''))\n const password = String(body.password ?? '')\n if (username !== store.username || !store.verify(password)) {\n limiter.recordFailure(ip)\n sendJson(res, 401, { ok: false, error: '用户名或密码错误' })\n return\n }\n limiter.recordSuccess(ip)\n const cookie = issueSession(username, store.sessionSecret!)\n sendJson(res, 200, { ok: true }, { 'set-cookie': sessionCookieHeader(cookie) })\n return\n }\n\n if (path === '/api/auth/logout') {\n sendJson(res, 200, { ok: true }, { 'set-cookie': clearCookieHeader() })\n return\n }\n\n if (path === '/api/auth/change-password' || path === '/api/auth/change-username') {\n const secret = store.sessionSecret\n const currentName = store.username\n const cookie = secret && currentName ? readCookie(req.headers.cookie, SESSION_COOKIE) : undefined\n if (!secret || !currentName || !cookie || !verifySession(cookie, secret, currentName)) {\n sendJson(res, 401, { ok: false, error: 'unauthorized' })\n return\n }\n const ip = clientIp(req)\n if (limiter.isLocked(ip)) {\n sendJson(res, 429, { ok: false, error: '尝试次数过多,请 30 秒后再试' })\n return\n }\n if (!store.verify(String(body.currentPassword ?? ''))) {\n limiter.recordFailure(ip)\n sendJson(res, 401, { ok: false, error: '当前密码错误' })\n return\n }\n limiter.recordSuccess(ip)\n if (path === '/api/auth/change-password') {\n const password = String(body.password ?? '')\n if (password.length < 8) {\n sendJson(res, 400, { ok: false, error: '密码至少 8 个字符' })\n return\n }\n store.changePassword(currentName, password)\n } else {\n const username = sanitizeUsername(String(body.username ?? ''))\n if (!username) {\n sendJson(res, 400, { ok: false, error: '用户名不能为空' })\n return\n }\n store.changeUsername(username)\n }\n const fresh = issueSession(store.username!, store.sessionSecret!)\n sendJson(res, 200, { ok: true }, { 'set-cookie': sessionCookieHeader(fresh) })\n return\n }\n\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n }),\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ //#region src/auth/session.d.ts
2
+ declare const SESSION_COOKIE = "dshb_sid";
3
+ declare const SESSION_MAX_AGE_SEC: number;
4
+ declare function issueSession(username: string, secret: Buffer): string;
5
+ declare function verifySession(value: string | undefined, secret: Buffer, currentUsername: string): boolean;
6
+ declare function readCookie(header: string | undefined, name: string): string | undefined;
7
+ declare function sessionCookieHeader(value: string): string;
8
+ declare function clearCookieHeader(): string;
9
+ //#endregion
10
+ export { SESSION_COOKIE, SESSION_MAX_AGE_SEC, clearCookieHeader, issueSession, readCookie, sessionCookieHeader, verifySession };
11
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ //#region src/auth/session.ts
3
+ const SESSION_COOKIE = "dshb_sid";
4
+ const SESSION_MAX_AGE_SEC = 1209600;
5
+ function base64url(input) {
6
+ return Buffer.from(input).toString("base64url");
7
+ }
8
+ function sign(secret, payload) {
9
+ return createHmac("sha256", secret).update(payload).digest("base64url");
10
+ }
11
+ function issueSession(username, secret) {
12
+ const payload = `${base64url(username)}.${Math.floor(Date.now() / 1e3)}`;
13
+ return `${payload}.${sign(secret, payload)}`;
14
+ }
15
+ function verifySession(value, secret, currentUsername) {
16
+ if (!value) return false;
17
+ const parts = value.split(".");
18
+ if (parts.length !== 3) return false;
19
+ const [b64user, iatText, sig] = parts;
20
+ const expected = sign(secret, `${b64user}.${iatText}`);
21
+ const a = Buffer.from(sig);
22
+ const b = Buffer.from(expected);
23
+ if (a.length !== b.length || !timingSafeEqual(a, b)) return false;
24
+ const iat = Number(iatText);
25
+ if (!Number.isInteger(iat)) return false;
26
+ if (Math.floor(Date.now() / 1e3) - iat > 1209600) return false;
27
+ let username;
28
+ try {
29
+ username = Buffer.from(b64user, "base64url").toString("utf8");
30
+ } catch {
31
+ return false;
32
+ }
33
+ return username === currentUsername;
34
+ }
35
+ function readCookie(header, name) {
36
+ if (!header) return void 0;
37
+ for (const part of header.split(";")) {
38
+ const eq = part.indexOf("=");
39
+ if (eq < 0) continue;
40
+ if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim();
41
+ }
42
+ }
43
+ function sessionCookieHeader(value) {
44
+ return `${SESSION_COOKIE}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE_SEC}`;
45
+ }
46
+ function clearCookieHeader() {
47
+ return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
48
+ }
49
+ //#endregion
50
+ export { SESSION_COOKIE, SESSION_MAX_AGE_SEC, clearCookieHeader, issueSession, readCookie, sessionCookieHeader, verifySession };
51
+
52
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","names":[],"sources":["../../src/auth/session.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from 'node:crypto'\n\nexport const SESSION_COOKIE = 'dshb_sid'\nexport const SESSION_MAX_AGE_SEC = 14 * 24 * 3600\n\nfunction base64url(input: Buffer | string): string {\n return Buffer.from(input).toString('base64url')\n}\n\nfunction sign(secret: Buffer, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('base64url')\n}\n\nexport function issueSession(username: string, secret: Buffer): string {\n const payload = `${base64url(username)}.${Math.floor(Date.now() / 1000)}`\n return `${payload}.${sign(secret, payload)}`\n}\n\nexport function verifySession(\n value: string | undefined,\n secret: Buffer,\n currentUsername: string,\n): boolean {\n if (!value) return false\n const parts = value.split('.')\n if (parts.length !== 3) return false\n const [b64user, iatText, sig] = parts\n const payload = `${b64user}.${iatText}`\n const expected = sign(secret, payload)\n const a = Buffer.from(sig)\n const b = Buffer.from(expected)\n if (a.length !== b.length || !timingSafeEqual(a, b)) return false\n const iat = Number(iatText)\n if (!Number.isInteger(iat)) return false\n if (Math.floor(Date.now() / 1000) - iat > SESSION_MAX_AGE_SEC) return false\n let username: string\n try {\n username = Buffer.from(b64user, 'base64url').toString('utf8')\n } catch {\n return false\n }\n return username === currentUsername\n}\n\nexport function readCookie(header: string | undefined, name: string): string | undefined {\n if (!header) return undefined\n for (const part of header.split(';')) {\n const eq = part.indexOf('=')\n if (eq < 0) continue\n if (part.slice(0, eq).trim() === name) return part.slice(eq + 1).trim()\n }\n return undefined\n}\n\nexport function sessionCookieHeader(value: string): string {\n return `${SESSION_COOKIE}=${value}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE_SEC}`\n}\n\nexport function clearCookieHeader(): string {\n return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`\n}\n"],"mappings":";;AAEA,MAAa,iBAAiB;AAC9B,MAAa,sBAAsB;AAEnC,SAAS,UAAU,OAAgC;CACjD,OAAO,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,WAAW;AAChD;AAEA,SAAS,KAAK,QAAgB,SAAyB;CACrD,OAAO,WAAW,UAAU,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,OAAO,WAAW;AACxE;AAEA,SAAgB,aAAa,UAAkB,QAAwB;CACrE,MAAM,UAAU,GAAG,UAAU,QAAQ,EAAE,GAAG,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;CACtE,OAAO,GAAG,QAAQ,GAAG,KAAK,QAAQ,OAAO;AAC3C;AAEA,SAAgB,cACd,OACA,QACA,iBACS;CACT,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,QAAQ,MAAM,MAAM,GAAG;CAC7B,IAAI,MAAM,WAAW,GAAG,OAAO;CAC/B,MAAM,CAAC,SAAS,SAAS,OAAO;CAEhC,MAAM,WAAW,KAAK,QAAQ,GADX,QAAQ,GAAG,SACO;CACrC,MAAM,IAAI,OAAO,KAAK,GAAG;CACzB,MAAM,IAAI,OAAO,KAAK,QAAQ;CAC9B,IAAI,EAAE,WAAW,EAAE,UAAU,CAAC,gBAAgB,GAAG,CAAC,GAAG,OAAO;CAC5D,MAAM,MAAM,OAAO,OAAO;CAC1B,IAAI,CAAC,OAAO,UAAU,GAAG,GAAG,OAAO;CACnC,IAAI,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,IAAI,MAAA,SAA2B,OAAO;CACtE,IAAI;CACJ,IAAI;EACF,WAAW,OAAO,KAAK,SAAS,WAAW,CAAC,CAAC,SAAS,MAAM;CAC9D,QAAQ;EACN,OAAO;CACT;CACA,OAAO,aAAa;AACtB;AAEA,SAAgB,WAAW,QAA4B,MAAkC;CACvF,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,KAAK,MAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;EACpC,MAAM,KAAK,KAAK,QAAQ,GAAG;EAC3B,IAAI,KAAK,GAAG;EACZ,IAAI,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC,KAAK,MAAM,MAAM,OAAO,KAAK,MAAM,KAAK,CAAC,CAAC,CAAC,KAAK;CACxE;AAEF;AAEA,SAAgB,oBAAoB,OAAuB;CACzD,OAAO,GAAG,eAAe,GAAG,MAAM,4CAA4C;AAChF;AAEA,SAAgB,oBAA4B;CAC1C,OAAO,GAAG,eAAe;AAC3B"}
@@ -0,0 +1,15 @@
1
+ import { Context } from "@deepseek-ai/cordis";
2
+ //#region src/auth/startup.d.ts
3
+ declare const name = "web-startup";
4
+ declare const inject: string[];
5
+ declare const WEB_STARTUP_SERVICE = "webStartup";
6
+ interface WebStartupValues {
7
+ openBrowser: boolean;
8
+ host?: string;
9
+ port?: number;
10
+ trustedHosts: string[];
11
+ }
12
+ declare function apply(ctx: Context): void;
13
+ //#endregion
14
+ export { WEB_STARTUP_SERVICE, WebStartupValues, apply, inject, name };
15
+ //# sourceMappingURL=startup.d.ts.map
@@ -0,0 +1,68 @@
1
+ import { sanitizeUsername, sharedCredentialStore } from "./credentials.js";
2
+ import { Command } from "commander";
3
+ import { parseCmdline } from "@deepseek-ai/dsh-cmdline";
4
+ //#region src/auth/startup.ts
5
+ const name = "web-startup";
6
+ const inject = ["cmdlineArgs"];
7
+ const WEB_STARTUP_SERVICE = "webStartup";
8
+ function runAuthReset(args) {
9
+ const store = sharedCredentialStore();
10
+ if (!store.isInitialized()) {
11
+ console.error("auth-reset: 尚未初始化管理员账号");
12
+ process.exit(1);
13
+ }
14
+ let password;
15
+ let username;
16
+ for (let i = 1; i < args.length; i++) if (args[i] === "--password") password = args[++i];
17
+ else if (args[i] === "--username") username = args[++i];
18
+ if (password === void 0 && username === void 0) {
19
+ console.error("auth-reset: 需要 --password <新密码> 和/或 --username <新用户名>");
20
+ process.exit(1);
21
+ }
22
+ if (password !== void 0 && password.length < 8) {
23
+ console.error("auth-reset: 密码至少 8 个字符");
24
+ process.exit(1);
25
+ }
26
+ if (username !== void 0) {
27
+ username = sanitizeUsername(username);
28
+ if (!username) {
29
+ console.error("auth-reset: 用户名不能为空");
30
+ process.exit(1);
31
+ }
32
+ }
33
+ if (password !== void 0) store.changePassword(username ?? store.username, password);
34
+ else store.changeUsername(username);
35
+ console.log("auth-reset: 已更新凭据并轮换会话密钥,全部已签发会话作废");
36
+ process.exit(0);
37
+ }
38
+ function webCommand() {
39
+ return new Command().name("dsh --profile web").description("Serve the DeepSeek Harness browser UI.").helpOption("-h, --help", "show this help").option("--host <host>", "bind host").option("--no-open", "do not open the Web UI in the default browser").option("--port <port>", "listen port; pass 0 to let the OS pick a free one").option("--trusted-host <authority...>", "extra authority the /api browser-trust fence accepts (host or host:port; repeatable)").addHelpText("after", `
40
+ Examples:
41
+ dsh --profile web serve on the composed host and port
42
+ dsh --profile web --no-open serve without opening a browser
43
+ dsh --profile web --port 8080 serve on another port
44
+ dsh --profile web --host 0.0.0.0 serve on all interfaces (DSHB: auth plugin guards access)
45
+ dsh --profile web auth-reset --password <新密码> reset admin credentials and revoke all sessions
46
+ `);
47
+ }
48
+ function apply(ctx) {
49
+ const args = ctx.cmdlineArgs?.get() ?? [];
50
+ if (args[0] === "auth-reset") runAuthReset(args);
51
+ const program = webCommand();
52
+ program.action(() => {
53
+ const options = program.opts();
54
+ if (options.port !== void 0 && !/^\d+$/.test(options.port)) program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`);
55
+ const values = {
56
+ openBrowser: options.open,
57
+ ...options.host !== void 0 && { host: options.host },
58
+ ...options.port !== void 0 && { port: Number(options.port) },
59
+ trustedHosts: options.trustedHost ?? []
60
+ };
61
+ ctx.provide(WEB_STARTUP_SERVICE, values);
62
+ });
63
+ parseCmdline(ctx, program);
64
+ }
65
+ //#endregion
66
+ export { WEB_STARTUP_SERVICE, apply, inject, name };
67
+
68
+ //# sourceMappingURL=startup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"startup.js","names":[],"sources":["../../src/auth/startup.ts"],"sourcesContent":["import { Command } from 'commander'\nimport { parseCmdline } from '@deepseek-ai/dsh-cmdline'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { sanitizeUsername, sharedCredentialStore } from './credentials.js'\n\nexport const name = 'web-startup'\n\nexport const inject = ['cmdlineArgs']\n\nexport const WEB_STARTUP_SERVICE = 'webStartup'\n\nexport interface WebStartupValues {\n openBrowser: boolean\n host?: string\n port?: number\n trustedHosts: string[]\n}\n\nfunction runAuthReset(args: readonly string[]): never {\n const store = sharedCredentialStore()\n if (!store.isInitialized()) {\n console.error('auth-reset: 尚未初始化管理员账号')\n process.exit(1)\n }\n let password: string | undefined\n let username: string | undefined\n for (let i = 1; i < args.length; i++) {\n if (args[i] === '--password') password = args[++i]\n else if (args[i] === '--username') username = args[++i]\n }\n if (password === undefined && username === undefined) {\n console.error('auth-reset: 需要 --password <新密码> 和/或 --username <新用户名>')\n process.exit(1)\n }\n if (password !== undefined && password.length < 8) {\n console.error('auth-reset: 密码至少 8 个字符')\n process.exit(1)\n }\n if (username !== undefined) {\n username = sanitizeUsername(username)\n if (!username) {\n console.error('auth-reset: 用户名不能为空')\n process.exit(1)\n }\n }\n if (password !== undefined) store.changePassword(username ?? store.username!, password)\n else store.changeUsername(username!)\n console.log('auth-reset: 已更新凭据并轮换会话密钥,全部已签发会话作废')\n process.exit(0)\n}\n\nfunction webCommand(): Command {\n return new Command()\n .name('dsh --profile web')\n .description('Serve the DeepSeek Harness browser UI.')\n .helpOption('-h, --help', 'show this help')\n .option('--host <host>', 'bind host')\n .option('--no-open', 'do not open the Web UI in the default browser')\n .option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')\n .option(\n '--trusted-host <authority...>',\n 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)',\n )\n .addHelpText(\n 'after',\n `\nExamples:\n dsh --profile web serve on the composed host and port\n dsh --profile web --no-open serve without opening a browser\n dsh --profile web --port 8080 serve on another port\n dsh --profile web --host 0.0.0.0 serve on all interfaces (DSHB: auth plugin guards access)\n dsh --profile web auth-reset --password <新密码> reset admin credentials and revoke all sessions\n`,\n )\n}\n\nexport function apply(ctx: Context): void {\n const args = ctx.cmdlineArgs?.get() ?? []\n if (args[0] === 'auth-reset') runAuthReset(args)\n const program = webCommand()\n program.action(() => {\n const options = program.opts()\n if (options.port !== undefined && !/^\\d+$/.test(options.port)) {\n program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`)\n }\n const values: WebStartupValues = {\n openBrowser: options.open,\n ...(options.host !== undefined && { host: options.host }),\n ...(options.port !== undefined && { port: Number(options.port) }),\n trustedHosts: options.trustedHost ?? [],\n }\n ctx.provide(WEB_STARTUP_SERVICE, values)\n })\n parseCmdline(ctx, program)\n}\n"],"mappings":";;;;AAKA,MAAa,OAAO;AAEpB,MAAa,SAAS,CAAC,aAAa;AAEpC,MAAa,sBAAsB;AASnC,SAAS,aAAa,MAAgC;CACpD,MAAM,QAAQ,sBAAsB;CACpC,IAAI,CAAC,MAAM,cAAc,GAAG;EAC1B,QAAQ,MAAM,wBAAwB;EACtC,QAAQ,KAAK,CAAC;CAChB;CACA,IAAI;CACJ,IAAI;CACJ,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAC/B,IAAI,KAAK,OAAO,cAAc,WAAW,KAAK,EAAE;MAC3C,IAAI,KAAK,OAAO,cAAc,WAAW,KAAK,EAAE;CAEvD,IAAI,aAAa,KAAA,KAAa,aAAa,KAAA,GAAW;EACpD,QAAQ,MAAM,uDAAuD;EACrE,QAAQ,KAAK,CAAC;CAChB;CACA,IAAI,aAAa,KAAA,KAAa,SAAS,SAAS,GAAG;EACjD,QAAQ,MAAM,wBAAwB;EACtC,QAAQ,KAAK,CAAC;CAChB;CACA,IAAI,aAAa,KAAA,GAAW;EAC1B,WAAW,iBAAiB,QAAQ;EACpC,IAAI,CAAC,UAAU;GACb,QAAQ,MAAM,qBAAqB;GACnC,QAAQ,KAAK,CAAC;EAChB;CACF;CACA,IAAI,aAAa,KAAA,GAAW,MAAM,eAAe,YAAY,MAAM,UAAW,QAAQ;MACjF,MAAM,eAAe,QAAS;CACnC,QAAQ,IAAI,oCAAoC;CAChD,QAAQ,KAAK,CAAC;AAChB;AAEA,SAAS,aAAsB;CAC7B,OAAO,IAAI,QAAQ,CAAC,CACjB,KAAK,mBAAmB,CAAC,CACzB,YAAY,wCAAwC,CAAC,CACrD,WAAW,cAAc,gBAAgB,CAAC,CAC1C,OAAO,iBAAiB,WAAW,CAAC,CACpC,OAAO,aAAa,+CAA+C,CAAC,CACpE,OAAO,iBAAiB,mDAAmD,CAAC,CAC5E,OACC,iCACA,sFACF,CAAC,CACA,YACC,SACA;;;;;;;CAQF;AACJ;AAEA,SAAgB,MAAM,KAAoB;CACxC,MAAM,OAAO,IAAI,aAAa,IAAI,KAAK,CAAC;CACxC,IAAI,KAAK,OAAO,cAAc,aAAa,IAAI;CAC/C,MAAM,UAAU,WAAW;CAC3B,QAAQ,aAAa;EACnB,MAAM,UAAU,QAAQ,KAAK;EAC7B,IAAI,QAAQ,SAAS,KAAA,KAAa,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAC1D,QAAQ,MAAM,uCAAuC,KAAK,UAAU,QAAQ,IAAI,GAAG;EAErF,MAAM,SAA2B;GAC/B,aAAa,QAAQ;GACrB,GAAI,QAAQ,SAAS,KAAA,KAAa,EAAE,MAAM,QAAQ,KAAK;GACvD,GAAI,QAAQ,SAAS,KAAA,KAAa,EAAE,MAAM,OAAO,QAAQ,IAAI,EAAE;GAC/D,cAAc,QAAQ,eAAe,CAAC;EACxC;EACA,IAAI,QAAQ,qBAAqB,MAAM;CACzC,CAAC;CACD,aAAa,KAAK,OAAO;AAC3B"}