@rpcbase/auth 0.159.0 → 0.160.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/api/request-password-reset/handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAO,MAAM,cAAc,CAAA;AAInD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,OAAO,KAAK,oBAAoB,MAAM,SAAS,CAAA;AAG/C,eAAO,MAAM,+BAA+B,KAAK,CAAA;AAsBjD,eAAO,MAAM,oBAAoB,EAAE,UAAU,CAC3C,oBAAoB,CAAC,cAAc,EACnC,oBAAoB,CAAC,eAAe,EACpC,eAAe,CAiEhB,CAAA;yBAEe,KAAK,GAAG,CAAC,eAAe,CAAC;AAAzC,wBAEC"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/api/request-password-reset/handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAO,MAAM,cAAc,CAAA;AAInD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,OAAO,KAAK,oBAAoB,MAAM,SAAS,CAAA;AAG/C,eAAO,MAAM,+BAA+B,KAAK,CAAA;AAsBjD,eAAO,MAAM,oBAAoB,EAAE,UAAU,CAC3C,oBAAoB,CAAC,cAAc,EACnC,oBAAoB,CAAC,eAAe,EACpC,eAAe,CAqFhB,CAAA;yBAEe,KAAK,GAAG,CAAC,eAAe,CAAC;AAAzC,wBAEC"}
@@ -1,5 +1,7 @@
1
- import { Api } from '@rpcbase/api';
1
+ import { Api, ApiHandler } from '@rpcbase/api';
2
2
  import { AuthSessionUser } from '../../types';
3
+ import * as ResendOtp from "./index";
4
+ export declare const resendOtp: ApiHandler<ResendOtp.RequestPayload, ResendOtp.ResponsePayload, AuthSessionUser>;
3
5
  declare const _default: (api: Api<AuthSessionUser>) => void;
4
6
  export default _default;
5
7
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/api/resend-otp/handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAmB,MAAM,cAAc,CAAA;AAInD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;yBAqDlC,KAAK,GAAG,CAAC,eAAe,CAAC;AAAzC,wBAEC"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/api/resend-otp/handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAE,UAAU,EAAO,MAAM,cAAc,CAAA;AAInD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,OAAO,KAAK,SAAS,MAAM,SAAS,CAAA;AAGpC,eAAO,MAAM,SAAS,EAAE,UAAU,CAAC,SAAS,CAAC,cAAc,EAAE,SAAS,CAAC,eAAe,EAAE,eAAe,CA+DtG,CAAA;yBAEe,KAAK,GAAG,CAAC,eAAe,CAAC;AAAzC,wBAEC"}
@@ -1,5 +1,7 @@
1
- import { Api } from '@rpcbase/api';
1
+ import { Api, ApiHandler } from '@rpcbase/api';
2
2
  import { AuthSessionUser } from '../../types';
3
+ import * as SignUp from "./index";
4
+ export declare const signUp: ApiHandler<SignUp.RequestPayload, SignUp.ResponsePayload, AuthSessionUser>;
3
5
  declare const _default: (api: Api<AuthSessionUser>) => void;
4
6
  export default _default;
5
7
  //# sourceMappingURL=handler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/api/sign-up/handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAmB,MAAM,cAAc,CAAA;AAInD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;yBAiFlC,KAAK,GAAG,CAAC,eAAe,CAAC;AAAzC,wBAEC"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/api/sign-up/handler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,GAAG,EAAO,UAAU,EAAE,MAAM,cAAc,CAAA;AAInD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,OAAO,KAAK,MAAM,MAAM,SAAS,CAAA;AAGjC,eAAO,MAAM,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,eAAe,EAAE,eAAe,CAmF7F,CAAA;yBAEe,KAAK,GAAG,CAAC,eAAe,CAAC;AAAzC,wBAEC"}
@@ -86,7 +86,28 @@ const requestPasswordReset = async (payload, ctx) => {
86
86
  text: `Your password reset code is ${passwordResetCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`
87
87
  });
88
88
  } catch (err) {
89
- console.warn("failed to send password reset email", err);
89
+ console.error("failed to send password reset email", err);
90
+ try {
91
+ const releaseResult = await User.updateOne({
92
+ _id: user._id,
93
+ passwordResetCode
94
+ }, {
95
+ $unset: {
96
+ passwordResetCode: "",
97
+ passwordResetCodeExpiresAt: ""
98
+ }
99
+ });
100
+ if (releaseResult.modifiedCount > 0) {
101
+ ctx.res.removeHeader("Retry-After");
102
+ }
103
+ } catch (releaseErr) {
104
+ console.error("failed to release password reset cooldown", releaseErr);
105
+ }
106
+ ctx.res.status(500);
107
+ return {
108
+ success: false,
109
+ error: "email_delivery_failed"
110
+ };
90
111
  }
