@fidt/saul-server 1.2.3 → 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 +28 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -74,6 +74,13 @@ declare class MachineTokenProvider {
|
|
|
74
74
|
/** Envelope một lời gọi gateway đẩy xuống. */
|
|
75
75
|
interface TunnelDispatchBody {
|
|
76
76
|
module: string;
|
|
77
|
+
/**
|
|
78
|
+
* Tên service KHAI BÁO trong definition (không phải tên class cài đặt).
|
|
79
|
+
*
|
|
80
|
+
* Optional vì gateway cũ không gửi: runtime tự resolve theo `module.function` khi tên
|
|
81
|
+
* function là duy nhất trong module, và báo `AMBIGUOUS_FUNCTION` khi không.
|
|
82
|
+
*/
|
|
83
|
+
service?: string;
|
|
77
84
|
function: string;
|
|
78
85
|
params: Record<string, any>;
|
|
79
86
|
}
|
|
@@ -132,6 +139,17 @@ declare class TunnelAgent {
|
|
|
132
139
|
private agentId?;
|
|
133
140
|
private readonly blobBaseUrl;
|
|
134
141
|
private readonly inlineMaxBytes;
|
|
142
|
+
/**
|
|
143
|
+
* Lần bắt tay sau phải BỎ QUA cache token.
|
|
144
|
+
*
|
|
145
|
+
* 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,
|
|
146
|
+
* và `MachineTokenProvider` thì chỉ nhìn `exp` nên vẫn vui vẻ trả lại đúng cái token vừa
|
|
147
|
+
* bị chối — nối lại bao nhiêu lần cũng hỏng y hệt.
|
|
148
|
+
*/
|
|
149
|
+
private forceTokenRefresh;
|
|
150
|
+
/** Số lần nối lại hỏng liên tiếp; reset khi đăng ký lại được. */
|
|
151
|
+
private reconnectFailures;
|
|
152
|
+
private lastReconnectLogAt;
|
|
135
153
|
constructor(options: TunnelAgentOptions);
|
|
136
154
|
get connected(): boolean;
|
|
137
155
|
/**
|
|
@@ -188,6 +206,13 @@ declare class TunnelAgent {
|
|
|
188
206
|
/** Đẩy kết quả lớn lên gateway, trả về id để gửi trong ack. */
|
|
189
207
|
private pushBlob;
|
|
190
208
|
private fetchWithToken;
|
|
209
|
+
/**
|
|
210
|
+
* Log một lần nối lại hỏng, có tiết chế theo `RECONNECT_LOG_INTERVAL_MS`.
|
|
211
|
+
*
|
|
212
|
+
* Dùng `console.error` chứ không `warn`: agent không nối lại được là service đang chạy mà
|
|
213
|
+
* 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.
|
|
214
|
+
*/
|
|
215
|
+
private logReconnectFailure;
|
|
191
216
|
private startHeartbeat;
|
|
192
217
|
private stopHeartbeat;
|
|
193
218
|
private emitWithAck;
|
|
@@ -474,6 +499,8 @@ type UploadState = 'open' | 'completing' | 'completed';
|
|
|
474
499
|
interface UploadRecord {
|
|
475
500
|
uploadId: string;
|
|
476
501
|
module: string;
|
|
502
|
+
/** Service khai báo trong definition. Vắng khi gateway cũ khởi tạo upload. */
|
|
503
|
+
service?: string;
|
|
477
504
|
function: string;
|
|
478
505
|
/** Tên param kiểu FileStream của function đích. */
|
|
479
506
|
param: string;
|
|
@@ -626,6 +653,7 @@ interface UploadRouterOptions {
|
|
|
626
653
|
/** Gọi hàm đích sau khi đã ghép file. Runtime truyền `dispatch` của nó vào đây. */
|
|
627
654
|
dispatch: (body: {
|
|
628
655
|
module: string;
|
|
656
|
+
service?: string;
|
|
629
657
|
function: string;
|
|
630
658
|
params: Record<string, unknown>;
|
|
631
659
|
}) => Promise<unknown>;
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ declare class MachineTokenProvider {
|
|
|
74
74
|
/** Envelope một lời gọi gateway đẩy xuống. */
|
|
75
75
|
interface TunnelDispatchBody {
|
|
76
76
|
module: string;
|
|
77
|
+
/**
|
|
78
|
+
* Tên service KHAI BÁO trong definition (không phải tên class cài đặt).
|
|
79
|
+
*
|
|
80
|
+
* Optional vì gateway cũ không gửi: runtime tự resolve theo `module.function` khi tên
|
|
81
|
+
* function là duy nhất trong module, và báo `AMBIGUOUS_FUNCTION` khi không.
|
|
82
|
+
*/
|
|
83
|
+
service?: string;
|
|
77
84
|
function: string;
|
|
78
85
|
params: Record<string, any>;
|
|
79
86
|
}
|
|
@@ -132,6 +139,17 @@ declare class TunnelAgent {
|
|
|
132
139
|
private agentId?;
|
|
133
140
|
private readonly blobBaseUrl;
|
|
134
141
|
private readonly inlineMaxBytes;
|
|
142
|
+
/**
|
|
143
|
+
* Lần bắt tay sau phải BỎ QUA cache token.
|
|
144
|
+
*
|
|
145
|
+
* 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,
|
|
146
|
+
* và `MachineTokenProvider` thì chỉ nhìn `exp` nên vẫn vui vẻ trả lại đúng cái token vừa
|
|
147
|
+
* bị chối — nối lại bao nhiêu lần cũng hỏng y hệt.
|
|
148
|
+
*/
|
|
149
|
+
private forceTokenRefresh;
|
|
150
|
+
/** Số lần nối lại hỏng liên tiếp; reset khi đăng ký lại được. */
|
|
151
|
+
private reconnectFailures;
|
|
152
|
+
private lastReconnectLogAt;
|
|
135
153
|
constructor(options: TunnelAgentOptions);
|
|
136
154
|
get connected(): boolean;
|
|
137
155
|
/**
|
|
@@ -188,6 +206,13 @@ declare class TunnelAgent {
|
|
|
188
206
|
/** Đẩy kết quả lớn lên gateway, trả về id để gửi trong ack. */
|
|
189
207
|
private pushBlob;
|
|
190
208
|
private fetchWithToken;
|
|
209
|
+
/**
|
|
210
|
+
* Log một lần nối lại hỏng, có tiết chế theo `RECONNECT_LOG_INTERVAL_MS`.
|
|
211
|
+
*
|
|
212
|
+
* Dùng `console.error` chứ không `warn`: agent không nối lại được là service đang chạy mà
|
|
213
|
+
* 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.
|
|
214
|
+
*/
|
|
215
|
+
private logReconnectFailure;
|
|
191
216
|
private startHeartbeat;
|
|
192
217
|
private stopHeartbeat;
|
|
193
218
|
private emitWithAck;
|
|
@@ -474,6 +499,8 @@ type UploadState = 'open' | 'completing' | 'completed';
|
|
|
474
499
|
interface UploadRecord {
|
|
475
500
|
uploadId: string;
|
|
476
501
|
module: string;
|
|
502
|
+
/** Service khai báo trong definition. Vắng khi gateway cũ khởi tạo upload. */
|
|
503
|
+
service?: string;
|
|
477
504
|
function: string;
|
|
478
505
|
/** Tên param kiểu FileStream của function đích. */
|
|
479
506
|
param: string;
|
|
@@ -626,6 +653,7 @@ interface UploadRouterOptions {
|
|
|
626
653
|
/** Gọi hàm đích sau khi đã ghép file. Runtime truyền `dispatch` của nó vào đây. */
|
|
627
654
|
dispatch: (body: {
|
|
628
655
|
module: string;
|
|
656
|
+
service?: string;
|
|
629
657
|
function: string;
|
|
630
658
|
params: Record<string, unknown>;
|
|
631
659
|
}) => Promise<unknown>;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import {Hono}from'hono';export*from'hono';export{Hono as WebServer}from'hono';export{serve}from'@hono/node-server';export*from'socket.io';export{createAdapter}from'@socket.io/redis-adapter';import {io}from'socket.io-client';export{io as connectSocket}from'socket.io-client';import {hostname,tmpdir}from'os';import*as zod from'zod';export{zod as Zod };import {Readable,Transform}from'stream';import {pipeline}from'stream/promises';import {createHash}from'crypto';import {createReadStream}from'fs';import {mkdir,open,unlink,rename,readdir,stat,rm,writeFile,readFile}from'fs/promises';import {join}from'path';export*from'zod-validation-error';// @fidt/saul-server - A server library for the Saul framework
|
|
2
|
-
var S=class{constructor(e){this.options=e;}options;cachedToken=null;expiresAt=0;inflight=null;async getToken(e=false){return this.options.machineToken?this.options.machineToken:!e&&this.cachedToken&&Date.now()<this.expiresAt?this.cachedToken:this.inflight?this.inflight:(this.inflight=this.requestToken().finally(()=>{this.inflight=null;}),this.inflight)}reset(){this.cachedToken=null,this.expiresAt=0;}async requestToken(){let{clientId:e,clientSecret:t}=this.options,r=this.options.oidcTokenUrl||C(),n=[];if(r||n.push("oidcTokenUrl (ho\u1EB7c env OIDC_TOKEN_URL)"),e||n.push("clientId"),t||n.push("clientSecret"),n.length)throw new Error(`Machine token thi\u1EBFu c\u1EA5u h\xECnh: ${n.join(", ")}`);let s=this.options.resource||this.options.fallbackResource||"",o=new URLSearchParams({grant_type:"client_credentials",client_id:e,client_secret:t,scope:this.options.scope??"access",resource:s}).toString(),d=await(this.options.fetchImpl??H())(r,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:o}),c=await d.json().catch(()=>{});if(!d.ok){let f=c&&(c.error_description||c.error)||"kh\xF4ng r\xF5 nguy\xEAn nh\xE2n";throw new Error(`OIDC token request th\u1EA5t b\u1EA1i (${d.status}): ${f}`)}if(!c||typeof c.access_token!="string")throw new Error("OIDC token response kh\xF4ng c\xF3 access_token");let p=typeof c.expires_in=="number"?c.expires_in:300;return this.cachedToken=c.access_token,this.expiresAt=Date.now()+Math.max(p*1e3-6e4,0),this.cachedToken}},C=()=>globalThis.process?.env?.OIDC_TOKEN_URL,H=()=>{let i=globalThis.fetch;if(typeof i!="function")throw new Error("FETCH_UNAVAILABLE: runtime kh\xF4ng c\xF3 global fetch (c\u1EA7n Node >= 18)");return i};var E=class i extends Error{code;constructor(e,t="SAUL_EXCEPTION"){super(e),this.name="SaulException",this.code=t,Object.setPrototypeOf(this,new.target.prototype);}static is(e){return e instanceof i||e instanceof Error&&e.name==="SaulException"}};var j="saul:agent:dispatch",q="saul:agent:register",W="saul:agent:heartbeat",K="saul:agent:upload",D=i=>i.replace(/^ws(s?):\/\//i,"http$1://"),V=1e4,X=256*1024,I="/saul/upload",Y=i=>{if(!i.startsWith(I))return i;let e=i.slice(I.length);return e.startsWith("/")?e:`/${e}`},R=class{constructor(e){this.options=e;this.tokens=new S({...e,fallbackResource:e.fallbackResource??e.gatewayUrl}),this.instanceId=e.instanceId?.trim()||`${hostname()}-${process.pid}`,this.blobBaseUrl=D(e.gatewayUrl).replace(/\/+$/,""),this.inlineMaxBytes=e.inlineMaxBytes??X;}options;socket;heartbeatTimer;tokens;instanceId;closed=false;agentId;blobBaseUrl;inlineMaxBytes;get connected(){return this.socket?.connected===true}async connect(){this.closed=false;let e=await this.tokens.getToken(),t=this.options.socketFactory??((a,d)=>io(a,d)),{origin:r,socketPath:n}=this.resolveEndpoint(),s=t(r,{auth:{token:e},path:n,transports:["websocket"],reconnection:true,randomizationFactor:.5,reconnectionDelay:1e3,reconnectionDelayMax:3e4});this.socket=s;let o=new Promise((a,d)=>{let c=false;s.on("connect",()=>{this.register().then(()=>{c||(c=true,a());}).catch(p=>{if(!c){c=true,d(p);return}console.error("[saul][tunnel] \u0111\u0103ng k\xFD l\u1EA1i th\u1EA5t b\u1EA1i:",p.message);});}),s.on("connect_error",p=>{c||(c=true,d(new Error(`Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c gateway '${r}${n??""}': ${p.message}. Ki\u1EC3m: gateway c\xF3 \u0111ang ch\u1EA1y kh\xF4ng, reverse proxy \u0111\xE3 cho ph\xE9p n\xE2ng c\u1EA5p WebSocket ch\u01B0a, v\xE0 WS_PATH c\u1EE7a gateway c\xF3 kh\u1EDBp kh\xF4ng (\u0111\u1EB7t option 'path' \u0111\u1EC3 ch\u1EC9 \u0111\u1ECBnh tay).`)));});});s.on(j,(a,d)=>{this.handleDispatch(a,d);}),s.on(K,(a,d)=>{this.handleUpload(a,d);}),s.on("disconnect",a=>{console.warn(`[saul][tunnel] m\u1EA5t k\u1EBFt n\u1ED1i t\u1EDBi gateway (${a}) \u2014 \u0111ang th\u1EED n\u1ED1i l\u1EA1i`);}),await o,this.startHeartbeat();}async close(){this.closed=true,this.stopHeartbeat(),this.socket?.disconnect(),this.socket=void 0;}resolveEndpoint(){try{let e=new URL(D(this.options.gatewayUrl)),t=e.pathname.replace(/\/+$/,"");return {origin:e.origin,socketPath:this.options.path??`${t}/socket.io`}}catch{return {origin:this.options.gatewayUrl,socketPath:this.options.path}}}async register(){let e=await this.emitWithAck(q,{instanceId:this.instanceId,modules:this.options.modules,capabilities:this.options.capabilities??[],version:this.options.version});if(!e?.ok)throw new Error(`Gateway t\u1EEB ch\u1ED1i \u0111\u0103ng k\xFD agent: ${e?.error??"kh\xF4ng r\xF5 l\xFD do"}${e?.code?` [${e.code}]`:""}`);let t=e.data??{};this.agentId=t.agentId,Array.isArray(t.denied)&&t.denied.length&&console.warn("[saul][tunnel] module b\u1ECB t\u1EEB ch\u1ED1i:",t.denied.map(r=>`${r.module} (${r.code})`).join(", ")),console.log(`[saul][tunnel] agent \u0111\xE3 k\u1EBFt n\u1ED1i gateway: instanceId=${this.instanceId}, version=${this.options.version??"?"}, capabilities=${JSON.stringify(this.options.capabilities??[])}, modules=${JSON.stringify(t.accepted??[])}, agentId=${t.agentId}, ip=${t.ip??"?"}, gateway=${this.options.gatewayUrl}`);}async handleDispatch(e,t){try{if(!e||typeof e.module!="string"||typeof e.function!="string")throw new Error("Envelope dispatch thi\u1EBFu module/function");let r=e.paramsBlob?await this.pullBlob(e.paramsBlob,e.module):e.params??{},n=await this.options.dispatch({module:e.module,function:e.function,params:r}),s=JSON.stringify(n??null);if(Buffer.byteLength(s,"utf8")>this.inlineMaxBytes){let o=await this.pushBlob(s,e.module);t?.({ok:!0,resultBlob:o});return}t?.({ok:!0,result:n});}catch(r){t?.({ok:false,error:r?.message??"L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh \u1EDF downstream",code:E.is(r)?r.code:void 0});}}async handleUpload(e,t){try{if(!this.options.uploadRouter)throw new Error("Service n\xE0y kh\xF4ng b\u1EADt multipart upload (thi\u1EBFu uploadRouter)");if(!e||typeof e.path!="string"||typeof e.method!="string")throw new Error("Envelope upload thi\u1EBFu method/path");let r=e.paramsBlob?(await this.pullBlob(e.paramsBlob,e.module)).body:e.params?.body??"",n=r?Buffer.from(String(r),"base64"):void 0,s=await this.options.uploadRouter.request(Y(e.path),{method:e.method,headers:e.headers??{},body:n&&e.method!=="GET"&&e.method!=="DELETE"?n:void 0}),o=await s.text(),a={status:s.status,body:o},d=JSON.stringify(a);if(Buffer.byteLength(d,"utf8")>this.inlineMaxBytes){t?.({ok:!0,resultBlob:await this.pushBlob(d,e.module)});return}t?.({ok:!0,result:a});}catch(r){t?.({ok:false,error:r?.message??"L\u1ED7i upload kh\xF4ng x\xE1c \u0111\u1ECBnh"});}}async pullBlob(e,t){let r=`${this.blobBaseUrl}/tunnel/blob/${encodeURIComponent(e)}?module=${encodeURIComponent(t)}&agentId=${encodeURIComponent(this.agentId??"")}`,n=await this.fetchWithToken(r,{method:"GET"});if(!n.ok){let s=await n.text().catch(()=>"");throw new Error(`Kh\xF4ng k\xE9o \u0111\u01B0\u1EE3c payload t\u1EEB gateway (${n.status}): ${s}`)}return JSON.parse(await n.text())}async pushBlob(e,t){let r=await this.fetchWithToken(`${this.blobBaseUrl}/tunnel/blob`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({module:t,agentId:this.agentId,data:e})}),n=await r.json().catch(()=>{});if(!r.ok||!n?.blobId)throw new Error(`Kh\xF4ng \u0111\u1EA9y \u0111\u01B0\u1EE3c k\u1EBFt qu\u1EA3 l\xEAn gateway (${r.status}): ${n?.error??"kh\xF4ng c\xF3 blobId"}`);return n.blobId}async fetchWithToken(e,t){let r=globalThis.fetch;if(typeof r!="function")throw new Error("FETCH_UNAVAILABLE (c\u1EA7n Node >= 18)");let n=await this.tokens.getToken();return await r(e,{...t,headers:{...t.headers??{},authorization:`Bearer ${n}`}})}startHeartbeat(){this.stopHeartbeat();let e=this.options.heartbeatMs??V;this.heartbeatTimer=setInterval(()=>{this.closed||!this.socket||this.socket.emit(W,{},t=>{t&&t.ok===false&&this.register().catch(r=>console.error("[saul][tunnel] \u0111\u0103ng k\xFD l\u1EA1i sau heartbeat th\u1EA5t b\u1EA1i:",r.message));});},e),this.heartbeatTimer.unref?.();}stopHeartbeat(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0;}emitWithAck(e,t){return new Promise(r=>{this.socket.emit(e,t,r);})}};var B=class{content;metadata;constructor(e){this.metadata=e||{name:"",size:0,type:""},this.content="";}encodeBase64(e){return Buffer.from(e).toString("base64")}decodeBase64(e){let t=Buffer.from(e,"base64"),r=new Uint8Array(t.length);return t.copy(r),r.buffer}read(){if(!this.content)throw new Error("File content is not set");return this.decodeBase64(this.content)}write(e){this.content=this.encodeBase64(e);}};var O=Symbol.for("@fidt/saul.FileStream"),T=class i{[O]=true;metadata;source;consumed=false;constructor(e,t){this.metadata=e,this.source=t;}stream(){if(this.consumed)throw new Error(`FILE_STREAM_CONSUMED: '${this.metadata.name}' \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\u1ECDc r\u1ED3i \u2014 m\u1ED9t FileStream ch\u1EC9 ti\xEAu th\u1EE5 \u0111\u01B0\u1EE3c m\u1ED9t l\u1EA7n`);return this.consumed=true,this.source()}get isConsumed(){return this.consumed}async pipeTo(e){await pipeline(this.stream(),e);}async buffer(e){let t=[],r=0;for await(let n of this.stream()){let s=Buffer.isBuffer(n)?n:Buffer.from(n);if(r+=s.length,r>e)throw new Error(`FILE_STREAM_TOO_LARGE: '${this.metadata.name}' v\u01B0\u1EE3t ${e} byte khi buffer()`);t.push(s);}return Buffer.concat(t,r)}static is(e){return typeof e=="object"&&e!==null&&e[O]===true}static fromBuffer(e,t){return new i({...e,size:e.size??t.length},()=>Readable.from(t))}};var Ie=(...i)=>{let e=i.filter(t=>!!t&&Object.keys(t).length>0);return e.length===0?{}:e.length===1?e[0]:{$and:e}};var h=class i extends Error{constructor(t,r){super(r);this.code=t;this.name="UploadError";}code;static is(t){return t instanceof i}},Q={UPLOAD_NOT_FOUND:404,UPLOAD_EXISTS:409,UPLOAD_EXPIRED:410,UPLOAD_COMPLETED:409,UPLOAD_BUSY:409,PART_OUT_OF_RANGE:416,PART_IN_FLIGHT:409,PART_TOO_LARGE:413,PART_MISSING:422,SIZE_MISMATCH:422,CHECKSUM_MISMATCH:422,DISK_BUDGET:503,FORBIDDEN:403},M=i=>Q[i]??500;var ce=1024*1024,P=class{rootDir;fsync;constructor(e){this.rootDir=e.rootDir,this.fsync=e.fsync??true;}dirFor(e){return de(e),join(this.rootDir,e)}pathFor(e,t,r){return he(t,r),join(this.dirFor(e),`${String(t).padStart(6,"0")}.part`)}async ensureDir(e){await mkdir(this.dirFor(e),{recursive:true});}async writePart(e,t,r,n){let s=this.pathFor(e,t,n.partCount),o=`${s}.tmp`;await this.ensureDir(e);let a;try{a=await open(o,"wx");}catch(u){throw u?.code==="EEXIST"?new h("PART_IN_FLIGHT",`Part ${t} c\u1EE7a '${e}' \u0111ang \u0111\u01B0\u1EE3c ghi b\u1EDFi m\u1ED9t request kh\xE1c`):u}let d=createHash("sha256"),c=0,p=n.declaredLength!==void 0?Math.min(n.maxBytes,n.declaredLength):n.maxBytes,f=new Transform({transform(u,b,k){if(c+=u.length,c>p){k(new h("PART_TOO_LARGE",`Part ${t} v\u01B0\u1EE3t ${p} byte`));return}d.update(u),k(null,u);}});try{await pipeline(r,f,a.createWriteStream()),this.fsync&&await a.sync();}catch(u){throw await a.close().catch(()=>{}),await unlink(o).catch(()=>{}),u}if(await a.close(),n.declaredLength!==void 0&&c!==n.declaredLength)throw await unlink(o).catch(()=>{}),new h("SIZE_MISMATCH",`Part ${t}: nh\u1EADn ${c} byte, content-length khai ${n.declaredLength}`);let l=d.digest("hex");if(n.expectedSha256&&n.expectedSha256!==l)throw await unlink(o).catch(()=>{}),new h("CHECKSUM_MISMATCH",`Part ${t}: sha256 kh\xF4ng kh\u1EDBp (khai ${n.expectedSha256}, th\u1EADt ${l})`);return await rename(o,s),{size:c,etag:l}}createPartStream(e,t,r){return createReadStream(this.pathFor(e,t,r),{highWaterMark:ce})}async listParts(e){let t;try{t=await readdir(this.dirFor(e));}catch(n){if(n?.code==="ENOENT")return [];throw n}let r=[];for(let n of t){let s=/^(\d{6})\.part$/.exec(n);if(!s)continue;let o=await stat(join(this.dirFor(e),n));r.push({partNumber:Number.parseInt(s[1],10),size:o.size});}return r.sort((n,s)=>n.partNumber-s.partNumber)}async removeUpload(e){let t=this.dirFor(e),r=0;try{for(let n of await readdir(t)){let s=await stat(join(t,n)).catch(()=>{});s&&(r+=s.size);}}catch(n){if(n?.code==="ENOENT")return 0;throw n}return await rm(t,{recursive:true,force:true}),r}async listStaleDirs(e,t=Date.now()){let r;try{r=await readdir(this.rootDir);}catch(s){if(s?.code==="ENOENT")return [];throw s}let n=[];for(let s of r){let o=await stat(join(this.rootDir,s)).catch(()=>{});o?.isDirectory()&&t-o.mtimeMs>e&&n.push(s);}return n}async totalBytes(){let e;try{e=await readdir(this.rootDir);}catch(r){if(r?.code==="ENOENT")return 0;throw r}let t=0;for(let r of e){let n=join(this.rootDir,r);if((await stat(n).catch(()=>{}))?.isDirectory())for(let o of await readdir(n).catch(()=>[])){let a=await stat(join(n,o)).catch(()=>{});a?.isFile()&&(t+=a.size);}}return t}};function de(i){if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(i))throw new h("FORBIDDEN",`uploadId kh\xF4ng h\u1EE3p l\u1EC7: '${i}'`)}function he(i,e){if(!Number.isSafeInteger(i)||i<1||i>e)throw new h("PART_OUT_OF_RANGE",`partNumber ${i} ngo\xE0i d\u1EA3i 1..${e}`)}var _="meta.json",x=class{records=new Map;store;maxDiskBytes;usedBytes=0;constructor(e){this.store=e.store,this.maxDiskBytes=e.maxDiskBytes;}get diskUsedBytes(){return this.usedBytes}setDiskUsedBytes(e){this.usedBytes=Math.max(0,e);}async init(e){if(this.records.has(e.uploadId))throw new h("UPLOAD_EXISTS",`Upload '${e.uploadId}' \u0111\xE3 t\u1ED3n t\u1EA1i`);if(this.usedBytes+e.file.size>this.maxDiskBytes)throw new h("DISK_BUDGET",`H\u1EBFt ng\xE2n s\xE1ch \u0111\u0129a (${this.usedBytes}/${this.maxDiskBytes} byte) \u2014 th\u1EED l\u1EA1i sau`);let t=Date.now(),r={...e,state:"open",createdAt:t,expiresAt:Math.min(t+e.idleTimeoutMs,e.hardExpiresAt)};return this.records.set(r.uploadId,r),await this.store.ensureDir(r.uploadId),await this.persist(r),r}get(e,t=["open"]){let r=this.records.get(e);if(!r)throw new h("UPLOAD_NOT_FOUND",`Upload '${e}' kh\xF4ng t\u1ED3n t\u1EA1i`);if(Date.now()>r.hardExpiresAt||Date.now()>r.expiresAt)throw new h("UPLOAD_EXPIRED",`Upload '${e}' \u0111\xE3 h\u1EBFt h\u1EA1n`);if(!t.includes(r.state))throw new h(r.state==="completed"?"UPLOAD_COMPLETED":"UPLOAD_BUSY",`Upload '${e}' \u0111ang \u1EDF tr\u1EA1ng th\xE1i '${r.state}'`);return r}peek(e){return this.records.get(e)}list(){return [...this.records.values()]}touch(e,t){let r=this.records.get(e);r&&(r.expiresAt=Math.min(Date.now()+r.idleTimeoutMs,r.hardExpiresAt),this.usedBytes=Math.max(0,this.usedBytes+t));}beginComplete(e){let t=this.get(e,["open"]);return t.state="completing",t}rollbackComplete(e){let t=this.records.get(e);t?.state==="completing"&&(t.state="open");}async remove(e){this.records.delete(e);let t=await this.store.removeUpload(e);this.usedBytes=Math.max(0,this.usedBytes-t);}async persist(e){let t=this.store.dirFor(e.uploadId),r=join(t,`${_}.tmp`);await writeFile(r,JSON.stringify(e),"utf-8"),await rename(r,join(t,_));}async readPersisted(e){try{let t=await readFile(join(this.store.dirFor(e),_),"utf-8");return JSON.parse(t)}catch{return}}};var v=class{registry;store;intervalMs;orphanGraceMs;reconcileEvery;timer;ticks=0;constructor(e){this.registry=e.registry,this.store=e.store,this.intervalMs=e.intervalMs??6e4,this.orphanGraceMs=e.orphanGraceMs??20*6e4,this.reconcileEvery=e.reconcileEvery??10;}start(){this.timer||(this.timer=setInterval(()=>{this.sweep().catch(e=>console.error("[saul][upload] sweep l\u1ED7i:",e));},this.intervalMs),typeof this.timer.unref=="function"&&this.timer.unref());}stop(){this.timer&&(clearInterval(this.timer),this.timer=void 0);}async sweep(e=Date.now()){let t={expired:0,orphans:0};for(let r of this.registry.list())(e>r.hardExpiresAt||e>r.expiresAt)&&(await this.registry.remove(r.uploadId),t.expired+=1);for(let r of await this.store.listStaleDirs(this.orphanGraceMs,e))this.registry.peek(r)||(await this.store.removeUpload(r).catch(()=>{}),t.orphans+=1);if(this.ticks+=1,this.ticks%this.reconcileEvery===0){let r=await this.store.totalBytes();this.registry.setDiskUsedBytes(r),t.reconciledBytes=r;}return t}};function $(i,e,t,r){async function*n(){for(let s of t){let o=i.createPartStream(e,s,r);try{for await(let a of o)yield a;}finally{o.destroy();}}}return Readable.from(n())}var L=(i,e)=>{let t=process.env[i],r=t?Number.parseInt(t,10):NaN;return Number.isFinite(r)&&r>0?r:e},be=(i,e)=>{let t=process.env[i];return t===void 0?e:t==="true"||t==="1"};function at(i){let e=new P({rootDir:i.rootDir??process.env.SAUL_UPLOAD_TMP_DIR??join(tmpdir(),"saul-uploads"),fsync:i.fsync??be("SAUL_UPLOAD_FSYNC",true)}),t=new x({store:e,maxDiskBytes:i.maxDiskBytes??L("SAUL_UPLOAD_MAX_DISK_BYTES",20*1024*1024*1024)}),r=new v({registry:t,store:e,intervalMs:i.sweepIntervalMs}),n=i.maxPartBytes??L("SAUL_UPLOAD_MAX_PART_BYTES",32*1024*1024),s=new Hono;return s.onError((o,a)=>h.is(o)?a.json({error:o.message,code:o.code},M(o.code)):(console.error("[saul][upload] l\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh:",o),a.json({error:o instanceof Error?o.message:String(o)},500))),s.post("/init",async o=>{let a=await o.req.json(),d=await t.init(a);return o.json({uploadId:d.uploadId,expiresAt:d.expiresAt})}),s.put("/:uploadId/part/:partNumber",async o=>{let a=o.req.param("uploadId"),d=Number(o.req.param("partNumber")),c=t.get(a),p=o.req.header("content-length");if(!p)throw new h("SIZE_MISMATCH","Thi\u1EBFu content-length \u2014 part ph\u1EA3i khai \u0111\u1ED9 d\xE0i");let f=(await e.listParts(a)).find(b=>b.partNumber===d),l=o.req.raw.body;if(!l)throw new h("SIZE_MISMATCH","Request kh\xF4ng c\xF3 body");let u=await e.writePart(a,d,Readable.fromWeb(l),{maxBytes:n,partCount:c.partCount,declaredLength:Number(p),expectedSha256:o.req.header("x-saul-part-sha256")});return t.touch(a,u.size-(f?.size??0)),o.json({partNumber:d,size:u.size,etag:u.etag})}),s.get("/:uploadId",async o=>{let a=o.req.param("uploadId"),d=t.get(a,["open","completing"]),c=await e.listParts(a);return o.json({uploadId:a,size:d.file.size,chunkSize:d.chunkSize,partCount:d.partCount,received:c,expiresAt:d.expiresAt})}),s.post("/:uploadId/complete",async o=>{let a=o.req.param("uploadId"),{ctx:d}=await o.req.json().catch(()=>({})),c=t.beginComplete(a);try{let p=await e.listParts(a),f=new Set(p.map(m=>m.partNumber)),l=[];for(let m=1;m<=c.partCount;m+=1)f.has(m)||l.push(m);if(l.length>0)throw new h("PART_MISSING",`Thi\u1EBFu part: ${l.slice(0,10).join(", ")}${l.length>10?"\u2026":""}`);let u=p.reduce((m,F)=>m+F.size,0);if(c.file.size>0&&u!==c.file.size)throw new h("SIZE_MISMATCH",`Nh\u1EADn ${u} byte, khai b\xE1o ${c.file.size}`);let b=p.map(m=>m.partNumber),k=new T(c.file,()=>$(e,a,b,c.partCount)),N=await i.dispatch({module:c.module,function:c.function,params:{...c.params,[c.param]:k,ctx:d}});return o.json(N)}catch(p){throw t.rollbackComplete(a),p}finally{t.peek(a)?.state!=="open"&&await t.remove(a).catch(()=>{});}}),s.delete("/:uploadId",async o=>(await t.remove(o.req.param("uploadId")).catch(()=>{}),o.body(null,204))),{router:s,registry:t,store:e,sweeper:r}}
|
|
3
|
-
export{B as File,
|
|
2
|
+
var T=class{constructor(e){this.options=e;}options;cachedToken=null;expiresAt=0;inflight=null;async getToken(e=false){return this.options.machineToken?this.options.machineToken:!e&&this.cachedToken&&Date.now()<this.expiresAt?this.cachedToken:this.inflight?this.inflight:(this.inflight=this.requestToken().finally(()=>{this.inflight=null;}),this.inflight)}reset(){this.cachedToken=null,this.expiresAt=0;}async requestToken(){let{clientId:e,clientSecret:t}=this.options,r=this.options.oidcTokenUrl||C(),n=[];if(r||n.push("oidcTokenUrl (ho\u1EB7c env OIDC_TOKEN_URL)"),e||n.push("clientId"),t||n.push("clientSecret"),n.length)throw new Error(`Machine token thi\u1EBFu c\u1EA5u h\xECnh: ${n.join(", ")}`);let a=this.options.resource||this.options.fallbackResource||"",s=new URLSearchParams({grant_type:"client_credentials",client_id:e,client_secret:t,scope:this.options.scope??"access",resource:a}).toString(),h=await(this.options.fetchImpl??H())(r,{method:"POST",headers:{"content-type":"application/x-www-form-urlencoded"},body:s}),c=await h.json().catch(()=>{});if(!h.ok){let f=c&&(c.error_description||c.error)||"kh\xF4ng r\xF5 nguy\xEAn nh\xE2n";throw new Error(`OIDC token request th\u1EA5t b\u1EA1i (${h.status}): ${f}`)}if(!c||typeof c.access_token!="string")throw new Error("OIDC token response kh\xF4ng c\xF3 access_token");let l=typeof c.expires_in=="number"?c.expires_in:300;return this.cachedToken=c.access_token,this.expiresAt=Date.now()+Math.max(l*1e3-6e4,0),this.cachedToken}},C=()=>globalThis.process?.env?.OIDC_TOKEN_URL,H=()=>{let i=globalThis.fetch;if(typeof i!="function")throw new Error("FETCH_UNAVAILABLE: runtime kh\xF4ng c\xF3 global fetch (c\u1EA7n Node >= 18)");return i};var E=class i extends Error{code;constructor(e,t="SAUL_EXCEPTION"){super(e),this.name="SaulException",this.code=t,Object.setPrototypeOf(this,new.target.prototype);}static is(e){return e instanceof i||e instanceof Error&&e.name==="SaulException"}};var W="saul:agent:dispatch",j="saul:agent:register",q="saul:agent:heartbeat",K="saul:agent:upload",_=i=>i.replace(/^ws(s?):\/\//i,"http$1://"),V=1e4,X=256*1024,Y=6e4,D="/saul/upload",J=i=>{if(!i.startsWith(D))return i;let e=i.slice(D.length);return e.startsWith("/")?e:`/${e}`},I=class{constructor(e){this.options=e;this.tokens=new T({...e,fallbackResource:e.fallbackResource??e.gatewayUrl}),this.instanceId=e.instanceId?.trim()||`${hostname()}-${process.pid}`,this.blobBaseUrl=_(e.gatewayUrl).replace(/\/+$/,""),this.inlineMaxBytes=e.inlineMaxBytes??X;}options;socket;heartbeatTimer;tokens;instanceId;closed=false;agentId;blobBaseUrl;inlineMaxBytes;forceTokenRefresh=false;reconnectFailures=0;lastReconnectLogAt=0;get connected(){return this.socket?.connected===true}async connect(){this.closed=false,await this.tokens.getToken();let e=this.options.socketFactory??((s,o)=>io(s,o)),{origin:t,socketPath:r}=this.resolveEndpoint(),n=e(t,{auth:s=>{this.tokens.getToken(this.forceTokenRefresh).then(o=>{this.forceTokenRefresh=false,s({token:o});}).catch(o=>{this.logReconnectFailure(`kh\xF4ng l\u1EA5y \u0111\u01B0\u1EE3c token m\xE1y: ${o.message}`),s({token:""});});},path:r,transports:["websocket"],reconnection:true,randomizationFactor:.5,reconnectionDelay:1e3,reconnectionDelayMax:3e4});this.socket=n;let a=new Promise((s,o)=>{let h=false;n.on("connect",()=>{this.register().then(()=>{h||(h=true,s());}).catch(c=>{if(!h){h=true,o(c);return}console.error("[saul][tunnel] \u0111\u0103ng k\xFD l\u1EA1i th\u1EA5t b\u1EA1i:",c.message);});}),n.on("connect_error",c=>{let l=c?.message??"kh\xF4ng r\xF5 l\xFD do";if(l.startsWith("WS_AUTH")&&(this.forceTokenRefresh=true),!h){h=true,o(new Error(`Kh\xF4ng k\u1EBFt n\u1ED1i \u0111\u01B0\u1EE3c gateway '${t}${r??""}': ${c.message}. Ki\u1EC3m: gateway c\xF3 \u0111ang ch\u1EA1y kh\xF4ng, reverse proxy \u0111\xE3 cho ph\xE9p n\xE2ng c\u1EA5p WebSocket ch\u01B0a, v\xE0 WS_PATH c\u1EE7a gateway c\xF3 kh\u1EDBp kh\xF4ng (\u0111\u1EB7t option 'path' \u0111\u1EC3 ch\u1EC9 \u0111\u1ECBnh tay).`));return}this.logReconnectFailure(`n\u1ED1i l\u1EA1i gateway '${t}${r??""}' th\u1EA5t b\u1EA1i: ${l}`);});});n.on(W,(s,o)=>{this.handleDispatch(s,o);}),n.on(K,(s,o)=>{this.handleUpload(s,o);}),n.on("disconnect",s=>{console.warn(`[saul][tunnel] m\u1EA5t k\u1EBFt n\u1ED1i t\u1EDBi gateway (${s}) \u2014 \u0111ang th\u1EED n\u1ED1i l\u1EA1i`);}),await a,this.startHeartbeat();}async close(){this.closed=true,this.stopHeartbeat(),this.socket?.disconnect(),this.socket=void 0;}resolveEndpoint(){try{let e=new URL(_(this.options.gatewayUrl)),t=e.pathname.replace(/\/+$/,"");return {origin:e.origin,socketPath:this.options.path??`${t}/socket.io`}}catch{return {origin:this.options.gatewayUrl,socketPath:this.options.path}}}async register(){let e=await this.emitWithAck(j,{instanceId:this.instanceId,modules:this.options.modules,capabilities:this.options.capabilities??[],version:this.options.version});if(!e?.ok)throw new Error(`Gateway t\u1EEB ch\u1ED1i \u0111\u0103ng k\xFD agent: ${e?.error??"kh\xF4ng r\xF5 l\xFD do"}${e?.code?` [${e.code}]`:""}`);let t=e.data??{};this.reconnectFailures=0,this.agentId=t.agentId,Array.isArray(t.denied)&&t.denied.length&&console.warn("[saul][tunnel] module b\u1ECB t\u1EEB ch\u1ED1i:",t.denied.map(r=>`${r.module} (${r.code})`).join(", ")),console.log(`[saul][tunnel] agent \u0111\xE3 k\u1EBFt n\u1ED1i gateway: instanceId=${this.instanceId}, version=${this.options.version??"?"}, capabilities=${JSON.stringify(this.options.capabilities??[])}, modules=${JSON.stringify(t.accepted??[])}, agentId=${t.agentId}, ip=${t.ip??"?"}, gateway=${this.options.gatewayUrl}`);}async handleDispatch(e,t){try{if(!e||typeof e.module!="string"||typeof e.function!="string")throw new Error("Envelope dispatch thi\u1EBFu module/function");let r=e.paramsBlob?await this.pullBlob(e.paramsBlob,e.module):e.params??{},n=await this.options.dispatch({module:e.module,...typeof e.service=="string"&&e.service?{service:e.service}:{},function:e.function,params:r}),a=JSON.stringify(n??null);if(Buffer.byteLength(a,"utf8")>this.inlineMaxBytes){let s=await this.pushBlob(a,e.module);t?.({ok:!0,resultBlob:s});return}t?.({ok:!0,result:n});}catch(r){t?.({ok:false,error:r?.message??"L\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh \u1EDF downstream",code:E.is(r)?r.code:void 0});}}async handleUpload(e,t){try{if(!this.options.uploadRouter)throw new Error("Service n\xE0y kh\xF4ng b\u1EADt multipart upload (thi\u1EBFu uploadRouter)");if(!e||typeof e.path!="string"||typeof e.method!="string")throw new Error("Envelope upload thi\u1EBFu method/path");let r=e.paramsBlob?(await this.pullBlob(e.paramsBlob,e.module)).body:e.params?.body??"",n=r?Buffer.from(String(r),"base64"):void 0,a=await this.options.uploadRouter.request(J(e.path),{method:e.method,headers:e.headers??{},body:n&&e.method!=="GET"&&e.method!=="DELETE"?n:void 0}),s=await a.text(),o={status:a.status,body:s},h=JSON.stringify(o);if(Buffer.byteLength(h,"utf8")>this.inlineMaxBytes){t?.({ok:!0,resultBlob:await this.pushBlob(h,e.module)});return}t?.({ok:!0,result:o});}catch(r){t?.({ok:false,error:r?.message??"L\u1ED7i upload kh\xF4ng x\xE1c \u0111\u1ECBnh"});}}async pullBlob(e,t){let r=`${this.blobBaseUrl}/tunnel/blob/${encodeURIComponent(e)}?module=${encodeURIComponent(t)}&agentId=${encodeURIComponent(this.agentId??"")}`,n=await this.fetchWithToken(r,{method:"GET"});if(!n.ok){let a=await n.text().catch(()=>"");throw new Error(`Kh\xF4ng k\xE9o \u0111\u01B0\u1EE3c payload t\u1EEB gateway (${n.status}): ${a}`)}return JSON.parse(await n.text())}async pushBlob(e,t){let r=await this.fetchWithToken(`${this.blobBaseUrl}/tunnel/blob`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({module:t,agentId:this.agentId,data:e})}),n=await r.json().catch(()=>{});if(!r.ok||!n?.blobId)throw new Error(`Kh\xF4ng \u0111\u1EA9y \u0111\u01B0\u1EE3c k\u1EBFt qu\u1EA3 l\xEAn gateway (${r.status}): ${n?.error??"kh\xF4ng c\xF3 blobId"}`);return n.blobId}async fetchWithToken(e,t){let r=globalThis.fetch;if(typeof r!="function")throw new Error("FETCH_UNAVAILABLE (c\u1EA7n Node >= 18)");let n=await this.tokens.getToken();return await r(e,{...t,headers:{...t.headers??{},authorization:`Bearer ${n}`}})}logReconnectFailure(e){this.reconnectFailures+=1;let t=Date.now();this.reconnectFailures>1&&t-this.lastReconnectLogAt<Y||(this.lastReconnectLogAt=t,console.error(`[saul][tunnel] ${e} (h\u1ECFng li\xEAn ti\u1EBFp l\u1EA7n ${this.reconnectFailures})`));}startHeartbeat(){this.stopHeartbeat();let e=this.options.heartbeatMs??V;this.heartbeatTimer=setInterval(()=>{this.closed||!this.socket||this.socket.emit(q,{},t=>{t&&t.ok===false&&this.register().catch(r=>console.error("[saul][tunnel] \u0111\u0103ng k\xFD l\u1EA1i sau heartbeat th\u1EA5t b\u1EA1i:",r.message));});},e),this.heartbeatTimer.unref?.();}stopHeartbeat(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=void 0;}emitWithAck(e,t){return new Promise(r=>{this.socket.emit(e,t,r);})}};var B=class{content;metadata;constructor(e){this.metadata=e||{name:"",size:0,type:""},this.content="";}encodeBase64(e){return Buffer.from(e).toString("base64")}decodeBase64(e){let t=Buffer.from(e,"base64"),r=new Uint8Array(t.length);return t.copy(r),r.buffer}read(){if(!this.content)throw new Error("File content is not set");return this.decodeBase64(this.content)}write(e){this.content=this.encodeBase64(e);}};var O=Symbol.for("@fidt/saul.FileStream"),S=class i{[O]=true;metadata;source;consumed=false;constructor(e,t){this.metadata=e,this.source=t;}stream(){if(this.consumed)throw new Error(`FILE_STREAM_CONSUMED: '${this.metadata.name}' \u0111\xE3 \u0111\u01B0\u1EE3c \u0111\u1ECDc r\u1ED3i \u2014 m\u1ED9t FileStream ch\u1EC9 ti\xEAu th\u1EE5 \u0111\u01B0\u1EE3c m\u1ED9t l\u1EA7n`);return this.consumed=true,this.source()}get isConsumed(){return this.consumed}async pipeTo(e){await pipeline(this.stream(),e);}async buffer(e){let t=[],r=0;for await(let n of this.stream()){let a=Buffer.isBuffer(n)?n:Buffer.from(n);if(r+=a.length,r>e)throw new Error(`FILE_STREAM_TOO_LARGE: '${this.metadata.name}' v\u01B0\u1EE3t ${e} byte khi buffer()`);t.push(a);}return Buffer.concat(t,r)}static is(e){return typeof e=="object"&&e!==null&&e[O]===true}static fromBuffer(e,t){return new i({...e,size:e.size??t.length},()=>Readable.from(t))}};var Ie=(...i)=>{let e=i.filter(t=>!!t&&Object.keys(t).length>0);return e.length===0?{}:e.length===1?e[0]:{$and:e}};var d=class i extends Error{constructor(t,r){super(r);this.code=t;this.name="UploadError";}code;static is(t){return t instanceof i}},ee={UPLOAD_NOT_FOUND:404,UPLOAD_EXISTS:409,UPLOAD_EXPIRED:410,UPLOAD_COMPLETED:409,UPLOAD_BUSY:409,PART_OUT_OF_RANGE:416,PART_IN_FLIGHT:409,PART_TOO_LARGE:413,PART_MISSING:422,SIZE_MISMATCH:422,CHECKSUM_MISMATCH:422,DISK_BUDGET:503,FORBIDDEN:403},$=i=>ee[i]??500;var he=1024*1024,v=class{rootDir;fsync;constructor(e){this.rootDir=e.rootDir,this.fsync=e.fsync??true;}dirFor(e){return de(e),join(this.rootDir,e)}pathFor(e,t,r){return le(t,r),join(this.dirFor(e),`${String(t).padStart(6,"0")}.part`)}async ensureDir(e){await mkdir(this.dirFor(e),{recursive:true});}async writePart(e,t,r,n){let a=this.pathFor(e,t,n.partCount),s=`${a}.tmp`;await this.ensureDir(e);let o;try{o=await open(s,"wx");}catch(u){throw u?.code==="EEXIST"?new d("PART_IN_FLIGHT",`Part ${t} c\u1EE7a '${e}' \u0111ang \u0111\u01B0\u1EE3c ghi b\u1EDFi m\u1ED9t request kh\xE1c`):u}let h=createHash("sha256"),c=0,l=n.declaredLength!==void 0?Math.min(n.maxBytes,n.declaredLength):n.maxBytes,f=new Transform({transform(u,b,k){if(c+=u.length,c>l){k(new d("PART_TOO_LARGE",`Part ${t} v\u01B0\u1EE3t ${l} byte`));return}h.update(u),k(null,u);}});try{await pipeline(r,f,o.createWriteStream()),this.fsync&&await o.sync();}catch(u){throw await o.close().catch(()=>{}),await unlink(s).catch(()=>{}),u}if(await o.close(),n.declaredLength!==void 0&&c!==n.declaredLength)throw await unlink(s).catch(()=>{}),new d("SIZE_MISMATCH",`Part ${t}: nh\u1EADn ${c} byte, content-length khai ${n.declaredLength}`);let p=h.digest("hex");if(n.expectedSha256&&n.expectedSha256!==p)throw await unlink(s).catch(()=>{}),new d("CHECKSUM_MISMATCH",`Part ${t}: sha256 kh\xF4ng kh\u1EDBp (khai ${n.expectedSha256}, th\u1EADt ${p})`);return await rename(s,a),{size:c,etag:p}}createPartStream(e,t,r){return createReadStream(this.pathFor(e,t,r),{highWaterMark:he})}async listParts(e){let t;try{t=await readdir(this.dirFor(e));}catch(n){if(n?.code==="ENOENT")return [];throw n}let r=[];for(let n of t){let a=/^(\d{6})\.part$/.exec(n);if(!a)continue;let s=await stat(join(this.dirFor(e),n));r.push({partNumber:Number.parseInt(a[1],10),size:s.size});}return r.sort((n,a)=>n.partNumber-a.partNumber)}async removeUpload(e){let t=this.dirFor(e),r=0;try{for(let n of await readdir(t)){let a=await stat(join(t,n)).catch(()=>{});a&&(r+=a.size);}}catch(n){if(n?.code==="ENOENT")return 0;throw n}return await rm(t,{recursive:true,force:true}),r}async listStaleDirs(e,t=Date.now()){let r;try{r=await readdir(this.rootDir);}catch(a){if(a?.code==="ENOENT")return [];throw a}let n=[];for(let a of r){let s=await stat(join(this.rootDir,a)).catch(()=>{});s?.isDirectory()&&t-s.mtimeMs>e&&n.push(a);}return n}async totalBytes(){let e;try{e=await readdir(this.rootDir);}catch(r){if(r?.code==="ENOENT")return 0;throw r}let t=0;for(let r of e){let n=join(this.rootDir,r);if((await stat(n).catch(()=>{}))?.isDirectory())for(let s of await readdir(n).catch(()=>[])){let o=await stat(join(n,s)).catch(()=>{});o?.isFile()&&(t+=o.size);}}return t}};function de(i){if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(i))throw new d("FORBIDDEN",`uploadId kh\xF4ng h\u1EE3p l\u1EC7: '${i}'`)}function le(i,e){if(!Number.isSafeInteger(i)||i<1||i>e)throw new d("PART_OUT_OF_RANGE",`partNumber ${i} ngo\xE0i d\u1EA3i 1..${e}`)}var R="meta.json",P=class{records=new Map;store;maxDiskBytes;usedBytes=0;constructor(e){this.store=e.store,this.maxDiskBytes=e.maxDiskBytes;}get diskUsedBytes(){return this.usedBytes}setDiskUsedBytes(e){this.usedBytes=Math.max(0,e);}async init(e){if(this.records.has(e.uploadId))throw new d("UPLOAD_EXISTS",`Upload '${e.uploadId}' \u0111\xE3 t\u1ED3n t\u1EA1i`);if(this.usedBytes+e.file.size>this.maxDiskBytes)throw new d("DISK_BUDGET",`H\u1EBFt ng\xE2n s\xE1ch \u0111\u0129a (${this.usedBytes}/${this.maxDiskBytes} byte) \u2014 th\u1EED l\u1EA1i sau`);let t=Date.now(),r={...e,state:"open",createdAt:t,expiresAt:Math.min(t+e.idleTimeoutMs,e.hardExpiresAt)};return this.records.set(r.uploadId,r),await this.store.ensureDir(r.uploadId),await this.persist(r),r}get(e,t=["open"]){let r=this.records.get(e);if(!r)throw new d("UPLOAD_NOT_FOUND",`Upload '${e}' kh\xF4ng t\u1ED3n t\u1EA1i`);if(Date.now()>r.hardExpiresAt||Date.now()>r.expiresAt)throw new d("UPLOAD_EXPIRED",`Upload '${e}' \u0111\xE3 h\u1EBFt h\u1EA1n`);if(!t.includes(r.state))throw new d(r.state==="completed"?"UPLOAD_COMPLETED":"UPLOAD_BUSY",`Upload '${e}' \u0111ang \u1EDF tr\u1EA1ng th\xE1i '${r.state}'`);return r}peek(e){return this.records.get(e)}list(){return [...this.records.values()]}touch(e,t){let r=this.records.get(e);r&&(r.expiresAt=Math.min(Date.now()+r.idleTimeoutMs,r.hardExpiresAt),this.usedBytes=Math.max(0,this.usedBytes+t));}beginComplete(e){let t=this.get(e,["open"]);return t.state="completing",t}rollbackComplete(e){let t=this.records.get(e);t?.state==="completing"&&(t.state="open");}async remove(e){this.records.delete(e);let t=await this.store.removeUpload(e);this.usedBytes=Math.max(0,this.usedBytes-t);}async persist(e){let t=this.store.dirFor(e.uploadId),r=join(t,`${R}.tmp`);await writeFile(r,JSON.stringify(e),"utf-8"),await rename(r,join(t,R));}async readPersisted(e){try{let t=await readFile(join(this.store.dirFor(e),R),"utf-8");return JSON.parse(t)}catch{return}}};var x=class{registry;store;intervalMs;orphanGraceMs;reconcileEvery;timer;ticks=0;constructor(e){this.registry=e.registry,this.store=e.store,this.intervalMs=e.intervalMs??6e4,this.orphanGraceMs=e.orphanGraceMs??20*6e4,this.reconcileEvery=e.reconcileEvery??10;}start(){this.timer||(this.timer=setInterval(()=>{this.sweep().catch(e=>console.error("[saul][upload] sweep l\u1ED7i:",e));},this.intervalMs),typeof this.timer.unref=="function"&&this.timer.unref());}stop(){this.timer&&(clearInterval(this.timer),this.timer=void 0);}async sweep(e=Date.now()){let t={expired:0,orphans:0};for(let r of this.registry.list())(e>r.hardExpiresAt||e>r.expiresAt)&&(await this.registry.remove(r.uploadId),t.expired+=1);for(let r of await this.store.listStaleDirs(this.orphanGraceMs,e))this.registry.peek(r)||(await this.store.removeUpload(r).catch(()=>{}),t.orphans+=1);if(this.ticks+=1,this.ticks%this.reconcileEvery===0){let r=await this.store.totalBytes();this.registry.setDiskUsedBytes(r),t.reconciledBytes=r;}return t}};function M(i,e,t,r){async function*n(){for(let a of t){let s=i.createPartStream(e,a,r);try{for await(let o of s)yield o;}finally{s.destroy();}}}return Readable.from(n())}var L=(i,e)=>{let t=process.env[i],r=t?Number.parseInt(t,10):NaN;return Number.isFinite(r)&&r>0?r:e},ke=(i,e)=>{let t=process.env[i];return t===void 0?e:t==="true"||t==="1"};function ct(i){let e=new v({rootDir:i.rootDir??process.env.SAUL_UPLOAD_TMP_DIR??join(tmpdir(),"saul-uploads"),fsync:i.fsync??ke("SAUL_UPLOAD_FSYNC",true)}),t=new P({store:e,maxDiskBytes:i.maxDiskBytes??L("SAUL_UPLOAD_MAX_DISK_BYTES",20*1024*1024*1024)}),r=new x({registry:t,store:e,intervalMs:i.sweepIntervalMs}),n=i.maxPartBytes??L("SAUL_UPLOAD_MAX_PART_BYTES",32*1024*1024),a=new Hono;return a.onError((s,o)=>d.is(s)?o.json({error:s.message,code:s.code},$(s.code)):(console.error("[saul][upload] l\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh:",s),o.json({error:s instanceof Error?s.message:String(s)},500))),a.post("/init",async s=>{let o=await s.req.json(),h=await t.init(o);return s.json({uploadId:h.uploadId,expiresAt:h.expiresAt})}),a.put("/:uploadId/part/:partNumber",async s=>{let o=s.req.param("uploadId"),h=Number(s.req.param("partNumber")),c=t.get(o),l=s.req.header("content-length");if(!l)throw new d("SIZE_MISMATCH","Thi\u1EBFu content-length \u2014 part ph\u1EA3i khai \u0111\u1ED9 d\xE0i");let f=(await e.listParts(o)).find(b=>b.partNumber===h),p=s.req.raw.body;if(!p)throw new d("SIZE_MISMATCH","Request kh\xF4ng c\xF3 body");let u=await e.writePart(o,h,Readable.fromWeb(p),{maxBytes:n,partCount:c.partCount,declaredLength:Number(l),expectedSha256:s.req.header("x-saul-part-sha256")});return t.touch(o,u.size-(f?.size??0)),s.json({partNumber:h,size:u.size,etag:u.etag})}),a.get("/:uploadId",async s=>{let o=s.req.param("uploadId"),h=t.get(o,["open","completing"]),c=await e.listParts(o);return s.json({uploadId:o,size:h.file.size,chunkSize:h.chunkSize,partCount:h.partCount,received:c,expiresAt:h.expiresAt})}),a.post("/:uploadId/complete",async s=>{let o=s.req.param("uploadId"),{ctx:h}=await s.req.json().catch(()=>({})),c=t.beginComplete(o);try{let l=await e.listParts(o),f=new Set(l.map(m=>m.partNumber)),p=[];for(let m=1;m<=c.partCount;m+=1)f.has(m)||p.push(m);if(p.length>0)throw new d("PART_MISSING",`Thi\u1EBFu part: ${p.slice(0,10).join(", ")}${p.length>10?"\u2026":""}`);let u=l.reduce((m,F)=>m+F.size,0);if(c.file.size>0&&u!==c.file.size)throw new d("SIZE_MISMATCH",`Nh\u1EADn ${u} byte, khai b\xE1o ${c.file.size}`);let b=l.map(m=>m.partNumber),k=new S(c.file,()=>M(e,o,b,c.partCount)),N=await i.dispatch({module:c.module,...c.service?{service:c.service}:{},function:c.function,params:{...c.params,[c.param]:k,ctx:h}});return s.json(N)}catch(l){throw t.rollbackComplete(o),l}finally{t.peek(o)?.state!=="open"&&await t.remove(o).catch(()=>{});}}),a.delete("/:uploadId",async s=>(await t.remove(s.req.param("uploadId")).catch(()=>{}),s.body(null,204))),{router:a,registry:t,store:e,sweeper:r}}
|
|
3
|
+
export{B as File,S as FileStream,T as MachineTokenProvider,v as PartDiskStore,E as SaulException,I as TunnelAgent,ee as UPLOAD_ERROR_STATUS,d as UploadError,P as UploadRegistry,x as UploadSweeper,le as assertSafePartNumber,de as assertSafeUploadId,M as concatParts,ct as createUploadSubsystem,Ie as narrow,$ as uploadErrorStatus};//# sourceMappingURL=index.js.map
|
|
4
4
|
//# sourceMappingURL=index.js.map
|