@n24q02m/hull 0.0.0-stage → 0.4.0-beta.9

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 (231) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +166 -2
  3. package/build/auth/credential-form.d.ts +163 -0
  4. package/build/auth/credential-form.d.ts.map +1 -0
  5. package/build/auth/credential-form.js +1869 -0
  6. package/build/auth/credential-form.js.map +1 -0
  7. package/build/auth/delegated-oauth-app.d.ts +90 -0
  8. package/build/auth/delegated-oauth-app.d.ts.map +1 -0
  9. package/build/auth/delegated-oauth-app.js +881 -0
  10. package/build/auth/delegated-oauth-app.js.map +1 -0
  11. package/build/auth/index.d.ts +12 -0
  12. package/build/auth/index.d.ts.map +1 -0
  13. package/build/auth/index.js +12 -0
  14. package/build/auth/index.js.map +1 -0
  15. package/build/auth/local-oauth-app.d.ts +152 -0
  16. package/build/auth/local-oauth-app.d.ts.map +1 -0
  17. package/build/auth/local-oauth-app.js +975 -0
  18. package/build/auth/local-oauth-app.js.map +1 -0
  19. package/build/auth/relay-login.d.ts +46 -0
  20. package/build/auth/relay-login.d.ts.map +1 -0
  21. package/build/auth/relay-login.js +197 -0
  22. package/build/auth/relay-login.js.map +1 -0
  23. package/build/auth/router.d.ts +18 -0
  24. package/build/auth/router.d.ts.map +1 -0
  25. package/build/auth/router.js +62 -0
  26. package/build/auth/router.js.map +1 -0
  27. package/build/auth/session-store.d.ts +49 -0
  28. package/build/auth/session-store.d.ts.map +1 -0
  29. package/build/auth/session-store.js +94 -0
  30. package/build/auth/session-store.js.map +1 -0
  31. package/build/auth/stable-sub.d.ts +27 -0
  32. package/build/auth/stable-sub.d.ts.map +1 -0
  33. package/build/auth/stable-sub.js +38 -0
  34. package/build/auth/stable-sub.js.map +1 -0
  35. package/build/auth/well-known.d.ts +4 -0
  36. package/build/auth/well-known.d.ts.map +1 -0
  37. package/build/auth/well-known.js +26 -0
  38. package/build/auth/well-known.js.map +1 -0
  39. package/build/cli/build-cli.d.ts +97 -0
  40. package/build/cli/build-cli.d.ts.map +1 -0
  41. package/build/cli/build-cli.js +309 -0
  42. package/build/cli/build-cli.js.map +1 -0
  43. package/build/cli/index.d.ts +2 -0
  44. package/build/cli/index.d.ts.map +1 -0
  45. package/build/cli/index.js +2 -0
  46. package/build/cli/index.js.map +1 -0
  47. package/build/crypto/aes.d.ts +8 -0
  48. package/build/crypto/aes.d.ts.map +1 -0
  49. package/build/crypto/aes.js +27 -0
  50. package/build/crypto/aes.js.map +1 -0
  51. package/build/crypto/ecdh.d.ts +5 -0
  52. package/build/crypto/ecdh.d.ts.map +1 -0
  53. package/build/crypto/ecdh.js +16 -0
  54. package/build/crypto/ecdh.js.map +1 -0
  55. package/build/crypto/index.d.ts +4 -0
  56. package/build/crypto/index.d.ts.map +1 -0
  57. package/build/crypto/index.js +4 -0
  58. package/build/crypto/index.js.map +1 -0
  59. package/build/crypto/kdf.d.ts +15 -0
  60. package/build/crypto/kdf.d.ts.map +1 -0
  61. package/build/crypto/kdf.js +26 -0
  62. package/build/crypto/kdf.js.map +1 -0
  63. package/build/index.d.ts +19 -0
  64. package/build/index.d.ts.map +1 -0
  65. package/build/index.js +28 -0
  66. package/build/index.js.map +1 -0
  67. package/build/lifecycle/lock.d.ts +68 -0
  68. package/build/lifecycle/lock.d.ts.map +1 -0
  69. package/build/lifecycle/lock.js +173 -0
  70. package/build/lifecycle/lock.js.map +1 -0
  71. package/build/oauth/cache.d.ts +22 -0
  72. package/build/oauth/cache.d.ts.map +1 -0
  73. package/build/oauth/cache.js +24 -0
  74. package/build/oauth/cache.js.map +1 -0
  75. package/build/oauth/index.d.ts +6 -0
  76. package/build/oauth/index.d.ts.map +1 -0
  77. package/build/oauth/index.js +6 -0
  78. package/build/oauth/index.js.map +1 -0
  79. package/build/oauth/jwt-issuer.d.ts +87 -0
  80. package/build/oauth/jwt-issuer.d.ts.map +1 -0
  81. package/build/oauth/jwt-issuer.js +205 -0
  82. package/build/oauth/jwt-issuer.js.map +1 -0
  83. package/build/oauth/pkce.d.ts +10 -0
  84. package/build/oauth/pkce.d.ts.map +1 -0
  85. package/build/oauth/pkce.js +34 -0
  86. package/build/oauth/pkce.js.map +1 -0
  87. package/build/oauth/provider.d.ts +52 -0
  88. package/build/oauth/provider.d.ts.map +1 -0
  89. package/build/oauth/provider.js +98 -0
  90. package/build/oauth/provider.js.map +1 -0
  91. package/build/oauth/user-store.d.ts +25 -0
  92. package/build/oauth/user-store.d.ts.map +1 -0
  93. package/build/oauth/user-store.js +85 -0
  94. package/build/oauth/user-store.js.map +1 -0
  95. package/build/relay/browser.d.ts +15 -0
  96. package/build/relay/browser.d.ts.map +1 -0
  97. package/build/relay/browser.js +121 -0
  98. package/build/relay/browser.js.map +1 -0
  99. package/build/relay/client.d.ts +36 -0
  100. package/build/relay/client.d.ts.map +1 -0
  101. package/build/relay/client.js +135 -0
  102. package/build/relay/client.js.map +1 -0
  103. package/build/relay/session.d.ts +12 -0
  104. package/build/relay/session.d.ts.map +1 -0
  105. package/build/relay/session.js +55 -0
  106. package/build/relay/session.js.map +1 -0
  107. package/build/relay/tool-helpers.d.ts +85 -0
  108. package/build/relay/tool-helpers.d.ts.map +1 -0
  109. package/build/relay/tool-helpers.js +103 -0
  110. package/build/relay/tool-helpers.js.map +1 -0
  111. package/build/relay/wordlist.d.ts +2 -0
  112. package/build/relay/wordlist.d.ts.map +1 -0
  113. package/build/relay/wordlist.js +7782 -0
  114. package/build/relay/wordlist.js.map +1 -0
  115. package/build/schema/types.d.ts +67 -0
  116. package/build/schema/types.d.ts.map +1 -0
  117. package/build/schema/types.js +2 -0
  118. package/build/schema/types.js.map +1 -0
  119. package/build/storage/atomic-write.d.ts +8 -0
  120. package/build/storage/atomic-write.d.ts.map +1 -0
  121. package/build/storage/atomic-write.js +26 -0
  122. package/build/storage/atomic-write.js.map +1 -0
  123. package/build/storage/backends.d.ts +74 -0
  124. package/build/storage/backends.d.ts.map +1 -0
  125. package/build/storage/backends.js +193 -0
  126. package/build/storage/backends.js.map +1 -0
  127. package/build/storage/config-file.d.ts +35 -0
  128. package/build/storage/config-file.d.ts.map +1 -0
  129. package/build/storage/config-file.js +259 -0
  130. package/build/storage/config-file.js.map +1 -0
  131. package/build/storage/credential-store.d.ts +71 -0
  132. package/build/storage/credential-store.d.ts.map +1 -0
  133. package/build/storage/credential-store.js +91 -0
  134. package/build/storage/credential-store.js.map +1 -0
  135. package/build/storage/data-dir.d.ts +58 -0
  136. package/build/storage/data-dir.d.ts.map +1 -0
  137. package/build/storage/data-dir.js +79 -0
  138. package/build/storage/data-dir.js.map +1 -0
  139. package/build/storage/encryption.d.ts +9 -0
  140. package/build/storage/encryption.d.ts.map +1 -0
  141. package/build/storage/encryption.js +27 -0
  142. package/build/storage/encryption.js.map +1 -0
  143. package/build/storage/home-dir.d.ts +11 -0
  144. package/build/storage/home-dir.d.ts.map +1 -0
  145. package/build/storage/home-dir.js +18 -0
  146. package/build/storage/home-dir.js.map +1 -0
  147. package/build/storage/index.d.ts +12 -0
  148. package/build/storage/index.d.ts.map +1 -0
  149. package/build/storage/index.js +15 -0
  150. package/build/storage/index.js.map +1 -0
  151. package/build/storage/machine-id.d.ts +4 -0
  152. package/build/storage/machine-id.d.ts.map +1 -0
  153. package/build/storage/machine-id.js +73 -0
  154. package/build/storage/machine-id.js.map +1 -0
  155. package/build/storage/mode.d.ts +23 -0
  156. package/build/storage/mode.d.ts.map +1 -0
  157. package/build/storage/mode.js +46 -0
  158. package/build/storage/mode.js.map +1 -0
  159. package/build/storage/per-plugin-store.d.ts +65 -0
  160. package/build/storage/per-plugin-store.d.ts.map +1 -0
  161. package/build/storage/per-plugin-store.js +194 -0
  162. package/build/storage/per-plugin-store.js.map +1 -0
  163. package/build/storage/resolver.d.ts +7 -0
  164. package/build/storage/resolver.d.ts.map +1 -0
  165. package/build/storage/resolver.js +38 -0
  166. package/build/storage/resolver.js.map +1 -0
  167. package/build/storage/session-lock.d.ts +19 -0
  168. package/build/storage/session-lock.d.ts.map +1 -0
  169. package/build/storage/session-lock.js +105 -0
  170. package/build/storage/session-lock.js.map +1 -0
  171. package/build/token-auth/authenticator.d.ts +70 -0
  172. package/build/token-auth/authenticator.d.ts.map +1 -0
  173. package/build/token-auth/authenticator.js +146 -0
  174. package/build/token-auth/authenticator.js.map +1 -0
  175. package/build/token-auth/cli.d.ts +38 -0
  176. package/build/token-auth/cli.d.ts.map +1 -0
  177. package/build/token-auth/cli.js +192 -0
  178. package/build/token-auth/cli.js.map +1 -0
  179. package/build/token-auth/context.d.ts +30 -0
  180. package/build/token-auth/context.d.ts.map +1 -0
  181. package/build/token-auth/context.js +32 -0
  182. package/build/token-auth/context.js.map +1 -0
  183. package/build/token-auth/http-guard.d.ts +26 -0
  184. package/build/token-auth/http-guard.d.ts.map +1 -0
  185. package/build/token-auth/http-guard.js +51 -0
  186. package/build/token-auth/http-guard.js.map +1 -0
  187. package/build/token-auth/index.d.ts +14 -0
  188. package/build/token-auth/index.d.ts.map +1 -0
  189. package/build/token-auth/index.js +14 -0
  190. package/build/token-auth/index.js.map +1 -0
  191. package/build/token-auth/limiter.d.ts +20 -0
  192. package/build/token-auth/limiter.d.ts.map +1 -0
  193. package/build/token-auth/limiter.js +41 -0
  194. package/build/token-auth/limiter.js.map +1 -0
  195. package/build/token-auth/loopback.d.ts +17 -0
  196. package/build/token-auth/loopback.d.ts.map +1 -0
  197. package/build/token-auth/loopback.js +62 -0
  198. package/build/token-auth/loopback.js.map +1 -0
  199. package/build/token-auth/py-compat.d.ts +24 -0
  200. package/build/token-auth/py-compat.d.ts.map +1 -0
  201. package/build/token-auth/py-compat.js +76 -0
  202. package/build/token-auth/py-compat.js.map +1 -0
  203. package/build/token-auth/tokens.d.ts +36 -0
  204. package/build/token-auth/tokens.d.ts.map +1 -0
  205. package/build/token-auth/tokens.js +115 -0
  206. package/build/token-auth/tokens.js.map +1 -0
  207. package/build/token-auth/users.d.ts +62 -0
  208. package/build/token-auth/users.d.ts.map +1 -0
  209. package/build/token-auth/users.js +174 -0
  210. package/build/token-auth/users.js.map +1 -0
  211. package/build/transport/cache.d.ts +19 -0
  212. package/build/transport/cache.d.ts.map +1 -0
  213. package/build/transport/cache.js +76 -0
  214. package/build/transport/cache.js.map +1 -0
  215. package/build/transport/index.d.ts +5 -0
  216. package/build/transport/index.d.ts.map +1 -0
  217. package/build/transport/index.js +4 -0
  218. package/build/transport/index.js.map +1 -0
  219. package/build/transport/local-server.d.ts +173 -0
  220. package/build/transport/local-server.d.ts.map +1 -0
  221. package/build/transport/local-server.js +457 -0
  222. package/build/transport/local-server.js.map +1 -0
  223. package/build/transport/oauth-middleware.d.ts +45 -0
  224. package/build/transport/oauth-middleware.d.ts.map +1 -0
  225. package/build/transport/oauth-middleware.js +86 -0
  226. package/build/transport/oauth-middleware.js.map +1 -0
  227. package/build/transport/streamable-http.d.ts +54 -0
  228. package/build/transport/streamable-http.d.ts.map +1 -0
  229. package/build/transport/streamable-http.js +57 -0
  230. package/build/transport/streamable-http.js.map +1 -0
  231. package/package.json +107 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/token-auth/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAkBpD,MAAM,OAAO,GAAG,IAAI,iBAAiB,EAAe,CAAA;AAEpD,qEAAqE;AACrE,MAAM,UAAU,gBAAgB;IAC9B,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,GAAG,EAAE,OAAO;QACZ,SAAS,EAAE,SAAS;QACpB,IAAI,EAAE,SAAS;QACf,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAsB;QACpD,GAAG,EAAE,IAAI;QACT,QAAQ,EAAE,KAAK;QACf,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;KAC9B,CAAC,CAAA;AACJ,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,WAAW;IACzB,OAAO,OAAO,CAAC,QAAQ,EAAE,IAAI,gBAAgB,EAAE,CAAA;AACjD,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,kBAAkB,CAAI,GAAgB,EAAE,EAAW;IACjE,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;AAC7B,CAAC"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * node:http guard that authenticates a request and binds the identity,