91
112
  return {
92
113
  success: true,
@@ -101,4 +122,4 @@ export {
101
122
  handler as default,
102
123
  requestPasswordReset
103
124
  };
104
- //# sourceMappingURL=handler-CYL3nA6F.js.map
125
+ //# sourceMappingURL=handler-B2Qn8_Fq.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-B2Qn8_Fq.js","sources":["../src/api/request-password-reset/index.ts","../src/api/request-password-reset/handler.ts"],"sourcesContent":["import { z } from \"zod\"\n\n\nexport const Route = \"/api/rb/auth/request-password-reset\"\n\nexport const requestSchema = z.object({\n email: z.string().email(),\n})\n\nexport type RequestPayload = z.infer<typeof requestSchema>\n\nexport const responseSchema = z.object({\n success: z.boolean(),\n error: z.string().optional(),\n retryAfterSeconds: z.number().int().positive().optional(),\n})\n\nexport type ResponsePayload = z.infer<typeof responseSchema>\n","import crypto from \"crypto\"\n\nimport { Api, ApiHandler, Ctx } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { sendEmail } from \"@rpcbase/server\"\n\nimport type { AuthSessionUser } from \"../../types\"\n\nimport * as RequestPasswordReset from \"./index\"\n\n\nexport const PASSWORD_RESET_COOLDOWN_SECONDS = 60\nconst PASSWORD_RESET_CODE_TTL_SECONDS = 10 * 60\n\nconst maskEmailForLog = (email: string) => {\n const [localRaw, domainRaw = \"\"] = email.split(\"@\")\n const local = localRaw || \"\"\n const domain = domainRaw || \"\"\n const localPrefix = local.slice(0, Math.min(3, local.length))\n const localMask = \"*\".repeat(Math.max(5, local.length - localPrefix.length))\n\n if (!domain) {\n return `${localPrefix}${localMask}@****`\n }\n\n const domainTailLength = Math.min(7, domain.length)\n const domainTail = domain.slice(-domainTailLength)\n const domainMask = \"*\".repeat(Math.max(4, domain.length - domainTailLength))\n\n return `${localPrefix}${localMask}@${domainMask}${domainTail}`\n}\n\n\nexport const requestPasswordReset: ApiHandler<\n RequestPasswordReset.RequestPayload,\n RequestPasswordReset.ResponsePayload,\n AuthSessionUser\n> = async(\n payload,\n ctx: Ctx<AuthSessionUser>\n): Promise<RequestPasswordReset.ResponsePayload> => {\n const User = await models.getGlobal(\"RBUser\", ctx)\n\n const parsed = RequestPasswordReset.requestSchema.safeParse(payload)\n\n if (!parsed.success) {\n ctx.res.status(400)\n return { success: false, error: \"invalid_payload\" }\n }\n\n const { email } = parsed.data\n const requestedAt = new Date()\n const cooldownEligibleExpiresAt = new Date(\n requestedAt.getTime() + (PASSWORD_RESET_CODE_TTL_SECONDS - PASSWORD_RESET_COOLDOWN_SECONDS) * 1000,\n )\n const passwordResetCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, \"0\")\n const passwordResetCodeExpiresAt = new Date(requestedAt.getTime() + PASSWORD_RESET_CODE_TTL_SECONDS * 1000)\n\n const user = await User.findOneAndUpdate(\n {\n email,\n $or: [\n { passwordResetCodeExpiresAt: { $exists: false } },\n { passwordResetCodeExpiresAt: { $lte: cooldownEligibleExpiresAt } },\n ],\n },\n {\n $set: {\n passwordResetCode,\n passwordResetCodeExpiresAt,\n },\n $unset: {\n passwordResetToken: \"\",\n passwordResetTokenExpiresAt: \"\",\n },\n },\n { returnDocument: \"after\" },\n )\n\n ctx.res.setHeader(\"Retry-After\", String(PASSWORD_RESET_COOLDOWN_SECONDS))\n\n if (!user) {\n return { success: true, retryAfterSeconds: PASSWORD_RESET_COOLDOWN_SECONDS }\n }\n\n try {\n console.info(`sending password reset to email ${maskEmailForLog(email)}`)\n await sendEmail({\n to: email,\n subject: `Your password reset code: ${passwordResetCode}`,\n html: `\n <p>Your password reset code is <strong>${passwordResetCode}</strong>. It expires in 10 minutes.</p>\n <p>If you didn't request this, you can ignore this message.</p>\n `,\n text: `Your password reset code is ${passwordResetCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`,\n })\n } catch (err) {\n console.error(\"failed to send password reset email\", err)\n\n try {\n const releaseResult = await User.updateOne(\n { _id: user._id, passwordResetCode },\n {\n $unset: {\n passwordResetCode: \"\",\n passwordResetCodeExpiresAt: \"\",\n },\n },\n )\n if (releaseResult.modifiedCount > 0) {\n ctx.res.removeHeader(\"Retry-After\")\n }\n } catch (releaseErr) {\n console.error(\"failed to release password reset cooldown\", releaseErr)\n }\n\n ctx.res.status(500)\n return { success: false, error: \"email_delivery_failed\" }\n }\n\n return { success: true, retryAfterSeconds: PASSWORD_RESET_COOLDOWN_SECONDS }\n}\n\nexport default (api: Api<AuthSessionUser>) => {\n api.post(RequestPasswordReset.Route, requestPasswordReset)\n}\n"],"names":["Route","requestSchema","z","email","string","success","boolean","error","optional","retryAfterSeconds","number","int","positive","PASSWORD_RESET_COOLDOWN_SECONDS","PASSWORD_RESET_CODE_TTL_SECONDS","maskEmailForLog","localRaw","domainRaw","split","local","domain","localPrefix","slice","Math","min","length","localMask","repeat","max","domainTailLength","domainTail","domainMask","requestPasswordReset","payload","ctx","User","models","getGlobal","parsed","RequestPasswordReset","safeParse","res","status","data","requestedAt","Date","cooldownEligibleExpiresAt","getTime","passwordResetCode","crypto","randomInt","toString","padStart","passwordResetCodeExpiresAt","user","findOneAndUpdate","$or","$exists","$lte","$set","$unset","passwordResetToken","passwordResetTokenExpiresAt","returnDocument","setHeader","String","console","info","sendEmail","to","subject","html","text","err","releaseResult","updateOne","_id","modifiedCount","removeHeader","releaseErr","api","post"],"mappings":"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"}
@@ -49,7 +49,25 @@ const resendOtp = async (payload, ctx) => {
49
49
  text: `Your verification code is ${emailVerificationCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`
50
50
  });
51
51
  } catch (err) {
52
- console.warn("failed to resend otp email", err);
52
+ console.error("failed to resend otp email", err);
53
+ try {
54
+ await User.updateOne({
55
+ _id: user._id,
56
+ emailVerificationCode
57
+ }, {
58
+ $unset: {
59
+ emailVerificationCode: "",
60
+ emailVerificationExpiresAt: ""
61
+ }
62
+ });
63
+ } catch (releaseErr) {
64
+ console.error("failed to clear unsent otp", releaseErr);
65
+ }
66
+ ctx.res.status(500);
67
+ return {
68
+ success: false,
69
+ error: "email_delivery_failed"
70
+ };
53
71
  }
