@mateosuarezdev/brpc 1.0.112 → 1.0.114
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 +49 -8
- package/dist/auth/index.js +3 -3
- package/dist/auth/service.d.ts +5 -0
- package/dist/auth/types.d.ts +2 -0
- package/dist/index.js +4 -4
- package/dist/schemas.d.ts +4 -0
- package/dist/storage/index.js +3 -3
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -181,6 +181,37 @@ Repeated unindexed names and append-style `files[]` are intentionally not
|
|
|
181
181
|
accepted. They cannot distinguish a one-item array from a single file; direct
|
|
182
182
|
callers use indexed keys just like the BRPC client.
|
|
183
183
|
|
|
184
|
+
#### Typed context in a form mutation
|
|
185
|
+
|
|
186
|
+
`formMutation` is also useful when a file upload carries current UI context.
|
|
187
|
+
The router rebuilds its nested shape, but every non-file leaf is still text.
|
|
188
|
+
Describe that context at the procedure input boundary and coerce only the
|
|
189
|
+
fields that need their native type before passing it to application code or an
|
|
190
|
+
LLM prompt:
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
const dayContextSchema = z.object({
|
|
194
|
+
targets: z.object({
|
|
195
|
+
calories: z.coerce.number(),
|
|
196
|
+
protein: z.coerce.number(),
|
|
197
|
+
}),
|
|
198
|
+
proteinProtection: z.stringbool(), // "true" / "false"
|
|
199
|
+
meals: z.array(z.object({ calories: z.coerce.number() })).default([]),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
procedure.input(z.object({
|
|
203
|
+
image: createFileSchema({ acceptedTypes: { image: "*" }, maxSize: 15 }),
|
|
204
|
+
day: dayContextSchema.optional(),
|
|
205
|
+
}));
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Do not use `z.coerce.boolean()` for multipart text: JavaScript treats the
|
|
209
|
+
non-empty string `"false"` as truthy. Use `z.stringbool()` instead. Empty
|
|
210
|
+
arrays emit no multipart fields, so use `.default([])` when an absent array
|
|
211
|
+
means an empty one. Send a nested value as one JSON string only when it must
|
|
212
|
+
retain every native primitive before its route-specific parsing; otherwise the
|
|
213
|
+
typed Zod input is the normal path.
|
|
214
|
+
|
|
184
215
|
```ts
|
|
185
216
|
import { createFileSchema } from "@mateosuarezdev/brpc";
|
|
186
217
|
|
|
@@ -1229,9 +1260,13 @@ Import from the `auth` subpath:
|
|
|
1229
1260
|
import { createAuth, setAuthCookies, clearAuthCookies, parseCookies, COOKIE_NAMES } from "@mateosuarezdev/brpc/auth";
|
|
1230
1261
|
```
|
|
1231
1262
|
|
|
1232
|
-
Requires the `0000_initial.surql` migration to be applied (sets up `users`, `user_identities`, `sessions`, `otp_codes` tables).
|
|
1233
|
-
|
|
1234
|
-
|
|
1263
|
+
Requires the `0000_initial.surql` migration to be applied (sets up `users`, `user_identities`, `sessions`, `otp_codes` tables).
|
|
1264
|
+
|
|
1265
|
+
For the complete, copyable BRPC route layer — including OTP, refresh,
|
|
1266
|
+
password-reset, sign-out, and restore/reconnect session validation — see
|
|
1267
|
+
[Auth routes](docs/auth-routes.md).
|
|
1268
|
+
|
|
1269
|
+
### Server Setup
|
|
1235
1270
|
|
|
1236
1271
|
```ts
|
|
1237
1272
|
// services/auth/index.ts
|
|
@@ -1390,11 +1425,17 @@ export const context = createContext(async (req) => {
|
|
|
1390
1425
|
}
|
|
1391
1426
|
}
|
|
1392
1427
|
|
|
1393
|
-
return { session, headers };
|
|
1394
|
-
});
|
|
1395
|
-
```
|
|
1396
|
-
|
|
1397
|
-
|
|
1428
|
+
return { session, headers };
|
|
1429
|
+
});
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
`fastVerifyToken()` intentionally verifies only the JWT signature for this
|
|
1433
|
+
request-time context. Applications validate a restored or reconnected client
|
|
1434
|
+
session with `verifyToken()` and clear local auth state when that authoritative
|
|
1435
|
+
check fails; this is what makes deleted-account session revocation take effect
|
|
1436
|
+
without adding a database read to every API call.
|
|
1437
|
+
|
|
1438
|
+
#### Session Type (`DecodedJWT`)
|
|
1398
1439
|
|
|
1399
1440
|
| Field | Type | Description |
|
|
1400
1441
|
| -------------- | -------------- | --------------------------------- |
|
package/dist/auth/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import*as w from"jsonwebtoken";import{RecordId as
|
|
2
|
+
import*as w from"jsonwebtoken";import{RecordId as W,DateTime as a,Table as T,eq as B,or as c,surql as _}from"surrealdb";import*as A from"bcryptjs";import n from"node-cron";import i from"crypto";class J 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=J.STATUS_MAP[$.code],this.data=$.data,this.cause=$.cause,Error.captureStackTrace)Error.captureStackTrace(this,J)}toJSON(){return{name:this.name,code:this.code,clientCode:this.clientCode,message:this.message,data:this.data,httpStatus:this.httpStatus}}static badRequest($,K,j){return new J({code:"BAD_REQUEST",message:$,clientCode:K,data:j})}static unauthorized($="Unauthorized",K,j){return new J({code:"UNAUTHORIZED",message:$,clientCode:K,data:j})}static forbidden($="Forbidden",K,j){return new J({code:"FORBIDDEN",message:$,clientCode:K,data:j})}static notFound($="Not Found",K,j){return new J({code:"NOT_FOUND",message:$,clientCode:K,data:j})}static preconditionFailed($="Precondition failed",K,j){//! 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 Q({code:"NOT_FOUND",message:$,clientCode:K,data:j})}static conflict($,K,j){return new Q({code:"CONFLICT",message:$,clientCode:K,data:j})}static unprocessableContent($,K,j){return new Q({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:K,data:j})}static tooManyRequests($="Too many requests",K,j){return new Q({code:"TOO_MANY_REQUESTS",message:$,clientCode:K,data:j})}static internalServerError($="Internal Server Error",K,j){return new Q({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:K,data:j})}static timeout($="Request timeout",K,j){return new Q({code:"TIMEOUT",message:$,clientCode:K,data:j})}}var O={ACCESS_TOKEN:"access_token",REFRESH_TOKEN:"refresh_token",SESSION_ID:"session_id"},E={httpOnly:!0,sameSite:"Lax",path:"/",maxAge:1209600};function v($){return Object.entries($).map(([K,j])=>{if(j===void 0||j===null)return"";switch(K){case"maxAge":return`Max-Age=${j}`;case"httpOnly":return j?"HttpOnly":"";case"secure":return j?"Secure":"";case"sameSite":return`SameSite=${j}`;case"domain":return`Domain=${j}`;case"path":return`Path=${j}`;default:return`${K}=${j}`}}).filter(Boolean).join("; ")}function o($,K,j){let F={...E,secure:j};[`${O.ACCESS_TOKEN}=${K.accessToken}; ${v(F)}`,`${O.REFRESH_TOKEN}=${K.refreshToken}; ${v(F)}`,`${O.SESSION_ID}=${K.sessionId}; ${v({...F,maxAge:86400})}`].forEach((G)=>$.append("Set-Cookie",G))}function e($,K){let j={...E,secure:K,maxAge:0};[O.ACCESS_TOKEN,O.REFRESH_TOKEN,O.SESSION_ID].forEach((F)=>$.append("Set-Cookie",`${F}=; ${v(j)}`))}function C($){if(!$)return{};return $.split(";").reduce((K,j)=>{let F=j.trim().split("="),G=F[0],J=F.slice(1).join("=");if(G&&J)K[G]=J;return K},{})}var i=10,t=3,m=15,d=3,s=["cf-connecting-ip","x-real-ip","x-client-ip","x-forwarded","forwarded-for","x-forwarded-host"],b=new q("users"),D=new q("user_identities"),U=new q("sessions"),P=new q("otp_codes");function L($){return`${$.table}:${$.id}`}function N($=new Date){return new u($)}function y($){let K=$?.headers.get("user-agent")?.toLowerCase();if(!K)return"desktop";if(K.includes("android"))return"android";if(/iphone|ipad|ipod/.test(K))return"apple";if(/windows|macintosh|linux|cros/.test(K))return"desktop";return"other"}class g{db;defaultSignupRoles;constructor($,K){this.db=$;this.defaultSignupRoles=K}async findByEmail($){let[K]=await this.db.select(b).where(B("email",$)).limit(1);return K}async findByPhone($){let[K]=await this.db.select(b).where(B("phone",$)).limit(1);return K}async findByContact($){if(!$.email&&!$.phone)return;let K=[];if($.email)K.push(B("email",$.email));if($.phone)K.push(B("phone",$.phone));let[j]=await this.db.select(b).where(c(...K)).limit(1);return j}async findById($){let[K,j]=$.split(":");return await this.db.select(new X(K,j))??void 0}async create({email:$,phone:K,options:j,ctx:F=this.db}){let[G]=await F.create(b).content({email:$??null,phone:K??null,app_metadata:{...j?.appMetadata,roles:this.defaultSignupRoles,signup_device:y()},user_metadata:j?.userMetadata,last_sign_in_at:null});if(!G)throw Q.internalServerError("Failed to create user","USER_CREATE_FAILED");return G}async updateAppMetadata($,K){let j=await this.findById($);if(!j)throw Q.notFound("User not found","USER_NOT_FOUND");await this.db.update(j.id).merge({app_metadata:K})}async updateUserMetadata($,K){let j=await this.findById($);if(!j)throw Q.notFound("User not found","USER_NOT_FOUND");await this.db.update(j.id).merge({user_metadata:K})}async updateLastSignIn($,K=this.db){let[j,F]=$.split(":");await K.update(new X(j,F)).merge({last_sign_in_at:N()})}}class S{db;constructor($){this.db=$}async findByProvider($,K){let[j]=await this.db.query(A`SELECT * FROM type::table(${D}) WHERE provider = ${$} AND provider_id = ${K} LIMIT 1`);return j?.[0]}async findByUserId($){let[K,j]=$.split(":"),F=new X(K,j),[G]=await this.db.query(A`SELECT * FROM type::table(${D}) WHERE user_id = ${F}`);return G??[]}async findPasswordIdentity($){let[K,j]=$.split(":"),F=new X(K,j),[G]=await this.db.query(A`SELECT * FROM type::table(${D}) WHERE user_id = ${F} AND identity_data.password_hash != NONE LIMIT 1`);return G?.[0]}async create({userId:$,provider:K,providerId:j,identityData:F={},ctx:G=this.db}){let[J]=await G.create(D).content({user_id:$,provider:K,provider_id:j,identity_data:F});if(!J)throw Q.internalServerError("Failed to create identity","IDENTITY_CREATE_FAILED");return J}async updateData($,K){await this.db.update($).merge({identity_data:K})}}class h{db;debug;constructor($,K){this.db=$;this.debug=K}async create({userId:$,req:K,ctx:j=this.db}){let F=this.getClientIP(K),G=K.headers.get("user-agent"),[J,V]=$.split(":"),Y=new X(J,V),[Z]=await j.create(U).content({user_id:Y,ip_address:F,user_agent:G,invalidated_at:null});if(!Z)throw Q.internalServerError("Failed to create session","SESSION_CREATE_FAILED");return k(Z)}async get($){let[K,j]=$.split(":"),F=await this.db.select(new X(K,j));if(!F||F.invalidated_at)return;return k(F)}async getUserSessions($){let[K,j]=$.split(":"),F=new X(K,j),[G]=await this.db.query(A`SELECT * FROM type::table(${U}) WHERE user_id = ${F} AND invalidated_at = null`);return(G??[]).map((J)=>({id:L(J.id),ipAddress:J.ip_address,userAgent:J.user_agent,invalidatedAt:J.invalidated_at}))}async invalidate($){let[K,j]=$.split(":");await this.db.update(new X(K,j)).merge({invalidated_at:N()})}async invalidateAllForUser($){let[K,j]=$.split(":"),F=new X(K,j);await this.db.query(A`UPDATE type::table(${U}) SET invalidated_at = time::now() WHERE user_id = ${F} AND invalidated_at = null`)}async invalidateAllExceptCurrent($,K){let[j,F]=$.split(":"),[G,J]=K.split(":"),V=new X(j,F),Y=new X(G,J);await this.db.query(A`UPDATE type::table(${U}) SET invalidated_at = time::now() WHERE user_id = ${V} AND id != ${Y} AND invalidated_at = null`)}async cleanup(){let[$]=await this.db.query(A`DELETE type::table(${U}) WHERE invalidated_at != NONE AND invalidated_at != null RETURN BEFORE`);return $?.length??0}getClientIP($){let K=$.headers.get("x-forwarded-for");if(K)return K.split(",")[0].trim();for(let j of s){let F=$.headers.get(j);if(F)return F.trim()}return this.debug?"127.0.0.1":null}}class R{db;otpProvider;constructor($,K){this.db=$;this.otpProvider=K}async checkRateLimit($){let K=new Date;K.setMinutes(K.getMinutes()-m);let j=[];if($.email)j.push(A`email = ${$.email}`);if($.phone)j.push(A`phone = ${$.phone}`);let F=j.length===1?j[0]:A`(${j[0]} OR ${j[1]})`,[G]=await this.db.query(A`SELECT * FROM type::table(${P}) WHERE (${F}) AND created_at >= ${N(K)}`);if((G?.length??0)>=d)throw Q.tooManyRequests(`Too many OTP requests. Please wait ${m} minutes.`,"OTP_RATE_LIMIT")}async generate({user:$,type:K,signupOptions:j}){await this.checkRateLimit({email:$.email,phone:$.phone});let F=n.randomInt(1e5,999999).toString(),G=await W.hash(F,10),J=new Date;J.setMinutes(J.getMinutes()+i);let V=$.id?(()=>{let[Z,_]=$.id.split(":");return new X(Z,_)})():null,[Y]=await this.db.create(P).content({user_id:V,email:$.email??null,phone:$.phone??null,code:F,hashed_code:G,type:K,signup_options:j??null,expires_at:N(J),attempts:0,verified_at:null});if(!Y)throw Q.internalServerError("Failed to create OTP record","OTP_CREATE_FAILED");try{await this.sendOTP({email:$.email,phone:$.phone,code:F})}catch{throw await this.db.delete(Y.id),Q.internalServerError("Failed to send OTP","OTP_SEND_FAILED")}return{otpId:L(Y.id),expiresAt:J.toISOString(),message:`OTP sent to ${$.email?"email":"phone"}`}}async verify($,K){let[j,F]=$.split(":"),G=await this.db.select(new X(j,F));if(!G)throw Q.notFound("Invalid OTP","INVALID_OTP");if(new Date>new Date(G.expires_at))throw Q.badRequest("OTP has expired","OTP_EXPIRED");if(G.verified_at)throw Q.badRequest("OTP already used","OTP_ALREADY_USED");if(G.attempts>=t)throw Q.badRequest("Too many failed attempts","OTP_MAX_ATTEMPTS");if(!await W.compare(K,G.hashed_code))throw await this.db.update(G.id).merge({attempts:G.attempts+1}),Q.badRequest("Invalid OTP code","INVALID_OTP_CODE");return await this.db.update(G.id).merge({verified_at:N()}),G}async cleanup(){let $=N(),[K]=await this.db.query(A`DELETE type::table(${P}) WHERE expires_at < ${$} OR verified_at != NONE RETURN BEFORE`);return K?.length??0}async sendOTP({email:$,phone:K,code:j}){if($&&this.otpProvider?.sendEmailOTP)await this.otpProvider.sendEmailOTP({email:$,code:j});if(K&&this.otpProvider?.sendSMSOTP)await this.otpProvider.sendSMSOTP({phone:K,code:j})}}class I{jwtSecret;jwtRefreshSecret;accessTokenExpiry;constructor($,K,j){this.jwtSecret=$;this.jwtRefreshSecret=K;this.accessTokenExpiry=j}generateAccessToken($){return w.sign($,this.jwtSecret,{expiresIn:this.accessTokenExpiry})}generateRefreshToken($,K){return w.sign({sub:$,sessionId:K},this.jwtRefreshSecret)}verifyAccessToken($){return w.verify($,this.jwtSecret)}verifyRefreshToken($){return w.verify($,this.jwtRefreshSecret)}}function f($){return{id:L($.id),email:$.email,phone:$.phone,appMetadata:$.app_metadata,userMetadata:$.user_metadata}}function k($){return{id:L($.id),userId:L($.user_id),ipAddress:$.ip_address,userAgent:$.user_agent,invalidatedAt:$.invalidated_at}}class p{userManager;identityManager;sessionManager;otpManager;tokenManager;cache;config;constructor($){this.config=$,this.userManager=new g($.db,$.defaultSignupRoles),this.identityManager=new S($.db),this.sessionManager=new h($.db,$.debug??!1),this.otpManager=new R($.db,$.otpProvider),this.tokenManager=new I($.secrets.jwt,$.secrets.jwtRefresh,$.accessTokenExpiry??"24h"),this.cache=$.cache?new $.cache({ttl:900000,maxSize:500,cleanupInterval:300000}):null}async initialize(){if(!this.config.disableCleanupCron)this.initializeCleanup()}async signUp($,K){this.validateSignUpMethod($);let{provider:j,providerId:F}=this.extractIdentityKey($);if(await this.identityManager.findByProvider(j,F))throw Q.conflict("User already exists","USER_ALREADY_EXISTS");let J=this.extractContact($);if($.passwordless||!$.password)return this.startPasswordlessSignUp(J,$.options);return this.completePasswordSignUp(j,F,J,$.password,K,$.options)}async signIn($,K){this.validateSignInMethod($);let{provider:j,providerId:F}=this.extractIdentityKey($),G=await this.identityManager.findByProvider(j,F);if(!G)throw Q.notFound(`No account found with this ${$.type}`,"USER_NOT_FOUND");if($.passwordless||!$.password)return this.startPasswordlessSignIn(G);return this.completePasswordSignIn(G,$.password,K)}async verifyOTP($,K,j){let F=await this.otpManager.verify($,K);if(F.type==="signup")return this.completePasswordlessSignUp(F,j);if(F.type==="signin")return this.completePasswordlessSignIn(F,j);throw Q.badRequest("Invalid OTP type","INVALID_OTP_TYPE")}async signOut($){await this.sessionManager.invalidate($)}async signOutAll($){await this.sessionManager.invalidateAllForUser($)}async signOutAllExcept($,K){await this.sessionManager.invalidateAllExceptCurrent($,K)}async getUserSessions($){return this.sessionManager.getUserSessions($)}async refreshTokens($){let K=this.tokenManager.verifyRefreshToken($),j=await this.sessionManager.get(K.sessionId);if(!j)throw Q.notFound("Session not found","SESSION_NOT_FOUND");if(j.userId!==K.sub)throw Q.conflict("Session mismatch","SESSION_USER_MISMATCH");let F=await this.userManager.findById(K.sub);if(!F)throw Q.notFound("User not found","USER_NOT_FOUND");let G=this.tokenManager.generateAccessToken({sub:L(F.id),sessionId:j.id,appMetadata:F.app_metadata,userMetadata:F.user_metadata}),J=this.tokenManager.generateRefreshToken(L(F.id),j.id);return{accessToken:G,refreshToken:J,sessionId:j.id,user:f(F)}}async verifyToken($){try{if(!$||!$.startsWith("Bearer "))return null;let K=$.split(" ")[1],j=this.tokenManager.verifyAccessToken(K),F=await this.sessionManager.get(j.sessionId);if(!F||F.userId!==j.sub)return null;let G=await this.userManager.findById(j.sub);if(!G)return null;return{sub:j.sub,sessionId:j.sessionId,appMetadata:G.app_metadata,userMetadata:G.user_metadata,exp:j.exp}}catch{return null}}async fastVerifyToken($){try{if(!$||!$.startsWith("Bearer "))return null;let K=$.split(" ")[1];return this.tokenManager.verifyAccessToken(K)}catch{return null}}async verifyTokenFromCookie($){if(!$)return null;let j=C($)[O.ACCESS_TOKEN];if(!j)return null;return this.verifyToken(`Bearer ${j}`)}async refreshTokenFromCookie($){if(!$)return null;let j=C($)[O.REFRESH_TOKEN];if(!j)return null;try{return await this.refreshTokens(j)}catch{return null}}async getUserById($){return this.userManager.findById($)}async getUserByContact($){return this.userManager.findByContact($)}async getUserIdentities($){return this.identityManager.findByUserId($)}async updateUserPassword($,K){let j=await this.identityManager.findPasswordIdentity($);if(!j)throw Q.notFound("No password identity found","NO_PASSWORD_SET");let F=await W.hash(K,10);await this.identityManager.updateData(j.id,{password_hash:F})}async updateAppMetadata($,K){return this.userManager.updateAppMetadata($,K)}async updateUserMetadata($,K){return this.userManager.updateUserMetadata($,K)}async changePassword($,K,j){let F=await this.identityManager.findPasswordIdentity($);if(!F)throw Q.badRequest("User has no password set","NO_PASSWORD_SET");if(!await W.compare(K,F.identity_data.password_hash))throw Q.badRequest("Current password is incorrect","INVALID_PASSWORD");let J=await W.hash(j,10);await this.identityManager.updateData(F.id,{password_hash:J})}async requestPasswordReset($){let K=$.email?"email":"phone",j=$.email??$.phone,F=await this.identityManager.findByProvider(K,j);if(!F)throw Q.notFound("User not found","USER_NOT_FOUND");let G=L(F.user_id);return this.otpManager.generate({user:{id:G,email:$.email,phone:$.phone},type:"password_reset"})}async resetPassword($,K,j){let F=await this.otpManager.verify($,K);if(F.type!=="password_reset")throw Q.badRequest("Invalid OTP type for password reset","INVALID_OTP_TYPE");if(!F.user_id)throw Q.internalServerError("Invalid OTP record","INVALID_OTP_RECORD");let G=F.email?"email":"phone",J=F.email??F.phone,V=await this.identityManager.findByProvider(G,J);if(!V)throw Q.internalServerError("Identity not found","IDENTITY_NOT_FOUND");let Y=await W.hash(j,10);await this.identityManager.updateData(V.id,{password_hash:Y});let Z=L(F.user_id);return await this.sessionManager.invalidateAllForUser(Z),{success:!0}}async adminCreateUser({email:$,phone:K,password:j,options:F}){let G=j?await W.hash(j,10):null,J=await this.config.db.beginTransaction();try{let[V]=await J.create(b).content({email:$??null,phone:K??null,app_metadata:{...F?.appMetadata,roles:F?.appMetadata?.roles??this.config.defaultSignupRoles,signup_device:y()},user_metadata:F?.userMetadata,last_sign_in_at:null});if(!V)throw Error("Failed to create user");if($||K){let Y=$?"email":"phone",Z=$??K;await J.create(D).content({user_id:V.id,provider:Y,provider_id:Z,identity_data:G?{password_hash:G}:{}})}return await J.commit(),f(V)}catch(V){throw await J.cancel(),V}}cacheSession($,K){this.cache?.set($,K)}getCachedSession($){return this.cache?.get($)??null}invalidateCachedSession($){return this.cache?.delete($)??!1}getCacheStats(){return this.cache?.stats()??null}clearCache(){this.cache?.clear()}isTokenNearExpiry($){return $-Math.floor(Date.now()/1000)<300}async testCreateUserWithSession({email:$,phone:K,password:j,options:F,req:G}){let J=j?await W.hash(j,10):null,V=this.sessionManager.getClientIP(G),Y=await this.config.db.beginTransaction();try{let[Z]=await Y.create(b).content({email:$??null,phone:K??null,app_metadata:{...F?.appMetadata,roles:this.config.defaultSignupRoles,signup_device:y(G)},user_metadata:F?.userMetadata,last_sign_in_at:N()});if(!Z)throw Error("Failed to create user");if($||K){let l=$?"email":"phone",T=$??K;await Y.create(D).content({user_id:Z.id,provider:l,provider_id:T,identity_data:J?{password_hash:J}:{}})}let[_]=await Y.create(U).content({user_id:Z.id,ip_address:V,user_agent:G.headers.get("user-agent"),invalidated_at:null});if(!_)throw Error("Failed to create session");let H=L(Z.id),z=L(_.id),x=this.tokenManager.generateAccessToken({sub:H,sessionId:z,appMetadata:Z.app_metadata,userMetadata:Z.user_metadata}),M=this.tokenManager.generateRefreshToken(H,z);return await Y.commit(),{user:f(Z),session:k(_),accessToken:x,refreshToken:M}}catch(Z){throw await Y.cancel(),Z}}async startPasswordlessSignUp($,K){return this.otpManager.generate({user:{id:void 0,email:$.email,phone:$.phone},type:"signup",signupOptions:K})}async startPasswordlessSignIn($){let K=L($.user_id),j=$.provider==="email"?$.provider_id:void 0,F=$.provider==="phone"?$.provider_id:void 0;return this.otpManager.generate({user:{id:K,email:j,phone:F},type:"signin"})}async completePasswordSignUp($,K,j,F,G,J){let V=await W.hash(F,10),Y=await this.config.db.beginTransaction();try{let[Z]=await Y.create(b).content({email:j.email??null,phone:j.phone??null,app_metadata:{...J?.appMetadata,roles:this.config.defaultSignupRoles,signup_device:y(G)},user_metadata:J?.userMetadata,last_sign_in_at:N()});if(!Z)throw Error("Failed to create user");let[_]=await Y.create(D).content({user_id:Z.id,provider:$,provider_id:K,identity_data:{password_hash:V}});if(!_)throw Error("Failed to create identity");let H=this.sessionManager.getClientIP(G),[z]=await Y.create(U).content({user_id:Z.id,ip_address:H,user_agent:G.headers.get("user-agent"),invalidated_at:null});if(!z)throw Error("Failed to create session");let x=L(Z.id),M=L(z.id),l=this.tokenManager.generateAccessToken({sub:x,sessionId:M,appMetadata:Z.app_metadata,userMetadata:Z.user_metadata}),T=this.tokenManager.generateRefreshToken(x,M);return await Y.commit(),{user:f(Z),session:k(z),accessToken:l,refreshToken:T}}catch(Z){throw await Y.cancel(),Z}}async completePasswordSignIn($,K,j){let{password_hash:F}=$.identity_data;if(!F)throw Q.conflict("This account has no password. Use OTP to sign in.","NO_PASSWORD_SET");if(!await W.compare(K,F))throw Q.conflict("Invalid credentials","INVALID_CREDENTIALS");let J=L($.user_id),V=await this.userManager.findById(J);if(!V)throw Q.notFound("User not found","USER_NOT_FOUND");return this.createAuthSession(V,j)}async completePasswordlessSignUp($,K){let j=$.email?"email":"phone",F=$.email??$.phone,G=$.signup_options,J=await this.config.db.beginTransaction();try{let[V]=await J.create(b).content({email:$.email??null,phone:$.phone??null,app_metadata:{...G?.appMetadata,roles:this.config.defaultSignupRoles,signup_device:y(K)},user_metadata:G?.userMetadata,last_sign_in_at:N()});if(!V)throw Error("Failed to create user");let[Y]=await J.create(D).content({user_id:V.id,provider:j,provider_id:F,identity_data:{}});if(!Y)throw Error("Failed to create identity");let Z=this.sessionManager.getClientIP(K),[_]=await J.create(U).content({user_id:V.id,ip_address:Z,user_agent:K.headers.get("user-agent"),invalidated_at:null});if(!_)throw Error("Failed to create session");let H=L(V.id),z=L(_.id),x=this.tokenManager.generateAccessToken({sub:H,sessionId:z,appMetadata:V.app_metadata,userMetadata:V.user_metadata}),M=this.tokenManager.generateRefreshToken(H,z);return await J.commit(),{user:f(V),session:k(_),accessToken:x,refreshToken:M}}catch(V){throw await J.cancel(),V}}async completePasswordlessSignIn($,K){if(!$.user_id)throw Q.internalServerError("Invalid OTP record","INVALID_OTP_RECORD");let j=L($.user_id),F=await this.userManager.findById(j);if(!F)throw Q.notFound("User not found","USER_NOT_FOUND");return this.createAuthSession(F,K)}async createAuthSession($,K){let j=await this.config.db.beginTransaction();try{let F=this.sessionManager.getClientIP(K),[G]=await j.create(U).content({user_id:$.id,ip_address:F,user_agent:K.headers.get("user-agent"),invalidated_at:null});if(!G)throw Error("Failed to create session");await j.update($.id).merge({last_sign_in_at:N()});let J=L($.id),V=L(G.id),Y=this.tokenManager.generateAccessToken({sub:J,sessionId:V,appMetadata:$.app_metadata,userMetadata:$.user_metadata}),Z=this.tokenManager.generateRefreshToken(J,V);return await j.commit(),{user:f($),session:k(G),accessToken:Y,refreshToken:Z}}catch(F){throw await j.cancel(),F}}validateSignUpMethod($){if($.type==="email")this.validateEmail($.email);else $.phone=this.normalizePhoneNumber($.phone)}validateSignInMethod($){if($.type==="email")this.validateEmail($.email);else $.phone=this.normalizePhoneNumber($.phone)}validateEmail($){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($))throw Q.badRequest("Invalid email format","INVALID_EMAIL")}normalizePhoneNumber($){let K=$.replace(/\D/g,"");if(K.length===10)return`+1${K}`;if(K.length===11&&K.startsWith("1"))return`+${K}`;if(!K.startsWith("+"))return`+${K}`;return K}extractContact($){return $.type==="email"?{email:$.email}:{phone:$.phone}}extractIdentityKey($){return $.type==="email"?{provider:"email",providerId:$.email}:{provider:"phone",providerId:$.phone}}initializeCleanup(){Promise.all([this.sessionManager.cleanup(),this.otpManager.cleanup()]).catch(()=>{}),a.schedule("0 2 * * *",async()=>{await Promise.all([this.sessionManager.cleanup(),this.otpManager.cleanup()]).catch(()=>{})})}}function V$($){return new p($)}export{o as setAuthCookies,C as parseCookies,V$ as createAuth,e as clearAuthCookies,O as COOKIE_NAMES,p as AuthService};
|
|
5
|
+
return new J({code:"NOT_FOUND",message:$,clientCode:K,data:j})}static conflict($,K,j){return new J({code:"CONFLICT",message:$,clientCode:K,data:j})}static unprocessableContent($,K,j){return new J({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:K,data:j})}static tooManyRequests($="Too many requests",K,j){return new J({code:"TOO_MANY_REQUESTS",message:$,clientCode:K,data:j})}static internalServerError($="Internal Server Error",K,j){return new J({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:K,data:j})}static timeout($="Request timeout",K,j){return new J({code:"TIMEOUT",message:$,clientCode:K,data:j})}}var O={ACCESS_TOKEN:"access_token",REFRESH_TOKEN:"refresh_token",SESSION_ID:"session_id"},S={httpOnly:!0,sameSite:"Lax",path:"/",maxAge:1209600};function q($){return Object.entries($).map(([K,j])=>{if(j===void 0||j===null)return"";switch(K){case"maxAge":return`Max-Age=${j}`;case"httpOnly":return j?"HttpOnly":"";case"secure":return j?"Secure":"";case"sameSite":return`SameSite=${j}`;case"domain":return`Domain=${j}`;case"path":return`Path=${j}`;default:return`${K}=${j}`}}).filter(Boolean).join("; ")}function K$($,K,j){let F={...S,secure:j};[`${O.ACCESS_TOKEN}=${K.accessToken}; ${q(F)}`,`${O.REFRESH_TOKEN}=${K.refreshToken}; ${q(F)}`,`${O.SESSION_ID}=${K.sessionId}; ${q({...F,maxAge:86400})}`].forEach((G)=>$.append("Set-Cookie",G))}function j$($,K){let j={...S,secure:K,maxAge:0};[O.ACCESS_TOKEN,O.REFRESH_TOKEN,O.SESSION_ID].forEach((F)=>$.append("Set-Cookie",`${F}=; ${q(j)}`))}function P($){if(!$)return{};return $.split(";").reduce((K,j)=>{let F=j.trim().split("="),G=F[0],Q=F.slice(1).join("=");if(G&&Q)K[G]=Q;return K},{})}var t=10,d=3,g=15,s=3,o=["cf-connecting-ip","x-real-ip","x-client-ip","x-forwarded","forwarded-for","x-forwarded-host"],H=new T("users"),U=new T("user_identities"),b=new T("sessions"),m=new T("otp_codes");function L($){return`${$.table}:${$.id}`}function N($=new Date){return new a($)}function y($){return $?.deleted_at!==void 0}function v($){let K=$?.headers.get("user-agent")?.toLowerCase();if(!K)return"desktop";if(K.includes("android"))return"android";if(/iphone|ipad|ipod/.test(K))return"apple";if(/windows|macintosh|linux|cros/.test(K))return"desktop";return"other"}class E{db;defaultSignupRoles;constructor($,K){this.db=$;this.defaultSignupRoles=K}async findByEmail($){let[K]=await this.db.select(H).where(B("email",$)).limit(1);return y(K)?void 0:K}async findByPhone($){let[K]=await this.db.select(H).where(B("phone",$)).limit(1);return y(K)?void 0:K}async findByContact($){if(!$.email&&!$.phone)return;let K=[];if($.email)K.push(B("email",$.email));if($.phone)K.push(B("phone",$.phone));let[j]=await this.db.select(H).where(c(...K)).limit(1);return y(j)?void 0:j}async findById($){let[K,j]=$.split(":"),F=await this.db.select(new W(K,j));return y(F)?void 0:F??void 0}async create({email:$,phone:K,options:j,ctx:F=this.db}){let[G]=await F.create(H).content({email:$??null,phone:K??null,app_metadata:{...j?.appMetadata,roles:this.defaultSignupRoles,signup_device:v()},user_metadata:j?.userMetadata,last_sign_in_at:null});if(!G)throw J.internalServerError("Failed to create user","USER_CREATE_FAILED");return G}async updateAppMetadata($,K){let j=await this.findById($);if(!j)throw J.notFound("User not found","USER_NOT_FOUND");await this.db.update(j.id).merge({app_metadata:K})}async updateUserMetadata($,K){let j=await this.findById($);if(!j)throw J.notFound("User not found","USER_NOT_FOUND");await this.db.update(j.id).merge({user_metadata:K})}async updateLastSignIn($,K=this.db){let[j,F]=$.split(":");await K.update(new W(j,F)).merge({last_sign_in_at:N()})}}class h{db;constructor($){this.db=$}async findByProvider($,K){let[j]=await this.db.query(_`SELECT * FROM type::table(${U}) WHERE provider = ${$} AND provider_id = ${K} LIMIT 1`);return j?.[0]}async findByUserId($){let[K,j]=$.split(":"),F=new W(K,j),[G]=await this.db.query(_`SELECT * FROM type::table(${U}) WHERE user_id = ${F}`);return G??[]}async findPasswordIdentity($){let[K,j]=$.split(":"),F=new W(K,j),[G]=await this.db.query(_`SELECT * FROM type::table(${U}) WHERE user_id = ${F} AND identity_data.password_hash != NONE LIMIT 1`);return G?.[0]}async create({userId:$,provider:K,providerId:j,identityData:F={},ctx:G=this.db}){let[Q]=await G.create(U).content({user_id:$,provider:K,provider_id:j,identity_data:F});if(!Q)throw J.internalServerError("Failed to create identity","IDENTITY_CREATE_FAILED");return Q}async updateData($,K){await this.db.update($).merge({identity_data:K})}}class R{db;debug;constructor($,K){this.db=$;this.debug=K}async create({userId:$,req:K,ctx:j=this.db}){let F=this.getClientIP(K),G=K.headers.get("user-agent"),[Q,V]=$.split(":"),Y=new W(Q,V),[Z]=await j.create(b).content({user_id:Y,ip_address:F,user_agent:G,invalidated_at:null});if(!Z)throw J.internalServerError("Failed to create session","SESSION_CREATE_FAILED");return k(Z)}async get($){let[K,j]=$.split(":"),F=await this.db.select(new W(K,j));if(!F||F.invalidated_at)return;return k(F)}async getUserSessions($){let[K,j]=$.split(":"),F=new W(K,j),[G]=await this.db.query(_`SELECT * FROM type::table(${b}) WHERE user_id = ${F} AND invalidated_at = null`);return(G??[]).map((Q)=>({id:L(Q.id),ipAddress:Q.ip_address,userAgent:Q.user_agent,invalidatedAt:Q.invalidated_at}))}async invalidate($){let[K,j]=$.split(":");await this.db.update(new W(K,j)).merge({invalidated_at:N()})}async invalidateAllForUser($,K=this.db){let[j,F]=$.split(":"),G=new W(j,F);await K.query(_`UPDATE type::table(${b}) SET invalidated_at = time::now() WHERE user_id = ${G} AND invalidated_at = null`)}async invalidateAllExceptCurrent($,K){let[j,F]=$.split(":"),[G,Q]=K.split(":"),V=new W(j,F),Y=new W(G,Q);await this.db.query(_`UPDATE type::table(${b}) SET invalidated_at = time::now() WHERE user_id = ${V} AND id != ${Y} AND invalidated_at = null`)}async cleanup(){let[$]=await this.db.query(_`DELETE type::table(${b}) WHERE invalidated_at != NONE AND invalidated_at != null RETURN BEFORE`);return $?.length??0}getClientIP($){let K=$.headers.get("x-forwarded-for");if(K)return K.split(",")[0].trim();for(let j of o){let F=$.headers.get(j);if(F)return F.trim()}return this.debug?"127.0.0.1":null}}class p{db;otpProvider;constructor($,K){this.db=$;this.otpProvider=K}async checkRateLimit($){let K=new Date;K.setMinutes(K.getMinutes()-g);let j=[];if($.email)j.push(_`email = ${$.email}`);if($.phone)j.push(_`phone = ${$.phone}`);let F=j.length===1?j[0]:_`(${j[0]} OR ${j[1]})`,[G]=await this.db.query(_`SELECT * FROM type::table(${m}) WHERE (${F}) AND created_at >= ${N(K)}`);if((G?.length??0)>=s)throw J.tooManyRequests(`Too many OTP requests. Please wait ${g} minutes.`,"OTP_RATE_LIMIT")}async generate({user:$,type:K,signupOptions:j}){await this.checkRateLimit({email:$.email,phone:$.phone});let F=i.randomInt(1e5,999999).toString(),G=await A.hash(F,10),Q=new Date;Q.setMinutes(Q.getMinutes()+t);let V=$.id?(()=>{let[Z,X]=$.id.split(":");return new W(Z,X)})():null,[Y]=await this.db.create(m).content({user_id:V,email:$.email??null,phone:$.phone??null,code:F,hashed_code:G,type:K,signup_options:j??null,expires_at:N(Q),attempts:0,verified_at:null});if(!Y)throw J.internalServerError("Failed to create OTP record","OTP_CREATE_FAILED");try{await this.sendOTP({email:$.email,phone:$.phone,code:F})}catch{throw await this.db.delete(Y.id),J.internalServerError("Failed to send OTP","OTP_SEND_FAILED")}return{otpId:L(Y.id),expiresAt:Q.toISOString(),message:`OTP sent to ${$.email?"email":"phone"}`}}async verify($,K){let[j,F]=$.split(":"),G=await this.db.select(new W(j,F));if(!G)throw J.notFound("Invalid OTP","INVALID_OTP");if(new Date>new Date(G.expires_at))throw J.badRequest("OTP has expired","OTP_EXPIRED");if(G.verified_at)throw J.badRequest("OTP already used","OTP_ALREADY_USED");if(G.attempts>=d)throw J.badRequest("Too many failed attempts","OTP_MAX_ATTEMPTS");if(!await A.compare(K,G.hashed_code))throw await this.db.update(G.id).merge({attempts:G.attempts+1}),J.badRequest("Invalid OTP code","INVALID_OTP_CODE");return await this.db.update(G.id).merge({verified_at:N()}),G}async cleanup(){let $=N(),[K]=await this.db.query(_`DELETE type::table(${m}) WHERE expires_at < ${$} OR verified_at != NONE RETURN BEFORE`);return K?.length??0}async sendOTP({email:$,phone:K,code:j}){if($&&this.otpProvider?.sendEmailOTP)await this.otpProvider.sendEmailOTP({email:$,code:j});if(K&&this.otpProvider?.sendSMSOTP)await this.otpProvider.sendSMSOTP({phone:K,code:j})}}class I{jwtSecret;jwtRefreshSecret;accessTokenExpiry;constructor($,K,j){this.jwtSecret=$;this.jwtRefreshSecret=K;this.accessTokenExpiry=j}generateAccessToken($){return w.sign($,this.jwtSecret,{expiresIn:this.accessTokenExpiry})}generateRefreshToken($,K){return w.sign({sub:$,sessionId:K},this.jwtRefreshSecret)}verifyAccessToken($){return w.verify($,this.jwtSecret)}verifyRefreshToken($){return w.verify($,this.jwtRefreshSecret)}}function x($){return{id:L($.id),email:$.email,phone:$.phone,appMetadata:$.app_metadata,userMetadata:$.user_metadata}}function k($){return{id:L($.id),userId:L($.user_id),ipAddress:$.ip_address,userAgent:$.user_agent,invalidatedAt:$.invalidated_at}}class u{userManager;identityManager;sessionManager;otpManager;tokenManager;cache;config;constructor($){this.config=$,this.userManager=new E($.db,$.defaultSignupRoles),this.identityManager=new h($.db),this.sessionManager=new R($.db,$.debug??!1),this.otpManager=new p($.db,$.otpProvider),this.tokenManager=new I($.secrets.jwt,$.secrets.jwtRefresh,$.accessTokenExpiry??"24h"),this.cache=$.cache?new $.cache({ttl:900000,maxSize:500,cleanupInterval:300000}):null}async initialize(){if(!this.config.disableCleanupCron)this.initializeCleanup()}async signUp($,K){this.validateSignUpMethod($);let{provider:j,providerId:F}=this.extractIdentityKey($);if(await this.identityManager.findByProvider(j,F))throw J.conflict("User already exists","USER_ALREADY_EXISTS");let Q=this.extractContact($);if($.passwordless||!$.password)return this.startPasswordlessSignUp(Q,$.options);return this.completePasswordSignUp(j,F,Q,$.password,K,$.options)}async signIn($,K){this.validateSignInMethod($);let{provider:j,providerId:F}=this.extractIdentityKey($),G=await this.identityManager.findByProvider(j,F);if(!G)throw J.notFound(`No account found with this ${$.type}`,"USER_NOT_FOUND");if($.passwordless||!$.password)return this.startPasswordlessSignIn(G);return this.completePasswordSignIn(G,$.password,K)}async verifyOTP($,K,j){let F=await this.otpManager.verify($,K);if(F.type==="signup")return this.completePasswordlessSignUp(F,j);if(F.type==="signin")return this.completePasswordlessSignIn(F,j);throw J.badRequest("Invalid OTP type","INVALID_OTP_TYPE")}async signOut($){await this.sessionManager.invalidate($)}async signOutAll($){await this.sessionManager.invalidateAllForUser($)}async signOutAllExcept($,K){await this.sessionManager.invalidateAllExceptCurrent($,K)}async getUserSessions($){if(!await this.userManager.findById($))throw J.notFound("User not found","USER_NOT_FOUND");return this.sessionManager.getUserSessions($)}async refreshTokens($){let K=this.tokenManager.verifyRefreshToken($),j=await this.sessionManager.get(K.sessionId);if(!j)throw J.notFound("Session not found","SESSION_NOT_FOUND");if(j.userId!==K.sub)throw J.conflict("Session mismatch","SESSION_USER_MISMATCH");let F=await this.userManager.findById(K.sub);if(!F)throw J.notFound("User not found","USER_NOT_FOUND");let G=this.tokenManager.generateAccessToken({sub:L(F.id),sessionId:j.id,appMetadata:F.app_metadata,userMetadata:F.user_metadata}),Q=this.tokenManager.generateRefreshToken(L(F.id),j.id);return{accessToken:G,refreshToken:Q,sessionId:j.id,user:x(F)}}async verifyToken($){try{if(!$||!$.startsWith("Bearer "))return null;let K=$.split(" ")[1],j=this.tokenManager.verifyAccessToken(K),F=await this.sessionManager.get(j.sessionId);if(!F||F.userId!==j.sub)return null;let G=await this.userManager.findById(j.sub);if(!G)return null;return{sub:j.sub,sessionId:j.sessionId,appMetadata:G.app_metadata,userMetadata:G.user_metadata,exp:j.exp}}catch{return null}}async fastVerifyToken($){try{if(!$||!$.startsWith("Bearer "))return null;let K=$.split(" ")[1];return this.tokenManager.verifyAccessToken(K)}catch{return null}}async verifyTokenFromCookie($){if(!$)return null;let j=P($)[O.ACCESS_TOKEN];if(!j)return null;return this.verifyToken(`Bearer ${j}`)}async refreshTokenFromCookie($){if(!$)return null;let j=P($)[O.REFRESH_TOKEN];if(!j)return null;try{return await this.refreshTokens(j)}catch{return null}}async getUserById($){return this.userManager.findById($)}async getUserByContact($){return this.userManager.findByContact($)}async getUserIdentities($){if(!await this.userManager.findById($))throw J.notFound("User not found","USER_NOT_FOUND");return this.identityManager.findByUserId($)}async markUserDeleted($){let K=await this.userManager.findById($);if(!K)throw J.notFound("User not found","USER_NOT_FOUND");let j=await this.config.db.beginTransaction();try{await j.update(K.id).merge({deleted_at:N()}),await this.sessionManager.invalidateAllForUser($,j),await j.commit(),this.clearCache()}catch(F){throw await j.cancel(),F}}async updateUserPassword($,K){if(!await this.userManager.findById($))throw J.notFound("User not found","USER_NOT_FOUND");let F=await this.identityManager.findPasswordIdentity($);if(!F)throw J.notFound("No password identity found","NO_PASSWORD_SET");let G=await A.hash(K,10);await this.identityManager.updateData(F.id,{password_hash:G})}async updateAppMetadata($,K){return this.userManager.updateAppMetadata($,K)}async updateUserMetadata($,K){return this.userManager.updateUserMetadata($,K)}async changePassword($,K,j){if(!await this.userManager.findById($))throw J.notFound("User not found","USER_NOT_FOUND");let G=await this.identityManager.findPasswordIdentity($);if(!G)throw J.badRequest("User has no password set","NO_PASSWORD_SET");if(!await A.compare(K,G.identity_data.password_hash))throw J.badRequest("Current password is incorrect","INVALID_PASSWORD");let V=await A.hash(j,10);await this.identityManager.updateData(G.id,{password_hash:V})}async requestPasswordReset($){let K=$.email?"email":"phone",j=$.email??$.phone,F=await this.identityManager.findByProvider(K,j);if(!F)throw J.notFound("User not found","USER_NOT_FOUND");let G=L(F.user_id);if(!await this.userManager.findById(G))throw J.notFound("User not found","USER_NOT_FOUND");return this.otpManager.generate({user:{id:G,email:$.email,phone:$.phone},type:"password_reset"})}async resetPassword($,K,j){let F=await this.otpManager.verify($,K);if(F.type!=="password_reset")throw J.badRequest("Invalid OTP type for password reset","INVALID_OTP_TYPE");if(!F.user_id)throw J.internalServerError("Invalid OTP record","INVALID_OTP_RECORD");let G=F.email?"email":"phone",Q=F.email??F.phone,V=await this.identityManager.findByProvider(G,Q);if(!V)throw J.internalServerError("Identity not found","IDENTITY_NOT_FOUND");let Y=L(F.user_id);if(!await this.userManager.findById(Y))throw J.notFound("User not found","USER_NOT_FOUND");let X=await A.hash(j,10);return await this.identityManager.updateData(V.id,{password_hash:X}),await this.sessionManager.invalidateAllForUser(Y),{success:!0}}async adminCreateUser({email:$,phone:K,password:j,options:F}){let G=j?await A.hash(j,10):null,Q=await this.config.db.beginTransaction();try{let[V]=await Q.create(H).content({email:$??null,phone:K??null,app_metadata:{...F?.appMetadata,roles:F?.appMetadata?.roles??this.config.defaultSignupRoles,signup_device:v()},user_metadata:F?.userMetadata,last_sign_in_at:null});if(!V)throw Error("Failed to create user");if($||K){let Y=$?"email":"phone",Z=$??K;await Q.create(U).content({user_id:V.id,provider:Y,provider_id:Z,identity_data:G?{password_hash:G}:{}})}return await Q.commit(),x(V)}catch(V){throw await Q.cancel(),V}}cacheSession($,K){this.cache?.set($,K)}getCachedSession($){return this.cache?.get($)??null}invalidateCachedSession($){return this.cache?.delete($)??!1}getCacheStats(){return this.cache?.stats()??null}clearCache(){this.cache?.clear()}isTokenNearExpiry($){return $-Math.floor(Date.now()/1000)<300}async testCreateUserWithSession({email:$,phone:K,password:j,options:F,req:G}){let Q=j?await A.hash(j,10):null,V=this.sessionManager.getClientIP(G),Y=await this.config.db.beginTransaction();try{let[Z]=await Y.create(H).content({email:$??null,phone:K??null,app_metadata:{...F?.appMetadata,roles:this.config.defaultSignupRoles,signup_device:v(G)},user_metadata:F?.userMetadata,last_sign_in_at:N()});if(!Z)throw Error("Failed to create user");if($||K){let l=$?"email":"phone",C=$??K;await Y.create(U).content({user_id:Z.id,provider:l,provider_id:C,identity_data:Q?{password_hash:Q}:{}})}let[X]=await Y.create(b).content({user_id:Z.id,ip_address:V,user_agent:G.headers.get("user-agent"),invalidated_at:null});if(!X)throw Error("Failed to create session");let D=L(Z.id),z=L(X.id),M=this.tokenManager.generateAccessToken({sub:D,sessionId:z,appMetadata:Z.app_metadata,userMetadata:Z.user_metadata}),f=this.tokenManager.generateRefreshToken(D,z);return await Y.commit(),{user:x(Z),session:k(X),accessToken:M,refreshToken:f}}catch(Z){throw await Y.cancel(),Z}}async startPasswordlessSignUp($,K){return this.otpManager.generate({user:{id:void 0,email:$.email,phone:$.phone},type:"signup",signupOptions:K})}async startPasswordlessSignIn($){let K=L($.user_id);if(!await this.userManager.findById(K))throw J.notFound("User not found","USER_NOT_FOUND");let F=$.provider==="email"?$.provider_id:void 0,G=$.provider==="phone"?$.provider_id:void 0;return this.otpManager.generate({user:{id:K,email:F,phone:G},type:"signin"})}async completePasswordSignUp($,K,j,F,G,Q){let V=await A.hash(F,10),Y=await this.config.db.beginTransaction();try{let[Z]=await Y.create(H).content({email:j.email??null,phone:j.phone??null,app_metadata:{...Q?.appMetadata,roles:this.config.defaultSignupRoles,signup_device:v(G)},user_metadata:Q?.userMetadata,last_sign_in_at:N()});if(!Z)throw Error("Failed to create user");let[X]=await Y.create(U).content({user_id:Z.id,provider:$,provider_id:K,identity_data:{password_hash:V}});if(!X)throw Error("Failed to create identity");let D=this.sessionManager.getClientIP(G),[z]=await Y.create(b).content({user_id:Z.id,ip_address:D,user_agent:G.headers.get("user-agent"),invalidated_at:null});if(!z)throw Error("Failed to create session");let M=L(Z.id),f=L(z.id),l=this.tokenManager.generateAccessToken({sub:M,sessionId:f,appMetadata:Z.app_metadata,userMetadata:Z.user_metadata}),C=this.tokenManager.generateRefreshToken(M,f);return await Y.commit(),{user:x(Z),session:k(z),accessToken:l,refreshToken:C}}catch(Z){throw await Y.cancel(),Z}}async completePasswordSignIn($,K,j){let{password_hash:F}=$.identity_data;if(!F)throw J.conflict("This account has no password. Use OTP to sign in.","NO_PASSWORD_SET");if(!await A.compare(K,F))throw J.conflict("Invalid credentials","INVALID_CREDENTIALS");let Q=L($.user_id),V=await this.userManager.findById(Q);if(!V)throw J.notFound("User not found","USER_NOT_FOUND");return this.createAuthSession(V,j)}async completePasswordlessSignUp($,K){let j=$.email?"email":"phone",F=$.email??$.phone,G=$.signup_options,Q=await this.config.db.beginTransaction();try{let[V]=await Q.create(H).content({email:$.email??null,phone:$.phone??null,app_metadata:{...G?.appMetadata,roles:this.config.defaultSignupRoles,signup_device:v(K)},user_metadata:G?.userMetadata,last_sign_in_at:N()});if(!V)throw Error("Failed to create user");let[Y]=await Q.create(U).content({user_id:V.id,provider:j,provider_id:F,identity_data:{}});if(!Y)throw Error("Failed to create identity");let Z=this.sessionManager.getClientIP(K),[X]=await Q.create(b).content({user_id:V.id,ip_address:Z,user_agent:K.headers.get("user-agent"),invalidated_at:null});if(!X)throw Error("Failed to create session");let D=L(V.id),z=L(X.id),M=this.tokenManager.generateAccessToken({sub:D,sessionId:z,appMetadata:V.app_metadata,userMetadata:V.user_metadata}),f=this.tokenManager.generateRefreshToken(D,z);return await Q.commit(),{user:x(V),session:k(X),accessToken:M,refreshToken:f}}catch(V){throw await Q.cancel(),V}}async completePasswordlessSignIn($,K){if(!$.user_id)throw J.internalServerError("Invalid OTP record","INVALID_OTP_RECORD");let j=L($.user_id),F=await this.userManager.findById(j);if(!F)throw J.notFound("User not found","USER_NOT_FOUND");return this.createAuthSession(F,K)}async createAuthSession($,K){if(y($))throw J.notFound("User not found","USER_NOT_FOUND");let j=await this.config.db.beginTransaction();try{let F=this.sessionManager.getClientIP(K),[G]=await j.create(b).content({user_id:$.id,ip_address:F,user_agent:K.headers.get("user-agent"),invalidated_at:null});if(!G)throw Error("Failed to create session");await j.update($.id).merge({last_sign_in_at:N()});let Q=L($.id),V=L(G.id),Y=this.tokenManager.generateAccessToken({sub:Q,sessionId:V,appMetadata:$.app_metadata,userMetadata:$.user_metadata}),Z=this.tokenManager.generateRefreshToken(Q,V);return await j.commit(),{user:x($),session:k(G),accessToken:Y,refreshToken:Z}}catch(F){throw await j.cancel(),F}}validateSignUpMethod($){if($.type==="email")this.validateEmail($.email);else $.phone=this.normalizePhoneNumber($.phone)}validateSignInMethod($){if($.type==="email")this.validateEmail($.email);else $.phone=this.normalizePhoneNumber($.phone)}validateEmail($){if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test($))throw J.badRequest("Invalid email format","INVALID_EMAIL")}normalizePhoneNumber($){let K=$.replace(/\D/g,"");if(K.length===10)return`+1${K}`;if(K.length===11&&K.startsWith("1"))return`+${K}`;if(!K.startsWith("+"))return`+${K}`;return K}extractContact($){return $.type==="email"?{email:$.email}:{phone:$.phone}}extractIdentityKey($){return $.type==="email"?{provider:"email",providerId:$.email}:{provider:"phone",providerId:$.phone}}initializeCleanup(){Promise.all([this.sessionManager.cleanup(),this.otpManager.cleanup()]).catch(()=>{}),n.schedule("0 2 * * *",async()=>{await Promise.all([this.sessionManager.cleanup(),this.otpManager.cleanup()]).catch(()=>{})})}}function L$($){return new u($)}export{K$ as setAuthCookies,P as parseCookies,L$ as createAuth,j$ as clearAuthCookies,O as COOKIE_NAMES,u as AuthService};
|
|
6
6
|
|
|
7
|
-
//# debugId=
|
|
7
|
+
//# debugId=9EA54D295237742464756E2164756E21
|
package/dist/auth/service.d.ts
CHANGED
|
@@ -33,6 +33,11 @@ export declare class AuthService<TAppMetadata extends AppMetadata = AppMetadata,
|
|
|
33
33
|
getUserById(userId: string): Promise<UserRecord<TAppMetadata, TUserMetadata> | undefined>;
|
|
34
34
|
getUserByContact(contact: ContactMethod): Promise<UserRecord<TAppMetadata, TUserMetadata> | undefined>;
|
|
35
35
|
getUserIdentities(userId: string): Promise<IdentityRecord[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Framework-level account tombstone only. Applications perform their own
|
|
38
|
+
* PII/domain cleanup around this call, then use it to revoke every session.
|
|
39
|
+
*/
|
|
40
|
+
markUserDeleted(userId: string): Promise<void>;
|
|
36
41
|
updateUserPassword(userId: string, password: string): Promise<void>;
|
|
37
42
|
updateAppMetadata(userId: string, appMetadata?: Partial<TAppMetadata>): Promise<void>;
|
|
38
43
|
updateUserMetadata(userId: string, userMetadata?: Partial<TUserMetadata>): Promise<void>;
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type UserRecord<TAppMetadata extends AppMetadata = AppMetadata, TUserMeta
|
|
|
23
23
|
app_metadata: TAppMetadata;
|
|
24
24
|
user_metadata: TUserMetadata;
|
|
25
25
|
last_sign_in_at: string | null;
|
|
26
|
+
/** Present after the application has privacy-tombstoned this account. */
|
|
27
|
+
deleted_at?: string;
|
|
26
28
|
};
|
|
27
29
|
export type IdentityData = {
|
|
28
30
|
password_hash?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{z as N}from"zod";class Q{middlewares=[];subscribeMiddlewares=[];constructor($=[],F=[]){this.middlewares=$,this.subscribeMiddlewares=F}use($){return new Q([...this.middlewares,$],this.subscribeMiddlewares)}onSubscribe($){return new Q(this.middlewares,[...this.subscribeMiddlewares,$])}input($){return new I(this.middlewares,$,void 0,this.subscribeMiddlewares)}query($){return{_input:N.object({}),_output:null,_ctx:null,_type:"query",handler:$,middlewares:this.middlewares}}mutation($){return{_input:N.object({}),_output:null,_ctx:null,_type:"mutation",handler:$,middlewares:this.middlewares}}formMutation($){return{_input:N.object({}),_output:null,_ctx:null,_type:"formMutation",handler:$,middlewares:this.middlewares}}subscription($){return{_input:N.object({}),_output:null,_ctx:null,_type:"subscription",handler:$,middlewares:this.middlewares,subscribeMiddlewares:this.subscribeMiddlewares}}file($){return{_input:N.object({}),_output:null,_ctx:null,_type:"file",handler:$,middlewares:this.middlewares}}fileStream($){return{_input:N.object({}),_output:null,_ctx:null,_type:"fileStream",handler:$,middlewares:this.middlewares}}html($){return{_input:N.object({}),_output:null,_ctx:null,_type:"html",handler:$,middlewares:this.middlewares}}}class I{middlewares;inputSchema;timeoutMs;subscribeMiddlewares;constructor($,F,G
|
|
2
|
+
import{z as N}from"zod";class Q{middlewares=[];subscribeMiddlewares=[];constructor($=[],F=[]){this.middlewares=$,this.subscribeMiddlewares=F}use($){return new Q([...this.middlewares,$],this.subscribeMiddlewares)}onSubscribe($){return new Q(this.middlewares,[...this.subscribeMiddlewares,$])}input($){return new I(this.middlewares,$,void 0,this.subscribeMiddlewares)}query($){return{_input:N.object({}),_output:null,_ctx:null,_type:"query",handler:$,middlewares:this.middlewares}}mutation($){return{_input:N.object({}),_output:null,_ctx:null,_type:"mutation",handler:$,middlewares:this.middlewares}}formMutation($){return{_input:N.object({}),_output:null,_ctx:null,_type:"formMutation",handler:$,middlewares:this.middlewares}}subscription($){return{_input:N.object({}),_output:null,_ctx:null,_type:"subscription",handler:$,middlewares:this.middlewares,subscribeMiddlewares:this.subscribeMiddlewares}}file($){return{_input:N.object({}),_output:null,_ctx:null,_type:"file",handler:$,middlewares:this.middlewares}}fileStream($){return{_input:N.object({}),_output:null,_ctx:null,_type:"fileStream",handler:$,middlewares:this.middlewares}}html($){return{_input:N.object({}),_output:null,_ctx:null,_type:"html",handler:$,middlewares:this.middlewares}}}class I{middlewares;inputSchema;timeoutMs;subscribeMiddlewares;constructor($,F,j,G=[]){this.middlewares=$;this.inputSchema=F;this.timeoutMs=j;this.subscribeMiddlewares=G}timeout($){return new I(this.middlewares,this.inputSchema,$,this.subscribeMiddlewares)}onSubscribe($){return new I(this.middlewares,this.inputSchema,this.timeoutMs,[...this.subscribeMiddlewares,$])}query($){return{_input:this.inputSchema,_output:null,_ctx:null,_type:"query",_timeout:this.timeoutMs,handler:$,middlewares:this.middlewares}}mutation($){return{_input:this.inputSchema,_output:null,_ctx:null,_type:"mutation",_timeout:this.timeoutMs,handler:$,middlewares:this.middlewares}}formMutation($){return{_input:this.inputSchema,_output:null,_ctx:null,_type:"formMutation",_timeout:this.timeoutMs,handler:$,middlewares:this.middlewares}}subscription($){return{_input:this.inputSchema,_output:null,_ctx:null,_type:"subscription",_timeout:this.timeoutMs,handler:$,middlewares:this.middlewares,subscribeMiddlewares:this.subscribeMiddlewares}}file($){return{_input:this.inputSchema,_output:null,_ctx:null,_type:"file",_timeout:this.timeoutMs,handler:$,middlewares:this.middlewares}}fileStream($){return{_input:this.inputSchema,_output:null,_ctx:null,_type:"fileStream",_timeout:this.timeoutMs,handler:$,middlewares:this.middlewares}}html($){return{_input:N.object({}),_output:null,_ctx:null,_type:"html",_timeout:this.timeoutMs,handler:$,middlewares:this.middlewares}}}function k($){return new Q}class A 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=A.STATUS_MAP[$.code],this.data=$.data,this.cause=$.cause,Error.captureStackTrace)Error.captureStackTrace(this,A)}toJSON(){return{name:this.name,code:this.code,clientCode:this.clientCode,message:this.message,data:this.data,httpStatus:this.httpStatus}}static badRequest($,F,j){return new A({code:"BAD_REQUEST",message:$,clientCode:F,data:j})}static unauthorized($="Unauthorized",F,j){return new A({code:"UNAUTHORIZED",message:$,clientCode:F,data:j})}static forbidden($="Forbidden",F,j){return new A({code:"FORBIDDEN",message:$,clientCode:F,data:j})}static notFound($="Not Found",F,j){return new A({code:"NOT_FOUND",message:$,clientCode:F,data:j})}static preconditionFailed($="Precondition failed",F,j){//! 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 A({code:"NOT_FOUND",message:$,clientCode:F,data:
|
|
5
|
+
return new A({code:"NOT_FOUND",message:$,clientCode:F,data:j})}static conflict($,F,j){return new A({code:"CONFLICT",message:$,clientCode:F,data:j})}static unprocessableContent($,F,j){return new A({code:"UNPROCESSABLE_CONTENT",message:$,clientCode:F,data:j})}static tooManyRequests($="Too many requests",F,j){return new A({code:"TOO_MANY_REQUESTS",message:$,clientCode:F,data:j})}static internalServerError($="Internal Server Error",F,j){return new A({code:"INTERNAL_SERVER_ERROR",message:$,clientCode:F,data:j})}static timeout($="Request timeout",F,j){return new A({code:"TIMEOUT",message:$,clientCode:F,data:j})}}class M{routeTree;routeCache;options;routeStats;constructor($,F={}){if(this.options={enableCaching:!0,maxCacheSize:1000,debug:!1,...F},this.routeCache=new Map,this.routeStats=this.analyzeRoutes($),this.routeTree=this.compileRoutes($),this.options.debug)console.log("[ROUTEMATCHER]","initialized with stats:",this.routeStats)}match($){if(this.options.enableCaching)return this.matchWithCache($);return this.matchDirect($)}matchWithCache($){let F=$.join("/");if(this.routeCache.has(F)){let G=this.routeCache.get(F);if(this.options.debug&&G)console.log("[ROUTEMATCHER]",`\uD83C\uDFAF Cache hit for route: /${F}`);return G}let j=this.matchDirect($);if(this.cacheResult(F,j),this.options.debug)console.log("[ROUTEMATCHER]",`\uD83D\uDCBE Cached route: /${F} -> ${j?"found":"not found"}`);return j}matchDirect($){let F=this.options.debug?performance.now():0,j=this.traverseTree(this.routeTree,$,0,{});if(this.options.debug){let G=performance.now()-F,_=j?.route._type||"none";console.log("[ROUTEMATCHER]",`\u26A1 Route match: /${$.join("/")} (${_}) took ${G.toFixed(3)}ms`)}return j}traverseTree($,F,j,G){if(j>=F.length){if($.handler)return{params:G,route:$.handler.handler};if($.index)return{params:G,route:$.index.handler};//! This was the last change I made trying to fix
|
|
6
6
|
//! Wildacrd not matching root (/) when index wasn't defined
|
|
7
|
-
if($.wildcard&&F.length===0)return{params:{...J,wildcardPath:""},route:$.wildcard.handler};return null}let W=F[G],_=decodeURIComponent(W),X=$.static.get(_);if(X){let Y=this.traverseTree(X,F,G+1,J);if(Y)return Y}if($.param){let Y={...J,[$.param.name]:_},D=this.traverseTree($.param.node,F,G+1,Y);if(D)return D}if($.wildcard){let Y=F.slice(G).join("/");return{params:{...J,wildcardPath:Y},route:$.wildcard.handler}}return null}compileRoutes($,F=[]){let G={static:new Map};for(let[J,W]of Object.entries($)){if(J==="index"&&this.isProcedure(W)){G.index={handler:W,paramNames:[...F],hasWildcard:!1};continue}if(J==="*"){if(this.isProcedure(W))G.wildcard={handler:W,paramNames:[...F,"wildcardPath"],hasWildcard:!0,wildcardIndex:F.length};continue}if(this.isProcedure(W)){let _={handler:W,paramNames:[...F],hasWildcard:!1};if(J.startsWith(":")){let X=J.slice(1),Y={static:new Map,handler:_};G.param={name:X,node:Y}}else{let X={static:new Map,handler:_};G.static.set(J,X)}}else if(typeof W==="object"&&W!==null)if(J.startsWith(":")){let _=J.slice(1),X=this.compileRoutes(W,[...F,_]);G.param={name:_,node:X}}else{let _=this.compileRoutes(W,F);G.static.set(J,_)}}return G}isProcedure($){return $&&typeof $==="object"&&"_type"in $&&"_input"in $}cacheResult($,F){if(this.routeCache.size>=this.options.maxCacheSize){let G=this.routeCache.keys().next().value;if(G!==void 0){if(this.routeCache.delete(G),this.options.debug)console.log(`\uD83D\uDDD1\uFE0F Evicted oldest cache entry: ${G}`)}}this.routeCache.set($,F)}analyzeRoutes($){let F={totalRoutes:0,staticRoutes:0,paramRoutes:0,wildcardRoutes:0,indexRoutes:0,maxDepth:0,procedureTypes:{query:0,mutation:0,subscription:0,file:0,fileStream:0,streamQuery:0,streamMutation:0,formMutation:0,html:0}},G=(J,W=0)=>{F.maxDepth=Math.max(F.maxDepth,W);for(let[_,X]of Object.entries(J))if(this.isProcedure(X)){if(F.totalRoutes++,X._type in F.procedureTypes)F.procedureTypes[X._type]++;if(_==="index")F.indexRoutes++;else if(_.startsWith(":"))F.paramRoutes++;else if(_==="*")F.wildcardRoutes++;else F.staticRoutes++}else if(typeof X==="object"&&X!==null)G(X,W+1)};return G($),F}clearCache(){if(this.routeCache.clear(),this.options.debug)console.log("[ROUTEMATCHER]","\uD83E\uDDF9 Route cache cleared")}getCacheStats(){return{size:this.routeCache.size,maxSize:this.options.maxCacheSize,routeStats:this.routeStats}}getOptimizationSuggestions(){let $=[],{routeStats:F}=this;if(F.paramRoutes>F.staticRoutes*2)$.push("\uD83D\uDE80 Consider grouping routes to reduce parameter route overhead");if(F.maxDepth>6)$.push("\uD83D\uDCCA Deep route nesting detected - consider flattening some routes");if(F.totalRoutes>200)$.push("\u26A1 Large route table - consider route splitting or lazy loading");if(!this.options.enableCaching&&F.totalRoutes>50)$.push("\uD83D\uDCBE Enable caching for better performance with many routes");if(F.procedureTypes.file>20)$.push("\uD83D\uDCC1 Many file routes detected - consider static file serving");if(F.procedureTypes.subscription>10)$.push("\uD83D\uDD04 Many subscription routes - ensure WebSocket optimization");return $}findRoutesByPattern($){return[]}getRouteInfo($){let F=$.split("/").filter(Boolean),G=F.join("/"),J=this.routeCache.has(G),W=performance.now(),_=this.matchDirect(F),X=performance.now()-W;return{found:!!_,route:_?.route,params:_?.params,cached:J,matchTime:X}}benchmark($,F=1000){let G=$.map((V)=>V.split("/").filter(Boolean)),J=new Map(this.routeCache);this.routeCache.clear();let W=performance.now();for(let V=0;V<F;V++)for(let U of G)this.matchDirect(U);let X=performance.now()-W,Y=F*$.length,D=X/Y,j=Y/X*1000;return this.routeCache=J,{averageTime:D,totalTime:X,routesPerSecond:j}}warmupCache($){if(!this.options.enableCaching)return;let F=performance.now(),G=0;for(let W of $){let _=W.split("/").filter(Boolean),X=this.matchDirect(_);if(X)this.cacheResult(_.join("/"),X),G++}let J=performance.now();if(this.options.debug)console.log("[ROUTEMATCHER]",`\uD83D\uDD25 Cache warmed up: ${G} routes in ${(J-F).toFixed(2)}ms`)}}var q=import.meta.require,v={NO_CACHE:{cacheControl:"no-cache, no-store, must-revalidate",expires:new Date(0).toUTCString()},ONE_YEAR:{cacheControl:"public, max-age=31536000",expires:new Date(Date.now()+31536000000).toUTCString()},IMMUTABLE:{cacheControl:"public, max-age=31536000, immutable",expires:new Date(Date.now()+31536000000).toUTCString()},ONE_MONTH:{cacheControl:"public, max-age=2592000"},ONE_WEEK:{cacheControl:"public, max-age=604800"},ONE_DAY:{cacheControl:"public, max-age=86400, must-revalidate"},ONE_HOUR:{cacheControl:"public, max-age=3600, must-revalidate"},FIVE_MINUTES:{cacheControl:"public, max-age=300"},API:{cacheControl:"public, max-age=300, s-maxage=3600"},STYLESHEET:{cacheControl:"public, max-age=2592000, must-revalidate"},JAVASCRIPT:{cacheControl:"public, max-age=2592000, must-revalidate"},HASHED_ASSET:{cacheControl:"public, max-age=31536000, immutable",expires:new Date(Date.now()+31536000000).toUTCString()},FONT:{cacheControl:"public, max-age=31536000",expires:new Date(Date.now()+31536000000).toUTCString()},IMAGE:{cacheControl:"public, max-age=2592000"},FAVICON:{cacheControl:"public, max-age=31536000",expires:new Date(Date.now()+31536000000).toUTCString()},HTML_PAGE:{cacheControl:"public, max-age=3600, must-revalidate"},API_RESPONSE:{cacheControl:"public, max-age=300, s-maxage=900"},FEED:{cacheControl:"public, max-age=3600"},SITEMAP:{cacheControl:"public, max-age=86400"},MANIFEST:{cacheControl:"public, max-age=86400, must-revalidate"},SERVICE_WORKER:{cacheControl:"no-cache, no-store, must-revalidate",expires:new Date(0).toUTCString()},MEDIA_STREAM:{cacheControl:"no-cache, no-store"},DOCUMENT:{cacheControl:"public, max-age=86400"},ARCHIVE:{cacheControl:"public, max-age=604800"}};function C($){return v[$]}var f={html:"text/html; charset=utf-8",css:"text/css",javascript:"application/javascript",json:"application/json",xml:"application/xml",text:"text/plain; charset=utf-8",csv:"text/csv",markdown:"text/markdown; charset=utf-8",png:"image/png",jpeg:"image/jpeg",gif:"image/gif",svg:"image/svg+xml",webp:"image/webp",icon:"image/x-icon",bmp:"image/bmp",woff:"font/woff",woff2:"font/woff2",truetype:"font/ttf","embedded-opentype":"application/vnd.ms-fontobject",opentype:"font/otf",mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",mpeg:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",zip:"application/zip",tar:"application/x-tar",gzip:"application/gzip","7z":"application/x-7z-compressed",js:"application/javascript",txt:"text/plain; charset=utf-8",jpg:"image/jpeg",ico:"image/x-icon",ttf:"font/ttf",eot:"application/vnd.ms-fontobject",otf:"font/otf",mp3:"audio/mpeg",gz:"application/gzip"};function K($){return f[$]||"application/octet-stream"}function z($){if(typeof Bun<"u"&&Bun.hash)return Bun.hash($).toString(16);try{let F=q("crypto"),G;if(typeof $==="string")G=Buffer.from($);else if($ instanceof ArrayBuffer)G=Buffer.from(new Uint8Array($));else G=Buffer.from($);return F.createHash("md5").update(G).digest("hex")}catch{let F=typeof $==="string"?new TextEncoder().encode($):$ instanceof ArrayBuffer?new Uint8Array($):$,G=0;for(let J=0;J<F.length;J++)G=(G<<5)-G+F[J],G=G&G;return Math.abs(G).toString(16).padStart(16,"0")}}function L($,F,G){let J=$.headers.get("Origin"),W=!0;if((G?.allowAll??!0)&&J)return J;if(F.length===0)return J||"*";if(!J)return F[0];if(F.includes(J))return J;return F[0]}class S{headers={};contentType($){return this.headers["Content-Type"]=K($),this}filePath($){let F=$.split(".").pop()?.toLowerCase()||"";return this.headers["Content-Type"]=K(F),this}mimeType($){return this.headers["Content-Type"]=$,this}cache($){let{cacheControl:F,expires:G}=C($);if(this.headers["Cache-Control"]=F,G)this.headers.Expires=G;return this}eTag($){if(!$)return this;let F;if(typeof $==="string"&&$.startsWith('"'))F=$;else F=`"${z($)}"`;return this.headers.ETag=F,this}lastModified($){return this.headers["Last-Modified"]=$.toUTCString(),this}contentLength($){return this.headers["Content-Length"]=$.toString(),this}redirect($,F=!1){return this.headers.Location=$,this.headers["X-Redirect-Type"]=F?"permanent":"temporary",this}cors({origin:$="*",methods:F=["GET","POST","PUT","DELETE","OPTIONS"],headers:G=["Content-Type","Authorization"],maxAge:J=86400,credentials:W}={}){let _=W??$!=="*";if(_&&$==="*")throw Error('CORS: Cannot use credentials with wildcard origin "*". Specify an exact origin or set credentials to false.');if(this.headers["Access-Control-Allow-Origin"]=$,this.headers["Access-Control-Allow-Methods"]=F.join(", "),this.headers["Access-Control-Allow-Headers"]=G.join(", "),_)this.headers["Access-Control-Allow-Credentials"]="true";return this.headers["Access-Control-Max-Age"]=J.toString(),this}security($){let F={hsts:!0,noSniff:!0,frameOptions:"SAMEORIGIN",xssProtection:!0,...$};if(F.csp)this.headers["Content-Security-Policy"]=F.csp;if(F.hsts){let G=typeof F.hsts==="number"?F.hsts:31536000;this.headers["Strict-Transport-Security"]=`max-age=${G}; includeSubDomains`}if(F.noSniff)this.headers["X-Content-Type-Options"]="nosniff";if(F.frameOptions)this.headers["X-Frame-Options"]=F.frameOptions;if(F.xssProtection)this.headers["X-XSS-Protection"]="1; mode=block";return this}custom($,F){return this.headers[$]=F,this}customHeaders($){return Object.assign(this.headers,$),this}vary(...$){let F=this.headers.Vary,G=F?`${F}, ${$.join(", ")}`:$.join(", ");return this.headers.Vary=G,this}compress($){if($)this.headers["Content-Encoding"]=$;return this}build($){let F={...this.headers};if($&&!F["Content-Length"]){let G=typeof $==="string"?new TextEncoder().encode($).length:$.byteLength;F["Content-Length"]=G.toString()}return F}buildHeaders($){return new Headers(this.build($))}}function Z(){return new S}function p($,F){let G=Z().contentType($);if(F)G.cache(F);return G.build()}var l={css:($)=>Z().contentType("css").cache("STYLESHEET").eTag($),javascript:($)=>Z().contentType("javascript").cache("JAVASCRIPT").eTag($),hashedAsset:($,F)=>Z().filePath($).cache("HASHED_ASSET").eTag(F),font:($)=>Z().filePath($).cache("FONT").cors({origin:"*"}),image:($)=>Z().filePath($).cache("IMAGE"),favicon:()=>Z().contentType("icon").cache("FAVICON"),api:()=>Z().contentType("json").cache("API_RESPONSE").cors(),expensiveApi:($)=>Z().contentType("json").cache("API_RESPONSE").cors().eTag($),realtime:()=>Z().contentType("json").cache("NO_CACHE").cors(),html:()=>Z().contentType("html").cache("HTML_PAGE").security(),expensiveHtml:($)=>Z().contentType("html").cache("HTML_PAGE").security().eTag($),manifest:($)=>Z().contentType("json").cache("MANIFEST").eTag($),serviceWorker:()=>Z().contentType("javascript").cache("SERVICE_WORKER"),sitemap:($)=>Z().contentType("xml").cache("SITEMAP").eTag($),feed:($,F="rss")=>Z().contentType("xml").cache("FEED").eTag($).mimeType(F==="rss"?"application/rss+xml":"application/atom+xml"),video:($)=>Z().filePath($).cache("MEDIA_STREAM").custom("Accept-Ranges","bytes"),audio:($)=>Z().filePath($).cache("MEDIA_STREAM").custom("Accept-Ranges","bytes"),pdf:($)=>Z().contentType("pdf").cache("DOCUMENT").custom("Content-Disposition","inline").eTag($),download:($,F,G)=>Z().filePath(F).cache("ARCHIVE").custom("Content-Disposition",`attachment; filename="${$}"`).eTag(G),secure:()=>Z().security({csp:"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",hsts:31536000}).custom("X-Powered-By","Bun"),permanentRedirect:($)=>Z().redirect($,!0),temporaryRedirect:($)=>Z().redirect($,!1)};async function T($,F,G,J={}){let{maxChunkSize:W=2097152,cacheMaxAge:_=3600,acceptedExtensions:X}=J;if(!await $.exists())return new Response("Media file not found",{status:404,headers:Z().contentType("text").cors({origin:L(F,G)}).build()});if(X&&X.length>0){let b=$.name?.toLowerCase().split(".").pop();if(!b||!X.includes(b))return new Response("File type not supported",{status:415,headers:Z().contentType("text").cors({origin:L(F,G)}).build()})}let D=$.size,j=F.headers.get("Range");if(!j)return new Response($,{headers:Z().filePath($.name||"media").custom("Accept-Ranges","bytes").custom("Cache-Control",`public, max-age=${_}`).cors({origin:L(F,G)}).build()});let V=j.match(/bytes=(\d*)-(\d*)/);if(!V)return new Response("Invalid Range header",{status:416,headers:Z().contentType("text").cors({origin:L(F,G)}).custom("Content-Range",`bytes */${D}`).build()});let U=V[1]?parseInt(V[1]):0,R=V[2]?parseInt(V[2]):D-1,O=Math.min(R,U+W-1,D-1);if(U>=D||O>=D||U>O)return new Response("Range not satisfiable",{status:416,headers:Z().contentType("text").cors({origin:L(F,G)}).custom("Content-Range",`bytes */${D}`).build()});return new Response($.slice(U,O+1),{status:206,headers:Z().filePath($.name||"media").custom("Accept-Ranges","bytes").custom("Cache-Control",`public, max-age=${_}`).cors({origin:L(F,G)}).build()})}function y($,F,G){return{type:"stream",file:$,request:F,options:G}}class H{routes;routeMatcher;contextCreator;prefix;globalMiddlewares;server=null;wsConfig;integrations;development=!0;debug=!1;allowedOrigins=[];onError;activeConnections=new Set;maxConnections=parseInt(process.env.MAX_WS_CONNECTIONS??"1000");connectionTimeout=parseInt(process.env.WS_TIMEOUT??"30000");maxRequestSize=parseInt(process.env.MAX_REQUEST_SIZE??"10485760");requestTimeout=parseInt(process.env.REQUEST_TIMEOUT??"30000");constructor($){if(this.contextCreator=$.context,this.prefix=$.prefix??"",this.routes=$.routes,this.globalMiddlewares=$.globalMiddlewares??[],this.wsConfig=$.websocket??{},this.integrations=$.integrations,this.debug=!!$.debug&&!0,this.allowedOrigins=$.allowedOrigins??[],this.onError=$.onError,this.routeMatcher=new M($.routes,{enableCaching:!this.development,maxCacheSize:parseInt(process.env.ROUTE_CACHE_SIZE??"1000"),debug:this.debug}),!this.development)setInterval(()=>this.cleanupStaleConnections(),60000);if(this.debug)console.log("Router initialized with route matcher"),console.log("Route cache stats:",this.routeMatcher.getCacheStats())}findRoute($){return this.routeMatcher.match($)}async runMiddlewares($,F){for(let G of $){let J=await G(F);if(J&&typeof J==="object")Object.assign(F,J)}}async parseInput($,F,G){let J={},W=new URL($.url),_=Object.fromEntries(W.searchParams),X=$.method;if(X==="POST"){if(parseInt($.headers.get("content-length")||"0")>this.maxRequestSize)throw Error("Request payload too large")}if(X==="GET"&&["query","streamQuery","file","fileStream","html"].includes(G))J=_;else if(X==="POST"&&["mutation","streamMutation"].includes(G))if($.headers.get("content-type")?.includes("application/json"))J=await $.json();else throw new A({code:"BAD_REQUEST",message:"Unsupported content type"});else if(X==="POST"&&G==="formMutation"){if(!$.headers.get("content-type")?.includes("multipart/form-data"))throw new A({code:"BAD_REQUEST",message:"Unsupported content type"});J=await this.parseFormData(await $.formData())}else throw new A({code:"METHOD_NOT_SUPPORTED",message:"Unsupported request method"});return F.parse(J)}async parseFormData($){let F=Object.create(null);return $.forEach((G,J)=>{this.setFormDataValue(F,this.parseFormDataPath(J),G)}),F}setFormDataValue($,F,G){let J=$;for(let W=0;W<F.length;W++){let _=F[W],X=W===F.length-1,Y=this.isFormDataArrayIndex(F[W+1]);if(Array.isArray(J)){if(!this.isFormDataArrayIndex(_))throw A.badRequest("Invalid multipart array path");let j=Number(_);if(j>J.length)throw A.badRequest("Multipart array indexes must be contiguous");if(X){if(j===J.length)J.push(G);else if(J[j]===void 0)J[j]=G;else throw A.badRequest("Duplicate multipart field path");return}let V=J[j];if(V===void 0)J[j]=Y?[]:Object.create(null);else if(Array.isArray(V)!==Y)throw A.badRequest("Conflicting multipart field path");J=J[j];continue}if(X){if(Object.hasOwn(J,_))throw A.badRequest("Duplicate multipart field path");J[_]=G;return}let D=J[_];if(D===void 0)J[_]=Y?[]:Object.create(null);else if(Array.isArray(D)!==Y)throw A.badRequest("Conflicting multipart field path");J=J[_]}}parseFormDataPath($){let F=/^([^\[\]]+)((?:\[[^\[\]]+\])*)$/.exec($);if(!F)throw A.badRequest("Invalid multipart field path");let G=[F[1]];for(let J of F[2].matchAll(/\[([^\[\]]+)\]/g))G.push(J[1]);if(G.some((J)=>["__proto__","constructor","prototype"].includes(J)))throw A.badRequest("Invalid multipart field path");return G}isFormDataArrayIndex($){return $!==void 0&&/^(0|[1-9]\d*)$/.test($)}handleError($,F){if(this.onError)try{this.onError($,{req:F,route:F.url})}catch(J){console.error("Error logger failed:",J)}let G=this.serializeError($);return new Response(JSON.stringify({error:G.error}),{status:G.status,headers:Z().contentType("json").cors({origin:L(F,this.allowedOrigins)}).build()})}handleFileCacheControl($){let F;if($?.includes("text/html"))F="public, max-age=0, must-revalidate";else if($?.includes("text/css")||$?.includes("text/javascript")||$?.includes("application/javascript"))F="public, max-age=31536000, immutable";else F="public, max-age=86400, stale-while-revalidate=604800";return F}async handleRequest($,F){try{if($.method==="OPTIONS")return new Response(null,{status:204,headers:Z().cors({origin:L($,this.allowedOrigins)}).build()});let J=new URL($.url).pathname;if(J=J.startsWith(this.prefix)?J.slice(this.prefix.length):J,J=J.startsWith("/")?J:"/"+J,J==="/ws"&&F){if(this.debug)console.log("\uD83D\uDD0C WebSocket upgrade requested");return this.handleWebSocketUpgrade($,F)}let W=J==="/"?[]:J.split("/").filter(Boolean),_=this.findRoute(W);if(!_)return new Response("Not Found",{status:404,headers:Z().contentType("text").cors({origin:L($,this.allowedOrigins)}).build()});let{route:X}=_,Y=X._timeout??this.requestTimeout,D=new Promise((j,V)=>{setTimeout(()=>V(Error("Request timeout")),Y)});return await Promise.race([this._handleRequest($,F,_),D])}catch(G){return this.handleError(G,$)}}async _handleRequest($,F,G){try{if(!G)return new Response("Not Found",{status:404,headers:Z().contentType("text").cors({origin:L($,this.allowedOrigins)}).build()});let{params:J,route:W}=G;if(!W.handler)return new Response("Handler not implemented",{status:501,headers:Z().contentType("text").cors({origin:L($,this.allowedOrigins)}).build()});let _=await this.contextCreator($);Object.assign(_,{req:$,params:J,publishToProcedure:async(j,V,U)=>{await this.publishToProcedure(j,V,_,U)}});for(let j of this.globalMiddlewares){let V=await j(_);if(V!==null&&V!==void 0&&typeof V==="object")Object.assign(_,V)}for(let j of W.middlewares){let V=await j(_);if(V!==null&&V!==void 0&&typeof V==="object")Object.assign(_,V)}let X=await this.parseInput($,W._input,W._type),Y=await W.handler({ctx:_,input:X}),D=Z().cors({origin:L($,_.allowedOrigins??this.allowedOrigins)}).build();return Object.keys(D).forEach((j)=>{_.headers.set(j,D[j])}),await this.handleProcedureResponse(W._type,Y,_.headers)}catch(J){return this.handleError(J,$)}}async testRequest($){return this.handleRequest($)}async handleProcedureResponse($,F,G){switch($){case"file":if(F instanceof Response)return F;let J=F.type;return G.append("Content-Type",J),G.append("Cache-Control",this.development?"public, max-age=0, must-revalidate":this.handleFileCacheControl(J)),new Response(F,{headers:G});case"fileStream":if(typeof F==="object"&&F!==null&&"type"in F&&F.type==="stream"){let _=F;return await T(F.file,F.request,this.allowedOrigins,_.options)}let W=F.type;return G.append("Content-Type",W),G.append("Cache-Control",this.development?"public, max-age=0, must-revalidate":this.handleFileCacheControl(W)),new Response(F,{headers:G});case"html":if(typeof F==="string")return G.append("Content-Type","text/html"),new Response(F,{headers:G});break;case"query":case"mutation":case"formMutation":case"streamQuery":case"streamMutation":case"subscription":default:return G.append("Content-Type","application/json"),new Response(JSON.stringify({data:F}),{headers:G})}return G.append("Content-Type","application/json"),new Response(JSON.stringify({data:F}),{headers:G})}cleanupStaleConnections(){let $=Date.now();for(let F of this.activeConnections)if($-F.data.lastActivity>this.connectionTimeout)F.close(),this.activeConnections.delete(F)}async handleWebSocketUpgrade($,F){if(this.activeConnections.size>=this.maxConnections)return new Response("Too many connections",{status:503});let G=await this.createWebSocketContext($);if(F.upgrade($,{data:{ctx:G,topics:new Set,lastActivity:Date.now()}}))return new Response(null,{status:101});return new Response("WebSocket upgrade failed",{status:500})}async createWebSocketContext($){let F=await this.contextCreator($);return Object.assign(F,{req:$}),await this.runMiddlewares(this.globalMiddlewares,F),F}createWebSocketOperationContext($,F){return{...$,params:F}}serializeError($){let F=$,G=F?.issues??F?.errors;if(Array.isArray(G)&&F.name==="ZodError")return{status:400,error:{name:"ValidationError",code:"BAD_REQUEST",clientCode:"VALIDATION_ERROR",message:"Input validation failed",data:{validationErrors:G.map((J)=>({path:J.path.join("."),message:J.message,code:J.code,received:J.received}))},httpStatus:400}};if($ instanceof A)return{status:$.httpStatus,error:$.toJSON()};if($ instanceof Error){let J=$.message==="Not Found"?404:$.message==="Unauthorized"?401:$.message==="Forbidden"?403:$.message==="Request payload too large"?413:$.message==="Request timeout"?504:500;return{status:J,error:{code:J===404?"NOT_FOUND":J===401?"UNAUTHORIZED":J===403?"FORBIDDEN":J===413?"PAYLOAD_TOO_LARGE":J===504?"GATEWAY_TIMEOUT":"INTERNAL_SERVER_ERROR",message:J<500?$.message:"Internal Server Error"}}}return{status:500,error:{code:"INTERNAL_SERVER_ERROR",message:"Internal Server Error"}}}sendWebSocketError($,F,G,J){let W=F==="authenticate"?"auth_error":F==="subscribe"?"subscribe_error":F==="unsubscribe"?"unsubscribe_error":F==="publish"?"publish_error":"error",_=this.serializeError(J);$.send(JSON.stringify({type:W,...G?{topic:G}:{},error:{..._.error,httpStatus:_.error.httpStatus??_.status}}))}async handleAuthenticate($,F,G){try{let J=F.startsWith("Bearer ")?F.substring(7):F,W=new Headers(G.req.headers);if(J)W.set("authorization",`Bearer ${J}`);else W.delete("authorization");let _=new Request(G.req,{headers:W});if($.data.ctx=await this.createWebSocketContext(_),$.send(JSON.stringify({type:"auth_success",message:"Authentication successful"})),this.debug)console.log("[BRPC]","WebSocket authenticated successfully")}catch(J){if(this.sendWebSocketError($,"authenticate",void 0,J),this.debug)console.error("WebSocket authentication error:",J)}}async handleSubscribe($,F,G){let J=this.findRoute(F.split("/").filter(Boolean));if(!J||J.route._type!=="subscription")throw Error("Invalid subscription route");let W=this.createWebSocketOperationContext(G,J.params);await this.runMiddlewares(J.route.subscribeMiddlewares??[],W),$.subscribe(F),$.data.topics.add(F),$.send(JSON.stringify({type:"subscribe_success",topic:F}))}async handleUnsubscribe($,F,G){let J=this.findRoute(F.split("/").filter(Boolean));if(!J||J.route._type!=="subscription")throw Error("Invalid subscription route");$.unsubscribe(F),$.data.topics.delete(F)}async handlePublish($,F,G,J){let W=this.findRoute(F.split("/").filter(Boolean));if(!W)throw Error("Subscription not found");let{params:_,route:X}=W;if(X._type!=="subscription"||!X.handler)throw Error("Invalid subscription route");let Y=this.createWebSocketOperationContext(J,_);await this.runMiddlewares(X.middlewares,Y);let D=X._input.fields?X._input.parse(G):G,j=await X.handler({ctx:Y,input:D}),V={topic:F,data:j,type:"publish"};if(this.debug)console.log(`Server will publish to ${F}`,V);this.server.publish(F,JSON.stringify(V))}async publishToProcedure($,F,G,J){if($._type!=="subscription"||!$.handler)throw Error("Can only publish to subscription procedures");let W=this.resolveSubscriptionTopic($,J),_=await $.handler({ctx:{...G,params:J},input:F}),X={topic:W,data:_,type:"publish"};if(this.debug)console.log(`Server will publish to ${W}`,X);return this.server?.publish(W,JSON.stringify(X)),_}async publish($,F,G){if($._type!=="subscription"||!$.handler)throw Error("Can only publish to subscription procedures");let J=this.resolveSubscriptionTopic($,G),W=await $.handler({ctx:{params:G},input:F}),_={topic:J,data:W,type:"publish"};if(this.debug)console.log(`Server will publish to ${J}`,_);return this.server?.publish(J,JSON.stringify(_)),W}freePublish($,F){let G={topic:$,data:F,type:"publish"};this.server?.publish($,JSON.stringify(G))}findRoutePath($,F,G=""){for(let[J,W]of Object.entries($)){let _=G?`${G}/${J}`:J;if(W===F)return _;if(W&&typeof W==="object"&&!("_type"in W)){let X=this.findRoutePath(W,F,_);if(X)return X}}return null}resolveSubscriptionTopic($,F){let G=this.findRoutePath(this.routes,$);if(!G)throw Error("Subscription route not found");return G.split("/").map((J)=>{if(!J.startsWith(":"))return J;let W=J.slice(1),_=F?.[W];if(!_)throw Error(`Missing parameter: ${W}`);return encodeURIComponent(_)}).join("/")}getIntegrationRoutes(){if(!this.integrations)return;let $={};if(this.integrations.betterAuth){let F=this.integrations.betterAuth.handler;if(!F)throw Error("Please provide a handler for betterAuth integration");$["/api/auth/*"]=async(G)=>{return await F(G)}}if(this.integrations.rawRoutes)Object.keys(this.integrations.rawRoutes).forEach((F)=>{$[F]=this.integrations.rawRoutes[F]});return Object.keys($).length>0?$:void 0}getRouteStats(){return{cache:this.routeMatcher.getCacheStats(),suggestions:this.routeMatcher.getOptimizationSuggestions()}}clearRouteCache(){if(this.routeMatcher.clearCache(),this.debug)console.log("[ROUTEMATCHER] Route cache cleared")}benchmarkRoutes($=["/users/123","/posts/456/comments"],F=1000){if(this.debug){let G=this.routeMatcher.benchmark($,F);return console.log("[ROUTEMATCHER]","Route matching benchmark:",G),G}}updateRoutes($){if(this.development)this.routes=$,this.routeMatcher=new M($,{enableCaching:!1,debug:this.debug}),console.log("[HMR]","Server Routes updated");else console.warn("Route updates are only allowed in development mode")}get url(){return this.server?.url.toString()??null}listen($,F){if(this.server=Bun.serve({port:$,routes:this.getIntegrationRoutes(),fetch:(G,J)=>this.handleRequest(G,J),websocket:{open:(G)=>{if(this.activeConnections.add(G),this.wsConfig?.onOpen)this.wsConfig.onOpen(G,G.data.ctx);if(this.debug)console.log("[BRPC]","WebSocket connection opened",{connections:this.activeConnections.size})},message:async(G,J)=>{let W,_;try{G.data.lastActivity=Date.now();let X=JSON.parse(J),{token:Y,data:D}=X;W=X.type,_=X.topic;let j=G.data.ctx;if(this.debug)console.log("[BRPC]","WebSocket message received:",{type:W,topic:_});switch(W){case"authenticate":await this.handleAuthenticate(G,Y,j);break;case"subscribe":if(!_)throw A.badRequest("Subscription topic is required");await this.handleSubscribe(G,_,j);break;case"unsubscribe":if(!_)throw A.badRequest("Subscription topic is required");await this.handleUnsubscribe(G,_,j);break;case"publish":if(!_)throw A.badRequest("Subscription topic is required");await this.handlePublish(G,_,D,j);break;default:if(this.sendWebSocketError(G,W,_,A.badRequest("Unknown message type")),this.debug)console.error("Unknown WebSocket message type:",W)}}catch(X){if(this.debug)console.error("[BRPC]","WebSocket message error:",X);this.sendWebSocketError(G,W,_,X)}},close:(G,J,W)=>{if(this.activeConnections.delete(G),G.data.topics.forEach((_)=>{G.unsubscribe(_)}),this.debug)console.log("[BRPC]","WebSocket connection closed",{code:J,reason:W,remainingConnections:this.activeConnections.size});if(this.wsConfig?.onClose)this.wsConfig.onClose(G,J,W,G.data.ctx)}}}),F)F()}stop(){this.server?.stop(!0),this.server=null,this.activeConnections.clear()}}function a($){return new H($)}function r($){return $}import{z as x}from"zod";var P={image:["image/jpeg","image/png","image/gif","image/webp","image/svg+xml"],video:["video/mp4","video/webm","video/ogg"],audio:["audio/mpeg","audio/ogg","audio/wav"],document:["application/pdf","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]},B=($,F="MB")=>{switch(F){case"MB":return $*1024*1024;case"KB":return $*1024;case"B":return $}},F$=({acceptedTypes:$,maxSize:F=1/0,minSize:G,messages:J={}}={})=>{let W=F?B(F):1/0,_=G?B(G):0,X=$?Object.entries($).flatMap(([D,j])=>{let V=D;if(j==="*")return P[V];if(Array.isArray(j))return j;return[]}):void 0,Y=x.instanceof(File,{message:J.required??"File is required"});if(Y=Y.refine((D)=>D.size<=W,J.maxSize??`File size must be less than ${F}MB`).refine((D)=>D.size>=_,J.minSize??`File size must be at least ${G}MB`),X?.length)Y=Y.refine((D)=>X.includes(D.type),J.type??`File must be of type: ${X.join(", ")}`);return Y};var J$=($)=>{let F=$.paths.map((J)=>new RegExp(J)),G="Not Found";return async(J)=>{let _=new URL(J.req.url).pathname;if(F.some((X)=>X.test(_)))throw Error("Not Found")}};class E{requests=new Map;lastCleanupTime=Date.now();alertThreshold;config;constructor($){this.config={windowMs:$.windowMs,maxRequests:$.maxRequests,maxEntries:$.maxEntries??1e4,cleanupIntervalMs:$.cleanupIntervalMs??60000,message:$.message??"Too Many Requests",statusCode:$.statusCode??429,headerPrefix:$.headerPrefix??"X-RateLimit"},this.alertThreshold=this.config.maxEntries*0.8}cleanup(){let $=Date.now(),F=0;for(let[G,J]of this.requests.entries()){let W=$-J.startTime>this.config.windowMs,_=$-J.lastAccessed>this.config.windowMs*2;if(W||_)this.requests.delete(G),F++}return this.lastCleanupTime=$,F}handleMaxEntries($){let F=this.requests.size;if(Date.now()-this.lastCleanupTime>this.config.cleanupIntervalMs)this.cleanup();if(F>=this.alertThreshold)console.warn(`Rate limiter at ${Math.round(F/this.config.maxEntries*100)}% capacity`);if(this.requests.has($))return!0;return F<this.config.maxEntries}check($){if(!this.handleMaxEntries($))return{isLimited:!0,remaining:0,resetTime:Date.now()+this.config.windowMs};let F=Date.now(),G=this.requests.get($);if(!G||F-G.startTime>=this.config.windowMs)return this.requests.set($,{count:1,startTime:F,lastAccessed:F}),{isLimited:!1,remaining:this.config.maxRequests-1,resetTime:F+this.config.windowMs};G.lastAccessed=F;let J=G.count>=this.config.maxRequests;if(!J)G.count++;return{isLimited:J,remaining:Math.max(0,this.config.maxRequests-G.count),resetTime:G.startTime+this.config.windowMs}}getConfig(){return this.config}}var W$=($)=>{let F=new E($);return async(G)=>{if(G.req.headers.get("range"))return;let J=G.req.headers.get("x-forwarded-for")?.split(",")[0]||G.req.headers.get("x-real-ip")||"unknown",{isLimited:W,remaining:_,resetTime:X}=F.check(J),{headerPrefix:Y}=F.getConfig();if(G.headers.set(`${Y}-Remaining`,_.toString()),G.headers.set(`${Y}-Reset`,X.toString()),W)throw G.headers.set(`${Y}-Exceeded`,"true"),Error(F.getConfig().message)}};function Y$($,F){return F??$}async function D$($,F){let G=performance.now();try{return await F()}catch(J){let W=performance.now();throw console.log(`[PERF] ${$} failed after ${(W-G).toFixed(4)} ms`),J}finally{let J=performance.now();console.log(`[PERF] ${$}: ${(J-G).toFixed(4)} ms`)}}export{y as streamMedia,p as quickHeaders,D$ as measure,z as hashContent,K as getMimeType,L as getCorsOrigin,a as createRouter,W$ as createRateLimiter,k as createProcedure,J$ as createPathBlocker,Y$ as createMiddleware,F$ as createFileSchema,r as createContext,l as commonHeaders,Z as buildHeaders,H as Router,A as BRPCError};
|
|
7
|
+
if($.wildcard&&F.length===0)return{params:{...G,wildcardPath:""},route:$.wildcard.handler};return null}let _=F[j],W=decodeURIComponent(_),J=$.static.get(W);if(J){let Y=this.traverseTree(J,F,j+1,G);if(Y)return Y}if($.param){let Y={...G,[$.param.name]:W},V=this.traverseTree($.param.node,F,j+1,Y);if(V)return V}if($.wildcard){let Y=F.slice(j).join("/");return{params:{...G,wildcardPath:Y},route:$.wildcard.handler}}return null}compileRoutes($,F=[]){let j={static:new Map};for(let[G,_]of Object.entries($)){if(G==="index"&&this.isProcedure(_)){j.index={handler:_,paramNames:[...F],hasWildcard:!1};continue}if(G==="*"){if(this.isProcedure(_))j.wildcard={handler:_,paramNames:[...F,"wildcardPath"],hasWildcard:!0,wildcardIndex:F.length};continue}if(this.isProcedure(_)){let W={handler:_,paramNames:[...F],hasWildcard:!1};if(G.startsWith(":")){let J=G.slice(1),Y={static:new Map,handler:W};j.param={name:J,node:Y}}else{let J={static:new Map,handler:W};j.static.set(G,J)}}else if(typeof _==="object"&&_!==null)if(G.startsWith(":")){let W=G.slice(1),J=this.compileRoutes(_,[...F,W]);j.param={name:W,node:J}}else{let W=this.compileRoutes(_,F);j.static.set(G,W)}}return j}isProcedure($){return $&&typeof $==="object"&&"_type"in $&&"_input"in $}cacheResult($,F){if(this.routeCache.size>=this.options.maxCacheSize){let j=this.routeCache.keys().next().value;if(j!==void 0){if(this.routeCache.delete(j),this.options.debug)console.log(`\uD83D\uDDD1\uFE0F Evicted oldest cache entry: ${j}`)}}this.routeCache.set($,F)}analyzeRoutes($){let F={totalRoutes:0,staticRoutes:0,paramRoutes:0,wildcardRoutes:0,indexRoutes:0,maxDepth:0,procedureTypes:{query:0,mutation:0,subscription:0,file:0,fileStream:0,streamQuery:0,streamMutation:0,formMutation:0,html:0}},j=(G,_=0)=>{F.maxDepth=Math.max(F.maxDepth,_);for(let[W,J]of Object.entries(G))if(this.isProcedure(J)){if(F.totalRoutes++,J._type in F.procedureTypes)F.procedureTypes[J._type]++;if(W==="index")F.indexRoutes++;else if(W.startsWith(":"))F.paramRoutes++;else if(W==="*")F.wildcardRoutes++;else F.staticRoutes++}else if(typeof J==="object"&&J!==null)j(J,_+1)};return j($),F}clearCache(){if(this.routeCache.clear(),this.options.debug)console.log("[ROUTEMATCHER]","\uD83E\uDDF9 Route cache cleared")}getCacheStats(){return{size:this.routeCache.size,maxSize:this.options.maxCacheSize,routeStats:this.routeStats}}getOptimizationSuggestions(){let $=[],{routeStats:F}=this;if(F.paramRoutes>F.staticRoutes*2)$.push("\uD83D\uDE80 Consider grouping routes to reduce parameter route overhead");if(F.maxDepth>6)$.push("\uD83D\uDCCA Deep route nesting detected - consider flattening some routes");if(F.totalRoutes>200)$.push("\u26A1 Large route table - consider route splitting or lazy loading");if(!this.options.enableCaching&&F.totalRoutes>50)$.push("\uD83D\uDCBE Enable caching for better performance with many routes");if(F.procedureTypes.file>20)$.push("\uD83D\uDCC1 Many file routes detected - consider static file serving");if(F.procedureTypes.subscription>10)$.push("\uD83D\uDD04 Many subscription routes - ensure WebSocket optimization");return $}findRoutesByPattern($){return[]}getRouteInfo($){let F=$.split("/").filter(Boolean),j=F.join("/"),G=this.routeCache.has(j),_=performance.now(),W=this.matchDirect(F),J=performance.now()-_;return{found:!!W,route:W?.route,params:W?.params,cached:G,matchTime:J}}benchmark($,F=1000){let j=$.map((D)=>D.split("/").filter(Boolean)),G=new Map(this.routeCache);this.routeCache.clear();let _=performance.now();for(let D=0;D<F;D++)for(let U of j)this.matchDirect(U);let J=performance.now()-_,Y=F*$.length,V=J/Y,Z=Y/J*1000;return this.routeCache=G,{averageTime:V,totalTime:J,routesPerSecond:Z}}warmupCache($){if(!this.options.enableCaching)return;let F=performance.now(),j=0;for(let _ of $){let W=_.split("/").filter(Boolean),J=this.matchDirect(W);if(J)this.cacheResult(W.join("/"),J),j++}let G=performance.now();if(this.options.debug)console.log("[ROUTEMATCHER]",`\uD83D\uDD25 Cache warmed up: ${j} routes in ${(G-F).toFixed(2)}ms`)}}var f=import.meta.require,v={NO_CACHE:{cacheControl:"no-cache, no-store, must-revalidate",expires:new Date(0).toUTCString()},ONE_YEAR:{cacheControl:"public, max-age=31536000",expires:new Date(Date.now()+31536000000).toUTCString()},IMMUTABLE:{cacheControl:"public, max-age=31536000, immutable",expires:new Date(Date.now()+31536000000).toUTCString()},ONE_MONTH:{cacheControl:"public, max-age=2592000"},ONE_WEEK:{cacheControl:"public, max-age=604800"},ONE_DAY:{cacheControl:"public, max-age=86400, must-revalidate"},ONE_HOUR:{cacheControl:"public, max-age=3600, must-revalidate"},FIVE_MINUTES:{cacheControl:"public, max-age=300"},API:{cacheControl:"public, max-age=300, s-maxage=3600"},STYLESHEET:{cacheControl:"public, max-age=2592000, must-revalidate"},JAVASCRIPT:{cacheControl:"public, max-age=2592000, must-revalidate"},HASHED_ASSET:{cacheControl:"public, max-age=31536000, immutable",expires:new Date(Date.now()+31536000000).toUTCString()},FONT:{cacheControl:"public, max-age=31536000",expires:new Date(Date.now()+31536000000).toUTCString()},IMAGE:{cacheControl:"public, max-age=2592000"},FAVICON:{cacheControl:"public, max-age=31536000",expires:new Date(Date.now()+31536000000).toUTCString()},HTML_PAGE:{cacheControl:"public, max-age=3600, must-revalidate"},API_RESPONSE:{cacheControl:"public, max-age=300, s-maxage=900"},FEED:{cacheControl:"public, max-age=3600"},SITEMAP:{cacheControl:"public, max-age=86400"},MANIFEST:{cacheControl:"public, max-age=86400, must-revalidate"},SERVICE_WORKER:{cacheControl:"no-cache, no-store, must-revalidate",expires:new Date(0).toUTCString()},MEDIA_STREAM:{cacheControl:"no-cache, no-store"},DOCUMENT:{cacheControl:"public, max-age=86400"},ARCHIVE:{cacheControl:"public, max-age=604800"}};function q($){return v[$]}var C={html:"text/html; charset=utf-8",css:"text/css",javascript:"application/javascript",json:"application/json",xml:"application/xml",text:"text/plain; charset=utf-8",csv:"text/csv",markdown:"text/markdown; charset=utf-8",png:"image/png",jpeg:"image/jpeg",gif:"image/gif",svg:"image/svg+xml",webp:"image/webp",icon:"image/x-icon",bmp:"image/bmp",woff:"font/woff",woff2:"font/woff2",truetype:"font/ttf","embedded-opentype":"application/vnd.ms-fontobject",opentype:"font/otf",mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",mpeg:"audio/mpeg",wav:"audio/wav",flac:"audio/flac",pdf:"application/pdf",doc:"application/msword",docx:"application/vnd.openxmlformats-officedocument.wordprocessingml.document",zip:"application/zip",tar:"application/x-tar",gzip:"application/gzip","7z":"application/x-7z-compressed",js:"application/javascript",txt:"text/plain; charset=utf-8",jpg:"image/jpeg",ico:"image/x-icon",ttf:"font/ttf",eot:"application/vnd.ms-fontobject",otf:"font/otf",mp3:"audio/mpeg",gz:"application/gzip"};function K($){return C[$]||"application/octet-stream"}function z($){if(typeof Bun<"u"&&Bun.hash)return Bun.hash($).toString(16);try{let F=f("crypto"),j;if(typeof $==="string")j=Buffer.from($);else if($ instanceof ArrayBuffer)j=Buffer.from(new Uint8Array($));else j=Buffer.from($);return F.createHash("md5").update(j).digest("hex")}catch{let F=typeof $==="string"?new TextEncoder().encode($):$ instanceof ArrayBuffer?new Uint8Array($):$,j=0;for(let G=0;G<F.length;G++)j=(j<<5)-j+F[G],j=j&j;return Math.abs(j).toString(16).padStart(16,"0")}}function L($,F,j){let G=$.headers.get("Origin"),_=Reflect.get(process.env,"NODE_ENV")!=="production";if((j?.allowAll??_)&&G)return G;if(F.length===0)return G||"*";if(!G)return F[0];if(F.includes(G))return G;return F[0]}class S{headers={};contentType($){return this.headers["Content-Type"]=K($),this}filePath($){let F=$.split(".").pop()?.toLowerCase()||"";return this.headers["Content-Type"]=K(F),this}mimeType($){return this.headers["Content-Type"]=$,this}cache($){let{cacheControl:F,expires:j}=q($);if(this.headers["Cache-Control"]=F,j)this.headers.Expires=j;return this}eTag($){if(!$)return this;let F;if(typeof $==="string"&&$.startsWith('"'))F=$;else F=`"${z($)}"`;return this.headers.ETag=F,this}lastModified($){return this.headers["Last-Modified"]=$.toUTCString(),this}contentLength($){return this.headers["Content-Length"]=$.toString(),this}redirect($,F=!1){return this.headers.Location=$,this.headers["X-Redirect-Type"]=F?"permanent":"temporary",this}cors({origin:$="*",methods:F=["GET","POST","PUT","DELETE","OPTIONS"],headers:j=["Content-Type","Authorization"],maxAge:G=86400,credentials:_}={}){let W=_??$!=="*";if(W&&$==="*")throw Error('CORS: Cannot use credentials with wildcard origin "*". Specify an exact origin or set credentials to false.');if(this.headers["Access-Control-Allow-Origin"]=$,this.headers["Access-Control-Allow-Methods"]=F.join(", "),this.headers["Access-Control-Allow-Headers"]=j.join(", "),W)this.headers["Access-Control-Allow-Credentials"]="true";return this.headers["Access-Control-Max-Age"]=G.toString(),this}security($){let F={hsts:!0,noSniff:!0,frameOptions:"SAMEORIGIN",xssProtection:!0,...$};if(F.csp)this.headers["Content-Security-Policy"]=F.csp;if(F.hsts){let j=typeof F.hsts==="number"?F.hsts:31536000;this.headers["Strict-Transport-Security"]=`max-age=${j}; includeSubDomains`}if(F.noSniff)this.headers["X-Content-Type-Options"]="nosniff";if(F.frameOptions)this.headers["X-Frame-Options"]=F.frameOptions;if(F.xssProtection)this.headers["X-XSS-Protection"]="1; mode=block";return this}custom($,F){return this.headers[$]=F,this}customHeaders($){return Object.assign(this.headers,$),this}vary(...$){let F=this.headers.Vary,j=F?`${F}, ${$.join(", ")}`:$.join(", ");return this.headers.Vary=j,this}compress($){if($)this.headers["Content-Encoding"]=$;return this}build($){let F={...this.headers};if($&&!F["Content-Length"]){let j=typeof $==="string"?new TextEncoder().encode($).length:$.byteLength;F["Content-Length"]=j.toString()}return F}buildHeaders($){return new Headers(this.build($))}}function X(){return new S}function p($,F){let j=X().contentType($);if(F)j.cache(F);return j.build()}var l={css:($)=>X().contentType("css").cache("STYLESHEET").eTag($),javascript:($)=>X().contentType("javascript").cache("JAVASCRIPT").eTag($),hashedAsset:($,F)=>X().filePath($).cache("HASHED_ASSET").eTag(F),font:($)=>X().filePath($).cache("FONT").cors({origin:"*"}),image:($)=>X().filePath($).cache("IMAGE"),favicon:()=>X().contentType("icon").cache("FAVICON"),api:()=>X().contentType("json").cache("API_RESPONSE").cors(),expensiveApi:($)=>X().contentType("json").cache("API_RESPONSE").cors().eTag($),realtime:()=>X().contentType("json").cache("NO_CACHE").cors(),html:()=>X().contentType("html").cache("HTML_PAGE").security(),expensiveHtml:($)=>X().contentType("html").cache("HTML_PAGE").security().eTag($),manifest:($)=>X().contentType("json").cache("MANIFEST").eTag($),serviceWorker:()=>X().contentType("javascript").cache("SERVICE_WORKER"),sitemap:($)=>X().contentType("xml").cache("SITEMAP").eTag($),feed:($,F="rss")=>X().contentType("xml").cache("FEED").eTag($).mimeType(F==="rss"?"application/rss+xml":"application/atom+xml"),video:($)=>X().filePath($).cache("MEDIA_STREAM").custom("Accept-Ranges","bytes"),audio:($)=>X().filePath($).cache("MEDIA_STREAM").custom("Accept-Ranges","bytes"),pdf:($)=>X().contentType("pdf").cache("DOCUMENT").custom("Content-Disposition","inline").eTag($),download:($,F,j)=>X().filePath(F).cache("ARCHIVE").custom("Content-Disposition",`attachment; filename="${$}"`).eTag(j),secure:()=>X().security({csp:"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'",hsts:31536000}).custom("X-Powered-By","Bun"),permanentRedirect:($)=>X().redirect($,!0),temporaryRedirect:($)=>X().redirect($,!1)};async function T($,F,j,G={}){let{maxChunkSize:_=2097152,cacheMaxAge:W=3600,acceptedExtensions:J}=G;if(!await $.exists())return new Response("Media file not found",{status:404,headers:X().contentType("text").cors({origin:L(F,j)}).build()});if(J&&J.length>0){let b=$.name?.toLowerCase().split(".").pop();if(!b||!J.includes(b))return new Response("File type not supported",{status:415,headers:X().contentType("text").cors({origin:L(F,j)}).build()})}let V=$.size,Z=F.headers.get("Range");if(!Z)return new Response($,{headers:X().filePath($.name||"media").custom("Accept-Ranges","bytes").custom("Cache-Control",`public, max-age=${W}`).cors({origin:L(F,j)}).build()});let D=Z.match(/bytes=(\d*)-(\d*)/);if(!D)return new Response("Invalid Range header",{status:416,headers:X().contentType("text").cors({origin:L(F,j)}).custom("Content-Range",`bytes */${V}`).build()});let U=D[1]?parseInt(D[1]):0,R=D[2]?parseInt(D[2]):V-1,O=Math.min(R,U+_-1,V-1);if(U>=V||O>=V||U>O)return new Response("Range not satisfiable",{status:416,headers:X().contentType("text").cors({origin:L(F,j)}).custom("Content-Range",`bytes */${V}`).build()});return new Response($.slice(U,O+1),{status:206,headers:X().filePath($.name||"media").custom("Accept-Ranges","bytes").custom("Cache-Control",`public, max-age=${W}`).cors({origin:L(F,j)}).build()})}function y($,F,j){return{type:"stream",file:$,request:F,options:j}}class H{routes;routeMatcher;contextCreator;prefix;globalMiddlewares;server=null;wsConfig;integrations;development=Reflect.get(process.env,"NODE_ENV")!=="production";debug=!1;allowedOrigins=[];onError;activeConnections=new Set;maxConnections=parseInt(process.env.MAX_WS_CONNECTIONS??"1000");connectionTimeout=parseInt(process.env.WS_TIMEOUT??"30000");maxRequestSize=parseInt(process.env.MAX_REQUEST_SIZE??"10485760");requestTimeout=parseInt(process.env.REQUEST_TIMEOUT??"30000");constructor($){if(this.contextCreator=$.context,this.prefix=$.prefix??"",this.routes=$.routes,this.globalMiddlewares=$.globalMiddlewares??[],this.wsConfig=$.websocket??{},this.integrations=$.integrations,this.debug=!!$.debug&&Reflect.get(process.env,"NODE_ENV")==="development",this.allowedOrigins=$.allowedOrigins??[],this.onError=$.onError,this.routeMatcher=new M($.routes,{enableCaching:!this.development,maxCacheSize:parseInt(process.env.ROUTE_CACHE_SIZE??"1000"),debug:this.debug}),!this.development)setInterval(()=>this.cleanupStaleConnections(),60000);if(this.debug)console.log("Router initialized with route matcher"),console.log("Route cache stats:",this.routeMatcher.getCacheStats())}findRoute($){return this.routeMatcher.match($)}async runMiddlewares($,F){for(let j of $){let G=await j(F);if(G&&typeof G==="object")Object.assign(F,G)}}async parseInput($,F,j){let G={},_=new URL($.url),W=Object.fromEntries(_.searchParams),J=$.method;if(J==="POST"){if(parseInt($.headers.get("content-length")||"0")>this.maxRequestSize)throw Error("Request payload too large")}if(J==="GET"&&["query","streamQuery","file","fileStream","html"].includes(j))G=W;else if(J==="POST"&&["mutation","streamMutation"].includes(j))if($.headers.get("content-type")?.includes("application/json"))G=await $.json();else throw new A({code:"BAD_REQUEST",message:"Unsupported content type"});else if(J==="POST"&&j==="formMutation"){if(!$.headers.get("content-type")?.includes("multipart/form-data"))throw new A({code:"BAD_REQUEST",message:"Unsupported content type"});G=await this.parseFormData(await $.formData())}else throw new A({code:"METHOD_NOT_SUPPORTED",message:"Unsupported request method"});return F.parse(G)}async parseFormData($){let F=Object.create(null);return $.forEach((j,G)=>{this.setFormDataValue(F,this.parseFormDataPath(G),j)}),F}setFormDataValue($,F,j){let G=$;for(let _=0;_<F.length;_++){let W=F[_],J=_===F.length-1,Y=this.isFormDataArrayIndex(F[_+1]);if(Array.isArray(G)){if(!this.isFormDataArrayIndex(W))throw A.badRequest("Invalid multipart array path");let Z=Number(W);if(Z>G.length)throw A.badRequest("Multipart array indexes must be contiguous");if(J){if(Z===G.length)G.push(j);else if(G[Z]===void 0)G[Z]=j;else throw A.badRequest("Duplicate multipart field path");return}let D=G[Z];if(D===void 0)G[Z]=Y?[]:Object.create(null);else if(Array.isArray(D)!==Y)throw A.badRequest("Conflicting multipart field path");G=G[Z];continue}if(J){if(Object.hasOwn(G,W))throw A.badRequest("Duplicate multipart field path");G[W]=j;return}let V=G[W];if(V===void 0)G[W]=Y?[]:Object.create(null);else if(Array.isArray(V)!==Y)throw A.badRequest("Conflicting multipart field path");G=G[W]}}parseFormDataPath($){let F=/^([^\[\]]+)((?:\[[^\[\]]+\])*)$/.exec($);if(!F)throw A.badRequest("Invalid multipart field path");let j=[F[1]];for(let G of F[2].matchAll(/\[([^\[\]]+)\]/g))j.push(G[1]);if(j.some((G)=>["__proto__","constructor","prototype"].includes(G)))throw A.badRequest("Invalid multipart field path");return j}isFormDataArrayIndex($){return $!==void 0&&/^(0|[1-9]\d*)$/.test($)}handleError($,F){if(this.onError)try{this.onError($,{req:F,route:F.url})}catch(G){console.error("Error logger failed:",G)}let j=this.serializeError($);return new Response(JSON.stringify({error:j.error}),{status:j.status,headers:X().contentType("json").cors({origin:L(F,this.allowedOrigins)}).build()})}handleFileCacheControl($){let F;if($?.includes("text/html"))F="public, max-age=0, must-revalidate";else if($?.includes("text/css")||$?.includes("text/javascript")||$?.includes("application/javascript"))F="public, max-age=31536000, immutable";else F="public, max-age=86400, stale-while-revalidate=604800";return F}async handleRequest($,F){try{if($.method==="OPTIONS")return new Response(null,{status:204,headers:X().cors({origin:L($,this.allowedOrigins)}).build()});let G=new URL($.url).pathname;if(G=G.startsWith(this.prefix)?G.slice(this.prefix.length):G,G=G.startsWith("/")?G:"/"+G,G==="/ws"&&F){if(this.debug)console.log("\uD83D\uDD0C WebSocket upgrade requested");return this.handleWebSocketUpgrade($,F)}let _=G==="/"?[]:G.split("/").filter(Boolean),W=this.findRoute(_);if(!W)return new Response("Not Found",{status:404,headers:X().contentType("text").cors({origin:L($,this.allowedOrigins)}).build()});let{route:J}=W,Y=J._timeout??this.requestTimeout,V=new Promise((Z,D)=>{setTimeout(()=>D(Error("Request timeout")),Y)});return await Promise.race([this._handleRequest($,F,W),V])}catch(j){return this.handleError(j,$)}}async _handleRequest($,F,j){try{if(!j)return new Response("Not Found",{status:404,headers:X().contentType("text").cors({origin:L($,this.allowedOrigins)}).build()});let{params:G,route:_}=j;if(!_.handler)return new Response("Handler not implemented",{status:501,headers:X().contentType("text").cors({origin:L($,this.allowedOrigins)}).build()});let W=await this.contextCreator($);Object.assign(W,{req:$,params:G,publishToProcedure:async(Z,D,U)=>{await this.publishToProcedure(Z,D,W,U)}});for(let Z of this.globalMiddlewares){let D=await Z(W);if(D!==null&&D!==void 0&&typeof D==="object")Object.assign(W,D)}for(let Z of _.middlewares){let D=await Z(W);if(D!==null&&D!==void 0&&typeof D==="object")Object.assign(W,D)}let J=await this.parseInput($,_._input,_._type),Y=await _.handler({ctx:W,input:J}),V=X().cors({origin:L($,W.allowedOrigins??this.allowedOrigins)}).build();return Object.keys(V).forEach((Z)=>{W.headers.set(Z,V[Z])}),await this.handleProcedureResponse(_._type,Y,W.headers)}catch(G){return this.handleError(G,$)}}async testRequest($){return this.handleRequest($)}async handleProcedureResponse($,F,j){switch($){case"file":if(F instanceof Response)return F;let G=F.type;return j.append("Content-Type",G),j.append("Cache-Control",this.development?"public, max-age=0, must-revalidate":this.handleFileCacheControl(G)),new Response(F,{headers:j});case"fileStream":if(typeof F==="object"&&F!==null&&"type"in F&&F.type==="stream"){let W=F;return await T(F.file,F.request,this.allowedOrigins,W.options)}let _=F.type;return j.append("Content-Type",_),j.append("Cache-Control",this.development?"public, max-age=0, must-revalidate":this.handleFileCacheControl(_)),new Response(F,{headers:j});case"html":if(typeof F==="string")return j.append("Content-Type","text/html"),new Response(F,{headers:j});break;case"query":case"mutation":case"formMutation":case"streamQuery":case"streamMutation":case"subscription":default:return j.append("Content-Type","application/json"),new Response(JSON.stringify({data:F}),{headers:j})}return j.append("Content-Type","application/json"),new Response(JSON.stringify({data:F}),{headers:j})}cleanupStaleConnections(){let $=Date.now();for(let F of this.activeConnections)if($-F.data.lastActivity>this.connectionTimeout)F.close(),this.activeConnections.delete(F)}async handleWebSocketUpgrade($,F){if(this.activeConnections.size>=this.maxConnections)return new Response("Too many connections",{status:503});let j=await this.createWebSocketContext($);if(F.upgrade($,{data:{ctx:j,topics:new Set,lastActivity:Date.now()}}))return new Response(null,{status:101});return new Response("WebSocket upgrade failed",{status:500})}async createWebSocketContext($){let F=await this.contextCreator($);return Object.assign(F,{req:$}),await this.runMiddlewares(this.globalMiddlewares,F),F}createWebSocketOperationContext($,F){return{...$,params:F}}serializeError($){let F=$,j=F?.issues??F?.errors;if(Array.isArray(j)&&F.name==="ZodError")return{status:400,error:{name:"ValidationError",code:"BAD_REQUEST",clientCode:"VALIDATION_ERROR",message:"Input validation failed",data:{validationErrors:j.map((G)=>({path:G.path.join("."),message:G.message,code:G.code,received:G.received}))},httpStatus:400}};if($ instanceof A)return{status:$.httpStatus,error:$.toJSON()};if($ instanceof Error){let G=$.message==="Not Found"?404:$.message==="Unauthorized"?401:$.message==="Forbidden"?403:$.message==="Request payload too large"?413:$.message==="Request timeout"?504:500;return{status:G,error:{code:G===404?"NOT_FOUND":G===401?"UNAUTHORIZED":G===403?"FORBIDDEN":G===413?"PAYLOAD_TOO_LARGE":G===504?"GATEWAY_TIMEOUT":"INTERNAL_SERVER_ERROR",message:G<500?$.message:"Internal Server Error"}}}return{status:500,error:{code:"INTERNAL_SERVER_ERROR",message:"Internal Server Error"}}}sendWebSocketError($,F,j,G){let _=F==="authenticate"?"auth_error":F==="subscribe"?"subscribe_error":F==="unsubscribe"?"unsubscribe_error":F==="publish"?"publish_error":"error",W=this.serializeError(G);$.send(JSON.stringify({type:_,...j?{topic:j}:{},error:{...W.error,httpStatus:W.error.httpStatus??W.status}}))}async handleAuthenticate($,F,j){try{let G=F.startsWith("Bearer ")?F.substring(7):F,_=new Headers(j.req.headers);if(G)_.set("authorization",`Bearer ${G}`);else _.delete("authorization");let W=new Request(j.req,{headers:_});if($.data.ctx=await this.createWebSocketContext(W),$.send(JSON.stringify({type:"auth_success",message:"Authentication successful"})),this.debug)console.log("[BRPC]","WebSocket authenticated successfully")}catch(G){if(this.sendWebSocketError($,"authenticate",void 0,G),this.debug)console.error("WebSocket authentication error:",G)}}async handleSubscribe($,F,j){let G=this.findRoute(F.split("/").filter(Boolean));if(!G||G.route._type!=="subscription")throw Error("Invalid subscription route");let _=this.createWebSocketOperationContext(j,G.params);await this.runMiddlewares(G.route.subscribeMiddlewares??[],_),$.subscribe(F),$.data.topics.add(F),$.send(JSON.stringify({type:"subscribe_success",topic:F}))}async handleUnsubscribe($,F,j){let G=this.findRoute(F.split("/").filter(Boolean));if(!G||G.route._type!=="subscription")throw Error("Invalid subscription route");$.unsubscribe(F),$.data.topics.delete(F)}async handlePublish($,F,j,G){let _=this.findRoute(F.split("/").filter(Boolean));if(!_)throw Error("Subscription not found");let{params:W,route:J}=_;if(J._type!=="subscription"||!J.handler)throw Error("Invalid subscription route");let Y=this.createWebSocketOperationContext(G,W);await this.runMiddlewares(J.middlewares,Y);let V=J._input.fields?J._input.parse(j):j,Z=await J.handler({ctx:Y,input:V}),D={topic:F,data:Z,type:"publish"};if(this.debug)console.log(`Server will publish to ${F}`,D);this.server.publish(F,JSON.stringify(D))}async publishToProcedure($,F,j,G){if($._type!=="subscription"||!$.handler)throw Error("Can only publish to subscription procedures");let _=this.resolveSubscriptionTopic($,G),W=await $.handler({ctx:{...j,params:G},input:F}),J={topic:_,data:W,type:"publish"};if(this.debug)console.log(`Server will publish to ${_}`,J);return this.server?.publish(_,JSON.stringify(J)),W}async publish($,F,j){if($._type!=="subscription"||!$.handler)throw Error("Can only publish to subscription procedures");let G=this.resolveSubscriptionTopic($,j),_=await $.handler({ctx:{params:j},input:F}),W={topic:G,data:_,type:"publish"};if(this.debug)console.log(`Server will publish to ${G}`,W);return this.server?.publish(G,JSON.stringify(W)),_}freePublish($,F){let j={topic:$,data:F,type:"publish"};this.server?.publish($,JSON.stringify(j))}findRoutePath($,F,j=""){for(let[G,_]of Object.entries($)){let W=j?`${j}/${G}`:G;if(_===F)return W;if(_&&typeof _==="object"&&!("_type"in _)){let J=this.findRoutePath(_,F,W);if(J)return J}}return null}resolveSubscriptionTopic($,F){let j=this.findRoutePath(this.routes,$);if(!j)throw Error("Subscription route not found");return j.split("/").map((G)=>{if(!G.startsWith(":"))return G;let _=G.slice(1),W=F?.[_];if(!W)throw Error(`Missing parameter: ${_}`);return encodeURIComponent(W)}).join("/")}getIntegrationRoutes(){if(!this.integrations)return;let $={};if(this.integrations.betterAuth){let F=this.integrations.betterAuth.handler;if(!F)throw Error("Please provide a handler for betterAuth integration");$["/api/auth/*"]=async(j)=>{return await F(j)}}if(this.integrations.rawRoutes)Object.keys(this.integrations.rawRoutes).forEach((F)=>{$[F]=this.integrations.rawRoutes[F]});return Object.keys($).length>0?$:void 0}getRouteStats(){return{cache:this.routeMatcher.getCacheStats(),suggestions:this.routeMatcher.getOptimizationSuggestions()}}clearRouteCache(){if(this.routeMatcher.clearCache(),this.debug)console.log("[ROUTEMATCHER] Route cache cleared")}benchmarkRoutes($=["/users/123","/posts/456/comments"],F=1000){if(this.debug){let j=this.routeMatcher.benchmark($,F);return console.log("[ROUTEMATCHER]","Route matching benchmark:",j),j}}updateRoutes($){if(this.development)this.routes=$,this.routeMatcher=new M($,{enableCaching:!1,debug:this.debug}),console.log("[HMR]","Server Routes updated");else console.warn("Route updates are only allowed in development mode")}get url(){return this.server?.url.toString()??null}listen($,F){if(this.server=Bun.serve({port:$,routes:this.getIntegrationRoutes(),fetch:(j,G)=>this.handleRequest(j,G),websocket:{open:(j)=>{if(this.activeConnections.add(j),this.wsConfig?.onOpen)this.wsConfig.onOpen(j,j.data.ctx);if(this.debug)console.log("[BRPC]","WebSocket connection opened",{connections:this.activeConnections.size})},message:async(j,G)=>{let _,W;try{j.data.lastActivity=Date.now();let J=JSON.parse(G),{token:Y,data:V}=J;_=J.type,W=J.topic;let Z=j.data.ctx;if(this.debug)console.log("[BRPC]","WebSocket message received:",{type:_,topic:W});switch(_){case"authenticate":await this.handleAuthenticate(j,Y,Z);break;case"subscribe":if(!W)throw A.badRequest("Subscription topic is required");await this.handleSubscribe(j,W,Z);break;case"unsubscribe":if(!W)throw A.badRequest("Subscription topic is required");await this.handleUnsubscribe(j,W,Z);break;case"publish":if(!W)throw A.badRequest("Subscription topic is required");await this.handlePublish(j,W,V,Z);break;default:if(this.sendWebSocketError(j,_,W,A.badRequest("Unknown message type")),this.debug)console.error("Unknown WebSocket message type:",_)}}catch(J){if(this.debug)console.error("[BRPC]","WebSocket message error:",J);this.sendWebSocketError(j,_,W,J)}},close:(j,G,_)=>{if(this.activeConnections.delete(j),j.data.topics.forEach((W)=>{j.unsubscribe(W)}),this.debug)console.log("[BRPC]","WebSocket connection closed",{code:G,reason:_,remainingConnections:this.activeConnections.size});if(this.wsConfig?.onClose)this.wsConfig.onClose(j,G,_,j.data.ctx)}}}),F)F()}stop(){this.server?.stop(!0),this.server=null,this.activeConnections.clear()}}function s($){return new H($)}function $$($){return $}import{z as x}from"zod";var P={image:["image/jpeg","image/png","image/gif","image/webp","image/svg+xml"],video:["video/mp4","video/webm","video/ogg"],audio:["audio/mpeg","audio/ogg","audio/wav"],document:["application/pdf","application/msword","application/vnd.openxmlformats-officedocument.wordprocessingml.document","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]},B=($,F="MB")=>{switch(F){case"MB":return $*1024*1024;case"KB":return $*1024;case"B":return $}},G$=({acceptedTypes:$,maxSize:F=1/0,minSize:j,messages:G={}}={})=>{let _=F?B(F):1/0,W=j?B(j):0,J=$?Object.entries($).flatMap(([V,Z])=>{let D=V;if(Z==="*")return P[D];if(Array.isArray(Z))return Z;return[]}):void 0,Y=x.instanceof(File,{message:G.required??"File is required"});if(Y=Y.refine((V)=>V.size<=_,G.maxSize??`File size must be less than ${F}MB`).refine((V)=>V.size>=W,G.minSize??`File size must be at least ${j}MB`),J?.length)Y=Y.refine((V)=>J.includes(V.type),G.type??`File must be of type: ${J.join(", ")}`);return Y};var _$=($)=>{let F=$.paths.map((G)=>new RegExp(G)),j="Not Found";return async(G)=>{let W=new URL(G.req.url).pathname;if(F.some((J)=>J.test(W)))throw Error("Not Found")}};class E{requests=new Map;lastCleanupTime=Date.now();alertThreshold;config;constructor($){if(this.config={windowMs:$.windowMs,maxRequests:$.maxRequests,maxEntries:$.maxEntries??1e4,cleanupIntervalMs:$.cleanupIntervalMs??60000,message:$.message??"Too Many Requests",statusCode:$.statusCode??429,headerPrefix:$.headerPrefix??"X-RateLimit"},this.alertThreshold=this.config.maxEntries*0.8,Reflect.get(process.env,"NODE_ENV")!=="development")setInterval(()=>this.cleanup(),this.config.cleanupIntervalMs)}cleanup(){let $=Date.now(),F=0;for(let[j,G]of this.requests.entries()){let _=$-G.startTime>this.config.windowMs,W=$-G.lastAccessed>this.config.windowMs*2;if(_||W)this.requests.delete(j),F++}return this.lastCleanupTime=$,F}handleMaxEntries($){let F=this.requests.size;if(Date.now()-this.lastCleanupTime>this.config.cleanupIntervalMs)this.cleanup();if(F>=this.alertThreshold)console.warn(`Rate limiter at ${Math.round(F/this.config.maxEntries*100)}% capacity`);if(this.requests.has($))return!0;return F<this.config.maxEntries}check($){if(!this.handleMaxEntries($))return{isLimited:!0,remaining:0,resetTime:Date.now()+this.config.windowMs};let F=Date.now(),j=this.requests.get($);if(!j||F-j.startTime>=this.config.windowMs)return this.requests.set($,{count:1,startTime:F,lastAccessed:F}),{isLimited:!1,remaining:this.config.maxRequests-1,resetTime:F+this.config.windowMs};j.lastAccessed=F;let G=j.count>=this.config.maxRequests;if(!G)j.count++;return{isLimited:G,remaining:Math.max(0,this.config.maxRequests-j.count),resetTime:j.startTime+this.config.windowMs}}getConfig(){return this.config}}var Y$=($)=>{let F=new E($);return async(j)=>{if(j.req.headers.get("range"))return;let G=j.req.headers.get("x-forwarded-for")?.split(",")[0]||j.req.headers.get("x-real-ip")||"unknown",{isLimited:_,remaining:W,resetTime:J}=F.check(G),{headerPrefix:Y}=F.getConfig();if(j.headers.set(`${Y}-Remaining`,W.toString()),j.headers.set(`${Y}-Reset`,J.toString()),_)throw j.headers.set(`${Y}-Exceeded`,"true"),Error(F.getConfig().message)}};function V$($,F){return F??$}async function S$($,F){let j=performance.now();try{return await F()}catch(G){let _=performance.now();throw console.log(`[PERF] ${$} failed after ${(_-j).toFixed(4)} ms`),G}finally{let G=performance.now();console.log(`[PERF] ${$}: ${(G-j).toFixed(4)} ms`)}}export{y as streamMedia,p as quickHeaders,S$ as measure,z as hashContent,K as getMimeType,L as getCorsOrigin,s as createRouter,Y$ as createRateLimiter,k as createProcedure,_$ as createPathBlocker,V$ as createMiddleware,G$ as createFileSchema,$$ as createContext,l as commonHeaders,X as buildHeaders,H as Router,A as BRPCError};
|
|
8
8
|
|
|
9
|
-
//# debugId=
|
|
9
|
+
//# debugId=30A0CB67AA924BB664756E2164756E21
|
package/dist/schemas.d.ts
CHANGED
|
@@ -39,6 +39,10 @@ type FileSchemaOptions = {
|
|
|
39
39
|
};
|
|
40
40
|
};
|
|
41
41
|
/**
|
|
42
|
+
* @deprecated Only while brpc upgrades to Zod 4: this is built on Zod 3, so
|
|
43
|
+
* apps on Zod 4 get an `unknown` output type. Use the app's own
|
|
44
|
+
* `createFileSchema` (template: `apps/brpc/zod/files.ts`) until then.
|
|
45
|
+
*
|
|
42
46
|
* Creates a Zod schema for file validation with support for MIME types and size constraints
|
|
43
47
|
* you can extend it as any other zod schema after calling it
|
|
44
48
|
* @param {Object} options - The configuration options for the file schema
|
package/dist/storage/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
var T=(
|
|
2
|
+
var T=(x)=>x.startsWith("image/"),w=(x)=>x.startsWith("video/"),z=(x)=>x.startsWith("audio/"),k=(x)=>x.startsWith("text/")||x.includes("pdf")||x.includes("word")||x.includes("sheet")||x.includes("presentation")||x.includes("opendocument")||x==="application/rtf",I=(x)=>x.includes("zip")||x.includes("rar")||x.includes("tar")||x.includes("7z")||x.includes("gzip"),R=(x)=>x==="application/javascript"||x==="application/json"||x==="text/html"||x==="text/css"||x==="application/xml"||x.includes("javascript"),b=(x)=>x.startsWith("font/")||x.includes("font"),P=(x)=>{if(!x)return"other";let $=x.toLowerCase();if(T($))return"image";if(w($))return"video";if(z($))return"audio";if(k($))return"document";if(I($))return"archive";if(R($))return"code";if(b($))return"font";return"other"},U={isImage:T,isVideo:w,isAudio:z,isDocument:k,isArchive:I,isCode:R,isFont:b};function xx(){let $=Buffer.from("Hello, this is a test file for S3 upload!","utf-8");return new File([$],"test.txt",{type:"text/plain"})}function q(x,$){if(!x||x<1||!$||$<1)return"1:1";let W=(J,Q)=>Q===0?J:W(Q,J%Q),H=W(x,$);return`${x/H}:${$/H}`}function Wx(x,$){if(!x||!$||x<1||$<1)return"1:1";let W=x/$,H=[{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"}],J=0.03;for(let{ratio:X,label:M}of H)if(Math.abs(W-X)/X<J)return M;let Q=(X,M)=>M===0?X:Q(M,X%M),Y=Q(x,$);return`${x/Y}:${$/Y}`}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(x){super(x.message);if(this.name="BRPCError",this.code=x.code,this.clientCode=x.clientCode,this.httpStatus=L.STATUS_MAP[x.code],this.data=x.data,this.cause=x.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(x,$,W){return new L({code:"BAD_REQUEST",message:x,clientCode:$,data:W})}static unauthorized(x="Unauthorized",$,W){return new L({code:"UNAUTHORIZED",message:x,clientCode:$,data:W})}static forbidden(x="Forbidden",$,W){return new L({code:"FORBIDDEN",message:x,clientCode:$,data:W})}static notFound(x="Not Found",$,W){return new L({code:"NOT_FOUND",message:x,clientCode:$,data:W})}static preconditionFailed(x="Precondition failed",$,W){//! 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
|
|
5
|
+
return new L({code:"NOT_FOUND",message:x,clientCode:$,data:W})}static conflict(x,$,W){return new L({code:"CONFLICT",message:x,clientCode:$,data:W})}static unprocessableContent(x,$,W){return new L({code:"UNPROCESSABLE_CONTENT",message:x,clientCode:$,data:W})}static tooManyRequests(x="Too many requests",$,W){return new L({code:"TOO_MANY_REQUESTS",message:x,clientCode:$,data:W})}static internalServerError(x="Internal Server Error",$,W){return new L({code:"INTERNAL_SERVER_ERROR",message:x,clientCode:$,data:W})}static timeout(x="Request timeout",$,W){return new L({code:"TIMEOUT",message:x,clientCode:$,data:W})}}import m from"sharp";var E={xs:200,sm:400,md:800,lg:1200,xl:1920},O=1920;async function S(x,$,W,H,J){try{let{data:Q,info:Y}=await m(x,{limitInputPixels:1e8}).rotate().resize({width:$,height:$,fit:"inside",withoutEnlargement:!0}).webp({quality:W}).toBuffer({resolveWithObject:!0});return{buffer:Q,object:{name:H.replace(/\.[^/.]+$/,".webp"),resolvedType:"image",metadata:{type:"image/webp",size:Q.length,extension:".webp",width:Y.width,height:Y.height,aspectRatio:Y.width&&Y.height?Y.width/Y.height:1,aspectRatioStr:Y.width&&Y.height?q(Y.width,Y.height):"1:1",acl:J}}}}catch(Q){throw L.conflict(`Failed to optimize image: ${Q instanceof Error?Q.message:"Unknown error"}`)}}async function B(x){if(!x)throw L.conflict("A file must be provided");let $,W="unknown.webp";if(typeof x==="string"){if(console.log("I'm going to fail here for file",x),!x.startsWith("data:"))throw console.log("I never got here"),L.conflict("Invalid base64 format");let[,H]=x.split(",");if(!H)throw L.conflict("Invalid base64 data");$=Buffer.from(H,"base64")}else{if(!U.isImage(x.type))throw L.conflict("File must be an image");let H=await x.arrayBuffer();$=Buffer.from(H),W=x.name??W}return{buffer:$,name:W}}async function Ux(x,{size:$,quality:W,acl:H}={}){let J=await B(x);return S(J.buffer,$??O,W??80,J.name,H??"public-read")}async function qx(x,{name:$="unknown.webp",size:W,quality:H,acl:J}={}){if(!x?.length)throw L.conflict("A buffer must be provided");return S(x,W??O,H??80,$,J??"public-read")}async function v(x,{quality:$=80,acl:W="public-read"}={}){let H=await B(x),J=await Promise.all(Object.entries(E).map(async([Q,Y])=>{let X=await S(H.buffer,Y,$,H.name,W);return[Q,X]}));return{variants:Object.fromEntries(J)}}import n from"sharp";import{randomUUID as y}from"crypto";import{extname as u}from"path";async function C(x,$,W){if(!$)return{data:null,error:Error("File not found")};let H=x.replace(/^\/+|\/+$/g,""),J=u($.name),Q=`${y()}${J}`,Y=`${H}/${Q}`,X=`./buckets/${H}/${Q}`;try{let M=await Bun.write(X,$);return{data:{key:Y,file:$,bytesWritten:M},error:null}}catch(M){return{data:null,error:Error(`Failed to upload ${$.name} to local filesystem`)}}}async function f(x,$,W){if(!$||$.length===0)return[];let H=$.map((Q)=>{let Y=W?.(Q)??Q.name;return C(x,Q,Y)});return await Promise.all(H)}async function d(x){let $=Bun.file(`./buckets/${x}`);if(!await $.exists())return null;return $}async function p(x){let $=Bun.file(`./buckets/${x}`);if(!await $.exists())return!1;return await $.delete(),!0}var V={uploadOne:C,uploadMany:f,getOne:d,deleteOne:p};async function wx(x){let $=`./cache/images/${x.key}_${x.width}_${x.quality}.webp`,W=await V.getOne($);if(W){if(!U.isImage(W.type))return null;return W}let H=x.key,J=await V.getOne(H);if(!J)return null;if(!U.isImage(J.type))return null;let Q=await J.arrayBuffer(),Y=await n(Q).resize(x.width,null,{withoutEnlargement:!0}).webp({quality:x.quality??75}).toBuffer();await Bun.write($,Y);let X=Bun.file($);if(!await X.exists())return null;return X}import s from"sharp";async function Rx(x){if(!x?.length)return null;try{let $=await s(x).metadata(),W=$.width??0,H=$.height??0,J=$.height>0?W/H:1;return{type:`image/${$.format}`,size:$.size??0,extension:`.${$.format}`,width:W,height:H,aspectRatio:J,aspectRatioStr:q(W,H)}}catch($){return console.warn("Failed to read image metadata:",$),null}}var g=process.env.AWS_FOLDER,h=g?`${g}/`:"";var{s3:t}=globalThis.Bun;import{extname as i}from"path";function c(x={}){return x}var{s3:o}=globalThis.Bun;function l(x){return[...new Set(x.flatMap(({variants:$})=>Object.values($).flatMap((W)=>W&&typeof W==="object"&&("key"in W)&&typeof W.key==="string"&&W.key.length>0?[W.key]:[])))]}async function r(x,$){if(!x||typeof x!=="string")throw L.badRequest("Key is required");let W=x.trim();if(!W||W.length===0)throw L.badRequest("Key must be a non-empty string");try{return await o.delete(W),{key:W,deleted:!0,error:null}}catch(H){return{key:x,deleted:!1,error:H}}}async function F(x,$){return(await Promise.allSettled(x.map((H)=>r(H,$)))).map((H,J)=>{if(H.status==="fulfilled")return H.value;return{key:x[J],deleted:!1,error:H.reason}})}async function yx(x,$){return F(l(x),$)}async function N(x,$,W){let H="buffer"in $?$:{buffer:$,object:{name:$.name,resolvedType:P($.type),metadata:{type:$.type,extension:i($.name),size:$.size,acl:W?.acl??"public-read"}}};if("buffer"in $){if(!$.object?.name||!$.object?.metadata)throw L.badRequest("Complete metadata required when uploading buffer")}let J=x.length>1?`${x.replace(/^\/+|\/+$/g,"")}/`:"",Q=H.object.metadata.extension.startsWith(".")?H.object.metadata.extension:`.${H.object.metadata.extension}`,Y=H.object.name?H.object.name:`${crypto.randomUUID()}${Q}`,X=`${h}${J}${Y}`,M=t.file(X),G=H?.object?.metadata?.acl??"public-read",Z=G!=="public-read"&&G!=="public-read-write",D=await M.write(H.buffer,{acl:G,type:H.object.metadata.type});return{uploadedBy:W?.uploadedBy,key:X,name:H.object.name,resolvedType:H.object.resolvedType,provider:"s3",isPrivate:Z,metadata:{...H.object.metadata,size:D,acl:G},isActive:!0}}async function tx(x,$,W={throwIf:"any"}){let H=await Promise.allSettled($.map((Y)=>N(x,Y,{acl:W?.acl,uploadedBy:W?.uploadedBy}))),J=[],Q=[];if(H.forEach((Y)=>{if(Y.status==="fulfilled"){if(J.push(Y.value),W?.externalKeys)W.externalKeys.push(Y.value.key)}else Q.push(Y.reason)}),W?.throwIf==="any"&&Q.length>0||W?.throwIf==="all"&&Q.length===$.length)throw L.internalServerError(`Upload failed: ${Q.length} of ${$.length}`);return{data:J,errors:Q.length?Q:void 0}}async function a(x,$,W){let H=W?.baseId??crypto.randomUUID(),J=Object.values($.variants).find(Boolean),Q=await Promise.allSettled(Object.entries($.variants).map(async([Z,D])=>{if(!D)return[Z,null];let K=D.object.metadata.extension,j=`${H}_${Z}${K}`,A=await N(x,{...D,object:{...D.object,name:j}},W);if(!A.metadata)throw L.internalServerError("Upload returned no metadata, cannot build a storage variant");let _=A.metadata;return[Z,{key:A.key,name:j,metadata:{type:_.type,extension:_.extension,size:_.size,hash:_.hash,width:_.width,height:_.height,aspectRatio:_.aspectRatio,aspectRatioStr:_.aspectRatioStr,resolution:_.resolution,bitrate:_.bitrate,codec:_.codec,fps:_.fps}}]})),Y=Q.flatMap((Z)=>Z.status==="fulfilled"?[Z.value]:[]),X=Q.flatMap((Z)=>Z.status==="rejected"?[Z.reason]:[]);if(X.length>0){let Z=Y.flatMap(([,j])=>j?.key?[j.key]:[]),K=(await F(Z)).filter(({deleted:j})=>!j);if(K.length>0)console.error("Failed to clean up partially uploaded image variants",{keys:Z,failures:K});throw X[0]}let M=(W?.acl??"public-read")!=="public-read"&&(W?.acl??"public-read")!=="public-read-write",G={};for(let[Z,D]of Y)if(D)G[Z]=D;return{shared:{name:J.object.name,resolvedType:J.object.resolvedType,provider:"s3",acl:W?.acl??"public-read",isPrivate:M,uploadedBy:W?.uploadedBy},variants:G}}async function ix(x,$,W){let H=await N(x,$,W);if(!H.metadata)throw L.internalServerError("Upload returned no metadata, cannot build a storage object");let J=H.name??H.key.split("/").pop()??H.key;return{shared:{name:J,resolvedType:H.resolvedType,provider:H.provider,acl:H.metadata.acl,isPrivate:H.isPrivate,uploadedBy:W?.uploadedBy},variants:c({default:{key:H.key,name:J,metadata:{type:H.metadata.type,extension:H.metadata.extension,size:H.metadata.size,hash:H.metadata.hash,width:H.metadata.width,height:H.metadata.height,aspectRatio:H.metadata.aspectRatio,aspectRatioStr:H.metadata.aspectRatioStr,resolution:H.metadata.resolution,bitrate:H.metadata.bitrate,codec:H.metadata.codec,fps:H.metadata.fps}}})}}async function ax(x,$,W){if(Array.isArray($))throw Error("File can't be an array, pass a single file");let H=await v($,{quality:W?.quality,acl:W?.acl});return a(x,H,{acl:W?.acl,uploadedBy:W?.uploadedBy})}var{s3:e}=globalThis.Bun;async function $0(x,$={debug:Reflect.get(process.env,"NODE_ENV")!=="production"}){try{return{exists:await e.exists(x),error:null}}catch(W){if($?.debug)console.error("There was an error checking for file existance",W);return{exists:null,error:W}}}export{ix as uploadSingleObject,N as uploadOne,tx as uploadMany,a as uploadGeneratedVariants,qx as optimizeImageBuffer,Ux as optimizeImage,P as mimeTypeToResolvedType,wx as getOptimizedImage,Rx as getImageMetadata,Wx as getAspectRatioStringRound,q as getAspectRatioString,v as generateVariants,xx as generateTestFile,ax as generateAndUploadImage,U as fileTypePredicates,l as extractObjectVariantKeys,r as deleteOne,yx as deleteManyObjects,F as deleteManyKeys,c as createUploadVariants,$0 as checkFileExistance};
|
|
6
6
|
|
|
7
|
-
//# debugId=
|
|
7
|
+
//# debugId=4D1A53711E0C7A1164756E2164756E21
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mateosuarezdev/brpc",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.114",
|
|
4
4
|
"description": "A Type-Safe, Flexible Web application framework for Bun",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"dist/**/*.cjs",
|
|
44
44
|
"dist/**/*.d.ts"
|
|
45
45
|
],
|
|
46
|
-
"sideEffects": false,
|
|
47
46
|
"scripts": {
|
|
48
47
|
"typecheck": "tsc --noEmit",
|
|
49
48
|
"test": "bun test",
|
|
@@ -131,6 +130,6 @@
|
|
|
131
130
|
"zod": "^3.23.8"
|
|
132
131
|
},
|
|
133
132
|
"dependencies": {
|
|
134
|
-
"@mateosuarezdev/headers-builder": "^1.0.
|
|
133
|
+
"@mateosuarezdev/headers-builder": "^1.0.6"
|
|
135
134
|
}
|
|
136
135
|
}
|