@mateosuarezdev/brpc 1.0.109 → 1.0.111
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 +19 -0
- package/dist/auth/index.js +3 -3
- package/dist/auth/types.d.ts +3 -0
- package/dist/client/helpers.d.ts +12 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +2 -2
- package/dist/client/types.d.ts +21 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,6 +217,25 @@ await client.routes.documents.upload.formMutation({
|
|
|
217
217
|
});
|
|
218
218
|
```
|
|
219
219
|
|
|
220
|
+
In React Native, pass the descriptor produced from an Expo picker asset. The
|
|
221
|
+
client serializes it as the same multipart file leaf; the route still receives
|
|
222
|
+
a normal server-side `File`.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import type { NativeFileDescriptor } from "@mateosuarezdev/brpc/client";
|
|
226
|
+
|
|
227
|
+
const voucher: NativeFileDescriptor = {
|
|
228
|
+
uri: asset.uri,
|
|
229
|
+
name: asset.fileName ?? "voucher.pdf",
|
|
230
|
+
type: asset.mimeType ?? "application/pdf",
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
await client.routes.documents.upload.formMutation({
|
|
234
|
+
title: "Tax documents",
|
|
235
|
+
attachments: [{ label: "Invoice", file: voucher }],
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
220
239
|
Any JavaScript caller can send the same request without the BRPC client:
|
|
221
240
|
|
|
222
241
|
```ts
|
package/dist/auth/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import*as
|
|
2
|
+
import*as w from"jsonwebtoken";import{RecordId as A,DateTime as u,Table as q,eq as B,or as c,surql as W}from"surrealdb";import*as X from"bcryptjs";import a from"node-cron";import n from"crypto";class Q 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=Q.STATUS_MAP[$.code],this.data=$.data,this.cause=$.cause,Error.captureStackTrace)Error.captureStackTrace(this,Q)}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 Q({code:"BAD_REQUEST",message:$,clientCode:K,data:j})}static unauthorized($="Unauthorized",K,j){return new Q({code:"UNAUTHORIZED",message:$,clientCode:K,data:j})}static forbidden($="Forbidden",K,j){return new Q({code:"FORBIDDEN",message:$,clientCode:K,data:j})}static notFound($="Not Found",K,j){return new Q({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"},P={httpOnly:!0,sameSite:"Lax",path:"/",maxAge:1209600};function w($){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 r($,K,j){let F={...P,secure:j};[`${O.ACCESS_TOKEN}=${K.accessToken}; ${w(F)}`,`${O.REFRESH_TOKEN}=${K.refreshToken}; ${w(F)}`,`${O.SESSION_ID}=${K.sessionId}; ${w({...F,maxAge:86400})}`].forEach((G)=>$.append("Set-Cookie",G))}function o($,K){let j={...P,secure:K,maxAge:0};[O.ACCESS_TOKEN,O.REFRESH_TOKEN,O.SESSION_ID].forEach((F)=>$.append("Set-Cookie",`${F}=; ${w(j)}`))}function T($){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 n=10,i=3,g=15,t=3,d=["cf-connecting-ip","x-real-ip","x-client-ip","x-forwarded","forwarded-for","x-forwarded-host"],U=new q("users"),b=new q("user_identities"),D=new q("sessions"),C=new q("otp_codes");function Y($){return`${$.table}:${$.id}`}function N($=new Date){return new p($)}class E{db;defaultSignupRoles;constructor($,K){this.db=$;this.defaultSignupRoles=K}async findByEmail($){let[K]=await this.db.select(U).where(y("email",$)).limit(1);return K}async findByPhone($){let[K]=await this.db.select(U).where(y("phone",$)).limit(1);return K}async findByContact($){if(!$.email&&!$.phone)return;let K=[];if($.email)K.push(y("email",$.email));if($.phone)K.push(y("phone",$.phone));let[j]=await this.db.select(U).where(u(...K)).limit(1);return j}async findById($){let[K,j]=$.split(":");return await this.db.select(new L(K,j))??void 0}async create({email:$,phone:K,options:j,ctx:F=this.db}){let[G]=await F.create(U).content({email:$??null,phone:K??null,app_metadata:{...j?.appMetadata,roles:this.defaultSignupRoles},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 L(j,F)).merge({last_sign_in_at:N()})}}class m{db;constructor($){this.db=$}async findByProvider($,K){let[j]=await this.db.query(W`SELECT * FROM type::table(${b}) WHERE provider = ${$} AND provider_id = ${K} LIMIT 1`);return j?.[0]}async findByUserId($){let[K,j]=$.split(":"),F=new L(K,j),[G]=await this.db.query(W`SELECT * FROM type::table(${b}) WHERE user_id = ${F}`);return G??[]}async findPasswordIdentity($){let[K,j]=$.split(":"),F=new L(K,j),[G]=await this.db.query(W`SELECT * FROM type::table(${b}) 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(b).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 S{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(":"),A=new L(J,V),[Z]=await j.create(D).content({user_id:A,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 L(K,j));if(!F||F.invalidated_at)return;return k(F)}async getUserSessions($){let[K,j]=$.split(":"),F=new L(K,j),[G]=await this.db.query(W`SELECT * FROM type::table(${D}) WHERE user_id = ${F} AND invalidated_at = null`);return(G??[]).map((J)=>({id:Y(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 L(K,j)).merge({invalidated_at:N()})}async invalidateAllForUser($){let[K,j]=$.split(":"),F=new L(K,j);await this.db.query(W`UPDATE type::table(${D}) 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 L(j,F),A=new L(G,J);await this.db.query(W`UPDATE type::table(${D}) SET invalidated_at = time::now() WHERE user_id = ${V} AND id != ${A} AND invalidated_at = null`)}async cleanup(){let[$]=await this.db.query(W`DELETE type::table(${D}) 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 d){let F=$.headers.get(j);if(F)return F.trim()}return this.debug?"127.0.0.1":null}}class h{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(W`email = ${$.email}`);if($.phone)j.push(W`phone = ${$.phone}`);let F=j.length===1?j[0]:W`(${j[0]} OR ${j[1]})`,[G]=await this.db.query(W`SELECT * FROM type::table(${C}) WHERE (${F}) AND created_at >= ${N(K)}`);if((G?.length??0)>=t)throw Q.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=a.randomInt(1e5,999999).toString(),G=await X.hash(F,10),J=new Date;J.setMinutes(J.getMinutes()+n);let V=$.id?(()=>{let[Z,_]=$.id.split(":");return new L(Z,_)})():null,[A]=await this.db.create(C).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(!A)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(A.id),Q.internalServerError("Failed to send OTP","OTP_SEND_FAILED")}return{otpId:Y(A.id),expiresAt:J.toISOString(),message:`OTP sent to ${$.email?"email":"phone"}`}}async verify($,K){let[j,F]=$.split(":"),G=await this.db.select(new L(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>=i)throw Q.badRequest("Too many failed attempts","OTP_MAX_ATTEMPTS");if(!await X.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(W`DELETE type::table(${C}) 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 R{jwtSecret;jwtRefreshSecret;accessTokenExpiry;constructor($,K,j){this.jwtSecret=$;this.jwtRefreshSecret=K;this.accessTokenExpiry=j}generateAccessToken($){return v.sign($,this.jwtSecret,{expiresIn:this.accessTokenExpiry})}generateRefreshToken($,K){return v.sign({sub:$,sessionId:K},this.jwtRefreshSecret)}verifyAccessToken($){return v.verify($,this.jwtSecret)}verifyRefreshToken($){return v.verify($,this.jwtRefreshSecret)}}function f($){return{id:Y($.id),email:$.email,phone:$.phone,appMetadata:$.app_metadata,userMetadata:$.user_metadata}}function k($){return{id:Y($.id),userId:Y($.user_id),ipAddress:$.ip_address,userAgent:$.user_agent,invalidatedAt:$.invalidated_at}}class I{userManager;identityManager;sessionManager;otpManager;tokenManager;cache;config;constructor($){this.config=$,this.userManager=new E($.db,$.defaultSignupRoles),this.identityManager=new m($.db),this.sessionManager=new S($.db,$.debug??!1),this.otpManager=new h($.db,$.otpProvider),this.tokenManager=new R($.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:Y(F.id),sessionId:j.id,appMetadata:F.app_metadata,userMetadata:F.user_metadata}),J=this.tokenManager.generateRefreshToken(Y(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=T($)[O.ACCESS_TOKEN];if(!j)return null;return this.verifyToken(`Bearer ${j}`)}async refreshTokenFromCookie($){if(!$)return null;let j=T($)[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 X.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 X.compare(K,F.identity_data.password_hash))throw Q.badRequest("Current password is incorrect","INVALID_PASSWORD");let J=await X.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=Y(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 A=await X.hash(j,10);await this.identityManager.updateData(V.id,{password_hash:A});let Z=Y(F.user_id);return await this.sessionManager.invalidateAllForUser(Z),{success:!0}}async adminCreateUser({email:$,phone:K,password:j,options:F}){let G=j?await X.hash(j,10):null,J=await this.config.db.beginTransaction();try{let[V]=await J.create(U).content({email:$??null,phone:K??null,app_metadata:{...F?.appMetadata,roles:F?.appMetadata?.roles??this.config.defaultSignupRoles},user_metadata:F?.userMetadata,last_sign_in_at:null});if(!V)throw Error("Failed to create user");if($||K){let A=$?"email":"phone",Z=$??K;await J.create(b).content({user_id:V.id,provider:A,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 X.hash(j,10):null,V=this.sessionManager.getClientIP(G),A=await this.config.db.beginTransaction();try{let[Z]=await A.create(U).content({email:$??null,phone:K??null,app_metadata:{...F?.appMetadata,roles:this.config.defaultSignupRoles},user_metadata:F?.userMetadata,last_sign_in_at:N()});if(!Z)throw Error("Failed to create user");if($||K){let B=$?"email":"phone",l=$??K;await A.create(b).content({user_id:Z.id,provider:B,provider_id:l,identity_data:J?{password_hash:J}:{}})}let[_]=await A.create(D).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=Y(Z.id),z=Y(_.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 A.commit(),{user:f(Z),session:k(_),accessToken:x,refreshToken:M}}catch(Z){throw await A.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=Y($.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 X.hash(F,10),A=await this.config.db.beginTransaction();try{let[Z]=await A.create(U).content({email:j.email??null,phone:j.phone??null,app_metadata:{...J?.appMetadata,roles:this.config.defaultSignupRoles},user_metadata:J?.userMetadata,last_sign_in_at:N()});if(!Z)throw Error("Failed to create user");let[_]=await A.create(b).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 A.create(D).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=Y(Z.id),M=Y(z.id),B=this.tokenManager.generateAccessToken({sub:x,sessionId:M,appMetadata:Z.app_metadata,userMetadata:Z.user_metadata}),l=this.tokenManager.generateRefreshToken(x,M);return await A.commit(),{user:f(Z),session:k(z),accessToken:B,refreshToken:l}}catch(Z){throw await A.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 X.compare(K,F))throw Q.conflict("Invalid credentials","INVALID_CREDENTIALS");let J=Y($.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(U).content({email:$.email??null,phone:$.phone??null,app_metadata:{...G?.appMetadata,roles:this.config.defaultSignupRoles},user_metadata:G?.userMetadata,last_sign_in_at:N()});if(!V)throw Error("Failed to create user");let[A]=await J.create(b).content({user_id:V.id,provider:j,provider_id:F,identity_data:{}});if(!A)throw Error("Failed to create identity");let Z=this.sessionManager.getClientIP(K),[_]=await J.create(D).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=Y(V.id),z=Y(_.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=Y($.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(D).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=Y($.id),V=Y(G.id),A=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:A,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(()=>{}),c.schedule("0 2 * * *",async()=>{await Promise.all([this.sessionManager.cleanup(),this.otpManager.cleanup()]).catch(()=>{})})}}function Q$($){return new I($)}export{r as setAuthCookies,T as parseCookies,Q$ as createAuth,o as clearAuthCookies,O as COOKIE_NAMES,I as AuthService};
|
|
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,g=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.includes("android"))return"android";if(/iphone|ipad|ipod/.test(K))return"apple";return"desktop"}class m{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 A(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 A(j,F)).merge({last_sign_in_at:N()})}}class S{db;constructor($){this.db=$}async findByProvider($,K){let[j]=await this.db.query(W`SELECT * FROM type::table(${D}) WHERE provider = ${$} AND provider_id = ${K} LIMIT 1`);return j?.[0]}async findByUserId($){let[K,j]=$.split(":"),F=new A(K,j),[G]=await this.db.query(W`SELECT * FROM type::table(${D}) WHERE user_id = ${F}`);return G??[]}async findPasswordIdentity($){let[K,j]=$.split(":"),F=new A(K,j),[G]=await this.db.query(W`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 A(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 A(K,j));if(!F||F.invalidated_at)return;return k(F)}async getUserSessions($){let[K,j]=$.split(":"),F=new A(K,j),[G]=await this.db.query(W`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 A(K,j)).merge({invalidated_at:N()})}async invalidateAllForUser($){let[K,j]=$.split(":"),F=new A(K,j);await this.db.query(W`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 A(j,F),Y=new A(G,J);await this.db.query(W`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(W`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()-g);let j=[];if($.email)j.push(W`email = ${$.email}`);if($.phone)j.push(W`phone = ${$.phone}`);let F=j.length===1?j[0]:W`(${j[0]} OR ${j[1]})`,[G]=await this.db.query(W`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 ${g} 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 X.hash(F,10),J=new Date;J.setMinutes(J.getMinutes()+i);let V=$.id?(()=>{let[Z,_]=$.id.split(":");return new A(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 A(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 X.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(W`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 m($.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 X.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 X.compare(K,F.identity_data.password_hash))throw Q.badRequest("Current password is incorrect","INVALID_PASSWORD");let J=await X.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 X.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 X.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 X.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 X.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 X.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};
|
|
6
6
|
|
|
7
|
-
//# debugId=
|
|
7
|
+
//# debugId=A76EC03884E3323564756E2164756E21
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { RecordId, Surreal } from "surrealdb";
|
|
2
2
|
import type { CacheService } from "../cache";
|
|
3
|
+
export type SignupDevice = "desktop" | "apple" | "android";
|
|
3
4
|
export type AppMetadata = {
|
|
4
5
|
roles: string[];
|
|
6
|
+
/** Server-derived device class from the request that created the user. */
|
|
7
|
+
signup_device: SignupDevice;
|
|
5
8
|
[key: string]: unknown;
|
|
6
9
|
};
|
|
7
10
|
export type UserMetadata = {
|
package/dist/client/helpers.d.ts
CHANGED
|
@@ -4,4 +4,15 @@
|
|
|
4
4
|
* @param clientCode
|
|
5
5
|
* @returns boolean
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Native multipart representation accepted by React Native's FormData.
|
|
9
|
+
*/
|
|
10
|
+
export type NativeFileDescriptor = {
|
|
11
|
+
uri: string;
|
|
12
|
+
name: string;
|
|
13
|
+
type: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Checks browser files and React Native multipart file descriptors.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isFileLike(value: unknown): value is File | Blob | NativeFileDescriptor;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export * from "./storage";
|
|
|
2
2
|
export * from "./service";
|
|
3
3
|
export * from "./brpc-client-error";
|
|
4
4
|
export * from "./subscription-service";
|
|
5
|
-
export type { Headers, InferRouterOutput } from "./types";
|
|
5
|
+
export type { Headers, InferRouterOutput, FormMutationInput, } from "./types";
|
|
6
|
+
export type { NativeFileDescriptor } from "./helpers";
|
package/dist/client/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
function u(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class M extends Error{status;code;clientCode;data;constructor(q,J,Q,N,_){super(q);this.name="BrpcClientError",this.status=J,this.code=Q,this.clientCode=N,this.data=_}isClientError(q){return this.clientCode===q}isUnauthorized(){return this.status===401}isForbidden(){return this.status===403}isNotFound(){return this.status===404}isValidationError(){return this.status===400||this.code==="BAD_REQUEST"}}class m extends M{type;constructor(q,J){super(J.message??"WebSocket request failed",J.httpStatus??0,J.code,J.clientCode,J.data);this.name="BrpcClientSubscriptionError",this.type=q}}function e(){return function(J,Q){if(Q)return J instanceof M&&J.clientCode===Q;return J instanceof M}}var c=(q)=>{return q instanceof File||q instanceof Blob||typeof q==="object"&&q!==null&&"uri"in q&&"name"in q&&"type"in q};function P(q){if(!q)return null;return q.replace(/\//g,"")}function p(q){let J=q&&q.length>0?q:typeof window<"u"?window.location.origin:"";return J=J.endsWith("/")?J.slice(0,-1):J,J}function l(q,J){let Q=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${Q}`}function i(q,J){if(q&&J)return`/${q}/${J}`;else if(q&&!J)return`/${q}`;else if(!q&&J)return`/${J}`;else return""}class v{baseUrl;reconnectDelayMs;ws=null;subscriptions=new Map;messageQueue=[];isConnected=!1;reconnectTimeout=null;hasConnected=!1;reconnectListeners=new Set;WebSocketImpl;getHeaders;authToken=null;debug;pendingAuth=!1;authActions=[];constructor(q,J,Q,N=!1,_,T=2000){this.baseUrl=q;this.reconnectDelayMs=T;this.WebSocketImpl=J,this.debug=N&&_==="development",this.getHeaders=async()=>{if(typeof Q==="function")return await Q();return Q??{}},this.connect()}startTopicSubscription(q){let J=this.subscriptions.get(q);if(!J)return;if(J.status="pending",J.auth==="required"&&this.pendingAuth){this.authActions.push(()=>this.startTopicSubscription(q));return}this.send({type:"subscribe",topic:q},{allowDuringPendingAuth:J.auth==="none"})}async connect(){try{let q=await this.getHeaders();this.authToken=q.Authorization||null;let J=this.baseUrl;if(this.debug)console.log(`Connecting to WebSocket: ${J}`);this.ws=new this.WebSocketImpl(J),this.ws.onopen=async()=>{if(this.debug)console.log("WebSocket connection established");let Q=this.hasConnected;if(this.hasConnected=!0,this.isConnected=!0,this.authActions=[],this.authToken){if(this.pendingAuth=!0,this.send({type:"authenticate",token:this.authToken}),this.debug)console.log("Sent authentication token, waiting for response");for(let N of this.subscriptions.keys())this.startTopicSubscription(N);this.authActions.push(()=>{if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((N)=>this.send(N)),this.messageQueue=[]})}else{for(let N of this.subscriptions.keys())this.startTopicSubscription(N);if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((N)=>this.send(N)),this.messageQueue=[]}if(Q)queueMicrotask(()=>{this.reconnectListeners.forEach((N)=>{try{N()}catch(_){console.error("WebSocket reconnect listener failed:",_)}})})},this.ws.onmessage=(Q)=>{try{if(this.debug)console.log("WebSocket message received:",Q.data);let N=JSON.parse(Q.data);if(N.type==="auth_success"){let _=N.authenticated!==!1;if(this.debug)console.log(`WebSocket ${_?"authentication":"deauthentication"} successful`);if(this.pendingAuth=!1,_)this.authActions.forEach((T)=>T());this.authActions=[];return}if(N.type==="auth_error"){console.error("WebSocket authentication failed:",this.createClientError(N.error)),this.pendingAuth=!1,this.authActions=[];return}if(this.debug)console.log("Got new message",N);if(N.type==="subscribe_success"&&N.topic&&this.subscriptions.has(N.topic))this.subscriptions.get(N.topic).status="subscribed";else if(N.type==="publish"&&N.topic&&this.subscriptions.has(N.topic)){let{callbacks:_}=this.subscriptions.get(N.topic);if(this.debug)console.log("Calling subscription callbacks");_?.forEach((T)=>T(void 0,N.data))}else if(["subscribe_error","publish_error"].includes(N.type)&&N.topic&&this.subscriptions.has(N.topic)){let _=this.createSubscriptionError(N.type,N.error),T=this.subscriptions.get(N.topic);T.status="failed",T.callbacks.forEach(($)=>$(_,void 0))}else if(N.type==="unsubscribe_error")console.error(`WebSocket unsubscribe failed for topic ${N.topic??"unknown"}:`,this.createClientError(N.error));else if(N.error)console.error("WebSocket error:",this.createClientError(N.error));else console.warn("Unhandled WebSocket message format:",N)}catch(N){if(this.debug)console.error("Error processing WebSocket message:",N),console.error("Raw message:",Q.data)}},this.ws.onclose=(Q)=>{if(this.isConnected=!1,this.pendingAuth=!1,this.subscriptions.forEach((N)=>{N.status="stale"}),this.debug)console.log(`WebSocket connection closed. Code: ${Q.code}, Reason: ${Q.reason}`);this.reconnectTimeout=setTimeout(()=>this.connect(),this.reconnectDelayMs)},this.ws.onerror=(Q)=>{if(this.debug)console.log("WebSocket error:",Q)}}catch(q){if(this.debug)console.log("Failed to create WebSocket connection:",q);this.pendingAuth=!1,this.reconnectTimeout=setTimeout(()=>this.connect(),this.reconnectDelayMs)}}createClientError(q){return new M(q?.message??"WebSocket request failed",q?.httpStatus??0,q?.code,q?.clientCode,q?.data)}createSubscriptionError(q,J){return new m(q,J??{})}disconnect(){if(this.reconnectTimeout)clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null;if(this.ws)this.ws.close(),this.ws=null;this.isConnected=!1,this.pendingAuth=!1,this.authActions=[],this.subscriptions.clear(),this.reconnectListeners.clear()}onReconnect(q){return this.reconnectListeners.add(q),()=>this.reconnectListeners.delete(q)}send(q,J={}){if(this.isConnected&&this.ws?.readyState===this.WebSocketImpl.OPEN){if(this.pendingAuth&&q.type!=="authenticate"&&!J.allowDuringPendingAuth){if(this.messageQueue.push(q),this.debug)console.log("Message queued until authentication completes:",q);return}let Q=JSON.stringify(q);if(this.ws.send(Q),this.debug)console.log("WebSocket message sent:",Q)}else if(this.messageQueue.push(q),this.debug)console.log("WebSocket message queued (not connected):",q)}subscribe(q,J,Q={}){let N=this.subscriptions.get(q);if(!N){if(N={auth:Q.auth??"required",callbacks:new Set,status:"pending"},this.subscriptions.set(q,N),N.callbacks.add(J),this.isConnected)this.startTopicSubscription(q)}else N.callbacks.add(J);return{unsubscribe:()=>this.unsubscribe(q,J),publish:(_)=>this.publish(q,_,J)}}unsubscribe(q,J){let Q=this.subscriptions.get(q);if(Q){if(Q.callbacks.delete(J),Q.callbacks.size===0){if(this.subscriptions.delete(q),this.send({type:"unsubscribe",topic:q}),this.debug)console.log(`Unsubscribed from topic: ${q}`)}}}publish(q,J,Q){let N=this.subscriptions.get(q);if(N?.status!=="subscribed"){Q(this.createSubscriptionError("publish_error",{message:"Cannot publish to a topic that is not subscribed",code:"SUBSCRIPTION_NOT_SUBSCRIBED",clientCode:"SUBSCRIPTION_NOT_SUBSCRIBED",data:{status:N?.status??"unsubscribed"}}),void 0);return}if(this.send({type:"publish",topic:q,data:J}),this.debug)console.log(`Published to topic: ${q}`,J)}async updateAuth(){let J=(await this.getHeaders()).Authorization||"",Q=J.startsWith("Bearer ")?J.substring(7).trim():J.trim(),N=Q.length>0,_=this.authToken!==null&&this.authToken.trim().length>0;if((N!==_||Q!==this.authToken)&&this.isConnected){if(this.authToken=Q,this.pendingAuth=N,this.send({type:"authenticate",token:J}),this.debug)if(N)console.log("Updated authentication token, waiting for confirmation");else console.log("Sent empty token for deauthentication"),this.pendingAuth=!1,this.authActions=[]}}}function n(q){if(!q)return[];let J=[];return Object.keys(q).sort().forEach((Q)=>{let N=q[Q];if(N===void 0||N===null||N==="")return;J.push(Q,String(N))}),J}function Gq(q,J={}){let Q=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),_=J.prefix??"",T=J.apiPrefix??"",$=J.debug??!1,D=J.s3Endpoint,U=J.nodeEnv;if(!J.s3Endpoint)throw Error("BRPC Client: Pass s3Endpoint option to createBrpcClient");if(!J.nodeEnv)throw Error("BRPC Client: Pass nodeEnv option to createBrpcClient");if(!N)throw Error("WebSocket is not available in this environment");let w=J.headers??{},k=async()=>{if(typeof w==="function")return await w();return w},S=p(q),g=P(J.prefix),o=P(J.apiPrefix),E=i(g,o),y=l(S,g),H=new v(y,N,k,$,U),x=async(Y=!0)=>{let R={};if(Y)R["Content-Type"]="application/json";let X=await k();return{...R,...X}},t=async(Y)=>{if(!Y.ok){let X;try{X=await Y.json()}catch{try{X={error:await Y.text()}}catch{X={error:"Failed to parse error response"}}}if(X?.error&&typeof X.error==="object"){let A=X.error;throw new M(A.message||Y.statusText,Y.status,A.code,A.clientCode,A.data)}let W=X?.error||X?.message||Y.statusText;throw new M(W,Y.status,void 0,void 0,X)}let R=Y.headers.get("Content-Type")||"";try{if(R.includes("application/json"))return(await Y.json()).data;else if(R.includes("text/"))return Y.text();else return Y.blob()}catch(X){throw new M(`Failed to parse response: ${X instanceof Error?X.message:"Unknown error"}`,Y.status,"PARSE_ERROR")}},f=async(Y,R)=>{try{let X=await Q(Y,R);return await t(X)}catch(X){if(X instanceof M)throw X;throw new M(X instanceof Error?X.message:"Network request failed",0,"NETWORK_ERROR")}},O=(Y,R)=>{return Y.split("/").map((X)=>{if(!X.startsWith(":"))return X;let W=X.slice(1),A=R?.[W];if(A===void 0||A===null)throw new M(`Missing route parameter: ${W}`,400,"MISSING_ROUTE_PARAM","MISSING_ROUTE_PARAM",{param:W});return encodeURIComponent(String(A))}).join("/")},d=(Y=[])=>{return new Proxy({},{get(R,X){if(typeof X==="symbol")return;if(X==="query"||X==="mutation"||X==="formMutation"||X==="subscription"||X==="getStringKey"||X==="getArrayKey"||X==="getNoInputsArrayKey"){let W=Y.join("/"),A=(Z)=>`${S}${E}/${Z}`;if($)console.log(`BRPC ${X} procedure path: ${W}`);if(X==="query")return async(Z,V={})=>{let j=O(W,V.params),L=await x(!0);if($)console.log(`BRPC query request to ${j}`,{input:Z,headers:L});let F=new URL(A(j));if(Z&&typeof Z==="object")Object.entries(Z).forEach(([K,I])=>{if(I!==void 0)F.searchParams.append(K,String(I))});let z=await f(F.toString(),{method:"GET",headers:L,credentials:"include"});if($)console.log(`BRPC query response from ${j}:`,z);return z};else if(X==="mutation")return async(Z,V={})=>{let j=O(W,V.params),L=await x(!0);if($)console.log(`BRPC mutation request to ${j}`,{input:Z,headers:L});let F=JSON.stringify(Z??{}),z=await f(A(j),{method:"POST",headers:L,body:F,credentials:"include"});if($)console.log(`BRPC mutation response from ${j}:`,z);return z};else if(X==="formMutation")return async(Z,V={})=>{let j=O(W,V.params),L=await x(!1);if($)console.log(`BRPC formMutation request to ${j}`,{input:Z,headers:L});let F=new FormData,z=(I,G,C)=>{let B=C?`${C}[${I}]`:I;if(G===null||G===void 0)return;else if(c(G))F.append(B,G);else if(Array.isArray(G))G.forEach((b,h)=>{z(String(h),b,B)});else if(typeof G==="object"&&G!==null)Object.entries(G).forEach(([b,h])=>{z(b,h,B)});else F.append(B,String(G))};Object.entries(Z).forEach(([I,G])=>{z(I,G)});let K=await f(A(j),{method:"POST",headers:L,body:F,credentials:"include"});if($)console.log(`BRPC formMutation response from ${j}:`,K);return K};else if(X==="subscription")return(Z,V={})=>{let j=O(W,V.params);if($)console.log(`BRPC subscription to ${j}`);let L=E?`${E.slice(1)}/${j}`:j;return H.updateAuth(),H.subscribe(L,Z,V)};else if(X==="getStringKey")return(Z,V={})=>{let j=O(W,V.params);if(!Z||Object.keys(Z).length===0)return j;let L=Object.keys(Z).sort().reduce((z,K)=>{let I=Z[K];if(I!==void 0&&I!==null)z[K]=I;return z},{});if(Object.keys(L).length===0)return j;let F=JSON.stringify(L);return`${j}?${F}`};else if(X==="getArrayKey")return(Z,V={})=>{let j=O(W,V.params),L=n(V.keys),F=Z??{};if(Object.keys(F).length===0)return[j,...L];let z=[];if(Object.keys(F).sort().forEach((I)=>{let G=F[I];if(G!==void 0&&G!==null){let C=G;if(typeof G==="boolean"||typeof G==="number")C=String(G);else if(typeof G==="object")C=JSON.stringify(G);z.push([I,C])}}),z.length===0)return[j,...L];let K=z.flatMap(([I,G])=>[I,G]);return[j,...K,...L]};else if(X==="getNoInputsArrayKey")return(Z={})=>{return[O(W,Z.params),...n(Z.keys)]}}return d([...Y,X])}})},a=d();if($)console.log("BRPC client created",{baseUrl:S,prefix:E,wsUrl:y});return{routes:a,storage:{getObjectUrl:u(D)},utils:{updateWsAuth:async()=>await H.updateAuth(),onWsReconnect:(Y)=>H.onReconnect(Y),setHeader:async(Y,R)=>{if(typeof w==="function"){console.warn("Cannot use setHeader with function-based headers resolver");return}w={...w,[Y]:R},await H.updateAuth()},setHeaders:async(Y)=>{if(typeof w==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}w={...w,...Y},await H.updateAuth()}}}}class s{entries=new Map;subscribe({key:q,subscription:J,options:Q,listener:N}){let _=JSON.stringify(q),T=(...D)=>N(...D),$=this.entries.get(_);if(!$){$={listeners:new Set,publish:()=>{},unsubscribe:()=>{}},this.entries.set(_,$);try{let D=J((...U)=>{$?.listeners.forEach((w)=>w(...U))},Q);$.publish=D.publish,$.unsubscribe=D.unsubscribe}catch(D){throw this.entries.delete(_),D}}return $.listeners.add(T),{publish:(D)=>{if(this.entries.get(_)===$)$.publish(D)},unsubscribe:()=>this.unsubscribe(_,$,T)}}unsubscribe(q,J,Q){if(J.listeners.delete(Q),J.listeners.size>0||this.entries.get(q)!==J)return;this.entries.delete(q),J.unsubscribe()}}export{e as isBrpcClientErrorUtility,u as createGetObjectUrl,Gq as createBrpcClient,s as SubscriptionService,m as BrpcClientSubscriptionError,M as BrpcClientError};
|
|
2
|
+
function u(q){return(J)=>{if(!J)return null;return`${q}/${J}`}}class V extends Error{status;code;clientCode;data;constructor(q,J,Q,N,_){super(q);this.name="BrpcClientError",this.status=J,this.code=Q,this.clientCode=N,this.data=_}isClientError(q){return this.clientCode===q}isUnauthorized(){return this.status===401}isForbidden(){return this.status===403}isNotFound(){return this.status===404}isValidationError(){return this.status===400||this.code==="BAD_REQUEST"}}class P extends V{type;constructor(q,J){super(J.message??"WebSocket request failed",J.httpStatus??0,J.code,J.clientCode,J.data);this.name="BrpcClientSubscriptionError",this.type=q}}function e(){return function(J,Q){if(Q)return J instanceof V&&J.clientCode===Q;return J instanceof V}}function c(q){return typeof File<"u"&&q instanceof File||typeof Blob<"u"&&q instanceof Blob||typeof q==="object"&&q!==null&&"uri"in q&&"name"in q&&"type"in q}function m(q){if(!q)return null;return q.replace(/\//g,"")}function p(q){let J=q&&q.length>0?q:typeof window<"u"?window.location.origin:"";return J=J.endsWith("/")?J.slice(0,-1):J,J}function l(q,J){let Q=J?`/${J}/ws`:"/ws";return`${q.replace(/^http/,"ws")}${Q}`}function i(q,J){if(q&&J)return`/${q}/${J}`;else if(q&&!J)return`/${q}`;else if(!q&&J)return`/${J}`;else return""}class v{baseUrl;reconnectDelayMs;ws=null;subscriptions=new Map;messageQueue=[];isConnected=!1;reconnectTimeout=null;hasConnected=!1;reconnectListeners=new Set;WebSocketImpl;getHeaders;authToken=null;debug;pendingAuth=!1;authActions=[];constructor(q,J,Q,N=!1,_,D=2000){this.baseUrl=q;this.reconnectDelayMs=D;this.WebSocketImpl=J,this.debug=N&&_==="development",this.getHeaders=async()=>{if(typeof Q==="function")return await Q();return Q??{}},this.connect()}startTopicSubscription(q){let J=this.subscriptions.get(q);if(!J)return;if(J.status="pending",J.auth==="required"&&this.pendingAuth){this.authActions.push(()=>this.startTopicSubscription(q));return}this.send({type:"subscribe",topic:q},{allowDuringPendingAuth:J.auth==="none"})}async connect(){try{let q=await this.getHeaders();this.authToken=q.Authorization||null;let J=this.baseUrl;if(this.debug)console.log(`Connecting to WebSocket: ${J}`);this.ws=new this.WebSocketImpl(J),this.ws.onopen=async()=>{if(this.debug)console.log("WebSocket connection established");let Q=this.hasConnected;if(this.hasConnected=!0,this.isConnected=!0,this.authActions=[],this.authToken){if(this.pendingAuth=!0,this.send({type:"authenticate",token:this.authToken}),this.debug)console.log("Sent authentication token, waiting for response");for(let N of this.subscriptions.keys())this.startTopicSubscription(N);this.authActions.push(()=>{if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((N)=>this.send(N)),this.messageQueue=[]})}else{for(let N of this.subscriptions.keys())this.startTopicSubscription(N);if(this.messageQueue.length>0&&this.debug)console.log(`Sending ${this.messageQueue.length} queued messages`);this.messageQueue.forEach((N)=>this.send(N)),this.messageQueue=[]}if(Q)queueMicrotask(()=>{this.reconnectListeners.forEach((N)=>{try{N()}catch(_){console.error("WebSocket reconnect listener failed:",_)}})})},this.ws.onmessage=(Q)=>{try{if(this.debug)console.log("WebSocket message received:",Q.data);let N=JSON.parse(Q.data);if(N.type==="auth_success"){let _=N.authenticated!==!1;if(this.debug)console.log(`WebSocket ${_?"authentication":"deauthentication"} successful`);if(this.pendingAuth=!1,_)this.authActions.forEach((D)=>D());this.authActions=[];return}if(N.type==="auth_error"){console.error("WebSocket authentication failed:",this.createClientError(N.error)),this.pendingAuth=!1,this.authActions=[];return}if(this.debug)console.log("Got new message",N);if(N.type==="subscribe_success"&&N.topic&&this.subscriptions.has(N.topic))this.subscriptions.get(N.topic).status="subscribed";else if(N.type==="publish"&&N.topic&&this.subscriptions.has(N.topic)){let{callbacks:_}=this.subscriptions.get(N.topic);if(this.debug)console.log("Calling subscription callbacks");_?.forEach((D)=>D(void 0,N.data))}else if(["subscribe_error","publish_error"].includes(N.type)&&N.topic&&this.subscriptions.has(N.topic)){let _=this.createSubscriptionError(N.type,N.error),D=this.subscriptions.get(N.topic);D.status="failed",D.callbacks.forEach(($)=>$(_,void 0))}else if(N.type==="unsubscribe_error")console.error(`WebSocket unsubscribe failed for topic ${N.topic??"unknown"}:`,this.createClientError(N.error));else if(N.error)console.error("WebSocket error:",this.createClientError(N.error));else console.warn("Unhandled WebSocket message format:",N)}catch(N){if(this.debug)console.error("Error processing WebSocket message:",N),console.error("Raw message:",Q.data)}},this.ws.onclose=(Q)=>{if(this.isConnected=!1,this.pendingAuth=!1,this.subscriptions.forEach((N)=>{N.status="stale"}),this.debug)console.log(`WebSocket connection closed. Code: ${Q.code}, Reason: ${Q.reason}`);this.reconnectTimeout=setTimeout(()=>this.connect(),this.reconnectDelayMs)},this.ws.onerror=(Q)=>{if(this.debug)console.log("WebSocket error:",Q)}}catch(q){if(this.debug)console.log("Failed to create WebSocket connection:",q);this.pendingAuth=!1,this.reconnectTimeout=setTimeout(()=>this.connect(),this.reconnectDelayMs)}}createClientError(q){return new V(q?.message??"WebSocket request failed",q?.httpStatus??0,q?.code,q?.clientCode,q?.data)}createSubscriptionError(q,J){return new P(q,J??{})}disconnect(){if(this.reconnectTimeout)clearTimeout(this.reconnectTimeout),this.reconnectTimeout=null;if(this.ws)this.ws.close(),this.ws=null;this.isConnected=!1,this.pendingAuth=!1,this.authActions=[],this.subscriptions.clear(),this.reconnectListeners.clear()}onReconnect(q){return this.reconnectListeners.add(q),()=>this.reconnectListeners.delete(q)}send(q,J={}){if(this.isConnected&&this.ws?.readyState===this.WebSocketImpl.OPEN){if(this.pendingAuth&&q.type!=="authenticate"&&!J.allowDuringPendingAuth){if(this.messageQueue.push(q),this.debug)console.log("Message queued until authentication completes:",q);return}let Q=JSON.stringify(q);if(this.ws.send(Q),this.debug)console.log("WebSocket message sent:",Q)}else if(this.messageQueue.push(q),this.debug)console.log("WebSocket message queued (not connected):",q)}subscribe(q,J,Q={}){let N=this.subscriptions.get(q);if(!N){if(N={auth:Q.auth??"required",callbacks:new Set,status:"pending"},this.subscriptions.set(q,N),N.callbacks.add(J),this.isConnected)this.startTopicSubscription(q)}else N.callbacks.add(J);return{unsubscribe:()=>this.unsubscribe(q,J),publish:(_)=>this.publish(q,_,J)}}unsubscribe(q,J){let Q=this.subscriptions.get(q);if(Q){if(Q.callbacks.delete(J),Q.callbacks.size===0){if(this.subscriptions.delete(q),this.send({type:"unsubscribe",topic:q}),this.debug)console.log(`Unsubscribed from topic: ${q}`)}}}publish(q,J,Q){let N=this.subscriptions.get(q);if(N?.status!=="subscribed"){Q(this.createSubscriptionError("publish_error",{message:"Cannot publish to a topic that is not subscribed",code:"SUBSCRIPTION_NOT_SUBSCRIBED",clientCode:"SUBSCRIPTION_NOT_SUBSCRIBED",data:{status:N?.status??"unsubscribed"}}),void 0);return}if(this.send({type:"publish",topic:q,data:J}),this.debug)console.log(`Published to topic: ${q}`,J)}async updateAuth(){let J=(await this.getHeaders()).Authorization||"",Q=J.startsWith("Bearer ")?J.substring(7).trim():J.trim(),N=Q.length>0,_=this.authToken!==null&&this.authToken.trim().length>0;if((N!==_||Q!==this.authToken)&&this.isConnected){if(this.authToken=Q,this.pendingAuth=N,this.send({type:"authenticate",token:J}),this.debug)if(N)console.log("Updated authentication token, waiting for confirmation");else console.log("Sent empty token for deauthentication"),this.pendingAuth=!1,this.authActions=[]}}}function n(q){if(!q)return[];let J=[];return Object.keys(q).sort().forEach((Q)=>{let N=q[Q];if(N===void 0||N===null||N==="")return;J.push(Q,String(N))}),J}function jq(q,J={}){let Q=J.fetch||fetch,N=J.WebSocket??(typeof WebSocket<"u"?WebSocket:null),_=J.prefix??"",D=J.apiPrefix??"",$=J.debug??!1,K=J.s3Endpoint,C=J.nodeEnv;if(!J.s3Endpoint)throw Error("BRPC Client: Pass s3Endpoint option to createBrpcClient");if(!J.nodeEnv)throw Error("BRPC Client: Pass nodeEnv option to createBrpcClient");if(!N)throw Error("WebSocket is not available in this environment");let T=J.headers??{},k=async()=>{if(typeof T==="function")return await T();return T},S=p(q),g=m(J.prefix),o=m(J.apiPrefix),B=i(g,o),y=l(S,g),w=new v(y,N,k,$,C),x=async(Y=!0)=>{let A={};if(Y)A["Content-Type"]="application/json";let X=await k();return{...A,...X}},t=async(Y)=>{if(!Y.ok){let X;try{X=await Y.json()}catch{try{X={error:await Y.text()}}catch{X={error:"Failed to parse error response"}}}if(X?.error&&typeof X.error==="object"){let R=X.error;throw new V(R.message||Y.statusText,Y.status,R.code,R.clientCode,R.data)}let W=X?.error||X?.message||Y.statusText;throw new V(W,Y.status,void 0,void 0,X)}let A=Y.headers.get("Content-Type")||"";try{if(A.includes("application/json"))return(await Y.json()).data;else if(A.includes("text/"))return Y.text();else return Y.blob()}catch(X){throw new V(`Failed to parse response: ${X instanceof Error?X.message:"Unknown error"}`,Y.status,"PARSE_ERROR")}},f=async(Y,A)=>{try{let X=await Q(Y,A);return await t(X)}catch(X){if(X instanceof V)throw X;throw new V(X instanceof Error?X.message:"Network request failed",0,"NETWORK_ERROR")}},H=(Y,A)=>{return Y.split("/").map((X)=>{if(!X.startsWith(":"))return X;let W=X.slice(1),R=A?.[W];if(R===void 0||R===null)throw new V(`Missing route parameter: ${W}`,400,"MISSING_ROUTE_PARAM","MISSING_ROUTE_PARAM",{param:W});return encodeURIComponent(String(R))}).join("/")},d=(Y=[])=>{return new Proxy({},{get(A,X){if(typeof X==="symbol")return;if(X==="query"||X==="mutation"||X==="formMutation"||X==="subscription"||X==="getStringKey"||X==="getArrayKey"||X==="getNoInputsArrayKey"){let W=Y.join("/"),R=(Z)=>`${S}${B}/${Z}`;if($)console.log(`BRPC ${X} procedure path: ${W}`);if(X==="query")return async(Z,M={})=>{let G=H(W,M.params),I=await x(!0);if($)console.log(`BRPC query request to ${G}`,{input:Z,headers:I});let F=new URL(R(G));if(Z&&typeof Z==="object")Object.entries(Z).forEach(([O,z])=>{if(z!==void 0)F.searchParams.append(O,String(z))});let L=await f(F.toString(),{method:"GET",headers:I,credentials:"include"});if($)console.log(`BRPC query response from ${G}:`,L);return L};else if(X==="mutation")return async(Z,M={})=>{let G=H(W,M.params),I=await x(!0);if($)console.log(`BRPC mutation request to ${G}`,{input:Z,headers:I});let F=JSON.stringify(Z??{}),L=await f(R(G),{method:"POST",headers:I,body:F,credentials:"include"});if($)console.log(`BRPC mutation response from ${G}:`,L);return L};else if(X==="formMutation")return async(Z,M={})=>{let G=H(W,M.params),I=await x(!1);if($)console.log(`BRPC formMutation request to ${G}`,{input:Z,headers:I});let F=new FormData,L=(z,j,E)=>{let U=E?`${E}[${z}]`:z;if(j===null||j===void 0)return;else if(c(j))F.append(U,j);else if(Array.isArray(j))j.forEach((b,h)=>{L(String(h),b,U)});else if(typeof j==="object"&&j!==null)Object.entries(j).forEach(([b,h])=>{L(b,h,U)});else F.append(U,String(j))};Object.entries(Z).forEach(([z,j])=>{L(z,j)});let O=await f(R(G),{method:"POST",headers:I,body:F,credentials:"include"});if($)console.log(`BRPC formMutation response from ${G}:`,O);return O};else if(X==="subscription")return(Z,M={})=>{let G=H(W,M.params);if($)console.log(`BRPC subscription to ${G}`);let I=B?`${B.slice(1)}/${G}`:G;return w.updateAuth(),w.subscribe(I,Z,M)};else if(X==="getStringKey")return(Z,M={})=>{let G=H(W,M.params);if(!Z||Object.keys(Z).length===0)return G;let I=Object.keys(Z).sort().reduce((L,O)=>{let z=Z[O];if(z!==void 0&&z!==null)L[O]=z;return L},{});if(Object.keys(I).length===0)return G;let F=JSON.stringify(I);return`${G}?${F}`};else if(X==="getArrayKey")return(Z,M={})=>{let G=H(W,M.params),I=n(M.keys),F=Z??{};if(Object.keys(F).length===0)return[G,...I];let L=[];if(Object.keys(F).sort().forEach((z)=>{let j=F[z];if(j!==void 0&&j!==null){let E=j;if(typeof j==="boolean"||typeof j==="number")E=String(j);else if(typeof j==="object")E=JSON.stringify(j);L.push([z,E])}}),L.length===0)return[G,...I];let O=L.flatMap(([z,j])=>[z,j]);return[G,...O,...I]};else if(X==="getNoInputsArrayKey")return(Z={})=>{return[H(W,Z.params),...n(Z.keys)]}}return d([...Y,X])}})},a=d();if($)console.log("BRPC client created",{baseUrl:S,prefix:B,wsUrl:y});return{routes:a,storage:{getObjectUrl:u(K)},utils:{updateWsAuth:async()=>await w.updateAuth(),onWsReconnect:(Y)=>w.onReconnect(Y),setHeader:async(Y,A)=>{if(typeof T==="function"){console.warn("Cannot use setHeader with function-based headers resolver");return}T={...T,[Y]:A},await w.updateAuth()},setHeaders:async(Y)=>{if(typeof T==="function"){console.warn("Cannot use setHeaders with function-based headers resolver");return}T={...T,...Y},await w.updateAuth()}}}}class s{entries=new Map;subscribe({key:q,subscription:J,options:Q,listener:N}){let _=JSON.stringify(q),D=(...K)=>N(...K),$=this.entries.get(_);if(!$){$={listeners:new Set,publish:()=>{},unsubscribe:()=>{}},this.entries.set(_,$);try{let K=J((...C)=>{$?.listeners.forEach((T)=>T(...C))},Q);$.publish=K.publish,$.unsubscribe=K.unsubscribe}catch(K){throw this.entries.delete(_),K}}return $.listeners.add(D),{publish:(K)=>{if(this.entries.get(_)===$)$.publish(K)},unsubscribe:()=>this.unsubscribe(_,$,D)}}unsubscribe(q,J,Q){if(J.listeners.delete(Q),J.listeners.size>0||this.entries.get(q)!==J)return;this.entries.delete(q),J.unsubscribe()}}export{e as isBrpcClientErrorUtility,u as createGetObjectUrl,jq as createBrpcClient,s as SubscriptionService,P as BrpcClientSubscriptionError,V as BrpcClientError};
|
|
3
3
|
|
|
4
|
-
//# debugId=
|
|
4
|
+
//# debugId=0D60FB067272A2FE64756E2164756E21
|
package/dist/client/types.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import type { BaseProcedure } from "../types";
|
|
3
3
|
import type { BrpcClientSubscriptionError } from "./brpc-client-error";
|
|
4
4
|
import type { GetObjectUrl } from "./storage";
|
|
5
|
+
import type { NativeFileDescriptor } from "./helpers";
|
|
5
6
|
export type InferProcedureInput<T> = T extends {
|
|
6
7
|
_input: z.ZodType<infer I>;
|
|
7
8
|
} ? I : never;
|
|
@@ -11,6 +12,12 @@ export type InferProcedureOutput<T> = T extends {
|
|
|
11
12
|
export type InferProcedureType<T> = T extends {
|
|
12
13
|
_type: infer P;
|
|
13
14
|
} ? P : never;
|
|
15
|
+
/**
|
|
16
|
+
* Adapts browser file leaves so a form mutation can receive native descriptors too.
|
|
17
|
+
*/
|
|
18
|
+
export type FormMutationInput<T> = T extends File | Blob ? T | NativeFileDescriptor : T extends readonly (infer Item)[] ? FormMutationInput<Item>[] : T extends object ? {
|
|
19
|
+
[Key in keyof T]: FormMutationInput<T[Key]>;
|
|
20
|
+
} : T;
|
|
14
21
|
export type InferRouterOutput<T> = {
|
|
15
22
|
[K in keyof T]: T[K] extends BaseProcedure ? InferProcedureOutput<T[K]> : T[K] extends Record<string, any> ? InferRouterOutput<T[K]> : never;
|
|
16
23
|
};
|
|
@@ -63,6 +70,19 @@ export type SubscriptionHandlers<InputType, _OutputType> = {
|
|
|
63
70
|
type CallOptionsArgs<TParams extends RouteParams> = HasRouteParams<TParams> extends true ? [options: ProcedureCallOptions<TParams>] : [options?: ProcedureCallOptions<TParams>];
|
|
64
71
|
type SubscriptionOptionsArgs<TParams extends RouteParams> = HasRouteParams<TParams> extends true ? [options: SubscriptionOptions<TParams>] : [options?: SubscriptionOptions<TParams>];
|
|
65
72
|
type InputCallArgs<T extends BaseProcedure, TParams extends RouteParams> = HasRouteParams<TParams> extends true ? IsOptionalInput<T> extends true ? [input: InferProcedureInput<T> | undefined, ...CallOptionsArgs<TParams>] : [input: InferProcedureInput<T>, ...CallOptionsArgs<TParams>] : IsOptionalInput<T> extends true ? [input?: InferProcedureInput<T>, ...CallOptionsArgs<TParams>] : [input: InferProcedureInput<T>, ...CallOptionsArgs<TParams>];
|
|
73
|
+
type FormMutationInputCallArgs<T extends BaseProcedure, TParams extends RouteParams> = HasRouteParams<TParams> extends true ? IsOptionalInput<T> extends true ? [
|
|
74
|
+
input: FormMutationInput<InferProcedureInput<T>> | undefined,
|
|
75
|
+
...CallOptionsArgs<TParams>
|
|
76
|
+
] : [
|
|
77
|
+
input: FormMutationInput<InferProcedureInput<T>>,
|
|
78
|
+
...CallOptionsArgs<TParams>
|
|
79
|
+
] : IsOptionalInput<T> extends true ? [
|
|
80
|
+
input?: FormMutationInput<InferProcedureInput<T>>,
|
|
81
|
+
...CallOptionsArgs<TParams>
|
|
82
|
+
] : [
|
|
83
|
+
input: FormMutationInput<InferProcedureInput<T>>,
|
|
84
|
+
...CallOptionsArgs<TParams>
|
|
85
|
+
];
|
|
66
86
|
type CacheKeyOptionsArgs<TParams extends RouteParams> = HasRouteParams<TParams> extends true ? [options: CacheKeyOptions<TParams>] : [options?: CacheKeyOptions<TParams>];
|
|
67
87
|
type CacheKeyInputArgs<T extends BaseProcedure, TParams extends RouteParams> = HasRouteParams<TParams> extends true ? IsOptionalInput<T> extends true ? [input: InferProcedureInput<T> | undefined, ...CacheKeyOptionsArgs<TParams>] : [input: InferProcedureInput<T>, ...CacheKeyOptionsArgs<TParams>] : IsOptionalInput<T> extends true ? [input?: InferProcedureInput<T>, ...CacheKeyOptionsArgs<TParams>] : [input: InferProcedureInput<T>, ...CacheKeyOptionsArgs<TParams>];
|
|
68
88
|
type CacheKeyMethods<T extends BaseProcedure, TParams extends RouteParams> = {
|
|
@@ -77,7 +97,7 @@ type MutationMethods<T extends BaseProcedure, TParams extends RouteParams> = {
|
|
|
77
97
|
mutation: (...args: InputCallArgs<T, TParams>) => Promise<InferProcedureOutput<T>>;
|
|
78
98
|
};
|
|
79
99
|
type FormMutationMethods<T extends BaseProcedure, TParams extends RouteParams> = {
|
|
80
|
-
formMutation: (...args:
|
|
100
|
+
formMutation: (...args: FormMutationInputCallArgs<T, TParams>) => Promise<InferProcedureOutput<T>>;
|
|
81
101
|
};
|
|
82
102
|
type SubscriptionMethods<T extends BaseProcedure, TParams extends RouteParams> = {
|
|
83
103
|
subscription: (callback: SubscriptionCallback<InferProcedureOutput<T>>, ...args: SubscriptionOptionsArgs<TParams>) => SubscriptionHandlers<InferProcedureInput<T>, InferProcedureOutput<T>>;
|