@mateosuarezdev/brpc 1.0.105 → 1.0.106
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/README.md +23 -37
- package/dist/storage/index.js +3 -3
- package/dist/storage/s3/types.d.ts +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,10 +29,9 @@ npm install @mateosuarezdev/brpc zod
|
|
|
29
29
|
- [Schemas](#schemas)
|
|
30
30
|
- [Errors](#errors)
|
|
31
31
|
- [Streaming Media](#streaming-media)
|
|
32
|
-
- [Storage](#storage)
|
|
33
|
-
- [S3](#s3)
|
|
34
|
-
- [Images](#images)
|
|
35
|
-
- [Local](#local)
|
|
32
|
+
- [Storage](#storage)
|
|
33
|
+
- [S3](#s3)
|
|
34
|
+
- [Images](#images)
|
|
36
35
|
- [Auth](#auth)
|
|
37
36
|
- [Server Setup](#server-setup)
|
|
38
37
|
- [Auth Routes](#auth-routes)
|
|
@@ -1004,14 +1003,20 @@ import {
|
|
|
1004
1003
|
uploadGeneratedVariants,
|
|
1005
1004
|
deleteOne,
|
|
1006
1005
|
deleteMany,
|
|
1007
|
-
generateVariants,
|
|
1008
|
-
getOptimizedImage,
|
|
1009
|
-
getImageMetadata,
|
|
1010
|
-
|
|
1011
|
-
} from "@mateosuarezdev/brpc";
|
|
1006
|
+
generateVariants,
|
|
1007
|
+
getOptimizedImage,
|
|
1008
|
+
getImageMetadata,
|
|
1009
|
+
} from "@mateosuarezdev/brpc";
|
|
1012
1010
|
```
|
|
1013
1011
|
|
|
1014
|
-
Requires Bun's S3 client to be configured via environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_ENDPOINT`, `AWS_BUCKET`).
|
|
1012
|
+
Requires Bun's S3 client to be configured via environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`, `AWS_ENDPOINT`, `AWS_BUCKET`).
|
|
1013
|
+
|
|
1014
|
+
S3 is the only persisted-object provider. Every stored upload has a non-empty
|
|
1015
|
+
`variants` object: `default` for a single representation, or named image sizes
|
|
1016
|
+
from `xs` through `xl`. A variant always has its S3 `key`, stored `name`, and
|
|
1017
|
+
typed metadata. Keys are canonical: derive public URLs from the configured
|
|
1018
|
+
endpoint and authorize/presign private URLs in the consuming application's
|
|
1019
|
+
domain route; do not store either URL on a variant.
|
|
1015
1020
|
|
|
1016
1021
|
### S3
|
|
1017
1022
|
|
|
@@ -1056,7 +1061,7 @@ const upload = await uploadSingleObject("/payouts", pdfFile, {
|
|
|
1056
1061
|
});
|
|
1057
1062
|
|
|
1058
1063
|
// upload.shared — name, resolvedType ("document" for a PDF), acl, isPrivate
|
|
1059
|
-
// upload.variants.default — key,
|
|
1064
|
+
// upload.variants.default — key, name, metadata
|
|
1060
1065
|
```
|
|
1061
1066
|
|
|
1062
1067
|
Use this rather than `uploadOne` when the result is going into storage. `uploadOne` returns a `RawUploadResult`, which is what S3 returned; `UploadResult` is what an application stores, and this performs the conversion between them.
|
|
@@ -1075,10 +1080,14 @@ const uploadResult = await generateAndUploadImage("/exercises", imageFile, {
|
|
|
1075
1080
|
});
|
|
1076
1081
|
|
|
1077
1082
|
// uploadResult.shared — name, resolvedType, provider, acl, isPrivate
|
|
1078
|
-
// uploadResult.variants — keyed object with each variant's key,
|
|
1083
|
+
// uploadResult.variants — keyed object with each variant's key, name, metadata
|
|
1079
1084
|
```
|
|
1080
1085
|
|
|
1081
|
-
Variants are keyed by `VariantName`, which is an image size (`xs`, `sm`, `md`, `lg`, `xl`) or `default` for a single-representation file.
|
|
1086
|
+
Variants are keyed by `VariantName`, which is an image size (`xs`, `sm`, `md`, `lg`, `xl`) or `default` for a single-representation file.
|
|
1087
|
+
|
|
1088
|
+
`uploadGeneratedVariants` settles all variant writes. If one fails, it deletes
|
|
1089
|
+
every fulfilled sibling key before rethrowing the original failure, so a failed
|
|
1090
|
+
generated upload does not leave partial S3 objects behind.
|
|
1082
1091
|
|
|
1083
1092
|
#### `uploadGeneratedVariants(path, generated, opts?)`
|
|
1084
1093
|
|
|
@@ -1140,30 +1149,7 @@ if (!file) throw BRPCError.notFound();
|
|
|
1140
1149
|
return file;
|
|
1141
1150
|
```
|
|
1142
1151
|
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
The `local` object provides a filesystem storage backend that mirrors the S3 API. Files are stored under `./buckets/`.
|
|
1146
|
-
|
|
1147
|
-
```ts
|
|
1148
|
-
// Upload
|
|
1149
|
-
const { data, error } = await local.uploadOne("public/avatars", file);
|
|
1150
|
-
if (error) throw BRPCError.internalServerError();
|
|
1151
|
-
// data.key, data.bytesWritten
|
|
1152
|
-
|
|
1153
|
-
// Upload many
|
|
1154
|
-
const results = await local.uploadMany("public/photos", files);
|
|
1155
|
-
|
|
1156
|
-
// Get
|
|
1157
|
-
const file = await local.getOne("public/avatars/abc.jpg");
|
|
1158
|
-
if (!file) throw BRPCError.notFound();
|
|
1159
|
-
|
|
1160
|
-
// Delete
|
|
1161
|
-
const deleted = await local.deleteOne("public/avatars/abc.jpg");
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
---
|
|
1165
|
-
|
|
1166
|
-
## Auth
|
|
1152
|
+
## Auth
|
|
1167
1153
|
|
|
1168
1154
|
Built-in auth service for SurrealDB. Supports email/phone sign-up and sign-in with password or OTP (passwordless), JWT sessions with refresh tokens, and cookie management.
|
|
1169
1155
|
|
package/dist/storage/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var
|
|
2
|
+
var N=($)=>$.startsWith("image/"),z=($)=>$.startsWith("video/"),T=($)=>$.startsWith("audio/"),w=($)=>$.startsWith("text/")||$.includes("pdf")||$.includes("word")||$.includes("sheet")||$.includes("presentation")||$.includes("opendocument")||$==="application/rtf",I=($)=>$.includes("zip")||$.includes("rar")||$.includes("tar")||$.includes("7z")||$.includes("gzip"),k=($)=>$==="application/javascript"||$==="application/json"||$==="text/html"||$==="text/css"||$==="application/xml"||$.includes("javascript"),R=($)=>$.startsWith("font/")||$.includes("font"),b=($)=>{if(!$)return"other";let W=$.toLowerCase();if(N(W))return"image";if(z(W))return"video";if(T(W))return"audio";if(w(W))return"document";if(I(W))return"archive";if(k(W))return"code";if(R(W))return"font";return"other"},U={isImage:N,isVideo:z,isAudio:T,isDocument:w,isArchive:I,isCode:k,isFont:R};function e(){let W=Buffer.from("Hello, this is a test file for S3 upload!","utf-8");return new File([W],"test.txt",{type:"text/plain"})}function O($,W){if(!$||$<1||!W||W<1)return"1:1";let H=(Q,Y)=>Y===0?Q:H(Y,Q%Y),J=H($,W);return`${$/J}:${W/J}`}function W0($,W){if(!$||!W||$<1||W<1)return"1:1";let H=$/W,J=[{ratio:1,label:"1:1"},{ratio:0.8,label:"4:5"},{ratio:0.75,label:"3:4"},{ratio:0.6666666666666666,label:"2:3"},{ratio:0.5625,label:"9:16"},{ratio:1.5,label:"3:2"},{ratio:1.3333333333333333,label:"4:3"},{ratio:1.7777777777777777,label:"16:9"},{ratio:2.3333333333333335,label:"21:9"}],Q=0.03;for(let{ratio:X,label:K}of J)if(Math.abs(H-X)/X<Q)return K;let Y=(X,K)=>K===0?X:Y(K,X%K),j=Y($,W);return`${$/j}:${W/j}`}class L extends Error{code;clientCode;httpStatus;data;cause;static STATUS_MAP={BAD_REQUEST:400,UNAUTHORIZED:401,FORBIDDEN:403,NOT_FOUND:404,METHOD_NOT_SUPPORTED:405,TIMEOUT:408,CONFLICT:409,PRECONDITION_FAILED:412,PAYLOAD_TOO_LARGE:413,UNPROCESSABLE_CONTENT:422,TOO_MANY_REQUESTS:429,CLIENT_CLOSED_REQUEST:499,INTERNAL_SERVER_ERROR:500,NOT_IMPLEMENTED:501,BAD_GATEWAY:502,SERVICE_UNAVAILABLE:503,GATEWAY_TIMEOUT:504};constructor($){super($.message);if(this.name="BRPCError",this.code=$.code,this.clientCode=$.clientCode,this.httpStatus=L.STATUS_MAP[$.code],this.data=$.data,this.cause=$.cause,Error.captureStackTrace)Error.captureStackTrace(this,L)}toJSON(){return{name:this.name,code:this.code,clientCode:this.clientCode,message:this.message,data:this.data,httpStatus:this.httpStatus}}static badRequest($,W,H){return new L({code:"BAD_REQUEST",message:$,clientCode:W,data:H})}static unauthorized($="Unauthorized",W,H){return new L({code:"UNAUTHORIZED",message:$,clientCode:W,data:H})}static forbidden($="Forbidden",W,H){return new L({code:"FORBIDDEN",message:$,clientCode:W,data:H})}static notFound($="Not Found",W,H){return new L({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static preconditionFailed($="Precondition failed",W,H){//! Intentionally maps to NOT_FOUND instead of PRECONDITION_FAILED — this was changed to fix a bug
|
|
3
3
|
//! but the exact cause is unknown. Suspected: some client/proxy/middleware misbehaved on 412
|
|
4
4
|
//! (not a browser auto-retry issue — 412 doesn't trigger those). Investigate before changing back.
|
|
5
|
-
return new L({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static conflict($,W,H){return new L({code:"CONFLICT",message:$,clientCode:W,data:H})}static unprocessableContent($,W,H){return new L({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:W,data:H})}static tooManyRequests($="Too many requests",W,H){return new L({code:"TOO_MANY_REQUESTS",message:$,clientCode:W,data:H})}static internalServerError($="Internal Server Error",W,H){return new L({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:W,data:H})}static timeout($="Request timeout",W,H){return new L({code:"TIMEOUT",message:$,clientCode:W,data:H})}}import m from"sharp";var
|
|
5
|
+
return new L({code:"NOT_FOUND",message:$,clientCode:W,data:H})}static conflict($,W,H){return new L({code:"CONFLICT",message:$,clientCode:W,data:H})}static unprocessableContent($,W,H){return new L({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:W,data:H})}static tooManyRequests($="Too many requests",W,H){return new L({code:"TOO_MANY_REQUESTS",message:$,clientCode:W,data:H})}static internalServerError($="Internal Server Error",W,H){return new L({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:W,data:H})}static timeout($="Request timeout",W,H){return new L({code:"TIMEOUT",message:$,clientCode:W,data:H})}}import m from"sharp";var v={xs:200,sm:400,md:800,lg:1200,xl:1920},V=1920;async function A($,W,H,J,Q){try{let{data:Y,info:j}=await m($,{limitInputPixels:1e8}).rotate().resize({width:W,height:W,fit:"inside",withoutEnlargement:!0}).webp({quality:H}).toBuffer({resolveWithObject:!0});return{buffer:Y,object:{name:J.replace(/\.[^/.]+$/,".webp"),resolvedType:"image",metadata:{type:"image/webp",size:Y.length,extension:".webp",width:j.width,height:j.height,aspectRatio:j.width&&j.height?j.width/j.height:1,aspectRatioStr:j.width&&j.height?O(j.width,j.height):"1:1",acl:Q}}}}catch(Y){throw L.conflict(`Failed to optimize image: ${Y instanceof Error?Y.message:"Unknown error"}`)}}async function P($){if(!$)throw L.conflict("A file must be provided");let W,H="unknown.webp";if(typeof $==="string"){if(console.log("I'm going to fail here for file",$),!$.startsWith("data:"))throw console.log("I never got here"),L.conflict("Invalid base64 format");let[,J]=$.split(",");if(!J)throw L.conflict("Invalid base64 data");W=Buffer.from(J,"base64")}else{if(!U.isImage($.type))throw L.conflict("File must be an image");let J=await $.arrayBuffer();W=Buffer.from(J),H=$.name??H}return{buffer:W,name:H}}async function K0($,{size:W,quality:H,acl:J}={}){let Q=await P($);return A(Q.buffer,W??V,H??80,Q.name,J??"public-read")}async function M0($,{name:W="unknown.webp",size:H,quality:J,acl:Q}={}){if(!$?.length)throw L.conflict("A buffer must be provided");return A($,H??V,J??80,W,Q??"public-read")}async function E($,{quality:W=80,acl:H="public-read"}={}){let J=await P($),Q=await Promise.all(Object.entries(v).map(async([Y,j])=>{let X=await A(J.buffer,j,W,J.name,H);return[Y,X]}));return{variants:Object.fromEntries(Q)}}import p from"sharp";import{randomUUID as y}from"crypto";import{extname as u}from"path";async function B($,W,H){if(!W)return{data:null,error:Error("File not found")};let J=$.replace(/^\/+|\/+$/g,""),Q=u(W.name),Y=`${y()}${Q}`,j=`${J}/${Y}`,X=`./buckets/${J}/${Y}`;try{let K=await Bun.write(X,W);return{data:{key:j,file:W,bytesWritten:K},error:null}}catch(K){return{data:null,error:Error(`Failed to upload ${W.name} to local filesystem`)}}}async function f($,W,H){if(!W||W.length===0)return[];let J=W.map((Y)=>{let j=H?.(Y)??Y.name;return B($,Y,j)});return await Promise.all(J)}async function d($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return null;return W}async function n($){let W=Bun.file(`./buckets/${$}`);if(!await W.exists())return!1;return await W.delete(),!0}var S={uploadOne:B,uploadMany:f,getOne:d,deleteOne:n};async function V0($){let W=`./cache/images/${$.key}_${$.width}_${$.quality}.webp`,H=await S.getOne(W);if(H){if(!U.isImage(H.type))return null;return H}let J=$.key,Q=await S.getOne(J);if(!Q)return null;if(!U.isImage(Q.type))return null;let Y=await Q.arrayBuffer(),j=await p(Y).resize($.width,null,{withoutEnlargement:!0}).webp({quality:$.quality??75}).toBuffer();await Bun.write(W,j);let X=Bun.file(W);if(!await X.exists())return null;return X}import s from"sharp";async function N0($){if(!$?.length)return null;try{let W=await s($).metadata(),H=W.width??0,J=W.height??0,Q=W.height>0?H/J:1;return{type:`image/${W.format}`,size:W.size??0,extension:`.${W.format}`,width:H,height:J,aspectRatio:Q,aspectRatioStr:O(H,J)}}catch(W){return console.warn("Failed to read image metadata:",W),null}}var C=process.env.AWS_FOLDER,g=C?`${C}/`:"";var{s3:r}=globalThis.Bun;import{extname as t}from"path";function c($={}){return $}var{s3:l}=globalThis.Bun;async function o($,W){if(!$||typeof $!=="string")throw L.badRequest("Key is required");let H=$.trim();if(!H||H.length===0)throw L.badRequest("Key must be a non-empty string");try{return await l.delete($),{key:$,deleted:!0,error:null}}catch(J){return{key:$,deleted:!1,error:J}}}async function h($,W){return(await Promise.allSettled($.map((J)=>o(J,W)))).map((J,Q)=>{if(J.status==="fulfilled")return J.value;return{key:$[Q],deleted:!1,error:J.reason}})}async function F($,W,H){let J="buffer"in W?W:{buffer:W,object:{name:W.name,resolvedType:b(W.type),metadata:{type:W.type,extension:t(W.name),size:W.size,acl:H?.acl??"public-read"}}};if("buffer"in W){if(!W.object?.name||!W.object?.metadata)throw L.badRequest("Complete metadata required when uploading buffer")}let Q=$.length>1?`${$.replace(/^\/+|\/+$/g,"")}/`:"",Y=J.object.metadata.extension.startsWith(".")?J.object.metadata.extension:`.${J.object.metadata.extension}`,j=J.object.name?J.object.name:`${crypto.randomUUID()}${Y}`,X=`${g}${Q}${j}`,K=r.file(X),_=J?.object?.metadata?.acl??"public-read",Z=_!=="public-read"&&_!=="public-read-write",x=await K.write(J.buffer,{acl:_,type:J.object.metadata.type});return{uploadedBy:H?.uploadedBy,key:X,name:J.object.name,resolvedType:J.object.resolvedType,provider:"s3",isPrivate:Z,metadata:{...J.object.metadata,size:x,acl:_},isActive:!0}}async function h0($,W,H={throwIf:"any"}){let J=await Promise.allSettled(W.map((j)=>F($,j,{acl:H?.acl,uploadedBy:H?.uploadedBy}))),Q=[],Y=[];if(J.forEach((j)=>{if(j.status==="fulfilled"){if(Q.push(j.value),H?.externalKeys)H.externalKeys.push(j.value.key)}else Y.push(j.reason)}),H?.throwIf==="any"&&Y.length>0||H?.throwIf==="all"&&Y.length===W.length)throw L.internalServerError(`Upload failed: ${Y.length} of ${W.length}`);return{data:Q,errors:Y.length?Y:void 0}}async function i($,W,H){let J=H?.baseId??crypto.randomUUID(),Q=Object.values(W.variants).find(Boolean),Y=await Promise.allSettled(Object.entries(W.variants).map(async([Z,x])=>{if(!x)return[Z,null];let G=x.object.metadata.extension,D=`${J}_${Z}${G}`,q=await F($,{...x,object:{...x.object,name:D}},H);if(!q.metadata)throw L.internalServerError("Upload returned no metadata, cannot build a storage variant");let M=q.metadata;return[Z,{key:q.key,name:D,metadata:{type:M.type,extension:M.extension,size:M.size,hash:M.hash,width:M.width,height:M.height,aspectRatio:M.aspectRatio,aspectRatioStr:M.aspectRatioStr,resolution:M.resolution,bitrate:M.bitrate,codec:M.codec,fps:M.fps}}]})),j=Y.flatMap((Z)=>Z.status==="fulfilled"?[Z.value]:[]),X=Y.flatMap((Z)=>Z.status==="rejected"?[Z.reason]:[]);if(X.length>0){let Z=j.flatMap(([,D])=>D?.key?[D.key]:[]),G=(await h(Z)).filter(({deleted:D})=>!D);if(G.length>0)console.error("Failed to clean up partially uploaded image variants",{keys:Z,failures:G});throw X[0]}let K=(H?.acl??"public-read")!=="public-read"&&(H?.acl??"public-read")!=="public-read-write",_={};for(let[Z,x]of j)if(x)_[Z]=x;return{shared:{name:Q.object.name,resolvedType:Q.object.resolvedType,provider:"s3",acl:H?.acl??"public-read",isPrivate:K,uploadedBy:H?.uploadedBy},variants:_}}async function m0($,W,H){let J=await F($,W,H);if(!J.metadata)throw L.internalServerError("Upload returned no metadata, cannot build a storage object");let Q=J.name??J.key.split("/").pop()??J.key;return{shared:{name:Q,resolvedType:J.resolvedType,provider:J.provider,acl:J.metadata.acl,isPrivate:J.isPrivate,uploadedBy:H?.uploadedBy},variants:c({default:{key:J.key,name:Q,metadata:{type:J.metadata.type,extension:J.metadata.extension,size:J.metadata.size,hash:J.metadata.hash,width:J.metadata.width,height:J.metadata.height,aspectRatio:J.metadata.aspectRatio,aspectRatioStr:J.metadata.aspectRatioStr,resolution:J.metadata.resolution,bitrate:J.metadata.bitrate,codec:J.metadata.codec,fps:J.metadata.fps}}})}}async function y0($,W,H){if(Array.isArray(W))throw Error("File can't be an array, pass a single file");let J=await E(W,{quality:H?.quality,acl:H?.acl});return i($,J,{acl:H?.acl,uploadedBy:H?.uploadedBy})}var{s3:a}=globalThis.Bun;async function d0($,W={debug:!0}){try{return{exists:await a.exists($),error:null}}catch(H){if(W?.debug)console.error("There was an error checking for file existance",H);return{exists:null,error:H}}}export{m0 as uploadSingleObject,F as uploadOne,h0 as uploadMany,i as uploadGeneratedVariants,M0 as optimizeImageBuffer,K0 as optimizeImage,b as mimeTypeToResolvedType,V0 as getOptimizedImage,N0 as getImageMetadata,W0 as getAspectRatioStringRound,O as getAspectRatioString,E as generateVariants,e as generateTestFile,y0 as generateAndUploadImage,U as fileTypePredicates,o as deleteOne,h as deleteMany,c as createUploadVariants,d0 as checkFileExistance};
|
|
6
6
|
|
|
7
|
-
//# debugId=
|
|
7
|
+
//# debugId=D07C2FC1790C739264756E2164756E21
|
|
@@ -22,8 +22,8 @@ type DeleteOneResult = DeleteOneNiceResult | DeleteOneBadResult;
|
|
|
22
22
|
* surrender that everywhere to accommodate one case.
|
|
23
23
|
*/
|
|
24
24
|
export type VariantName = ImageVariantName | "default";
|
|
25
|
-
export type UploadVariants = Record<VariantName, StorageObjectVariant
|
|
26
|
-
/** Builds the
|
|
25
|
+
export type UploadVariants = Partial<Record<VariantName, StorageObjectVariant>>;
|
|
26
|
+
/** Builds the sparse stored-object variant shape from uploaded representations. */
|
|
27
27
|
export declare function createUploadVariants(values?: Partial<Record<VariantName, StorageObjectVariant>>): UploadVariants;
|
|
28
28
|
export type UploadResult = {
|
|
29
29
|
shared: {
|