tempest-express-sdk 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -8197,6 +8197,190 @@ function makeWhatsAppWebhookRouter(options) {
8197
8197
  return router;
8198
8198
  }
8199
8199
 
8200
+ // src/integrations/telegram.ts
8201
+ var MEDIA_METHOD = {
8202
+ image: { method: "sendPhoto", field: "photo" },
8203
+ video: { method: "sendVideo", field: "video" },
8204
+ audio: { method: "sendAudio", field: "audio" },
8205
+ document: { method: "sendDocument", field: "document" }
8206
+ };
8207
+ var TelegramProvider = class {
8208
+ http;
8209
+ pollTimeout;
8210
+ /**
8211
+ * @param options - Bot token and API options.
8212
+ */
8213
+ constructor(options) {
8214
+ const base = `${options.apiBase ?? "https://api.telegram.org"}/bot${options.token}`;
8215
+ this.pollTimeout = options.pollTimeoutSeconds ?? 30;
8216
+ this.http = new HTTPClient({
8217
+ baseUrl: base,
8218
+ defaultHeaders: { "content-type": "application/json" },
8219
+ // Timeout must exceed the long-poll window.
8220
+ timeoutMs: (this.pollTimeout + 10) * 1e3
8221
+ });
8222
+ }
8223
+ /** Call a Bot API method, returning the `result`, throwing on `ok: false`. */
8224
+ async call(method, body) {
8225
+ const res = await this.http.post(`/${method}`, { body: JSON.stringify(body) });
8226
+ const data = await res.json().catch(() => ({}));
8227
+ if (!res.ok || !data.ok) {
8228
+ throw new Error(`Telegram ${method} failed: ${data.description ?? res.statusText}`);
8229
+ }
8230
+ return data.result;
8231
+ }
8232
+ async sendText(to, text) {
8233
+ const result = await this.call("sendMessage", {
8234
+ chat_id: to,
8235
+ text
8236
+ });
8237
+ return { id: String(result.message_id), status: "sent" };
8238
+ }
8239
+ async sendMedia(to, media) {
8240
+ const { method, field } = MEDIA_METHOD[media.kind];
8241
+ const body = { chat_id: to, [field]: media.media };
8242
+ if (media.caption !== void 0) body.caption = media.caption;
8243
+ const result = await this.call(method, body);
8244
+ return { id: String(result.message_id), status: "sent" };
8245
+ }
8246
+ async status() {
8247
+ try {
8248
+ await this.call("getMe", {});
8249
+ return "connected";
8250
+ } catch {
8251
+ return "disconnected";
8252
+ }
8253
+ }
8254
+ /**
8255
+ * Subscribe to inbound messages via `getUpdates` long-polling.
8256
+ *
8257
+ * @param handler - Invoked for each inbound text message.
8258
+ * @returns A stop function that ends the polling loop.
8259
+ */
8260
+ async onMessage(handler) {
8261
+ let running = true;
8262
+ let offset = 0;
8263
+ const loop = async () => {
8264
+ while (running) {
8265
+ let updates = [];
8266
+ try {
8267
+ updates = await this.call("getUpdates", {
8268
+ offset,
8269
+ timeout: this.pollTimeout
8270
+ });
8271
+ } catch {
8272
+ if (running) await new Promise((r) => setTimeout(r, 1e3));
8273
+ continue;
8274
+ }
8275
+ for (const update2 of updates) {
8276
+ offset = update2.update_id + 1;
8277
+ const message = update2.message;
8278
+ if (!message) continue;
8279
+ await handler({
8280
+ from: String(message.chat.id),
8281
+ messageId: String(message.message_id),
8282
+ ...typeof message.text === "string" ? { text: message.text } : {},
8283
+ mediaType: null,
8284
+ timestamp: new Date(message.date * 1e3).toISOString(),
8285
+ direction: "incoming"
8286
+ });
8287
+ }
8288
+ }
8289
+ };
8290
+ void loop();
8291
+ return async () => {
8292
+ running = false;
8293
+ };
8294
+ }
8295
+ };
8296
+ var TwilioSmsProvider = class {
8297
+ http;
8298
+ from;
8299
+ messagesPath;
8300
+ accountPath;
8301
+ /**
8302
+ * @param options - Account SID, auth token and default sender.
8303
+ */
8304
+ constructor(options) {
8305
+ this.from = options.from;
8306
+ this.messagesPath = `/2010-04-01/Accounts/${options.accountSid}/Messages.json`;
8307
+ this.accountPath = `/2010-04-01/Accounts/${options.accountSid}.json`;
8308
+ const basic = Buffer.from(`${options.accountSid}:${options.authToken}`).toString(
8309
+ "base64"
8310
+ );
8311
+ this.http = new HTTPClient({
8312
+ baseUrl: options.apiBase ?? "https://api.twilio.com",
8313
+ defaultHeaders: {
8314
+ Authorization: `Basic ${basic}`,
8315
+ "content-type": "application/x-www-form-urlencoded"
8316
+ }
8317
+ });
8318
+ }
8319
+ /** POST a form body to Twilio and parse the JSON, throwing on non-2xx. */
8320
+ async postForm(params) {
8321
+ const res = await this.http.post(this.messagesPath, {
8322
+ body: new URLSearchParams(params).toString()
8323
+ });
8324
+ const data = await res.json().catch(() => ({}));
8325
+ if (!res.ok) {
8326
+ throw new Error(
8327
+ `Twilio send failed (${res.status}): ${data.message ?? res.statusText}`
8328
+ );
8329
+ }
8330
+ return {
8331
+ status: data.status ?? "queued",
8332
+ ...data.sid ? { id: data.sid } : {}
8333
+ };
8334
+ }
8335
+ async sendText(to, text) {
8336
+ return this.postForm({ To: to, From: this.from, Body: text });
8337
+ }
8338
+ async sendMedia(to, media) {
8339
+ return this.postForm({
8340
+ To: to,
8341
+ From: this.from,
8342
+ MediaUrl: media.media,
8343
+ ...media.caption !== void 0 ? { Body: media.caption } : {}
8344
+ });
8345
+ }
8346
+ async status() {
8347
+ const res = await this.http.get(this.accountPath);
8348
+ const data = await res.json().catch(() => ({}));
8349
+ return data.status ?? (res.ok ? "connected" : "disconnected");
8350
+ }
8351
+ };
8352
+ function validateTwilioSignature(authToken, url, params, signature) {
8353
+ const data = url + Object.keys(params).sort().map((key) => key + params[key]).join("");
8354
+ const expected = crypto.createHmac("sha1", authToken).update(data, "utf8").digest("base64");
8355
+ const a = Buffer.from(expected);
8356
+ const b = Buffer.from(signature);
8357
+ return a.length === b.length && crypto.timingSafeEqual(a, b);
8358
+ }
8359
+ function makeTwilioWebhookRouter(options) {
8360
+ const path = options.path ?? "/sms/inbound";
8361
+ const router = express2.Router();
8362
+ router.post(path, async (req, res) => {
8363
+ const body = req.body ?? {};
8364
+ if (options.authToken) {
8365
+ const url = options.publicUrl ?? `${req.protocol}://${req.get("host")}${req.originalUrl}`;
8366
+ const signature = req.header("x-twilio-signature") ?? "";
8367
+ if (!validateTwilioSignature(options.authToken, url, body, signature)) {
8368
+ throw new UnauthorizedException({ message: "Invalid Twilio signature" });
8369
+ }
8370
+ }
8371
+ await options.onMessage({
8372
+ from: String(body.From ?? ""),
8373
+ messageId: String(body.MessageSid ?? ""),
8374
+ ...body.Body ? { text: body.Body } : {},
8375
+ mediaType: null,
8376
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8377
+ direction: "incoming"
8378
+ });
8379
+ res.type("text/xml").send("<Response></Response>");
8380
+ });
8381
+ return router;
8382
+ }
8383
+
8200
8384
  // src/admin/site.ts
