tempest-express-sdk 0.1.0 → 0.2.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
@@ -5,6 +5,8 @@ var zodToOpenapi = require('@asteasolutions/zod-to-openapi');
5
5
  var zod = require('zod');
6
6
  var tempestDbJs = require('tempest-db-js');
7
7
  var crypto = require('crypto');
8
+ var promises = require('fs/promises');
9
+ var path = require('path');
8
10
  var express2 = require('express');
9
11
  var swaggerUiDist = require('swagger-ui-dist');
10
12
 
@@ -6753,6 +6755,846 @@ var AttemptThrottle = class {
6753
6755
  }
6754
6756
  };
6755
6757
 
6758
+ // src/cache/manager.ts
6759
+ var MemoryCacheManager = class {
6760
+ store = /* @__PURE__ */ new Map();
6761
+ live(key) {
6762
+ const entry = this.store.get(key);
6763
+ if (!entry) return null;
6764
+ if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
6765
+ this.store.delete(key);
6766
+ return null;
6767
+ }
6768
+ return entry;
6769
+ }
6770
+ async get(key) {
6771
+ const entry = this.live(key);
6772
+ return entry ? JSON.parse(entry.value) : null;
6773
+ }
6774
+ async set(key, value, ttlSeconds) {
6775
+ this.store.set(key, {
6776
+ value: JSON.stringify(value),
6777
+ expiresAt: ttlSeconds !== void 0 ? Date.now() + ttlSeconds * 1e3 : null
6778
+ });
6779
+ }
6780
+ async delete(key) {
6781
+ this.store.delete(key);
6782
+ }
6783
+ async has(key) {
6784
+ return this.live(key) !== null;
6785
+ }
6786
+ async clear() {
6787
+ this.store.clear();
6788
+ }
6789
+ };
6790
+ var RedisCacheManager = class {
6791
+ /**
6792
+ * @param client - A connected `redis` (node-redis v4) client or compatible.
6793
+ * @param prefix - Optional key prefix applied to every operation.
6794
+ */
6795
+ constructor(client, prefix = "") {
6796
+ this.client = client;
6797
+ this.prefix = prefix;
6798
+ }
6799
+ client;
6800
+ prefix;
6801
+ key(key) {
6802
+ return this.prefix ? `${this.prefix}${key}` : key;
6803
+ }
6804
+ async get(key) {
6805
+ const raw = await this.client.get(this.key(key));
6806
+ return raw === null ? null : JSON.parse(raw);
6807
+ }
6808
+ async set(key, value, ttlSeconds) {
6809
+ const payload = JSON.stringify(value);
6810
+ await this.client.set(
6811
+ this.key(key),
6812
+ payload,
6813
+ ttlSeconds !== void 0 ? { EX: ttlSeconds } : void 0
6814
+ );
6815
+ }
6816
+ async delete(key) {
6817
+ await this.client.del(this.key(key));
6818
+ }
6819
+ async has(key) {
6820
+ return await this.client.exists(this.key(key)) > 0;
6821
+ }
6822
+ async clear() {
6823
+ await this.client.flushDb();
6824
+ }
6825
+ };
6826
+
6827
+ // src/cache/cached.ts
6828
+ function cached3(fn, options) {
6829
+ return async (...args) => {
6830
+ const key = options.key(...args);
6831
+ const hit = await options.manager.get(key);
6832
+ if (hit !== null) return hit;
6833
+ const result = await fn(...args);
6834
+ await options.manager.set(key, result, options.ttlSeconds);
6835
+ return result;
6836
+ };
6837
+ }
6838
+
6839
+ // src/sessions/store.ts
6840
+ var MemorySessionStore = class {
6841
+ byHash = /* @__PURE__ */ new Map();
6842
+ live(session, idHash) {
6843
+ if (!session) return null;
6844
+ if (session.expiresAt <= Date.now()) {
6845
+ this.byHash.delete(idHash);
6846
+ return null;
6847
+ }
6848
+ return session;
6849
+ }
6850
+ async get(idHash) {
6851
+ return this.live(this.byHash.get(idHash), idHash);
6852
+ }
6853
+ async set(session) {
6854
+ this.byHash.set(session.idHash, session);
6855
+ }
6856
+ async delete(idHash) {
6857
+ this.byHash.delete(idHash);
6858
+ }
6859
+ async deleteByUser(userId) {
6860
+ let count = 0;
6861
+ for (const [hash, session] of this.byHash) {
6862
+ if (session.userId === userId) {
6863
+ this.byHash.delete(hash);
6864
+ count += 1;
6865
+ }
6866
+ }
6867
+ return count;
6868
+ }
6869
+ async listByUser(userId) {
6870
+ const now = Date.now();
6871
+ return [...this.byHash.values()].filter((s) => s.userId === userId && s.expiresAt > now).sort((a, b) => a.createdAt - b.createdAt);
6872
+ }
6873
+ };
6874
+
6875
+ // src/sessions/service.ts
6876
+ var SessionService = class {
6877
+ store;
6878
+ ttlSeconds;
6879
+ /**
6880
+ * @param options - Store and default TTL.
6881
+ */
6882
+ constructor(options) {
6883
+ this.store = options.store;
6884
+ this.ttlSeconds = options.ttlSeconds ?? 60 * 60 * 24 * 7;
6885
+ }
6886
+ /**
6887
+ * Create a session for `userId` and return its one-time cookie value.
6888
+ *
6889
+ * @param userId - The owning user id.
6890
+ * @param data - Arbitrary session payload.
6891
+ * @param ttlSeconds - Override the default lifetime.
6892
+ * @returns The plaintext token (set as a cookie) and the stored session.
6893
+ */
6894
+ async create(userId, data = {}, ttlSeconds) {
6895
+ const { plaintext, tokenHash } = generateOpaqueToken();
6896
+ const now = Date.now();
6897
+ const session = {
6898
+ idHash: tokenHash,
6899
+ userId,
6900
+ data,
6901
+ createdAt: now,
6902
+ expiresAt: now + (ttlSeconds ?? this.ttlSeconds) * 1e3
6903
+ };
6904
+ await this.store.set(session);
6905
+ return { token: plaintext, session };
6906
+ }
6907
+ /**
6908
+ * Resolve an opaque cookie value to its live session.
6909
+ *
6910
+ * @param token - The plaintext cookie value.
6911
+ * @returns The session, or `null` when missing/expired.
6912
+ */
6913
+ async resolve(token) {
6914
+ return this.store.get(hashOpaqueToken(token));
6915
+ }
6916
+ /**
6917
+ * Revoke a single session by its cookie value.
6918
+ *
6919
+ * @param token - The plaintext cookie value.
6920
+ */
6921
+ async destroy(token) {
6922
+ await this.store.delete(hashOpaqueToken(token));
6923
+ }
6924
+ /**
6925
+ * Revoke every session a user owns (global logout).
6926
+ *
6927
+ * @param userId - The user id.
6928
+ * @returns The number of sessions removed.
6929
+ */
6930
+ async destroyByUser(userId) {
6931
+ return this.store.deleteByUser(userId);
6932
+ }
6933
+ /**
6934
+ * List a user's live sessions (e.g. an "active devices" view).
6935
+ *
6936
+ * @param userId - The user id.
6937
+ * @returns The user's sessions, oldest first.
6938
+ */
6939
+ async listByUser(userId) {
6940
+ return this.store.listByUser(userId);
6941
+ }
6942
+ };
6943
+
6944
+ // src/sessions/middleware.ts
6945
+ function parseCookies(header) {
6946
+ const out = {};
6947
+ if (!header) return out;
6948
+ for (const part of header.split(";")) {
6949
+ const index = part.indexOf("=");
6950
+ if (index < 0) continue;
6951
+ const name = part.slice(0, index).trim();
6952
+ const value = part.slice(index + 1).trim();
6953
+ if (name) out[name] = decodeURIComponent(value);
6954
+ }
6955
+ return out;
6956
+ }
6957
+ function sessionCookie(req, cookieName) {
6958
+ return parseCookies(req.header("cookie") ?? void 0)[cookieName] ?? null;
6959
+ }
6960
+ function makeSessionMiddleware(service, options = {}) {
6961
+ const cookieName = options.cookieName ?? "sid";
6962
+ return (req, _res, next) => {
6963
+ const token = sessionCookie(req, cookieName);
6964
+ if (!token) {
6965
+ req.session = null;
6966
+ next();
6967
+ return;
6968
+ }
6969
+ service.resolve(token).then((session) => {
6970
+ req.session = session;
6971
+ next();
6972
+ }).catch(next);
6973
+ };
6974
+ }
6975
+
6976
+ // src/sse/eventStream.ts
6977
+ var ServerSentEvent = class {
6978
+ constructor(init) {
6979
+ this.init = init;
6980
+ }
6981
+ init;
6982
+ /**
6983
+ * Encode to the SSE wire format (terminated by a blank line).
6984
+ *
6985
+ * @returns The encoded event block.
6986
+ */
6987
+ encode() {
6988
+ const lines = [];
6989
+ if (this.init.event) lines.push(`event: ${this.init.event}`);
6990
+ if (this.init.id) lines.push(`id: ${this.init.id}`);
6991
+ if (this.init.retry !== void 0) lines.push(`retry: ${this.init.retry}`);
6992
+ for (const line of this.init.data.split("\n")) lines.push(`data: ${line}`);
6993
+ return `${lines.join("\n")}
6994
+
6995
+ `;
6996
+ }
6997
+ };
6998
+ function deferred() {
6999
+ let resolve;
7000
+ const promise = new Promise((r) => {
7001
+ resolve = r;
7002
+ });
7003
+ return { promise, resolve };
7004
+ }
7005
+ var EventStream = class {
7006
+ queue = [];
7007
+ waiter = null;
7008
+ closed = false;
7009
+ heartbeatSeconds;
7010
+ /**
7011
+ * @param options - Heartbeat configuration.
7012
+ */
7013
+ constructor(options = {}) {
7014
+ this.heartbeatSeconds = options.heartbeatSeconds ?? 15;
7015
+ }
7016
+ /** Enqueue raw SSE-encoded text and wake the iterator. */
7017
+ push(raw) {
7018
+ if (this.closed) return;
7019
+ this.queue.push(raw);
7020
+ this.waiter?.resolve();
7021
+ this.waiter = null;
7022
+ }
7023
+ /**
7024
+ * Publish a data payload as an SSE event.
7025
+ *
7026
+ * @param data - The payload; objects are JSON-encoded.
7027
+ * @param event - Optional event name.
7028
+ */
7029
+ publish(data, event) {
7030
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
7031
+ this.push(
7032
+ new ServerSentEvent({ data: payload, ...event ? { event } : {} }).encode()
7033
+ );
7034
+ }
7035
+ /** Publish a pre-built {@link ServerSentEvent}. */
7036
+ publishEvent(event) {
7037
+ this.push(event.encode());
7038
+ }
7039
+ /** Close the stream; the iterator finishes after draining. */
7040
+ close() {
7041
+ this.closed = true;
7042
+ this.waiter?.resolve();
7043
+ this.waiter = null;
7044
+ }
7045
+ /**
7046
+ * Async iterator yielding encoded SSE chunks, with periodic heartbeats.
7047
+ *
7048
+ * @returns An async iterator of encoded event strings.
7049
+ */
7050
+ async *stream() {
7051
+ const heartbeatMs = this.heartbeatSeconds !== null ? this.heartbeatSeconds * 1e3 : null;
7052
+ while (!this.closed || this.queue.length > 0) {
7053
+ if (this.queue.length > 0) {
7054
+ yield this.queue.shift();
7055
+ continue;
7056
+ }
7057
+ if (this.closed) break;
7058
+ this.waiter = deferred();
7059
+ if (heartbeatMs === null) {
7060
+ await this.waiter.promise;
7061
+ } else {
7062
+ let timer;
7063
+ const heartbeat = new Promise((resolve) => {
7064
+ timer = setTimeout(resolve, heartbeatMs);
7065
+ });
7066
+ await Promise.race([this.waiter.promise, heartbeat]);
7067
+ if (timer) clearTimeout(timer);
7068
+ if (this.queue.length === 0 && !this.closed) yield ": ping\n\n";
7069
+ }
7070
+ }
7071
+ }
7072
+ };
7073
+ async function sseResponse(req, res, stream) {
7074
+ res.setHeader("Content-Type", "text/event-stream");
7075
+ res.setHeader("Cache-Control", "no-cache");
7076
+ res.setHeader("Connection", "keep-alive");
7077
+ res.flushHeaders?.();
7078
+ req.on("close", () => stream.close());
7079
+ for await (const chunk of stream.stream()) {
7080
+ if (res.writableEnded) break;
7081
+ res.write(chunk);
7082
+ }
7083
+ res.end();
7084
+ }
7085
+
7086
+ // src/sse/broker.ts
7087
+ var SSEBroker = class {
7088
+ /**
7089
+ * @param streamOptions - Options applied to every {@link EventStream} created
7090
+ * by {@link register} (e.g. heartbeat interval).
7091
+ */
7092
+ constructor(streamOptions = {}) {
7093
+ this.streamOptions = streamOptions;
7094
+ }
7095
+ streamOptions;
7096
+ channels = /* @__PURE__ */ new Map();
7097
+ /**
7098
+ * Register a new subscriber stream on `channel`.
7099
+ *
7100
+ * @param channel - The channel name.
7101
+ * @returns A fresh {@link EventStream} to serve to the subscriber.
7102
+ */
7103
+ register(channel) {
7104
+ const stream = new EventStream(this.streamOptions);
7105
+ const set = this.channels.get(channel) ?? /* @__PURE__ */ new Set();
7106
+ set.add(stream);
7107
+ this.channels.set(channel, set);
7108
+ return stream;
7109
+ }
7110
+ /**
7111
+ * Remove a subscriber stream from `channel` and close it.
7112
+ *
7113
+ * @param channel - The channel name.
7114
+ * @param stream - The stream to remove.
7115
+ */
7116
+ unregister(channel, stream) {
7117
+ const set = this.channels.get(channel);
7118
+ if (!set) return;
7119
+ set.delete(stream);
7120
+ stream.close();
7121
+ if (set.size === 0) this.channels.delete(channel);
7122
+ }
7123
+ /**
7124
+ * Number of live subscribers on `channel`.
7125
+ *
7126
+ * @param channel - The channel name.
7127
+ * @returns The subscriber count.
7128
+ */
7129
+ localSubscribers(channel) {
7130
+ return this.channels.get(channel)?.size ?? 0;
7131
+ }
7132
+ /**
7133
+ * Publish an event to every subscriber on `channel`.
7134
+ *
7135
+ * @param channel - The channel name.
7136
+ * @param data - The payload (objects are JSON-encoded).
7137
+ * @param event - Optional event name.
7138
+ * @returns The number of subscribers the event was delivered to.
7139
+ */
7140
+ publish(channel, data, event) {
7141
+ const set = this.channels.get(channel);
7142
+ if (!set) return 0;
7143
+ for (const stream of set) stream.publish(data, event);
7144
+ return set.size;
7145
+ }
7146
+ };
7147
+
7148
+ // src/websockets/schemas.ts
7149
+ var wsEnvelopeSchema = zod.z.object({
7150
+ type: zod.z.string().openapi({ description: "Message type discriminator." }),
7151
+ data: zod.z.unknown().optional().openapi({ description: "Arbitrary payload." })
7152
+ }).openapi("WSEnvelope");
7153
+ var WebSocketHub = class {
7154
+ byId = /* @__PURE__ */ new Map();
7155
+ byUser = /* @__PURE__ */ new Map();
7156
+ maxPerUser;
7157
+ /**
7158
+ * @param options - Per-user connection cap.
7159
+ */
7160
+ constructor(options = {}) {
7161
+ this.maxPerUser = options.maxPerUser ?? 5;
7162
+ }
7163
+ /**
7164
+ * Register a new connection for `userId`, evicting the oldest if over cap.
7165
+ *
7166
+ * @param userId - The owning user id.
7167
+ * @param ws - The socket to register.
7168
+ * @returns The created connection record.
7169
+ */
7170
+ register(userId, ws) {
7171
+ const connection = {
7172
+ id: crypto.randomUUID(),
7173
+ userId,
7174
+ ws,
7175
+ topics: /* @__PURE__ */ new Set()
7176
+ };
7177
+ this.byId.set(connection.id, connection);
7178
+ const ids = this.byUser.get(userId) ?? /* @__PURE__ */ new Set();
7179
+ ids.add(connection.id);
7180
+ this.byUser.set(userId, ids);
7181
+ if (ids.size > this.maxPerUser) {
7182
+ const oldest = ids.values().next().value;
7183
+ if (oldest) this.unregister(oldest, 1008);
7184
+ }
7185
+ return connection;
7186
+ }
7187
+ /**
7188
+ * Remove a connection and close its socket.
7189
+ *
7190
+ * @param connectionId - The connection id.
7191
+ * @param code - Optional WebSocket close code.
7192
+ */
7193
+ unregister(connectionId, code) {
7194
+ const connection = this.byId.get(connectionId);
7195
+ if (!connection) return;
7196
+ this.byId.delete(connectionId);
7197
+ const ids = this.byUser.get(connection.userId);
7198
+ if (ids) {
7199
+ ids.delete(connectionId);
7200
+ if (ids.size === 0) this.byUser.delete(connection.userId);
7201
+ }
7202
+ try {
7203
+ connection.ws.close(code);
7204
+ } catch {
7205
+ }
7206
+ }
7207
+ /** Subscribe a connection to a topic. */
7208
+ subscribe(connectionId, topic) {
7209
+ this.byId.get(connectionId)?.topics.add(topic);
7210
+ }
7211
+ /** Unsubscribe a connection from a topic. */
7212
+ unsubscribe(connectionId, topic) {
7213
+ this.byId.get(connectionId)?.topics.delete(topic);
7214
+ }
7215
+ /** Serialize and send an envelope to a single connection. */
7216
+ deliver(connection, payload) {
7217
+ try {
7218
+ connection.ws.send(payload);
7219
+ return true;
7220
+ } catch {
7221
+ this.unregister(connection.id);
7222
+ return false;
7223
+ }
7224
+ }
7225
+ /**
7226
+ * Send an envelope to every connection of `userId`.
7227
+ *
7228
+ * @param userId - The target user.
7229
+ * @param envelope - The message envelope.
7230
+ * @returns The number of connections delivered to.
7231
+ */
7232
+ sendTo(userId, envelope) {
7233
+ const ids = this.byUser.get(userId);
7234
+ if (!ids) return 0;
7235
+ const payload = JSON.stringify(envelope);
7236
+ let count = 0;
7237
+ for (const id of [...ids]) {
7238
+ const connection = this.byId.get(id);
7239
+ if (connection && this.deliver(connection, payload)) count += 1;
7240
+ }
7241
+ return count;
7242
+ }
7243
+ /**
7244
+ * Broadcast an envelope to all connections, or only a topic's subscribers.
7245
+ *
7246
+ * @param envelope - The message envelope.
7247
+ * @param topic - Optional topic to scope the broadcast.
7248
+ * @returns The number of connections delivered to.
7249
+ */
7250
+ broadcast(envelope, topic) {
7251
+ const payload = JSON.stringify(envelope);
7252
+ let count = 0;
7253
+ for (const connection of [...this.byId.values()]) {
7254
+ if (topic && !connection.topics.has(topic)) continue;
7255
+ if (this.deliver(connection, payload)) count += 1;
7256
+ }
7257
+ return count;
7258
+ }
7259
+ /** The set of users with at least one live connection. */
7260
+ onlineUsers() {
7261
+ return new Set(this.byUser.keys());
7262
+ }
7263
+ /** Total live connection count. */
7264
+ connectionCount() {
7265
+ return this.byId.size;
7266
+ }
7267
+ /** Number of connections subscribed to `topic`. */
7268
+ topicCount(topic) {
7269
+ let count = 0;
7270
+ for (const connection of this.byId.values()) {
7271
+ if (connection.topics.has(topic)) count += 1;
7272
+ }
7273
+ return count;
7274
+ }
7275
+ };
7276
+
7277
+ // src/websockets/attach.ts
7278
+ function tokenFromUrl(url) {
7279
+ const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : "";
7280
+ return new URLSearchParams(query).get("token");
7281
+ }
7282
+ async function attachWebSocketHub(server, hub, options = {}) {
7283
+ let ws;
7284
+ try {
7285
+ ws = await import('ws');
7286
+ } catch (cause) {
7287
+ throw new Error(
7288
+ "attachWebSocketHub requires the 'ws' peer dependency. Install with `npm i ws`.",
7289
+ { cause }
7290
+ );
7291
+ }
7292
+ const path = options.path ?? "/ws";
7293
+ const heartbeatMs = (options.heartbeatSeconds ?? 30) * 1e3;
7294
+ const authenticate = options.authenticate ?? (() => "anonymous");
7295
+ const wss = new ws.WebSocketServer({ server, path });
7296
+ wss.on("connection", (socket, req) => {
7297
+ void (async () => {
7298
+ const userId = await authenticate({
7299
+ url: req.url ?? "",
7300
+ headers: req.headers
7301
+ });
7302
+ if (userId === null) {
7303
+ socket.close(1008);
7304
+ return;
7305
+ }
7306
+ const connection = hub.register(userId, socket);
7307
+ let alive = true;
7308
+ socket.on("pong", () => {
7309
+ alive = true;
7310
+ });
7311
+ socket.on("message", (data) => {
7312
+ options.onMessage?.(connection, String(data));
7313
+ });
7314
+ socket.on("close", () => {
7315
+ alive = false;
7316
+ hub.unregister(connection.id);
7317
+ });
7318
+ if (heartbeatMs > 0) {
7319
+ const timer = setInterval(() => {
7320
+ if (!alive) {
7321
+ clearInterval(timer);
7322
+ hub.unregister(connection.id);
7323
+ return;
7324
+ }
7325
+ alive = false;
7326
+ socket.ping();
7327
+ }, heartbeatMs);
7328
+ socket.on("close", () => clearInterval(timer));
7329
+ }
7330
+ })();
7331
+ });
7332
+ return wss;
7333
+ }
7334
+
7335
+ // src/queue/broker.ts
7336
+ var MemoryBroker = class {
7337
+ handlers = /* @__PURE__ */ new Map();
7338
+ async publish(queue, message) {
7339
+ const set = this.handlers.get(queue);
7340
+ if (!set) return;
7341
+ const payload = JSON.parse(JSON.stringify(message));
7342
+ for (const handler of [...set]) await handler(payload);
7343
+ }
7344
+ async subscribe(queue, handler) {
7345
+ const set = this.handlers.get(queue) ?? /* @__PURE__ */ new Set();
7346
+ set.add(handler);
7347
+ this.handlers.set(queue, set);
7348
+ return async () => {
7349
+ set.delete(handler);
7350
+ if (set.size === 0) this.handlers.delete(queue);
7351
+ };
7352
+ }
7353
+ async close() {
7354
+ this.handlers.clear();
7355
+ }
7356
+ };
7357
+ var RabbitBroker = class {
7358
+ /**
7359
+ * @param options - Connection URL and queue durability.
7360
+ */
7361
+ constructor(options) {
7362
+ this.options = options;
7363
+ this.durable = options.durable ?? true;
7364
+ }
7365
+ options;
7366
+ connection = null;
7367
+ channel = null;
7368
+ durable;
7369
+ /** Lazily connect and open a channel. */
7370
+ async ready() {
7371
+ if (this.channel) return this.channel;
7372
+ let amqp;
7373
+ try {
7374
+ amqp = await import('amqplib');
7375
+ } catch (cause) {
7376
+ throw new Error(
7377
+ "RabbitBroker requires the 'amqplib' peer dependency. Install with `npm i amqplib`.",
7378
+ { cause }
7379
+ );
7380
+ }
7381
+ this.connection = await amqp.connect(this.options.url);
7382
+ this.channel = await this.connection.createChannel();
7383
+ return this.channel;
7384
+ }
7385
+ async publish(queue, message) {
7386
+ const channel = await this.ready();
7387
+ await channel.assertQueue(queue, { durable: this.durable });
7388
+ channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), {
7389
+ persistent: this.durable
7390
+ });
7391
+ }
7392
+ async subscribe(queue, handler) {
7393
+ const channel = await this.ready();
7394
+ await channel.assertQueue(queue, { durable: this.durable });
7395
+ const { consumerTag } = await channel.consume(queue, (msg) => {
7396
+ if (!msg) return;
7397
+ void Promise.resolve(handler(JSON.parse(msg.content.toString()))).then(() => channel.ack(msg)).catch(() => channel.nack(msg, false, false));
7398
+ });
7399
+ return async () => {
7400
+ await channel.cancel(consumerTag);
7401
+ };
7402
+ }
7403
+ async close() {
7404
+ await this.channel?.close();
7405
+ await this.connection?.close();
7406
+ this.channel = null;
7407
+ this.connection = null;
7408
+ }
7409
+ };
7410
+
7411
+ // src/tasks/manager.ts
7412
+ var logger = new JSONLogger("tempest_express_sdk.tasks");
7413
+ var TaskManager = class {
7414
+ broker;
7415
+ queue;
7416
+ handlers = /* @__PURE__ */ new Map();
7417
+ unsubscribe = null;
7418
+ /**
7419
+ * @param options - Broker and queue name.
7420
+ */
7421
+ constructor(options = {}) {
7422
+ this.broker = options.broker ?? new MemoryBroker();
7423
+ this.queue = options.queue ?? "tasks";
7424
+ }
7425
+ /**
7426
+ * Register a handler for a named task.
7427
+ *
7428
+ * @param name - The task name.
7429
+ * @param handler - The handler invoked with the task payload.
7430
+ */
7431
+ register(name, handler) {
7432
+ this.handlers.set(name, handler);
7433
+ }
7434
+ /**
7435
+ * Enqueue a task by name.
7436
+ *
7437
+ * @param name - The registered task name.
7438
+ * @param payload - The JSON-serializable payload.
7439
+ */
7440
+ async enqueue(name, payload = {}) {
7441
+ const envelope = { name, payload };
7442
+ await this.broker.publish(this.queue, envelope);
7443
+ }
7444
+ /**
7445
+ * Start the worker: subscribe to the task queue and dispatch to handlers.
7446
+ * A task with no registered handler is logged and skipped.
7447
+ */
7448
+ async start() {
7449
+ if (this.unsubscribe) return;
7450
+ this.unsubscribe = await this.broker.subscribe(this.queue, async (message) => {
7451
+ const { name, payload } = message;
7452
+ const handler = this.handlers.get(name);
7453
+ if (!handler) {
7454
+ logger.warning("No handler for task", { task: name });
7455
+ return;
7456
+ }
7457
+ await handler(payload);
7458
+ });
7459
+ }
7460
+ /** Stop the worker (stops consuming; does not close the broker). */
7461
+ async stop() {
7462
+ await this.unsubscribe?.();
7463
+ this.unsubscribe = null;
7464
+ }
7465
+ };
7466
+
7467
+ // src/flags/backends.ts
7468
+ function coerceFlag(value) {
7469
+ if (typeof value === "boolean") return value;
7470
+ if (typeof value === "number") return value !== 0;
7471
+ if (typeof value === "string") {
7472
+ return ["1", "true", "on", "yes", "enabled"].includes(value.trim().toLowerCase());
7473
+ }
7474
+ return false;
7475
+ }
7476
+ var MemoryFeatureFlagBackend = class {
7477
+ flags = /* @__PURE__ */ new Map();
7478
+ /**
7479
+ * @param initial - Initial flag → enabled map.
7480
+ */
7481
+ constructor(initial = {}) {
7482
+ for (const [flag, enabled] of Object.entries(initial)) this.flags.set(flag, enabled);
7483
+ }
7484
+ /** Set or override a flag. */
7485
+ set(flag, enabled) {
7486
+ this.flags.set(flag, enabled);
7487
+ }
7488
+ resolve(flag) {
7489
+ return this.flags.has(flag) ? this.flags.get(flag) : null;
7490
+ }
7491
+ };
7492
+ var EnvFeatureFlagBackend = class {
7493
+ /**
7494
+ * @param env - Environment source (defaults to `process.env`).
7495
+ * @param prefix - Env var prefix. Default `FLAG_`.
7496
+ */
7497
+ constructor(env = process.env, prefix = "FLAG_") {
7498
+ this.env = env;
7499
+ this.prefix = prefix;
7500
+ }
7501
+ env;
7502
+ prefix;
7503
+ key(flag) {
7504
+ return this.prefix + flag.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
7505
+ }
7506
+ resolve(flag) {
7507
+ const raw = this.env[this.key(flag)];
7508
+ return raw === void 0 ? null : coerceFlag(raw);
7509
+ }
7510
+ };
7511
+ var CompositeFeatureFlagBackend = class {
7512
+ /**
7513
+ * @param backends - Backends in priority order.
7514
+ */
7515
+ constructor(backends) {
7516
+ this.backends = backends;
7517
+ }
7518
+ backends;
7519
+ async resolve(flag, context) {
7520
+ for (const backend of this.backends) {
7521
+ const answer = await backend.resolve(flag, context);
7522
+ if (answer !== null) return answer;
7523
+ }
7524
+ return null;
7525
+ }
7526
+ };
7527
+
7528
+ // src/flags/service.ts
7529
+ var FeatureFlags = class {
7530
+ /**
7531
+ * @param backend - The resolving backend.
7532
+ * @param defaultEnabled - Value used when the backend returns `null`.
7533
+ */
7534
+ constructor(backend, defaultEnabled = false) {
7535
+ this.backend = backend;
7536
+ this.defaultEnabled = defaultEnabled;
7537
+ }
7538
+ backend;
7539
+ defaultEnabled;
7540
+ /**
7541
+ * Whether `flag` is enabled for the given context.
7542
+ *
7543
+ * @param flag - The flag name.
7544
+ * @param context - Optional evaluation context.
7545
+ * @returns `true` when enabled (or default when the backend is undecided).
7546
+ */
7547
+ async isEnabled(flag, context) {
7548
+ const answer = await this.backend.resolve(flag, context);
7549
+ return answer ?? this.defaultEnabled;
7550
+ }
7551
+ };
7552
+ function makeFlagGuard(flags, flag) {
7553
+ return (_req, _res, next) => {
7554
+ flags.isEnabled(flag).then((enabled) => {
7555
+ if (enabled) next();
7556
+ else next(new NotFoundException({ message: "Not found", details: { flag } }));
7557
+ }).catch(next);
7558
+ };
7559
+ }
7560
+ var LocalUploadStorage = class {
7561
+ root;
7562
+ baseUrl;
7563
+ /**
7564
+ * @param options - Filesystem root and public base URL.
7565
+ */
7566
+ constructor(options) {
7567
+ this.root = options.root;
7568
+ this.baseUrl = options.baseUrl ?? "";
7569
+ }
7570
+ async save(key, data, options = {}) {
7571
+ const target = path.join(this.root, key);
7572
+ await promises.mkdir(path.dirname(target), { recursive: true });
7573
+ await promises.writeFile(target, data);
7574
+ return {
7575
+ key,
7576
+ url: this.url(key),
7577
+ size: data.byteLength,
7578
+ ...options.contentType !== void 0 ? { contentType: options.contentType } : {}
7579
+ };
7580
+ }
7581
+ async read(key) {
7582
+ return promises.readFile(path.join(this.root, key));
7583
+ }
7584
+ async delete(key) {
7585
+ await promises.rm(path.join(this.root, key), { force: true });
7586
+ }
7587
+ url(key) {
7588
+ const base = this.baseUrl.replace(/\/$/, "");
7589
+ return base ? `${base}/${key}` : `/${key}`;
7590
+ }
7591
+ };
7592
+ function buildContentDisposition(filename, inline = false) {
7593
+ const disposition = inline ? "inline" : "attachment";
7594
+ const encoded = encodeURIComponent(filename);
7595
+ return `${disposition}; filename*=UTF-8''${encoded}`;
7596
+ }
7597
+
6756
7598
  // src/auth/schemas.ts
