@rpcbase/server 0.534.0 → 0.536.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/{email-Dzauaq11.js → email-H8nTAGxe.js} +3 -3
- package/dist/{email-Dzauaq11.js.map → email-H8nTAGxe.js.map} +1 -1
- package/dist/{handler-Cq-OJ0Rf.js → handler-BBzEodA0.js} +2 -2
- package/dist/{handler-Cq-OJ0Rf.js.map → handler-BBzEodA0.js.map} +1 -1
- package/dist/{handler-Cx_ZP_NB.js → handler-BU_tEK6x.js} +2 -2
- package/dist/{handler-Cx_ZP_NB.js.map → handler-BU_tEK6x.js.map} +1 -1
- package/dist/handler-CZD5p1Jv.js +28 -0
- package/dist/handler-CZD5p1Jv.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/notifications.js +1 -1
- package/dist/{render_resend-CQb8_8G7.js → render_resend-DQANggpW.js} +1 -1
- package/dist/{render_resend-CQb8_8G7.js.map → render_resend-DQANggpW.js.map} +1 -1
- package/dist/rts/api/cleanup/handler.d.ts +9 -0
- package/dist/rts/api/cleanup/handler.d.ts.map +1 -0
- package/dist/rts/api/cleanup/index.d.ts +11 -0
- package/dist/rts/api/cleanup/index.d.ts.map +1 -0
- package/dist/rts/index.d.ts +1 -0
- package/dist/rts/index.d.ts.map +1 -1
- package/dist/rts/index.js +39 -2
- package/dist/rts/index.js.map +1 -1
- package/dist/{shared-BfMSZm2P.js → shared-DhZ_rDdo.js} +1 -1
- package/dist/{shared-BfMSZm2P.js.map → shared-DhZ_rDdo.js.map} +1 -1
- package/dist/uploads.js +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as getUploadKeyHash, i as getBucketName, p as getUserId, u as getTenantId } from "./shared-
|
|
1
|
+
import { d as getUploadKeyHash, i as getBucketName, p as getUserId, u as getTenantId } from "./shared-DhZ_rDdo.js";
|
|
2
2
|
import { GridFSBucket, ObjectId } from "mongodb";
|
|
3
3
|
import { getTenantFilesystemDb } from "@rpcbase/db";
|
|
4
4
|
//#region src/uploads/api/files/handlers/deleteFile.ts
|
|
@@ -179,4 +179,4 @@ var handler_default = (api) => {
|
|
|
179
179
|
//#endregion
|
|
180
180
|
export { handler_default as default };
|
|
181
181
|
|
|
182
|
-
//# sourceMappingURL=handler-
|
|
182
|
+
//# sourceMappingURL=handler-BBzEodA0.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-Cq-OJ0Rf.js","names":["ApiHandler","getTenantFilesystemDb","GridFSBucket","ObjectId","getBucketName","getTenantId","getUploadKeyHash","getUserId","SessionUser","DeleteResponsePayload","ok","error","resolveHeaderString","value","normalized","trim","deleteFile","Record","_payload","ctx","Promise","tenantId","res","status","fileIdRaw","String","req","params","fileId","fileObjectId","fsDb","nativeDb","db","bucketName","bucket","userId","uploadKeyHash","file","find","_id","limit","toArray","metadataUserId","metadata","ownerKeyHash","authorizedByUser","Boolean","authorizedByKey","delete","message","Error","includes","ApiHandler","getTenantFilesystemDb","GridFSBucket","ObjectId","getBucketName","getTenantId","getUploadKeyHash","getUserId","SessionUser","resolveHeaderString","value","normalized","trim","resolveHeaderBoolean","toLowerCase","escapeHeaderFilename","filename","replace","getFile","Record","_payload","ctx","Promise","tenantId","res","status","end","fileIdRaw","String","req","params","fileId","fileObjectId","fsDb","nativeDb","db","bucketName","bucket","file","find","_id","limit","toArray","isPublic","metadata","userId","uploadKeyHash","metadataUserId","ownerKeyHash","authorizedByUser","Boolean","authorizedByKey","mimeTypeFromMetadata","mimeType","filenameFromDb","filenameSafe","cacheControl","md5","uploadDate","Date","etagValue","toHexString","length","getTime","etag","ifNoneMatch","headers","candidates","split","map","filter","includes","setHeader","flushHeaders","method","stream","openDownloadStream","on","destroy","pipe","Route","GetRoute","DeleteRoute","Api","deleteFile","getFile","Files","api","get","GetRoute","delete","DeleteRoute"],"sources":["../src/uploads/api/files/handlers/deleteFile.ts","../src/uploads/api/files/handlers/getFile.ts","../src/uploads/api/files/index.ts","../src/uploads/api/files/handler.ts"],"sourcesContent":["import { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb } from \"@rpcbase/db\"\nimport { GridFSBucket, ObjectId } from \"mongodb\"\n\nimport { getBucketName, getTenantId, getUploadKeyHash, getUserId, type SessionUser } from \"../../file-uploads/shared\"\n\n\ntype DeleteResponsePayload = {\n ok: boolean\n error?: string\n}\n\nconst resolveHeaderString = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null\n const normalized = value.trim()\n return normalized ? normalized : null\n}\n\nexport const deleteFile: ApiHandler<Record<string, never>, DeleteResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<DeleteResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const fileIdRaw = String(ctx.req.params?.fileId ?? \"\").trim()\n let fileObjectId: ObjectId\n try {\n fileObjectId = new ObjectId(fileIdRaw)\n } catch {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_file_id\" }\n }\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n ctx.res.status(500)\n return { ok: false, error: \"filesystem_db_unavailable\" }\n }\n\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n\n const userId = getUserId(ctx)\n const uploadKeyHash = getUploadKeyHash(ctx)\n if (!userId && !uploadKeyHash) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const [file] = await bucket.find({ _id: fileObjectId }).limit(1).toArray()\n if (!file) {\n ctx.res.status(204)\n return { ok: true }\n }\n\n const metadataUserId = resolveHeaderString((file as any)?.metadata?.userId)\n const ownerKeyHash = resolveHeaderString((file as any)?.metadata?.ownerKeyHash)\n\n const authorizedByUser = Boolean(userId && metadataUserId && userId === metadataUserId)\n const authorizedByKey = Boolean(uploadKeyHash && ownerKeyHash && uploadKeyHash === ownerKeyHash)\n\n if (!authorizedByUser && !authorizedByKey) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n try {\n await bucket.delete(fileObjectId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (!message.includes(\"FileNotFound\")) {\n ctx.res.status(500)\n return { ok: false, error: \"delete_failed\" }\n }\n }\n\n ctx.res.status(204)\n return { ok: true }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb } from \"@rpcbase/db\"\nimport { GridFSBucket, ObjectId } from \"mongodb\"\n\nimport { getBucketName, getTenantId, getUploadKeyHash, getUserId, type SessionUser } from \"../../file-uploads/shared\"\n\n\nconst resolveHeaderString = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null\n const normalized = value.trim()\n return normalized ? normalized : null\n}\n\nconst resolveHeaderBoolean = (value: unknown): boolean | null => {\n if (typeof value === \"boolean\") return value\n if (typeof value !== \"string\") return null\n const normalized = value.trim().toLowerCase()\n if (!normalized) return null\n if (normalized === \"true\") return true\n if (normalized === \"false\") return false\n return null\n}\n\nconst escapeHeaderFilename = (filename: string): string => filename.replace(/[\\\\\"]/g, \"_\")\n\nexport const getFile: ApiHandler<Record<string, never>, Record<string, never>, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Record<string, never>> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400).end()\n return {}\n }\n\n const fileIdRaw = String(ctx.req.params?.fileId ?? \"\").trim()\n let fileObjectId: ObjectId\n try {\n fileObjectId = new ObjectId(fileIdRaw)\n } catch {\n ctx.res.status(400).end()\n return {}\n }\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n ctx.res.status(500).end()\n return {}\n }\n\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n\n const [file] = await bucket.find({ _id: fileObjectId }).limit(1).toArray()\n if (!file) {\n ctx.res.status(404).end()\n return {}\n }\n\n const isPublic = resolveHeaderBoolean((file as any)?.metadata?.isPublic) ?? false\n if (!isPublic) {\n const userId = getUserId(ctx)\n const uploadKeyHash = getUploadKeyHash(ctx)\n const metadataUserId = resolveHeaderString((file as any)?.metadata?.userId)\n const ownerKeyHash = resolveHeaderString((file as any)?.metadata?.ownerKeyHash)\n\n const authorizedByUser = Boolean(userId && metadataUserId && userId === metadataUserId)\n const authorizedByKey = Boolean(uploadKeyHash && ownerKeyHash && uploadKeyHash === ownerKeyHash)\n\n if (!authorizedByUser && !authorizedByKey) {\n ctx.res.status(401).end()\n return {}\n }\n }\n\n const mimeTypeFromMetadata = resolveHeaderString((file as any)?.metadata?.mimeType)\n const mimeType = mimeTypeFromMetadata ?? \"application/octet-stream\"\n const filenameFromDb = resolveHeaderString((file as any)?.filename)\n const filename = filenameFromDb ?? fileIdRaw\n const filenameSafe = escapeHeaderFilename(filename)\n\n const cacheControl = \"private, max-age=0, must-revalidate\"\n const md5 = resolveHeaderString((file as any)?.md5)\n const uploadDate = (file as any)?.uploadDate instanceof Date ? (file as any).uploadDate : null\n const etagValue = md5 ?? `${fileObjectId.toHexString()}-${String((file as any)?.length ?? 0)}-${String(uploadDate?.getTime() ?? 0)}`\n const etag = md5 ? `\"${etagValue}\"` : `W/\"${etagValue}\"`\n const ifNoneMatch = resolveHeaderString((ctx.req.headers as any)?.[\"if-none-match\"])\n if (ifNoneMatch) {\n const candidates = ifNoneMatch.split(\",\").map((value) => value.trim()).filter(Boolean)\n if (candidates.includes(\"*\") || candidates.includes(etag)) {\n ctx.res.status(304)\n ctx.res.setHeader(\"Cache-Control\", cacheControl)\n ctx.res.setHeader(\"ETag\", etag)\n ctx.res.end()\n return {}\n }\n }\n\n ctx.res.status(200)\n ctx.res.setHeader(\"Content-Type\", mimeType)\n ctx.res.setHeader(\"Content-Length\", String((file as any)?.length ?? 0))\n ctx.res.setHeader(\"Content-Disposition\", `inline; filename=\"${filenameSafe}\"`)\n ctx.res.setHeader(\"Cache-Control\", cacheControl)\n ctx.res.setHeader(\"ETag\", etag)\n ctx.res.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n if (mimeType === \"image/svg+xml\") {\n ctx.res.setHeader(\n \"Content-Security-Policy\",\n \"default-src 'none'; style-src 'unsafe-inline'; sandbox; base-uri 'none'; form-action 'none'\",\n )\n }\n ctx.res.flushHeaders()\n\n if (ctx.req.method === \"HEAD\") {\n ctx.res.end()\n return {}\n }\n\n const stream = bucket.openDownloadStream(fileObjectId)\n\n stream.on(\"error\", () => {\n try {\n ctx.res.destroy()\n } catch {\n //\n }\n })\n\n stream.pipe(ctx.res)\n return {}\n}\n","export const Route = \"/api/rb/files/:fileId\"\n\nexport const GetRoute = Route\nexport const DeleteRoute = Route\n","import { Api } from \"@rpcbase/api\"\n\nimport { deleteFile } from \"./handlers/deleteFile\"\nimport { getFile } from \"./handlers/getFile\"\n\nimport * as Files from \"./index\"\n\n\nexport default (api: Api) => {\n api.get(Files.GetRoute, getFile)\n api.delete(Files.DeleteRoute, deleteFile)\n}\n"],"mappings":";;;;AAYA,IAAMY,yBAAuBC,UAAkC;AAC7D,KAAI,OAAOA,UAAU,SAAU,QAAO;CACtC,MAAMC,aAAaD,MAAME,MAAM;AAC/B,QAAOD,aAAaA,aAAa;;AAGnC,IAAaE,aAAoF,OAC/FE,UACAC,QACmC;CACnC,MAAME,WAAWhB,YAAYc,IAAI;AACjC,KAAI,CAACE,UAAU;AACbF,MAAIG,IAAIC,OAAO,IAAI;AACnB,SAAO;GAAEb,IAAI;GAAOC,OAAO;GAAkB;;CAG/C,MAAMa,YAAYC,OAAON,IAAIO,IAAIC,QAAQC,UAAU,GAAG,CAACb,MAAM;CAC7D,IAAIc;AACJ,KAAI;AACFA,iBAAe,IAAI1B,SAASqB,UAAU;SAChC;AACNL,MAAIG,IAAIC,OAAO,IAAI;AACnB,SAAO;GAAEb,IAAI;GAAOC,OAAO;GAAmB;;CAIhD,MAAMoB,YADO,MAAM9B,sBAAsBoB,SAAS,EAC5BW;AACtB,KAAI,CAACD,UAAU;AACbZ,MAAIG,IAAIC,OAAO,IAAI;AACnB,SAAO;GAAEb,IAAI;GAAOC,OAAO;GAA6B;;CAI1D,MAAMuB,SAAS,IAAIhC,aAAa6B,UAAU,EAAEE,YADzB7B,eAAe,EACsB,CAAC;CAEzD,MAAM+B,SAAS5B,UAAUY,IAAI;CAC7B,MAAMiB,gBAAgB9B,iBAAiBa,IAAI;AAC3C,KAAI,CAACgB,UAAU,CAACC,eAAe;AAC7BjB,MAAIG,IAAIC,OAAO,IAAI;AACnB,SAAO;GAAEb,IAAI;GAAOC,OAAO;GAAgB;;CAG7C,MAAM,CAAC0B,QAAQ,MAAMH,OAAOI,KAAK,EAAEC,KAAKV,cAAc,CAAC,CAACW,MAAM,EAAE,CAACC,SAAS;AAC1E,KAAI,CAACJ,MAAM;AACTlB,MAAIG,IAAIC,OAAO,IAAI;AACnB,SAAO,EAAEb,IAAI,MAAM;;CAGrB,MAAMgC,iBAAiB9B,sBAAqByB,MAAcM,UAAUR,OAAO;CAC3E,MAAMS,eAAehC,sBAAqByB,MAAcM,UAAUC,aAAa;AAK/E,KAAI,CAHqBE,QAAQX,UAAUO,kBAAkBP,WAAWO,eAAe,IAG9D,CAFDI,QAAQV,iBAAiBQ,gBAAgBR,kBAAkBQ,aAAa,EAErD;AACzCzB,MAAIG,IAAIC,OAAO,IAAI;AACnB,SAAO;GAAEb,IAAI;GAAOC,OAAO;GAAgB;;AAG7C,KAAI;AACF,QAAMuB,OAAOc,OAAOnB,aAAa;UAC1BlB,OAAO;AAEd,MAAI,EADYA,iBAAiBuC,QAAQvC,MAAMsC,UAAUxB,OAAOd,MAAM,EACzDwC,SAAS,eAAe,EAAE;AACrChC,OAAIG,IAAIC,OAAO,IAAI;AACnB,UAAO;IAAEb,IAAI;IAAOC,OAAO;IAAiB;;;AAIhDQ,KAAIG,IAAIC,OAAO,IAAI;AACnB,QAAO,EAAEb,IAAI,MAAM;;;;AC3ErB,IAAMmD,uBAAuBC,UAAkC;AAC7D,KAAI,OAAOA,UAAU,SAAU,QAAO;CACtC,MAAMC,aAAaD,MAAME,MAAM;AAC/B,QAAOD,aAAaA,aAAa;;AAGnC,IAAME,wBAAwBH,UAAmC;AAC/D,KAAI,OAAOA,UAAU,UAAW,QAAOA;AACvC,KAAI,OAAOA,UAAU,SAAU,QAAO;CACtC,MAAMC,aAAaD,MAAME,MAAM,CAACE,aAAa;AAC7C,KAAI,CAACH,WAAY,QAAO;AACxB,KAAIA,eAAe,OAAQ,QAAO;AAClC,KAAIA,eAAe,QAAS,QAAO;AACnC,QAAO;;AAGT,IAAMI,wBAAwBC,aAA6BA,SAASC,QAAQ,UAAU,IAAI;AAE1F,IAAaC,UAAiF,OAC5FE,UACAC,QACmC;CACnC,MAAME,WAAWlB,YAAYgB,IAAI;AACjC,KAAI,CAACE,UAAU;AACbF,MAAIG,IAAIC,OAAO,IAAI,CAACC,KAAK;AACzB,SAAO,EAAE;;CAGX,MAAMC,YAAYC,OAAOP,IAAIQ,IAAIC,QAAQC,UAAU,GAAG,CAACnB,MAAM;CAC7D,IAAIoB;AACJ,KAAI;AACFA,iBAAe,IAAI7B,SAASwB,UAAU;SAChC;AACNN,MAAIG,IAAIC,OAAO,IAAI,CAACC,KAAK;AACzB,SAAO,EAAE;;CAIX,MAAMQ,YADO,MAAMjC,sBAAsBsB,SAAS,EAC5BY;AACtB,KAAI,CAACD,UAAU;AACbb,MAAIG,IAAIC,OAAO,IAAI,CAACC,KAAK;AACzB,SAAO,EAAE;;CAIX,MAAMW,SAAS,IAAInC,aAAagC,UAAU,EAAEE,YADzBhC,eAAe,EACsB,CAAC;CAEzD,MAAM,CAACkC,QAAQ,MAAMD,OAAOE,KAAK,EAAEC,KAAKR,cAAc,CAAC,CAACS,MAAM,EAAE,CAACC,SAAS;AAC1E,KAAI,CAACJ,MAAM;AACTjB,MAAIG,IAAIC,OAAO,IAAI,CAACC,KAAK;AACzB,SAAO,EAAE;;AAIX,KAAI,EADab,qBAAsByB,MAAcM,UAAUD,SAAS,IAAI,QAC7D;EACb,MAAME,SAAStC,UAAUc,IAAI;EAC7B,MAAMyB,gBAAgBxC,iBAAiBe,IAAI;EAC3C,MAAM0B,iBAAiBtC,oBAAqB6B,MAAcM,UAAUC,OAAO;EAC3E,MAAMG,eAAevC,oBAAqB6B,MAAcM,UAAUI,aAAa;AAK/E,MAAI,CAHqBE,QAAQL,UAAUE,kBAAkBF,WAAWE,eAAe,IAG9D,CAFDG,QAAQJ,iBAAiBE,gBAAgBF,kBAAkBE,aAAa,EAErD;AACzC3B,OAAIG,IAAIC,OAAO,IAAI,CAACC,KAAK;AACzB,UAAO,EAAE;;;CAKb,MAAM2B,WADuB5C,oBAAqB6B,MAAcM,UAAUS,SAAS,IAC1C;CAGzC,MAAME,eAAexC,qBAFEN,oBAAqB6B,MAActB,SAAS,IAChCW,UACgB;CAEnD,MAAM6B,eAAe;CACrB,MAAMC,MAAMhD,oBAAqB6B,MAAcmB,IAAI;CACnD,MAAMC,aAAcpB,MAAcoB,sBAAsBC,OAAQrB,KAAaoB,aAAa;CAC1F,MAAME,YAAYH,OAAO,GAAGzB,aAAa6B,aAAa,CAAA,GAAIjC,OAAQU,MAAcwB,UAAU,EAAE,CAAA,GAAIlC,OAAO8B,YAAYK,SAAS,IAAI,EAAE;CAClI,MAAMC,OAAOP,MAAM,IAAIG,UAAS,KAAM,MAAMA,UAAS;CACrD,MAAMK,cAAcxD,oBAAqBY,IAAIQ,IAAIqC,UAAkB,iBAAiB;AACpF,KAAID,aAAa;EACf,MAAME,aAAaF,YAAYG,MAAM,IAAI,CAACC,KAAK3D,UAAUA,MAAME,MAAM,CAAC,CAAC0D,OAAOpB,QAAQ;AACtF,MAAIiB,WAAWI,SAAS,IAAI,IAAIJ,WAAWI,SAASP,KAAK,EAAE;AACzD3C,OAAIG,IAAIC,OAAO,IAAI;AACnBJ,OAAIG,IAAIgD,UAAU,iBAAiBhB,aAAa;AAChDnC,OAAIG,IAAIgD,UAAU,QAAQR,KAAK;AAC/B3C,OAAIG,IAAIE,KAAK;AACb,UAAO,EAAE;;;AAIbL,KAAIG,IAAIC,OAAO,IAAI;AACnBJ,KAAIG,IAAIgD,UAAU,gBAAgBnB,SAAS;AAC3ChC,KAAIG,IAAIgD,UAAU,kBAAkB5C,OAAQU,MAAcwB,UAAU,EAAE,CAAC;AACvEzC,KAAIG,IAAIgD,UAAU,uBAAuB,qBAAqBjB,aAAY,GAAI;AAC9ElC,KAAIG,IAAIgD,UAAU,iBAAiBhB,aAAa;AAChDnC,KAAIG,IAAIgD,UAAU,QAAQR,KAAK;AAC/B3C,KAAIG,IAAIgD,UAAU,0BAA0B,UAAU;AACtD,KAAInB,aAAa,gBACfhC,KAAIG,IAAIgD,UACN,2BACA,8FACD;AAEHnD,KAAIG,IAAIiD,cAAc;AAEtB,KAAIpD,IAAIQ,IAAI6C,WAAW,QAAQ;AAC7BrD,MAAIG,IAAIE,KAAK;AACb,SAAO,EAAE;;CAGX,MAAMiD,SAAStC,OAAOuC,mBAAmB5C,aAAa;AAEtD2C,QAAOE,GAAG,eAAe;AACvB,MAAI;AACFxD,OAAIG,IAAIsD,SAAS;UACX;GAGR;AAEFH,QAAOI,KAAK1D,IAAIG,IAAI;AACpB,QAAO,EAAE;;;;AClIX,IAAawD,QAAQ;AAErB,IAAaC,WAAWD;AACxB,IAAaE,cAAcF;;;ACK3B,IAAA,mBAAgBO,QAAa;AAC3BA,KAAIC,IAAIF,UAAgBD,QAAQ;AAChCE,KAAIG,OAAOJ,aAAmBF,WAAW"}
|
|
1
|
+
{"version":3,"file":"handler-BBzEodA0.js","names":["ApiHandler","getTenantFilesystemDb","GridFSBucket","ObjectId","getBucketName","getTenantId","getUploadKeyHash","getUserId","SessionUser","DeleteResponsePayload","ok","error","resolveHeaderString","value","normalized","trim","deleteFile","Record","_payload","ctx","Promise","tenantId","res","status","fileIdRaw","String","req","params","fileId","fileObjectId","fsDb","nativeDb","db","bucketName","bucket","userId","uploadKeyHash","file","find","_id","limit","toArray","metadataUserId","metadata","ownerKeyHash","authorizedByUser","Boolean","authorizedByKey","delete","message","Error","includes","ApiHandler","getTenantFilesystemDb","GridFSBucket","ObjectId","getBucketName","getTenantId","getUploadKeyHash","getUserId","SessionUser","resolveHeaderString","value","normalized","trim","resolveHeaderBoolean","toLowerCase","escapeHeaderFilename","filename","replace","getFile","Record","_payload","ctx","Promise","tenantId","res","status","end","fileIdRaw","String","req","params","fileId","fileObjectId","fsDb","nativeDb","db","bucketName","bucket","file","find","_id","limit","toArray","isPublic","metadata","userId","uploadKeyHash","metadataUserId","ownerKeyHash","authorizedByUser","Boolean","authorizedByKey","mimeTypeFromMetadata","mimeType","filenameFromDb","filenameSafe","cacheControl","md5","uploadDate","Date","etagValue","toHexString","length","getTime","etag","ifNoneMatch","headers","candidates","split","map","filter","includes","setHeader","flushHeaders","method","stream","openDownloadStream","on","destroy","pipe","Route","GetRoute","DeleteRoute","Api","deleteFile","getFile","Files","api","get","GetRoute","delete","DeleteRoute"],"sources":["../src/uploads/api/files/handlers/deleteFile.ts","../src/uploads/api/files/handlers/getFile.ts","../src/uploads/api/files/index.ts","../src/uploads/api/files/handler.ts"],"sourcesContent":["import { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb } from \"@rpcbase/db\"\nimport { GridFSBucket, ObjectId } from \"mongodb\"\n\nimport { getBucketName, getTenantId, getUploadKeyHash, getUserId, type SessionUser } from \"../../file-uploads/shared\"\n\n\ntype DeleteResponsePayload = {\n ok: boolean\n error?: string\n}\n\nconst resolveHeaderString = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null\n const normalized = value.trim()\n return normalized ? normalized : null\n}\n\nexport const deleteFile: ApiHandler<Record<string, never>, DeleteResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<DeleteResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const fileIdRaw = String(ctx.req.params?.fileId ?? \"\").trim()\n let fileObjectId: ObjectId\n try {\n fileObjectId = new ObjectId(fileIdRaw)\n } catch {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_file_id\" }\n }\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n ctx.res.status(500)\n return { ok: false, error: \"filesystem_db_unavailable\" }\n }\n\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n\n const userId = getUserId(ctx)\n const uploadKeyHash = getUploadKeyHash(ctx)\n if (!userId && !uploadKeyHash) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const [file] = await bucket.find({ _id: fileObjectId }).limit(1).toArray()\n if (!file) {\n ctx.res.status(204)\n return { ok: true }\n }\n\n const metadataUserId = resolveHeaderString((file as any)?.metadata?.userId)\n const ownerKeyHash = resolveHeaderString((file as any)?.metadata?.ownerKeyHash)\n\n const authorizedByUser = Boolean(userId && metadataUserId && userId === metadataUserId)\n const authorizedByKey = Boolean(uploadKeyHash && ownerKeyHash && uploadKeyHash === ownerKeyHash)\n\n if (!authorizedByUser && !authorizedByKey) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n try {\n await bucket.delete(fileObjectId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n if (!message.includes(\"FileNotFound\")) {\n ctx.res.status(500)\n return { ok: false, error: \"delete_failed\" }\n }\n }\n\n ctx.res.status(204)\n return { ok: true }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb } from \"@rpcbase/db\"\nimport { GridFSBucket, ObjectId } from \"mongodb\"\n\nimport { getBucketName, getTenantId, getUploadKeyHash, getUserId, type SessionUser } from \"../../file-uploads/shared\"\n\n\nconst resolveHeaderString = (value: unknown): string | null => {\n if (typeof value !== \"string\") return null\n const normalized = value.trim()\n return normalized ? normalized : null\n}\n\nconst resolveHeaderBoolean = (value: unknown): boolean | null => {\n if (typeof value === \"boolean\") return value\n if (typeof value !== \"string\") return null\n const normalized = value.trim().toLowerCase()\n if (!normalized) return null\n if (normalized === \"true\") return true\n if (normalized === \"false\") return false\n return null\n}\n\nconst escapeHeaderFilename = (filename: string): string => filename.replace(/[\\\\\"]/g, \"_\")\n\nexport const getFile: ApiHandler<Record<string, never>, Record<string, never>, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Record<string, never>> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400).end()\n return {}\n }\n\n const fileIdRaw = String(ctx.req.params?.fileId ?? \"\").trim()\n let fileObjectId: ObjectId\n try {\n fileObjectId = new ObjectId(fileIdRaw)\n } catch {\n ctx.res.status(400).end()\n return {}\n }\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n ctx.res.status(500).end()\n return {}\n }\n\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n\n const [file] = await bucket.find({ _id: fileObjectId }).limit(1).toArray()\n if (!file) {\n ctx.res.status(404).end()\n return {}\n }\n\n const isPublic = resolveHeaderBoolean((file as any)?.metadata?.isPublic) ?? false\n if (!isPublic) {\n const userId = getUserId(ctx)\n const uploadKeyHash = getUploadKeyHash(ctx)\n const metadataUserId = resolveHeaderString((file as any)?.metadata?.userId)\n const ownerKeyHash = resolveHeaderString((file as any)?.metadata?.ownerKeyHash)\n\n const authorizedByUser = Boolean(userId && metadataUserId && userId === metadataUserId)\n const authorizedByKey = Boolean(uploadKeyHash && ownerKeyHash && uploadKeyHash === ownerKeyHash)\n\n if (!authorizedByUser && !authorizedByKey) {\n ctx.res.status(401).end()\n return {}\n }\n }\n\n const mimeTypeFromMetadata = resolveHeaderString((file as any)?.metadata?.mimeType)\n const mimeType = mimeTypeFromMetadata ?? \"application/octet-stream\"\n const filenameFromDb = resolveHeaderString((file as any)?.filename)\n const filename = filenameFromDb ?? fileIdRaw\n const filenameSafe = escapeHeaderFilename(filename)\n\n const cacheControl = \"private, max-age=0, must-revalidate\"\n const md5 = resolveHeaderString((file as any)?.md5)\n const uploadDate = (file as any)?.uploadDate instanceof Date ? (file as any).uploadDate : null\n const etagValue = md5 ?? `${fileObjectId.toHexString()}-${String((file as any)?.length ?? 0)}-${String(uploadDate?.getTime() ?? 0)}`\n const etag = md5 ? `\"${etagValue}\"` : `W/\"${etagValue}\"`\n const ifNoneMatch = resolveHeaderString((ctx.req.headers as any)?.[\"if-none-match\"])\n if (ifNoneMatch) {\n const candidates = ifNoneMatch.split(\",\").map((value) => value.trim()).filter(Boolean)\n if (candidates.includes(\"*\") || candidates.includes(etag)) {\n ctx.res.status(304)\n ctx.res.setHeader(\"Cache-Control\", cacheControl)\n ctx.res.setHeader(\"ETag\", etag)\n ctx.res.end()\n return {}\n }\n }\n\n ctx.res.status(200)\n ctx.res.setHeader(\"Content-Type\", mimeType)\n ctx.res.setHeader(\"Content-Length\", String((file as any)?.length ?? 0))\n ctx.res.setHeader(\"Content-Disposition\", `inline; filename=\"${filenameSafe}\"`)\n ctx.res.setHeader(\"Cache-Control\", cacheControl)\n ctx.res.setHeader(\"ETag\", etag)\n ctx.res.setHeader(\"X-Content-Type-Options\", \"nosniff\")\n if (mimeType === \"image/svg+xml\") {\n ctx.res.setHeader(\n \"Content-Security-Policy\",\n \"default-src 'none'; style-src 'unsafe-inline'; sandbox; base-uri 'none'; form-action 'none'\",\n )\n }\n ctx.res.flushHeaders()\n\n if (ctx.req.method === \"HEAD\") {\n ctx.res.end()\n return {}\n }\n\n const stream = bucket.openDownloadStream(fileObjectId)\n\n stream.on(\"error\", () => {\n try {\n ctx.res.destroy()\n } catch {\n //\n }\n })\n\n stream.pipe(ctx.res)\n return {}\n}\n","export const Route = \"/api/rb/files/:fileId\"\n\nexport const GetRoute = Route\nexport const DeleteRoute = Route\n","import { Api } from \"@rpcbase/api\"\n\nimport { deleteFile } from \"./handlers/deleteFile\"\nimport { getFile } from \"./handlers/getFile\"\n\nimport * as Files from \"./index\"\n\n\nexport default (api: Api) => {\n api.get(Files.GetRoute, getFile)\n api.delete(Files.DeleteRoute, deleteFile)\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { runUploadPostProcessors } from "./uploads.js";
|
|
2
2
|
import { a as object, i as number, n as array, r as boolean, s as string, t as _enum } from "./schemas-BR3K5Luo.js";
|
|
3
|
-
import { a as getChunkSizeBytes, c as getRawBodyLimitBytes, f as getUploadSessionAccessQuery, h as toBufferPayload, i as getBucketName, l as getSessionTtlMs, m as normalizeSha256Hex, n as computeSha256Hex, o as getMaxClientUploadBytesPerSecond, p as getUserId, r as ensureUploadIndexes, s as getModelCtx, t as buildUploadsAbility, u as getTenantId } from "./shared-
|
|
3
|
+
import { a as getChunkSizeBytes, c as getRawBodyLimitBytes, f as getUploadSessionAccessQuery, h as toBufferPayload, i as getBucketName, l as getSessionTtlMs, m as normalizeSha256Hex, n as computeSha256Hex, o as getMaxClientUploadBytesPerSecond, p as getUserId, r as ensureUploadIndexes, s as getModelCtx, t as buildUploadsAbility, u as getTenantId } from "./shared-DhZ_rDdo.js";
|
|
4
4
|
import { GridFSBucket, ObjectId } from "mongodb";
|
|
5
5
|
import { getTenantFilesystemDb, models } from "@rpcbase/db";
|
|
6
6
|
import { randomBytes } from "node:crypto";
|
|
@@ -746,4 +746,4 @@ var handler_default = (api) => {
|
|
|
746
746
|
//#endregion
|
|
747
747
|
export { handler_default as default };
|
|
748
748
|
|
|
749
|
-
//# sourceMappingURL=handler-
|
|
749
|
+
//# sourceMappingURL=handler-BU_tEK6x.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler-Cx_ZP_NB.js","names":["JSDOM","createDOMPurify","UploadFileProcessor","MAX_SVG_BYTES","window","DOMPurify","normalizeForSniff","raw","replace","trimStart","looksLikeSvgText","text","normalized","startsWith","test","looksLikeSvg","sniff","Buffer","toString","sanitizeSvg","svg","sanitize","USE_PROFILES","svgFilters","sanitizeSvgProcessor","id","maxBytes","match","process","data","mimeType","length","Error","svgText","sanitized","trim","sanitizedBuffer","from","sanitizeSvgProcessor","UploadFileProcessorContext","filename","clientMimeType","totalSize","sniff","Buffer","UploadFileProcessorResult","data","mimeType","UploadFileProcessor","id","maxBytes","match","ctx","process","Promise","uploadProcessors","Object","freeze","getMaxUploadProcessorBytes","reduce","max","processor","Math","selectUploadProcessors","filter","applyUploadProcessors","Omit","applied","currentData","currentMimeType","processorCtx","length","Error","result","trim","push","ApiHandler","getTenantFilesystemDb","models","GridFSBucket","Model","Uploads","runUploadPostProcessors","applyUploadProcessors","getMaxUploadProcessorBytes","selectUploadProcessors","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","ensureUploadIndexes","getBucketName","getModelCtx","getUploadSessionAccessQuery","getTenantId","waitForStreamFinished","stream","NodeJS","WritableStream","Promise","resolve","reject","once","writeToStream","chunk","Buffer","ok","write","onDrain","cleanup","onError","error","off","on","abortUploadStream","abort","destroy","completeUpload","Record","CompleteResponsePayload","_payload","ctx","tenantId","res","status","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","existing","findOne","$and","_id","lean","fileId","locked","findOneAndUpdate","$set","$unset","returnDocument","fsDb","nativeDb","db","updateOne","bucketName","bucket","lockedUserId","userId","undefined","maxProcessorBytes","shouldBufferForProcessing","totalSize","declaredMimeType","mimeType","toLowerCase","declaredSvg","filename","endsWith","uploadStream","finalMimeType","inlineProcessors","finalMetadata","isPublic","ownerKeyHash","Error","cursor","find","sort","index","AsyncIterable","close","expectedIndex","chunks","bufferedBytes","pendingChunks","sniffParts","sniffBytes","chunkDoc","data","push","length","slice","subarray","Math","min","sniff","concat","processors","clientMimeType","openUploadStream","metadata","pending","chunksTotal","assembled","processed","processedMimeType","applied","processedSize","finished","end","id","deleteMany","message","ApiHandler","models","Model","Uploads","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","getModelCtx","getUploadSessionAccessQuery","getTenantId","getStatus","Record","StatusResponsePayload","_payload","ctx","Promise","tenantId","res","status","ok","error","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","receivedDocs","find","index","sort","received","Array","map","doc","filter","n","Number","isInteger","chunkSize","chunksTotal","fileId","z","InitRoute","ChunkRoute","StatusRoute","CompleteRoute","initRequestSchema","object","filename","string","min","mimeType","isPublic","boolean","optional","totalSize","number","int","InitRequestPayload","infer","initResponseSchema","ok","error","uploadId","uploadKey","chunkSize","chunksTotal","InitResponsePayload","statusResponseSchema","status","enum","received","array","fileId","StatusResponsePayload","completeResponseSchema","CompleteResponsePayload","randomBytes","ApiHandler","models","ObjectId","Model","Uploads","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","computeSha256Hex","ensureUploadIndexes","getChunkSizeBytes","getModelCtx","getSessionTtlMs","getTenantId","getUserId","initUpload","InitRequestPayload","InitResponsePayload","payload","ctx","Promise","tenantId","res","status","ok","error","userId","parsed","initRequestSchema","safeParse","success","chunkSize","filename","mimeType","totalSize","isPublic","data","chunksTotal","Math","ceil","ability","modelCtx","UploadSession","UploadChunk","all","get","uploadId","toString","now","Date","expiresAt","uploadKey","ownerKeyHash","Buffer","from","undefined","create","_id","createdAt","ApiHandler","models","Model","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","computeSha256Hex","ensureUploadIndexes","getModelCtx","getUploadSessionAccessQuery","getTenantId","normalizeSha256Hex","toBufferPayload","ChunkResponsePayload","ok","error","uploadChunk","Buffer","payload","ctx","Promise","tenantId","res","status","uploadId","String","req","params","trim","indexRaw","index","Number","isInteger","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","chunksTotal","data","expectedSize","totalSize","chunkSize","length","checksumHeader","sha256","undefined","expectedSha256","updateOne","$set","size","expiresAt","$setOnInsert","createdAt","Date","upsert","rawBodyParser","limitBytes","maxClientBytesPerSecond","req","res","next","contentType","headers","String","includes","total","chunks","Buffer","done","paused","throttleTimeout","ReturnType","setTimeout","rateBytesPerSecond","cleanup","off","onData","onEnd","onError","onAborted","clearTimeout","finish","error","body","concat","chunk","buffer","isBuffer","from","length","destroy","status","json","ok","push","now","Date","clientKey","getClientKey","state","getClientRateState","waitMs","consumeRateBudget","pause","resume","err","Error","on","ClientRateState","tokens","lastRefillMs","lastSeenMs","MAX_BURST_SECONDS","STALE_CLIENT_MS","clientRateStates","Map","lastCleanupMs","rawClientIp","clientIp","trim","rawIp","ip","maybeCleanupStates","size","key","delete","capacity","existing","get","Math","min","set","bytes","elapsedMs","max","ceil","Api","completeUpload","getStatus","initUpload","uploadChunk","rawBodyParser","getChunkSizeBytes","getMaxClientUploadBytesPerSecond","getRawBodyLimitBytes","SessionUser","Uploads","api","chunkSizeBytes","use","InitRoute","limitBytes","maxClientBytesPerSecond","post","put","ChunkRoute","get","StatusRoute","CompleteRoute"],"sources":["../src/uploads/api/file-uploads/processors/sanitizeSvg.ts","../src/uploads/api/file-uploads/processors/index.ts","../src/uploads/api/file-uploads/handlers/completeUpload.ts","../src/uploads/api/file-uploads/handlers/getStatus.ts","../src/uploads/api/file-uploads/index.ts","../src/uploads/api/file-uploads/handlers/initUpload.ts","../src/uploads/api/file-uploads/handlers/uploadChunk.ts","../src/uploads/api/file-uploads/middleware/rawBodyParser.ts","../src/uploads/api/file-uploads/handler.ts"],"sourcesContent":["import { JSDOM } from \"jsdom\"\nimport createDOMPurify from \"dompurify\"\n\nimport type { UploadFileProcessor } from \"./index\"\n\n\nconst MAX_SVG_BYTES = 128 * 1024\n\nconst window = new JSDOM(\"\").window\nconst DOMPurify = createDOMPurify(window)\n\nconst normalizeForSniff = (raw: string): string => raw.replace(/^\\uFEFF/, \"\").trimStart()\n\nconst looksLikeSvgText = (text: string): boolean => {\n const normalized = normalizeForSniff(text)\n if (!normalized.startsWith(\"<\")) return false\n return /<svg(?:\\s|>)/i.test(normalized)\n}\n\nexport const looksLikeSvg = (sniff: Buffer): boolean => looksLikeSvgText(sniff.toString(\"utf8\"))\n\nexport const sanitizeSvg = (svg: string): string =>\n DOMPurify.sanitize(svg, {\n USE_PROFILES: { svg: true, svgFilters: true },\n })\n\nexport const sanitizeSvgProcessor: UploadFileProcessor = {\n id: \"sanitize-svg\",\n maxBytes: MAX_SVG_BYTES,\n match: ({ sniff }) => looksLikeSvg(sniff),\n process: (data): { data: Buffer; mimeType: string } => {\n if (data.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n const svgText = data.toString(\"utf8\")\n if (!looksLikeSvgText(svgText)) {\n throw new Error(\"svg_invalid\")\n }\n\n const sanitized = sanitizeSvg(svgText)\n if (!sanitized.trim() || !looksLikeSvgText(sanitized)) {\n throw new Error(\"svg_sanitize_failed\")\n }\n\n const sanitizedBuffer = Buffer.from(sanitized, \"utf8\")\n if (sanitizedBuffer.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n return { data: sanitizedBuffer, mimeType: \"image/svg+xml\" }\n },\n}\n\n","import { sanitizeSvgProcessor } from \"./sanitizeSvg\"\n\n\nexport type UploadFileProcessorContext = {\n filename: string\n clientMimeType: string\n totalSize: number\n sniff: Buffer\n}\n\nexport type UploadFileProcessorResult = {\n data: Buffer\n mimeType?: string\n}\n\nexport type UploadFileProcessor = {\n id: string\n maxBytes: number\n match: (ctx: UploadFileProcessorContext) => boolean\n process: (data: Buffer, ctx: UploadFileProcessorContext) => Promise<UploadFileProcessorResult> | UploadFileProcessorResult\n}\n\nexport const uploadProcessors = Object.freeze([sanitizeSvgProcessor] satisfies UploadFileProcessor[])\n\nexport const getMaxUploadProcessorBytes = (): number =>\n uploadProcessors.reduce((max, processor) => Math.max(max, processor.maxBytes), 0)\n\nexport const selectUploadProcessors = (ctx: UploadFileProcessorContext): UploadFileProcessor[] =>\n uploadProcessors.filter((processor) => processor.match(ctx))\n\nexport const applyUploadProcessors = async (\n data: Buffer,\n ctx: Omit<UploadFileProcessorContext, \"sniff\" | \"totalSize\">,\n): Promise<{ data: Buffer; mimeType: string; applied: string[] }> => {\n let currentData = data\n let currentMimeType = ctx.clientMimeType\n const applied: string[] = []\n\n for (const processor of uploadProcessors) {\n const processorCtx: UploadFileProcessorContext = {\n filename: ctx.filename,\n clientMimeType: currentMimeType,\n totalSize: currentData.length,\n sniff: currentData,\n }\n\n if (!processor.match(processorCtx)) continue\n\n if (currentData.length > processor.maxBytes) {\n throw new Error(\"processor_input_too_large\")\n }\n\n const result = await processor.process(currentData, processorCtx)\n currentData = result.data\n if (typeof result.mimeType === \"string\" && result.mimeType.trim()) {\n currentMimeType = result.mimeType.trim()\n }\n applied.push(processor.id)\n }\n\n return {\n data: currentData,\n mimeType: currentMimeType,\n applied,\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb, models } from \"@rpcbase/db\"\nimport { GridFSBucket } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport { runUploadPostProcessors } from \"../postProcessors\"\nimport { applyUploadProcessors, getMaxUploadProcessorBytes, selectUploadProcessors } from \"../processors\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n ensureUploadIndexes,\n getBucketName,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n} from \"../shared\"\n\n\nconst waitForStreamFinished = async (stream: NodeJS.WritableStream): Promise<void> => new Promise((resolve, reject) => {\n stream.once(\"finish\", resolve)\n stream.once(\"error\", reject)\n})\n\nconst writeToStream = async (stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> => {\n const ok = stream.write(chunk)\n if (ok) return\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n cleanup()\n resolve()\n }\n\n const onError = (error: unknown) => {\n cleanup()\n reject(error)\n }\n\n const cleanup = () => {\n stream.off(\"drain\", onDrain)\n stream.off(\"error\", onError)\n }\n\n stream.on(\"drain\", onDrain)\n stream.on(\"error\", onError)\n })\n}\n\nconst abortUploadStream = async (stream: unknown): Promise<void> => {\n if (!stream) return\n if (typeof (stream as { abort?: unknown }).abort === \"function\") {\n try {\n await (stream as { abort: () => Promise<void> | void }).abort()\n return\n } catch {\n //\n }\n }\n try {\n ;(stream as { destroy?: () => void }).destroy?.()\n } catch {\n //\n }\n}\n\nexport const completeUpload: ApiHandler<Record<string, never>, Uploads.CompleteResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.CompleteResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const existing = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!existing) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (existing.status === \"done\" && existing.fileId) {\n return { ok: true, fileId: existing.fileId }\n }\n\n const locked = await UploadSession.findOneAndUpdate(\n { $and: [{ _id: uploadId }, { status: \"uploading\" }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"assembling\" }, $unset: { error: \"\" } },\n { returnDocument: \"after\" },\n ).lean()\n\n if (!locked) {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: \"filesystem_db_unavailable\" } },\n )\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n\n const lockedUserId = typeof locked.userId === \"string\" ? locked.userId : undefined\n const maxProcessorBytes = getMaxUploadProcessorBytes()\n const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes\n const declaredMimeType = locked.mimeType.trim().toLowerCase()\n const declaredSvg = declaredMimeType === \"image/svg+xml\" || locked.filename.trim().toLowerCase().endsWith(\".svg\")\n\n let uploadStream: NodeJS.WritableStream | null = null\n let finalMimeType = locked.mimeType\n let inlineProcessors: string[] = []\n let finalMetadata: Record<string, unknown> = {\n uploadId,\n tenantId,\n mimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n\n try {\n if (!shouldBufferForProcessing && declaredSvg) {\n throw new Error(\"svg_too_large\")\n }\n\n const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor() as unknown as AsyncIterable<UploadChunkDoc> & {\n close: () => Promise<void>\n }\n\n let expectedIndex = 0\n const chunks: Buffer[] = []\n let bufferedBytes = 0\n\n const pendingChunks: Buffer[] = []\n const sniffParts: Buffer[] = []\n let sniffBytes = 0\n\n try {\n for await (const chunkDoc of cursor) {\n if (chunkDoc.index !== expectedIndex) {\n throw new Error(\"missing_chunks\")\n }\n\n const chunk = chunkDoc.data\n\n if (shouldBufferForProcessing) {\n chunks.push(chunk)\n bufferedBytes += chunk.length\n } else if (!uploadStream) {\n pendingChunks.push(chunk)\n\n if (sniffBytes < maxProcessorBytes) {\n const slice = chunk.subarray(0, Math.min(chunk.length, maxProcessorBytes - sniffBytes))\n if (slice.length) {\n sniffParts.push(slice)\n sniffBytes += slice.length\n }\n }\n\n if (sniffBytes >= maxProcessorBytes) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(\"svg_too_large\")\n }\n\n finalMetadata = {\n uploadId,\n tenantId,\n mimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n } else {\n await writeToStream(uploadStream, chunk)\n }\n\n expectedIndex += 1\n }\n } finally {\n try {\n await cursor.close()\n } catch {\n //\n }\n }\n\n if (expectedIndex !== locked.chunksTotal) {\n throw new Error(\"missing_chunks\")\n }\n\n if (shouldBufferForProcessing) {\n const assembled = Buffer.concat(chunks, bufferedBytes)\n const { data: processed, mimeType: processedMimeType, applied } = await applyUploadProcessors(assembled, {\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n })\n\n finalMimeType = processedMimeType\n inlineProcessors = applied\n finalMetadata = {\n uploadId,\n tenantId,\n mimeType: processedMimeType,\n totalSize: locked.totalSize,\n ...(applied.length ? { processors: applied, processedSize: processed.length } : {}),\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end(processed)\n await finished\n } else {\n if (!uploadStream) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(\"svg_too_large\")\n }\n\n finalMetadata = {\n uploadId,\n tenantId,\n mimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end()\n await finished\n }\n\n const fileId = String((uploadStream as unknown as { id?: unknown }).id ?? \"\")\n if (!fileId) {\n throw new Error(\"missing_file_id\")\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"done\", fileId }, $unset: { error: \"\" } },\n )\n\n await runUploadPostProcessors({\n tenantId,\n uploadId,\n fileId,\n filename: locked.filename,\n mimeType: finalMimeType,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n inlineProcessors,\n metadata: finalMetadata,\n })\n\n try {\n await UploadChunk.deleteMany({ uploadId })\n } catch {\n //\n }\n\n return { ok: true, fileId }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n await abortUploadStream(uploadStream)\n\n if (message === \"missing_chunks\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"uploading\" } },\n )\n ctx.res.status(409)\n return { ok: false, error: \"missing_chunks\" }\n }\n\n if (message === \"svg_too_large\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(413)\n return { ok: false, error: message }\n }\n\n if (message === \"svg_invalid\" || message === \"svg_sanitize_failed\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(400)\n return { ok: false, error: message }\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n} from \"../shared\"\n\n\nexport const getStatus: ApiHandler<Record<string, never>, Uploads.StatusResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.StatusResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"read\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n const receivedDocs = await UploadChunk.find(\n { uploadId },\n { index: 1, _id: 0 },\n ).sort({ index: 1 }).lean()\n\n const received = (receivedDocs as unknown as Array<{ index?: unknown }>)\n .map((doc) => (typeof doc.index === \"number\" ? doc.index : -1))\n .filter((n) => Number.isInteger(n) && n >= 0)\n\n return {\n ok: true,\n status: session.status,\n chunkSize: session.chunkSize,\n chunksTotal: session.chunksTotal,\n received,\n ...(session.fileId ? { fileId: session.fileId } : {}),\n }\n}\n","import { z } from \"zod\"\n\n\nexport const InitRoute = \"/api/rb/file-uploads\"\nexport const ChunkRoute = \"/api/rb/file-uploads/:uploadId/chunks/:index\"\nexport const StatusRoute = \"/api/rb/file-uploads/:uploadId/status\"\nexport const CompleteRoute = \"/api/rb/file-uploads/:uploadId/complete\"\n\nexport const initRequestSchema = z.object({\n filename: z.string().min(1),\n mimeType: z.string().min(1),\n isPublic: z.boolean().optional(),\n totalSize: z.number().int().min(1),\n})\n\nexport type InitRequestPayload = z.infer<typeof initRequestSchema>\n\nexport const initResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n uploadId: z.string().optional(),\n uploadKey: z.string().optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n})\n\nexport type InitResponsePayload = z.infer<typeof initResponseSchema>\n\nexport const statusResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n status: z.enum([\"uploading\", \"assembling\", \"done\", \"error\"]).optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n received: z.array(z.number().int().min(0)).optional(),\n fileId: z.string().optional(),\n})\n\nexport type StatusResponsePayload = z.infer<typeof statusResponseSchema>\n\nexport const completeResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n fileId: z.string().optional(),\n})\n\nexport type CompleteResponsePayload = z.infer<typeof completeResponseSchema>\n","import { randomBytes } from \"node:crypto\"\n\nimport { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { ObjectId } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n computeSha256Hex,\n ensureUploadIndexes,\n getChunkSizeBytes,\n getModelCtx,\n getSessionTtlMs,\n getTenantId,\n getUserId,\n} from \"../shared\"\n\n\nexport const initUpload: ApiHandler<Uploads.InitRequestPayload, Uploads.InitResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<Uploads.InitResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const userId = getUserId(ctx)\n\n const parsed = Uploads.initRequestSchema.safeParse(payload ?? {})\n if (!parsed.success) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_payload\" }\n }\n\n const chunkSize = getChunkSizeBytes()\n const { filename, mimeType, totalSize, isPublic } = parsed.data\n const chunksTotal = Math.ceil(totalSize / chunkSize)\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const uploadId = new ObjectId().toString()\n const now = Date.now()\n const expiresAt = new Date(now + getSessionTtlMs())\n\n const uploadKey = userId ? null : randomBytes(32).toString(\"base64url\")\n const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : undefined\n\n await UploadSession.create({\n _id: uploadId,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n filename,\n mimeType,\n ...(typeof isPublic === \"boolean\" ? { isPublic } : {}),\n totalSize,\n chunkSize,\n chunksTotal,\n status: \"uploading\",\n createdAt: new Date(now),\n expiresAt,\n })\n\n return {\n ok: true,\n uploadId,\n chunkSize,\n chunksTotal,\n ...(uploadKey ? { uploadKey } : {}),\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n computeSha256Hex,\n ensureUploadIndexes,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n normalizeSha256Hex,\n toBufferPayload,\n} from \"../shared\"\n\n\ntype ChunkResponsePayload = {\n ok: boolean\n error?: string\n}\n\nexport const uploadChunk: ApiHandler<Buffer, ChunkResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<ChunkResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n const indexRaw = String(ctx.req.params?.index ?? \"\").trim()\n const index = Number(indexRaw)\n\n if (!uploadId || !Number.isInteger(index) || index < 0) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_ref\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (session.status !== \"uploading\") {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n if (index >= session.chunksTotal) {\n ctx.res.status(400)\n return { ok: false, error: \"index_out_of_range\" }\n }\n\n const data = toBufferPayload(payload)\n if (!data) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_body\" }\n }\n\n const expectedSize = index === session.chunksTotal - 1\n ? session.totalSize - session.chunkSize * (session.chunksTotal - 1)\n : session.chunkSize\n\n if (data.length > expectedSize) {\n ctx.res.status(413)\n return { ok: false, error: \"chunk_too_large\" }\n }\n\n if (data.length !== expectedSize) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_size\" }\n }\n\n const checksumHeader = ctx.req.get(\"X-Chunk-SHA256\")\n const sha256 = checksumHeader ? computeSha256Hex(data) : undefined\n\n if (checksumHeader) {\n const expectedSha256 = normalizeSha256Hex(checksumHeader)\n if (sha256 !== expectedSha256) {\n ctx.res.status(400)\n return { ok: false, error: \"checksum_mismatch\" }\n }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n await UploadChunk.updateOne(\n { uploadId, index },\n {\n $set: {\n uploadId,\n index,\n data,\n size: data.length,\n sha256,\n expiresAt: session.expiresAt,\n },\n $setOnInsert: {\n createdAt: new Date(),\n },\n },\n { upsert: true },\n )\n\n ctx.res.status(204)\n return { ok: true }\n}\n","export const rawBodyParser = ({\n limitBytes,\n maxClientBytesPerSecond,\n}: {\n limitBytes: number\n maxClientBytesPerSecond?: number | null\n}) => {\n return (req: any, res: any, next: any) => {\n const contentType = typeof req?.headers?.[\"content-type\"] === \"string\"\n ? String(req.headers[\"content-type\"])\n : \"\"\n\n if (!contentType.includes(\"application/octet-stream\")) {\n next()\n return\n }\n\n let total = 0\n const chunks: Buffer[] = []\n let done = false\n let paused = false\n let throttleTimeout: ReturnType<typeof setTimeout> | null = null\n\n const rateBytesPerSecond = typeof maxClientBytesPerSecond === \"number\" && maxClientBytesPerSecond > 0\n ? maxClientBytesPerSecond\n : null\n\n const cleanup = () => {\n req.off(\"data\", onData)\n req.off(\"end\", onEnd)\n req.off(\"error\", onError)\n req.off(\"aborted\", onAborted)\n if (throttleTimeout) {\n clearTimeout(throttleTimeout)\n throttleTimeout = null\n }\n }\n\n const finish = (error?: unknown) => {\n if (done) return\n done = true\n\n cleanup()\n\n if (error) {\n next(error)\n return\n }\n\n req.body = Buffer.concat(chunks, total)\n next()\n }\n\n const onData = (chunk: any) => {\n if (done) return\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n total += buffer.length\n\n if (total > limitBytes) {\n done = true\n cleanup()\n req.destroy()\n res.status(413).json({ ok: false, error: \"chunk_too_large\" })\n return\n }\n\n chunks.push(buffer)\n\n if (!rateBytesPerSecond) return\n\n const now = Date.now()\n const clientKey = getClientKey(req)\n const state = getClientRateState(clientKey, rateBytesPerSecond, now)\n const waitMs = consumeRateBudget(state, buffer.length, rateBytesPerSecond, now)\n\n if (waitMs > 0 && !paused) {\n paused = true\n req.pause()\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null\n paused = false\n if (done) return\n try {\n req.resume()\n } catch {\n //\n }\n }, waitMs)\n }\n }\n\n const onEnd = () => finish()\n const onError = (err: unknown) => finish(err)\n const onAborted = () => finish(new Error(\"request_aborted\"))\n\n req.on(\"data\", onData)\n req.on(\"end\", onEnd)\n req.on(\"error\", onError)\n req.on(\"aborted\", onAborted)\n }\n}\n\ntype ClientRateState = {\n tokens: number\n lastRefillMs: number\n lastSeenMs: number\n}\n\nconst MAX_BURST_SECONDS = 1\nconst STALE_CLIENT_MS = 15 * 60 * 1000\n\nconst clientRateStates = new Map<string, ClientRateState>()\nlet lastCleanupMs = 0\n\nconst getClientKey = (req: any): string => {\n const rawClientIp = typeof req?.clientIp === \"string\" ? req.clientIp : \"\"\n if (rawClientIp.trim()) return rawClientIp.trim()\n const rawIp = typeof req?.ip === \"string\" ? req.ip : \"\"\n return rawIp.trim() || \"unknown\"\n}\n\nconst maybeCleanupStates = (now: number) => {\n if (now - lastCleanupMs < 60_000) return\n lastCleanupMs = now\n\n if (clientRateStates.size < 2000) return\n\n for (const [key, state] of clientRateStates) {\n if (now - state.lastSeenMs > STALE_CLIENT_MS) {\n clientRateStates.delete(key)\n }\n }\n}\n\nconst getClientRateState = (key: string, rateBytesPerSecond: number, now: number): ClientRateState => {\n maybeCleanupStates(now)\n\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const existing = clientRateStates.get(key)\n if (existing) {\n existing.lastSeenMs = now\n existing.tokens = Math.min(capacity, existing.tokens)\n return existing\n }\n\n const next: ClientRateState = {\n tokens: capacity,\n lastRefillMs: now,\n lastSeenMs: now,\n }\n clientRateStates.set(key, next)\n return next\n}\n\nconst consumeRateBudget = (\n state: ClientRateState,\n bytes: number,\n rateBytesPerSecond: number,\n now: number,\n): number => {\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const elapsedMs = Math.max(0, now - state.lastRefillMs)\n\n if (elapsedMs > 0) {\n state.tokens = Math.min(capacity, state.tokens + (elapsedMs * rateBytesPerSecond) / 1000)\n state.lastRefillMs = now\n }\n\n state.tokens -= bytes\n\n if (state.tokens >= 0) return 0\n return Math.ceil((-state.tokens / rateBytesPerSecond) * 1000)\n}\n","import { Api } from \"@rpcbase/api\"\n\nimport { completeUpload } from \"./handlers/completeUpload\"\nimport { getStatus } from \"./handlers/getStatus\"\nimport { initUpload } from \"./handlers/initUpload\"\nimport { uploadChunk } from \"./handlers/uploadChunk\"\nimport { rawBodyParser } from \"./middleware/rawBodyParser\"\nimport { getChunkSizeBytes, getMaxClientUploadBytesPerSecond, getRawBodyLimitBytes, type SessionUser } from \"./shared\"\n\nimport * as Uploads from \"./index\"\n\n\nexport default (api: Api<SessionUser>) => {\n const chunkSizeBytes = getChunkSizeBytes()\n api.use(\n Uploads.InitRoute,\n rawBodyParser({\n limitBytes: getRawBodyLimitBytes(chunkSizeBytes),\n maxClientBytesPerSecond: getMaxClientUploadBytesPerSecond(),\n }),\n )\n\n api.post(Uploads.InitRoute, initUpload)\n api.put(Uploads.ChunkRoute, uploadChunk)\n api.get(Uploads.StatusRoute, getStatus)\n api.post(Uploads.CompleteRoute, completeUpload)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"handler-BU_tEK6x.js","names":["JSDOM","createDOMPurify","UploadFileProcessor","MAX_SVG_BYTES","window","DOMPurify","normalizeForSniff","raw","replace","trimStart","looksLikeSvgText","text","normalized","startsWith","test","looksLikeSvg","sniff","Buffer","toString","sanitizeSvg","svg","sanitize","USE_PROFILES","svgFilters","sanitizeSvgProcessor","id","maxBytes","match","process","data","mimeType","length","Error","svgText","sanitized","trim","sanitizedBuffer","from","sanitizeSvgProcessor","UploadFileProcessorContext","filename","clientMimeType","totalSize","sniff","Buffer","UploadFileProcessorResult","data","mimeType","UploadFileProcessor","id","maxBytes","match","ctx","process","Promise","uploadProcessors","Object","freeze","getMaxUploadProcessorBytes","reduce","max","processor","Math","selectUploadProcessors","filter","applyUploadProcessors","Omit","applied","currentData","currentMimeType","processorCtx","length","Error","result","trim","push","ApiHandler","getTenantFilesystemDb","models","GridFSBucket","Model","Uploads","runUploadPostProcessors","applyUploadProcessors","getMaxUploadProcessorBytes","selectUploadProcessors","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","ensureUploadIndexes","getBucketName","getModelCtx","getUploadSessionAccessQuery","getTenantId","waitForStreamFinished","stream","NodeJS","WritableStream","Promise","resolve","reject","once","writeToStream","chunk","Buffer","ok","write","onDrain","cleanup","onError","error","off","on","abortUploadStream","abort","destroy","completeUpload","Record","CompleteResponsePayload","_payload","ctx","tenantId","res","status","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","existing","findOne","$and","_id","lean","fileId","locked","findOneAndUpdate","$set","$unset","returnDocument","fsDb","nativeDb","db","updateOne","bucketName","bucket","lockedUserId","userId","undefined","maxProcessorBytes","shouldBufferForProcessing","totalSize","declaredMimeType","mimeType","toLowerCase","declaredSvg","filename","endsWith","uploadStream","finalMimeType","inlineProcessors","finalMetadata","isPublic","ownerKeyHash","Error","cursor","find","sort","index","AsyncIterable","close","expectedIndex","chunks","bufferedBytes","pendingChunks","sniffParts","sniffBytes","chunkDoc","data","push","length","slice","subarray","Math","min","sniff","concat","processors","clientMimeType","openUploadStream","metadata","pending","chunksTotal","assembled","processed","processedMimeType","applied","processedSize","finished","end","id","deleteMany","message","ApiHandler","models","Model","Uploads","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","getModelCtx","getUploadSessionAccessQuery","getTenantId","getStatus","Record","StatusResponsePayload","_payload","ctx","Promise","tenantId","res","status","ok","error","uploadId","String","req","params","trim","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","receivedDocs","find","index","sort","received","Array","map","doc","filter","n","Number","isInteger","chunkSize","chunksTotal","fileId","z","InitRoute","ChunkRoute","StatusRoute","CompleteRoute","initRequestSchema","object","filename","string","min","mimeType","isPublic","boolean","optional","totalSize","number","int","InitRequestPayload","infer","initResponseSchema","ok","error","uploadId","uploadKey","chunkSize","chunksTotal","InitResponsePayload","statusResponseSchema","status","enum","received","array","fileId","StatusResponsePayload","completeResponseSchema","CompleteResponsePayload","randomBytes","ApiHandler","models","ObjectId","Model","Uploads","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","computeSha256Hex","ensureUploadIndexes","getChunkSizeBytes","getModelCtx","getSessionTtlMs","getTenantId","getUserId","initUpload","InitRequestPayload","InitResponsePayload","payload","ctx","Promise","tenantId","res","status","ok","error","userId","parsed","initRequestSchema","safeParse","success","chunkSize","filename","mimeType","totalSize","isPublic","data","chunksTotal","Math","ceil","ability","modelCtx","UploadSession","UploadChunk","all","get","uploadId","toString","now","Date","expiresAt","uploadKey","ownerKeyHash","Buffer","from","undefined","create","_id","createdAt","ApiHandler","models","Model","SessionUser","UploadChunkDoc","UploadSessionDoc","buildUploadsAbility","computeSha256Hex","ensureUploadIndexes","getModelCtx","getUploadSessionAccessQuery","getTenantId","normalizeSha256Hex","toBufferPayload","ChunkResponsePayload","ok","error","uploadChunk","Buffer","payload","ctx","Promise","tenantId","res","status","uploadId","String","req","params","trim","indexRaw","index","Number","isInteger","ability","modelCtx","UploadSession","UploadChunk","all","get","can","session","findOne","$and","_id","lean","chunksTotal","data","expectedSize","totalSize","chunkSize","length","checksumHeader","sha256","undefined","expectedSha256","updateOne","$set","size","expiresAt","$setOnInsert","createdAt","Date","upsert","rawBodyParser","limitBytes","maxClientBytesPerSecond","req","res","next","contentType","headers","String","includes","total","chunks","Buffer","done","paused","throttleTimeout","ReturnType","setTimeout","rateBytesPerSecond","cleanup","off","onData","onEnd","onError","onAborted","clearTimeout","finish","error","body","concat","chunk","buffer","isBuffer","from","length","destroy","status","json","ok","push","now","Date","clientKey","getClientKey","state","getClientRateState","waitMs","consumeRateBudget","pause","resume","err","Error","on","ClientRateState","tokens","lastRefillMs","lastSeenMs","MAX_BURST_SECONDS","STALE_CLIENT_MS","clientRateStates","Map","lastCleanupMs","rawClientIp","clientIp","trim","rawIp","ip","maybeCleanupStates","size","key","delete","capacity","existing","get","Math","min","set","bytes","elapsedMs","max","ceil","Api","completeUpload","getStatus","initUpload","uploadChunk","rawBodyParser","getChunkSizeBytes","getMaxClientUploadBytesPerSecond","getRawBodyLimitBytes","SessionUser","Uploads","api","chunkSizeBytes","use","InitRoute","limitBytes","maxClientBytesPerSecond","post","put","ChunkRoute","get","StatusRoute","CompleteRoute"],"sources":["../src/uploads/api/file-uploads/processors/sanitizeSvg.ts","../src/uploads/api/file-uploads/processors/index.ts","../src/uploads/api/file-uploads/handlers/completeUpload.ts","../src/uploads/api/file-uploads/handlers/getStatus.ts","../src/uploads/api/file-uploads/index.ts","../src/uploads/api/file-uploads/handlers/initUpload.ts","../src/uploads/api/file-uploads/handlers/uploadChunk.ts","../src/uploads/api/file-uploads/middleware/rawBodyParser.ts","../src/uploads/api/file-uploads/handler.ts"],"sourcesContent":["import { JSDOM } from \"jsdom\"\nimport createDOMPurify from \"dompurify\"\n\nimport type { UploadFileProcessor } from \"./index\"\n\n\nconst MAX_SVG_BYTES = 128 * 1024\n\nconst window = new JSDOM(\"\").window\nconst DOMPurify = createDOMPurify(window)\n\nconst normalizeForSniff = (raw: string): string => raw.replace(/^\\uFEFF/, \"\").trimStart()\n\nconst looksLikeSvgText = (text: string): boolean => {\n const normalized = normalizeForSniff(text)\n if (!normalized.startsWith(\"<\")) return false\n return /<svg(?:\\s|>)/i.test(normalized)\n}\n\nexport const looksLikeSvg = (sniff: Buffer): boolean => looksLikeSvgText(sniff.toString(\"utf8\"))\n\nexport const sanitizeSvg = (svg: string): string =>\n DOMPurify.sanitize(svg, {\n USE_PROFILES: { svg: true, svgFilters: true },\n })\n\nexport const sanitizeSvgProcessor: UploadFileProcessor = {\n id: \"sanitize-svg\",\n maxBytes: MAX_SVG_BYTES,\n match: ({ sniff }) => looksLikeSvg(sniff),\n process: (data): { data: Buffer; mimeType: string } => {\n if (data.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n const svgText = data.toString(\"utf8\")\n if (!looksLikeSvgText(svgText)) {\n throw new Error(\"svg_invalid\")\n }\n\n const sanitized = sanitizeSvg(svgText)\n if (!sanitized.trim() || !looksLikeSvgText(sanitized)) {\n throw new Error(\"svg_sanitize_failed\")\n }\n\n const sanitizedBuffer = Buffer.from(sanitized, \"utf8\")\n if (sanitizedBuffer.length > MAX_SVG_BYTES) {\n throw new Error(\"svg_too_large\")\n }\n\n return { data: sanitizedBuffer, mimeType: \"image/svg+xml\" }\n },\n}\n\n","import { sanitizeSvgProcessor } from \"./sanitizeSvg\"\n\n\nexport type UploadFileProcessorContext = {\n filename: string\n clientMimeType: string\n totalSize: number\n sniff: Buffer\n}\n\nexport type UploadFileProcessorResult = {\n data: Buffer\n mimeType?: string\n}\n\nexport type UploadFileProcessor = {\n id: string\n maxBytes: number\n match: (ctx: UploadFileProcessorContext) => boolean\n process: (data: Buffer, ctx: UploadFileProcessorContext) => Promise<UploadFileProcessorResult> | UploadFileProcessorResult\n}\n\nexport const uploadProcessors = Object.freeze([sanitizeSvgProcessor] satisfies UploadFileProcessor[])\n\nexport const getMaxUploadProcessorBytes = (): number =>\n uploadProcessors.reduce((max, processor) => Math.max(max, processor.maxBytes), 0)\n\nexport const selectUploadProcessors = (ctx: UploadFileProcessorContext): UploadFileProcessor[] =>\n uploadProcessors.filter((processor) => processor.match(ctx))\n\nexport const applyUploadProcessors = async (\n data: Buffer,\n ctx: Omit<UploadFileProcessorContext, \"sniff\" | \"totalSize\">,\n): Promise<{ data: Buffer; mimeType: string; applied: string[] }> => {\n let currentData = data\n let currentMimeType = ctx.clientMimeType\n const applied: string[] = []\n\n for (const processor of uploadProcessors) {\n const processorCtx: UploadFileProcessorContext = {\n filename: ctx.filename,\n clientMimeType: currentMimeType,\n totalSize: currentData.length,\n sniff: currentData,\n }\n\n if (!processor.match(processorCtx)) continue\n\n if (currentData.length > processor.maxBytes) {\n throw new Error(\"processor_input_too_large\")\n }\n\n const result = await processor.process(currentData, processorCtx)\n currentData = result.data\n if (typeof result.mimeType === \"string\" && result.mimeType.trim()) {\n currentMimeType = result.mimeType.trim()\n }\n applied.push(processor.id)\n }\n\n return {\n data: currentData,\n mimeType: currentMimeType,\n applied,\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { getTenantFilesystemDb, models } from \"@rpcbase/db\"\nimport { GridFSBucket } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport { runUploadPostProcessors } from \"../postProcessors\"\nimport { applyUploadProcessors, getMaxUploadProcessorBytes, selectUploadProcessors } from \"../processors\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n ensureUploadIndexes,\n getBucketName,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n} from \"../shared\"\n\n\nconst waitForStreamFinished = async (stream: NodeJS.WritableStream): Promise<void> => new Promise((resolve, reject) => {\n stream.once(\"finish\", resolve)\n stream.once(\"error\", reject)\n})\n\nconst writeToStream = async (stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> => {\n const ok = stream.write(chunk)\n if (ok) return\n await new Promise<void>((resolve, reject) => {\n const onDrain = () => {\n cleanup()\n resolve()\n }\n\n const onError = (error: unknown) => {\n cleanup()\n reject(error)\n }\n\n const cleanup = () => {\n stream.off(\"drain\", onDrain)\n stream.off(\"error\", onError)\n }\n\n stream.on(\"drain\", onDrain)\n stream.on(\"error\", onError)\n })\n}\n\nconst abortUploadStream = async (stream: unknown): Promise<void> => {\n if (!stream) return\n if (typeof (stream as { abort?: unknown }).abort === \"function\") {\n try {\n await (stream as { abort: () => Promise<void> | void }).abort()\n return\n } catch {\n //\n }\n }\n try {\n ;(stream as { destroy?: () => void }).destroy?.()\n } catch {\n //\n }\n}\n\nexport const completeUpload: ApiHandler<Record<string, never>, Uploads.CompleteResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.CompleteResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const existing = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!existing) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (existing.status === \"done\" && existing.fileId) {\n return { ok: true, fileId: existing.fileId }\n }\n\n const locked = await UploadSession.findOneAndUpdate(\n { $and: [{ _id: uploadId }, { status: \"uploading\" }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"assembling\" }, $unset: { error: \"\" } },\n { returnDocument: \"after\" },\n ).lean()\n\n if (!locked) {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const fsDb = await getTenantFilesystemDb(tenantId)\n const nativeDb = fsDb.db\n if (!nativeDb) {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: \"filesystem_db_unavailable\" } },\n )\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n const bucketName = getBucketName()\n const bucket = new GridFSBucket(nativeDb, { bucketName })\n\n const lockedUserId = typeof locked.userId === \"string\" ? locked.userId : undefined\n const maxProcessorBytes = getMaxUploadProcessorBytes()\n const shouldBufferForProcessing = locked.totalSize <= maxProcessorBytes\n const declaredMimeType = locked.mimeType.trim().toLowerCase()\n const declaredSvg = declaredMimeType === \"image/svg+xml\" || locked.filename.trim().toLowerCase().endsWith(\".svg\")\n\n let uploadStream: NodeJS.WritableStream | null = null\n let finalMimeType = locked.mimeType\n let inlineProcessors: string[] = []\n let finalMetadata: Record<string, unknown> = {\n uploadId,\n tenantId,\n mimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n\n try {\n if (!shouldBufferForProcessing && declaredSvg) {\n throw new Error(\"svg_too_large\")\n }\n\n const cursor = UploadChunk.find({ uploadId }).sort({ index: 1 }).cursor() as unknown as AsyncIterable<UploadChunkDoc> & {\n close: () => Promise<void>\n }\n\n let expectedIndex = 0\n const chunks: Buffer[] = []\n let bufferedBytes = 0\n\n const pendingChunks: Buffer[] = []\n const sniffParts: Buffer[] = []\n let sniffBytes = 0\n\n try {\n for await (const chunkDoc of cursor) {\n if (chunkDoc.index !== expectedIndex) {\n throw new Error(\"missing_chunks\")\n }\n\n const chunk = chunkDoc.data\n\n if (shouldBufferForProcessing) {\n chunks.push(chunk)\n bufferedBytes += chunk.length\n } else if (!uploadStream) {\n pendingChunks.push(chunk)\n\n if (sniffBytes < maxProcessorBytes) {\n const slice = chunk.subarray(0, Math.min(chunk.length, maxProcessorBytes - sniffBytes))\n if (slice.length) {\n sniffParts.push(slice)\n sniffBytes += slice.length\n }\n }\n\n if (sniffBytes >= maxProcessorBytes) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(\"svg_too_large\")\n }\n\n finalMetadata = {\n uploadId,\n tenantId,\n mimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n } else {\n await writeToStream(uploadStream, chunk)\n }\n\n expectedIndex += 1\n }\n } finally {\n try {\n await cursor.close()\n } catch {\n //\n }\n }\n\n if (expectedIndex !== locked.chunksTotal) {\n throw new Error(\"missing_chunks\")\n }\n\n if (shouldBufferForProcessing) {\n const assembled = Buffer.concat(chunks, bufferedBytes)\n const { data: processed, mimeType: processedMimeType, applied } = await applyUploadProcessors(assembled, {\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n })\n\n finalMimeType = processedMimeType\n inlineProcessors = applied\n finalMetadata = {\n uploadId,\n tenantId,\n mimeType: processedMimeType,\n totalSize: locked.totalSize,\n ...(applied.length ? { processors: applied, processedSize: processed.length } : {}),\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end(processed)\n await finished\n } else {\n if (!uploadStream) {\n const sniff = Buffer.concat(sniffParts, sniffBytes)\n const processors = selectUploadProcessors({\n filename: locked.filename,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n sniff,\n })\n\n if (processors.length) {\n throw new Error(\"svg_too_large\")\n }\n\n finalMetadata = {\n uploadId,\n tenantId,\n mimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n }\n uploadStream = bucket.openUploadStream(locked.filename, {\n metadata: finalMetadata,\n })\n\n for (const pending of pendingChunks) {\n await writeToStream(uploadStream, pending)\n }\n pendingChunks.length = 0\n }\n\n const finished = waitForStreamFinished(uploadStream)\n uploadStream.end()\n await finished\n }\n\n const fileId = String((uploadStream as unknown as { id?: unknown }).id ?? \"\")\n if (!fileId) {\n throw new Error(\"missing_file_id\")\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"done\", fileId }, $unset: { error: \"\" } },\n )\n\n await runUploadPostProcessors({\n tenantId,\n uploadId,\n fileId,\n filename: locked.filename,\n mimeType: finalMimeType,\n clientMimeType: locked.mimeType,\n totalSize: locked.totalSize,\n ...(typeof locked.isPublic === \"boolean\" ? { isPublic: locked.isPublic } : {}),\n ...(typeof locked.ownerKeyHash === \"string\" ? { ownerKeyHash: locked.ownerKeyHash } : {}),\n ...(lockedUserId ? { userId: lockedUserId } : {}),\n inlineProcessors,\n metadata: finalMetadata,\n })\n\n try {\n await UploadChunk.deleteMany({ uploadId })\n } catch {\n //\n }\n\n return { ok: true, fileId }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n await abortUploadStream(uploadStream)\n\n if (message === \"missing_chunks\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"uploading\" } },\n )\n ctx.res.status(409)\n return { ok: false, error: \"missing_chunks\" }\n }\n\n if (message === \"svg_too_large\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(413)\n return { ok: false, error: message }\n }\n\n if (message === \"svg_invalid\" || message === \"svg_sanitize_failed\") {\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n ctx.res.status(400)\n return { ok: false, error: message }\n }\n\n await UploadSession.updateOne(\n { $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] },\n { $set: { status: \"error\", error: message } },\n )\n\n ctx.res.status(500)\n return { ok: false, error: \"assembly_failed\" }\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n} from \"../shared\"\n\n\nexport const getStatus: ApiHandler<Record<string, never>, Uploads.StatusResponsePayload, SessionUser> = async (\n _payload,\n ctx,\n): Promise<Uploads.StatusResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n if (!uploadId) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_upload_id\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"read\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"read\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n const receivedDocs = await UploadChunk.find(\n { uploadId },\n { index: 1, _id: 0 },\n ).sort({ index: 1 }).lean()\n\n const received = (receivedDocs as unknown as Array<{ index?: unknown }>)\n .map((doc) => (typeof doc.index === \"number\" ? doc.index : -1))\n .filter((n) => Number.isInteger(n) && n >= 0)\n\n return {\n ok: true,\n status: session.status,\n chunkSize: session.chunkSize,\n chunksTotal: session.chunksTotal,\n received,\n ...(session.fileId ? { fileId: session.fileId } : {}),\n }\n}\n","import { z } from \"zod\"\n\n\nexport const InitRoute = \"/api/rb/file-uploads\"\nexport const ChunkRoute = \"/api/rb/file-uploads/:uploadId/chunks/:index\"\nexport const StatusRoute = \"/api/rb/file-uploads/:uploadId/status\"\nexport const CompleteRoute = \"/api/rb/file-uploads/:uploadId/complete\"\n\nexport const initRequestSchema = z.object({\n filename: z.string().min(1),\n mimeType: z.string().min(1),\n isPublic: z.boolean().optional(),\n totalSize: z.number().int().min(1),\n})\n\nexport type InitRequestPayload = z.infer<typeof initRequestSchema>\n\nexport const initResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n uploadId: z.string().optional(),\n uploadKey: z.string().optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n})\n\nexport type InitResponsePayload = z.infer<typeof initResponseSchema>\n\nexport const statusResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n status: z.enum([\"uploading\", \"assembling\", \"done\", \"error\"]).optional(),\n chunkSize: z.number().int().optional(),\n chunksTotal: z.number().int().optional(),\n received: z.array(z.number().int().min(0)).optional(),\n fileId: z.string().optional(),\n})\n\nexport type StatusResponsePayload = z.infer<typeof statusResponseSchema>\n\nexport const completeResponseSchema = z.object({\n ok: z.boolean(),\n error: z.string().optional(),\n fileId: z.string().optional(),\n})\n\nexport type CompleteResponsePayload = z.infer<typeof completeResponseSchema>\n","import { randomBytes } from \"node:crypto\"\n\nimport { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { ObjectId } from \"mongodb\"\nimport type { Model } from \"mongoose\"\n\nimport * as Uploads from \"../index\"\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n computeSha256Hex,\n ensureUploadIndexes,\n getChunkSizeBytes,\n getModelCtx,\n getSessionTtlMs,\n getTenantId,\n getUserId,\n} from \"../shared\"\n\n\nexport const initUpload: ApiHandler<Uploads.InitRequestPayload, Uploads.InitResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<Uploads.InitResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const userId = getUserId(ctx)\n\n const parsed = Uploads.initRequestSchema.safeParse(payload ?? {})\n if (!parsed.success) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_payload\" }\n }\n\n const chunkSize = getChunkSizeBytes()\n const { filename, mimeType, totalSize, isPublic } = parsed.data\n const chunksTotal = Math.ceil(totalSize / chunkSize)\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n const uploadId = new ObjectId().toString()\n const now = Date.now()\n const expiresAt = new Date(now + getSessionTtlMs())\n\n const uploadKey = userId ? null : randomBytes(32).toString(\"base64url\")\n const ownerKeyHash = uploadKey ? computeSha256Hex(Buffer.from(uploadKey)) : undefined\n\n await UploadSession.create({\n _id: uploadId,\n ...(userId ? { userId } : {}),\n ...(ownerKeyHash ? { ownerKeyHash } : {}),\n filename,\n mimeType,\n ...(typeof isPublic === \"boolean\" ? { isPublic } : {}),\n totalSize,\n chunkSize,\n chunksTotal,\n status: \"uploading\",\n createdAt: new Date(now),\n expiresAt,\n })\n\n return {\n ok: true,\n uploadId,\n chunkSize,\n chunksTotal,\n ...(uploadKey ? { uploadKey } : {}),\n }\n}\n","import { ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport type { Model } from \"mongoose\"\n\nimport {\n type SessionUser,\n type UploadChunkDoc,\n type UploadSessionDoc,\n buildUploadsAbility,\n computeSha256Hex,\n ensureUploadIndexes,\n getModelCtx,\n getUploadSessionAccessQuery,\n getTenantId,\n normalizeSha256Hex,\n toBufferPayload,\n} from \"../shared\"\n\n\ntype ChunkResponsePayload = {\n ok: boolean\n error?: string\n}\n\nexport const uploadChunk: ApiHandler<Buffer, ChunkResponsePayload, SessionUser> = async (\n payload,\n ctx,\n): Promise<ChunkResponsePayload> => {\n const tenantId = getTenantId(ctx)\n if (!tenantId) {\n ctx.res.status(400)\n return { ok: false, error: \"tenant_missing\" }\n }\n\n const uploadId = String(ctx.req.params?.uploadId ?? \"\").trim()\n const indexRaw = String(ctx.req.params?.index ?? \"\").trim()\n const index = Number(indexRaw)\n\n if (!uploadId || !Number.isInteger(index) || index < 0) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_ref\" }\n }\n\n const ability = buildUploadsAbility(ctx, tenantId)\n const modelCtx = getModelCtx(ctx, tenantId, ability)\n\n const [UploadSession, UploadChunk] = await Promise.all([\n models.get(\"RBUploadSession\", modelCtx) as Promise<Model<UploadSessionDoc>>,\n models.get(\"RBUploadChunk\", modelCtx) as Promise<Model<UploadChunkDoc>>,\n ])\n\n if (!ability.can(\"update\", \"RBUploadSession\")) {\n ctx.res.status(401)\n return { ok: false, error: \"unauthorized\" }\n }\n\n const session = await UploadSession.findOne({ $and: [{ _id: uploadId }, getUploadSessionAccessQuery(ability, \"update\")] }).lean()\n if (!session) {\n ctx.res.status(404)\n return { ok: false, error: \"not_found\" }\n }\n\n if (session.status !== \"uploading\") {\n ctx.res.status(409)\n return { ok: false, error: \"not_uploading\" }\n }\n\n if (index >= session.chunksTotal) {\n ctx.res.status(400)\n return { ok: false, error: \"index_out_of_range\" }\n }\n\n const data = toBufferPayload(payload)\n if (!data) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_body\" }\n }\n\n const expectedSize = index === session.chunksTotal - 1\n ? session.totalSize - session.chunkSize * (session.chunksTotal - 1)\n : session.chunkSize\n\n if (data.length > expectedSize) {\n ctx.res.status(413)\n return { ok: false, error: \"chunk_too_large\" }\n }\n\n if (data.length !== expectedSize) {\n ctx.res.status(400)\n return { ok: false, error: \"invalid_chunk_size\" }\n }\n\n const checksumHeader = ctx.req.get(\"X-Chunk-SHA256\")\n const sha256 = checksumHeader ? computeSha256Hex(data) : undefined\n\n if (checksumHeader) {\n const expectedSha256 = normalizeSha256Hex(checksumHeader)\n if (sha256 !== expectedSha256) {\n ctx.res.status(400)\n return { ok: false, error: \"checksum_mismatch\" }\n }\n }\n\n await ensureUploadIndexes(UploadSession, UploadChunk)\n\n await UploadChunk.updateOne(\n { uploadId, index },\n {\n $set: {\n uploadId,\n index,\n data,\n size: data.length,\n sha256,\n expiresAt: session.expiresAt,\n },\n $setOnInsert: {\n createdAt: new Date(),\n },\n },\n { upsert: true },\n )\n\n ctx.res.status(204)\n return { ok: true }\n}\n","export const rawBodyParser = ({\n limitBytes,\n maxClientBytesPerSecond,\n}: {\n limitBytes: number\n maxClientBytesPerSecond?: number | null\n}) => {\n return (req: any, res: any, next: any) => {\n const contentType = typeof req?.headers?.[\"content-type\"] === \"string\"\n ? String(req.headers[\"content-type\"])\n : \"\"\n\n if (!contentType.includes(\"application/octet-stream\")) {\n next()\n return\n }\n\n let total = 0\n const chunks: Buffer[] = []\n let done = false\n let paused = false\n let throttleTimeout: ReturnType<typeof setTimeout> | null = null\n\n const rateBytesPerSecond = typeof maxClientBytesPerSecond === \"number\" && maxClientBytesPerSecond > 0\n ? maxClientBytesPerSecond\n : null\n\n const cleanup = () => {\n req.off(\"data\", onData)\n req.off(\"end\", onEnd)\n req.off(\"error\", onError)\n req.off(\"aborted\", onAborted)\n if (throttleTimeout) {\n clearTimeout(throttleTimeout)\n throttleTimeout = null\n }\n }\n\n const finish = (error?: unknown) => {\n if (done) return\n done = true\n\n cleanup()\n\n if (error) {\n next(error)\n return\n }\n\n req.body = Buffer.concat(chunks, total)\n next()\n }\n\n const onData = (chunk: any) => {\n if (done) return\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n total += buffer.length\n\n if (total > limitBytes) {\n done = true\n cleanup()\n req.destroy()\n res.status(413).json({ ok: false, error: \"chunk_too_large\" })\n return\n }\n\n chunks.push(buffer)\n\n if (!rateBytesPerSecond) return\n\n const now = Date.now()\n const clientKey = getClientKey(req)\n const state = getClientRateState(clientKey, rateBytesPerSecond, now)\n const waitMs = consumeRateBudget(state, buffer.length, rateBytesPerSecond, now)\n\n if (waitMs > 0 && !paused) {\n paused = true\n req.pause()\n throttleTimeout = setTimeout(() => {\n throttleTimeout = null\n paused = false\n if (done) return\n try {\n req.resume()\n } catch {\n //\n }\n }, waitMs)\n }\n }\n\n const onEnd = () => finish()\n const onError = (err: unknown) => finish(err)\n const onAborted = () => finish(new Error(\"request_aborted\"))\n\n req.on(\"data\", onData)\n req.on(\"end\", onEnd)\n req.on(\"error\", onError)\n req.on(\"aborted\", onAborted)\n }\n}\n\ntype ClientRateState = {\n tokens: number\n lastRefillMs: number\n lastSeenMs: number\n}\n\nconst MAX_BURST_SECONDS = 1\nconst STALE_CLIENT_MS = 15 * 60 * 1000\n\nconst clientRateStates = new Map<string, ClientRateState>()\nlet lastCleanupMs = 0\n\nconst getClientKey = (req: any): string => {\n const rawClientIp = typeof req?.clientIp === \"string\" ? req.clientIp : \"\"\n if (rawClientIp.trim()) return rawClientIp.trim()\n const rawIp = typeof req?.ip === \"string\" ? req.ip : \"\"\n return rawIp.trim() || \"unknown\"\n}\n\nconst maybeCleanupStates = (now: number) => {\n if (now - lastCleanupMs < 60_000) return\n lastCleanupMs = now\n\n if (clientRateStates.size < 2000) return\n\n for (const [key, state] of clientRateStates) {\n if (now - state.lastSeenMs > STALE_CLIENT_MS) {\n clientRateStates.delete(key)\n }\n }\n}\n\nconst getClientRateState = (key: string, rateBytesPerSecond: number, now: number): ClientRateState => {\n maybeCleanupStates(now)\n\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const existing = clientRateStates.get(key)\n if (existing) {\n existing.lastSeenMs = now\n existing.tokens = Math.min(capacity, existing.tokens)\n return existing\n }\n\n const next: ClientRateState = {\n tokens: capacity,\n lastRefillMs: now,\n lastSeenMs: now,\n }\n clientRateStates.set(key, next)\n return next\n}\n\nconst consumeRateBudget = (\n state: ClientRateState,\n bytes: number,\n rateBytesPerSecond: number,\n now: number,\n): number => {\n const capacity = rateBytesPerSecond * MAX_BURST_SECONDS\n const elapsedMs = Math.max(0, now - state.lastRefillMs)\n\n if (elapsedMs > 0) {\n state.tokens = Math.min(capacity, state.tokens + (elapsedMs * rateBytesPerSecond) / 1000)\n state.lastRefillMs = now\n }\n\n state.tokens -= bytes\n\n if (state.tokens >= 0) return 0\n return Math.ceil((-state.tokens / rateBytesPerSecond) * 1000)\n}\n","import { Api } from \"@rpcbase/api\"\n\nimport { completeUpload } from \"./handlers/completeUpload\"\nimport { getStatus } from \"./handlers/getStatus\"\nimport { initUpload } from \"./handlers/initUpload\"\nimport { uploadChunk } from \"./handlers/uploadChunk\"\nimport { rawBodyParser } from \"./middleware/rawBodyParser\"\nimport { getChunkSizeBytes, getMaxClientUploadBytesPerSecond, getRawBodyLimitBytes, type SessionUser } from \"./shared\"\n\nimport * as Uploads from \"./index\"\n\n\nexport default (api: Api<SessionUser>) => {\n const chunkSizeBytes = getChunkSizeBytes()\n api.use(\n Uploads.InitRoute,\n rawBodyParser({\n limitBytes: getRawBodyLimitBytes(chunkSizeBytes),\n maxClientBytesPerSecond: getMaxClientUploadBytesPerSecond(),\n }),\n )\n\n api.post(Uploads.InitRoute, initUpload)\n api.put(Uploads.ChunkRoute, uploadChunk)\n api.get(Uploads.StatusRoute, getStatus)\n api.post(Uploads.CompleteRoute, completeUpload)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cleanupRtsTenant } from "./rts/index.js";
|
|
2
|
+
import { a as object, r as boolean, s as string } from "./schemas-BR3K5Luo.js";
|
|
3
|
+
//#region src/rts/api/cleanup/index.ts
|
|
4
|
+
var Route = "/api/rb/rts/cleanup";
|
|
5
|
+
var requestSchema = object({ tenantId: string().min(1) });
|
|
6
|
+
object({ ok: boolean() });
|
|
7
|
+
//#endregion
|
|
8
|
+
//#region src/rts/api/cleanup/handler.ts
|
|
9
|
+
var cleanupHandler = async (payload, ctx) => {
|
|
10
|
+
if (process.env.NODE_ENV === "production") {
|
|
11
|
+
ctx.res.status(403);
|
|
12
|
+
return { ok: false };
|
|
13
|
+
}
|
|
14
|
+
const parsed = requestSchema.safeParse(payload ?? {});
|
|
15
|
+
if (!parsed.success) {
|
|
16
|
+
ctx.res.status(400);
|
|
17
|
+
return { ok: false };
|
|
18
|
+
}
|
|
19
|
+
cleanupRtsTenant(parsed.data.tenantId);
|
|
20
|
+
return { ok: true };
|
|
21
|
+
};
|
|
22
|
+
var handler_default = (api) => {
|
|
23
|
+
api.post(Route, cleanupHandler);
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { handler_default as default };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=handler-CZD5p1Jv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler-CZD5p1Jv.js","names":["z","Route","requestSchema","object","tenantId","string","min","RequestPayload","infer","responseSchema","ok","boolean","ResponsePayload","Api","ApiHandler","cleanupRtsTenant","Cleanup","SessionUser","id","currentTenantId","signedInTenants","cleanupHandler","RequestPayload","ResponsePayload","payload","ctx","Promise","process","env","NODE_ENV","res","status","ok","parsed","requestSchema","safeParse","success","data","tenantId","api","post","Route"],"sources":["../src/rts/api/cleanup/index.ts","../src/rts/api/cleanup/handler.ts"],"sourcesContent":["import { z } from \"zod\"\n\n\nexport const Route = \"/api/rb/rts/cleanup\"\n\nexport const requestSchema = z.object({\n tenantId: z.string().min(1),\n})\n\nexport type RequestPayload = z.infer<typeof requestSchema>\n\nexport const responseSchema = z.object({\n ok: z.boolean(),\n})\n\nexport type ResponsePayload = z.infer<typeof responseSchema>\n","import { Api, ApiHandler } from \"@rpcbase/api\"\n\nimport { cleanupRtsTenant } from \"../../index\"\n\nimport * as Cleanup from \"./index\"\n\n\ntype SessionUser = {\n id?: string\n currentTenantId?: string\n signedInTenants?: string[]\n}\n\nconst cleanupHandler: ApiHandler<Cleanup.RequestPayload, Cleanup.ResponsePayload, SessionUser> = async(\n payload,\n ctx,\n): Promise<Cleanup.ResponsePayload> => {\n if (process.env.NODE_ENV === \"production\") {\n ctx.res.status(403)\n return { ok: false }\n }\n\n const parsed = Cleanup.requestSchema.safeParse(payload ?? {})\n if (!parsed.success) {\n ctx.res.status(400)\n return { ok: false }\n }\n\n cleanupRtsTenant(parsed.data.tenantId)\n return { ok: true }\n}\n\nexport default (api: Api<SessionUser>) => {\n api.post(Cleanup.Route, cleanupHandler)\n}\n"],"mappings":";;;AAGA,IAAaC,QAAQ;AAErB,IAAaC,gBAAgBF,OAAS,EACpCI,UAAUJ,QAAU,CAACM,IAAI,EAAC,EAC3B,CAAC;AAI4BN,OAAS,EACrCU,IAAIV,SAAU,EACf,CAAC;;;ACAF,IAAMqB,iBAA2F,OAC/FG,SACAC,QACqC;AACrC,KAAA,QAAA,IAAA,aAA6B,cAAc;AACzCA,MAAIK,IAAIC,OAAO,IAAI;AACnB,SAAO,EAAEC,IAAI,OAAO;;CAGtB,MAAMC,SAAAA,cAA+BE,UAAUX,WAAW,EAAE,CAAC;AAC7D,KAAI,CAACS,OAAOG,SAAS;AACnBX,MAAIK,IAAIC,OAAO,IAAI;AACnB,SAAO,EAAEC,IAAI,OAAO;;AAGtBjB,kBAAiBkB,OAAOI,KAAKC,SAAS;AACtC,QAAO,EAAEN,IAAI,MAAM;;AAGrB,IAAA,mBAAgBO,QAA0B;AACxCA,KAAIC,KAAKxB,OAAeK,eAAe"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as __commonJSMin, r as __toESM, t as sendEmail } from "./email-
|
|
1
|
+
import { n as __commonJSMin, r as __toESM, t as sendEmail } from "./email-H8nTAGxe.js";
|
|
2
2
|
import { a as normalizeRtsQueryOptions, c as runRtsCount, i as isRtsRequestAuthorized, l as runRtsQuery, r as buildRtsAbilityFromRequest, s as resolveRtsRequestTenantId, u as getDerivedKey } from "./queryExecutor-JadZcQSQ.js";
|
|
3
3
|
import session from "express-session";
|
|
4
4
|
import { RedisStore } from "connect-redis";
|
package/dist/notifications.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as sendEmail } from "./email-
|
|
1
|
+
import { t as sendEmail } from "./email-H8nTAGxe.js";
|
|
2
2
|
import { models } from "@rpcbase/db";
|
|
3
3
|
//#region src/notifications/routes.ts
|
|
4
4
|
var routes = Object.entries({ .../* @__PURE__ */ Object.assign({ "./api/notifications/handler.ts": () => import("./handler-BLwgdQv-.js") }) }).reduce((acc, [path, mod]) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render_resend-
|
|
1
|
+
{"version":3,"file":"render_resend-DQANggpW.js","names":[],"sources":["../__vite-optional-peer-dep:@react-email/render:resend"],"sourcesContent":["export default {};\nthrow new Error(`Could not resolve \"@react-email/render\" imported by \"resend\". Is it installed?`)"],"mappings":";AAAA,IAAA,wBAAe,EAAE;AACjB,MAAM,IAAI,MAAM,iFAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/rts/api/cleanup/handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAc,MAAM,cAAc,CAAA;AAO9C,KAAK,WAAW,GAAG;IACjB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAA;CAC3B,CAAA;yBAqBe,KAAK,GAAG,CAAC,WAAW,CAAC;AAArC,wBAEC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const Route = "/api/rb/rts/cleanup";
|
|
3
|
+
export declare const requestSchema: z.ZodObject<{
|
|
4
|
+
tenantId: z.ZodString;
|
|
5
|
+
}, z.core.$strip>;
|
|
6
|
+
export type RequestPayload = z.infer<typeof requestSchema>;
|
|
7
|
+
export declare const responseSchema: z.ZodObject<{
|
|
8
|
+
ok: z.ZodBoolean;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
export type ResponsePayload = z.infer<typeof responseSchema>;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/rts/api/cleanup/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,eAAO,MAAM,KAAK,wBAAwB,CAAA;AAE1C,eAAO,MAAM,aAAa;;iBAExB,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAE1D,eAAO,MAAM,cAAc;;iBAEzB,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAA"}
|
package/dist/rts/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ declare class RtsSocket {
|
|
|
25
25
|
close(): void;
|
|
26
26
|
dispatch(event: string, payload: unknown): void;
|
|
27
27
|
}
|
|
28
|
+
export declare const cleanupRtsTenant: (tenantId: string) => void;
|
|
28
29
|
export declare const initRts: ({ server, path, sessionMiddleware, maxPayloadBytes: maxPayloadBytesArg, maxSubscriptionsPerSocket: maxSubscriptionsPerSocketArg, dispatchDebounceMs: dispatchDebounceMsArg, allowInternalModels: allowInternalModelsArg, }: {
|
|
29
30
|
server: HttpServer;
|
|
30
31
|
path?: string;
|
package/dist/rts/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rts/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAA;AAGtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,OAAO,EAA6F,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAG5I,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,IAAI,CAAA;AA+DlE,KAAK,UAAU,GAAG;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,UAAU,CAAA;CACpB,CAAA;AA+BD,KAAK,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;AA+B3D,cAAM,SAAS;IACb,SAAgB,EAAE,EAAE,MAAM,CAAA;IAC1B,SAAgB,QAAQ,EAAE,MAAM,CAAA;IAChC,SAAgB,MAAM,EAAE,MAAM,CAAA;IAE9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqD;gBAE3D,EACjB,EAAE,EACF,EAAE,EACF,IAAI,GACL,EAAE;QACD,EAAE,EAAE,MAAM,CAAA;QACV,EAAE,EAAE,SAAS,CAAA;QACb,IAAI,EAAE,UAAU,CAAA;KACjB;IAOM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI;IAOlE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAO7D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI;IAI5C,KAAK,IAAI,IAAI;IAQb,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;CAOvD;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rts/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAmB,MAAM,IAAI,UAAU,EAAE,MAAM,WAAW,CAAA;AAGtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAG7C,OAAO,EAA6F,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAG5I,OAAO,EAAiC,KAAK,SAAS,EAAE,MAAM,IAAI,CAAA;AA+DlE,KAAK,UAAU,GAAG;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,UAAU,CAAA;CACpB,CAAA;AA+BD,KAAK,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;AA+B3D,cAAM,SAAS;IACb,SAAgB,EAAE,EAAE,MAAM,CAAA;IAC1B,SAAgB,QAAQ,EAAE,MAAM,CAAA;IAChC,SAAgB,MAAM,EAAE,MAAM,CAAA;IAE9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAW;IAC9B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqD;gBAE3D,EACjB,EAAE,EACF,EAAE,EACF,IAAI,GACL,EAAE;QACD,EAAE,EAAE,MAAM,CAAA;QACV,EAAE,EAAE,SAAS,CAAA;QACb,IAAI,EAAE,UAAU,CAAA;KACjB;IAOM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI;IAOlE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAO7D,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI;IAI5C,KAAK,IAAI,IAAI;IAQb,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;CAOvD;AAutBD,eAAO,MAAM,gBAAgB,GAAI,UAAU,MAAM,KAAG,IAqCnD,CAAA;AA4LD,eAAO,MAAM,OAAO,GAAI,4NAQrB;IACD,MAAM,EAAE,UAAU,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,iBAAiB,CAAC,EAAE,cAAc,CAAA;IAClC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,yBAAyB,CAAC,EAAE,MAAM,CAAA;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B,KAAG,IA4HH,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,SAAS,SAAS,KAAG,IAEvD,CAAA;AAED,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,EAAE,WAAW,MAAM,KAAG,IAE3E,CAAA;AAED,cAAc,UAAU,CAAA"}
|
package/dist/rts/index.js
CHANGED
|
@@ -4,7 +4,10 @@ import { buildAbility, buildAbilityFromSession, getTenantRolesFromSessionUser }
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { WebSocketServer } from "ws";
|
|
6
6
|
//#region src/rts/routes.ts
|
|
7
|
-
var routes = Object.entries({ .../* @__PURE__ */ Object.assign({
|
|
7
|
+
var routes = Object.entries({ .../* @__PURE__ */ Object.assign({
|
|
8
|
+
"./api/changes/handler.ts": () => import("../handler-Cq6MsoD4.js"),
|
|
9
|
+
"./api/cleanup/handler.ts": () => import("../handler-CZD5p1Jv.js")
|
|
10
|
+
}) }).reduce((acc, [path, mod]) => {
|
|
8
11
|
acc[path.replace("./api/", "@rpcbase/server/rts/api/")] = mod;
|
|
9
12
|
return acc;
|
|
10
13
|
}, {});
|
|
@@ -510,6 +513,40 @@ var cleanupSocket = (socketId) => {
|
|
|
510
513
|
socketMeta.delete(socketId);
|
|
511
514
|
socketWrappers.delete(socketId);
|
|
512
515
|
};
|
|
516
|
+
var clearTenantDispatchTimers = (tenantId) => {
|
|
517
|
+
const prefix = `${tenantId}:`;
|
|
518
|
+
for (const key of Array.from(dispatchTimers.keys())) {
|
|
519
|
+
if (!key.startsWith(prefix)) continue;
|
|
520
|
+
const timer = dispatchTimers.get(key);
|
|
521
|
+
if (timer) clearTimeout(timer);
|
|
522
|
+
dispatchTimers.delete(key);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
var cleanupRtsTenant = (tenantId) => {
|
|
526
|
+
const normalizedTenantId = tenantId.trim();
|
|
527
|
+
if (!normalizedTenantId) return;
|
|
528
|
+
const tenantSocketIds = Array.from(socketMeta.entries()).filter(([, meta]) => meta.tenantId === normalizedTenantId).map(([socketId]) => socketId);
|
|
529
|
+
for (const socketId of tenantSocketIds) {
|
|
530
|
+
const ws = sockets.get(socketId);
|
|
531
|
+
cleanupSocket(socketId);
|
|
532
|
+
if (ws) try {
|
|
533
|
+
ws.close();
|
|
534
|
+
} catch {}
|
|
535
|
+
}
|
|
536
|
+
clearTenantDispatchTimers(normalizedTenantId);
|
|
537
|
+
const tenantStreams = changeStreams.get(normalizedTenantId);
|
|
538
|
+
if (tenantStreams) {
|
|
539
|
+
for (const [modelName, stream] of tenantStreams.entries()) {
|
|
540
|
+
try {
|
|
541
|
+
stream.close();
|
|
542
|
+
} catch {}
|
|
543
|
+
clearDispatchTimer(normalizedTenantId, modelName);
|
|
544
|
+
}
|
|
545
|
+
changeStreams.delete(normalizedTenantId);
|
|
546
|
+
}
|
|
547
|
+
subscriptions.delete(normalizedTenantId);
|
|
548
|
+
countSubscriptions.delete(normalizedTenantId);
|
|
549
|
+
};
|
|
513
550
|
var handleClientMessage = async ({ socketId, meta, message }) => {
|
|
514
551
|
const ws = sockets.get(socketId);
|
|
515
552
|
if (!ws) return;
|
|
@@ -803,6 +840,6 @@ var notifyRtsModelChanged = (tenantId, modelName) => {
|
|
|
803
840
|
scheduleDispatchSubscriptionsForModel(tenantId, modelName);
|
|
804
841
|
};
|
|
805
842
|
//#endregion
|
|
806
|
-
export { initRts, notifyRtsModelChanged, registerRtsHandler, routes };
|
|
843
|
+
export { cleanupRtsTenant, initRts, notifyRtsModelChanged, registerRtsHandler, routes };
|
|
807
844
|
|
|
808
845
|
//# sourceMappingURL=index.js.map
|