8201
8385
  var AdminSite = class {
8202
8386
  /**
@@ -8331,6 +8515,17 @@ var authResponseSchema = zod.z.object({
8331
8515
  user: userPublicSchema,
8332
8516
  tokens: tokenPairSchema
8333
8517
  }).openapi("AuthResponse");
8518
+ var mfaEnrollResponseSchema = zod.z.object({
8519
+ secret: zod.z.string().openapi({ description: "Base32 TOTP secret (manual entry)." }),
8520
+ otpauthUri: zod.z.string().openapi({ description: "otpauth:// URI to render as QR." })
8521
+ }).openapi("MfaEnrollResponse");
8522
+ var mfaCodeSchema = zod.z.object({ code: zod.z.string().min(1).openapi({ description: "Authenticator code." }) }).openapi("MfaCode");
8523
+ var activationSchema = zod.z.object({ token: zod.z.string().min(1).openapi({ description: "Activation token." }) }).openapi("Activation");
8524
+ var passwordResetRequestSchema = zod.z.object({ email: zod.z.string().email().openapi({ description: "Account email." }) }).openapi("PasswordResetRequest");
8525
+ var passwordResetConfirmSchema = zod.z.object({
8526
+ token: zod.z.string().min(1).openapi({ description: "Reset token." }),
8527
+ password: zod.z.string().min(1).openapi({ description: "New plaintext password." })
8528
+ }).openapi("PasswordResetConfirm");
8334
8529
 
8335
8530
  // src/auth/service.ts
8336
8531
  function toPublic(user) {
@@ -8443,6 +8638,178 @@ var UserAuthService = class {
8443
8638
  }
8444
8639
  };
8445
8640
 
8641
+ // src/auth/mfa.ts
8642
+ var MfaService = class {
8643
+ store;
8644
+ totp;
8645
+ /**
8646
+ * @param options - Store and TOTP helper.
8647
+ */
8648
+ constructor(options) {
8649
+ this.store = options.store;
8650
+ this.totp = options.totp;
8651
+ }
8652
+ /**
8653
+ * Begin enrollment: generate and persist a secret, return the QR URI.
8654
+ *
8655
+ * @param userId - The enrolling user.
8656
+ * @param accountName - Label shown in the authenticator (usually the email).
8657
+ * @returns The secret and provisioning URI.
8658
+ */
8659
+ async enroll(userId, accountName) {
8660
+ const secret = this.totp.generateSecret();
8661
+ await this.store.setSecret(userId, secret);
8662
+ await this.store.setEnabled(userId, false);
8663
+ return { secret, otpauthUri: this.totp.provisioningUri(secret, accountName) };
8664
+ }
8665
+ /**
8666
+ * Confirm enrollment by verifying a code, enabling MFA on success.
8667
+ *
8668
+ * @param userId - The user.
8669
+ * @param code - The 6-digit code from the authenticator.
8670
+ * @throws {ValidationException} When no secret is pending or the code is wrong.
8671
+ */
8672
+ async confirm(userId, code) {
8673
+ const secret = await this.store.getSecret(userId);
8674
+ if (!secret) throw new ValidationException({ message: "MFA not initialized" });
8675
+ if (!this.totp.verify(secret, code)) {
8676
+ throw new ValidationException({ message: "Invalid MFA code" });
8677
+ }
8678
+ await this.store.setEnabled(userId, true);
8679
+ }
8680
+ /**
8681
+ * Verify a code (login step). Returns `false` without throwing.
8682
+ *
8683
+ * @param userId - The user.
8684
+ * @param code - The submitted code.
8685
+ * @returns `true` when the code is valid.
8686
+ */
8687
+ async verify(userId, code) {
8688
+ const secret = await this.store.getSecret(userId);
8689
+ return secret ? this.totp.verify(secret, code) : false;
8690
+ }
8691
+ /**
8692
+ * Disable MFA after verifying a code.
8693
+ *
8694
+ * @param userId - The user.
8695
+ * @param code - The submitted code.
8696
+ * @throws {ValidationException} When the code is invalid.
8697
+ */
8698
+ async disable(userId, code) {
8699
+ if (!await this.verify(userId, code)) {
8700
+ throw new ValidationException({ message: "Invalid MFA code" });
8701
+ }
8702
+ await this.store.setEnabled(userId, false);
8703
+ await this.store.setSecret(userId, "");
8704
+ }
8705
+ };
8706
+
8707
+ // src/auth/activation.ts
8708
+ var ActivationService = class {
8709
+ store;
8710
+ ttlSeconds;
8711
+ /**
8712
+ * @param options - Store and token TTL.
8713
+ */
8714
+ constructor(options) {
8715
+ this.store = options.store;
8716
+ this.ttlSeconds = options.ttlSeconds ?? 60 * 60 * 24;
8717
+ }
8718
+ /**
8719
+ * Start activation: issue a token and persist its hash.
8720
+ *
8721
+ * @param userId - The user to activate.
8722
+ * @returns The one-time plaintext token (embed in the activation link).
8723
+ */
8724
+ async start(userId) {
8725
+ const { plaintext, tokenHash } = generateOpaqueToken();
8726
+ await this.store.saveActivationToken(
8727
+ userId,
8728
+ tokenHash,
8729
+ Date.now() + this.ttlSeconds * 1e3
8730
+ );
8731
+ return plaintext;
8732
+ }
8733
+ /**
8734
+ * Activate an account from a token.
8735
+ *
8736
+ * @param token - The plaintext token from the activation link.
8737
+ * @returns The activated user id.
8738
+ * @throws {InvalidTokenException} When the token is unknown or expired.
8739
+ */
8740
+ async activate(token) {
8741
+ const tokenHash = hashOpaqueToken(token);
8742
+ const record = await this.store.findActivationToken(tokenHash);
8743
+ if (!record || record.expiresAt <= Date.now()) {
8744
+ throw new InvalidTokenException({ message: "Invalid or expired activation token" });
8745
+ }
8746
+ await this.store.activate(record.userId);
8747
+ await this.store.clearActivationToken(tokenHash);
8748
+ return record.userId;
8749
+ }
8750
+ };
8751
+
8752
+ // src/auth/passwordReset.ts
8753
+ var PasswordResetService = class {
8754
+ store;
8755
+ password;
8756
+ ttlSeconds;
8757
+ passwordMinLength;
8758
+ /**
8759
+ * @param options - Store, password hasher and policy.
8760
+ */
8761
+ constructor(options) {
8762
+ this.store = options.store;
8763
+ this.password = options.password;
8764
+ this.ttlSeconds = options.ttlSeconds ?? 3600;
8765
+ this.passwordMinLength = options.passwordMinLength ?? 12;
8766
+ }
8767
+ /**
8768
+ * Request a reset for `email`.
8769
+ *
8770
+ * Returns the plaintext token only when the email maps to a user; otherwise
8771
+ * `null`. Callers should respond with the same success shape either way to
8772
+ * avoid user enumeration — email the token only when present.
8773
+ *
8774
+ * @param email - The account email.
8775
+ * @returns The one-time token, or `null` when no user matches.
8776
+ */
8777
+ async request(email) {
8778
+ const userId = await this.store.findUserIdByEmail(email.toLowerCase());
8779
+ if (!userId) return null;
8780
+ const { plaintext, tokenHash } = generateOpaqueToken();
8781
+ await this.store.saveResetToken(
8782
+ userId,
8783
+ tokenHash,
8784
+ Date.now() + this.ttlSeconds * 1e3
8785
+ );
8786
+ return plaintext;
8787
+ }
8788
+ /**
8789
+ * Confirm a reset: validate the token + new password and rehash.
8790
+ *
8791
+ * @param token - The plaintext reset token.
8792
+ * @param newPassword - The new plaintext password.
8793
+ * @throws {ValidationException} When the new password is too short.
8794
+ * @throws {InvalidTokenException} When the token is unknown or expired.
8795
+ */
8796
+ async confirm(token, newPassword) {
8797
+ if (newPassword.length < this.passwordMinLength) {
8798
+ throw new ValidationException({
8799
+ message: `Password must be at least ${this.passwordMinLength} characters`,
8800
+ details: { minLength: this.passwordMinLength }
8801
+ });
8802
+ }
8803
+ const tokenHash = hashOpaqueToken(token);
8804
+ const record = await this.store.findResetToken(tokenHash);
8805
+ if (!record || record.expiresAt <= Date.now()) {
8806
+ throw new InvalidTokenException({ message: "Invalid or expired reset token" });
8807
+ }
8808
+ await this.store.updatePassword(record.userId, await this.password.hash(newPassword));
8809
+ await this.store.clearResetToken(tokenHash);
8810
+ }
8811
+ };
8812
+
8446
8813
  // src/auth/middleware.ts