6757
7599
  var signupSchema = zod.z.object({
6758
7600
  email: zod.z.string().email().openapi({ description: "Login identifier (email)." }),
@@ -6999,7 +7841,7 @@ function makeAuthRouter(options) {
6999
7841
  });
7000
7842
  return router;
7001
7843
  }
7002
- var logger = new JSONLogger("tempest_express_sdk.api.handlers");
7844
+ var logger2 = new JSONLogger("tempest_express_sdk.api.handlers");
7003
7845
  var REQUEST_ID_HEADER = "X-Request-ID";
7004
7846
  function requestIdMiddleware() {
7005
7847
  return (req, res, next) => {
@@ -7040,7 +7882,7 @@ function makeAppExceptionHandler(options = {}) {
7040
7882
  return;
7041
7883
  }
7042
7884
  const isServerError = exc.statusCode >= 500;
7043
- logger.log(isServerError ? serverErrorLevel : "info", "AppException handled", {
7885
+ logger2.log(isServerError ? serverErrorLevel : "info", "AppException handled", {
7044
7886
  path: req.path,
7045
7887
  method: req.method,
7046
7888
  statusCode: exc.statusCode,
@@ -7064,7 +7906,7 @@ function makeUnhandledExceptionHandler(options = {}) {
7064
7906
  const { includeStack = false, logLevel = "error" } = options;
7065
7907
  return (err, req, res, _next) => {
7066
7908
  const error = err instanceof Error ? err : new Error(String(err));
7067
- logger.log(logLevel, "Unhandled exception", {
7909
+ logger2.log(logLevel, "Unhandled exception", {
7068
7910
  path: req.path,
7069
7911
  method: req.method,
7070
7912
  [HTTP_500_MARKER]: true,
@@ -7199,7 +8041,7 @@ function makeHealthRouter(options = {}) {
7199
8041
  });
7200
8042
  return router;
7201
8043
  }
7202
- var logger2 = new JSONLogger("tempest_express_sdk.api.server");
8044
+ var logger3 = new JSONLogger("tempest_express_sdk.api.server");
7203
8045
  function corsMiddleware(origins) {
7204
8046
  const allowAll = origins === "*";
7205
8047
  const allowList = new Set(Array.isArray(origins) ? origins : [origins]);
@@ -7258,14 +8100,14 @@ function runServer(app, options = {}) {
7258
8100
  const port = options.port ?? 8e3;
7259
8101
  return new Promise((resolve) => {
7260
8102
  const server = app.listen(port, host, () => {
7261
- logger2.info("Server listening", { host, port });
8103
+ logger3.info("Server listening", { host, port });
7262
8104
  resolve(server);
7263
8105
  });
7264
8106
  });
7265
8107
  }
7266
8108
 
7267
8109
  // src/version.ts
7268
- var VERSION = "0.1.0";
8110
+ var VERSION = "0.2.0";
7269
8111
 
7270
8112
  Object.defineProperty(exports, "OpenAPIRegistry", {
7271
8113
  enumerable: true,
@@ -7427,35 +8269,55 @@ exports.BaseService = BaseService;
7427
8269
  exports.CEP_PATTERN = CEP_PATTERN;
7428
8270
  exports.CNPJ_PATTERN = CNPJ_PATTERN;
7429
8271
  exports.CPF_PATTERN = CPF_PATTERN;
8272
+ exports.CompositeFeatureFlagBackend = CompositeFeatureFlagBackend;
7430
8273
  exports.ConflictException = ConflictException;
7431
8274
  exports.DEFAULT_LOCALE = DEFAULT_LOCALE;
8275
+ exports.EnvFeatureFlagBackend = EnvFeatureFlagBackend;
8276
+ exports.EventStream = EventStream;
7432
8277
  exports.ExpiredTokenException = ExpiredTokenException;
8278
+ exports.FeatureFlags = FeatureFlags;
7433
8279
  exports.ForbiddenException = ForbiddenException;
7434
8280
  exports.HTTP_500_MARKER = HTTP_500_MARKER;
7435
8281
  exports.InvalidTokenException = InvalidTokenException;
7436
8282
  exports.JSONLogger = JSONLogger;
7437
8283
  exports.JWTUtils = JWTUtils;
8284
+ exports.LocalUploadStorage = LocalUploadStorage;
8285
+ exports.MemoryBroker = MemoryBroker;
8286
+ exports.MemoryCacheManager = MemoryCacheManager;
8287
+ exports.MemoryFeatureFlagBackend = MemoryFeatureFlagBackend;
8288
+ exports.MemorySessionStore = MemorySessionStore;
7438
8289
  exports.MemoryThrottleBackend = MemoryThrottleBackend;
7439
8290
  exports.MessageCatalog = MessageCatalog;
7440
8291
  exports.NotFoundException = NotFoundException;
7441
8292
  exports.PHONE_BR_PATTERN = PHONE_BR_PATTERN;
7442
8293
  exports.PasswordUtils = PasswordUtils;
7443
8294
  exports.REQUEST_ID_HEADER = REQUEST_ID_HEADER;
8295
+ exports.RabbitBroker = RabbitBroker;
8296
+ exports.RedisCacheManager = RedisCacheManager;
7444
8297
  exports.Region = Region;
8298
+ exports.SSEBroker = SSEBroker;
8299
+ exports.ServerSentEvent = ServerSentEvent;
8300
+ exports.SessionService = SessionService;
8301
+ exports.TaskManager = TaskManager;
7445
8302
  exports.TooManyRequestsException = TooManyRequestsException;
7446
8303
  exports.UF = UF;
7447
8304
  exports.UnauthorizedException = UnauthorizedException;
7448
8305
  exports.UserAuthService = UserAuthService;
7449
8306
  exports.VERSION = VERSION;
7450
8307
  exports.ValidationException = ValidationException;
8308
+ exports.WebSocketHub = WebSocketHub;
8309
+ exports.attachWebSocketHub = attachWebSocketHub;
7451
8310
  exports.authResponseSchema = authResponseSchema;
7452
8311
  exports.baseAppSettingsSchema = baseAppSettingsSchema;
7453
8312
  exports.baseAppSettingsShape = baseAppSettingsShape;
7454
8313
  exports.baseResponseSchema = baseResponseSchema;
7455
8314
  exports.bearerToken = bearerToken;
8315
+ exports.buildContentDisposition = buildContentDisposition;
8316
+ exports.cached = cached3;
7456
8317
  exports.cepField = cepField;
7457
8318
  exports.citiesByUf = citiesByUf;
7458
8319
  exports.cnpjField = cnpjField;
8320
+ exports.coerceFlag = coerceFlag;
7459
8321
  exports.configureLogging = configureLogging;
7460
8322
  exports.corsSettingsShape = corsSettingsShape;
7461
8323
  exports.cpfField = cpfField;
@@ -7491,8 +8353,10 @@ exports.loadSettings = loadSettings;
7491
8353
  exports.loginSchema = loginSchema;
7492
8354
  exports.makeAppExceptionHandler = makeAppExceptionHandler;
7493
8355
  exports.makeAuthRouter = makeAuthRouter;
8356
+ exports.makeFlagGuard = makeFlagGuard;
7494
8357
  exports.makeHealthRouter = makeHealthRouter;
7495
8358
  exports.makeJwtAuthMiddleware = makeJwtAuthMiddleware;
8359
+ exports.makeSessionMiddleware = makeSessionMiddleware;
7496
8360
  exports.makeUnhandledExceptionHandler = makeUnhandledExceptionHandler;
7497
8361
  exports.modifyDict = modifyDict;
7498
8362
  exports.mountOpenApiJson = mountOpenApiJson;
@@ -7509,6 +8373,7 @@ exports.onlyDigits = onlyDigits;
7509
8373
  exports.paginationFilterSchema = paginationFilterSchema;
7510
8374
  exports.paginationSchema = paginationSchema;
7511
8375
  exports.parseAcceptLanguage = parseAcceptLanguage;
8376
+ exports.parseCookies = parseCookies;
7512
8377
  exports.phoneBrField = phoneBrField;
7513
8378
  exports.refreshSchema = refreshSchema;
7514
8379
  exports.registerExceptionHandlers = registerExceptionHandlers;
@@ -7517,17 +8382,21 @@ exports.requireRoles = requireRoles;
7517
8382
  exports.runServer = runServer;
7518
8383
  exports.runWithRequestContext = runWithRequestContext;
7519
8384
  exports.serverSettingsShape = serverSettingsShape;
8385
+ exports.sessionCookie = sessionCookie;
7520
8386
  exports.setRequestId = setRequestId;
7521
8387
  exports.signupSchema = signupSchema;
8388
+ exports.sseResponse = sseResponse;
7522
8389
  exports.statesByRegion = statesByRegion;
7523
8390
  exports.tableNameFor = tableNameFor;
7524
8391
  exports.toDict = toDict;
7525
8392
  exports.toUtc = toUtc;
8393
+ exports.tokenFromUrl = tokenFromUrl;
7526
8394
  exports.tokenPairSchema = tokenPairSchema;
7527
8395
  exports.ufField = ufField;
7528
8396
  exports.updatedByColumn = updatedByColumn;
7529
8397
  exports.userPublicSchema = userPublicSchema;
7530
8398
  exports.utcnow = utcnow;
7531
8399
  exports.verifyOpaqueToken = verifyOpaqueToken;
8400
+ exports.wsEnvelopeSchema = wsEnvelopeSchema;
7532
8401
  //# sourceMappingURL=index.cjs.map
7533
8402
  //# sourceMappingURL=index.cjs.map