54
72
  return {
55
73
  success: true
@@ -59,6 +77,7 @@ const handler = (api) => {
59
77
  api.post(Route, resendOtp);
60
78
  };
61
79
  export {
62
- handler as default
80
+ handler as default,
81
+ resendOtp
63
82
  };
64
- //# sourceMappingURL=handler-DBafWh0I.js.map
83
+ //# sourceMappingURL=handler-BKSik1j0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-BKSik1j0.js","sources":["../src/api/resend-otp/index.ts","../src/api/resend-otp/handler.ts"],"sourcesContent":["import { z } from \"zod\"\n\n\nexport const Route = \"/api/rb/auth/resend-otp\"\n\nexport const requestSchema = z.object({\n email: z.string().email(),\n})\n\nexport type RequestPayload = z.infer<typeof requestSchema>\n\nexport const responseSchema = z.object({\n success: z.boolean(),\n error: z.string().optional(),\n})\n\nexport type ResponsePayload = z.infer<typeof responseSchema>\n\n","import crypto from \"crypto\"\n\nimport { Api, ApiHandler, Ctx } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { sendEmail } from \"@rpcbase/server\"\n\nimport type { AuthSessionUser } from \"../../types\"\n\nimport * as ResendOtp from \"./index\"\n\n\nexport const resendOtp: ApiHandler<ResendOtp.RequestPayload, ResendOtp.ResponsePayload, AuthSessionUser> = async (\n payload,\n ctx: Ctx<AuthSessionUser>\n): Promise<ResendOtp.ResponsePayload> => {\n const User = await models.getGlobal(\"RBUser\", ctx)\n\n const parsed = ResendOtp.requestSchema.safeParse(payload)\n\n if (!parsed.success) {\n ctx.res.status(400)\n return { success: false, error: \"invalid_payload\" }\n }\n\n const { email } = parsed.data\n\n const user = await User.findOne({ email })\n\n if (!user) {\n // TODO(auth): avoid account enumeration (return a generic success response even when the user doesn't exist)\n ctx.res.status(404)\n return { success: false, error: \"user_not_found\" }\n }\n\n // TODO(auth): add server-side throttling / rate limiting for resend-otp\n const emailVerificationCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, \"0\")\n const emailVerificationExpiresAt = new Date(Date.now() + 10 * 60 * 1000)\n\n user.emailVerificationCode = emailVerificationCode\n user.emailVerificationExpiresAt = emailVerificationExpiresAt\n await user.save()\n\n try {\n await sendEmail({\n to: email,\n subject: `Your verification code: ${emailVerificationCode}`,\n html: `\n <p>Your verification code is <strong>${emailVerificationCode}</strong>. It expires in 10 minutes.</p>\n <p>If you didn't request this, you can ignore this message.</p>\n `,\n text: `Your verification code is ${emailVerificationCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`,\n })\n } catch (err) {\n console.error(\"failed to resend otp email\", err)\n\n try {\n await User.updateOne(\n { _id: user._id, emailVerificationCode },\n {\n $unset: {\n emailVerificationCode: \"\",\n emailVerificationExpiresAt: \"\",\n },\n },\n )\n } catch (releaseErr) {\n console.error(\"failed to clear unsent otp\", releaseErr)\n }\n\n ctx.res.status(500)\n return { success: false, error: \"email_delivery_failed\" }\n }\n\n return { success: true }\n}\n\nexport default (api: Api<AuthSessionUser>) => {\n api.post(ResendOtp.Route, resendOtp)\n}\n"],"names":["Route","requestSchema","z","email","string","success","boolean","error","optional","resendOtp","payload","ctx","User","models","getGlobal","parsed","ResendOtp","safeParse","res","status","data","user","findOne","emailVerificationCode","crypto","randomInt","toString","padStart","emailVerificationExpiresAt","Date","now","save","sendEmail","to","subject","html","text","err","console","updateOne","_id","$unset","releaseErr","api","post"],"mappings":";;;;AAGO,MAAMA,QAAQ;AAEd,MAAMC,gBAAgBC,OAAS;AAAA,EACpCC,OAAOD,OAAEE,EAASD,MAAAA;AACpB,CAAC;AAI6BD,OAAS;AAAA,EACrCG,SAASH,QAAEI;AAAAA,EACXC,OAAOL,OAAEE,EAASI,SAAAA;AACpB,CAAC;ACHM,MAAMC,YAA8F,OACzGC,SACAC,QACuC;AACvC,QAAMC,OAAO,MAAMC,OAAOC,UAAU,UAAUH,GAAG;AAEjD,QAAMI,SAASC,cAAwBC,UAAUP,OAAO;AAExD,MAAI,CAACK,OAAOV,SAAS;AACnBM,QAAIO,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEd,SAAS;AAAA,MAAOE,OAAO;AAAA,IAAA;AAAA,EAClC;AAEA,QAAM;AAAA,IAAEJ;AAAAA,EAAAA,IAAUY,OAAOK;AAEzB,QAAMC,OAAO,MAAMT,KAAKU,QAAQ;AAAA,IAAEnB;AAAAA,EAAAA,CAAO;AAEzC,MAAI,CAACkB,MAAM;AAETV,QAAIO,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEd,SAAS;AAAA,MAAOE,OAAO;AAAA,IAAA;AAAA,EAClC;AAGA,QAAMgB,wBAAwBC,OAAOC,UAAU,GAAG,GAAS,EAAEC,WAAWC,SAAS,GAAG,GAAG;AACvF,QAAMC,6BAA6B,IAAIC,KAAKA,KAAKC,QAAQ,KAAK,KAAK,GAAI;AAEvET,OAAKE,wBAAwBA;AAC7BF,OAAKO,6BAA6BA;AAClC,QAAMP,KAAKU,KAAAA;AAEX,MAAI;AACF,UAAMC,UAAU;AAAA,MACdC,IAAI9B;AAAAA,MACJ+B,SAAS,2BAA2BX,qBAAqB;AAAA,MACzDY,MAAM;AAAA,+CACmCZ,qBAAqB;AAAA;AAAA;AAAA,MAG9Da,MAAM,6BAA6Bb,qBAAqB;AAAA,IAAA,CACzD;AAAA,EACH,SAASc,KAAK;AACZC,YAAQ/B,MAAM,8BAA8B8B,GAAG;AAE/C,QAAI;AACF,YAAMzB,KAAK2B,UACT;AAAA,QAAEC,KAAKnB,KAAKmB;AAAAA,QAAKjB;AAAAA,MAAAA,GACjB;AAAA,QACEkB,QAAQ;AAAA,UACNlB,uBAAuB;AAAA,UACvBK,4BAA4B;AAAA,QAAA;AAAA,MAC9B,CAEJ;AAAA,IACF,SAASc,YAAY;AACnBJ,cAAQ/B,MAAM,8BAA8BmC,UAAU;AAAA,IACxD;AAEA/B,QAAIO,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEd,SAAS;AAAA,MAAOE,OAAO;AAAA,IAAA;AAAA,EAClC;AAEA,SAAO;AAAA,IAAEF,SAAS;AAAA,EAAA;AACpB;AAEA,MAAA,UAAe,CAACsC,QAA8B;AAC5CA,MAAIC,KAAK5B,OAAiBP,SAAS;AACrC;"}
@@ -56,7 +56,19 @@ const signUp = async (payload, ctx) => {
56
56
  text: `Welcome to rpcbase! Your verification code is ${emailVerificationCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`
57
57
  });