8447
8814
  function bearerToken(req) {
8448
8815
  const header = req.header("authorization");
@@ -8545,6 +8912,53 @@ function makeAuthRouter(options) {
8545
8912
  }
8546
8913
  res.json(claims);
8547
8914
  });
8915
+ if (options.activation) {
8916
+ const activation = options.activation;
8917
+ router.post(`${prefix}/activate`, async (req, res) => {
8918
+ const { token } = activationSchema.parse(req.body);
8919
+ const userId = await activation.activate(token);
8920
+ res.json({ activated: true, userId });
8921
+ });
8922
+ }
8923
+ if (options.passwordReset) {
8924
+ const reset = options.passwordReset;
8925
+ router.post(`${prefix}/password-reset/request`, async (req, res) => {
8926
+ const { email } = passwordResetRequestSchema.parse(req.body);
8927
+ const token = await reset.request(email);
8928
+ res.status(202).json({ requested: true, ...token ? { token } : {} });
8929
+ });
8930
+ router.post(`${prefix}/password-reset/confirm`, async (req, res) => {
8931
+ const { token, password } = passwordResetConfirmSchema.parse(req.body);
8932
+ await reset.confirm(token, password);
8933
+ res.json({ reset: true });
8934
+ });
8935
+ }
8936
+ if (options.mfa) {
8937
+ const mfa = options.mfa;
8938
+ const requireUser = (req) => {
8939
+ const claims = getAuth(req);
8940
+ if (!claims || typeof claims.sub !== "string") {
8941
+ throw new UnauthorizedException({ message: "Not authenticated" });
8942
+ }
8943
+ return claims.sub;
8944
+ };
8945
+ router.post(`${prefix}/mfa/enroll`, makeJwtAuthMiddleware(jwt), async (req, res) => {
8946
+ const claims = getAuth(req);
8947
+ const userId = requireUser(req);
8948
+ const label = typeof claims?.email === "string" ? claims.email : userId;
8949
+ res.json(await mfa.enroll(userId, label));
8950
+ });
8951
+ router.post(`${prefix}/mfa/confirm`, makeJwtAuthMiddleware(jwt), async (req, res) => {
8952
+ const { code } = mfaCodeSchema.parse(req.body);
8953
+ await mfa.confirm(requireUser(req), code);
8954
+ res.json({ enabled: true });
8955
+ });
8956
+ router.post(`${prefix}/mfa/disable`, makeJwtAuthMiddleware(jwt), async (req, res) => {
8957
+ const { code } = mfaCodeSchema.parse(req.body);
8958
+ await mfa.disable(requireUser(req), code);
8959
+ res.json({ enabled: false });
8960
+ });
8961
+ }
8548
8962
  return router;
