@fidt/saul-server 1.2.4 → 1.3.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.
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/MachineTokenProvider.ts","../src/models/saulException.ts","../src/tunnel/TunnelAgent.ts","../src/models/file.ts","../src/models/fileStream.ts","../src/models/predicate.ts","../src/upload/UploadError.ts","../src/upload/PartDiskStore.ts","../src/upload/UploadRegistry.ts","../src/upload/UploadSweeper.ts","../src/upload/concatParts.ts","../src/upload/uploadRouter.ts"],"names":["MachineTokenProvider","options","forceRefresh","clientId","clientSecret","oidcTokenUrl","envTokenUrl","missing","resource","form","response","resolveFetch","data","detail","ttlSeconds","fetchFn","SaulException","_SaulException","message","code","value","DISPATCH_EVENT","REGISTER_EVENT","HEARTBEAT_EVENT","UPLOAD_EVENT","httpScheme","url","DEFAULT_HEARTBEAT_MS","DEFAULT_INLINE_MAX_BYTES","RECONNECT_LOG_INTERVAL_MS","UPLOAD_PATH_PREFIX","routerPath","path","rest","TunnelAgent","hostname","factory","opts","connectSocket","origin","socketPath","socket","cb","token","err","firstRegistration","resolve","reject","settled","payload","ack","reason","basePath","d","params","result","serialized","resultBlob","encoded","body","text","blobId","module","init","now","every","event","File","meta","buffer","base64","uint8Array","FILE_STREAM_BRAND","FileStream","_FileStream","metadata","source","destination","pipeline","maxBytes","chunks","total","chunk","buf","Readable","narrow","predicates","present","p","UploadError","_UploadError","UPLOAD_ERROR_STATUS","uploadErrorStatus","READ_HIGH_WATER_MARK","PartDiskStore","uploadId","assertSafeUploadId","join","partNumber","partCount","assertSafePartNumber","mkdir","finalPath","tmpPath","handle","open","hash","createHash","received","limit","meter","Transform","_enc","unlink","etag","rename","createReadStream","entries","readdir","parts","entry","match","info","stat","a","b","dir","freed","rm","olderThanMs","stale","file","fileInfo","META_FILE","UploadRegistry","bytes","record","full","allowed","deltaBytes","tmp","writeFile","raw","readFile","UploadSweeper","dirName","actual","concatParts","store","partNumbers","generate","envInt","key","fallback","n","envBool","createUploadSubsystem","tmpdir","registry","sweeper","maxPartBytes","router","Hono","c","contentLength","before","ctx","sum","ordered","fileStream"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * MACHINE TOKEN PROVIDER — lấy và cache token `client_credentials` cho service downstream.\n *\n * Đây là bản dùng chung của đoạn logic vốn nằm trong template codegen `initWsPush`\n * (`__wsPushGetAccessToken`). Reverse tunnel cần đúng luồng đó để bắt tay với gateway, và\n * hai bản copy của một quy trình xin token là hai cơ hội để một bên quên mất `resource`\n * rồi nhận opaque token mà không hiểu vì sao verify JWT lại hỏng.\n *\n * BA THỨ QUYẾT ĐỊNH TÍNH ĐÚNG ĐẮN, không cái nào bỏ được:\n *\n * 1. **`resource`** — điều kiện để oidc-provider bind token vào Saul resource server và phát\n * JWT. Thiếu nó token ra opaque, và lỗi hiện ra ở tận chỗ gateway verify JWT dưới dạng\n * \"Unexpected token ... is not valid JSON\" — không có gì trỏ ngược về đây.\n * 2. **Refresh sớm 60s** — token lấy về sát hạn sẽ chết giữa lúc đang bay tới gateway, và\n * lỗi hiện ra dưới dạng 401 ngẫu nhiên không tái hiện được.\n * 3. **Gộp lời gọi đồng thời** — agent dispatch song song nhiều call, nên mỗi lần token hết\n * hạn là N request cùng lúc phát hiện điều đó. Không gộp thì identity ăn nguyên burst.\n */\n\nexport interface MachineTokenOptions {\n /**\n * Endpoint token OIDC (grant `client_credentials`). Bỏ trống thì đọc env\n * `OIDC_TOKEN_URL` tại runtime — đọc lúc GỌI chứ không lúc khởi tạo, để thứ tự nạp env\n * không biến thành lỗi cấu hình.\n */\n oidcTokenUrl?: string;\n /** Client M2M (collection `oidc_client` ở Directus). Không bake vì là secret. */\n clientId?: string;\n clientSecret?: string;\n /** Resource indicator — xem lý do ở đầu file. */\n resource?: string;\n /** Dùng khi `resource` không được khai (thường là base URL của gateway). */\n fallbackResource?: string;\n /** Scope của Saul resource server. */\n scope?: string;\n /**\n * Token TĨNH có sẵn (phải là JWT). Truyền vào thì bỏ qua hoàn toàn client_credentials.\n * Chỉ nên dùng cho demo/script — service thật nên tự xin token để còn xoay vòng được.\n */\n machineToken?: string;\n /** DI cho test: thay `fetch` thật bằng bản giả. */\n fetchImpl?: typeof fetch;\n}\n\n/** Biên an toàn: coi token là hết hạn sớm hơn thực tế ngần này. */\nconst EARLY_REFRESH_MS = 60_000;\n\n/** TTL giả định khi endpoint không trả `expires_in` dùng được. */\nconst DEFAULT_TTL_SECONDS = 300;\n\nexport class MachineTokenProvider {\n private cachedToken: string | null = null;\n private expiresAt = 0;\n /** Lời hứa đang bay — để N lời gọi đồng thời chỉ sinh đúng một round-trip. */\n private inflight: Promise<string> | null = null;\n\n constructor(private readonly options: MachineTokenOptions) {}\n\n /**\n * Token hiện hành, tự xin mới khi cần.\n *\n * @param forceRefresh Bỏ qua cache (dùng khi gateway vừa trả 401 — token có thể bị thu\n * hồi trước hạn, và thử lại với đúng token đó thì chỉ 401 thêm lần nữa).\n */\n async getToken(forceRefresh = false): Promise<string> {\n if (this.options.machineToken) return this.options.machineToken;\n\n if (!forceRefresh && this.cachedToken && Date.now() < this.expiresAt) {\n return this.cachedToken;\n }\n\n // Gộp vào lời hứa đang bay. `forceRefresh` cũng gộp: hai lời gọi cùng phát hiện token\n // hỏng thì vẫn chỉ cần MỘT token mới.\n if (this.inflight) return this.inflight;\n\n this.inflight = this.requestToken()\n .finally(() => {\n // Dọn TRONG MỌI TRƯỜNG HỢP, kể cả khi hỏng: giữ lại một lời hứa đã reject là\n // cách để một lỗi mạng thoáng qua khoá cứng provider tới lúc restart process.\n this.inflight = null;\n });\n\n return this.inflight;\n }\n\n /** Vứt cache — dùng khi đổi cấu hình hoặc muốn ép bắt tay lại từ đầu. */\n reset(): void {\n this.cachedToken = null;\n this.expiresAt = 0;\n }\n\n private async requestToken(): Promise<string> {\n const { clientId, clientSecret } = this.options;\n const oidcTokenUrl = this.options.oidcTokenUrl || envTokenUrl();\n\n // Báo thiếu thứ gì cụ thể. \"Thiếu cấu hình OIDC\" bắt người đọc đi dò từng biến.\n const missing: string[] = [];\n if (!oidcTokenUrl) missing.push('oidcTokenUrl (hoặc env OIDC_TOKEN_URL)');\n if (!clientId) missing.push('clientId');\n if (!clientSecret) missing.push('clientSecret');\n if (missing.length) {\n throw new Error(`Machine token thiếu cấu hình: ${missing.join(', ')}`);\n }\n\n const resource = this.options.resource || this.options.fallbackResource || '';\n const form = new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: clientId!,\n client_secret: clientSecret!,\n scope: this.options.scope ?? 'access',\n resource,\n }).toString();\n\n const fetchFn = this.options.fetchImpl ?? resolveFetch();\n const response = await fetchFn(oidcTokenUrl!, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: form,\n } as any);\n\n const data: any = await (response as any).json().catch(() => undefined);\n\n if (!(response as any).ok) {\n const detail = (data && (data.error_description || data.error)) || 'không rõ nguyên nhân';\n throw new Error(`OIDC token request thất bại (${(response as any).status}): ${detail}`);\n }\n if (!data || typeof data.access_token !== 'string') {\n throw new Error('OIDC token response không có access_token');\n }\n\n const ttlSeconds = typeof data.expires_in === 'number' ? data.expires_in : DEFAULT_TTL_SECONDS;\n this.cachedToken = data.access_token as string;\n this.expiresAt = Date.now() + Math.max(ttlSeconds * 1000 - EARLY_REFRESH_MS, 0);\n return this.cachedToken;\n }\n}\n\n/** Đọc env lúc gọi, không hằng số hoá lúc import (test đổi env giữa chừng vẫn đúng). */\nconst envTokenUrl = (): string | undefined => {\n const runtimeProcess = (globalThis as any).process as\n | { env?: Record<string, string | undefined> }\n | undefined;\n return runtimeProcess?.env?.OIDC_TOKEN_URL;\n};\n\nconst resolveFetch = (): typeof fetch => {\n const fetchFn = (globalThis as any).fetch as typeof fetch | undefined;\n if (typeof fetchFn !== 'function') {\n throw new Error('FETCH_UNAVAILABLE: runtime không có global fetch (cần Node >= 18)');\n }\n return fetchFn;\n};\n","/**\n * SaulException — lỗi NGHIỆP VỤ mà handler chủ động ném để từ chối một lời gọi.\n *\n * Khác `Error` trần ở đúng hai điểm, và cả hai đều là lý do nó tồn tại:\n *\n * 1. **Có `code`** đi qua được dây. Envelope lỗi của downstream mang cả `message` lẫn\n * `code`, gateway map sang ack `{ ok:false, error, code }`, client dựng lại thành\n * `SaulException` — nên client bắt được lỗi theo mã chứ không phải so khớp chuỗi tiếng\n * Việt. `Error` trần chỉ còn lại `message` sau chuyến đi.\n * 2. **Là ý ĐỊNH từ chối, không phải sự cố.** Gateway log `Error` lạ như một lỗi server,\n * còn `SaulException` là câu trả lời hợp lệ (\"bạn không được sub chat của người khác\")\n * nên không cần ồn ào trong log.\n *\n * Chỗ dùng chính là validator của subscription: controller `sub<Model>` ném `SaulException`\n * để từ chối, và cái ném đó nổi lên đúng chỗ client đang `catch` quanh vòng `for await`.\n */\nexport class SaulException extends Error {\n /** Mã ổn định để client rẽ nhánh. Mặc định là mã chung của một từ chối nghiệp vụ. */\n readonly code: string;\n\n constructor(message: string, code: string = 'SAUL_EXCEPTION') {\n super(message);\n this.name = 'SaulException';\n this.code = code;\n // Bắt buộc khi target ES5/ES2015: không có dòng này thì `instanceof SaulException`\n // trả false ở chính nơi người ta sẽ dùng nó nhiều nhất — khối catch.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Nhận diện KHÔNG dùng `instanceof`.\n *\n * Gateway, downstream và SDK có thể resolve ba bản copy vật lý khác nhau của package\n * này (pnpm workspace + code sinh ra được copy vào từng service), nên `instanceof` sẽ\n * trả false cho một SaulException hoàn toàn thật. Cùng lý do với `FileStream.is`.\n */\n static is(value: unknown): value is SaulException {\n return (\n value instanceof SaulException ||\n (value instanceof Error && (value as { name?: string }).name === 'SaulException')\n );\n }\n}\n","import { hostname } from 'os';\nimport { io as connectSocket } from 'socket.io-client';\nimport { SaulException } from '../models/saulException.ts';\nimport { MachineTokenProvider, type MachineTokenOptions } from '../auth/MachineTokenProvider.ts';\n\n/**\n * TUNNEL AGENT — đầu DOWNSTREAM của reverse tunnel.\n *\n * Service của bạn tự mở một WebSocket RA gateway, khai mình phục vụ những module nào, rồi\n * nằm nhận việc trên chính dây đó. Nhờ vậy service **không mở port nào** và **không có URL\n * public nào tồn tại** — gateway không bao giờ dial vào, nó chỉ trả lời trên kết nối mà bạn\n * đã mở. Đó là toàn bộ lý do mô hình này riêng tư.\n *\n * Nó KHÔNG biết gì về `functionMap`: `dispatch` được truyền vào từ ngoài, đúng cách\n * `createUploadSubsystem` nhận `dispatch` của runtime. Nhờ vậy agent dùng CHUNG một bảng\n * dispatch với HTTP và WebSocket — handler của bạn không đổi một dòng nào, và `ctx.transport`\n * vẫn là `'websocket'`.\n */\n\n/** Phải khớp hằng cùng tên ở gateway (`TunnelDispatcher.ts`). */\nconst DISPATCH_EVENT = 'saul:agent:dispatch';\nconst REGISTER_EVENT = 'saul:agent:register';\nconst HEARTBEAT_EVENT = 'saul:agent:heartbeat';\nconst UPLOAD_EVENT = 'saul:agent:upload';\n\n/**\n * Quy `ws://` → `http://` và `wss://` → `https://`.\n *\n * Một URL gateway phục vụ HAI đường: socket.io (chấp nhận cả bốn scheme) và `fetch` cho blob\n * store (chỉ chấp nhận http/https). `wss://` là thứ người ta gõ theo phản xạ cho \"URL\n * WebSocket\", và vì socket.io nuốt được nên nó chạy tới tận lúc cần kéo một blob mới nổ\n * `TypeError: protocol must be http:, https:` — ở một chỗ không gợi ý gì về nguyên nhân.\n *\n * Quy đổi ngay từ đầu, một lần, để cả hai đường dùng chung một giá trị hợp lệ. socket.io tự\n * nâng cấp `https:` thành `wss:` lúc bắt tay nên không mất gì.\n */\nconst httpScheme = (url: string): string => url.replace(/^ws(s?):\\/\\//i, 'http$1://');\n\n/** Chu kỳ heartbeat mặc định — khoảng một phần ba TTL directory của gateway (30s). */\nconst DEFAULT_HEARTBEAT_MS = 10_000;\n\n/** Khớp mặc định `TUNNEL_INLINE_MAX_BYTES` của gateway. */\nconst DEFAULT_INLINE_MAX_BYTES = 256 * 1024;\n\n/**\n * Nhịp tối thiểu giữa hai dòng log của các lần NỐI LẠI hỏng.\n *\n * Backoff tối đa là 30s, nên một agent hỏng qua đêm đẻ vài nghìn dòng giống hệt nhau — đủ\n * để người ta lọc nó đi, và thế là quay về đúng điểm mù cũ. Log lần đầu ngay lập tức rồi\n * giãn ra, kèm số lần đã hỏng, để một lần `grep` vẫn trả lời được \"hỏng từ bao giờ\".\n */\nconst RECONNECT_LOG_INTERVAL_MS = 60_000;\n\n/** Envelope một lời gọi gateway đẩy xuống. */\nexport interface TunnelDispatchBody {\n module: string;\n function: string;\n params: Record<string, any>;\n}\n\nexport interface TunnelAgentOptions extends MachineTokenOptions {\n /** Base URL của gateway (vd `https://saul.example.com`). */\n gatewayUrl: string;\n /** Module mà service này phục vụ. Cần permission `<module>.wsAgent` cho từng cái. */\n modules: string[];\n /**\n * Định danh ổn định qua reconnect — thứ con người dùng để nhận ra \"máy nào\" trong màn\n * Agents. Bỏ trống thì suy từ hostname + pid.\n */\n instanceId?: string;\n /** Khả năng tự khai, vd `['upload']`. Gateway dùng thay cho việc probe HTTP. */\n capabilities?: string[];\n /**\n * Phiên bản service (thường là `version` trong `package.json` của nó).\n *\n * KHÔNG dùng để định tuyến — chỉ để nhìn thấy được. Trong rolling deploy, M agent của\n * cùng một module chạy hai phiên bản code và gateway xoay vòng qua tất cả, nên cùng một\n * lời gọi có thể cư xử hai kiểu; không có cột này thì điều đó chỉ suy đoán được từ hành\n * vi lệch nhau giữa các request.\n */\n version?: string;\n /** Bảng dispatch của runtime — thường là `(body) => saul.dispatch(body)`. */\n dispatch: (body: TunnelDispatchBody) => Promise<any>;\n /** Path socket.io của gateway, nếu khác mặc định. */\n path?: string;\n heartbeatMs?: number;\n /**\n * Kết quả lớn hơn ngần này (byte) sẽ đi qua blob store thay vì nằm trong ack. PHẢI khớp\n * `TUNNEL_INLINE_MAX_BYTES` của gateway.\n */\n inlineMaxBytes?: number;\n /**\n * Router `/saul/upload` của service (`createUploadSubsystem().router`).\n *\n * Có nó thì multipart upload chạy được qua tunnel: gateway đóng gói request HTTP, agent\n * replay vào đúng router này. Không có thì mọi request upload trả lỗi rõ ràng thay vì\n * timeout khó hiểu. `Saul.connectTunnel()` tự truyền vào.\n */\n uploadRouter?: { request(path: string, init?: any): Response | Promise<Response> };\n /** DI cho test: thay socket.io-client bằng bản giả. */\n socketFactory?: (url: string, options: any) => any;\n}\n\n/** Prefix mà gateway gắn vào path upload trên dây — khớp `UploadRelay.sendViaTunnel`. */\nconst UPLOAD_PATH_PREFIX = '/saul/upload';\n\n/**\n * Path trên dây → path mà SUB-ROUTER upload hiểu.\n *\n * Gateway gửi path HTTP đầy đủ (`/saul/upload/init`) vì đó là thứ nó gõ khi đi đường HTTP.\n * Nhưng `uploadRouter` là sub-router độc lập, được `app.route('/saul/upload', router)` gắn\n * vào app — bản thân nó chỉ khai `/init`, `/:uploadId/part/:n`… Replay nguyên path đầy đủ\n * vào nó thì Hono không khớp route nào và trả `404 Not Found`, khiến gateway báo\n * \"Downstream từ chối init (404)\" dù service hoàn toàn có upload.\n */\nconst routerPath = (path: string): string => {\n if (!path.startsWith(UPLOAD_PATH_PREFIX)) return path;\n const rest = path.slice(UPLOAD_PATH_PREFIX.length);\n // `/saul/upload` trần (không có phần đuôi) → `/`, vì Hono cần path bắt đầu bằng `/`.\n return rest.startsWith('/') ? rest : `/${rest}`;\n};\n\nexport class TunnelAgent {\n private socket?: any;\n private heartbeatTimer?: ReturnType<typeof setInterval>;\n private readonly tokens: MachineTokenProvider;\n private readonly instanceId: string;\n private closed = false;\n /** Id gateway cấp lúc đăng ký — blob khoá theo nó, nên phải giữ lại sau mỗi lần register. */\n private agentId?: string;\n private readonly blobBaseUrl: string;\n private readonly inlineMaxBytes: number;\n /**\n * Lần bắt tay sau phải BỎ QUA cache token.\n *\n * Bật khi gateway vừa từ chối xác thực: token trong tay có thể đã bị thu hồi TRƯỚC hạn,\n * và `MachineTokenProvider` thì chỉ nhìn `exp` nên vẫn vui vẻ trả lại đúng cái token vừa\n * bị chối — nối lại bao nhiêu lần cũng hỏng y hệt.\n */\n private forceTokenRefresh = false;\n /** Số lần nối lại hỏng liên tiếp; reset khi đăng ký lại được. */\n private reconnectFailures = 0;\n private lastReconnectLogAt = 0;\n\n constructor(private readonly options: TunnelAgentOptions) {\n this.tokens = new MachineTokenProvider({\n ...options,\n // Không khai `resource` thì dùng chính gateway — đúng mặc định của `initWsPush`.\n fallbackResource: options.fallbackResource ?? options.gatewayUrl,\n });\n this.instanceId = options.instanceId?.trim() || `${hostname()}-${process.pid}`;\n // `httpScheme`: blob đi bằng `fetch`, mà `fetch` từ chối thẳng `ws:`/`wss:` với\n // `TypeError: protocol must be http:, https:` — xem ghi chú ở hàm đó.\n this.blobBaseUrl = httpScheme(options.gatewayUrl).replace(/\\/+$/, '');\n // Phải KHỚP `TUNNEL_INLINE_MAX_BYTES` của gateway. Lệch xuống thì agent đẩy blob cho\n // những kết quả gateway thừa sức nhận inline (tốn một round-trip vô ích); lệch lên thì\n // kết quả lớn vẫn chui qua pub/sub.\n this.inlineMaxBytes = options.inlineMaxBytes ?? DEFAULT_INLINE_MAX_BYTES;\n }\n\n get connected(): boolean {\n return this.socket?.connected === true;\n }\n\n /**\n * Mở kết nối và đăng ký.\n *\n * Trả về khi gateway đã NHẬN đăng ký. Đăng ký hỏng thì `connect()` NÉM chứ không im lặng\n * để agent chạy tiếp: một agent tưởng mình đang phục vụ trong khi gateway không biết nó\n * tồn tại là chế độ hỏng không quan sát được — mọi lời gọi trả `AGENT_OFFLINE` còn service\n * thì nằm đó khoẻ mạnh.\n */\n async connect(): Promise<void> {\n this.closed = false;\n // Xin token NGAY, dù `auth` bên dưới là hàm và sẽ tự xin lại: thiếu `clientId` hay\n // `oidcTokenUrl` phải nổ ở đây với thông điệp chỉ đúng biến còn thiếu của\n // `MachineTokenProvider`, chứ không hoá trang thành một `connect_error` của\n // socket.io — thứ không gợi ý gì về cấu hình.\n await this.tokens.getToken();\n\n const factory = this.options.socketFactory ?? ((url: string, opts: any) => connectSocket(url, opts));\n const { origin, socketPath } = this.resolveEndpoint();\n const socket = factory(origin, {\n // HÀM, không phải object. socket.io gọi lại nó trước MỖI lần nối, kể cả mọi lần\n // nối lại; một object tĩnh thì token bị chốt cứng ở `connect()` đầu tiên và agent\n // chết VĨNH VIỄN ngay khi token hết hạn (token máy sống 3 ngày). Chế độ hỏng đó\n // hoàn toàn câm: pod `Running`, gateway trả `AGENT_OFFLINE`, log trống trơn.\n auth: (cb: (data: any) => void) => {\n this.tokens\n .getToken(this.forceTokenRefresh)\n .then((token) => {\n this.forceTokenRefresh = false;\n cb({ token });\n })\n .catch((err: Error) => {\n // VẪN phải gọi `cb`: bỏ qua nó là treo lần bắt tay này mà không sinh\n // `connect_error` nào, nên socket.io cũng không hẹn lần sau — identity\n // chết một nhịp biến thành agent chết hẳn.\n this.logReconnectFailure(`không lấy được token máy: ${err.message}`);\n cb({ token: '' });\n });\n },\n path: socketPath,\n // Chỉ WebSocket: long-polling cần session affinity ở LB, và agent là kết nối dài\n // hạn nên không có lý do gì để chấp nhận cái giá đó.\n transports: ['websocket'],\n reconnection: true,\n // Có JITTER: một replica gateway restart sẽ làm TOÀN BỘ agent của nó nối lại cùng\n // một nhịp và dồn hết sang replica khác.\n randomizationFactor: 0.5,\n reconnectionDelay: 1_000,\n reconnectionDelayMax: 30_000,\n });\n this.socket = socket;\n\n const firstRegistration = new Promise<void>((resolve, reject) => {\n let settled = false;\n\n // Đăng ký lại sau MỖI lần nối — gateway dọn sạch sổ agent khi socket rớt, nên\n // không đăng ký lại là nằm im vĩnh viễn.\n socket.on('connect', () => {\n this.register()\n .then(() => {\n if (!settled) {\n settled = true;\n resolve();\n }\n })\n .catch((err: Error) => {\n if (!settled) {\n settled = true;\n reject(err);\n return;\n }\n // Lần nối lại mà đăng ký hỏng (quyền vừa bị gỡ): không giết process\n // của service, nhưng phải ồn ào — nó đang chạy mà không nhận được việc.\n console.error('[saul][tunnel] đăng ký lại thất bại:', err.message);\n });\n });\n\n socket.on('connect_error', (err: Error) => {\n const message = err?.message ?? 'không rõ lý do';\n // Gateway từ chối XÁC THỰC (`WS_AUTH:` là prefix nó gắn ở `io.use`): đưa lại\n // đúng token vừa bị chối chỉ tốn thêm một vòng, nên lần sau đi xin token mới.\n if (message.startsWith('WS_AUTH')) this.forceTokenRefresh = true;\n\n if (!settled) {\n settled = true;\n // In ĐÚNG endpoint đã gõ cửa, không phải `gatewayUrl` thô: hai giá trị đó\n // khác nhau khi gateway có path (xem `resolveEndpoint`), và chính chỗ lệch\n // đó là nguyên nhân thường gặp nhất của một `timeout` ở đây.\n reject(\n new Error(\n `Không kết nối được gateway '${origin}${socketPath ?? ''}': ${err.message}. ` +\n `Kiểm: gateway có đang chạy không, reverse proxy đã cho phép nâng cấp WebSocket chưa, ` +\n `và WS_PATH của gateway có khớp không (đặt option 'path' để chỉ định tay).`\n )\n );\n return;\n }\n\n // Nối lại hỏng PHẢI ồn ào. Bản trước chỉ log ở lần bắt tay đầu tiên, nên một\n // agent chết vì token hết hạn nằm im nhiều ngày mà không để lại dòng nào nối\n // \"service khoẻ\" với \"gateway bảo nó offline\".\n this.logReconnectFailure(\n `nối lại gateway '${origin}${socketPath ?? ''}' thất bại: ${message}`\n );\n });\n });\n\n socket.on(DISPATCH_EVENT, (payload: any, ack?: (response: any) => void) => {\n // KHÔNG await: nhiều lời gọi phải chạy song song trên cùng một socket. Chờ ở đây\n // biến agent thành hàng đợi một luồng.\n void this.handleDispatch(payload, ack);\n });\n\n socket.on(UPLOAD_EVENT, (payload: any, ack?: (response: any) => void) => {\n void this.handleUpload(payload, ack);\n });\n\n socket.on('disconnect', (reason: string) => {\n console.warn(`[saul][tunnel] mất kết nối tới gateway (${reason}) — đang thử nối lại`);\n });\n\n await firstRegistration;\n this.startHeartbeat();\n }\n\n /** Ngắt kết nối và dừng heartbeat. */\n async close(): Promise<void> {\n this.closed = true;\n this.stopHeartbeat();\n this.socket?.disconnect();\n this.socket = undefined;\n }\n\n /**\n * Tách `gatewayUrl` thành ORIGIN để kết nối và PATH cho socket.io.\n *\n * socket.io hiểu phần path của URL là **NAMESPACE**, không phải prefix:\n * `io('https://host/api')` nghĩa là \"namespace `/api`, path `/socket.io` ở ROOT\" — nó sẽ\n * gõ cửa `https://host/socket.io/…`. Với gateway đứng sau reverse proxy chỉ route\n * `/api/*`, request đó rơi vào hư không và agent chết bằng `timeout`, một thông điệp\n * không hề gợi ý nguyên nhân thật.\n *\n * Khác hẳn `initWsPush`, nơi `gatewayBaseUrl` được nối chuỗi (`${base}/ws/push`) nên một\n * base có path hoạt động đúng như trực giác. Hai API nhận cùng một URL nhưng diễn giải\n * khác nhau, nên chỗ này phải tự quy đổi.\n *\n * `options.path` khai tường minh luôn THẮNG — gateway đổi `WS_PATH` thì đó là đường duy\n * nhất để cắm vào.\n */\n private resolveEndpoint(): { origin: string; socketPath: string | undefined } {\n try {\n const url = new URL(httpScheme(this.options.gatewayUrl));\n const basePath = url.pathname.replace(/\\/+$/, '');\n return {\n origin: url.origin,\n socketPath: this.options.path ?? `${basePath}/socket.io`,\n };\n } catch {\n // URL không parse được: trả về NGUYÊN VẸN để lỗi nổ ở socket.io với đúng giá trị\n // sai, thay vì nổ ở đây bằng một lỗi parse không có ngữ cảnh.\n return { origin: this.options.gatewayUrl, socketPath: this.options.path };\n }\n }\n\n /** Khai module với gateway; ném khi không module nào được nhận. */\n private async register(): Promise<void> {\n const ack = await this.emitWithAck(REGISTER_EVENT, {\n instanceId: this.instanceId,\n modules: this.options.modules,\n capabilities: this.options.capabilities ?? [],\n version: this.options.version,\n });\n\n if (!ack?.ok) {\n throw new Error(\n `Gateway từ chối đăng ký agent: ${ack?.error ?? 'không rõ lý do'}${ack?.code ? ` [${ack.code}]` : ''}`\n );\n }\n\n const data = ack.data ?? {};\n // Nối lại được rồi: quên chuỗi hỏng cũ để lần hỏng sau lại được log ngay lập tức.\n this.reconnectFailures = 0;\n // Giữ lại: blob khoá theo `agentId`, và nó ĐỔI sau mỗi lần đăng ký lại (gateway cấp\n // mới cho mỗi connection). Dùng id cũ sau reconnect là mọi lần kéo blob đều 403.\n this.agentId = data.agentId;\n // Từ chối MỘT PHẦN không phải lỗi chí mạng — các module còn lại vẫn phục vụ được —\n // nhưng im lặng thì người vận hành chỉ phát hiện khi có người báo mất tính năng.\n if (Array.isArray(data.denied) && data.denied.length) {\n console.warn(\n '[saul][tunnel] module bị từ chối:',\n data.denied.map((d: any) => `${d.module} (${d.code})`).join(', ')\n );\n }\n\n // Log đầy đủ thông tin agent ngay khi kết nối thành công — nằm ở ĐÂY (thư viện dùng\n // chung) chứ không phải ở từng downstream service, để mọi service dùng `connectTunnel`\n // đều tự động có log này mà không ai phải tự viết.\n console.log(\n `[saul][tunnel] agent đã kết nối gateway: instanceId=${this.instanceId}, ` +\n `version=${this.options.version ?? '?'}, ` +\n `capabilities=${JSON.stringify(this.options.capabilities ?? [])}, ` +\n `modules=${JSON.stringify(data.accepted ?? [])}, ` +\n `agentId=${data.agentId}, ip=${data.ip ?? '?'}, gateway=${this.options.gatewayUrl}`\n );\n }\n\n /** Chạy một lời gọi và ack lại đúng envelope mà gateway đọc. */\n private async handleDispatch(payload: any, ack?: (response: any) => void): Promise<void> {\n try {\n if (!payload || typeof payload.module !== 'string' || typeof payload.function !== 'string') {\n throw new Error('Envelope dispatch thiếu module/function');\n }\n\n // Payload lớn không đi trong message socket (nó sẽ phải qua Redis pub/sub của\n // gateway và nghẹt cả cluster) — gateway cất vào blob store, ta kéo về ở đây.\n const params = payload.paramsBlob\n ? await this.pullBlob(payload.paramsBlob, payload.module)\n : payload.params ?? {};\n\n const result = await this.options.dispatch({\n module: payload.module,\n function: payload.function,\n params,\n });\n\n // Chiều ngược lại cũng vậy: kết quả lớn đi qua blob, ack chỉ mang id.\n const serialized = JSON.stringify(result ?? null);\n if (Buffer.byteLength(serialized, 'utf8') > this.inlineMaxBytes) {\n const resultBlob = await this.pushBlob(serialized, payload.module);\n ack?.({ ok: true, resultBlob });\n return;\n }\n\n ack?.({ ok: true, result });\n } catch (err: any) {\n // MỌI lỗi đều thành ack, không cái nào được nổi lên thành unhandled rejection:\n // một handler hỏng chỉ được làm hỏng ĐÚNG lời gọi đó, không phải cả agent.\n //\n // `SaulException.is` chứ không `instanceof`: package này có thể tồn tại nhiều bản\n // vật lý (pnpm workspace + code sinh copy vào từng service).\n ack?.({\n ok: false,\n error: err?.message ?? 'Lỗi không xác định ở downstream',\n code: SaulException.is(err) ? err.code : undefined,\n });\n }\n }\n\n /**\n * Replay một request multipart upload vào chính Hono router `/saul/upload` của service.\n *\n * KHÔNG dựng lại logic upload ở đây: registry, part store, mã lỗi, ràng buộc kích thước —\n * tất cả đã nằm trong router đó và đã được test. Đóng gói lại thành một `Request` rồi gọi\n * `router.request()` cho ta dùng nguyên bộ ấy, và giữ cho đường HTTP với đường tunnel\n * không thể trôi lệch hành vi.\n *\n * Path trên dây là path HTTP ĐẦY ĐỦ (`/saul/upload/init`) — xem `routerPath` về lý do\n * phải cắt prefix trước khi đưa vào sub-router.\n */\n private async handleUpload(payload: any, ack?: (response: any) => void): Promise<void> {\n try {\n if (!this.options.uploadRouter) {\n throw new Error('Service này không bật multipart upload (thiếu uploadRouter)');\n }\n if (!payload || typeof payload.path !== 'string' || typeof payload.method !== 'string') {\n throw new Error('Envelope upload thiếu method/path');\n }\n\n const encoded = payload.paramsBlob\n ? (await this.pullBlob(payload.paramsBlob, payload.module)).body\n : payload.params?.body ?? '';\n\n // Part là NHỊ PHÂN nên đi trên dây dưới dạng base64; giải mã lại ở đây để router\n // nhận đúng byte gốc.\n const body = encoded ? Buffer.from(String(encoded), 'base64') : undefined;\n\n const response = await this.options.uploadRouter.request(routerPath(payload.path), {\n method: payload.method,\n headers: payload.headers ?? {},\n body: body && payload.method !== 'GET' && payload.method !== 'DELETE' ? body : undefined,\n });\n\n const text = await response.text();\n const result = { status: response.status, body: text };\n\n // Response của `complete` mang kết quả handler, có thể lớn — đi qua blob như mọi\n // payload lớn khác.\n const serialized = JSON.stringify(result);\n if (Buffer.byteLength(serialized, 'utf8') > this.inlineMaxBytes) {\n ack?.({ ok: true, resultBlob: await this.pushBlob(serialized, payload.module) });\n return;\n }\n\n ack?.({ ok: true, result });\n } catch (err: any) {\n ack?.({ ok: false, error: err?.message ?? 'Lỗi upload không xác định' });\n }\n }\n\n /**\n * Kéo một blob payload về từ gateway.\n *\n * AGENT là bên gọi ra (outbound HTTPS), y như khi mở socket — gateway không bao giờ dial\n * vào đây. Đó là điều giữ cho mô hình vẫn riêng tư dù dữ liệu lớn đi đường HTTP.\n */\n private async pullBlob(blobId: string, module: string): Promise<Record<string, any>> {\n const url =\n `${this.blobBaseUrl}/tunnel/blob/${encodeURIComponent(blobId)}` +\n `?module=${encodeURIComponent(module)}&agentId=${encodeURIComponent(this.agentId ?? '')}`;\n\n const response = await this.fetchWithToken(url, { method: 'GET' });\n if (!response.ok) {\n const detail = await response.text().catch(() => '');\n throw new Error(`Không kéo được payload từ gateway (${response.status}): ${detail}`);\n }\n\n return JSON.parse(await response.text());\n }\n\n /** Đẩy kết quả lớn lên gateway, trả về id để gửi trong ack. */\n private async pushBlob(data: string, module: string): Promise<string> {\n const response = await this.fetchWithToken(`${this.blobBaseUrl}/tunnel/blob`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ module, agentId: this.agentId, data }),\n });\n\n const payload: any = await response.json().catch(() => undefined);\n if (!response.ok || !payload?.blobId) {\n throw new Error(\n `Không đẩy được kết quả lên gateway (${response.status}): ${payload?.error ?? 'không có blobId'}`\n );\n }\n return payload.blobId;\n }\n\n private async fetchWithToken(url: string, init: any): Promise<any> {\n const fetchFn = (globalThis as any).fetch;\n if (typeof fetchFn !== 'function') throw new Error('FETCH_UNAVAILABLE (cần Node >= 18)');\n\n const token = await this.tokens.getToken();\n return await fetchFn(url, {\n ...init,\n headers: { ...(init.headers ?? {}), authorization: `Bearer ${token}` },\n });\n }\n\n /**\n * Log một lần nối lại hỏng, có tiết chế theo `RECONNECT_LOG_INTERVAL_MS`.\n *\n * Dùng `console.error` chứ không `warn`: agent không nối lại được là service đang chạy mà\n * không nhận được việc — đúng thứ cần lọt qua bộ lọc mức log của mọi hệ thống thu thập.\n */\n private logReconnectFailure(message: string): void {\n this.reconnectFailures += 1;\n const now = Date.now();\n if (this.reconnectFailures > 1 && now - this.lastReconnectLogAt < RECONNECT_LOG_INTERVAL_MS) return;\n this.lastReconnectLogAt = now;\n console.error(`[saul][tunnel] ${message} (hỏng liên tiếp lần ${this.reconnectFailures})`);\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n const every = this.options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;\n\n this.heartbeatTimer = setInterval(() => {\n if (this.closed || !this.socket) return;\n this.socket.emit(HEARTBEAT_EVENT, {}, (ack: any) => {\n // Gateway quên mất agent này (thường do replica vừa restart): đăng ký lại thay\n // vì tiếp tục nằm chờ việc không bao giờ tới.\n if (ack && ack.ok === false) {\n void this.register().catch((err: Error) =>\n console.error('[saul][tunnel] đăng ký lại sau heartbeat thất bại:', err.message)\n );\n }\n });\n }, every);\n\n // Heartbeat không được giữ process sống: service tắt thì nó phải tắt theo.\n this.heartbeatTimer.unref?.();\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = undefined;\n }\n\n private emitWithAck(event: string, payload: any): Promise<any> {\n return new Promise((resolve) => {\n this.socket.emit(event, payload, resolve);\n });\n }\n}\n","export class File {\n private content: string | null\n\n public metadata: {\n name: string\n size: number\n type: string\n }\n\n constructor(meta?: { name: string, size: number, type: string }) {\n this.metadata = meta || { name: '', size: 0, type: '' }\n this.content = ''\n }\n\n private encodeBase64(buffer: ArrayBuffer): string {\n return Buffer.from(buffer).toString('base64');\n }\n\n private decodeBase64(base64: string): ArrayBuffer {\n const buffer = Buffer.from(base64, 'base64');\n const uint8Array = new Uint8Array(buffer.length);\n buffer.copy(uint8Array);\n return uint8Array.buffer;\n }\n\n /**\n * Đọc tất cả nội dụng của file\n */\n read(): ArrayBuffer {\n if (!this.content) {\n throw new Error('File content is not set')\n }\n return this.decodeBase64(this.content)\n }\n\n /**\n * Ghi nội dung vào file\n */\n write(buffer: ArrayBuffer) {\n this.content = this.encodeBase64(buffer)\n }\n}\n\n","import { Readable, type Writable } from 'stream'\nimport { pipeline } from 'stream/promises'\n\n/**\n * FILE DẠNG STREAM — cho file mà cả nội dung KHÔNG được phép nằm trong RAM.\n *\n * Khác `File` (buffered, giữ base64 trong bộ nhớ, trần vài chục MB), `FileStream` chỉ giữ\n * metadata + một hàm mở nguồn. Byte chỉ chảy khi có người đọc, nên RAM là hằng số bất kể\n * file 8MB hay 8GB.\n *\n * Dùng được ở CẢ HAI vị trí trong definition:\n * - param → chiều lên: multipart upload, gateway/downstream đưa `Readable` vào đây.\n * - returnType → chiều xuống: handler trả về, runtime pipe thẳng ra browser.\n *\n * Khai kiểu nào là chọn hợp đồng đó. Transport KHÔNG được đánh tráo `File` thành\n * `FileStream` hay ngược lại — một param có shape phụ thuộc transport là thứ tuyệt đối\n * không được xảy ra.\n */\n\n/** Brand để nhận diện qua ranh giới package — xem `FileStream.is`. */\nconst FILE_STREAM_BRAND = Symbol.for('@fidt/saul.FileStream')\n\nexport interface FileStreamMetadata {\n name: string\n /** Kích thước KHAI BÁO. `0` = không biết trước (dữ liệu sinh on-the-fly). */\n size: number\n type: string\n}\n\nexport class FileStream {\n /** @internal Đánh dấu instance để `is()` nhận ra qua nhiều bản copy của package. */\n readonly [FILE_STREAM_BRAND] = true as const\n\n readonly metadata: FileStreamMetadata\n\n private readonly source: () => Readable\n private consumed = false\n\n constructor(metadata: FileStreamMetadata, source: () => Readable) {\n this.metadata = metadata\n this.source = source\n }\n\n /**\n * Mở nguồn và trả về `Readable`.\n *\n * Gọi lần thứ hai sẽ NÉM. Một stream không phải là value: đọc hai lần thì lần thứ hai\n * hoặc rỗng, hoặc mở lại nguồn và trả về dữ liệu đã cũ — cả hai đều là bug im lặng.\n * Cần đọc nhiều lần thì `buffer()` một lần rồi tự giữ.\n */\n stream(): Readable {\n if (this.consumed) {\n throw new Error(\n `FILE_STREAM_CONSUMED: '${this.metadata.name}' đã được đọc rồi — một FileStream chỉ tiêu thụ được một lần`\n )\n }\n this.consumed = true\n return this.source()\n }\n\n /** Đã có ai đọc chưa — cho runtime biết có phải drain trước khi trả response không. */\n get isConsumed(): boolean {\n return this.consumed\n }\n\n /** Đẩy toàn bộ nội dung vào một `Writable`, chờ tới khi xong. */\n async pipeTo(destination: Writable): Promise<void> {\n await pipeline(this.stream(), destination)\n }\n\n /**\n * Đọc hết vào một `Buffer`.\n *\n * `maxBytes` là BẮT BUỘC, không có mặc định: đây là escape hatch duy nhất ra khỏi mô hình\n * streaming, và người dùng nó phải tự nói ra trần mình chấp nhận. Một `buffer()` không\n * trần đặt giữa đường xử lý file 3GB sẽ giết process — mà chỉ giết trong production, với\n * file lớn, chứ không bao giờ trong test.\n */\n async buffer(maxBytes: number): Promise<Buffer> {\n const chunks: Buffer[] = []\n let total = 0\n\n for await (const chunk of this.stream()) {\n const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array)\n total += buf.length\n if (total > maxBytes) {\n throw new Error(\n `FILE_STREAM_TOO_LARGE: '${this.metadata.name}' vượt ${maxBytes} byte khi buffer()`\n )\n }\n chunks.push(buf)\n }\n\n return Buffer.concat(chunks, total)\n }\n\n /**\n * Có phải `FileStream` không.\n *\n * Dùng brand chứ KHÔNG dùng `instanceof`: gateway và downstream có thể resolve hai bản\n * copy khác nhau của `@fidt/saul-server` (dual-package hazard), lúc đó `instanceof` trả\n * `false` cho một object hoàn toàn hợp lệ — và nó hỏng theo kiểu chỉ lộ ra khi deploy\n * thật, không bao giờ trong monorepo.\n */\n static is(value: unknown): value is FileStream {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[FILE_STREAM_BRAND] === true\n )\n }\n\n /** Dựng từ một `Buffer` sẵn có — tiện cho test và cho handler trả dữ liệu nhỏ. */\n static fromBuffer(metadata: Omit<FileStreamMetadata, 'size'> & { size?: number }, data: Buffer): FileStream {\n return new FileStream(\n { ...metadata, size: metadata.size ?? data.length },\n () => Readable.from(data)\n )\n }\n}\n","/**\n * PREDICATE — điều kiện lọc mà client khai khi mở một subscription.\n *\n * File này CHỈ có kiểu, không có runtime: việc đánh giá predicate xảy ra ở GATEWAY (nó giữ\n * socket và biết ai đang nghe gì), không ở downstream. Downstream chỉ cần đọc và thu hẹp\n * predicate trong validator `sub<Model>` rồi trả về.\n *\n * Cú pháp `$`-prefix, cố ý KHÁC filter authz kiểu Directus (`_eq`, `_or`) ở\n * `Permission.filter`. Hai thứ trả lời hai câu hỏi khác nhau — filter authz nói \"được thấy\n * BẢN GHI nào\" và được forward xuống query storage; predicate nói \"muốn nhận EVENT nào\" và\n * được Saul tự đánh giá in-memory. Trộn cú pháp sẽ khiến người đọc tưởng chúng thay nhau được.\n */\n\n/** So sánh trên một field. Nhiều toán tử trong cùng object thì AND với nhau. */\nexport interface PredicateComparison<V> {\n $eq?: V;\n $ne?: V;\n $gt?: V;\n $gte?: V;\n $lt?: V;\n $lte?: V;\n $in?: V[];\n $nin?: V[];\n /** Field có mặt trong payload hay không — nói về SỰ CÓ MẶT, không phải giá trị. */\n $exists?: boolean;\n}\n\n/**\n * Predicate trên một Model.\n *\n * Giá trị trần là shorthand của `$eq`: `{ chatId: 12 }` ≡ `{ chatId: { $eq: 12 } }`.\n * Các key cùng cấp AND với nhau, nên `{}` khớp TẤT CẢ — đó là lý do validator `sub<Model>`\n * mặc định TỪ CHỐI, và là lý do nên thu hẹp predicate thay vì chỉ kiểm rồi trả nguyên.\n *\n * KHÔNG có `$regex`: nó là đường ReDoS do client tự khai, chạy ở đường nóng của mọi lần push.\n */\nexport type Predicate<T> = {\n [K in keyof T]?: T[K] | PredicateComparison<T[K]>;\n} & {\n $and?: Predicate<T>[];\n $or?: Predicate<T>[];\n $not?: Predicate<T>;\n};\n\n/**\n * Một subscription đang mở, nhìn từ phía server.\n *\n * `id` do gateway sinh TRƯỚC khi gọi validator và truyền xuống qua `ctx.subId`, nên\n * validator lưu lại được để về sau `sub<Model>.unsub(id)` thu hồi (vd user bị mời khỏi\n * phòng chat trong lúc đang nghe).\n */\nexport interface Sub<T> {\n id: string;\n predicate: Predicate<T>;\n}\n\n/**\n * Gộp nhiều predicate bằng AND — cách chuẩn để validator THU HẸP thứ client xin.\n *\n * ```ts\n * return narrow(predicate, { userId: { $eq: ctx.principal.sub } })\n * ```\n *\n * Dùng `$and` thay vì trải phẳng bằng spread: spread sẽ để điều kiện sau GHI ĐÈ điều kiện\n * trước trên cùng một field, tức là client khai `{ userId: { $eq: 'ai-đó' } }` rồi ràng\n * buộc bảo mật ghi đè lên — đúng ý. Nhưng ở chiều ngược lại (`{...cứng, ...client}`) thì\n * client ghi đè ràng buộc bảo mật. `$and` không có chiều nào để nhầm: mọi nhánh đều phải thoả.\n */\nexport const narrow = <T>(...predicates: (Predicate<T> | null | undefined)[]): Predicate<T> => {\n const present = predicates.filter((p): p is Predicate<T> => !!p && Object.keys(p).length > 0);\n if (present.length === 0) return {} as Predicate<T>;\n if (present.length === 1) return present[0];\n return { $and: present } as Predicate<T>;\n};\n","/**\n * LỖI CỦA CHUNK STORE, có mã máy-đọc-được.\n *\n * Cùng kiểu với `BinaryStreamError` của đường WebSocket: mỗi tình huống một `code`, và mã\n * đó — chứ không phải chuỗi message — là thứ tầng trên map sang HTTP status. Một bảng map\n * duy nhất, không handler nào tự viết status.\n */\n\nexport type UploadErrorCode =\n | 'UPLOAD_NOT_FOUND'\n | 'UPLOAD_EXISTS'\n | 'UPLOAD_EXPIRED'\n | 'UPLOAD_COMPLETED'\n /** Đang complete dở — một lời gọi complete khác đã chiếm chỗ. */\n | 'UPLOAD_BUSY'\n | 'PART_OUT_OF_RANGE'\n | 'PART_IN_FLIGHT'\n | 'PART_TOO_LARGE'\n | 'PART_MISSING'\n | 'SIZE_MISMATCH'\n | 'CHECKSUM_MISMATCH'\n | 'DISK_BUDGET'\n | 'FORBIDDEN'\n\nexport class UploadError extends Error {\n constructor(\n public readonly code: UploadErrorCode,\n message: string\n ) {\n super(message)\n this.name = 'UploadError'\n }\n\n static is(value: unknown): value is UploadError {\n return value instanceof UploadError\n }\n}\n\n/**\n * MÃ LỖI → HTTP STATUS. Bảng DUY NHẤT, và nó phải phủ hết `UploadErrorCode`.\n *\n * `Record<UploadErrorCode, number>` cố ý không optional: thêm một mã mới mà quên map thì\n * TypeScript báo ngay, thay vì lặng lẽ trả 500 cho một lỗi vốn có nghĩa rõ ràng.\n */\nexport const UPLOAD_ERROR_STATUS: Record<UploadErrorCode, number> = {\n UPLOAD_NOT_FOUND: 404,\n UPLOAD_EXISTS: 409,\n // 410 chứ không 404: upload TỪNG tồn tại, client nên dừng retry và làm lại từ init.\n UPLOAD_EXPIRED: 410,\n UPLOAD_COMPLETED: 409,\n UPLOAD_BUSY: 409,\n PART_OUT_OF_RANGE: 416,\n PART_IN_FLIGHT: 409,\n PART_TOO_LARGE: 413,\n PART_MISSING: 422,\n SIZE_MISMATCH: 422,\n CHECKSUM_MISMATCH: 422,\n // 503 = lỗi TẠM THỜI: client nên backoff rồi thử lại, không phải báo lỗi cho người dùng.\n DISK_BUDGET: 503,\n FORBIDDEN: 403,\n}\n\nexport const uploadErrorStatus = (code: UploadErrorCode): number => UPLOAD_ERROR_STATUS[code] ?? 500\n","import { createHash } from 'crypto'\nimport { createReadStream, createWriteStream } from 'fs'\nimport { mkdir, open, readdir, rename, rm, stat, unlink } from 'fs/promises'\nimport { join } from 'path'\nimport { Readable, Transform } from 'stream'\nimport { pipeline } from 'stream/promises'\n\nimport { UploadError } from './UploadError.ts'\n\n/**\n * LƯU PART RA ĐĨA. Chỉ đĩa — không biết gì về Redis, HTTP hay vòng đời upload.\n *\n * Mỗi upload là một thư mục, mỗi part là một file. Không có gì thông minh ở đây, và đó là\n * chủ ý: thứ duy nhất cần đúng tuyệt đối là **không bao giờ giữ cả part trong RAM** và\n * **không bao giờ để lại một `.part` nửa vời mà tầng trên tưởng là hoàn chỉnh**.\n */\n\n/** Kích thước đọc khi ghép part. Đủ lớn để không syscall liên tục, đủ nhỏ để RAM phẳng. */\nconst READ_HIGH_WATER_MARK = 1024 * 1024\n\nexport interface WritePartResult {\n size: number\n /** sha256 hex của part — client dùng để retry có kiểm chứng. */\n etag: string\n}\n\nexport interface PartInfo {\n partNumber: number\n size: number\n}\n\nexport interface PartDiskStoreOptions {\n /** Thư mục gốc. Mỗi upload một thư mục con. */\n rootDir: string\n /** fsync mỗi part trước khi rename. Tắt = đổi durability-khi-crash lấy throughput. */\n fsync?: boolean\n}\n\nexport class PartDiskStore {\n private readonly rootDir: string\n private readonly fsync: boolean\n\n constructor(options: PartDiskStoreOptions) {\n this.rootDir = options.rootDir\n this.fsync = options.fsync ?? true\n }\n\n /**\n * Thư mục của một upload.\n *\n * `assertSafeUploadId` chạy TRƯỚC `join` — đây là bề mặt path-traversal duy nhất của\n * cả tính năng, và `join('/tmp', '../../etc')` là một đường dẫn hoàn toàn hợp lệ.\n */\n dirFor(uploadId: string): string {\n assertSafeUploadId(uploadId)\n return join(this.rootDir, uploadId)\n }\n\n private pathFor(uploadId: string, partNumber: number, partCount: number): string {\n assertSafePartNumber(partNumber, partCount)\n // Đệm 0 để thứ tự tên file trùng thứ tự part — tiện khi soi bằng `ls` lúc debug.\n return join(this.dirFor(uploadId), `${String(partNumber).padStart(6, '0')}.part`)\n }\n\n async ensureDir(uploadId: string): Promise<void> {\n await mkdir(this.dirFor(uploadId), { recursive: true })\n }\n\n /**\n * Ghi một part. Trả về số byte thật và sha256.\n *\n * Bốn bước, thứ tự bắt buộc:\n * 1. `open(tmp, 'wx')` — hai PUT cùng part cùng lúc thì một cái thua ngay, thay vì\n * hai writer xen kẽ vào cùng một file.\n * 2. pipeline có đếm byte + hash — vượt trần là abort GIỮA CHỪNG, không đọc hết rồi\n * mới than.\n * 3. fsync (nếu bật) rồi `rename` — rename trong cùng filesystem là atomic, nên\n * file `.part` xuất hiện là đã hoàn chỉnh, không có trạng thái nửa vời.\n * 4. Chỉ khi rename xong mới coi như part tồn tại.\n */\n async writePart(\n uploadId: string,\n partNumber: number,\n body: Readable,\n opts: { maxBytes: number; partCount: number; declaredLength?: number; expectedSha256?: string }\n ): Promise<WritePartResult> {\n const finalPath = this.pathFor(uploadId, partNumber, opts.partCount)\n const tmpPath = `${finalPath}.tmp`\n\n await this.ensureDir(uploadId)\n\n let handle\n try {\n handle = await open(tmpPath, 'wx')\n } catch (err: any) {\n if (err?.code === 'EEXIST') {\n throw new UploadError(\n 'PART_IN_FLIGHT',\n `Part ${partNumber} của '${uploadId}' đang được ghi bởi một request khác`\n )\n }\n throw err\n }\n\n const hash = createHash('sha256')\n let received = 0\n const limit = opts.declaredLength !== undefined\n ? Math.min(opts.maxBytes, opts.declaredLength)\n : opts.maxBytes\n\n const meter = new Transform({\n transform(chunk, _enc, cb) {\n received += chunk.length\n if (received > limit) {\n // Huỷ NGAY, không đợi hết body: mục đích của trần là không nhận byte\n // thừa, chứ không phải nhận xong rồi mới từ chối.\n cb(new UploadError('PART_TOO_LARGE', `Part ${partNumber} vượt ${limit} byte`))\n return\n }\n hash.update(chunk)\n cb(null, chunk)\n },\n })\n\n try {\n await pipeline(body, meter, handle.createWriteStream())\n if (this.fsync) await handle.sync()\n } catch (err) {\n await handle.close().catch(() => undefined)\n await unlink(tmpPath).catch(() => undefined)\n throw err\n }\n await handle.close()\n\n if (opts.declaredLength !== undefined && received !== opts.declaredLength) {\n await unlink(tmpPath).catch(() => undefined)\n throw new UploadError(\n 'SIZE_MISMATCH',\n `Part ${partNumber}: nhận ${received} byte, content-length khai ${opts.declaredLength}`\n )\n }\n\n const etag = hash.digest('hex')\n if (opts.expectedSha256 && opts.expectedSha256 !== etag) {\n await unlink(tmpPath).catch(() => undefined)\n throw new UploadError(\n 'CHECKSUM_MISMATCH',\n `Part ${partNumber}: sha256 không khớp (khai ${opts.expectedSha256}, thật ${etag})`\n )\n }\n\n // Ghi đè part cũ là HỢP LỆ và cần thiết: đó chính là thứ làm retry từng part an toàn.\n await rename(tmpPath, finalPath)\n\n return { size: received, etag }\n }\n\n /** Mở một part để đọc. Dùng bởi `concatParts`. */\n createPartStream(uploadId: string, partNumber: number, partCount: number): Readable {\n return createReadStream(this.pathFor(uploadId, partNumber, partCount), {\n highWaterMark: READ_HIGH_WATER_MARK,\n })\n }\n\n /** Các part ĐANG CÓ THẬT trên đĩa (không tính `.tmp`). */\n async listParts(uploadId: string): Promise<PartInfo[]> {\n let entries: string[]\n try {\n entries = await readdir(this.dirFor(uploadId))\n } catch (err: any) {\n if (err?.code === 'ENOENT') return []\n throw err\n }\n\n const parts: PartInfo[] = []\n for (const entry of entries) {\n const match = /^(\\d{6})\\.part$/.exec(entry)\n if (!match) continue\n const info = await stat(join(this.dirFor(uploadId), entry))\n parts.push({ partNumber: Number.parseInt(match[1], 10), size: info.size })\n }\n return parts.sort((a, b) => a.partNumber - b.partNumber)\n }\n\n /** Xoá cả thư mục upload. Trả về số byte giải phóng. Idempotent. */\n async removeUpload(uploadId: string): Promise<number> {\n const dir = this.dirFor(uploadId)\n let freed = 0\n try {\n for (const entry of await readdir(dir)) {\n const info = await stat(join(dir, entry)).catch(() => undefined)\n if (info) freed += info.size\n }\n } catch (err: any) {\n if (err?.code === 'ENOENT') return 0\n throw err\n }\n await rm(dir, { recursive: true, force: true })\n return freed\n }\n\n /** Thư mục cũ hơn `olderThanMs` — ứng viên dọn rác. Xem `UploadSweeper`. */\n async listStaleDirs(olderThanMs: number, now: number = Date.now()): Promise<string[]> {\n let entries: string[]\n try {\n entries = await readdir(this.rootDir)\n } catch (err: any) {\n if (err?.code === 'ENOENT') return []\n throw err\n }\n\n const stale: string[] = []\n for (const entry of entries) {\n const info = await stat(join(this.rootDir, entry)).catch(() => undefined)\n if (!info?.isDirectory()) continue\n if (now - info.mtimeMs > olderThanMs) stale.push(entry)\n }\n return stale\n }\n\n /** Tổng byte đang chiếm — để đối chiếu lại counter, vốn trôi sau mỗi lần crash. */\n async totalBytes(): Promise<number> {\n let entries: string[]\n try {\n entries = await readdir(this.rootDir)\n } catch (err: any) {\n if (err?.code === 'ENOENT') return 0\n throw err\n }\n\n let total = 0\n for (const entry of entries) {\n const dir = join(this.rootDir, entry)\n const info = await stat(dir).catch(() => undefined)\n if (!info?.isDirectory()) continue\n for (const file of await readdir(dir).catch(() => [] as string[])) {\n const fileInfo = await stat(join(dir, file)).catch(() => undefined)\n if (fileInfo?.isFile()) total += fileInfo.size\n }\n }\n return total\n }\n}\n\n/**\n * `uploadId` phải là UUID. Kiểm TRƯỚC mọi thao tác đường dẫn.\n *\n * Không dùng blacklist (`..`, `/`): danh sách cấm luôn thiếu một dạng nào đó\n * (`%2e%2e`, null byte, tên dài quá giới hạn OS). Whitelist một hình dạng đã biết thì không.\n */\nexport function assertSafeUploadId(uploadId: string): void {\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(uploadId)) {\n throw new UploadError('FORBIDDEN', `uploadId không hợp lệ: '${uploadId}'`)\n }\n}\n\n/** `partNumber` phải là số nguyên trong `1..partCount`. */\nexport function assertSafePartNumber(partNumber: number, partCount: number): void {\n if (!Number.isSafeInteger(partNumber) || partNumber < 1 || partNumber > partCount) {\n throw new UploadError(\n 'PART_OUT_OF_RANGE',\n `partNumber ${partNumber} ngoài dải 1..${partCount}`\n )\n }\n}\n","import { readFile, writeFile, rename } from 'fs/promises'\nimport { join } from 'path'\n\nimport { UploadError } from './UploadError.ts'\nimport type { PartDiskStore } from './PartDiskStore.ts'\n\n/**\n * MANIFEST của các upload đang mở, giữ trong RAM và ghi kèm ra `meta.json`.\n *\n * Vì sao KHÔNG dùng Redis ở đây: một upload sống trọn đời trên MỘT process — gateway đã ghim\n * `target` từ lúc init, nên mọi part của nó về cùng một nơi. Thêm Redis vào downstream chỉ để\n * lưu thứ không ai khác đọc là bắt mọi service dùng Saul phải nuôi thêm một dependency.\n *\n * `meta.json` tồn tại vì hai lý do, đều là chuyện lúc hỏng chứ không phải lúc chạy: sống sót\n * qua restart, và cho sweeper phân biệt \"thư mục của một upload đang chạy\" với \"rác của một\n * process đã chết\".\n */\n\nconst META_FILE = 'meta.json'\n\nexport type UploadState = 'open' | 'completing' | 'completed'\n\nexport interface UploadRecord {\n uploadId: string\n module: string\n function: string\n /** Tên param kiểu FileStream của function đích. */\n param: string\n /** Các param KHÔNG phải file của cùng lời gọi. */\n params: Record<string, unknown>\n file: { name: string; size: number; type: string }\n chunkSize: number\n partCount: number\n /** Deadline TUYỆT ĐỐI do gateway quyết, không bao giờ gia hạn. */\n hardExpiresAt: number\n /** Deadline TRƯỢT, gia hạn mỗi lần nhận được part. */\n expiresAt: number\n idleTimeoutMs: number\n state: UploadState\n createdAt: number\n}\n\nexport interface UploadRegistryOptions {\n store: PartDiskStore\n /** Ngân sách đĩa của MÁY NÀY. Vượt thì từ chối init. */\n maxDiskBytes: number\n}\n\nexport class UploadRegistry {\n private readonly records = new Map<string, UploadRecord>()\n private readonly store: PartDiskStore\n private readonly maxDiskBytes: number\n\n /**\n * Byte đang chiếm, đếm tăng dần. Đây là con số ƯỚC LƯỢNG — nó trôi sau mỗi lần crash,\n * nên `UploadSweeper` định kỳ tính lại bằng `du` thật. Counter trôi mà không ai sửa thì\n * ngân sách âm thầm khoá mọi upload, và không có gì trong log nói tại sao.\n */\n private usedBytes = 0\n\n constructor(options: UploadRegistryOptions) {\n this.store = options.store\n this.maxDiskBytes = options.maxDiskBytes\n }\n\n get diskUsedBytes(): number {\n return this.usedBytes\n }\n\n /** Đặt lại bộ đếm sau khi đối chiếu với đĩa thật. Xem `UploadSweeper`. */\n setDiskUsedBytes(bytes: number): void {\n this.usedBytes = Math.max(0, bytes)\n }\n\n async init(record: Omit<UploadRecord, 'state' | 'createdAt' | 'expiresAt'>): Promise<UploadRecord> {\n if (this.records.has(record.uploadId)) {\n throw new UploadError('UPLOAD_EXISTS', `Upload '${record.uploadId}' đã tồn tại`)\n }\n // Từ chối SỚM cái chắc chắn không vừa. Kế toán thật diễn ra theo từng part, nhưng\n // biết trước là không đủ chỗ thì không có lý do gì để nhận byte đầu tiên.\n if (this.usedBytes + record.file.size > this.maxDiskBytes) {\n throw new UploadError(\n 'DISK_BUDGET',\n `Hết ngân sách đĩa (${this.usedBytes}/${this.maxDiskBytes} byte) — thử lại sau`\n )\n }\n\n const now = Date.now()\n const full: UploadRecord = {\n ...record,\n state: 'open',\n createdAt: now,\n expiresAt: Math.min(now + record.idleTimeoutMs, record.hardExpiresAt),\n }\n\n this.records.set(full.uploadId, full)\n await this.store.ensureDir(full.uploadId)\n await this.persist(full)\n return full\n }\n\n /**\n * Record của một upload đang MỞ được.\n *\n * Hết hạn thì ném `UPLOAD_EXPIRED` chứ không `UPLOAD_NOT_FOUND`: hai thứ đó nói với\n * client hai điều khác nhau — \"id sai, đừng thử nữa\" và \"chậm quá, làm lại từ init\".\n */\n get(uploadId: string, allowed: UploadState[] = ['open']): UploadRecord {\n const record = this.records.get(uploadId)\n if (!record) {\n throw new UploadError('UPLOAD_NOT_FOUND', `Upload '${uploadId}' không tồn tại`)\n }\n if (Date.now() > record.hardExpiresAt || Date.now() > record.expiresAt) {\n throw new UploadError('UPLOAD_EXPIRED', `Upload '${uploadId}' đã hết hạn`)\n }\n if (!allowed.includes(record.state)) {\n throw new UploadError(\n record.state === 'completed' ? 'UPLOAD_COMPLETED' : 'UPLOAD_BUSY',\n `Upload '${uploadId}' đang ở trạng thái '${record.state}'`\n )\n }\n return record\n }\n\n /** Có record nào không, bất kể trạng thái — cho abort idempotent và cho sweeper. */\n peek(uploadId: string): UploadRecord | undefined {\n return this.records.get(uploadId)\n }\n\n list(): UploadRecord[] {\n return [...this.records.values()]\n }\n\n /**\n * Gia hạn deadline sau khi nhận được một part, và cộng chênh lệch byte vào bộ đếm.\n *\n * `delta` chứ không phải `size`: PUT lại một part là GHI ĐÈ, nên phần tăng thêm chỉ là\n * hiệu số. Cộng nguyên `size` mỗi lần retry sẽ thổi phồng bộ đếm cho tới khi ngân sách\n * khoá cả những upload hoàn toàn bình thường.\n */\n touch(uploadId: string, deltaBytes: number): void {\n const record = this.records.get(uploadId)\n if (!record) return\n record.expiresAt = Math.min(Date.now() + record.idleTimeoutMs, record.hardExpiresAt)\n this.usedBytes = Math.max(0, this.usedBytes + deltaBytes)\n }\n\n /**\n * Chuyển sang `completing`, một lần duy nhất.\n *\n * Đây là chốt chống complete đồng thời: hai request cùng lúc thì cái thứ hai thấy state\n * đã đổi và nhận `UPLOAD_BUSY`, thay vì cả hai cùng ghép file và cùng gọi handler.\n */\n beginComplete(uploadId: string): UploadRecord {\n const record = this.get(uploadId, ['open'])\n record.state = 'completing'\n return record\n }\n\n /** Quay lại `open` khi complete thất bại — cho client retry mà không mất part đã gửi. */\n rollbackComplete(uploadId: string): void {\n const record = this.records.get(uploadId)\n if (record?.state === 'completing') record.state = 'open'\n }\n\n /** Xoá record + part trên đĩa. Idempotent. */\n async remove(uploadId: string): Promise<void> {\n this.records.delete(uploadId)\n const freed = await this.store.removeUpload(uploadId)\n this.usedBytes = Math.max(0, this.usedBytes - freed)\n }\n\n /**\n * Ghi `meta.json` cạnh part.\n *\n * Ghi tmp rồi rename để không bao giờ có một `meta.json` cụt: sweeper đọc phải file cụt\n * sẽ coi thư mục là rác và xoá một upload đang chạy dở.\n */\n private async persist(record: UploadRecord): Promise<void> {\n const dir = this.store.dirFor(record.uploadId)\n const tmp = join(dir, `${META_FILE}.tmp`)\n await writeFile(tmp, JSON.stringify(record), 'utf-8')\n await rename(tmp, join(dir, META_FILE))\n }\n\n /** Đọc `meta.json` của một thư mục — cho sweeper phân biệt upload sống với rác. */\n async readPersisted(uploadId: string): Promise<UploadRecord | undefined> {\n try {\n const raw = await readFile(join(this.store.dirFor(uploadId), META_FILE), 'utf-8')\n return JSON.parse(raw) as UploadRecord\n } catch {\n return undefined\n }\n }\n}\n","import type { PartDiskStore } from './PartDiskStore.ts'\nimport type { UploadRegistry } from './UploadRegistry.ts'\n\n/**\n * DỌN RÁC ĐỊNH KỲ cho chunk store.\n *\n * Ba việc, và việc thứ hai/thứ ba mới là lý do tồn tại của nó: một `setTimeout` per-upload\n * (kiểu `BinaryStreamBuffer` đang dùng cho WS) chỉ dọn được thứ nó biết. Process chết giữa\n * chừng thì timer chết theo, còn file thì ở lại đĩa mãi mãi.\n *\n * 1. Upload quá hạn → xoá record + part.\n * 2. Thư mục KHÔNG có record trong RAM và đã cũ → rác của một process đã chết, xoá.\n * 3. Định kỳ đối chiếu bộ đếm byte với đĩa thật → counter trôi sau crash, mà counter trôi\n * thì ngân sách âm thầm khoá mọi upload và không có gì trong log nói tại sao.\n */\n\nexport interface UploadSweeperOptions {\n registry: UploadRegistry\n store: PartDiskStore\n intervalMs?: number\n /** Thư mục mồ côi phải cũ hơn ngần này mới xoá. Mặc định 2× idle timeout dài nhất. */\n orphanGraceMs?: number\n /** Bao nhiêu tick thì đối chiếu lại bộ đếm với đĩa (quét `du` không rẻ). */\n reconcileEvery?: number\n}\n\nexport interface SweepResult {\n expired: number\n orphans: number\n reconciledBytes?: number\n}\n\nexport class UploadSweeper {\n private readonly registry: UploadRegistry\n private readonly store: PartDiskStore\n private readonly intervalMs: number\n private readonly orphanGraceMs: number\n private readonly reconcileEvery: number\n\n private timer?: ReturnType<typeof setInterval>\n private ticks = 0\n\n constructor(options: UploadSweeperOptions) {\n this.registry = options.registry\n this.store = options.store\n this.intervalMs = options.intervalMs ?? 60_000\n this.orphanGraceMs = options.orphanGraceMs ?? 20 * 60_000\n this.reconcileEvery = options.reconcileEvery ?? 10\n }\n\n /** Bắt đầu quét. `unref()` để timer dọn dẹp không giữ process sống. */\n start(): void {\n if (this.timer) return\n this.timer = setInterval(() => {\n // Không `await`: đây là timer, không có ai để `catch`. Nuốt lỗi tại chỗ và log,\n // vì một lần quét hỏng không được phép làm chết process.\n this.sweep().catch((err) => console.error('[saul][upload] sweep lỗi:', err))\n }, this.intervalMs)\n if (typeof (this.timer as any).unref === 'function') (this.timer as any).unref()\n }\n\n stop(): void {\n if (!this.timer) return\n clearInterval(this.timer)\n this.timer = undefined\n }\n\n async sweep(now: number = Date.now()): Promise<SweepResult> {\n const result: SweepResult = { expired: 0, orphans: 0 }\n\n for (const record of this.registry.list()) {\n if (now > record.hardExpiresAt || now > record.expiresAt) {\n await this.registry.remove(record.uploadId)\n result.expired += 1\n }\n }\n\n // Thư mục cũ mà không có record trong RAM: hoặc process đã restart, hoặc record bị\n // xoá mà `rm` thất bại. Cả hai đều là rác — nhưng chỉ sau `orphanGraceMs`, vì một\n // upload vừa `ensureDir()` xong cũng chưa có part nào.\n for (const dirName of await this.store.listStaleDirs(this.orphanGraceMs, now)) {\n if (this.registry.peek(dirName)) continue\n await this.store.removeUpload(dirName).catch(() => undefined)\n result.orphans += 1\n }\n\n this.ticks += 1\n if (this.ticks % this.reconcileEvery === 0) {\n const actual = await this.store.totalBytes()\n this.registry.setDiskUsedBytes(actual)\n result.reconciledBytes = actual\n }\n\n return result\n }\n}\n","import { Readable } from 'stream'\n\nimport type { PartDiskStore } from './PartDiskStore.ts'\n\n/**\n * NỐI CÁC PART thành MỘT `Readable`, không buffer.\n *\n * `Readable.from(async generator)` cho backpressure đúng miễn phí: generator chỉ chạy tiếp\n * khi consumer kéo, nên tốc độ đọc đĩa tự khớp tốc độ ghi ra downstream. Và vì generator\n * chỉ mở part khi tới lượt, ở mọi thời điểm chỉ có **một** fd đang mở — RAM là\n * `highWaterMark`, không phụ thuộc file 8MB hay 8GB.\n *\n * `finally { rs.destroy() }` không phải cho đường thành công (stream tự đóng khi hết) mà cho\n * đường HUỶ: consumer `destroy()` giữa chừng sẽ ném vào generator, và không có `finally` thì\n * part đang đọc dở giữ fd tới tận lúc GC.\n */\nexport function concatParts(\n store: PartDiskStore,\n uploadId: string,\n partNumbers: number[],\n partCount: number\n): Readable {\n async function* generate(): AsyncGenerator<Buffer> {\n for (const partNumber of partNumbers) {\n const source = store.createPartStream(uploadId, partNumber, partCount)\n try {\n for await (const chunk of source) {\n yield chunk as Buffer\n }\n } finally {\n source.destroy()\n }\n }\n }\n\n return Readable.from(generate())\n}\n","import { Hono } from 'hono'\nimport { Readable } from 'stream'\nimport { tmpdir } from 'os'\nimport { join } from 'path'\n\nimport { FileStream } from '../models/fileStream.ts'\nimport { concatParts } from './concatParts.ts'\nimport { PartDiskStore } from './PartDiskStore.ts'\nimport { UploadError, uploadErrorStatus } from './UploadError.ts'\nimport { UploadRegistry, type UploadRecord } from './UploadRegistry.ts'\nimport { UploadSweeper } from './UploadSweeper.ts'\n\n/**\n * SUB-ROUTER `/saul/upload/*` — phía DOWNSTREAM của multipart upload.\n *\n * Gateway không lưu byte nào; nó bơm thẳng từng part sang đây. Nên đây mới là nơi part nằm\n * trên đĩa và được ghép lại lúc complete.\n *\n * Router này KHÔNG tự gác quyền. Nó tin gateway, đúng như `POST /saul` đang tin — quyền đã\n * được chốt ở `/upload/init` của gateway, trước byte đầu tiên. Đặt thêm một tầng authz ở đây\n * mà không có token của user thì cũng chỉ là kiểm lại chính mình.\n *\n * Chỉ ba biến env, và cả ba đều nói về TÀI NGUYÊN CỦA MÁY NÀY. Mọi thứ thuộc về chính sách\n * (deadline, chunk size, partCount) do gateway truyền xuống trong lời gọi init.\n */\n\nexport interface UploadRouterOptions {\n /** Gọi hàm đích sau khi đã ghép file. Runtime truyền `dispatch` của nó vào đây. */\n dispatch: (body: { module: string; function: string; params: Record<string, unknown> }) => Promise<unknown>\n rootDir?: string\n maxDiskBytes?: number\n fsync?: boolean\n /** Trần cứng một part. Gateway cũng gác, nhưng ở đây mới đếm byte THẬT. */\n maxPartBytes?: number\n sweepIntervalMs?: number\n}\n\nconst envInt = (key: string, fallback: number): number => {\n const raw = process.env[key]\n const n = raw ? Number.parseInt(raw, 10) : NaN\n return Number.isFinite(n) && n > 0 ? n : fallback\n}\nconst envBool = (key: string, fallback: boolean): boolean => {\n const raw = process.env[key]\n if (raw === undefined) return fallback\n return raw === 'true' || raw === '1'\n}\n\nexport interface UploadSubsystem {\n router: Hono\n registry: UploadRegistry\n store: PartDiskStore\n sweeper: UploadSweeper\n}\n\nexport function createUploadSubsystem(options: UploadRouterOptions): UploadSubsystem {\n const store = new PartDiskStore({\n rootDir: options.rootDir ?? process.env.SAUL_UPLOAD_TMP_DIR ?? join(tmpdir(), 'saul-uploads'),\n fsync: options.fsync ?? envBool('SAUL_UPLOAD_FSYNC', true),\n })\n const registry = new UploadRegistry({\n store,\n maxDiskBytes:\n options.maxDiskBytes ?? envInt('SAUL_UPLOAD_MAX_DISK_BYTES', 20 * 1024 * 1024 * 1024),\n })\n const sweeper = new UploadSweeper({ registry, store, intervalMs: options.sweepIntervalMs })\n const maxPartBytes = options.maxPartBytes ?? envInt('SAUL_UPLOAD_MAX_PART_BYTES', 32 * 1024 * 1024)\n\n const router = new Hono()\n\n router.onError((err, c) => {\n if (UploadError.is(err)) {\n return c.json({ error: err.message, code: err.code }, uploadErrorStatus(err.code) as any)\n }\n console.error('[saul][upload] lỗi không xác định:', err)\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500)\n })\n\n /**\n * Gateway sinh `uploadId` và áp đặt xuống — downstream không tự đặt id, nhờ vậy không\n * phải map hai id ở hai bên. Deadline cũng do gateway quyết: chính sách nằm ở\n * `remote_config` của Saul, downstream chỉ thi hành.\n */\n router.post('/init', async (c) => {\n const body = (await c.req.json()) as Omit<UploadRecord, 'state' | 'createdAt' | 'expiresAt'>\n const record = await registry.init(body)\n return c.json({ uploadId: record.uploadId, expiresAt: record.expiresAt })\n })\n\n router.put('/:uploadId/part/:partNumber', async (c) => {\n const uploadId = c.req.param('uploadId')\n const partNumber = Number(c.req.param('partNumber'))\n const record = registry.get(uploadId)\n\n const contentLength = c.req.header('content-length')\n if (!contentLength) {\n throw new UploadError('SIZE_MISMATCH', 'Thiếu content-length — part phải khai độ dài')\n }\n\n const before = (await store.listParts(uploadId)).find((p) => p.partNumber === partNumber)\n\n // `c.req.raw.body` là stream của socket. KHÔNG đọc nó thành buffer ở bất kỳ đâu —\n // một lần chạm là cả part vào RAM.\n const source = c.req.raw.body\n if (!source) {\n throw new UploadError('SIZE_MISMATCH', 'Request không có body')\n }\n\n const result = await store.writePart(uploadId, partNumber, Readable.fromWeb(source as any), {\n maxBytes: maxPartBytes,\n partCount: record.partCount,\n declaredLength: Number(contentLength),\n expectedSha256: c.req.header('x-saul-part-sha256'),\n })\n\n // Hiệu số, không phải `result.size`: PUT lại một part là ghi đè, cộng nguyên size\n // mỗi lần retry sẽ thổi phồng bộ đếm cho tới khi ngân sách khoá cả upload lành lặn.\n registry.touch(uploadId, result.size - (before?.size ?? 0))\n\n return c.json({ partNumber, size: result.size, etag: result.etag })\n })\n\n router.get('/:uploadId', async (c) => {\n const uploadId = c.req.param('uploadId')\n const record = registry.get(uploadId, ['open', 'completing'])\n const parts = await store.listParts(uploadId)\n\n return c.json({\n uploadId,\n size: record.file.size,\n chunkSize: record.chunkSize,\n partCount: record.partCount,\n received: parts,\n expiresAt: record.expiresAt,\n })\n })\n\n /**\n * Ghép part → dựng `FileStream` → gọi handler → trả về ĐÚNG giá trị handler trả.\n *\n * `ctx` tới ở ĐÂY chứ không phải ở init: nó phải phản ánh trạng thái quyền tại thời điểm\n * chạy handler, không phải lúc bắt đầu một upload có thể kéo dài 40 phút.\n */\n router.post('/:uploadId/complete', async (c) => {\n const uploadId = c.req.param('uploadId')\n const { ctx } = (await c.req.json().catch(() => ({}))) as { ctx?: unknown }\n const record = registry.beginComplete(uploadId)\n\n try {\n const parts = await store.listParts(uploadId)\n const received = new Set(parts.map((p) => p.partNumber))\n const missing: number[] = []\n for (let n = 1; n <= record.partCount; n += 1) {\n if (!received.has(n)) missing.push(n)\n }\n if (missing.length > 0) {\n throw new UploadError(\n 'PART_MISSING',\n `Thiếu part: ${missing.slice(0, 10).join(', ')}${missing.length > 10 ? '…' : ''}`\n )\n }\n\n const total = parts.reduce((sum, p) => sum + p.size, 0)\n // Fail-closed, cùng lý lẽ với `BinaryStreamBuffer.end()`: dispatch một file cụt\n // nghĩa là handler nhận dữ liệu sai mà không có gì báo.\n if (record.file.size > 0 && total !== record.file.size) {\n throw new UploadError(\n 'SIZE_MISMATCH',\n `Nhận ${total} byte, khai báo ${record.file.size}`\n )\n }\n\n const ordered = parts.map((p) => p.partNumber)\n const fileStream = new FileStream(record.file, () =>\n concatParts(store, uploadId, ordered, record.partCount)\n )\n\n const result = await options.dispatch({\n module: record.module,\n function: record.function,\n params: { ...record.params, [record.param]: fileStream, ctx },\n })\n\n return c.json(result as any)\n } catch (err) {\n // Trả về `open` để client retry được mà không mất part đã gửi. Nếu để nguyên\n // `completing` thì một lỗi tạm thời của handler biến thành phải upload lại 3GB.\n registry.rollbackComplete(uploadId)\n throw err\n } finally {\n // Dọn part dù thành công hay thất bại — trừ khi còn ở `completing` (đã rollback,\n // client sẽ thử lại).\n if (registry.peek(uploadId)?.state !== 'open') {\n await registry.remove(uploadId).catch(() => undefined)\n }\n }\n })\n\n router.delete('/:uploadId', async (c) => {\n // Idempotent: xoá cái không tồn tại vẫn 204, để gọi được từ `sendBeacon` lúc đóng tab.\n await registry.remove(c.req.param('uploadId')).catch(() => undefined)\n return c.body(null, 204)\n })\n\n return { router, registry, store, sweeper }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/auth/MachineTokenProvider.ts","../src/models/saulException.ts","../src/tunnel/TunnelAgent.ts","../src/models/file.ts","../src/models/fileStream.ts","../src/models/predicate.ts","../src/upload/UploadError.ts","../src/upload/PartDiskStore.ts","../src/upload/UploadRegistry.ts","../src/upload/UploadSweeper.ts","../src/upload/concatParts.ts","../src/upload/uploadRouter.ts"],"names":["MachineTokenProvider","options","forceRefresh","clientId","clientSecret","oidcTokenUrl","envTokenUrl","missing","resource","form","response","resolveFetch","data","detail","ttlSeconds","fetchFn","SaulException","_SaulException","message","code","value","DISPATCH_EVENT","REGISTER_EVENT","HEARTBEAT_EVENT","UPLOAD_EVENT","httpScheme","url","DEFAULT_HEARTBEAT_MS","DEFAULT_INLINE_MAX_BYTES","RECONNECT_LOG_INTERVAL_MS","UPLOAD_PATH_PREFIX","routerPath","path","rest","TunnelAgent","hostname","factory","opts","connectSocket","origin","socketPath","socket","cb","token","err","firstRegistration","resolve","reject","settled","payload","ack","reason","basePath","d","params","result","serialized","resultBlob","encoded","body","text","blobId","module","init","now","every","event","File","meta","buffer","base64","uint8Array","FILE_STREAM_BRAND","FileStream","_FileStream","metadata","source","destination","pipeline","maxBytes","chunks","total","chunk","buf","Readable","narrow","predicates","present","p","UploadError","_UploadError","UPLOAD_ERROR_STATUS","uploadErrorStatus","READ_HIGH_WATER_MARK","PartDiskStore","uploadId","assertSafeUploadId","join","partNumber","partCount","assertSafePartNumber","mkdir","finalPath","tmpPath","handle","open","hash","createHash","received","limit","meter","Transform","_enc","unlink","etag","rename","createReadStream","entries","readdir","parts","entry","match","info","stat","a","b","dir","freed","rm","olderThanMs","stale","file","fileInfo","META_FILE","UploadRegistry","bytes","record","full","allowed","deltaBytes","tmp","writeFile","raw","readFile","UploadSweeper","dirName","actual","concatParts","store","partNumbers","generate","envInt","key","fallback","n","envBool","createUploadSubsystem","tmpdir","registry","sweeper","maxPartBytes","router","Hono","c","contentLength","before","ctx","sum","ordered","fileStream"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * MACHINE TOKEN PROVIDER — lấy và cache token `client_credentials` cho service downstream.\n *\n * Đây là bản dùng chung của đoạn logic vốn nằm trong template codegen `initWsPush`\n * (`__wsPushGetAccessToken`). Reverse tunnel cần đúng luồng đó để bắt tay với gateway, và\n * hai bản copy của một quy trình xin token là hai cơ hội để một bên quên mất `resource`\n * rồi nhận opaque token mà không hiểu vì sao verify JWT lại hỏng.\n *\n * BA THỨ QUYẾT ĐỊNH TÍNH ĐÚNG ĐẮN, không cái nào bỏ được:\n *\n * 1. **`resource`** — điều kiện để oidc-provider bind token vào Saul resource server và phát\n * JWT. Thiếu nó token ra opaque, và lỗi hiện ra ở tận chỗ gateway verify JWT dưới dạng\n * \"Unexpected token ... is not valid JSON\" — không có gì trỏ ngược về đây.\n * 2. **Refresh sớm 60s** — token lấy về sát hạn sẽ chết giữa lúc đang bay tới gateway, và\n * lỗi hiện ra dưới dạng 401 ngẫu nhiên không tái hiện được.\n * 3. **Gộp lời gọi đồng thời** — agent dispatch song song nhiều call, nên mỗi lần token hết\n * hạn là N request cùng lúc phát hiện điều đó. Không gộp thì identity ăn nguyên burst.\n */\n\nexport interface MachineTokenOptions {\n /**\n * Endpoint token OIDC (grant `client_credentials`). Bỏ trống thì đọc env\n * `OIDC_TOKEN_URL` tại runtime — đọc lúc GỌI chứ không lúc khởi tạo, để thứ tự nạp env\n * không biến thành lỗi cấu hình.\n */\n oidcTokenUrl?: string;\n /** Client M2M (collection `oidc_client` ở Directus). Không bake vì là secret. */\n clientId?: string;\n clientSecret?: string;\n /** Resource indicator — xem lý do ở đầu file. */\n resource?: string;\n /** Dùng khi `resource` không được khai (thường là base URL của gateway). */\n fallbackResource?: string;\n /** Scope của Saul resource server. */\n scope?: string;\n /**\n * Token TĨNH có sẵn (phải là JWT). Truyền vào thì bỏ qua hoàn toàn client_credentials.\n * Chỉ nên dùng cho demo/script — service thật nên tự xin token để còn xoay vòng được.\n */\n machineToken?: string;\n /** DI cho test: thay `fetch` thật bằng bản giả. */\n fetchImpl?: typeof fetch;\n}\n\n/** Biên an toàn: coi token là hết hạn sớm hơn thực tế ngần này. */\nconst EARLY_REFRESH_MS = 60_000;\n\n/** TTL giả định khi endpoint không trả `expires_in` dùng được. */\nconst DEFAULT_TTL_SECONDS = 300;\n\nexport class MachineTokenProvider {\n private cachedToken: string | null = null;\n private expiresAt = 0;\n /** Lời hứa đang bay — để N lời gọi đồng thời chỉ sinh đúng một round-trip. */\n private inflight: Promise<string> | null = null;\n\n constructor(private readonly options: MachineTokenOptions) {}\n\n /**\n * Token hiện hành, tự xin mới khi cần.\n *\n * @param forceRefresh Bỏ qua cache (dùng khi gateway vừa trả 401 — token có thể bị thu\n * hồi trước hạn, và thử lại với đúng token đó thì chỉ 401 thêm lần nữa).\n */\n async getToken(forceRefresh = false): Promise<string> {\n if (this.options.machineToken) return this.options.machineToken;\n\n if (!forceRefresh && this.cachedToken && Date.now() < this.expiresAt) {\n return this.cachedToken;\n }\n\n // Gộp vào lời hứa đang bay. `forceRefresh` cũng gộp: hai lời gọi cùng phát hiện token\n // hỏng thì vẫn chỉ cần MỘT token mới.\n if (this.inflight) return this.inflight;\n\n this.inflight = this.requestToken()\n .finally(() => {\n // Dọn TRONG MỌI TRƯỜNG HỢP, kể cả khi hỏng: giữ lại một lời hứa đã reject là\n // cách để một lỗi mạng thoáng qua khoá cứng provider tới lúc restart process.\n this.inflight = null;\n });\n\n return this.inflight;\n }\n\n /** Vứt cache — dùng khi đổi cấu hình hoặc muốn ép bắt tay lại từ đầu. */\n reset(): void {\n this.cachedToken = null;\n this.expiresAt = 0;\n }\n\n private async requestToken(): Promise<string> {\n const { clientId, clientSecret } = this.options;\n const oidcTokenUrl = this.options.oidcTokenUrl || envTokenUrl();\n\n // Báo thiếu thứ gì cụ thể. \"Thiếu cấu hình OIDC\" bắt người đọc đi dò từng biến.\n const missing: string[] = [];\n if (!oidcTokenUrl) missing.push('oidcTokenUrl (hoặc env OIDC_TOKEN_URL)');\n if (!clientId) missing.push('clientId');\n if (!clientSecret) missing.push('clientSecret');\n if (missing.length) {\n throw new Error(`Machine token thiếu cấu hình: ${missing.join(', ')}`);\n }\n\n const resource = this.options.resource || this.options.fallbackResource || '';\n const form = new URLSearchParams({\n grant_type: 'client_credentials',\n client_id: clientId!,\n client_secret: clientSecret!,\n scope: this.options.scope ?? 'access',\n resource,\n }).toString();\n\n const fetchFn = this.options.fetchImpl ?? resolveFetch();\n const response = await fetchFn(oidcTokenUrl!, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: form,\n } as any);\n\n const data: any = await (response as any).json().catch(() => undefined);\n\n if (!(response as any).ok) {\n const detail = (data && (data.error_description || data.error)) || 'không rõ nguyên nhân';\n throw new Error(`OIDC token request thất bại (${(response as any).status}): ${detail}`);\n }\n if (!data || typeof data.access_token !== 'string') {\n throw new Error('OIDC token response không có access_token');\n }\n\n const ttlSeconds = typeof data.expires_in === 'number' ? data.expires_in : DEFAULT_TTL_SECONDS;\n this.cachedToken = data.access_token as string;\n this.expiresAt = Date.now() + Math.max(ttlSeconds * 1000 - EARLY_REFRESH_MS, 0);\n return this.cachedToken;\n }\n}\n\n/** Đọc env lúc gọi, không hằng số hoá lúc import (test đổi env giữa chừng vẫn đúng). */\nconst envTokenUrl = (): string | undefined => {\n const runtimeProcess = (globalThis as any).process as\n | { env?: Record<string, string | undefined> }\n | undefined;\n return runtimeProcess?.env?.OIDC_TOKEN_URL;\n};\n\nconst resolveFetch = (): typeof fetch => {\n const fetchFn = (globalThis as any).fetch as typeof fetch | undefined;\n if (typeof fetchFn !== 'function') {\n throw new Error('FETCH_UNAVAILABLE: runtime không có global fetch (cần Node >= 18)');\n }\n return fetchFn;\n};\n","/**\n * SaulException — lỗi NGHIỆP VỤ mà handler chủ động ném để từ chối một lời gọi.\n *\n * Khác `Error` trần ở đúng hai điểm, và cả hai đều là lý do nó tồn tại:\n *\n * 1. **Có `code`** đi qua được dây. Envelope lỗi của downstream mang cả `message` lẫn\n * `code`, gateway map sang ack `{ ok:false, error, code }`, client dựng lại thành\n * `SaulException` — nên client bắt được lỗi theo mã chứ không phải so khớp chuỗi tiếng\n * Việt. `Error` trần chỉ còn lại `message` sau chuyến đi.\n * 2. **Là ý ĐỊNH từ chối, không phải sự cố.** Gateway log `Error` lạ như một lỗi server,\n * còn `SaulException` là câu trả lời hợp lệ (\"bạn không được sub chat của người khác\")\n * nên không cần ồn ào trong log.\n *\n * Chỗ dùng chính là validator của subscription: controller `sub<Model>` ném `SaulException`\n * để từ chối, và cái ném đó nổi lên đúng chỗ client đang `catch` quanh vòng `for await`.\n */\nexport class SaulException extends Error {\n /** Mã ổn định để client rẽ nhánh. Mặc định là mã chung của một từ chối nghiệp vụ. */\n readonly code: string;\n\n constructor(message: string, code: string = 'SAUL_EXCEPTION') {\n super(message);\n this.name = 'SaulException';\n this.code = code;\n // Bắt buộc khi target ES5/ES2015: không có dòng này thì `instanceof SaulException`\n // trả false ở chính nơi người ta sẽ dùng nó nhiều nhất — khối catch.\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Nhận diện KHÔNG dùng `instanceof`.\n *\n * Gateway, downstream và SDK có thể resolve ba bản copy vật lý khác nhau của package\n * này (pnpm workspace + code sinh ra được copy vào từng service), nên `instanceof` sẽ\n * trả false cho một SaulException hoàn toàn thật. Cùng lý do với `FileStream.is`.\n */\n static is(value: unknown): value is SaulException {\n return (\n value instanceof SaulException ||\n (value instanceof Error && (value as { name?: string }).name === 'SaulException')\n );\n }\n}\n","import { hostname } from 'os';\nimport { io as connectSocket } from 'socket.io-client';\nimport { SaulException } from '../models/saulException.ts';\nimport { MachineTokenProvider, type MachineTokenOptions } from '../auth/MachineTokenProvider.ts';\n\n/**\n * TUNNEL AGENT — đầu DOWNSTREAM của reverse tunnel.\n *\n * Service của bạn tự mở một WebSocket RA gateway, khai mình phục vụ những module nào, rồi\n * nằm nhận việc trên chính dây đó. Nhờ vậy service **không mở port nào** và **không có URL\n * public nào tồn tại** — gateway không bao giờ dial vào, nó chỉ trả lời trên kết nối mà bạn\n * đã mở. Đó là toàn bộ lý do mô hình này riêng tư.\n *\n * Nó KHÔNG biết gì về `functionMap`: `dispatch` được truyền vào từ ngoài, đúng cách\n * `createUploadSubsystem` nhận `dispatch` của runtime. Nhờ vậy agent dùng CHUNG một bảng\n * dispatch với HTTP và WebSocket — handler của bạn không đổi một dòng nào, và `ctx.transport`\n * vẫn là `'websocket'`.\n */\n\n/** Phải khớp hằng cùng tên ở gateway (`TunnelDispatcher.ts`). */\nconst DISPATCH_EVENT = 'saul:agent:dispatch';\nconst REGISTER_EVENT = 'saul:agent:register';\nconst HEARTBEAT_EVENT = 'saul:agent:heartbeat';\nconst UPLOAD_EVENT = 'saul:agent:upload';\n\n/**\n * Quy `ws://` → `http://` và `wss://` → `https://`.\n *\n * Một URL gateway phục vụ HAI đường: socket.io (chấp nhận cả bốn scheme) và `fetch` cho blob\n * store (chỉ chấp nhận http/https). `wss://` là thứ người ta gõ theo phản xạ cho \"URL\n * WebSocket\", và vì socket.io nuốt được nên nó chạy tới tận lúc cần kéo một blob mới nổ\n * `TypeError: protocol must be http:, https:` — ở một chỗ không gợi ý gì về nguyên nhân.\n *\n * Quy đổi ngay từ đầu, một lần, để cả hai đường dùng chung một giá trị hợp lệ. socket.io tự\n * nâng cấp `https:` thành `wss:` lúc bắt tay nên không mất gì.\n */\nconst httpScheme = (url: string): string => url.replace(/^ws(s?):\\/\\//i, 'http$1://');\n\n/** Chu kỳ heartbeat mặc định — khoảng một phần ba TTL directory của gateway (30s). */\nconst DEFAULT_HEARTBEAT_MS = 10_000;\n\n/** Khớp mặc định `TUNNEL_INLINE_MAX_BYTES` của gateway. */\nconst DEFAULT_INLINE_MAX_BYTES = 256 * 1024;\n\n/**\n * Nhịp tối thiểu giữa hai dòng log của các lần NỐI LẠI hỏng.\n *\n * Backoff tối đa là 30s, nên một agent hỏng qua đêm đẻ vài nghìn dòng giống hệt nhau — đủ\n * để người ta lọc nó đi, và thế là quay về đúng điểm mù cũ. Log lần đầu ngay lập tức rồi\n * giãn ra, kèm số lần đã hỏng, để một lần `grep` vẫn trả lời được \"hỏng từ bao giờ\".\n */\nconst RECONNECT_LOG_INTERVAL_MS = 60_000;\n\n/** Envelope một lời gọi gateway đẩy xuống. */\nexport interface TunnelDispatchBody {\n module: string;\n /**\n * Tên service KHAI BÁO trong definition (không phải tên class cài đặt).\n *\n * Optional vì gateway cũ không gửi: runtime tự resolve theo `module.function` khi tên\n * function là duy nhất trong module, và báo `AMBIGUOUS_FUNCTION` khi không.\n */\n service?: string;\n function: string;\n params: Record<string, any>;\n}\n\nexport interface TunnelAgentOptions extends MachineTokenOptions {\n /** Base URL của gateway (vd `https://saul.example.com`). */\n gatewayUrl: string;\n /** Module mà service này phục vụ. Cần permission `<module>.wsAgent` cho từng cái. */\n modules: string[];\n /**\n * Định danh ổn định qua reconnect — thứ con người dùng để nhận ra \"máy nào\" trong màn\n * Agents. Bỏ trống thì suy từ hostname + pid.\n */\n instanceId?: string;\n /** Khả năng tự khai, vd `['upload']`. Gateway dùng thay cho việc probe HTTP. */\n capabilities?: string[];\n /**\n * Phiên bản service (thường là `version` trong `package.json` của nó).\n *\n * KHÔNG dùng để định tuyến — chỉ để nhìn thấy được. Trong rolling deploy, M agent của\n * cùng một module chạy hai phiên bản code và gateway xoay vòng qua tất cả, nên cùng một\n * lời gọi có thể cư xử hai kiểu; không có cột này thì điều đó chỉ suy đoán được từ hành\n * vi lệch nhau giữa các request.\n */\n version?: string;\n /** Bảng dispatch của runtime — thường là `(body) => saul.dispatch(body)`. */\n dispatch: (body: TunnelDispatchBody) => Promise<any>;\n /** Path socket.io của gateway, nếu khác mặc định. */\n path?: string;\n heartbeatMs?: number;\n /**\n * Kết quả lớn hơn ngần này (byte) sẽ đi qua blob store thay vì nằm trong ack. PHẢI khớp\n * `TUNNEL_INLINE_MAX_BYTES` của gateway.\n */\n inlineMaxBytes?: number;\n /**\n * Router `/saul/upload` của service (`createUploadSubsystem().router`).\n *\n * Có nó thì multipart upload chạy được qua tunnel: gateway đóng gói request HTTP, agent\n * replay vào đúng router này. Không có thì mọi request upload trả lỗi rõ ràng thay vì\n * timeout khó hiểu. `Saul.connectTunnel()` tự truyền vào.\n */\n uploadRouter?: { request(path: string, init?: any): Response | Promise<Response> };\n /** DI cho test: thay socket.io-client bằng bản giả. */\n socketFactory?: (url: string, options: any) => any;\n}\n\n/** Prefix mà gateway gắn vào path upload trên dây — khớp `UploadRelay.sendViaTunnel`. */\nconst UPLOAD_PATH_PREFIX = '/saul/upload';\n\n/**\n * Path trên dây → path mà SUB-ROUTER upload hiểu.\n *\n * Gateway gửi path HTTP đầy đủ (`/saul/upload/init`) vì đó là thứ nó gõ khi đi đường HTTP.\n * Nhưng `uploadRouter` là sub-router độc lập, được `app.route('/saul/upload', router)` gắn\n * vào app — bản thân nó chỉ khai `/init`, `/:uploadId/part/:n`… Replay nguyên path đầy đủ\n * vào nó thì Hono không khớp route nào và trả `404 Not Found`, khiến gateway báo\n * \"Downstream từ chối init (404)\" dù service hoàn toàn có upload.\n */\nconst routerPath = (path: string): string => {\n if (!path.startsWith(UPLOAD_PATH_PREFIX)) return path;\n const rest = path.slice(UPLOAD_PATH_PREFIX.length);\n // `/saul/upload` trần (không có phần đuôi) → `/`, vì Hono cần path bắt đầu bằng `/`.\n return rest.startsWith('/') ? rest : `/${rest}`;\n};\n\nexport class TunnelAgent {\n private socket?: any;\n private heartbeatTimer?: ReturnType<typeof setInterval>;\n private readonly tokens: MachineTokenProvider;\n private readonly instanceId: string;\n private closed = false;\n /** Id gateway cấp lúc đăng ký — blob khoá theo nó, nên phải giữ lại sau mỗi lần register. */\n private agentId?: string;\n private readonly blobBaseUrl: string;\n private readonly inlineMaxBytes: number;\n /**\n * Lần bắt tay sau phải BỎ QUA cache token.\n *\n * Bật khi gateway vừa từ chối xác thực: token trong tay có thể đã bị thu hồi TRƯỚC hạn,\n * và `MachineTokenProvider` thì chỉ nhìn `exp` nên vẫn vui vẻ trả lại đúng cái token vừa\n * bị chối — nối lại bao nhiêu lần cũng hỏng y hệt.\n */\n private forceTokenRefresh = false;\n /** Số lần nối lại hỏng liên tiếp; reset khi đăng ký lại được. */\n private reconnectFailures = 0;\n private lastReconnectLogAt = 0;\n\n constructor(private readonly options: TunnelAgentOptions) {\n this.tokens = new MachineTokenProvider({\n ...options,\n // Không khai `resource` thì dùng chính gateway — đúng mặc định của `initWsPush`.\n fallbackResource: options.fallbackResource ?? options.gatewayUrl,\n });\n this.instanceId = options.instanceId?.trim() || `${hostname()}-${process.pid}`;\n // `httpScheme`: blob đi bằng `fetch`, mà `fetch` từ chối thẳng `ws:`/`wss:` với\n // `TypeError: protocol must be http:, https:` — xem ghi chú ở hàm đó.\n this.blobBaseUrl = httpScheme(options.gatewayUrl).replace(/\\/+$/, '');\n // Phải KHỚP `TUNNEL_INLINE_MAX_BYTES` của gateway. Lệch xuống thì agent đẩy blob cho\n // những kết quả gateway thừa sức nhận inline (tốn một round-trip vô ích); lệch lên thì\n // kết quả lớn vẫn chui qua pub/sub.\n this.inlineMaxBytes = options.inlineMaxBytes ?? DEFAULT_INLINE_MAX_BYTES;\n }\n\n get connected(): boolean {\n return this.socket?.connected === true;\n }\n\n /**\n * Mở kết nối và đăng ký.\n *\n * Trả về khi gateway đã NHẬN đăng ký. Đăng ký hỏng thì `connect()` NÉM chứ không im lặng\n * để agent chạy tiếp: một agent tưởng mình đang phục vụ trong khi gateway không biết nó\n * tồn tại là chế độ hỏng không quan sát được — mọi lời gọi trả `AGENT_OFFLINE` còn service\n * thì nằm đó khoẻ mạnh.\n */\n async connect(): Promise<void> {\n this.closed = false;\n // Xin token NGAY, dù `auth` bên dưới là hàm và sẽ tự xin lại: thiếu `clientId` hay\n // `oidcTokenUrl` phải nổ ở đây với thông điệp chỉ đúng biến còn thiếu của\n // `MachineTokenProvider`, chứ không hoá trang thành một `connect_error` của\n // socket.io — thứ không gợi ý gì về cấu hình.\n await this.tokens.getToken();\n\n const factory = this.options.socketFactory ?? ((url: string, opts: any) => connectSocket(url, opts));\n const { origin, socketPath } = this.resolveEndpoint();\n const socket = factory(origin, {\n // HÀM, không phải object. socket.io gọi lại nó trước MỖI lần nối, kể cả mọi lần\n // nối lại; một object tĩnh thì token bị chốt cứng ở `connect()` đầu tiên và agent\n // chết VĨNH VIỄN ngay khi token hết hạn (token máy sống 3 ngày). Chế độ hỏng đó\n // hoàn toàn câm: pod `Running`, gateway trả `AGENT_OFFLINE`, log trống trơn.\n auth: (cb: (data: any) => void) => {\n this.tokens\n .getToken(this.forceTokenRefresh)\n .then((token) => {\n this.forceTokenRefresh = false;\n cb({ token });\n })\n .catch((err: Error) => {\n // VẪN phải gọi `cb`: bỏ qua nó là treo lần bắt tay này mà không sinh\n // `connect_error` nào, nên socket.io cũng không hẹn lần sau — identity\n // chết một nhịp biến thành agent chết hẳn.\n this.logReconnectFailure(`không lấy được token máy: ${err.message}`);\n cb({ token: '' });\n });\n },\n path: socketPath,\n // Chỉ WebSocket: long-polling cần session affinity ở LB, và agent là kết nối dài\n // hạn nên không có lý do gì để chấp nhận cái giá đó.\n transports: ['websocket'],\n reconnection: true,\n // Có JITTER: một replica gateway restart sẽ làm TOÀN BỘ agent của nó nối lại cùng\n // một nhịp và dồn hết sang replica khác.\n randomizationFactor: 0.5,\n reconnectionDelay: 1_000,\n reconnectionDelayMax: 30_000,\n });\n this.socket = socket;\n\n const firstRegistration = new Promise<void>((resolve, reject) => {\n let settled = false;\n\n // Đăng ký lại sau MỖI lần nối — gateway dọn sạch sổ agent khi socket rớt, nên\n // không đăng ký lại là nằm im vĩnh viễn.\n socket.on('connect', () => {\n this.register()\n .then(() => {\n if (!settled) {\n settled = true;\n resolve();\n }\n })\n .catch((err: Error) => {\n if (!settled) {\n settled = true;\n reject(err);\n return;\n }\n // Lần nối lại mà đăng ký hỏng (quyền vừa bị gỡ): không giết process\n // của service, nhưng phải ồn ào — nó đang chạy mà không nhận được việc.\n console.error('[saul][tunnel] đăng ký lại thất bại:', err.message);\n });\n });\n\n socket.on('connect_error', (err: Error) => {\n const message = err?.message ?? 'không rõ lý do';\n // Gateway từ chối XÁC THỰC (`WS_AUTH:` là prefix nó gắn ở `io.use`): đưa lại\n // đúng token vừa bị chối chỉ tốn thêm một vòng, nên lần sau đi xin token mới.\n if (message.startsWith('WS_AUTH')) this.forceTokenRefresh = true;\n\n if (!settled) {\n settled = true;\n // In ĐÚNG endpoint đã gõ cửa, không phải `gatewayUrl` thô: hai giá trị đó\n // khác nhau khi gateway có path (xem `resolveEndpoint`), và chính chỗ lệch\n // đó là nguyên nhân thường gặp nhất của một `timeout` ở đây.\n reject(\n new Error(\n `Không kết nối được gateway '${origin}${socketPath ?? ''}': ${err.message}. ` +\n `Kiểm: gateway có đang chạy không, reverse proxy đã cho phép nâng cấp WebSocket chưa, ` +\n `và WS_PATH của gateway có khớp không (đặt option 'path' để chỉ định tay).`\n )\n );\n return;\n }\n\n // Nối lại hỏng PHẢI ồn ào. Bản trước chỉ log ở lần bắt tay đầu tiên, nên một\n // agent chết vì token hết hạn nằm im nhiều ngày mà không để lại dòng nào nối\n // \"service khoẻ\" với \"gateway bảo nó offline\".\n this.logReconnectFailure(\n `nối lại gateway '${origin}${socketPath ?? ''}' thất bại: ${message}`\n );\n });\n });\n\n socket.on(DISPATCH_EVENT, (payload: any, ack?: (response: any) => void) => {\n // KHÔNG await: nhiều lời gọi phải chạy song song trên cùng một socket. Chờ ở đây\n // biến agent thành hàng đợi một luồng.\n void this.handleDispatch(payload, ack);\n });\n\n socket.on(UPLOAD_EVENT, (payload: any, ack?: (response: any) => void) => {\n void this.handleUpload(payload, ack);\n });\n\n socket.on('disconnect', (reason: string) => {\n console.warn(`[saul][tunnel] mất kết nối tới gateway (${reason}) — đang thử nối lại`);\n });\n\n await firstRegistration;\n this.startHeartbeat();\n }\n\n /** Ngắt kết nối và dừng heartbeat. */\n async close(): Promise<void> {\n this.closed = true;\n this.stopHeartbeat();\n this.socket?.disconnect();\n this.socket = undefined;\n }\n\n /**\n * Tách `gatewayUrl` thành ORIGIN để kết nối và PATH cho socket.io.\n *\n * socket.io hiểu phần path của URL là **NAMESPACE**, không phải prefix:\n * `io('https://host/api')` nghĩa là \"namespace `/api`, path `/socket.io` ở ROOT\" — nó sẽ\n * gõ cửa `https://host/socket.io/…`. Với gateway đứng sau reverse proxy chỉ route\n * `/api/*`, request đó rơi vào hư không và agent chết bằng `timeout`, một thông điệp\n * không hề gợi ý nguyên nhân thật.\n *\n * Khác hẳn `initWsPush`, nơi `gatewayBaseUrl` được nối chuỗi (`${base}/ws/push`) nên một\n * base có path hoạt động đúng như trực giác. Hai API nhận cùng một URL nhưng diễn giải\n * khác nhau, nên chỗ này phải tự quy đổi.\n *\n * `options.path` khai tường minh luôn THẮNG — gateway đổi `WS_PATH` thì đó là đường duy\n * nhất để cắm vào.\n */\n private resolveEndpoint(): { origin: string; socketPath: string | undefined } {\n try {\n const url = new URL(httpScheme(this.options.gatewayUrl));\n const basePath = url.pathname.replace(/\\/+$/, '');\n return {\n origin: url.origin,\n socketPath: this.options.path ?? `${basePath}/socket.io`,\n };\n } catch {\n // URL không parse được: trả về NGUYÊN VẸN để lỗi nổ ở socket.io với đúng giá trị\n // sai, thay vì nổ ở đây bằng một lỗi parse không có ngữ cảnh.\n return { origin: this.options.gatewayUrl, socketPath: this.options.path };\n }\n }\n\n /** Khai module với gateway; ném khi không module nào được nhận. */\n private async register(): Promise<void> {\n const ack = await this.emitWithAck(REGISTER_EVENT, {\n instanceId: this.instanceId,\n modules: this.options.modules,\n capabilities: this.options.capabilities ?? [],\n version: this.options.version,\n });\n\n if (!ack?.ok) {\n throw new Error(\n `Gateway từ chối đăng ký agent: ${ack?.error ?? 'không rõ lý do'}${ack?.code ? ` [${ack.code}]` : ''}`\n );\n }\n\n const data = ack.data ?? {};\n // Nối lại được rồi: quên chuỗi hỏng cũ để lần hỏng sau lại được log ngay lập tức.\n this.reconnectFailures = 0;\n // Giữ lại: blob khoá theo `agentId`, và nó ĐỔI sau mỗi lần đăng ký lại (gateway cấp\n // mới cho mỗi connection). Dùng id cũ sau reconnect là mọi lần kéo blob đều 403.\n this.agentId = data.agentId;\n // Từ chối MỘT PHẦN không phải lỗi chí mạng — các module còn lại vẫn phục vụ được —\n // nhưng im lặng thì người vận hành chỉ phát hiện khi có người báo mất tính năng.\n if (Array.isArray(data.denied) && data.denied.length) {\n console.warn(\n '[saul][tunnel] module bị từ chối:',\n data.denied.map((d: any) => `${d.module} (${d.code})`).join(', ')\n );\n }\n\n // Log đầy đủ thông tin agent ngay khi kết nối thành công — nằm ở ĐÂY (thư viện dùng\n // chung) chứ không phải ở từng downstream service, để mọi service dùng `connectTunnel`\n // đều tự động có log này mà không ai phải tự viết.\n console.log(\n `[saul][tunnel] agent đã kết nối gateway: instanceId=${this.instanceId}, ` +\n `version=${this.options.version ?? '?'}, ` +\n `capabilities=${JSON.stringify(this.options.capabilities ?? [])}, ` +\n `modules=${JSON.stringify(data.accepted ?? [])}, ` +\n `agentId=${data.agentId}, ip=${data.ip ?? '?'}, gateway=${this.options.gatewayUrl}`\n );\n }\n\n /** Chạy một lời gọi và ack lại đúng envelope mà gateway đọc. */\n private async handleDispatch(payload: any, ack?: (response: any) => void): Promise<void> {\n try {\n if (!payload || typeof payload.module !== 'string' || typeof payload.function !== 'string') {\n throw new Error('Envelope dispatch thiếu module/function');\n }\n\n // Payload lớn không đi trong message socket (nó sẽ phải qua Redis pub/sub của\n // gateway và nghẹt cả cluster) — gateway cất vào blob store, ta kéo về ở đây.\n const params = payload.paramsBlob\n ? await this.pullBlob(payload.paramsBlob, payload.module)\n : payload.params ?? {};\n\n const result = await this.options.dispatch({\n module: payload.module,\n // Chỉ đưa vào khi gateway gửi: runtime cũ không biết field này, runtime mới\n // thiếu nó thì tự resolve theo `module.function`.\n ...(typeof payload.service === 'string' && payload.service\n ? { service: payload.service }\n : {}),\n function: payload.function,\n params,\n });\n\n // Chiều ngược lại cũng vậy: kết quả lớn đi qua blob, ack chỉ mang id.\n const serialized = JSON.stringify(result ?? null);\n if (Buffer.byteLength(serialized, 'utf8') > this.inlineMaxBytes) {\n const resultBlob = await this.pushBlob(serialized, payload.module);\n ack?.({ ok: true, resultBlob });\n return;\n }\n\n ack?.({ ok: true, result });\n } catch (err: any) {\n // MỌI lỗi đều thành ack, không cái nào được nổi lên thành unhandled rejection:\n // một handler hỏng chỉ được làm hỏng ĐÚNG lời gọi đó, không phải cả agent.\n //\n // `SaulException.is` chứ không `instanceof`: package này có thể tồn tại nhiều bản\n // vật lý (pnpm workspace + code sinh copy vào từng service).\n ack?.({\n ok: false,\n error: err?.message ?? 'Lỗi không xác định ở downstream',\n code: SaulException.is(err) ? err.code : undefined,\n });\n }\n }\n\n /**\n * Replay một request multipart upload vào chính Hono router `/saul/upload` của service.\n *\n * KHÔNG dựng lại logic upload ở đây: registry, part store, mã lỗi, ràng buộc kích thước —\n * tất cả đã nằm trong router đó và đã được test. Đóng gói lại thành một `Request` rồi gọi\n * `router.request()` cho ta dùng nguyên bộ ấy, và giữ cho đường HTTP với đường tunnel\n * không thể trôi lệch hành vi.\n *\n * Path trên dây là path HTTP ĐẦY ĐỦ (`/saul/upload/init`) — xem `routerPath` về lý do\n * phải cắt prefix trước khi đưa vào sub-router.\n */\n private async handleUpload(payload: any, ack?: (response: any) => void): Promise<void> {\n try {\n if (!this.options.uploadRouter) {\n throw new Error('Service này không bật multipart upload (thiếu uploadRouter)');\n }\n if (!payload || typeof payload.path !== 'string' || typeof payload.method !== 'string') {\n throw new Error('Envelope upload thiếu method/path');\n }\n\n const encoded = payload.paramsBlob\n ? (await this.pullBlob(payload.paramsBlob, payload.module)).body\n : payload.params?.body ?? '';\n\n // Part là NHỊ PHÂN nên đi trên dây dưới dạng base64; giải mã lại ở đây để router\n // nhận đúng byte gốc.\n const body = encoded ? Buffer.from(String(encoded), 'base64') : undefined;\n\n const response = await this.options.uploadRouter.request(routerPath(payload.path), {\n method: payload.method,\n headers: payload.headers ?? {},\n body: body && payload.method !== 'GET' && payload.method !== 'DELETE' ? body : undefined,\n });\n\n const text = await response.text();\n const result = { status: response.status, body: text };\n\n // Response của `complete` mang kết quả handler, có thể lớn — đi qua blob như mọi\n // payload lớn khác.\n const serialized = JSON.stringify(result);\n if (Buffer.byteLength(serialized, 'utf8') > this.inlineMaxBytes) {\n ack?.({ ok: true, resultBlob: await this.pushBlob(serialized, payload.module) });\n return;\n }\n\n ack?.({ ok: true, result });\n } catch (err: any) {\n ack?.({ ok: false, error: err?.message ?? 'Lỗi upload không xác định' });\n }\n }\n\n /**\n * Kéo một blob payload về từ gateway.\n *\n * AGENT là bên gọi ra (outbound HTTPS), y như khi mở socket — gateway không bao giờ dial\n * vào đây. Đó là điều giữ cho mô hình vẫn riêng tư dù dữ liệu lớn đi đường HTTP.\n */\n private async pullBlob(blobId: string, module: string): Promise<Record<string, any>> {\n const url =\n `${this.blobBaseUrl}/tunnel/blob/${encodeURIComponent(blobId)}` +\n `?module=${encodeURIComponent(module)}&agentId=${encodeURIComponent(this.agentId ?? '')}`;\n\n const response = await this.fetchWithToken(url, { method: 'GET' });\n if (!response.ok) {\n const detail = await response.text().catch(() => '');\n throw new Error(`Không kéo được payload từ gateway (${response.status}): ${detail}`);\n }\n\n return JSON.parse(await response.text());\n }\n\n /** Đẩy kết quả lớn lên gateway, trả về id để gửi trong ack. */\n private async pushBlob(data: string, module: string): Promise<string> {\n const response = await this.fetchWithToken(`${this.blobBaseUrl}/tunnel/blob`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ module, agentId: this.agentId, data }),\n });\n\n const payload: any = await response.json().catch(() => undefined);\n if (!response.ok || !payload?.blobId) {\n throw new Error(\n `Không đẩy được kết quả lên gateway (${response.status}): ${payload?.error ?? 'không có blobId'}`\n );\n }\n return payload.blobId;\n }\n\n private async fetchWithToken(url: string, init: any): Promise<any> {\n const fetchFn = (globalThis as any).fetch;\n if (typeof fetchFn !== 'function') throw new Error('FETCH_UNAVAILABLE (cần Node >= 18)');\n\n const token = await this.tokens.getToken();\n return await fetchFn(url, {\n ...init,\n headers: { ...(init.headers ?? {}), authorization: `Bearer ${token}` },\n });\n }\n\n /**\n * Log một lần nối lại hỏng, có tiết chế theo `RECONNECT_LOG_INTERVAL_MS`.\n *\n * Dùng `console.error` chứ không `warn`: agent không nối lại được là service đang chạy mà\n * không nhận được việc — đúng thứ cần lọt qua bộ lọc mức log của mọi hệ thống thu thập.\n */\n private logReconnectFailure(message: string): void {\n this.reconnectFailures += 1;\n const now = Date.now();\n if (this.reconnectFailures > 1 && now - this.lastReconnectLogAt < RECONNECT_LOG_INTERVAL_MS) return;\n this.lastReconnectLogAt = now;\n console.error(`[saul][tunnel] ${message} (hỏng liên tiếp lần ${this.reconnectFailures})`);\n }\n\n private startHeartbeat(): void {\n this.stopHeartbeat();\n const every = this.options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;\n\n this.heartbeatTimer = setInterval(() => {\n if (this.closed || !this.socket) return;\n this.socket.emit(HEARTBEAT_EVENT, {}, (ack: any) => {\n // Gateway quên mất agent này (thường do replica vừa restart): đăng ký lại thay\n // vì tiếp tục nằm chờ việc không bao giờ tới.\n if (ack && ack.ok === false) {\n void this.register().catch((err: Error) =>\n console.error('[saul][tunnel] đăng ký lại sau heartbeat thất bại:', err.message)\n );\n }\n });\n }, every);\n\n // Heartbeat không được giữ process sống: service tắt thì nó phải tắt theo.\n this.heartbeatTimer.unref?.();\n }\n\n private stopHeartbeat(): void {\n if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = undefined;\n }\n\n private emitWithAck(event: string, payload: any): Promise<any> {\n return new Promise((resolve) => {\n this.socket.emit(event, payload, resolve);\n });\n }\n}\n","export class File {\n private content: string | null\n\n public metadata: {\n name: string\n size: number\n type: string\n }\n\n constructor(meta?: { name: string, size: number, type: string }) {\n this.metadata = meta || { name: '', size: 0, type: '' }\n this.content = ''\n }\n\n private encodeBase64(buffer: ArrayBuffer): string {\n return Buffer.from(buffer).toString('base64');\n }\n\n private decodeBase64(base64: string): ArrayBuffer {\n const buffer = Buffer.from(base64, 'base64');\n const uint8Array = new Uint8Array(buffer.length);\n buffer.copy(uint8Array);\n return uint8Array.buffer;\n }\n\n /**\n * Đọc tất cả nội dụng của file\n */\n read(): ArrayBuffer {\n if (!this.content) {\n throw new Error('File content is not set')\n }\n return this.decodeBase64(this.content)\n }\n\n /**\n * Ghi nội dung vào file\n */\n write(buffer: ArrayBuffer) {\n this.content = this.encodeBase64(buffer)\n }\n}\n\n","import { Readable, type Writable } from 'stream'\nimport { pipeline } from 'stream/promises'\n\n/**\n * FILE DẠNG STREAM — cho file mà cả nội dung KHÔNG được phép nằm trong RAM.\n *\n * Khác `File` (buffered, giữ base64 trong bộ nhớ, trần vài chục MB), `FileStream` chỉ giữ\n * metadata + một hàm mở nguồn. Byte chỉ chảy khi có người đọc, nên RAM là hằng số bất kể\n * file 8MB hay 8GB.\n *\n * Dùng được ở CẢ HAI vị trí trong definition:\n * - param → chiều lên: multipart upload, gateway/downstream đưa `Readable` vào đây.\n * - returnType → chiều xuống: handler trả về, runtime pipe thẳng ra browser.\n *\n * Khai kiểu nào là chọn hợp đồng đó. Transport KHÔNG được đánh tráo `File` thành\n * `FileStream` hay ngược lại — một param có shape phụ thuộc transport là thứ tuyệt đối\n * không được xảy ra.\n */\n\n/** Brand để nhận diện qua ranh giới package — xem `FileStream.is`. */\nconst FILE_STREAM_BRAND = Symbol.for('@fidt/saul.FileStream')\n\nexport interface FileStreamMetadata {\n name: string\n /** Kích thước KHAI BÁO. `0` = không biết trước (dữ liệu sinh on-the-fly). */\n size: number\n type: string\n}\n\nexport class FileStream {\n /** @internal Đánh dấu instance để `is()` nhận ra qua nhiều bản copy của package. */\n readonly [FILE_STREAM_BRAND] = true as const\n\n readonly metadata: FileStreamMetadata\n\n private readonly source: () => Readable\n private consumed = false\n\n constructor(metadata: FileStreamMetadata, source: () => Readable) {\n this.metadata = metadata\n this.source = source\n }\n\n /**\n * Mở nguồn và trả về `Readable`.\n *\n * Gọi lần thứ hai sẽ NÉM. Một stream không phải là value: đọc hai lần thì lần thứ hai\n * hoặc rỗng, hoặc mở lại nguồn và trả về dữ liệu đã cũ — cả hai đều là bug im lặng.\n * Cần đọc nhiều lần thì `buffer()` một lần rồi tự giữ.\n */\n stream(): Readable {\n if (this.consumed) {\n throw new Error(\n `FILE_STREAM_CONSUMED: '${this.metadata.name}' đã được đọc rồi — một FileStream chỉ tiêu thụ được một lần`\n )\n }\n this.consumed = true\n return this.source()\n }\n\n /** Đã có ai đọc chưa — cho runtime biết có phải drain trước khi trả response không. */\n get isConsumed(): boolean {\n return this.consumed\n }\n\n /** Đẩy toàn bộ nội dung vào một `Writable`, chờ tới khi xong. */\n async pipeTo(destination: Writable): Promise<void> {\n await pipeline(this.stream(), destination)\n }\n\n /**\n * Đọc hết vào một `Buffer`.\n *\n * `maxBytes` là BẮT BUỘC, không có mặc định: đây là escape hatch duy nhất ra khỏi mô hình\n * streaming, và người dùng nó phải tự nói ra trần mình chấp nhận. Một `buffer()` không\n * trần đặt giữa đường xử lý file 3GB sẽ giết process — mà chỉ giết trong production, với\n * file lớn, chứ không bao giờ trong test.\n */\n async buffer(maxBytes: number): Promise<Buffer> {\n const chunks: Buffer[] = []\n let total = 0\n\n for await (const chunk of this.stream()) {\n const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array)\n total += buf.length\n if (total > maxBytes) {\n throw new Error(\n `FILE_STREAM_TOO_LARGE: '${this.metadata.name}' vượt ${maxBytes} byte khi buffer()`\n )\n }\n chunks.push(buf)\n }\n\n return Buffer.concat(chunks, total)\n }\n\n /**\n * Có phải `FileStream` không.\n *\n * Dùng brand chứ KHÔNG dùng `instanceof`: gateway và downstream có thể resolve hai bản\n * copy khác nhau của `@fidt/saul-server` (dual-package hazard), lúc đó `instanceof` trả\n * `false` cho một object hoàn toàn hợp lệ — và nó hỏng theo kiểu chỉ lộ ra khi deploy\n * thật, không bao giờ trong monorepo.\n */\n static is(value: unknown): value is FileStream {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as Record<symbol, unknown>)[FILE_STREAM_BRAND] === true\n )\n }\n\n /** Dựng từ một `Buffer` sẵn có — tiện cho test và cho handler trả dữ liệu nhỏ. */\n static fromBuffer(metadata: Omit<FileStreamMetadata, 'size'> & { size?: number }, data: Buffer): FileStream {\n return new FileStream(\n { ...metadata, size: metadata.size ?? data.length },\n () => Readable.from(data)\n )\n }\n}\n","/**\n * PREDICATE — điều kiện lọc mà client khai khi mở một subscription.\n *\n * File này CHỈ có kiểu, không có runtime: việc đánh giá predicate xảy ra ở GATEWAY (nó giữ\n * socket và biết ai đang nghe gì), không ở downstream. Downstream chỉ cần đọc và thu hẹp\n * predicate trong validator `sub<Model>` rồi trả về.\n *\n * Cú pháp `$`-prefix, cố ý KHÁC filter authz kiểu Directus (`_eq`, `_or`) ở\n * `Permission.filter`. Hai thứ trả lời hai câu hỏi khác nhau — filter authz nói \"được thấy\n * BẢN GHI nào\" và được forward xuống query storage; predicate nói \"muốn nhận EVENT nào\" và\n * được Saul tự đánh giá in-memory. Trộn cú pháp sẽ khiến người đọc tưởng chúng thay nhau được.\n */\n\n/** So sánh trên một field. Nhiều toán tử trong cùng object thì AND với nhau. */\nexport interface PredicateComparison<V> {\n $eq?: V;\n $ne?: V;\n $gt?: V;\n $gte?: V;\n $lt?: V;\n $lte?: V;\n $in?: V[];\n $nin?: V[];\n /** Field có mặt trong payload hay không — nói về SỰ CÓ MẶT, không phải giá trị. */\n $exists?: boolean;\n}\n\n/**\n * Predicate trên một Model.\n *\n * Giá trị trần là shorthand của `$eq`: `{ chatId: 12 }` ≡ `{ chatId: { $eq: 12 } }`.\n * Các key cùng cấp AND với nhau, nên `{}` khớp TẤT CẢ — đó là lý do validator `sub<Model>`\n * mặc định TỪ CHỐI, và là lý do nên thu hẹp predicate thay vì chỉ kiểm rồi trả nguyên.\n *\n * KHÔNG có `$regex`: nó là đường ReDoS do client tự khai, chạy ở đường nóng của mọi lần push.\n */\nexport type Predicate<T> = {\n [K in keyof T]?: T[K] | PredicateComparison<T[K]>;\n} & {\n $and?: Predicate<T>[];\n $or?: Predicate<T>[];\n $not?: Predicate<T>;\n};\n\n/**\n * Một subscription đang mở, nhìn từ phía server.\n *\n * `id` do gateway sinh TRƯỚC khi gọi validator và truyền xuống qua `ctx.subId`, nên\n * validator lưu lại được để về sau `sub<Model>.unsub(id)` thu hồi (vd user bị mời khỏi\n * phòng chat trong lúc đang nghe).\n */\nexport interface Sub<T> {\n id: string;\n predicate: Predicate<T>;\n}\n\n/**\n * Gộp nhiều predicate bằng AND — cách chuẩn để validator THU HẸP thứ client xin.\n *\n * ```ts\n * return narrow(predicate, { userId: { $eq: ctx.principal.sub } })\n * ```\n *\n * Dùng `$and` thay vì trải phẳng bằng spread: spread sẽ để điều kiện sau GHI ĐÈ điều kiện\n * trước trên cùng một field, tức là client khai `{ userId: { $eq: 'ai-đó' } }` rồi ràng\n * buộc bảo mật ghi đè lên — đúng ý. Nhưng ở chiều ngược lại (`{...cứng, ...client}`) thì\n * client ghi đè ràng buộc bảo mật. `$and` không có chiều nào để nhầm: mọi nhánh đều phải thoả.\n */\nexport const narrow = <T>(...predicates: (Predicate<T> | null | undefined)[]): Predicate<T> => {\n const present = predicates.filter((p): p is Predicate<T> => !!p && Object.keys(p).length > 0);\n if (present.length === 0) return {} as Predicate<T>;\n if (present.length === 1) return present[0];\n return { $and: present } as Predicate<T>;\n};\n","/**\n * LỖI CỦA CHUNK STORE, có mã máy-đọc-được.\n *\n * Cùng kiểu với `BinaryStreamError` của đường WebSocket: mỗi tình huống một `code`, và mã\n * đó — chứ không phải chuỗi message — là thứ tầng trên map sang HTTP status. Một bảng map\n * duy nhất, không handler nào tự viết status.\n */\n\nexport type UploadErrorCode =\n | 'UPLOAD_NOT_FOUND'\n | 'UPLOAD_EXISTS'\n | 'UPLOAD_EXPIRED'\n | 'UPLOAD_COMPLETED'\n /** Đang complete dở — một lời gọi complete khác đã chiếm chỗ. */\n | 'UPLOAD_BUSY'\n | 'PART_OUT_OF_RANGE'\n | 'PART_IN_FLIGHT'\n | 'PART_TOO_LARGE'\n | 'PART_MISSING'\n | 'SIZE_MISMATCH'\n | 'CHECKSUM_MISMATCH'\n | 'DISK_BUDGET'\n | 'FORBIDDEN'\n\nexport class UploadError extends Error {\n constructor(\n public readonly code: UploadErrorCode,\n message: string\n ) {\n super(message)\n this.name = 'UploadError'\n }\n\n static is(value: unknown): value is UploadError {\n return value instanceof UploadError\n }\n}\n\n/**\n * MÃ LỖI → HTTP STATUS. Bảng DUY NHẤT, và nó phải phủ hết `UploadErrorCode`.\n *\n * `Record<UploadErrorCode, number>` cố ý không optional: thêm một mã mới mà quên map thì\n * TypeScript báo ngay, thay vì lặng lẽ trả 500 cho một lỗi vốn có nghĩa rõ ràng.\n */\nexport const UPLOAD_ERROR_STATUS: Record<UploadErrorCode, number> = {\n UPLOAD_NOT_FOUND: 404,\n UPLOAD_EXISTS: 409,\n // 410 chứ không 404: upload TỪNG tồn tại, client nên dừng retry và làm lại từ init.\n UPLOAD_EXPIRED: 410,\n UPLOAD_COMPLETED: 409,\n UPLOAD_BUSY: 409,\n PART_OUT_OF_RANGE: 416,\n PART_IN_FLIGHT: 409,\n PART_TOO_LARGE: 413,\n PART_MISSING: 422,\n SIZE_MISMATCH: 422,\n CHECKSUM_MISMATCH: 422,\n // 503 = lỗi TẠM THỜI: client nên backoff rồi thử lại, không phải báo lỗi cho người dùng.\n DISK_BUDGET: 503,\n FORBIDDEN: 403,\n}\n\nexport const uploadErrorStatus = (code: UploadErrorCode): number => UPLOAD_ERROR_STATUS[code] ?? 500\n","import { createHash } from 'crypto'\nimport { createReadStream, createWriteStream } from 'fs'\nimport { mkdir, open, readdir, rename, rm, stat, unlink } from 'fs/promises'\nimport { join } from 'path'\nimport { Readable, Transform } from 'stream'\nimport { pipeline } from 'stream/promises'\n\nimport { UploadError } from './UploadError.ts'\n\n/**\n * LƯU PART RA ĐĨA. Chỉ đĩa — không biết gì về Redis, HTTP hay vòng đời upload.\n *\n * Mỗi upload là một thư mục, mỗi part là một file. Không có gì thông minh ở đây, và đó là\n * chủ ý: thứ duy nhất cần đúng tuyệt đối là **không bao giờ giữ cả part trong RAM** và\n * **không bao giờ để lại một `.part` nửa vời mà tầng trên tưởng là hoàn chỉnh**.\n */\n\n/** Kích thước đọc khi ghép part. Đủ lớn để không syscall liên tục, đủ nhỏ để RAM phẳng. */\nconst READ_HIGH_WATER_MARK = 1024 * 1024\n\nexport interface WritePartResult {\n size: number\n /** sha256 hex của part — client dùng để retry có kiểm chứng. */\n etag: string\n}\n\nexport interface PartInfo {\n partNumber: number\n size: number\n}\n\nexport interface PartDiskStoreOptions {\n /** Thư mục gốc. Mỗi upload một thư mục con. */\n rootDir: string\n /** fsync mỗi part trước khi rename. Tắt = đổi durability-khi-crash lấy throughput. */\n fsync?: boolean\n}\n\nexport class PartDiskStore {\n private readonly rootDir: string\n private readonly fsync: boolean\n\n constructor(options: PartDiskStoreOptions) {\n this.rootDir = options.rootDir\n this.fsync = options.fsync ?? true\n }\n\n /**\n * Thư mục của một upload.\n *\n * `assertSafeUploadId` chạy TRƯỚC `join` — đây là bề mặt path-traversal duy nhất của\n * cả tính năng, và `join('/tmp', '../../etc')` là một đường dẫn hoàn toàn hợp lệ.\n */\n dirFor(uploadId: string): string {\n assertSafeUploadId(uploadId)\n return join(this.rootDir, uploadId)\n }\n\n private pathFor(uploadId: string, partNumber: number, partCount: number): string {\n assertSafePartNumber(partNumber, partCount)\n // Đệm 0 để thứ tự tên file trùng thứ tự part — tiện khi soi bằng `ls` lúc debug.\n return join(this.dirFor(uploadId), `${String(partNumber).padStart(6, '0')}.part`)\n }\n\n async ensureDir(uploadId: string): Promise<void> {\n await mkdir(this.dirFor(uploadId), { recursive: true })\n }\n\n /**\n * Ghi một part. Trả về số byte thật và sha256.\n *\n * Bốn bước, thứ tự bắt buộc:\n * 1. `open(tmp, 'wx')` — hai PUT cùng part cùng lúc thì một cái thua ngay, thay vì\n * hai writer xen kẽ vào cùng một file.\n * 2. pipeline có đếm byte + hash — vượt trần là abort GIỮA CHỪNG, không đọc hết rồi\n * mới than.\n * 3. fsync (nếu bật) rồi `rename` — rename trong cùng filesystem là atomic, nên\n * file `.part` xuất hiện là đã hoàn chỉnh, không có trạng thái nửa vời.\n * 4. Chỉ khi rename xong mới coi như part tồn tại.\n */\n async writePart(\n uploadId: string,\n partNumber: number,\n body: Readable,\n opts: { maxBytes: number; partCount: number; declaredLength?: number; expectedSha256?: string }\n ): Promise<WritePartResult> {\n const finalPath = this.pathFor(uploadId, partNumber, opts.partCount)\n const tmpPath = `${finalPath}.tmp`\n\n await this.ensureDir(uploadId)\n\n let handle\n try {\n handle = await open(tmpPath, 'wx')\n } catch (err: any) {\n if (err?.code === 'EEXIST') {\n throw new UploadError(\n 'PART_IN_FLIGHT',\n `Part ${partNumber} của '${uploadId}' đang được ghi bởi một request khác`\n )\n }\n throw err\n }\n\n const hash = createHash('sha256')\n let received = 0\n const limit = opts.declaredLength !== undefined\n ? Math.min(opts.maxBytes, opts.declaredLength)\n : opts.maxBytes\n\n const meter = new Transform({\n transform(chunk, _enc, cb) {\n received += chunk.length\n if (received > limit) {\n // Huỷ NGAY, không đợi hết body: mục đích của trần là không nhận byte\n // thừa, chứ không phải nhận xong rồi mới từ chối.\n cb(new UploadError('PART_TOO_LARGE', `Part ${partNumber} vượt ${limit} byte`))\n return\n }\n hash.update(chunk)\n cb(null, chunk)\n },\n })\n\n try {\n await pipeline(body, meter, handle.createWriteStream())\n if (this.fsync) await handle.sync()\n } catch (err) {\n await handle.close().catch(() => undefined)\n await unlink(tmpPath).catch(() => undefined)\n throw err\n }\n await handle.close()\n\n if (opts.declaredLength !== undefined && received !== opts.declaredLength) {\n await unlink(tmpPath).catch(() => undefined)\n throw new UploadError(\n 'SIZE_MISMATCH',\n `Part ${partNumber}: nhận ${received} byte, content-length khai ${opts.declaredLength}`\n )\n }\n\n const etag = hash.digest('hex')\n if (opts.expectedSha256 && opts.expectedSha256 !== etag) {\n await unlink(tmpPath).catch(() => undefined)\n throw new UploadError(\n 'CHECKSUM_MISMATCH',\n `Part ${partNumber}: sha256 không khớp (khai ${opts.expectedSha256}, thật ${etag})`\n )\n }\n\n // Ghi đè part cũ là HỢP LỆ và cần thiết: đó chính là thứ làm retry từng part an toàn.\n await rename(tmpPath, finalPath)\n\n return { size: received, etag }\n }\n\n /** Mở một part để đọc. Dùng bởi `concatParts`. */\n createPartStream(uploadId: string, partNumber: number, partCount: number): Readable {\n return createReadStream(this.pathFor(uploadId, partNumber, partCount), {\n highWaterMark: READ_HIGH_WATER_MARK,\n })\n }\n\n /** Các part ĐANG CÓ THẬT trên đĩa (không tính `.tmp`). */\n async listParts(uploadId: string): Promise<PartInfo[]> {\n let entries: string[]\n try {\n entries = await readdir(this.dirFor(uploadId))\n } catch (err: any) {\n if (err?.code === 'ENOENT') return []\n throw err\n }\n\n const parts: PartInfo[] = []\n for (const entry of entries) {\n const match = /^(\\d{6})\\.part$/.exec(entry)\n if (!match) continue\n const info = await stat(join(this.dirFor(uploadId), entry))\n parts.push({ partNumber: Number.parseInt(match[1], 10), size: info.size })\n }\n return parts.sort((a, b) => a.partNumber - b.partNumber)\n }\n\n /** Xoá cả thư mục upload. Trả về số byte giải phóng. Idempotent. */\n async removeUpload(uploadId: string): Promise<number> {\n const dir = this.dirFor(uploadId)\n let freed = 0\n try {\n for (const entry of await readdir(dir)) {\n const info = await stat(join(dir, entry)).catch(() => undefined)\n if (info) freed += info.size\n }\n } catch (err: any) {\n if (err?.code === 'ENOENT') return 0\n throw err\n }\n await rm(dir, { recursive: true, force: true })\n return freed\n }\n\n /** Thư mục cũ hơn `olderThanMs` — ứng viên dọn rác. Xem `UploadSweeper`. */\n async listStaleDirs(olderThanMs: number, now: number = Date.now()): Promise<string[]> {\n let entries: string[]\n try {\n entries = await readdir(this.rootDir)\n } catch (err: any) {\n if (err?.code === 'ENOENT') return []\n throw err\n }\n\n const stale: string[] = []\n for (const entry of entries) {\n const info = await stat(join(this.rootDir, entry)).catch(() => undefined)\n if (!info?.isDirectory()) continue\n if (now - info.mtimeMs > olderThanMs) stale.push(entry)\n }\n return stale\n }\n\n /** Tổng byte đang chiếm — để đối chiếu lại counter, vốn trôi sau mỗi lần crash. */\n async totalBytes(): Promise<number> {\n let entries: string[]\n try {\n entries = await readdir(this.rootDir)\n } catch (err: any) {\n if (err?.code === 'ENOENT') return 0\n throw err\n }\n\n let total = 0\n for (const entry of entries) {\n const dir = join(this.rootDir, entry)\n const info = await stat(dir).catch(() => undefined)\n if (!info?.isDirectory()) continue\n for (const file of await readdir(dir).catch(() => [] as string[])) {\n const fileInfo = await stat(join(dir, file)).catch(() => undefined)\n if (fileInfo?.isFile()) total += fileInfo.size\n }\n }\n return total\n }\n}\n\n/**\n * `uploadId` phải là UUID. Kiểm TRƯỚC mọi thao tác đường dẫn.\n *\n * Không dùng blacklist (`..`, `/`): danh sách cấm luôn thiếu một dạng nào đó\n * (`%2e%2e`, null byte, tên dài quá giới hạn OS). Whitelist một hình dạng đã biết thì không.\n */\nexport function assertSafeUploadId(uploadId: string): void {\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(uploadId)) {\n throw new UploadError('FORBIDDEN', `uploadId không hợp lệ: '${uploadId}'`)\n }\n}\n\n/** `partNumber` phải là số nguyên trong `1..partCount`. */\nexport function assertSafePartNumber(partNumber: number, partCount: number): void {\n if (!Number.isSafeInteger(partNumber) || partNumber < 1 || partNumber > partCount) {\n throw new UploadError(\n 'PART_OUT_OF_RANGE',\n `partNumber ${partNumber} ngoài dải 1..${partCount}`\n )\n }\n}\n","import { readFile, writeFile, rename } from 'fs/promises'\nimport { join } from 'path'\n\nimport { UploadError } from './UploadError.ts'\nimport type { PartDiskStore } from './PartDiskStore.ts'\n\n/**\n * MANIFEST của các upload đang mở, giữ trong RAM và ghi kèm ra `meta.json`.\n *\n * Vì sao KHÔNG dùng Redis ở đây: một upload sống trọn đời trên MỘT process — gateway đã ghim\n * `target` từ lúc init, nên mọi part của nó về cùng một nơi. Thêm Redis vào downstream chỉ để\n * lưu thứ không ai khác đọc là bắt mọi service dùng Saul phải nuôi thêm một dependency.\n *\n * `meta.json` tồn tại vì hai lý do, đều là chuyện lúc hỏng chứ không phải lúc chạy: sống sót\n * qua restart, và cho sweeper phân biệt \"thư mục của một upload đang chạy\" với \"rác của một\n * process đã chết\".\n */\n\nconst META_FILE = 'meta.json'\n\nexport type UploadState = 'open' | 'completing' | 'completed'\n\nexport interface UploadRecord {\n uploadId: string\n module: string\n /** Service khai báo trong definition. Vắng khi gateway cũ khởi tạo upload. */\n service?: string\n function: string\n /** Tên param kiểu FileStream của function đích. */\n param: string\n /** Các param KHÔNG phải file của cùng lời gọi. */\n params: Record<string, unknown>\n file: { name: string; size: number; type: string }\n chunkSize: number\n partCount: number\n /** Deadline TUYỆT ĐỐI do gateway quyết, không bao giờ gia hạn. */\n hardExpiresAt: number\n /** Deadline TRƯỢT, gia hạn mỗi lần nhận được part. */\n expiresAt: number\n idleTimeoutMs: number\n state: UploadState\n createdAt: number\n}\n\nexport interface UploadRegistryOptions {\n store: PartDiskStore\n /** Ngân sách đĩa của MÁY NÀY. Vượt thì từ chối init. */\n maxDiskBytes: number\n}\n\nexport class UploadRegistry {\n private readonly records = new Map<string, UploadRecord>()\n private readonly store: PartDiskStore\n private readonly maxDiskBytes: number\n\n /**\n * Byte đang chiếm, đếm tăng dần. Đây là con số ƯỚC LƯỢNG — nó trôi sau mỗi lần crash,\n * nên `UploadSweeper` định kỳ tính lại bằng `du` thật. Counter trôi mà không ai sửa thì\n * ngân sách âm thầm khoá mọi upload, và không có gì trong log nói tại sao.\n */\n private usedBytes = 0\n\n constructor(options: UploadRegistryOptions) {\n this.store = options.store\n this.maxDiskBytes = options.maxDiskBytes\n }\n\n get diskUsedBytes(): number {\n return this.usedBytes\n }\n\n /** Đặt lại bộ đếm sau khi đối chiếu với đĩa thật. Xem `UploadSweeper`. */\n setDiskUsedBytes(bytes: number): void {\n this.usedBytes = Math.max(0, bytes)\n }\n\n async init(record: Omit<UploadRecord, 'state' | 'createdAt' | 'expiresAt'>): Promise<UploadRecord> {\n if (this.records.has(record.uploadId)) {\n throw new UploadError('UPLOAD_EXISTS', `Upload '${record.uploadId}' đã tồn tại`)\n }\n // Từ chối SỚM cái chắc chắn không vừa. Kế toán thật diễn ra theo từng part, nhưng\n // biết trước là không đủ chỗ thì không có lý do gì để nhận byte đầu tiên.\n if (this.usedBytes + record.file.size > this.maxDiskBytes) {\n throw new UploadError(\n 'DISK_BUDGET',\n `Hết ngân sách đĩa (${this.usedBytes}/${this.maxDiskBytes} byte) — thử lại sau`\n )\n }\n\n const now = Date.now()\n const full: UploadRecord = {\n ...record,\n state: 'open',\n createdAt: now,\n expiresAt: Math.min(now + record.idleTimeoutMs, record.hardExpiresAt),\n }\n\n this.records.set(full.uploadId, full)\n await this.store.ensureDir(full.uploadId)\n await this.persist(full)\n return full\n }\n\n /**\n * Record của một upload đang MỞ được.\n *\n * Hết hạn thì ném `UPLOAD_EXPIRED` chứ không `UPLOAD_NOT_FOUND`: hai thứ đó nói với\n * client hai điều khác nhau — \"id sai, đừng thử nữa\" và \"chậm quá, làm lại từ init\".\n */\n get(uploadId: string, allowed: UploadState[] = ['open']): UploadRecord {\n const record = this.records.get(uploadId)\n if (!record) {\n throw new UploadError('UPLOAD_NOT_FOUND', `Upload '${uploadId}' không tồn tại`)\n }\n if (Date.now() > record.hardExpiresAt || Date.now() > record.expiresAt) {\n throw new UploadError('UPLOAD_EXPIRED', `Upload '${uploadId}' đã hết hạn`)\n }\n if (!allowed.includes(record.state)) {\n throw new UploadError(\n record.state === 'completed' ? 'UPLOAD_COMPLETED' : 'UPLOAD_BUSY',\n `Upload '${uploadId}' đang ở trạng thái '${record.state}'`\n )\n }\n return record\n }\n\n /** Có record nào không, bất kể trạng thái — cho abort idempotent và cho sweeper. */\n peek(uploadId: string): UploadRecord | undefined {\n return this.records.get(uploadId)\n }\n\n list(): UploadRecord[] {\n return [...this.records.values()]\n }\n\n /**\n * Gia hạn deadline sau khi nhận được một part, và cộng chênh lệch byte vào bộ đếm.\n *\n * `delta` chứ không phải `size`: PUT lại một part là GHI ĐÈ, nên phần tăng thêm chỉ là\n * hiệu số. Cộng nguyên `size` mỗi lần retry sẽ thổi phồng bộ đếm cho tới khi ngân sách\n * khoá cả những upload hoàn toàn bình thường.\n */\n touch(uploadId: string, deltaBytes: number): void {\n const record = this.records.get(uploadId)\n if (!record) return\n record.expiresAt = Math.min(Date.now() + record.idleTimeoutMs, record.hardExpiresAt)\n this.usedBytes = Math.max(0, this.usedBytes + deltaBytes)\n }\n\n /**\n * Chuyển sang `completing`, một lần duy nhất.\n *\n * Đây là chốt chống complete đồng thời: hai request cùng lúc thì cái thứ hai thấy state\n * đã đổi và nhận `UPLOAD_BUSY`, thay vì cả hai cùng ghép file và cùng gọi handler.\n */\n beginComplete(uploadId: string): UploadRecord {\n const record = this.get(uploadId, ['open'])\n record.state = 'completing'\n return record\n }\n\n /** Quay lại `open` khi complete thất bại — cho client retry mà không mất part đã gửi. */\n rollbackComplete(uploadId: string): void {\n const record = this.records.get(uploadId)\n if (record?.state === 'completing') record.state = 'open'\n }\n\n /** Xoá record + part trên đĩa. Idempotent. */\n async remove(uploadId: string): Promise<void> {\n this.records.delete(uploadId)\n const freed = await this.store.removeUpload(uploadId)\n this.usedBytes = Math.max(0, this.usedBytes - freed)\n }\n\n /**\n * Ghi `meta.json` cạnh part.\n *\n * Ghi tmp rồi rename để không bao giờ có một `meta.json` cụt: sweeper đọc phải file cụt\n * sẽ coi thư mục là rác và xoá một upload đang chạy dở.\n */\n private async persist(record: UploadRecord): Promise<void> {\n const dir = this.store.dirFor(record.uploadId)\n const tmp = join(dir, `${META_FILE}.tmp`)\n await writeFile(tmp, JSON.stringify(record), 'utf-8')\n await rename(tmp, join(dir, META_FILE))\n }\n\n /** Đọc `meta.json` của một thư mục — cho sweeper phân biệt upload sống với rác. */\n async readPersisted(uploadId: string): Promise<UploadRecord | undefined> {\n try {\n const raw = await readFile(join(this.store.dirFor(uploadId), META_FILE), 'utf-8')\n return JSON.parse(raw) as UploadRecord\n } catch {\n return undefined\n }\n }\n}\n","import type { PartDiskStore } from './PartDiskStore.ts'\nimport type { UploadRegistry } from './UploadRegistry.ts'\n\n/**\n * DỌN RÁC ĐỊNH KỲ cho chunk store.\n *\n * Ba việc, và việc thứ hai/thứ ba mới là lý do tồn tại của nó: một `setTimeout` per-upload\n * (kiểu `BinaryStreamBuffer` đang dùng cho WS) chỉ dọn được thứ nó biết. Process chết giữa\n * chừng thì timer chết theo, còn file thì ở lại đĩa mãi mãi.\n *\n * 1. Upload quá hạn → xoá record + part.\n * 2. Thư mục KHÔNG có record trong RAM và đã cũ → rác của một process đã chết, xoá.\n * 3. Định kỳ đối chiếu bộ đếm byte với đĩa thật → counter trôi sau crash, mà counter trôi\n * thì ngân sách âm thầm khoá mọi upload và không có gì trong log nói tại sao.\n */\n\nexport interface UploadSweeperOptions {\n registry: UploadRegistry\n store: PartDiskStore\n intervalMs?: number\n /** Thư mục mồ côi phải cũ hơn ngần này mới xoá. Mặc định 2× idle timeout dài nhất. */\n orphanGraceMs?: number\n /** Bao nhiêu tick thì đối chiếu lại bộ đếm với đĩa (quét `du` không rẻ). */\n reconcileEvery?: number\n}\n\nexport interface SweepResult {\n expired: number\n orphans: number\n reconciledBytes?: number\n}\n\nexport class UploadSweeper {\n private readonly registry: UploadRegistry\n private readonly store: PartDiskStore\n private readonly intervalMs: number\n private readonly orphanGraceMs: number\n private readonly reconcileEvery: number\n\n private timer?: ReturnType<typeof setInterval>\n private ticks = 0\n\n constructor(options: UploadSweeperOptions) {\n this.registry = options.registry\n this.store = options.store\n this.intervalMs = options.intervalMs ?? 60_000\n this.orphanGraceMs = options.orphanGraceMs ?? 20 * 60_000\n this.reconcileEvery = options.reconcileEvery ?? 10\n }\n\n /** Bắt đầu quét. `unref()` để timer dọn dẹp không giữ process sống. */\n start(): void {\n if (this.timer) return\n this.timer = setInterval(() => {\n // Không `await`: đây là timer, không có ai để `catch`. Nuốt lỗi tại chỗ và log,\n // vì một lần quét hỏng không được phép làm chết process.\n this.sweep().catch((err) => console.error('[saul][upload] sweep lỗi:', err))\n }, this.intervalMs)\n if (typeof (this.timer as any).unref === 'function') (this.timer as any).unref()\n }\n\n stop(): void {\n if (!this.timer) return\n clearInterval(this.timer)\n this.timer = undefined\n }\n\n async sweep(now: number = Date.now()): Promise<SweepResult> {\n const result: SweepResult = { expired: 0, orphans: 0 }\n\n for (const record of this.registry.list()) {\n if (now > record.hardExpiresAt || now > record.expiresAt) {\n await this.registry.remove(record.uploadId)\n result.expired += 1\n }\n }\n\n // Thư mục cũ mà không có record trong RAM: hoặc process đã restart, hoặc record bị\n // xoá mà `rm` thất bại. Cả hai đều là rác — nhưng chỉ sau `orphanGraceMs`, vì một\n // upload vừa `ensureDir()` xong cũng chưa có part nào.\n for (const dirName of await this.store.listStaleDirs(this.orphanGraceMs, now)) {\n if (this.registry.peek(dirName)) continue\n await this.store.removeUpload(dirName).catch(() => undefined)\n result.orphans += 1\n }\n\n this.ticks += 1\n if (this.ticks % this.reconcileEvery === 0) {\n const actual = await this.store.totalBytes()\n this.registry.setDiskUsedBytes(actual)\n result.reconciledBytes = actual\n }\n\n return result\n }\n}\n","import { Readable } from 'stream'\n\nimport type { PartDiskStore } from './PartDiskStore.ts'\n\n/**\n * NỐI CÁC PART thành MỘT `Readable`, không buffer.\n *\n * `Readable.from(async generator)` cho backpressure đúng miễn phí: generator chỉ chạy tiếp\n * khi consumer kéo, nên tốc độ đọc đĩa tự khớp tốc độ ghi ra downstream. Và vì generator\n * chỉ mở part khi tới lượt, ở mọi thời điểm chỉ có **một** fd đang mở — RAM là\n * `highWaterMark`, không phụ thuộc file 8MB hay 8GB.\n *\n * `finally { rs.destroy() }` không phải cho đường thành công (stream tự đóng khi hết) mà cho\n * đường HUỶ: consumer `destroy()` giữa chừng sẽ ném vào generator, và không có `finally` thì\n * part đang đọc dở giữ fd tới tận lúc GC.\n */\nexport function concatParts(\n store: PartDiskStore,\n uploadId: string,\n partNumbers: number[],\n partCount: number\n): Readable {\n async function* generate(): AsyncGenerator<Buffer> {\n for (const partNumber of partNumbers) {\n const source = store.createPartStream(uploadId, partNumber, partCount)\n try {\n for await (const chunk of source) {\n yield chunk as Buffer\n }\n } finally {\n source.destroy()\n }\n }\n }\n\n return Readable.from(generate())\n}\n","import { Hono } from 'hono'\nimport { Readable } from 'stream'\nimport { tmpdir } from 'os'\nimport { join } from 'path'\n\nimport { FileStream } from '../models/fileStream.ts'\nimport { concatParts } from './concatParts.ts'\nimport { PartDiskStore } from './PartDiskStore.ts'\nimport { UploadError, uploadErrorStatus } from './UploadError.ts'\nimport { UploadRegistry, type UploadRecord } from './UploadRegistry.ts'\nimport { UploadSweeper } from './UploadSweeper.ts'\n\n/**\n * SUB-ROUTER `/saul/upload/*` — phía DOWNSTREAM của multipart upload.\n *\n * Gateway không lưu byte nào; nó bơm thẳng từng part sang đây. Nên đây mới là nơi part nằm\n * trên đĩa và được ghép lại lúc complete.\n *\n * Router này KHÔNG tự gác quyền. Nó tin gateway, đúng như `POST /saul` đang tin — quyền đã\n * được chốt ở `/upload/init` của gateway, trước byte đầu tiên. Đặt thêm một tầng authz ở đây\n * mà không có token của user thì cũng chỉ là kiểm lại chính mình.\n *\n * Chỉ ba biến env, và cả ba đều nói về TÀI NGUYÊN CỦA MÁY NÀY. Mọi thứ thuộc về chính sách\n * (deadline, chunk size, partCount) do gateway truyền xuống trong lời gọi init.\n */\n\nexport interface UploadRouterOptions {\n /** Gọi hàm đích sau khi đã ghép file. Runtime truyền `dispatch` của nó vào đây. */\n dispatch: (body: { module: string; service?: string; function: string; params: Record<string, unknown> }) => Promise<unknown>\n rootDir?: string\n maxDiskBytes?: number\n fsync?: boolean\n /** Trần cứng một part. Gateway cũng gác, nhưng ở đây mới đếm byte THẬT. */\n maxPartBytes?: number\n sweepIntervalMs?: number\n}\n\nconst envInt = (key: string, fallback: number): number => {\n const raw = process.env[key]\n const n = raw ? Number.parseInt(raw, 10) : NaN\n return Number.isFinite(n) && n > 0 ? n : fallback\n}\nconst envBool = (key: string, fallback: boolean): boolean => {\n const raw = process.env[key]\n if (raw === undefined) return fallback\n return raw === 'true' || raw === '1'\n}\n\nexport interface UploadSubsystem {\n router: Hono\n registry: UploadRegistry\n store: PartDiskStore\n sweeper: UploadSweeper\n}\n\nexport function createUploadSubsystem(options: UploadRouterOptions): UploadSubsystem {\n const store = new PartDiskStore({\n rootDir: options.rootDir ?? process.env.SAUL_UPLOAD_TMP_DIR ?? join(tmpdir(), 'saul-uploads'),\n fsync: options.fsync ?? envBool('SAUL_UPLOAD_FSYNC', true),\n })\n const registry = new UploadRegistry({\n store,\n maxDiskBytes:\n options.maxDiskBytes ?? envInt('SAUL_UPLOAD_MAX_DISK_BYTES', 20 * 1024 * 1024 * 1024),\n })\n const sweeper = new UploadSweeper({ registry, store, intervalMs: options.sweepIntervalMs })\n const maxPartBytes = options.maxPartBytes ?? envInt('SAUL_UPLOAD_MAX_PART_BYTES', 32 * 1024 * 1024)\n\n const router = new Hono()\n\n router.onError((err, c) => {\n if (UploadError.is(err)) {\n return c.json({ error: err.message, code: err.code }, uploadErrorStatus(err.code) as any)\n }\n console.error('[saul][upload] lỗi không xác định:', err)\n return c.json({ error: err instanceof Error ? err.message : String(err) }, 500)\n })\n\n /**\n * Gateway sinh `uploadId` và áp đặt xuống — downstream không tự đặt id, nhờ vậy không\n * phải map hai id ở hai bên. Deadline cũng do gateway quyết: chính sách nằm ở\n * `remote_config` của Saul, downstream chỉ thi hành.\n */\n router.post('/init', async (c) => {\n const body = (await c.req.json()) as Omit<UploadRecord, 'state' | 'createdAt' | 'expiresAt'>\n const record = await registry.init(body)\n return c.json({ uploadId: record.uploadId, expiresAt: record.expiresAt })\n })\n\n router.put('/:uploadId/part/:partNumber', async (c) => {\n const uploadId = c.req.param('uploadId')\n const partNumber = Number(c.req.param('partNumber'))\n const record = registry.get(uploadId)\n\n const contentLength = c.req.header('content-length')\n if (!contentLength) {\n throw new UploadError('SIZE_MISMATCH', 'Thiếu content-length — part phải khai độ dài')\n }\n\n const before = (await store.listParts(uploadId)).find((p) => p.partNumber === partNumber)\n\n // `c.req.raw.body` là stream của socket. KHÔNG đọc nó thành buffer ở bất kỳ đâu —\n // một lần chạm là cả part vào RAM.\n const source = c.req.raw.body\n if (!source) {\n throw new UploadError('SIZE_MISMATCH', 'Request không có body')\n }\n\n const result = await store.writePart(uploadId, partNumber, Readable.fromWeb(source as any), {\n maxBytes: maxPartBytes,\n partCount: record.partCount,\n declaredLength: Number(contentLength),\n expectedSha256: c.req.header('x-saul-part-sha256'),\n })\n\n // Hiệu số, không phải `result.size`: PUT lại một part là ghi đè, cộng nguyên size\n // mỗi lần retry sẽ thổi phồng bộ đếm cho tới khi ngân sách khoá cả upload lành lặn.\n registry.touch(uploadId, result.size - (before?.size ?? 0))\n\n return c.json({ partNumber, size: result.size, etag: result.etag })\n })\n\n router.get('/:uploadId', async (c) => {\n const uploadId = c.req.param('uploadId')\n const record = registry.get(uploadId, ['open', 'completing'])\n const parts = await store.listParts(uploadId)\n\n return c.json({\n uploadId,\n size: record.file.size,\n chunkSize: record.chunkSize,\n partCount: record.partCount,\n received: parts,\n expiresAt: record.expiresAt,\n })\n })\n\n /**\n * Ghép part → dựng `FileStream` → gọi handler → trả về ĐÚNG giá trị handler trả.\n *\n * `ctx` tới ở ĐÂY chứ không phải ở init: nó phải phản ánh trạng thái quyền tại thời điểm\n * chạy handler, không phải lúc bắt đầu một upload có thể kéo dài 40 phút.\n */\n router.post('/:uploadId/complete', async (c) => {\n const uploadId = c.req.param('uploadId')\n const { ctx } = (await c.req.json().catch(() => ({}))) as { ctx?: unknown }\n const record = registry.beginComplete(uploadId)\n\n try {\n const parts = await store.listParts(uploadId)\n const received = new Set(parts.map((p) => p.partNumber))\n const missing: number[] = []\n for (let n = 1; n <= record.partCount; n += 1) {\n if (!received.has(n)) missing.push(n)\n }\n if (missing.length > 0) {\n throw new UploadError(\n 'PART_MISSING',\n `Thiếu part: ${missing.slice(0, 10).join(', ')}${missing.length > 10 ? '…' : ''}`\n )\n }\n\n const total = parts.reduce((sum, p) => sum + p.size, 0)\n // Fail-closed, cùng lý lẽ với `BinaryStreamBuffer.end()`: dispatch một file cụt\n // nghĩa là handler nhận dữ liệu sai mà không có gì báo.\n if (record.file.size > 0 && total !== record.file.size) {\n throw new UploadError(\n 'SIZE_MISMATCH',\n `Nhận ${total} byte, khai báo ${record.file.size}`\n )\n }\n\n const ordered = parts.map((p) => p.partNumber)\n const fileStream = new FileStream(record.file, () =>\n concatParts(store, uploadId, ordered, record.partCount)\n )\n\n const result = await options.dispatch({\n module: record.module,\n ...(record.service ? { service: record.service } : {}),\n function: record.function,\n params: { ...record.params, [record.param]: fileStream, ctx },\n })\n\n return c.json(result as any)\n } catch (err) {\n // Trả về `open` để client retry được mà không mất part đã gửi. Nếu để nguyên\n // `completing` thì một lỗi tạm thời của handler biến thành phải upload lại 3GB.\n registry.rollbackComplete(uploadId)\n throw err\n } finally {\n // Dọn part dù thành công hay thất bại — trừ khi còn ở `completing` (đã rollback,\n // client sẽ thử lại).\n if (registry.peek(uploadId)?.state !== 'open') {\n await registry.remove(uploadId).catch(() => undefined)\n }\n }\n })\n\n router.delete('/:uploadId', async (c) => {\n // Idempotent: xoá cái không tồn tại vẫn 204, để gọi được từ `sendBeacon` lúc đóng tab.\n await registry.remove(c.req.param('uploadId')).catch(() => undefined)\n return c.body(null, 204)\n })\n\n return { router, registry, store, sweeper }\n}\n"]}
|