@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,105 @@
1
+ /**
2
+ * File-based session lock to prevent duplicate relay sessions.
3
+ *
4
+ * When multiple MCP server processes start simultaneously (common in Claude Code),
5
+ * this lock prevents each from creating a separate relay session (rate limit:
6
+ * max 10 sessions/IP/10min).
7
+ *
8
+ * Lock file location: <config_dir>/mcp/relay-session-<server>.lock
9
+ */
10
+ import { existsSync } from 'node:fs';
11
+ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
12
+ import { dirname, join } from 'node:path';
13
+ import envPaths from 'env-paths';
14
+ const paths = envPaths('mcp', { suffix: '' });
15
+ const DEFAULT_LOCK_DIR = paths.config;
16
+ const DEFAULT_MAX_AGE_MS = 600_000;
17
+ // Allow overriding lock directory for testing
18
+ let lockDirOverride = null;
19
+ // Directory init (mkdir+chmod) runs exactly once per lock-dir lifetime so an
20
+ // existing overly-permissive directory is tightened (TOCTOU hardening); the
21
+ // flag resets whenever the lock dir changes.
22
+ let dirInitDone = false;
23
+ export function setLockDir(path) {
24
+ lockDirOverride = path;
25
+ dirInitDone = false;
26
+ }
27
+ function getLockDir() {
28
+ return lockDirOverride ?? DEFAULT_LOCK_DIR;
29
+ }
30
+ function lockPath(serverName) {
31
+ return join(getLockDir(), `relay-session-${serverName}.lock`);
32
+ }
33
+ export async function acquireSessionLock(serverName, maxAgeMs = DEFAULT_MAX_AGE_MS) {
34
+ const path = lockPath(serverName);
35
+ try {
36
+ if (!existsSync(path)) {
37
+ return null;
38
+ }
39
+ const raw = await readFile(path, 'utf-8');
40
+ const data = JSON.parse(raw);
41
+ // Validate required fields
42
+ if (typeof data.session_id !== 'string' ||
43
+ typeof data.relay_url !== 'string' ||
44
+ typeof data.created_at !== 'number') {
45
+ await releaseSessionLock(serverName);
46
+ return null;
47
+ }
48
+ const info = {
49
+ sessionId: data.session_id,
50
+ relayUrl: data.relay_url,
51
+ createdAt: data.created_at
52
+ };
53
+ const age = Date.now() - info.createdAt;
54
+ if (age > maxAgeMs) {
55
+ // Expired lock: clean up and return null
56
+ await releaseSessionLock(serverName);
57
+ return null;
58
+ }
59
+ return info;
60
+ }
61
+ catch {
62
+ // Corrupt or unreadable lock file: clean up and return null
63
+ try {
64
+ await releaseSessionLock(serverName);
65
+ }
66
+ catch {
67
+ /* ignore cleanup errors */
68
+ }
69
+ return null;
70
+ }
71
+ }
72
+ export async function writeSessionLock(serverName, info) {
73
+ const path = lockPath(serverName);
74
+ const dir = dirname(path);
75
+ if (!existsSync(dir)) {
76
+ await mkdir(dir, { recursive: true, mode: 0o700 });
77
+ }
78
+ if (!dirInitDone) {
79
+ if (process.platform !== 'win32') {
80
+ await chmod(dir, 0o700);
81
+ }
82
+ dirInitDone = true;
83
+ }
84
+ const data = {
85
+ session_id: info.sessionId,
86
+ relay_url: info.relayUrl,
87
+ created_at: info.createdAt
88
+ };
89
+ // Write atomically via temp file + rename
90
+ const tmpPath = `${path}.tmp`;
91
+ await writeFile(tmpPath, JSON.stringify(data), { encoding: 'utf-8', mode: 0o600 });
92
+ await rename(tmpPath, path);
93
+ }
94
+ export async function releaseSessionLock(serverName) {
95
+ const path = lockPath(serverName);
96
+ try {
97
+ if (existsSync(path)) {
98
+ await unlink(path);
99
+ }
100
+ }
101
+ catch {
102
+ /* ignore errors during cleanup */
103
+ }
104
+ }
105
+ //# sourceMappingURL=session-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-lock.js","sourceRoot":"","sources":["../../src/storage/session-lock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Token authentication: one mechanism, three modes. TS counterpart of
3
+ * core-py `hull_core.auth.middleware.Authenticator`.
4
+ *
5
+ * - `no-auth`: loopback-only; one shared namespace; no Authorization header.
6
+ * - `token`: one shared token; still one shared namespace.
7
+ * - `multi`: users.toml; token -> user -> namespace; 401 wrong token,
8
+ * 403 disabled user, 429 over limit.
9
+ */
10
+ import type { AuthContext, HttpAuthMode } from './context.js';
11
+ import { SlidingWindowLimiter } from './limiter.js';
12
+ import { type TokenUser } from './users.js';
13
+ export type { HttpAuthMode } from './context.js';
14
+ /** Mode names in core-py `AUTH_MODES` order. */
15
+ export declare const HTTP_AUTH_MODES: readonly HttpAuthMode[];
16
+ /** HTTP auth configuration for `runHttpServer({ auth })`. */
17
+ export type HttpAuthConfig = {
18
+ readonly mode: 'no-auth';
19
+ } | {
20
+ readonly mode: 'token';
21
+ /** `scrypt$...` hash of the shared token (mint with `token hash`). */
22
+ readonly tokenHash: string;
23
+ /** Optional shared per-minute cap; `null`/absent means unlimited. */
24
+ readonly rpm?: number | null;
25
+ } | {
26
+ readonly mode: 'multi';
27
+ /** users.toml path; `~` is expanded. Defaults to `~/.hull/users.toml`. */
28
+ readonly usersFile?: string;
29
+ /** Pre-loaded table (e.g. from `loadUsers`); takes precedence over `usersFile`. */
30
+ readonly users?: ReadonlyMap<string, TokenUser>;
31
+ };
32
+ /** Result of authenticating one request. `status === null` means proceed. */
33
+ export interface AuthOutcome {
34
+ readonly context: AuthContext | null;
35
+ readonly status: number | null;
36
+ readonly detail: string;
37
+ readonly ok: boolean;
38
+ }
39
+ /** Raised when an `HttpAuthConfig` is unusable (detected before the server binds). */
40
+ export declare class AuthConfigError extends Error {
41
+ constructor(message: string, options?: {
42
+ cause?: unknown;
43
+ });
44
+ }
45
+ /** Default users.toml location, same as core-py: `~/.hull/users.toml`. */
46
+ export declare function defaultUsersFile(): string;
47
+ /**
48
+ * Extract the bearer token the way core-py does: split on the first space,
49
+ * case-insensitive `bearer` scheme, Python-`strip()`ed non-empty value.
50
+ */
51
+ export declare function parseBearer(authz: string | null | undefined): string | null;
52
+ /** Resolves bearer tokens to `AuthContext`s according to the configured mode. */
53
+ export declare class Authenticator {
54
+ readonly mode: HttpAuthMode;
55
+ readonly limiter: SlidingWindowLimiter;
56
+ private readonly config;
57
+ /**
58
+ * Validates the whole config up front (unknown mode, empty or malformed
59
+ * token hash, bad rpm, unreadable users.toml) so a broken deployment
60
+ * fails at boot rather than answering 500 per request.
61
+ */
62
+ constructor(config: HttpAuthConfig, options?: {
63
+ limiter?: SlidingWindowLimiter;
64
+ });
65
+ /** Number of rows in the users table (`multi` mode), else 0. */
66
+ get userCount(): number;
67
+ authenticate(authz: string | null | undefined): Promise<AuthOutcome>;
68
+ private admit;
69
+ }
70
+ //# sourceMappingURL=authenticator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authenticator.d.ts","sourceRoot":"","sources":["../../src/token-auth/authenticator.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AAGnD,OAAO,EAA8B,KAAK,SAAS,EAAE,MAAM,YAAY,CAAA;AAEvE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,gDAAgD;AAChD,eAAO,MAAM,eAAe,EAAE,SAAS,YAAY,EAAiD,CAAA;AAEpG,6DAA6D;AAC7D,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE,GAC5B;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,sEAAsE;IACtE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B,GACD;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;CAChD,CAAA;AAEL,6EAA6E;AAC7E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;CACrB;AAED,sFAAsF;AACtF,qBAAa,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAGzD;CACF;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAQD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAO3E;AAgDD,iFAAiF;AACjF,qBAAa,aAAa;IACxB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAA;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IAEvC;;;;OAIG;IACH,YAAY,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,oBAAoB,CAAA;KAAO,EAInF;IAED,gEAAgE;IAChE,IAAI,SAAS,IAAI,MAAM,CAEtB;IAEK,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAgCzE;IAED,OAAO,CAAC,KAAK;CAId"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Token authentication: one mechanism, three modes. TS counterpart of
3
+ * core-py `hull_core.auth.middleware.Authenticator`.
4
+ *
5
+ * - `no-auth`: loopback-only; one shared namespace; no Authorization header.
6
+ * - `token`: one shared token; still one shared namespace.
7
+ * - `multi`: users.toml; token -> user -> namespace; 401 wrong token,
8
+ * 403 disabled user, 429 over limit.
9
+ */
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { localAuthContext } from './context.js';
13
+ import { SlidingWindowLimiter } from './limiter.js';
14
+ import { pyRepr, pyStrip } from './py-compat.js';
15
+ import { isValidTokenHash, verifyToken } from './tokens.js';
16
+ import { findUserByToken, loadUsers } from './users.js';
17
+ /** Mode names in core-py `AUTH_MODES` order. */
18
+ export const HTTP_AUTH_MODES = Object.freeze(['no-auth', 'token', 'multi']);
19
+ /** Raised when an `HttpAuthConfig` is unusable (detected before the server binds). */
20
+ export class AuthConfigError extends Error {
21
+ constructor(message, options) {
22
+ super(message, options);
23
+ this.name = 'AuthConfigError';
24
+ }
25
+ }
26
+ /** Default users.toml location, same as core-py: `~/.hull/users.toml`. */
27
+ export function defaultUsersFile() {
28
+ return join(homedir(), '.hull', 'users.toml');
29
+ }
30
+ function expandUser(path) {
31
+ if (path === '~')
32
+ return homedir();
33
+ if (path.startsWith('~/') || path.startsWith('~\\'))
34
+ return join(homedir(), path.slice(2));
35
+ return path;
36
+ }
37
+ /**
38
+ * Extract the bearer token the way core-py does: split on the first space,
39
+ * case-insensitive `bearer` scheme, Python-`strip()`ed non-empty value.
40
+ */
41
+ export function parseBearer(authz) {
42
+ if (!authz)
43
+ return null;
44
+ const space = authz.indexOf(' ');
45
+ const scheme = space === -1 ? authz : authz.slice(0, space);
46
+ const value = space === -1 ? '' : pyStrip(authz.slice(space + 1));
47
+ if (scheme.toLowerCase() !== 'bearer' || value.length === 0)
48
+ return null;
49
+ return value;
50
+ }
51
+ function outcome(context, status, detail = '') {
52
+ return Object.freeze({ context, status, detail, ok: status === null && context !== null });
53
+ }
54
+ function validRpm(rpm) {
55
+ return rpm === undefined || rpm === null || (Number.isSafeInteger(rpm) && rpm >= 1);
56
+ }
57
+ function resolveConfig(config) {
58
+ const mode = config.mode;
59
+ if (mode === 'no-auth')
60
+ return { mode };
61
+ if (mode === 'token') {
62
+ const { tokenHash, rpm } = config;
63
+ if (typeof tokenHash !== 'string' || tokenHash.length === 0) {
64
+ throw new AuthConfigError('[server] auth = "token" requires a non-empty token_hash');
65
+ }
66
+ if (!isValidTokenHash(tokenHash)) {
67
+ throw new AuthConfigError('[server] token_hash is not a scrypt encoding (mint one with `hull token hash`)');
68
+ }
69
+ if (!validRpm(rpm))
70
+ throw new AuthConfigError('[server] rpm must be a positive integer');
71
+ return { mode, tokenHash, rpm: rpm ?? null };
72
+ }
73
+ if (mode === 'multi') {
74
+ const { users, usersFile } = config;
75
+ if (users !== undefined) {
76
+ for (const [uid, user] of users) {
77
+ if (!isValidTokenHash(user.tokenHash)) {
78
+ throw new AuthConfigError(`user ${pyRepr(uid)}: token_hash missing or not a scrypt encoding (mint one with \`hull token hash\`)`);
79
+ }
80
+ }
81
+ return { mode, users };
82
+ }
83
+ const path = usersFile ? expandUser(usersFile) : defaultUsersFile();
84
+ return { mode, users: loadUsers(path) };
85
+ }
86
+ const shown = typeof mode === 'string' ? pyRepr(mode) : String(mode);
87
+ throw new AuthConfigError(`[server] auth must be one of ('no-auth', 'token', 'multi'), got ${shown}`);
88
+ }
89
+ /** Resolves bearer tokens to `AuthContext`s according to the configured mode. */
90
+ export class Authenticator {
91
+ /**
92
+ * Validates the whole config up front (unknown mode, empty or malformed
93
+ * token hash, bad rpm, unreadable users.toml) so a broken deployment
94
+ * fails at boot rather than answering 500 per request.
95
+ */
96
+ constructor(config, options = {}) {
97
+ this.config = resolveConfig(config);
98
+ this.mode = this.config.mode;
99
+ this.limiter = options.limiter ?? new SlidingWindowLimiter();
100
+ }
101
+ /** Number of rows in the users table (`multi` mode), else 0. */
102
+ get userCount() {
103
+ return this.config.mode === 'multi' ? this.config.users.size : 0;
104
+ }
105
+ async authenticate(authz) {
106
+ const config = this.config;
107
+ if (config.mode === 'no-auth')
108
+ return outcome(localAuthContext(), null);
109
+ const token = parseBearer(authz);
110
+ if (token === null)
111
+ return outcome(null, 401, 'missing or malformed Authorization header');
112
+ if (config.mode === 'token') {
113
+ if (!(await verifyToken(token, config.tokenHash)))
114
+ return outcome(null, 401, 'invalid token');
115
+ return this.admit({
116
+ uid: 'shared',
117
+ namespace: 'default',
118
+ mode: 'token',
119
+ allowedRoots: Object.freeze([]),
120
+ rpm: config.rpm,
121
+ disabled: false,
122
+ attributes: Object.freeze({})
123
+ });
124
+ }
125
+ const user = await findUserByToken(config.users, token);
126
+ if (user === null)
127
+ return outcome(null, 401, 'invalid token');
128
+ if (!user.enabled)
129
+ return outcome(null, 403, `user ${pyRepr(user.uid)} is disabled`);
130
+ return this.admit({
131
+ uid: user.uid,
132
+ namespace: user.namespace,
133
+ mode: 'multi',
134
+ allowedRoots: user.allowedRoots,
135
+ rpm: user.limits.rpm,
136
+ disabled: false,
137
+ attributes: Object.freeze({})
138
+ });
139
+ }
140
+ admit(ctx) {
141
+ if (!this.limiter.check(ctx.uid, ctx.rpm))
142
+ return outcome(null, 429, 'rate limit exceeded');
143
+ return outcome(Object.freeze(ctx), null);
144
+ }
145
+ }
146
+ //# sourceMappingURL=authenticator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authenticator.js","sourceRoot":"","sources":["../../src/token-auth/authenticator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `token hash` CLI subcommand: mint a `scrypt$...` hash for `token` mode
3
+ * (`tokenHash`) or a users.toml row (`token_hash`).
4
+ *
5
+ * Unlike core-py `hull token hash <token>`, the plaintext is never taken
6
+ * from argv (it would land in shell history and the process table). It is
7
+ * read from piped stdin, or from a hidden, confirmed TTY prompt. Only the
8
+ * hash is printed to stdout; prompts and errors go to stderr.
9
+ */
10
+ /** Same shape as `CliHandler` in `cli/build-cli.ts` (kept local to avoid a dependency). */
11
+ export type TokenCliHandler = (argv: string[]) => number | Promise<number>;
12
+ export interface TokenCliInput {
13
+ isTTY?: boolean;
14
+ setRawMode?(mode: boolean): unknown;
15
+ setEncoding?(encoding: BufferEncoding): unknown;
16
+ on(event: 'data', listener: (chunk: string | Buffer) => void): unknown;
17
+ on(event: 'end' | 'close', listener: () => void): unknown;
18
+ on(event: 'error', listener: (err: Error) => void): unknown;
19
+ removeListener(event: 'data', listener: (chunk: string | Buffer) => void): unknown;
20
+ removeListener(event: 'end' | 'close', listener: () => void): unknown;
21
+ removeListener(event: 'error', listener: (err: Error) => void): unknown;
22
+ resume?(): unknown;
23
+ pause?(): unknown;
24
+ }
25
+ export interface TokenCliOutput {
26
+ write(chunk: string): unknown;
27
+ }
28
+ export interface TokenCliIO {
29
+ stdin?: TokenCliInput;
30
+ stdout?: TokenCliOutput;
31
+ stderr?: TokenCliOutput;
32
+ }
33
+ /**
34
+ * Build the `token` subcommand handler (`token hash`). Built into
35
+ * `buildCli` under the name `token`; `io` is injectable for tests.
36
+ */
37
+ export declare function tokenCliHandler(io?: TokenCliIO): TokenCliHandler;
38
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/token-auth/cli.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,2FAA2F;AAC3F,MAAM,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAE1E,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,UAAU,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAA;IACnC,WAAW,CAAC,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAA;IAC/C,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,GAAG,OAAO,CAAA;IACtE,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAA;IACzD,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAA;IAC3D,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,GAAG,OAAO,CAAA;IAClF,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAA;IACrE,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAA;IACvE,MAAM,CAAC,IAAI,OAAO,CAAA;IAClB,KAAK,CAAC,IAAI,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAA;CAC9B;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB;AAkJD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,GAAE,UAAe,GAAG,eAAe,CAyBpE"}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * `token hash` CLI subcommand: mint a `scrypt$...` hash for `token` mode
3
+ * (`tokenHash`) or a users.toml row (`token_hash`).
4
+ *
5
+ * Unlike core-py `hull token hash <token>`, the plaintext is never taken
6
+ * from argv (it would land in shell history and the process table). It is
7
+ * read from piped stdin, or from a hidden, confirmed TTY prompt. Only the
8
+ * hash is printed to stdout; prompts and errors go to stderr.
9
+ */
10
+ import { pyStrip } from './py-compat.js';
11
+ import { hashToken } from './tokens.js';
12
+ const USAGE = 'usage: token hash (reads the token from stdin or a hidden prompt; never from argv)';
13
+ class InputAborted extends Error {
14
+ }
15
+ function readAll(stdin) {
16
+ return new Promise((resolve, reject) => {
17
+ const chunks = [];
18
+ const onData = (chunk) => {
19
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk);
20
+ };
21
+ const cleanup = () => {
22
+ stdin.removeListener('data', onData);
23
+ stdin.removeListener('end', onEnd);
24
+ stdin.removeListener('error', onError);
25
+ };
26
+ const onEnd = () => {
27
+ cleanup();
28
+ try {
29
+ resolve(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
30
+ }
31
+ catch (err) {
32
+ reject(err);
33
+ }
34
+ };
35
+ const onError = (err) => {
36
+ cleanup();
37
+ reject(err);
38
+ };
39
+ stdin.on('data', onData);
40
+ stdin.on('end', onEnd);
41
+ stdin.on('error', onError);
42
+ stdin.resume?.();
43
+ });
44
+ }
45
+ /** Reads hidden lines from a raw-mode TTY; keeps type-ahead between prompts. */
46
+ class HiddenReader {
47
+ constructor(stdin, stderr) {
48
+ this.stdin = stdin;
49
+ this.stderr = stderr;
50
+ this.pending = [];
51
+ }
52
+ async read(prompt) {
53
+ this.stderr.write(prompt);
54
+ const typed = [];
55
+ const consume = (chars) => {
56
+ while (chars.length > 0) {
57
+ const ch = chars.shift();
58
+ if (ch === '\r' || ch === '\n') {
59
+ this.pending = chars;
60
+ return 'done';
61
+ }
62
+ if (ch === '\u0003')
63
+ throw new InputAborted('interrupted');
64
+ if (ch === '\u0004') {
65
+ if (typed.length === 0)
66
+ throw new InputAborted('end of input');
67
+ this.pending = chars;
68
+ return 'done';
69
+ }
70
+ if (ch === '\u007f' || ch === '\b')
71
+ typed.pop();
72
+ else
73
+ typed.push(ch);
74
+ }
75
+ return 'more';
76
+ };
77
+ try {
78
+ if (consume(this.pending) === 'done')
79
+ return typed.join('');
80
+ return await new Promise((resolve, reject) => {
81
+ const cleanup = () => {
82
+ this.stdin.removeListener('data', onData);
83
+ this.stdin.removeListener('end', onEnd);
84
+ this.stdin.removeListener('error', onError);
85
+ };
86
+ const onData = (chunk) => {
87
+ try {
88
+ if (consume(Array.from(chunk.toString())) === 'done') {
89
+ cleanup();
90
+ resolve(typed.join(''));
91
+ }
92
+ }
93
+ catch (err) {
94
+ cleanup();
95
+ reject(err);
96
+ }
97
+ };
98
+ const onEnd = () => {
99
+ cleanup();
100
+ reject(new InputAborted('end of input'));
101
+ };
102
+ const onError = (err) => {
103
+ cleanup();
104
+ reject(err);
105
+ };
106
+ this.stdin.on('data', onData);
107
+ this.stdin.on('end', onEnd);
108
+ this.stdin.on('error', onError);
109
+ this.stdin.resume?.();
110
+ });
111
+ }
112
+ finally {
113
+ this.stderr.write('\n');
114
+ }
115
+ }
116
+ }
117
+ async function promptToken(stdin, stderr) {
118
+ stdin.setEncoding?.('utf8');
119
+ stdin.setRawMode?.(true);
120
+ try {
121
+ const reader = new HiddenReader(stdin, stderr);
122
+ const first = await reader.read('Token: ');
123
+ const second = await reader.read('Repeat token: ');
124
+ if (first !== second) {
125
+ stderr.write('token hash: tokens do not match\n');
126
+ return null;
127
+ }
128
+ return first;
129
+ }
130
+ finally {
131
+ stdin.setRawMode?.(false);
132
+ stdin.pause?.();
133
+ }
134
+ }
135
+ function fromPipe(text, stderr) {
136
+ let token = text;
137
+ if (token.endsWith('\r\n'))
138
+ token = token.slice(0, -2);
139
+ else if (token.endsWith('\n'))
140
+ token = token.slice(0, -1);
141
+ if (/[\r\n]/.test(token)) {
142
+ stderr.write('token hash: expected a single line on stdin\n');
143
+ return null;
144
+ }
145
+ return token;
146
+ }
147
+ function checkToken(token, stderr) {
148
+ if (token.length === 0) {
149
+ stderr.write('token hash: empty token\n');
150
+ return false;
151
+ }
152
+ // The guard strips the bearer value, so a padded token could never match.
153
+ if (pyStrip(token) !== token) {
154
+ stderr.write('token hash: token must not start or end with whitespace\n');
155
+ return false;
156
+ }
157
+ return true;
158
+ }
159
+ /**
160
+ * Build the `token` subcommand handler (`token hash`). Built into
161
+ * `buildCli` under the name `token`; `io` is injectable for tests.
162
+ */
163
+ export function tokenCliHandler(io = {}) {
164
+ return async (argv) => {
165
+ const stdin = io.stdin ?? process.stdin;
166
+ const stdout = io.stdout ?? process.stdout;
167
+ const stderr = io.stderr ?? process.stderr;
168
+ if (argv[0] !== 'hash' || argv.length > 1) {
169
+ if (argv[0] === 'hash')
170
+ stderr.write('token hash: refusing a token on argv (it would be logged)\n');
171
+ stderr.write(`${USAGE}\n`);
172
+ return 2;
173
+ }
174
+ let token;
175
+ try {
176
+ token = stdin.isTTY ? await promptToken(stdin, stderr) : fromPipe(await readAll(stdin), stderr);
177
+ }
178
+ catch (err) {
179
+ if (err instanceof InputAborted) {
180
+ stderr.write(`token hash: aborted (${err.message})\n`);
181
+ return 130;
182
+ }
183
+ stderr.write(`token hash: could not read token: ${err.message}\n`);
184
+ return 1;
185
+ }
186
+ if (token === null || !checkToken(token, stderr))
187
+ return 1;
188
+ stdout.write(`${await hashToken(token)}\n`);
189
+ return 0;
190
+ };
191
+ }
192
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/token-auth/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Request-scoped identity for MCP tools.
3
+ *
4
+ * TS counterpart of core-py `hull_core.auth.context`. The HTTP auth guard
5
+ * resolves the caller before the MCP handler runs and binds the resulting
6
+ * `AuthContext` in an `AsyncLocalStorage` (the analogue of Python's
7
+ * contextvar). Tools read it with `currentUser()` and scope every storage
8
+ * access by its `namespace`.
9
+ */
10
+ /** The three HTTP auth modes, mirroring core-py `AUTH_MODES`. */
11
+ export type HttpAuthMode = 'no-auth' | 'token' | 'multi';
12
+ /** Resolved caller identity attached to one HTTP request. */
13
+ export interface AuthContext {
14
+ readonly uid: string;
15
+ readonly namespace: string;
16
+ readonly mode: HttpAuthMode;
17
+ /** Filesystem roots the caller may touch; empty grants nothing. */
18
+ readonly allowedRoots: readonly string[];
19
+ /** Per-minute request cap; `null` means unlimited. */
20
+ readonly rpm: number | null;
21
+ readonly disabled: boolean;
22
+ readonly attributes: Readonly<Record<string, unknown>>;
23
+ }
24
+ /** Identity used in no-auth mode (and for in-process tool calls). */
25
+ export declare function localAuthContext(): AuthContext;
26
+ /** The caller's identity; falls back to the local identity when unset. */
27
+ export declare function currentUser(): AuthContext;
28
+ /** Run `fn` with `ctx` bound as the current user for its whole async scope. */
29
+ export declare function runWithAuthContext<T>(ctx: AuthContext, fn: () => T): T;
30
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/token-auth/context.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,iEAAiE;AACjE,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAExD,6DAA6D;AAC7D,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;CACvD;AAID,qEAAqE;AACrE,wBAAgB,gBAAgB,IAAI,WAAW,CAU9C;AAED,0EAA0E;AAC1E,wBAAgB,WAAW,IAAI,WAAW,CAEzC;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAEtE"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Request-scoped identity for MCP tools.
3
+ *
4
+ * TS counterpart of core-py `hull_core.auth.context`. The HTTP auth guard
5
+ * resolves the caller before the MCP handler runs and binds the resulting
6
+ * `AuthContext` in an `AsyncLocalStorage` (the analogue of Python's
7
+ * contextvar). Tools read it with `currentUser()` and scope every storage
8
+ * access by its `namespace`.
9
+ */
10
+ import { AsyncLocalStorage } from 'node:async_hooks';
11
+ const storage = new AsyncLocalStorage();
12
+ /** Identity used in no-auth mode (and for in-process tool calls). */
13
+ export function localAuthContext() {
14
+ return Object.freeze({
15
+ uid: 'local',
16
+ namespace: 'default',
17
+ mode: 'no-auth',
18
+ allowedRoots: Object.freeze([]),
19
+ rpm: null,
20
+ disabled: false,
21
+ attributes: Object.freeze({})
22
+ });
23
+ }
24
+ /** The caller's identity; falls back to the local identity when unset. */
25
+ export function currentUser() {
26
+ return storage.getStore() ?? localAuthContext();
27
+ }
28
+ /** Run `fn` with `ctx` bound as the current user for its whole async scope. */
29
+ export function runWithAuthContext(ctx, fn) {
30
+ return storage.run(ctx, fn);
31
+ }
32
+ //# sourceMappingURL=context.js.map