8549
8963
  }
8550
8964
  var logger2 = new JSONLogger("tempest_express_sdk.api.handlers");
@@ -8813,7 +9227,7 @@ function runServer(app, options = {}) {
8813
9227
  }
8814
9228
 
8815
9229
  // src/version.ts
8816
- var VERSION = "0.5.0";
9230
+ var VERSION = "0.7.0";
8817
9231
 
8818
9232
  Object.defineProperty(exports, "OpenAPIRegistry", {
8819
9233
  enumerable: true,
@@ -8967,6 +9381,7 @@ Object.defineProperty(exports, "update", {
8967
9381
  enumerable: true,
8968
9382
  get: function () { return tempestDbJs.update; }
8969
9383
  });
9384
+ exports.ActivationService = ActivationService;
8970
9385
  exports.AdminSite = AdminSite;
8971
9386
  exports.AppException = AppException;
8972
9387
  exports.AttemptThrottle = AttemptThrottle;
@@ -8999,8 +9414,10 @@ exports.MemorySessionStore = MemorySessionStore;
8999
9414
  exports.MemoryThrottleBackend = MemoryThrottleBackend;
9000
9415
  exports.MessageCatalog = MessageCatalog;
9001
9416
  exports.MetricsUtils = MetricsUtils;
9417
+ exports.MfaService = MfaService;
9002
9418
  exports.NotFoundException = NotFoundException;
9003
9419
  exports.PHONE_BR_PATTERN = PHONE_BR_PATTERN;
9420
+ exports.PasswordResetService = PasswordResetService;
9004
9421
  exports.PasswordUtils = PasswordUtils;
9005
9422
  exports.REQUEST_ID_HEADER = REQUEST_ID_HEADER;
9006
9423
  exports.RabbitBroker = RabbitBroker;
@@ -9012,7 +9429,9 @@ exports.ServerSentEvent = ServerSentEvent;
9012
9429
  exports.SessionService = SessionService;
9013
9430
  exports.TOTPHelper = TOTPHelper;
9014
9431
  exports.TaskManager = TaskManager;
9432
+ exports.TelegramProvider = TelegramProvider;
9015
9433
  exports.TooManyRequestsException = TooManyRequestsException;
9434
+ exports.TwilioSmsProvider = TwilioSmsProvider;
9016
9435
  exports.UF = UF;
9017
9436
  exports.UnauthorizedException = UnauthorizedException;
9018
9437
  exports.UserAuthService = UserAuthService;
@@ -9023,6 +9442,7 @@ exports.WebPushError = WebPushError;
9023
9442
  exports.WebPushGoneError = WebPushGoneError;
9024
9443
  exports.WebSocketHub = WebSocketHub;
9025
9444
  exports.WhatsAppProvider = WhatsAppProvider;
9445
+ exports.activationSchema = activationSchema;
9026
9446
  exports.attachWebSocketHub = attachWebSocketHub;
9027
9447
  exports.authResponseSchema = authResponseSchema;
9028
9448
  exports.baseAppSettingsSchema = baseAppSettingsSchema;
@@ -9077,8 +9497,11 @@ exports.makeFlagGuard = makeFlagGuard;
9077
9497
  exports.makeHealthRouter = makeHealthRouter;
9078
9498
  exports.makeJwtAuthMiddleware = makeJwtAuthMiddleware;
9079
9499
  exports.makeSessionMiddleware = makeSessionMiddleware;
9500
+ exports.makeTwilioWebhookRouter = makeTwilioWebhookRouter;
9080
9501
  exports.makeUnhandledExceptionHandler = makeUnhandledExceptionHandler;
9081
9502
  exports.makeWhatsAppWebhookRouter = makeWhatsAppWebhookRouter;
9503
+ exports.mfaCodeSchema = mfaCodeSchema;
9504
+ exports.mfaEnrollResponseSchema = mfaEnrollResponseSchema;
9082
9505
  exports.modifyDict = modifyDict;
9083
9506
  exports.mountOpenApiJson = mountOpenApiJson;
9084
9507
  exports.mountRedoc = mountRedoc;
@@ -9095,6 +9518,8 @@ exports.paginationFilterSchema = paginationFilterSchema;
9095
9518
  exports.paginationSchema = paginationSchema;
9096
9519
  exports.parseAcceptLanguage = parseAcceptLanguage;
9097
9520
  exports.parseCookies = parseCookies;
9521
+ exports.passwordResetConfirmSchema = passwordResetConfirmSchema;
9522
+ exports.passwordResetRequestSchema = passwordResetRequestSchema;
9098
9523
  exports.phoneBrField = phoneBrField;
9099
9524
  exports.refreshSchema = refreshSchema;
9100
9525
  exports.registerExceptionHandlers = registerExceptionHandlers;
@@ -9117,6 +9542,7 @@ exports.ufField = ufField;
9117
9542
  exports.updatedByColumn = updatedByColumn;
9118
9543
  exports.userPublicSchema = userPublicSchema;
9119
9544
  exports.utcnow = utcnow;
9545
+ exports.validateTwilioSignature = validateTwilioSignature;
9120
9546
  exports.verifyOpaqueToken = verifyOpaqueToken;
9121
9547
  exports.webPushKeysSchema = webPushKeysSchema;
9122
9548
  exports.webPushPayloadSchema = webPushPayloadSchema;