tempest-express-sdk 0.6.0 → 0.8.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
@@ -7329,6 +7329,76 @@ function makeSessionMiddleware(service, options = {}) {
7329
7329
  };
7330
7330
  }
7331
7331
 
7332
+ // src/sessions/redisStore.ts
7333
+ var RedisSessionStore = class {
7334
+ /**
7335
+ * @param client - A connected node-redis v4 (or compatible) client.
7336
+ * @param prefix - Key prefix. Default `sess:`.
7337
+ */
7338
+ constructor(client, prefix = "sess:") {
7339
+ this.client = client;
7340
+ this.prefix = prefix;
7341
+ }
7342
+ client;
7343
+ prefix;
7344
+ key(idHash) {
7345
+ return `${this.prefix}${idHash}`;
7346
+ }
7347
+ userKey(userId) {
7348
+ return `${this.prefix}user:${userId}`;
7349
+ }
7350
+ async get(idHash) {
7351
+ const raw = await this.client.get(this.key(idHash));
7352
+ if (raw === null) return null;
7353
+ const session = JSON.parse(raw);
7354
+ if (session.expiresAt <= Date.now()) {
7355
+ await this.delete(idHash);
7356
+ return null;
7357
+ }
7358
+ return session;
7359
+ }
7360
+ async set(session) {
7361
+ const ttlSeconds = Math.max(1, Math.ceil((session.expiresAt - Date.now()) / 1e3));
7362
+ await this.client.set(this.key(session.idHash), JSON.stringify(session), {
7363
+ EX: ttlSeconds
7364
+ });
7365
+ await this.client.sAdd(this.userKey(session.userId), session.idHash);
7366
+ }
7367
+ async delete(idHash) {
7368
+ const raw = await this.client.get(this.key(idHash));
7369
+ await this.client.del(this.key(idHash));
7370
+ if (raw) {
7371
+ const session = JSON.parse(raw);
7372
+ await this.client.sRem(this.userKey(session.userId), idHash);
7373
+ }
7374
+ }
7375
+ async deleteByUser(userId) {
7376
+ const ids = await this.client.sMembers(this.userKey(userId));
7377
+ let count = 0;
7378
+ for (const idHash of ids) {
7379
+ await this.client.del(this.key(idHash));
7380
+ await this.client.sRem(this.userKey(userId), idHash);
7381
+ count += 1;
7382
+ }
7383
+ return count;
7384
+ }
7385
+ async listByUser(userId) {
7386
+ const ids = await this.client.sMembers(this.userKey(userId));
7387
+ const sessions = [];
7388
+ const now = Date.now();
7389
+ for (const idHash of ids) {
7390
+ const raw = await this.client.get(this.key(idHash));
7391
+ if (raw === null) {
7392
+ await this.client.sRem(this.userKey(userId), idHash);
7393
+ continue;
7394
+ }
7395
+ const session = JSON.parse(raw);
7396
+ if (session.expiresAt > now) sessions.push(session);
7397
+ }
7398
+ return sessions.sort((a, b) => a.createdAt - b.createdAt);
7399
+ }
7400
+ };
7401
+
7332
7402
  // src/sse/eventStream.ts