3
+ * the counterpart of core-py `hull_core.auth.asgi.HullAuthMiddleware`.
4
+ *
5
+ * Rejections are byte-identical to core-py: a JSON body
6
+ * `{"error": <status text>, "detail": <detail>}` (Python `json.dumps`
7
+ * formatting), `content-type: application/json`, an explicit
8
+ * `content-length`, and `www-authenticate: Bearer` on 401.
9
+ */
10
+ import type { IncomingMessage, ServerResponse } from 'node:http';
11
+ import type { AuthOutcome } from './authenticator.js';
12
+ /** Status -> `error` field, same table as core-py `_STATUS_TEXT`. */
13
+ export declare const AUTH_STATUS_TEXT: Readonly<Record<number, string>>;
14
+ /** Minimal authenticator surface the guard needs. */
15
+ export interface RequestAuthenticator {
16
+ authenticate(authz: string | null | undefined): Promise<AuthOutcome>;
17
+ }
18
+ /** Write a core-py-compatible JSON auth rejection. */
19
+ export declare function writeAuthRejection(res: ServerResponse, status: number, detail: string): void;
20
+ /**
21
+ * Authenticate `req`; on success run `next` with the resolved `AuthContext`
22
+ * bound for its whole async scope (so `currentUser()` works inside MCP tool
23
+ * handlers), otherwise write the rejection and never call `next`.
24
+ */
25
+ export declare function guardRequest(authenticator: RequestAuthenticator, req: IncomingMessage, res: ServerResponse, next: () => Promise<void>): Promise<void>;
26
+ //# sourceMappingURL=http-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-guard.d.ts","sourceRoot":"","sources":["../../src/token-auth/http-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAIrD,qEAAqE;AACrE,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK5D,CAAA;AAEF,qDAAqD;AACrD,MAAM,WAAW,oBAAoB;IACnC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CACrE;AAED,sDAAsD;AACtD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAS5F;AAED;;;;GAIG;AACH,wBAAsB,YAAY,CAChC,aAAa,EAAE,oBAAoB,EACnC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GACxB,OAAO,CAAC,IAAI,CAAC,CAcf"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * node:http guard that authenticates a request and binds the identity,
3
+ * the counterpart of core-py `hull_core.auth.asgi.HullAuthMiddleware`.
4
+ *
5
+ * Rejections are byte-identical to core-py: a JSON body
6
+ * `{"error": <status text>, "detail": <detail>}` (Python `json.dumps`
7
+ * formatting), `content-type: application/json`, an explicit
8
+ * `content-length`, and `www-authenticate: Bearer` on 401.
9
+ */
10
+ import { runWithAuthContext } from './context.js';
11
+ import { pyJsonDumps } from './py-compat.js';
12
+ /** Status -> `error` field, same table as core-py `_STATUS_TEXT`. */
13
+ export const AUTH_STATUS_TEXT = Object.freeze({
14
+ 401: 'unauthorized',
15
+ 403: 'forbidden',
16
+ 429: 'rate limit exceeded',
17
+ 500: 'server misconfiguration'
18
+ });
19
+ /** Write a core-py-compatible JSON auth rejection. */
20
+ export function writeAuthRejection(res, status, detail) {
21
+ const body = Buffer.from(pyJsonDumps({ error: AUTH_STATUS_TEXT[status] ?? 'error', detail }), 'utf8');
22
+ const headers = {
23
+ 'content-type': 'application/json',
24
+ 'content-length': String(body.length)
25
+ };
26
+ if (status === 401)
27
+ headers['www-authenticate'] = 'Bearer';
28
+ res.writeHead(status, headers);
29
+ res.end(body);
30
+ }
31
+ /**
32
+ * Authenticate `req`; on success run `next` with the resolved `AuthContext`
33
+ * bound for its whole async scope (so `currentUser()` works inside MCP tool
34
+ * handlers), otherwise write the rejection and never call `next`.
35
+ */
36
+ export async function guardRequest(authenticator, req, res, next) {
37
+ const result = await authenticator.authenticate(req.headers.authorization);
38
+ if (!result.ok) {
39
+ writeAuthRejection(res, result.status ?? 500, result.detail);
40
+ return;
41
+ }
42
+ const context = result.context;
43
+ if (context === null) {
44
+ // ok=true always carries a context; fail closed rather than bind a
45
+ // null identity for the rest of the request.
46
+ writeAuthRejection(res, 500, 'authenticator returned no context');
47
+ return;
48
+ }
49
+ await runWithAuthContext(context, next);
50
+ }
51
+ //# sourceMappingURL=http-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-guard.js","sourceRoot":"","sources":["../../src/token-auth/http-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,qEAAqE;AACrE,MAAM,CAAC,MAAM,gBAAgB,GAAqC,MAAM,CAAC,MAAM,CAAC;IAC9E,GAAG,EAAE,cAAc;IACnB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,qBAAqB;IAC1B,GAAG,EAAE,yBAAyB;CAC/B,CAAC,CAAA;AAOF,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,GAAmB,EAAE,MAAc,EAAE,MAAc;IACpF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,gBAAgB,CAAC,MAAM,CAAC,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAA;IACrG,MAAM,OAAO,GAA2B;QACtC,cAAc,EAAE,kBAAkB;QAClC,gBAAgB,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;KACtC,CAAA;IACD,IAAI,MAAM,KAAK,GAAG;QAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,QAAQ,CAAA;IAC1D,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,aAAmC,EACnC,GAAoB,EACpB,GAAmB,EACnB,IAAyB;IAEzB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAA;IAC1E,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,IAAI,GAAG,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;QAC5D,OAAM;IACR,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAA;IAC9B,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;QACrB,mEAAmE;QACnE,6CAA6C;QAC7C,kBAAkB,CAAC,GAAG,EAAE,GAAG,EAAE,mCAAmC,CAAC,CAAA;QACjE,OAAM;IACR,CAAC;IACD,MAAM,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;AACzC,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * HTTP token-table auth (`no-auth` / `token` / `multi`), the TS port of
3
+ * core-py `hull_core.auth` + `hull_core.limits`. Self-contained: nothing
4
+ * here imports the relay, OAuth, or transport modules.
5
+ */
6
+ export { AuthConfigError, Authenticator, type AuthOutcome, defaultUsersFile, HTTP_AUTH_MODES, type HttpAuthConfig, parseBearer } from './authenticator.js';
7
+ export { type TokenCliHandler, type TokenCliIO, tokenCliHandler } from './cli.js';
8
+ export { type AuthContext, currentUser, type HttpAuthMode, localAuthContext, runWithAuthContext } from './context.js';
9
+ export { AUTH_STATUS_TEXT, guardRequest, type RequestAuthenticator, writeAuthRejection } from './http-guard.js';
10
+ export { SlidingWindowLimiter } from './limiter.js';
11
+ export { assertNoAuthLoopback, inetAton, isLoopbackHost, ServerConfigError } from './loopback.js';
12
+ export { hashToken, isValidTokenHash, TokenHashError, verifyToken } from './tokens.js';
13
+ export { ensurePathAllowed, findUserByToken, loadUsers, PathNotAllowedError, type PathScope, type TokenUser, type TokenUserLimits, UsersError } from './users.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/token-auth/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,eAAe,EACf,aAAa,EACb,KAAK,WAAW,EAChB,gBAAgB,EAChB,eAAe,EACf,KAAK,cAAc,EACnB,WAAW,EACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACjF,OAAO,EAAE,KAAK,WAAW,EAAE,WAAW,EAAE,KAAK,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACrH,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,KAAK,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAC/G,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjG,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACtF,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,UAAU,EACX,MAAM,YAAY,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * HTTP token-table auth (`no-auth` / `token` / `multi`), the TS port of
3
+ * core-py `hull_core.auth` + `hull_core.limits`. Self-contained: nothing
4
+ * here imports the relay, OAuth, or transport modules.
5
+ */
6
+ export { AuthConfigError, Authenticator, defaultUsersFile, HTTP_AUTH_MODES, parseBearer } from './authenticator.js';
7
+ export { tokenCliHandler } from './cli.js';
8
+ export { currentUser, localAuthContext, runWithAuthContext } from './context.js';
9
+ export { AUTH_STATUS_TEXT, guardRequest, writeAuthRejection } from './http-guard.js';
10
+ export { SlidingWindowLimiter } from './limiter.js';
11
+ export { assertNoAuthLoopback, inetAton, isLoopbackHost, ServerConfigError } from './loopback.js';
12
+ export { hashToken, isValidTokenHash, TokenHashError, verifyToken } from './tokens.js';
13
+ export { ensurePathAllowed, findUserByToken, loadUsers, PathNotAllowedError, UsersError } from './users.js';
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/token-auth/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACL,eAAe,EACf,aAAa,EAEb,gBAAgB,EAChB,eAAe,EAEf,WAAW,EACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAyC,eAAe,EAAE,MAAM,UAAU,CAAA;AACjF,OAAO,EAAoB,WAAW,EAAqB,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACrH,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAA6B,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAC/G,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACjG,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACtF,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,mBAAmB,EAInB,UAAU,EACX,MAAM,YAAY,CAAA"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Per-principal request limiter, mirroring core-py
3
+ * `hull_core.limits.limiter.SlidingWindowLimiter`.
4
+ *
5
+ * Deliberately in-memory: limits guard a single server process. Hosts that
6
+ * shard one namespace across processes terminate the limiter at their load
7
+ * balancer instead.
8
+ */
9
+ export declare class SlidingWindowLimiter {
10
+ private readonly events;
11
+ /**
12
+ * Record a request for `key` and report whether it is allowed.
13
+ *
14
+ * `rpm = null` means unlimited. The request is recorded either way so
15
+ * bursts cannot pre-load the window after a limit change. `now` is in
16
+ * seconds on a monotonic clock (tests inject it).
17
+ */
18
+ check(key: string, rpm: number | null, now?: number): boolean;
19
+ }
20
+ //# sourceMappingURL=limiter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limiter.d.ts","sourceRoot":"","sources":["../../src/token-auth/limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IAErD;;;;;;OAMG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAa5D;CACF"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Per-principal request limiter, mirroring core-py
3
+ * `hull_core.limits.limiter.SlidingWindowLimiter`.
4
+ *
5
+ * Deliberately in-memory: limits guard a single server process. Hosts that
6
+ * shard one namespace across processes terminate the limiter at their load
7
+ * balancer instead.
8
+ */
9
+ const WINDOW_SECONDS = 60;
10
+ function monotonicSeconds() {
11
+ return performance.now() / 1000;
12
+ }
13
+ export class SlidingWindowLimiter {
14
+ constructor() {
15
+ this.events = new Map();
16
+ }
17
+ /**
18
+ * Record a request for `key` and report whether it is allowed.
19
+ *
20
+ * `rpm = null` means unlimited. The request is recorded either way so
21
+ * bursts cannot pre-load the window after a limit change. `now` is in
22
+ * seconds on a monotonic clock (tests inject it).
23
+ */
24
+ check(key, rpm, now) {
25
+ const current = now ?? monotonicSeconds();
26
+ let events = this.events.get(key);
27
+ if (events === undefined) {
28
+ events = [];
29
+ this.events.set(key, events);
30
+ }
31
+ const cutoff = current - WINDOW_SECONDS;
32
+ let drop = 0;
33
+ while (drop < events.length && events[drop] <= cutoff)
34
+ drop++;
35
+ if (drop > 0)
36
+ events.splice(0, drop);
37
+ events.push(current);
38
+ return rpm === null || events.length <= rpm;
39
+ }
40
+ }
41
+ //# sourceMappingURL=limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limiter.js","sourceRoot":"","sources":["../../src/token-auth/limiter.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,cAAc,GAAG,EAAE,CAAA;AAEzB,SAAS,gBAAgB;IACvB,OAAO,WAAW,CAAC,GAAG,EAAE,GAAG,IAAI,CAAA;AACjC,CAAC;AAED,MAAM,OAAO,oBAAoB;IAAjC;QACmB,WAAM,GAAG,IAAI,GAAG,EAAoB,CAAA;IAuBvD,CAAC;IArBC;;;;;;OAMG;IACH,KAAK,CAAC,GAAW,EAAE,GAAkB,EAAE,GAAY;QACjD,MAAM,OAAO,GAAG,GAAG,IAAI,gBAAgB,EAAE,CAAA;QACzC,IAAI,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACjC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,GAAG,EAAE,CAAA;YACX,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;QAC9B,CAAC;QACD,MAAM,MAAM,GAAG,OAAO,GAAG,cAAc,CAAA;QACvC,IAAI,IAAI,GAAG,CAAC,CAAA;QACZ,OAAO,IAAI,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM;YAAE,IAAI,EAAE,CAAA;QAC7D,IAAI,IAAI,GAAG,CAAC;YAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;QACpC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACpB,OAAO,GAAG,KAAK,IAAI,IAAI,MAAM,CAAC,MAAM,IAAI,GAAG,CAAA;IAC7C,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Loopback detection for the `no-auth` bind guard, mirroring core-py
3
+ * `hull_core.server.run._is_loopback`: the literal names `localhost`,
4
+ * `127.0.0.1`, `::1`, or any IPv4 address in 127.0.0.0/8 written in the
5
+ * classic `inet_aton` forms (`127.1`, `0x7f.1`, `017700000001`, ...).
6
+ */
7
+ /** Invalid server start configuration (core-py `ServerConfigError`). */
8
+ export declare class ServerConfigError extends Error {
9
+ constructor(message: string);
10
+ }
11
+ /** Strict BSD `inet_aton`: returns the 32-bit address, or `null` when invalid. */
12
+ export declare function inetAton(host: string): number | null;
13
+ /** True when `host` is a loopback bind address under core-py's rules. */
14
+ export declare function isLoopbackHost(host: string): boolean;
15
+ /** Throw core-py's `ServerConfigError` when `no-auth` would bind beyond loopback. */
16
+ export declare function assertNoAuthLoopback(host: string): void;
17
+ //# sourceMappingURL=loopback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../src/token-auth/loopback.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,wEAAwE;AACxE,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AASD,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAepD;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAIpD;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAMvD"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Loopback detection for the `no-auth` bind guard, mirroring core-py
3
+ * `hull_core.server.run._is_loopback`: the literal names `localhost`,
4
+ * `127.0.0.1`, `::1`, or any IPv4 address in 127.0.0.0/8 written in the
5
+ * classic `inet_aton` forms (`127.1`, `0x7f.1`, `017700000001`, ...).
6
+ */
7
+ import { pyRepr } from './py-compat.js';
8
+ const LOOPBACK_NAMES = new Set(['localhost', '127.0.0.1', '::1']);
9
+ /** Invalid server start configuration (core-py `ServerConfigError`). */
10
+ export class ServerConfigError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = 'ServerConfigError';
14
+ }
15
+ }
16
+ function parsePart(part) {
17
+ if (/^0[xX][0-9a-fA-F]+$/.test(part))
18
+ return Number.parseInt(part.slice(2), 16);
19
+ if (/^0[0-7]*$/.test(part))
20
+ return part.length === 1 ? 0 : Number.parseInt(part.slice(1), 8);
21
+ if (/^[1-9][0-9]*$/.test(part))
22
+ return Number.parseInt(part, 10);
23
+ return null;
24
+ }
25
+ /** Strict BSD `inet_aton`: returns the 32-bit address, or `null` when invalid. */
26
+ export function inetAton(host) {
27
+ const parts = host.split('.');
28
+ if (parts.length < 1 || parts.length > 4)
29
+ return null;
30
+ const values = [];
31
+ for (const part of parts) {
32
+ const value = parsePart(part);
33
+ if (value === null)
34
+ return null;
35
+ values.push(value);
36
+ }
37
+ const last = values.length - 1;
38
+ for (let i = 0; i < last; i++)
39
+ if (values[i] > 0xff)
40
+ return null;
41
+ if (values[last] >= 2 ** (8 * (4 - last)))
42
+ return null;
43
+ let address = 0;
44
+ for (let i = 0; i < last; i++)
45
+ address += values[i] * 2 ** (8 * (3 - i));
46
+ return address + values[last];
47
+ }
48
+ /** True when `host` is a loopback bind address under core-py's rules. */
49
+ export function isLoopbackHost(host) {
50
+ if (LOOPBACK_NAMES.has(host))
51
+ return true;
52
+ const address = inetAton(host);
53
+ return address !== null && Math.floor(address / 2 ** 24) === 0x7f;
54
+ }
55
+ /** Throw core-py's `ServerConfigError` when `no-auth` would bind beyond loopback. */
56
+ export function assertNoAuthLoopback(host) {
57
+ if (isLoopbackHost(host))
58
+ return;
59
+ throw new ServerConfigError(`auth = 'no-auth' only permits loopback binds, refusing host ${pyRepr(host)} ` +
60
+ "(set [server] auth to 'token' or 'multi' for a shared listener)");
61
+ }
62
+ //# sourceMappingURL=loopback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback.js","sourceRoot":"","sources":["../../src/token-auth/loopback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Small helpers that reproduce Python string semantics the core-py auth stack
3
+ * leaks into its wire output and error messages, so the TS port emits the
4
+ * same bytes: `repr()` of a str, `str.strip()`, code-point slicing, and
5
+ * `json.dumps()` of a flat string map.
6
+ *
7
+ * Internal to `token-auth`; not part of the public API.
8
+ */
9
+ /** Python `str.strip()` with no argument. */
10
+ export declare function pyStrip(value: string): string;
11
+ /** Python `s[:n]` (code points, not UTF-16 units). */
12
+ export declare function pyPrefix(value: string, n: number): string;
13
+ /**
14
+ * Python `repr()` of a str: single quotes unless the text contains a single
15
+ * quote and no double quote; backslash, the chosen quote, and non-printable
16
+ * characters escaped the way CPython escapes them.
17
+ */
18
+ export declare function pyRepr(value: string): string;
19
+ /**
20
+ * Python `json.dumps(obj)` for a flat `str -> str` map with default settings
21
+ * (`ensure_ascii=True`, separators `', '` and `': '`).
22
+ */
23
+ export declare function pyJsonDumps(obj: Readonly<Record<string, string>>): string;
24
+ //# sourceMappingURL=py-compat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"py-compat.d.ts","sourceRoot":"","sources":["../../src/token-auth/py-compat.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAaH,6CAA6C;AAC7C,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO7C;AAED,sDAAsD;AACtD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzD;AAMD;;;;GAIG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAgB5C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAKzE"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Small helpers that reproduce Python string semantics the core-py auth stack
3
+ * leaks into its wire output and error messages, so the TS port emits the
4
+ * same bytes: `repr()` of a str, `str.strip()`, code-point slicing, and
5
+ * `json.dumps()` of a flat string map.
6
+ *
7
+ * Internal to `token-auth`; not part of the public API.
8
+ */
9
+ // Every code point for which Python's `str.isspace()` is true. JS `trim()`
10
+ // differs: it strips U+FEFF but not U+001C..U+001F or U+0085.
11
+ const PY_WHITESPACE = new Set([
12
+ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x85, 0xa0, 0x1680, 0x2000, 0x2001, 0x2002, 0x2003,
13
+ 0x2004, 0x2005, 0x2006, 0x2007, 0x2008, 0x2009, 0x200a, 0x2028, 0x2029, 0x202f, 0x205f, 0x3000
14
+ ]);
15
+ function isPySpace(ch) {
16
+ return PY_WHITESPACE.has(ch.codePointAt(0));
17
+ }
18
+ /** Python `str.strip()` with no argument. */
19
+ export function pyStrip(value) {
20
+ const chars = Array.from(value);
21
+ let start = 0;
22
+ let end = chars.length;
23
+ while (start < end && isPySpace(chars[start]))
24
+ start++;
25
+ while (end > start && isPySpace(chars[end - 1]))
26
+ end--;
27
+ return chars.slice(start, end).join('');
28
+ }
29
+ /** Python `s[:n]` (code points, not UTF-16 units). */
30
+ export function pyPrefix(value, n) {
31
+ return Array.from(value).slice(0, n).join('');
32
+ }
33
+ // Python's `str.isprintable()` is false for categories Cc, Cf, Cs, Co, Cn,
34
+ // Zl, Zp and Zs, except the ASCII space.
35
+ const NON_PRINTABLE = /^[\p{C}\p{Z}]$/u;
36
+ /**
37
+ * Python `repr()` of a str: single quotes unless the text contains a single
38
+ * quote and no double quote; backslash, the chosen quote, and non-printable
39
+ * characters escaped the way CPython escapes them.
40
+ */
41
+ export function pyRepr(value) {
42
+ const quote = value.includes("'") && !value.includes('"') ? '"' : "'";
43
+ let out = quote;
44
+ for (const ch of value) {
45
+ const code = ch.codePointAt(0);
46
+ if (ch === quote || ch === '\\')
47
+ out += `\\${ch}`;
48
+ else if (ch === '\t')
49
+ out += '\\t';
50
+ else if (ch === '\n')
51
+ out += '\\n';
52
+ else if (ch === '\r')
53
+ out += '\\r';
54
+ else if (ch !== ' ' && NON_PRINTABLE.test(ch)) {
55
+ if (code < 0x100)
56
+ out += `\\x${code.toString(16).padStart(2, '0')}`;
57
+ else if (code < 0x10000)
58
+ out += `\\u${code.toString(16).padStart(4, '0')}`;
59
+ else
60
+ out += `\\U${code.toString(16).padStart(8, '0')}`;
61
+ }
62
+ else
63
+ out += ch;
64
+ }
65
+ return out + quote;
66
+ }
67
+ /**
68
+ * Python `json.dumps(obj)` for a flat `str -> str` map with default settings
69
+ * (`ensure_ascii=True`, separators `', '` and `': '`).
70
+ */
71
+ export function pyJsonDumps(obj) {
72
+ const encode = (s) => JSON.stringify(s).replace(/[^\x20-\x7e]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
73
+ const items = Object.entries(obj).map(([k, v]) => `${encode(k)}: ${encode(v)}`);
74
+ return `{${items.join(', ')}}`;
75
+ }
76
+ //# sourceMappingURL=py-compat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"py-compat.js","sourceRoot":"","sources":["../../src/token-auth/py-compat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Bearer token hashing and verification for the HTTP token table.
3
+ *
4
+ * Byte-compatible with core-py `hull_core.auth.tokens`: tokens never reach
5
+ * disk in plaintext; the host stores a scrypt hash in `users.toml` /
6
+ * `config.toml` and verifies presented bearer tokens against it. Format:
7
+ *
8
+ * scrypt$<n>$<r>$<p>$<salt-hex>$<derived-key-hex>
9
+ *
10
+ * Defaults match Python exactly (N=2^15, r=8, p=1, 32-byte key, 16-byte
11
+ * salt, 64 MiB scrypt memory ceiling), so a hash minted by either language
12
+ * verifies in the other.
13
+ */
14
+ /** Raised when an encoded token hash is malformed. */
15
+ export declare class TokenHashError extends Error {
16
+ constructor(message: string, options?: {
17
+ cause?: unknown;
18
+ });
19
+ }
20
+ /** Hash a bearer token into the storable `scrypt$...` encoding. */
21
+ export declare function hashToken(token: string): Promise<string>;
22
+ /**
23
+ * Constant-time verify a presented token against an encoded hash.
24
+ *
25
+ * Rejects with `TokenHashError` when `encoded` is malformed (never resolves
26
+ * `true` for a hash it cannot parse); resolves `false` on a mismatch.
27
+ */
28
+ export declare function verifyToken(token: string, encoded: string): Promise<boolean>;
29
+ /**
30
+ * True when `encoded` parses as a scrypt token hash whose parameters the
31
+ * local scrypt accepts. Like core-py `is_valid_encoding`, it runs one full
32
+ * derivation so parameters that only fail inside scrypt (memory ceiling)
33
+ * are caught at load time instead of on the first request. Never throws.
34
+ */
35
+ export declare function isValidTokenHash(encoded: string): boolean;
36
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/token-auth/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmBH,sDAAsD;AACtD,qBAAa,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAGzD;CACF;AAiDD,mEAAmE;AACnE,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAW9D;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CASlF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAQzD"}
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Bearer token hashing and verification for the HTTP token table.
3
+ *
4
+ * Byte-compatible with core-py `hull_core.auth.tokens`: tokens never reach
5
+ * disk in plaintext; the host stores a scrypt hash in `users.toml` /
6
+ * `config.toml` and verifies presented bearer tokens against it. Format:
7
+ *
8
+ * scrypt$<n>$<r>$<p>$<salt-hex>$<derived-key-hex>
9
+ *
10
+ * Defaults match Python exactly (N=2^15, r=8, p=1, 32-byte key, 16-byte
11
+ * salt, 64 MiB scrypt memory ceiling), so a hash minted by either language
12
+ * verifies in the other.
13
+ */
14
+ import { randomBytes, scrypt, scryptSync, timingSafeEqual } from 'node:crypto';
15
+ import { pyPrefix, pyRepr } from './py-compat.js';
16
+ const SCRYPT_N = 2 ** 15;
17
+ const SCRYPT_R = 8;
18
+ const SCRYPT_P = 1;
19
+ const DKLEN = 32;
20
+ const SALT_BYTES = 16;
21
+ const PREFIX = 'scrypt';
22
+ // 128 * N * r = 32 MiB working memory; grant the same explicit 64 MiB
23
+ // ceiling core-py passes to hashlib.scrypt.
24
+ const MAXMEM = 64 * 1024 * 1024;
25
+ const DECIMAL = /^[0-9]+$/;
26
+ const HEX = /^(?:[0-9a-fA-F]{2})+$/;
27
+ /** Raised when an encoded token hash is malformed. */
28
+ export class TokenHashError extends Error {
29
+ constructor(message, options) {
30
+ super(message, options);
31
+ this.name = 'TokenHashError';
32
+ }
33
+ }
34
+ function malformed(encoded, cause) {
35
+ const shown = typeof encoded === 'string' ? pyRepr(pyPrefix(encoded, 24)) : String(encoded);
36
+ return new TokenHashError(`malformed token hash encoding: ${shown}`, cause === undefined ? undefined : { cause });
37
+ }
38
+ function parseInteger(text) {
39
+ if (!DECIMAL.test(text))
40
+ return null;
41
+ const value = Number(text);
42
+ return Number.isSafeInteger(value) ? value : null;
43
+ }
44
+ function parseHash(encoded) {
45
+ if (typeof encoded !== 'string')
46
+ throw malformed(encoded);
47
+ const parts = encoded.split('$');
48
+ if (parts.length !== 6)
49
+ throw malformed(encoded);
50
+ const [prefix, nText, rText, pText, saltHex, dkHex] = parts;
51
+ const n = parseInteger(nText);
52
+ const r = parseInteger(rText);
53
+ const p = parseInteger(pText);
54
+ if (prefix !== PREFIX || n === null || r === null || p === null)
55
+ throw malformed(encoded);
56
+ // scrypt requires N to be a power of two greater than one, r and p >= 1.
57
+ if (n < 2 || (n & (n - 1)) !== 0 || n > 2 ** 30 || r < 1 || p < 1)
58
+ throw malformed(encoded);
59
+ if (!HEX.test(saltHex) || !HEX.test(dkHex))
60
+ throw malformed(encoded);
61
+ return { n, r, p, salt: Buffer.from(saltHex, 'hex'), expected: Buffer.from(dkHex, 'hex') };
62
+ }
63
+ function scryptOptions(parsed) {
64
+ return { N: parsed.n, r: parsed.r, p: parsed.p, maxmem: MAXMEM };
65
+ }
66
+ function scryptAsync(password, salt, keylen, options) {
67
+ return new Promise((resolve, reject) => {
68
+ scrypt(password, salt, keylen, options, (err, derived) => {
69
+ if (err)
70
+ reject(err);
71
+ else
72
+ resolve(derived);
73
+ });
74
+ });
75
+ }
76
+ /** Hash a bearer token into the storable `scrypt$...` encoding. */
77
+ export async function hashToken(token) {
78
+ const salt = randomBytes(SALT_BYTES);
79
+ const dk = await scryptAsync(Buffer.from(token, 'utf8'), salt, DKLEN, scryptOptions({ n: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P }));
80
+ return [PREFIX, String(SCRYPT_N), String(SCRYPT_R), String(SCRYPT_P), salt.toString('hex'), dk.toString('hex')].join('$');
81
+ }
82
+ /**
83
+ * Constant-time verify a presented token against an encoded hash.
84
+ *
85
+ * Rejects with `TokenHashError` when `encoded` is malformed (never resolves
86
+ * `true` for a hash it cannot parse); resolves `false` on a mismatch.
87
+ */
88
+ export async function verifyToken(token, encoded) {
89
+ const parsed = parseHash(encoded);
90
+ let actual;
91
+ try {
92
+ actual = await scryptAsync(Buffer.from(token, 'utf8'), parsed.salt, parsed.expected.length, scryptOptions(parsed));
93
+ }
94
+ catch (err) {
95
+ throw malformed(encoded, err);
96
+ }
97
+ return timingSafeEqual(actual, parsed.expected);
98
+ }
99
+ /**
100
+ * True when `encoded` parses as a scrypt token hash whose parameters the
101
+ * local scrypt accepts. Like core-py `is_valid_encoding`, it runs one full
102
+ * derivation so parameters that only fail inside scrypt (memory ceiling)
103
+ * are caught at load time instead of on the first request. Never throws.
104
+ */
105
+ export function isValidTokenHash(encoded) {
106
+ try {
107
+ const parsed = parseHash(encoded);
108
+ scryptSync(Buffer.alloc(0), parsed.salt, parsed.expected.length, scryptOptions(parsed));
109
+ return true;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/token-auth/tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * users.toml: the multi-user token table (auth mode `multi`).
3
+ *
4
+ * Same schema and validation order as core-py `hull_core.auth.users`:
5
+ *
6
+ * [users.<uid>]
7
+ * token_hash = "scrypt$..." # mint with `hull token hash`
8
+ * enabled = true
9
+ * namespace = "<uid-or-shared>" # data isolation root
10
+ * allowed_roots = ["/data/alice"] # filesystem roots tools may touch
11
+ * [users.<uid>.limits]
12
+ * rpm = 60 # optional per-minute request cap
13
+ *
14
+ * Admin is host-side only: edit the file, restart the server. There is no
15
+ * admin API and no self-service.
16
+ */
17
+ /** Raised when users.toml is missing, malformed, or semantically invalid. */
18
+ export declare class UsersError extends Error {
19
+ constructor(message: string, options?: {
20
+ cause?: unknown;
21
+ });
22
+ }
23
+ /** Raised by `ensurePathAllowed` (core-py raises `PermissionError`). */
24
+ export declare class PathNotAllowedError extends Error {
25
+ readonly code = "EACCES";
26
+ constructor(message: string);
27
+ }
28
+ export interface TokenUserLimits {
29
+ /** Per-minute request cap; `null` means unlimited. */
30
+ readonly rpm: number | null;
31
+ }
32
+ /** One validated `[users.<uid>]` row. */
33
+ export interface TokenUser {
34
+ readonly uid: string;
35
+ readonly tokenHash: string;
36
+ readonly enabled: boolean;
37
+ readonly namespace: string;
38
+ readonly allowedRoots: readonly string[];
39
+ readonly limits: TokenUserLimits;
40
+ }
41
+ /** Anything carrying a caller's filesystem roots (a `TokenUser` or an `AuthContext`). */
42
+ export interface PathScope {
43
+ readonly uid: string;
44
+ readonly allowedRoots: readonly string[];
45
+ }
46
+ /**
47
+ * Parse and validate users.toml into a `uid -> TokenUser` map.
48
+ *
49
+ * Synchronous on purpose: it runs once at server build time, and each
50
+ * `token_hash` is checked with one full scrypt derivation (like core-py
51
+ * `is_valid_encoding`) so an unusable row fails the boot, not a request.
52
+ */
53
+ export declare function loadUsers(path: string): Map<string, TokenUser>;
54
+ /** Return the first user whose `tokenHash` verifies `token`, else `null`. */
55
+ export declare function findUserByToken(users: ReadonlyMap<string, TokenUser>, token: string): Promise<TokenUser | null>;
56
+ /**
57
+ * Enforce the caller's `allowedRoots`. An empty list grants nothing. Both
58
+ * sides are resolved first, so a symlinked escape needs its resolved target
59
+ * to sit under a root too. Throws `PathNotAllowedError` on denial.
60
+ */
61
+ export declare function ensurePathAllowed(scope: PathScope, path: string): void;
62
+ //# sourceMappingURL=users.d.ts.map