58
58
  } catch (err) {
59
- console.warn("failed to send sign-up email", err);
59
+ console.error("failed to send sign-up email", err);
60
+ try {
61
+ await User.deleteOne({
62
+ _id: user._id
63
+ });
64
+ } catch (rollbackErr) {
65
+ console.error("failed to roll back user after sign-up email failure", rollbackErr);
66
+ }
67
+ ctx.res.status(500);
68
+ return {
69
+ success: false,
70
+ error: "email_delivery_failed"
71
+ };
60
72
  }
61
73
  try {
62
74
  await Tenant.create({
@@ -77,6 +89,7 @@ const handler = (api) => {
77
89
  api.post(Route, signUp);
78
90
  };
79
91
  export {
80
- handler as default
92
+ handler as default,
93
+ signUp
81
94
  };
82
- //# sourceMappingURL=handler-Duy5AFEH.js.map
95
+ //# sourceMappingURL=handler-BXvh_Nx4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handler-BXvh_Nx4.js","sources":["../src/api/sign-up/handler.ts"],"sourcesContent":["import crypto from \"crypto\"\n\nimport { Api, Ctx, ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { hashPasswordForStorage, sendEmail } from \"@rpcbase/server\"\n\nimport type { AuthSessionUser } from \"../../types\"\n\nimport * as SignUp from \"./index\"\n\n\nexport const signUp: ApiHandler<SignUp.RequestPayload, SignUp.ResponsePayload, AuthSessionUser> = async(\n payload,\n ctx: Ctx<AuthSessionUser>\n): Promise<SignUp.ResponsePayload> => {\n const User = await models.getGlobal(\"RBUser\", ctx)\n const Tenant = await models.getGlobal(\"RBTenant\", ctx)\n\n const parsed = SignUp.requestSchema.safeParse(payload)\n\n if (!parsed.success) {\n ctx.res.status(400)\n return { success: false, error: \"invalid_payload\" }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { email, password, rememberMe: _rememberMe } = parsed.data\n\n const existingUser = await User.findOne({ email })\n\n if (existingUser) {\n console.log(\"user with email already exists\", email)\n // For security, we don't want to reveal if an email is registered.\n // But for this implementation, we'll make it simple.\n ctx.res.status(409)\n return { success: false, error: \"user_exists\" }\n }\n\n const hashedPassword = await hashPasswordForStorage(password)\n\n const tenantId = crypto.randomUUID()\n\n const emailVerificationCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, \"0\")\n const emailVerificationExpiresAt = new Date(Date.now() + 10 * 60 * 1000)\n\n const user = new User({\n email,\n password: hashedPassword,\n tenants: [tenantId],\n tenantRoles: {\n [tenantId]: [\"owner\"],\n },\n emailVerificationCode,\n emailVerificationExpiresAt,\n })\n await user.save()\n\n try {\n await sendEmail({\n to: email,\n subject: `Verify your email: ${emailVerificationCode}`,\n html: `\n <p>Welcome to rpcbase!</p>\n <p>Your verification code is <strong>${emailVerificationCode}</strong>. It expires in 10 minutes.</p>\n <p>If you didn't request this, you can ignore this message.</p>\n `,\n text: `Welcome to rpcbase! Your verification code is ${emailVerificationCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`,\n })\n } catch (err) {\n console.error(\"failed to send sign-up email\", err)\n\n try {\n await User.deleteOne({ _id: user._id })\n } catch (rollbackErr) {\n console.error(\"failed to roll back user after sign-up email failure\", rollbackErr)\n }\n\n ctx.res.status(500)\n return { success: false, error: \"email_delivery_failed\" }\n }\n\n try {\n await Tenant.create({\n tenantId,\n name: email,\n })\n } catch (err) {\n // best-effort tenant creation; log and continue\n console.warn(\"failed to create tenant for user\", err)\n }\n\n console.log(\"created new user\", user._id.toString())\n\n return { success: true, userId: user._id.toString(), tenantId }\n}\n\nexport default (api: Api<AuthSessionUser>) => {\n api.post(SignUp.Route, signUp)\n}\n"],"names":["signUp","payload","ctx","User","models","getGlobal","Tenant","parsed","SignUp","safeParse","success","res","status","error","email","password","rememberMe","_rememberMe","data","existingUser","findOne","console","log","hashedPassword","hashPasswordForStorage","tenantId","crypto","randomUUID","emailVerificationCode","randomInt","toString","padStart","emailVerificationExpiresAt","Date","now","user","tenants","tenantRoles","save","sendEmail","to","subject","html","text","err","deleteOne","_id","rollbackErr","create","name","warn","userId","api","post"],"mappings":";;;;AAWO,MAAMA,SAAqF,OAChGC,SACAC,QACoC;AACpC,QAAMC,OAAO,MAAMC,OAAOC,UAAU,UAAUH,GAAG;AACjD,QAAMI,SAAS,MAAMF,OAAOC,UAAU,YAAYH,GAAG;AAErD,QAAMK,SAASC,cAAqBC,UAAUR,OAAO;AAErD,MAAI,CAACM,OAAOG,SAAS;AACnBR,QAAIS,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEF,SAAS;AAAA,MAAOG,OAAO;AAAA,IAAA;AAAA,EAClC;AAGA,QAAM;AAAA,IAAEC;AAAAA,IAAOC;AAAAA,IAAUC,YAAYC;AAAAA,EAAAA,IAAgBV,OAAOW;AAE5D,QAAMC,eAAe,MAAMhB,KAAKiB,QAAQ;AAAA,IAAEN;AAAAA,EAAAA,CAAO;AAEjD,MAAIK,cAAc;AAChBE,YAAQC,IAAI,kCAAkCR,KAAK;AAGnDZ,QAAIS,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEF,SAAS;AAAA,MAAOG,OAAO;AAAA,IAAA;AAAA,EAClC;AAEA,QAAMU,iBAAiB,MAAMC,uBAAuBT,QAAQ;AAE5D,QAAMU,WAAWC,OAAOC,WAAAA;AAExB,QAAMC,wBAAwBF,OAAOG,UAAU,GAAG,GAAS,EAAEC,WAAWC,SAAS,GAAG,GAAG;AACvF,QAAMC,6BAA6B,IAAIC,KAAKA,KAAKC,QAAQ,KAAK,KAAK,GAAI;AAEvE,QAAMC,OAAO,IAAIhC,KAAK;AAAA,IACpBW;AAAAA,IACAC,UAAUQ;AAAAA,IACVa,SAAS,CAACX,QAAQ;AAAA,IAClBY,aAAa;AAAA,MACX,CAACZ,QAAQ,GAAG,CAAC,OAAO;AAAA,IAAA;AAAA,IAEtBG;AAAAA,IACAI;AAAAA,EAAAA,CACD;AACD,QAAMG,KAAKG,KAAAA;AAEX,MAAI;AACF,UAAMC,UAAU;AAAA,MACdC,IAAI1B;AAAAA,MACJ2B,SAAS,sBAAsBb,qBAAqB;AAAA,MACpDc,MAAM;AAAA;AAAA,+CAEmCd,qBAAqB;AAAA;AAAA;AAAA,MAG9De,MAAM,iDAAiDf,qBAAqB;AAAA,IAAA,CAC7E;AAAA,EACH,SAASgB,KAAK;AACZvB,YAAQR,MAAM,gCAAgC+B,GAAG;AAEjD,QAAI;AACF,YAAMzC,KAAK0C,UAAU;AAAA,QAAEC,KAAKX,KAAKW;AAAAA,MAAAA,CAAK;AAAA,IACxC,SAASC,aAAa;AACpB1B,cAAQR,MAAM,wDAAwDkC,WAAW;AAAA,IACnF;AAEA7C,QAAIS,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEF,SAAS;AAAA,MAAOG,OAAO;AAAA,IAAA;AAAA,EAClC;AAEA,MAAI;AACF,UAAMP,OAAO0C,OAAO;AAAA,MAClBvB;AAAAA,MACAwB,MAAMnC;AAAAA,IAAAA,CACP;AAAA,EACH,SAAS8B,KAAK;AAEZvB,YAAQ6B,KAAK,oCAAoCN,GAAG;AAAA,EACtD;AAEAvB,UAAQC,IAAI,oBAAoBa,KAAKW,IAAIhB,UAAU;AAEnD,SAAO;AAAA,IAAEpB,SAAS;AAAA,IAAMyC,QAAQhB,KAAKW,IAAIhB,SAAAA;AAAAA,IAAYL;AAAAA,EAAAA;AACvD;AAEA,MAAA,UAAe,CAAC2B,QAA8B;AAC5CA,MAAIC,KAAK7C,OAAcR,MAAM;AAC/B;"}
package/dist/routes.js CHANGED
@@ -1,5 +1,5 @@
1
1
  const routes = Object.entries({
2
- .../* @__PURE__ */ Object.assign({ "./api/me/handler.ts": () => import("./handler-C-hPxnHG.js"), "./api/request-password-reset/handler.ts": () => import("./handler-CYL3nA6F.js"), "./api/resend-otp/handler.ts": () => import("./handler-DBafWh0I.js"), "./api/set-active-account/handler.ts": () => import("./handler-B_lItxOC.js"), "./api/set-new-password/handler.ts": () => import("./handler-U3df_2Al.js"), "./api/sign-in/handler.ts": () => import("./handler-CoHpAhui.js"), "./api/sign-out/handler.ts": () => import("./handler-BuwKXMOu.js"), "./api/sign-up/handler.ts": () => import("./handler-Duy5AFEH.js"), "./api/verify-otp/handler.ts": () => import("./handler-DfZwgoA4.js"), "./api/verify-password-reset-otp/handler.ts": () => import("./handler-Eyjkxj9l.js") })
2
+ .../* @__PURE__ */ Object.assign({ "./api/me/handler.ts": () => import("./handler-C-hPxnHG.js"), "./api/request-password-reset/handler.ts": () => import("./handler-B2Qn8_Fq.js"), "./api/resend-otp/handler.ts": () => import("./handler-BKSik1j0.js"), "./api/set-active-account/handler.ts": () => import("./handler-B_lItxOC.js"), "./api/set-new-password/handler.ts": () => import("./handler-U3df_2Al.js"), "./api/sign-in/handler.ts": () => import("./handler-CoHpAhui.js"), "./api/sign-out/handler.ts": () => import("./handler-BuwKXMOu.js"), "./api/sign-up/handler.ts": () => import("./handler-BXvh_Nx4.js"), "./api/verify-otp/handler.ts": () => import("./handler-DfZwgoA4.js"), "./api/verify-password-reset-otp/handler.ts": () => import("./handler-Eyjkxj9l.js") })
3
3
  }).reduce((acc, [path, mod]) => {
4
4
  acc[path.replace("./api/", "@rpcbase/auth/api/")] = mod;
5
5
  return acc;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpcbase/auth",
3
- "version": "0.159.0",
3
+ "version": "0.160.0",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -1 +0,0 @@
1
- {"version":3,"file":"handler-CYL3nA6F.js","sources":["../src/api/request-password-reset/index.ts","../src/api/request-password-reset/handler.ts"],"sourcesContent":["import { z } from \"zod\"\n\n\nexport const Route = \"/api/rb/auth/request-password-reset\"\n\nexport const requestSchema = z.object({\n email: z.string().email(),\n})\n\nexport type RequestPayload = z.infer<typeof requestSchema>\n\nexport const responseSchema = z.object({\n success: z.boolean(),\n error: z.string().optional(),\n retryAfterSeconds: z.number().int().positive().optional(),\n})\n\nexport type ResponsePayload = z.infer<typeof responseSchema>\n","import crypto from \"crypto\"\n\nimport { Api, ApiHandler, Ctx } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { sendEmail } from \"@rpcbase/server\"\n\nimport type { AuthSessionUser } from \"../../types\"\n\nimport * as RequestPasswordReset from \"./index\"\n\n\nexport const PASSWORD_RESET_COOLDOWN_SECONDS = 60\nconst PASSWORD_RESET_CODE_TTL_SECONDS = 10 * 60\n\nconst maskEmailForLog = (email: string) => {\n const [localRaw, domainRaw = \"\"] = email.split(\"@\")\n const local = localRaw || \"\"\n const domain = domainRaw || \"\"\n const localPrefix = local.slice(0, Math.min(3, local.length))\n const localMask = \"*\".repeat(Math.max(5, local.length - localPrefix.length))\n\n if (!domain) {\n return `${localPrefix}${localMask}@****`\n }\n\n const domainTailLength = Math.min(7, domain.length)\n const domainTail = domain.slice(-domainTailLength)\n const domainMask = \"*\".repeat(Math.max(4, domain.length - domainTailLength))\n\n return `${localPrefix}${localMask}@${domainMask}${domainTail}`\n}\n\n\nexport const requestPasswordReset: ApiHandler<\n RequestPasswordReset.RequestPayload,\n RequestPasswordReset.ResponsePayload,\n AuthSessionUser\n> = async(\n payload,\n ctx: Ctx<AuthSessionUser>\n): Promise<RequestPasswordReset.ResponsePayload> => {\n const User = await models.getGlobal(\"RBUser\", ctx)\n\n const parsed = RequestPasswordReset.requestSchema.safeParse(payload)\n\n if (!parsed.success) {\n ctx.res.status(400)\n return { success: false, error: \"invalid_payload\" }\n }\n\n const { email } = parsed.data\n const requestedAt = new Date()\n const cooldownEligibleExpiresAt = new Date(\n requestedAt.getTime() + (PASSWORD_RESET_CODE_TTL_SECONDS - PASSWORD_RESET_COOLDOWN_SECONDS) * 1000,\n )\n const passwordResetCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, \"0\")\n const passwordResetCodeExpiresAt = new Date(requestedAt.getTime() + PASSWORD_RESET_CODE_TTL_SECONDS * 1000)\n\n const user = await User.findOneAndUpdate(\n {\n email,\n $or: [\n { passwordResetCodeExpiresAt: { $exists: false } },\n { passwordResetCodeExpiresAt: { $lte: cooldownEligibleExpiresAt } },\n ],\n },\n {\n $set: {\n passwordResetCode,\n passwordResetCodeExpiresAt,\n },\n $unset: {\n passwordResetToken: \"\",\n passwordResetTokenExpiresAt: \"\",\n },\n },\n { returnDocument: \"after\" },\n )\n\n ctx.res.setHeader(\"Retry-After\", String(PASSWORD_RESET_COOLDOWN_SECONDS))\n\n if (!user) {\n return { success: true, retryAfterSeconds: PASSWORD_RESET_COOLDOWN_SECONDS }\n }\n\n try {\n console.info(`sending password reset to email ${maskEmailForLog(email)}`)\n await sendEmail({\n to: email,\n subject: `Your password reset code: ${passwordResetCode}`,\n html: `\n <p>Your password reset code is <strong>${passwordResetCode}</strong>. It expires in 10 minutes.</p>\n <p>If you didn't request this, you can ignore this message.</p>\n `,\n text: `Your password reset code is ${passwordResetCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`,\n })\n } catch (err) {\n console.warn(\"failed to send password reset email\", err)\n }\n\n return { success: true, retryAfterSeconds: PASSWORD_RESET_COOLDOWN_SECONDS }\n}\n\nexport default (api: Api<AuthSessionUser>) => {\n api.post(RequestPasswordReset.Route, requestPasswordReset)\n}\n"],"names":["Route","requestSchema","z","email","string","success","boolean","error","optional","retryAfterSeconds","number","int","positive","PASSWORD_RESET_COOLDOWN_SECONDS","PASSWORD_RESET_CODE_TTL_SECONDS","maskEmailForLog","localRaw","domainRaw","split","local","domain","localPrefix","slice","Math","min","length","localMask","repeat","max","domainTailLength","domainTail","domainMask","requestPasswordReset","payload","ctx","User","models","getGlobal","parsed","RequestPasswordReset","safeParse","res","status","data","requestedAt","Date","cooldownEligibleExpiresAt","getTime","passwordResetCode","crypto","randomInt","toString","padStart","passwordResetCodeExpiresAt","user","findOneAndUpdate","$or","$exists","$lte","$set","$unset","passwordResetToken","passwordResetTokenExpiresAt","returnDocument","setHeader","String","console","info","sendEmail","to","subject","html","text","err","warn","api","post"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"handler-DBafWh0I.js","sources":["../src/api/resend-otp/index.ts","../src/api/resend-otp/handler.ts"],"sourcesContent":["import { z } from \"zod\"\n\n\nexport const Route = \"/api/rb/auth/resend-otp\"\n\nexport const requestSchema = z.object({\n email: z.string().email(),\n})\n\nexport type RequestPayload = z.infer<typeof requestSchema>\n\nexport const responseSchema = z.object({\n success: z.boolean(),\n error: z.string().optional(),\n})\n\nexport type ResponsePayload = z.infer<typeof responseSchema>\n\n","import crypto from \"crypto\"\n\nimport { Api, ApiHandler, Ctx } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { sendEmail } from \"@rpcbase/server\"\n\nimport type { AuthSessionUser } from \"../../types\"\n\nimport * as ResendOtp from \"./index\"\n\n\nconst resendOtp: ApiHandler<ResendOtp.RequestPayload, ResendOtp.ResponsePayload, AuthSessionUser> = async (\n payload,\n ctx: Ctx<AuthSessionUser>\n): Promise<ResendOtp.ResponsePayload> => {\n const User = await models.getGlobal(\"RBUser\", ctx)\n\n const parsed = ResendOtp.requestSchema.safeParse(payload)\n\n if (!parsed.success) {\n ctx.res.status(400)\n return { success: false, error: \"invalid_payload\" }\n }\n\n const { email } = parsed.data\n\n const user = await User.findOne({ email })\n\n if (!user) {\n // TODO(auth): avoid account enumeration (return a generic success response even when the user doesn't exist)\n ctx.res.status(404)\n return { success: false, error: \"user_not_found\" }\n }\n\n // TODO(auth): add server-side throttling / rate limiting for resend-otp\n const emailVerificationCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, \"0\")\n const emailVerificationExpiresAt = new Date(Date.now() + 10 * 60 * 1000)\n\n user.emailVerificationCode = emailVerificationCode\n user.emailVerificationExpiresAt = emailVerificationExpiresAt\n await user.save()\n\n try {\n await sendEmail({\n to: email,\n subject: `Your verification code: ${emailVerificationCode}`,\n html: `\n <p>Your verification code is <strong>${emailVerificationCode}</strong>. It expires in 10 minutes.</p>\n <p>If you didn't request this, you can ignore this message.</p>\n `,\n text: `Your verification code is ${emailVerificationCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`,\n })\n } catch (err) {\n console.warn(\"failed to resend otp email\", err)\n }\n\n return { success: true }\n}\n\nexport default (api: Api<AuthSessionUser>) => {\n api.post(ResendOtp.Route, resendOtp)\n}\n"],"names":["Route","requestSchema","z","email","string","success","boolean","error","optional","resendOtp","payload","ctx","User","models","getGlobal","parsed","ResendOtp","safeParse","res","status","data","user","findOne","emailVerificationCode","crypto","randomInt","toString","padStart","emailVerificationExpiresAt","Date","now","save","sendEmail","to","subject","html","text","err","console","warn","api","post"],"mappings":";;;;AAGO,MAAMA,QAAQ;AAEd,MAAMC,gBAAgBC,OAAS;AAAA,EACpCC,OAAOD,OAAEE,EAASD,MAAAA;AACpB,CAAC;AAI6BD,OAAS;AAAA,EACrCG,SAASH,QAAEI;AAAAA,EACXC,OAAOL,OAAEE,EAASI,SAAAA;AACpB,CAAC;ACHD,MAAMC,YAA8F,OAClGC,SACAC,QACuC;AACvC,QAAMC,OAAO,MAAMC,OAAOC,UAAU,UAAUH,GAAG;AAEjD,QAAMI,SAASC,cAAwBC,UAAUP,OAAO;AAExD,MAAI,CAACK,OAAOV,SAAS;AACnBM,QAAIO,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEd,SAAS;AAAA,MAAOE,OAAO;AAAA,IAAA;AAAA,EAClC;AAEA,QAAM;AAAA,IAAEJ;AAAAA,EAAAA,IAAUY,OAAOK;AAEzB,QAAMC,OAAO,MAAMT,KAAKU,QAAQ;AAAA,IAAEnB;AAAAA,EAAAA,CAAO;AAEzC,MAAI,CAACkB,MAAM;AAETV,QAAIO,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEd,SAAS;AAAA,MAAOE,OAAO;AAAA,IAAA;AAAA,EAClC;AAGA,QAAMgB,wBAAwBC,OAAOC,UAAU,GAAG,GAAS,EAAEC,WAAWC,SAAS,GAAG,GAAG;AACvF,QAAMC,6BAA6B,IAAIC,KAAKA,KAAKC,QAAQ,KAAK,KAAK,GAAI;AAEvET,OAAKE,wBAAwBA;AAC7BF,OAAKO,6BAA6BA;AAClC,QAAMP,KAAKU,KAAAA;AAEX,MAAI;AACF,UAAMC,UAAU;AAAA,MACdC,IAAI9B;AAAAA,MACJ+B,SAAS,2BAA2BX,qBAAqB;AAAA,MACzDY,MAAM;AAAA,+CACmCZ,qBAAqB;AAAA;AAAA;AAAA,MAG9Da,MAAM,6BAA6Bb,qBAAqB;AAAA,IAAA,CACzD;AAAA,EACH,SAASc,KAAK;AACZC,YAAQC,KAAK,8BAA8BF,GAAG;AAAA,EAChD;AAEA,SAAO;AAAA,IAAEhC,SAAS;AAAA,EAAA;AACpB;AAEA,MAAA,UAAe,CAACmC,QAA8B;AAC5CA,MAAIC,KAAKzB,OAAiBP,SAAS;AACrC;"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"handler-Duy5AFEH.js","sources":["../src/api/sign-up/handler.ts"],"sourcesContent":["import crypto from \"crypto\"\n\nimport { Api, Ctx, ApiHandler } from \"@rpcbase/api\"\nimport { models } from \"@rpcbase/db\"\nimport { hashPasswordForStorage, sendEmail } from \"@rpcbase/server\"\n\nimport type { AuthSessionUser } from \"../../types\"\n\nimport * as SignUp from \"./index\"\n\n\nconst signUp: ApiHandler<SignUp.RequestPayload, SignUp.ResponsePayload, AuthSessionUser> = async(\n payload,\n ctx: Ctx<AuthSessionUser>\n): Promise<SignUp.ResponsePayload> => {\n const User = await models.getGlobal(\"RBUser\", ctx)\n const Tenant = await models.getGlobal(\"RBTenant\", ctx)\n\n const parsed = SignUp.requestSchema.safeParse(payload)\n\n if (!parsed.success) {\n ctx.res.status(400)\n return { success: false, error: \"invalid_payload\" }\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n const { email, password, rememberMe: _rememberMe } = parsed.data\n\n const existingUser = await User.findOne({ email })\n\n if (existingUser) {\n console.log(\"user with email already exists\", email)\n // For security, we don't want to reveal if an email is registered.\n // But for this implementation, we'll make it simple.\n ctx.res.status(409)\n return { success: false, error: \"user_exists\" }\n }\n\n const hashedPassword = await hashPasswordForStorage(password)\n\n const tenantId = crypto.randomUUID()\n\n const emailVerificationCode = crypto.randomInt(0, 1_000_000).toString().padStart(6, \"0\")\n const emailVerificationExpiresAt = new Date(Date.now() + 10 * 60 * 1000)\n\n const user = new User({\n email,\n password: hashedPassword,\n tenants: [tenantId],\n tenantRoles: {\n [tenantId]: [\"owner\"],\n },\n emailVerificationCode,\n emailVerificationExpiresAt,\n })\n await user.save()\n\n try {\n await sendEmail({\n to: email,\n subject: `Verify your email: ${emailVerificationCode}`,\n html: `\n <p>Welcome to rpcbase!</p>\n <p>Your verification code is <strong>${emailVerificationCode}</strong>. It expires in 10 minutes.</p>\n <p>If you didn't request this, you can ignore this message.</p>\n `,\n text: `Welcome to rpcbase! Your verification code is ${emailVerificationCode}. It expires in 10 minutes. If you didn't request this, you can ignore this message.`,\n })\n } catch (err) {\n console.warn(\"failed to send sign-up email\", err)\n }\n\n try {\n await Tenant.create({\n tenantId,\n name: email,\n })\n } catch (err) {\n // best-effort tenant creation; log and continue\n console.warn(\"failed to create tenant for user\", err)\n }\n\n console.log(\"created new user\", user._id.toString())\n\n return { success: true, userId: user._id.toString(), tenantId }\n}\n\nexport default (api: Api<AuthSessionUser>) => {\n api.post(SignUp.Route, signUp)\n}\n"],"names":["signUp","payload","ctx","User","models","getGlobal","Tenant","parsed","SignUp","safeParse","success","res","status","error","email","password","rememberMe","_rememberMe","data","existingUser","findOne","console","log","hashedPassword","hashPasswordForStorage","tenantId","crypto","randomUUID","emailVerificationCode","randomInt","toString","padStart","emailVerificationExpiresAt","Date","now","user","tenants","tenantRoles","save","sendEmail","to","subject","html","text","err","warn","create","name","_id","userId","api","post"],"mappings":";;;;AAWA,MAAMA,SAAqF,OACzFC,SACAC,QACoC;AACpC,QAAMC,OAAO,MAAMC,OAAOC,UAAU,UAAUH,GAAG;AACjD,QAAMI,SAAS,MAAMF,OAAOC,UAAU,YAAYH,GAAG;AAErD,QAAMK,SAASC,cAAqBC,UAAUR,OAAO;AAErD,MAAI,CAACM,OAAOG,SAAS;AACnBR,QAAIS,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEF,SAAS;AAAA,MAAOG,OAAO;AAAA,IAAA;AAAA,EAClC;AAGA,QAAM;AAAA,IAAEC;AAAAA,IAAOC;AAAAA,IAAUC,YAAYC;AAAAA,EAAAA,IAAgBV,OAAOW;AAE5D,QAAMC,eAAe,MAAMhB,KAAKiB,QAAQ;AAAA,IAAEN;AAAAA,EAAAA,CAAO;AAEjD,MAAIK,cAAc;AAChBE,YAAQC,IAAI,kCAAkCR,KAAK;AAGnDZ,QAAIS,IAAIC,OAAO,GAAG;AAClB,WAAO;AAAA,MAAEF,SAAS;AAAA,MAAOG,OAAO;AAAA,IAAA;AAAA,EAClC;AAEA,QAAMU,iBAAiB,MAAMC,uBAAuBT,QAAQ;AAE5D,QAAMU,WAAWC,OAAOC,WAAAA;AAExB,QAAMC,wBAAwBF,OAAOG,UAAU,GAAG,GAAS,EAAEC,WAAWC,SAAS,GAAG,GAAG;AACvF,QAAMC,6BAA6B,IAAIC,KAAKA,KAAKC,QAAQ,KAAK,KAAK,GAAI;AAEvE,QAAMC,OAAO,IAAIhC,KAAK;AAAA,IACpBW;AAAAA,IACAC,UAAUQ;AAAAA,IACVa,SAAS,CAACX,QAAQ;AAAA,IAClBY,aAAa;AAAA,MACX,CAACZ,QAAQ,GAAG,CAAC,OAAO;AAAA,IAAA;AAAA,IAEtBG;AAAAA,IACAI;AAAAA,EAAAA,CACD;AACD,QAAMG,KAAKG,KAAAA;AAEX,MAAI;AACF,UAAMC,UAAU;AAAA,MACdC,IAAI1B;AAAAA,MACJ2B,SAAS,sBAAsBb,qBAAqB;AAAA,MACpDc,MAAM;AAAA;AAAA,+CAEmCd,qBAAqB;AAAA;AAAA;AAAA,MAG9De,MAAM,iDAAiDf,qBAAqB;AAAA,IAAA,CAC7E;AAAA,EACH,SAASgB,KAAK;AACZvB,YAAQwB,KAAK,gCAAgCD,GAAG;AAAA,EAClD;AAEA,MAAI;AACF,UAAMtC,OAAOwC,OAAO;AAAA,MAClBrB;AAAAA,MACAsB,MAAMjC;AAAAA,IAAAA,CACP;AAAA,EACH,SAAS8B,KAAK;AAEZvB,YAAQwB,KAAK,oCAAoCD,GAAG;AAAA,EACtD;AAEAvB,UAAQC,IAAI,oBAAoBa,KAAKa,IAAIlB,UAAU;AAEnD,SAAO;AAAA,IAAEpB,SAAS;AAAA,IAAMuC,QAAQd,KAAKa,IAAIlB,SAAAA;AAAAA,IAAYL;AAAAA,EAAAA;AACvD;AAEA,MAAA,UAAe,CAACyB,QAA8B;AAC5CA,MAAIC,KAAK3C,OAAcR,MAAM;AAC/B;"}