7333
7403
  var ServerSentEvent = class {
7334
7404
  constructor(init) {
@@ -7501,6 +7571,92 @@ var SSEBroker = class {
7501
7571
  }
7502
7572
  };
7503
7573
 
7574
+ // src/sse/redisBroker.ts
7575
+ var RedisSSEBroker = class {
7576
+ /**
7577
+ * @param publisher - The main Redis client (used to `publish`).
7578
+ * @param subscriber - A dedicated subscriber connection (`client.duplicate()`).
7579
+ * @param options - Channel prefix + per-stream options.
7580
+ */
7581
+ constructor(publisher, subscriber, options = {}) {
7582
+ this.publisher = publisher;
7583
+ this.subscriber = subscriber;
7584
+ this.prefix = options.prefix ?? "sse:";
7585
+ const { prefix: _p, ...streamOptions } = options;
7586
+ this.streamOptions = streamOptions;
7587
+ }
7588
+ publisher;
7589
+ subscriber;
7590
+ local = /* @__PURE__ */ new Map();
7591
+ prefix;
7592
+ streamOptions;
7593
+ channelKey(channel) {
7594
+ return `${this.prefix}${channel}`;
7595
+ }
7596
+ /** Emit a decoded payload to every local stream on a channel. */
7597
+ emitLocal(channel, data, event) {
7598
+ const set = this.local.get(channel);
7599
+ if (!set) return;
7600
+ for (const stream of set) stream.publish(data, event);
7601
+ }
7602
+ /**
7603
+ * Register a subscriber stream, subscribing to the Redis channel on first use.
7604
+ *
7605
+ * @param channel - The channel name.
7606
+ * @returns A fresh {@link EventStream} to serve to the client.
7607
+ */
7608
+ async register(channel) {
7609
+ const stream = new EventStream(this.streamOptions);
7610
+ let set = this.local.get(channel);
7611
+ if (!set) {
7612
+ set = /* @__PURE__ */ new Set();
7613
+ this.local.set(channel, set);
7614
+ await this.subscriber.subscribe(this.channelKey(channel), (raw) => {
7615
+ try {
7616
+ const { data, event } = JSON.parse(raw);
7617
+ this.emitLocal(channel, data, event);
7618
+ } catch {
7619
+ }
7620
+ });
7621
+ }
7622
+ set.add(stream);
7623
+ return stream;
7624
+ }
7625
+ /**
7626
+ * Remove a subscriber stream; unsubscribe from Redis when the last leaves.
7627
+ *
7628
+ * @param channel - The channel name.
7629
+ * @param stream - The stream to remove.
7630
+ */
7631
+ async unregister(channel, stream) {
7632
+ const set = this.local.get(channel);
7633
+ if (!set) return;
7634
+ set.delete(stream);
7635
+ stream.close();
7636
+ if (set.size === 0) {
7637
+ this.local.delete(channel);
7638
+ await this.subscriber.unsubscribe(this.channelKey(channel));
7639
+ }
7640
+ }
7641
+ /** Local subscriber count on `channel` (this replica only). */
7642
+ localSubscribers(channel) {
7643
+ return this.local.get(channel)?.size ?? 0;
7644
+ }
7645
+ /**
7646
+ * Publish to every subscriber across all replicas.
7647
+ *
7648
+ * @param channel - The channel name.
7649
+ * @param data - The payload (JSON-encoded).
7650
+ * @param event - Optional event name.
7651
+ */
7652
+ async publish(channel, data, event) {
7653
+ await this.publisher.publish(
7654
+ this.channelKey(channel),
7655
+ JSON.stringify({ data, ...event ? { event } : {} })
7656
+ );
7657
+ }
7658
+ };
7659
+
7504
7660
  // src/websockets/schemas.ts
7505
7661
  var wsEnvelopeSchema = zod.z.object({
7506
7662
  type: zod.z.string().openapi({ description: "Message type discriminator." }),
@@ -8515,6 +8671,17 @@ var authResponseSchema = zod.z.object({
8515
8671
  user: userPublicSchema,
8516
8672
  tokens: tokenPairSchema
8517
8673
  }).openapi("AuthResponse");
8674
+ var mfaEnrollResponseSchema = zod.z.object({
8675
+ secret: zod.z.string().openapi({ description: "Base32 TOTP secret (manual entry)." }),
8676
+ otpauthUri: zod.z.string().openapi({ description: "otpauth:// URI to render as QR." })
8677
+ }).openapi("MfaEnrollResponse");
8678
+ var mfaCodeSchema = zod.z.object({ code: zod.z.string().min(1).openapi({ description: "Authenticator code." }) }).openapi("MfaCode");
8679
+ var activationSchema = zod.z.object({ token: zod.z.string().min(1).openapi({ description: "Activation token." }) }).openapi("Activation");
8680
+ var passwordResetRequestSchema = zod.z.object({ email: zod.z.string().email().openapi({ description: "Account email." }) }).openapi("PasswordResetRequest");
8681
+ var passwordResetConfirmSchema = zod.z.object({
8682
+ token: zod.z.string().min(1).openapi({ description: "Reset token." }),
8683
+ password: zod.z.string().min(1).openapi({ description: "New plaintext password." })
8684
+ }).openapi("PasswordResetConfirm");
8518
8685
 
8519
8686
  // src/auth/service.ts
8520
8687
  function toPublic(user) {
@@ -8627,6 +8794,178 @@ var UserAuthService = class {
8627
8794
  }
8628
8795
  };
8629
8796
 
8797
+ // src/auth/mfa.ts
8798
+ var MfaService = class {
8799
+ store;
8800
+ totp;
8801
+ /**
8802
+ * @param options - Store and TOTP helper.
8803
+ */
8804
+ constructor(options) {
8805
+ this.store = options.store;
8806
+ this.totp = options.totp;
8807
+ }
8808
+ /**
8809
+ * Begin enrollment: generate and persist a secret, return the QR URI.
8810
+ *
8811
+ * @param userId - The enrolling user.
8812
+ * @param accountName - Label shown in the authenticator (usually the email).
8813
+ * @returns The secret and provisioning URI.
8814
+ */
8815
+ async enroll(userId, accountName) {
8816
+ const secret = this.totp.generateSecret();
8817
+ await this.store.setSecret(userId, secret);
8818
+ await this.store.setEnabled(userId, false);
8819
+ return { secret, otpauthUri: this.totp.provisioningUri(secret, accountName) };
8820
+ }
8821
+ /**
8822
+ * Confirm enrollment by verifying a code, enabling MFA on success.
8823
+ *
8824
+ * @param userId - The user.
8825
+ * @param code - The 6-digit code from the authenticator.
8826
+ * @throws {ValidationException} When no secret is pending or the code is wrong.
8827
+ */
8828
+ async confirm(userId, code) {
8829
+ const secret = await this.store.getSecret(userId);
8830
+ if (!secret) throw new ValidationException({ message: "MFA not initialized" });
8831
+ if (!this.totp.verify(secret, code)) {
8832
+ throw new ValidationException({ message: "Invalid MFA code" });
8833
+ }
8834
+ await this.store.setEnabled(userId, true);
8835
+ }
8836
+ /**
8837
+ * Verify a code (login step). Returns `false` without throwing.
8838
+ *
8839
+ * @param userId - The user.
8840
+ * @param code - The submitted code.
8841
+ * @returns `true` when the code is valid.
8842
+ */
8843
+ async verify(userId, code) {
8844
+ const secret = await this.store.getSecret(userId);
8845
+ return secret ? this.totp.verify(secret, code) : false;
8846
+ }
8847
+ /**
8848
+ * Disable MFA after verifying a code.
8849
+ *
8850
+ * @param userId - The user.
8851
+ * @param code - The submitted code.
8852
+ * @throws {ValidationException} When the code is invalid.
8853
+ */
8854
+ async disable(userId, code) {
8855
+ if (!await this.verify(userId, code)) {
8856
+ throw new ValidationException({ message: "Invalid MFA code" });
8857
+ }
8858
+ await this.store.setEnabled(userId, false);
8859
+ await this.store.setSecret(userId, "");
8860
+ }
8861
+ };
8862
+
8863
+ // src/auth/activation.ts
8864
+ var ActivationService = class {
8865
+ store;
8866
+ ttlSeconds;
8867
+ /**
8868
+ * @param options - Store and token TTL.
8869
+ */
8870
+ constructor(options) {
8871
+ this.store = options.store;
8872
+ this.ttlSeconds = options.ttlSeconds ?? 60 * 60 * 24;
8873
+ }
8874
+ /**
8875
+ * Start activation: issue a token and persist its hash.
8876
+ *
8877
+ * @param userId - The user to activate.
8878
+ * @returns The one-time plaintext token (embed in the activation link).
8879
+ */
8880
+ async start(userId) {
8881
+ const { plaintext, tokenHash } = generateOpaqueToken();
8882
+ await this.store.saveActivationToken(
8883
+ userId,
8884
+ tokenHash,
8885
+ Date.now() + this.ttlSeconds * 1e3
8886
+ );
8887
+ return plaintext;
8888
+ }
8889
+ /**
8890
+ * Activate an account from a token.
8891
+ *
8892
+ * @param token - The plaintext token from the activation link.
8893
+ * @returns The activated user id.
8894
+ * @throws {InvalidTokenException} When the token is unknown or expired.
8895
+ */
8896
+ async activate(token) {
8897
+ const tokenHash = hashOpaqueToken(token);
8898
+ const record = await this.store.findActivationToken(tokenHash);
8899
+ if (!record || record.expiresAt <= Date.now()) {
8900
+ throw new InvalidTokenException({ message: "Invalid or expired activation token" });
8901
+ }
8902
+ await this.store.activate(record.userId);
8903
+ await this.store.clearActivationToken(tokenHash);
8904
+ return record.userId;
8905
+ }
8906
+ };
8907
+
8908
+ // src/auth/passwordReset.ts
8909
+ var PasswordResetService = class {
8910
+ store;
8911
+ password;
8912
+ ttlSeconds;
8913
+ passwordMinLength;
8914
+ /**
8915
+ * @param options - Store, password hasher and policy.
8916
+ */
8917
+ constructor(options) {
8918
+ this.store = options.store;
8919
+ this.password = options.password;
8920
+ this.ttlSeconds = options.ttlSeconds ?? 3600;
8921
+ this.passwordMinLength = options.passwordMinLength ?? 12;
8922
+ }
8923
+ /**
8924
+ * Request a reset for `email`.
8925
+ *
8926
+ * Returns the plaintext token only when the email maps to a user; otherwise
8927
+ * `null`. Callers should respond with the same success shape either way to
8928
+ * avoid user enumeration — email the token only when present.
8929
+ *
8930
+ * @param email - The account email.
8931
+ * @returns The one-time token, or `null` when no user matches.
8932
+ */
8933
+ async request(email) {
8934
+ const userId = await this.store.findUserIdByEmail(email.toLowerCase());
8935
+ if (!userId) return null;
8936
+ const { plaintext, tokenHash } = generateOpaqueToken();
8937
+ await this.store.saveResetToken(
8938
+ userId,
8939
+ tokenHash,
8940
+ Date.now() + this.ttlSeconds * 1e3
8941
+ );
8942
+ return plaintext;
8943
+ }
8944
+ /**
8945
+ * Confirm a reset: validate the token + new password and rehash.
8946
+ *
8947
+ * @param token - The plaintext reset token.
8948
+ * @param newPassword - The new plaintext password.
8949
+ * @throws {ValidationException} When the new password is too short.
8950
+ * @throws {InvalidTokenException} When the token is unknown or expired.
8951
+ */
8952
+ async confirm(token, newPassword) {
8953
+ if (newPassword.length < this.passwordMinLength) {
8954
+ throw new ValidationException({
8955
+ message: `Password must be at least ${this.passwordMinLength} characters`,
8956
+ details: { minLength: this.passwordMinLength }
8957
+ });
8958
+ }
8959
+ const tokenHash = hashOpaqueToken(token);
8960
+ const record = await this.store.findResetToken(tokenHash);
8961
+ if (!record || record.expiresAt <= Date.now()) {
8962
+ throw new InvalidTokenException({ message: "Invalid or expired reset token" });
8963
+ }
8964
+ await this.store.updatePassword(record.userId, await this.password.hash(newPassword));
8965
+ await this.store.clearResetToken(tokenHash);
8966
+ }
8967
+ };
8968
+
8630
8969
  // src/auth/middleware.ts
8631
8970
  function bearerToken(req) {
8632
8971
  const header = req.header("authorization");
@@ -8729,6 +9068,53 @@ function makeAuthRouter(options) {
8729
9068
  }
8730
9069
  res.json(claims);
8731
9070
  });
9071
+ if (options.activation) {
9072
+ const activation = options.activation;
9073
+ router.post(`${prefix}/activate`, async (req, res) => {
9074
+ const { token } = activationSchema.parse(req.body);
9075
+ const userId = await activation.activate(token);
9076
+ res.json({ activated: true, userId });
9077
+ });
9078
+ }
9079
+ if (options.passwordReset) {
9080
+ const reset = options.passwordReset;
9081
+ router.post(`${prefix}/password-reset/request`, async (req, res) => {
9082
+ const { email } = passwordResetRequestSchema.parse(req.body);
9083
+ const token = await reset.request(email);
9084
+ res.status(202).json({ requested: true, ...token ? { token } : {} });
9085
+ });
9086
+ router.post(`${prefix}/password-reset/confirm`, async (req, res) => {
9087
+ const { token, password } = passwordResetConfirmSchema.parse(req.body);
9088
+ await reset.confirm(token, password);
9089
+ res.json({ reset: true });
9090
+ });
9091
+ }
9092
+ if (options.mfa) {
9093
+ const mfa = options.mfa;
9094
+ const requireUser = (req) => {
9095
+ const claims = getAuth(req);
9096
+ if (!claims || typeof claims.sub !== "string") {
9097
+ throw new UnauthorizedException({ message: "Not authenticated" });
9098
+ }
9099
+ return claims.sub;
9100
+ };
9101
+ router.post(`${prefix}/mfa/enroll`, makeJwtAuthMiddleware(jwt), async (req, res) => {
9102
+ const claims = getAuth(req);
9103
+ const userId = requireUser(req);
9104
+ const label = typeof claims?.email === "string" ? claims.email : userId;
9105
+ res.json(await mfa.enroll(userId, label));
9106
+ });
9107
+ router.post(`${prefix}/mfa/confirm`, makeJwtAuthMiddleware(jwt), async (req, res) => {
9108
+ const { code } = mfaCodeSchema.parse(req.body);
9109
+ await mfa.confirm(requireUser(req), code);
9110
+ res.json({ enabled: true });
9111
+ });
9112
+ router.post(`${prefix}/mfa/disable`, makeJwtAuthMiddleware(jwt), async (req, res) => {
9113
+ const { code } = mfaCodeSchema.parse(req.body);
9114
+ await mfa.disable(requireUser(req), code);
9115
+ res.json({ enabled: false });
9116
+ });
9117
+ }
8732
9118
  return router;
8733
9119
  }
8734
9120
  var logger2 = new JSONLogger("tempest_express_sdk.api.handlers");
@@ -8997,7 +9383,7 @@ function runServer(app, options = {}) {
8997
9383
  }
8998
9384
 
8999
9385
  // src/version.ts
9000
- var VERSION = "0.6.0";
9386
+ var VERSION = "0.8.0";
9001
9387
 
9002
9388
  Object.defineProperty(exports, "OpenAPIRegistry", {
9003
9389
  enumerable: true,
@@ -9151,6 +9537,7 @@ Object.defineProperty(exports, "update", {
9151
9537
  enumerable: true,
9152
9538
  get: function () { return tempestDbJs.update; }
9153
9539
  });
9540
+ exports.ActivationService = ActivationService;
9154
9541
  exports.AdminSite = AdminSite;
9155
9542
  exports.AppException = AppException;
9156
9543
  exports.AttemptThrottle = AttemptThrottle;
@@ -9183,12 +9570,16 @@ exports.MemorySessionStore = MemorySessionStore;
9183
9570
  exports.MemoryThrottleBackend = MemoryThrottleBackend;
9184
9571
  exports.MessageCatalog = MessageCatalog;
9185
9572
  exports.MetricsUtils = MetricsUtils;
9573
+ exports.MfaService = MfaService;
9186
9574
  exports.NotFoundException = NotFoundException;
9187
9575
  exports.PHONE_BR_PATTERN = PHONE_BR_PATTERN;
9576
+ exports.PasswordResetService = PasswordResetService;
9188
9577
  exports.PasswordUtils = PasswordUtils;
9189
9578
  exports.REQUEST_ID_HEADER = REQUEST_ID_HEADER;
9190
9579
  exports.RabbitBroker = RabbitBroker;
9191
9580
  exports.RedisCacheManager = RedisCacheManager;
9581
+ exports.RedisSSEBroker = RedisSSEBroker;
9582
+ exports.RedisSessionStore = RedisSessionStore;
9192
9583
  exports.Region = Region;
9193
9584
  exports.RetryPolicy = RetryPolicy;
9194
9585
  exports.SSEBroker = SSEBroker;
@@ -9209,6 +9600,7 @@ exports.WebPushError = WebPushError;
9209
9600
  exports.WebPushGoneError = WebPushGoneError;
9210
9601
  exports.WebSocketHub = WebSocketHub;
9211
9602
  exports.WhatsAppProvider = WhatsAppProvider;
9603
+ exports.activationSchema = activationSchema;
9212
9604
  exports.attachWebSocketHub = attachWebSocketHub;
9213
9605
  exports.authResponseSchema = authResponseSchema;
9214
9606
  exports.baseAppSettingsSchema = baseAppSettingsSchema;
@@ -9266,6 +9658,8 @@ exports.makeSessionMiddleware = makeSessionMiddleware;
9266
9658
  exports.makeTwilioWebhookRouter = makeTwilioWebhookRouter;
9267
9659
  exports.makeUnhandledExceptionHandler = makeUnhandledExceptionHandler;
9268
9660
  exports.makeWhatsAppWebhookRouter = makeWhatsAppWebhookRouter;
9661
+ exports.mfaCodeSchema = mfaCodeSchema;
9662
+ exports.mfaEnrollResponseSchema = mfaEnrollResponseSchema;
9269
9663
  exports.modifyDict = modifyDict;
9270
9664
  exports.mountOpenApiJson = mountOpenApiJson;
9271
9665
  exports.mountRedoc = mountRedoc;
@@ -9282,6 +9676,8 @@ exports.paginationFilterSchema = paginationFilterSchema;
9282
9676
  exports.paginationSchema = paginationSchema;
9283
9677
  exports.parseAcceptLanguage = parseAcceptLanguage;
9284
9678
  exports.parseCookies = parseCookies;
9679
+ exports.passwordResetConfirmSchema = passwordResetConfirmSchema;
9680
+ exports.passwordResetRequestSchema = passwordResetRequestSchema;
9285
9681
  exports.phoneBrField = phoneBrField;
9286
9682
  exports.refreshSchema = refreshSchema;
9287
9683
  exports.registerExceptionHandlers = registerExceptionHandlers;