@digilogiclabs/platform-core 1.4.0 → 1.5.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.mjs CHANGED
@@ -1745,7 +1745,7 @@ var init_RedisCache = __esm({
1745
1745
  * Create a RedisCache from configuration
1746
1746
  */
1747
1747
  static async create(config) {
1748
- const { default: Redis } = await import("ioredis");
1748
+ const { default: Redis2 } = await import("ioredis");
1749
1749
  let client;
1750
1750
  if (config.cluster) {
1751
1751
  const { Cluster } = await import("ioredis");
@@ -1758,7 +1758,7 @@ var init_RedisCache = __esm({
1758
1758
  }
1759
1759
  });
1760
1760
  } else if (config.sentinel) {
1761
- client = new Redis({
1761
+ client = new Redis2({
1762
1762
  sentinels: config.sentinel.sentinels,
1763
1763
  name: config.sentinel.name,
1764
1764
  password: config.password,
@@ -1767,7 +1767,7 @@ var init_RedisCache = __esm({
1767
1767
  });
1768
1768
  } else {
1769
1769
  const options = toIORedisOptions(config);
1770
- client = typeof options === "string" ? new Redis(options) : new Redis(options);
1770
+ client = typeof options === "string" ? new Redis2(options) : new Redis2(options);
1771
1771
  }
1772
1772
  if (!config.lazyConnect) {
1773
1773
  await new Promise((resolve, reject) => {
@@ -1901,9 +1901,9 @@ var init_RedisCache = __esm({
1901
1901
  }
1902
1902
  async subscribe(channel, callback) {
1903
1903
  if (!this.subscriberClient) {
1904
- const { default: Redis } = await import("ioredis");
1904
+ const { default: Redis2 } = await import("ioredis");
1905
1905
  const options = toIORedisOptions(this.config);
1906
- this.subscriberClient = typeof options === "string" ? new Redis(options) : new Redis(options);
1906
+ this.subscriberClient = typeof options === "string" ? new Redis2(options) : new Redis2(options);
1907
1907
  this.subscriberClient.on("message", (ch, message) => {
1908
1908
  const callbacks = this.subscriptions.get(ch);
1909
1909
  if (callbacks) {
@@ -6974,9 +6974,7 @@ var init_NodeCrypto = __esm({
6974
6974
  );
6975
6975
  }
6976
6976
  this.masterKey = Buffer.from(config.masterKey, "hex");
6977
- this.hmacKey = config.hmacKey ? Buffer.from(config.hmacKey, "hex") : Buffer.from(
6978
- hkdfSync("sha256", this.masterKey, "", "hmac-key", 32)
6979
- );
6977
+ this.hmacKey = config.hmacKey ? Buffer.from(config.hmacKey, "hex") : Buffer.from(hkdfSync("sha256", this.masterKey, "", "hmac-key", 32));
6980
6978
  const keyId = this.generateKeyId();
6981
6979
  const dek = this.deriveDEK(keyId);
6982
6980
  this.keys.set(keyId, {
@@ -14095,7 +14093,9 @@ var RAGConfigSchema = z.object({
14095
14093
  var CryptoConfigSchema = z.object({
14096
14094
  enabled: z.boolean().default(false).describe("Enable field-level encryption"),
14097
14095
  masterKey: z.string().optional().describe("256-bit master key as hex (64 chars). Required when enabled."),
14098
- hmacKey: z.string().optional().describe("HMAC key for deterministic hashing (derived from master key if not provided)")
14096
+ hmacKey: z.string().optional().describe(
14097
+ "HMAC key for deterministic hashing (derived from master key if not provided)"
14098
+ )
14099
14099
  }).refine(
14100
14100
  (data) => {
14101
14101
  if (data.enabled) {
@@ -15416,7 +15416,12 @@ init_IAI();
15416
15416
  init_IRAG();
15417
15417
 
15418
15418
  // src/adapters/memory/MemoryCrypto.ts
15419
- import { randomBytes as randomBytes21, createCipheriv, createDecipheriv, createHmac } from "crypto";
15419
+ import {
15420
+ randomBytes as randomBytes21,
15421
+ createCipheriv,
15422
+ createDecipheriv,
15423
+ createHmac
15424
+ } from "crypto";
15420
15425
  var MemoryCrypto = class {
15421
15426
  keys = /* @__PURE__ */ new Map();
15422
15427
  activeKeyId;
@@ -15610,9 +15615,9 @@ async function createCacheAdapter(config) {
15610
15615
  "Upstash requires UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN environment variables"
15611
15616
  );
15612
15617
  }
15613
- const { Redis } = await import("@upstash/redis");
15618
+ const { Redis: Redis2 } = await import("@upstash/redis");
15614
15619
  const { UpstashCache: UpstashCache2 } = await Promise.resolve().then(() => (init_UpstashCache(), UpstashCache_exports));
15615
- const client = new Redis({
15620
+ const client = new Redis2({
15616
15621
  url: config.cache.upstashUrl,
15617
15622
  token: config.cache.upstashToken
15618
15623
  });
@@ -15911,15 +15916,17 @@ function validateTlsSecurity(config) {
15911
15916
  async function createPlatformAsync(config) {
15912
15917
  const finalConfig = config ? deepMerge(loadConfig(), config) : loadConfig();
15913
15918
  validateTlsSecurity(finalConfig);
15914
- const [db, cache, storage, email, queue, tracing, crypto2] = await Promise.all([
15915
- createDatabaseAdapter(finalConfig),
15916
- createCacheAdapter(finalConfig),
15917
- createStorageAdapter(finalConfig),
15918
- createEmailAdapter(finalConfig),
15919
- createQueueAdapter(finalConfig),
15920
- createTracingAdapter(finalConfig),
15921
- createCryptoAdapter(finalConfig)
15922
- ]);
15919
+ const [db, cache, storage, email, queue, tracing, crypto2] = await Promise.all(
15920
+ [
15921
+ createDatabaseAdapter(finalConfig),
15922
+ createCacheAdapter(finalConfig),
15923
+ createStorageAdapter(finalConfig),
15924
+ createEmailAdapter(finalConfig),
15925
+ createQueueAdapter(finalConfig),
15926
+ createTracingAdapter(finalConfig),
15927
+ createCryptoAdapter(finalConfig)
15928
+ ]
15929
+ );
15923
15930
  const logger = createLogger(finalConfig);
15924
15931
  const metrics = createMetrics(finalConfig);
15925
15932
  const ai = await createAIAdapter(finalConfig);
@@ -17499,6 +17506,7 @@ var FallbackStrategies = {
17499
17506
  };
17500
17507
 
17501
17508
  // src/security.ts
17509
+ import { timingSafeEqual } from "crypto";
17502
17510
  var URL_PROTOCOL_PATTERN = /(https?:\/\/|ftp:\/\/|www\.)\S+/i;
17503
17511
  var URL_DOMAIN_PATTERN = /\b[\w.-]+\.(com|net|org|io|co|dev|app|xyz|info|biz|me|us|uk|edu|gov)\b/i;
17504
17512
  var HTML_TAG_PATTERN = /<[^>]*>/;
@@ -17523,6 +17531,37 @@ function defangUrl(str) {
17523
17531
  function sanitizeForEmail(str) {
17524
17532
  return escapeHtml(str);
17525
17533
  }
17534
+ function constantTimeEqual(a, b) {
17535
+ try {
17536
+ const aBuf = Buffer.from(a, "utf-8");
17537
+ const bBuf = Buffer.from(b, "utf-8");
17538
+ if (aBuf.length !== bBuf.length) return false;
17539
+ return timingSafeEqual(aBuf, bBuf);
17540
+ } catch {
17541
+ return false;
17542
+ }
17543
+ }
17544
+ function sanitizeApiError(error, statusCode, isDevelopment = false) {
17545
+ if (statusCode >= 400 && statusCode < 500) {
17546
+ const message = error instanceof Error ? error.message : String(error || "Bad request");
17547
+ return { message };
17548
+ }
17549
+ const result = {
17550
+ message: "An internal error occurred. Please try again later.",
17551
+ code: "INTERNAL_ERROR"
17552
+ };
17553
+ if (isDevelopment && error instanceof Error) {
17554
+ result.stack = error.stack;
17555
+ }
17556
+ return result;
17557
+ }
17558
+ function getCorrelationId2(headers) {
17559
+ const get = typeof headers === "function" ? headers : (name) => {
17560
+ const val = headers[name] ?? headers[name.toLowerCase()];
17561
+ return Array.isArray(val) ? val[0] : val;
17562
+ };
17563
+ return get("x-request-id") || get("X-Request-ID") || get("x-correlation-id") || get("X-Correlation-ID") || crypto.randomUUID();
17564
+ }
17526
17565
 
17527
17566
  // src/security-headers.ts
17528
17567
  var SecurityHeaderPresets = {
@@ -17756,6 +17795,850 @@ function buildPagination(page, limit, total) {
17756
17795
  };
17757
17796
  }
17758
17797
 
17798
+ // src/auth/keycloak.ts
17799
+ var KEYCLOAK_DEFAULT_ROLES = [
17800
+ "offline_access",
17801
+ "uma_authorization"
17802
+ ];
17803
+ function parseKeycloakRoles(accessToken, additionalDefaultRoles = []) {
17804
+ if (!accessToken) return [];
17805
+ try {
17806
+ const parts = accessToken.split(".");
17807
+ if (parts.length !== 3) return [];
17808
+ const payload = parts[1];
17809
+ const decoded = JSON.parse(atob(payload));
17810
+ const realmRoles = decoded.realm_roles ?? decoded.realm_access?.roles;
17811
+ if (!Array.isArray(realmRoles)) return [];
17812
+ const filterSet = /* @__PURE__ */ new Set([
17813
+ ...KEYCLOAK_DEFAULT_ROLES,
17814
+ ...additionalDefaultRoles
17815
+ ]);
17816
+ return realmRoles.filter(
17817
+ (role) => typeof role === "string" && !filterSet.has(role)
17818
+ );
17819
+ } catch {
17820
+ return [];
17821
+ }
17822
+ }
17823
+ function hasRole(roles, role) {
17824
+ return roles?.includes(role) ?? false;
17825
+ }
17826
+ function hasAnyRole(roles, requiredRoles) {
17827
+ if (!roles || roles.length === 0) return false;
17828
+ return requiredRoles.some((role) => roles.includes(role));
17829
+ }
17830
+ function hasAllRoles(roles, requiredRoles) {
17831
+ if (!roles || roles.length === 0) return false;
17832
+ return requiredRoles.every((role) => roles.includes(role));
17833
+ }
17834
+ function isTokenExpired(expiresAt, bufferMs = 6e4) {
17835
+ if (!expiresAt) return true;
17836
+ return Date.now() >= expiresAt - bufferMs;
17837
+ }
17838
+ function buildTokenRefreshParams(config, refreshToken) {
17839
+ return new URLSearchParams({
17840
+ grant_type: "refresh_token",
17841
+ client_id: config.clientId,
17842
+ client_secret: config.clientSecret,
17843
+ refresh_token: refreshToken
17844
+ });
17845
+ }
17846
+ function getTokenEndpoint(issuer) {
17847
+ const base = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
17848
+ return `${base}/protocol/openid-connect/token`;
17849
+ }
17850
+ function getEndSessionEndpoint(issuer) {
17851
+ const base = issuer.endsWith("/") ? issuer.slice(0, -1) : issuer;
17852
+ return `${base}/protocol/openid-connect/logout`;
17853
+ }
17854
+ async function refreshKeycloakToken(config, refreshToken, additionalDefaultRoles) {
17855
+ try {
17856
+ const endpoint = getTokenEndpoint(config.issuer);
17857
+ const params = buildTokenRefreshParams(config, refreshToken);
17858
+ const response = await fetch(endpoint, {
17859
+ method: "POST",
17860
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
17861
+ body: params
17862
+ });
17863
+ if (!response.ok) {
17864
+ const body = await response.text().catch(() => "");
17865
+ return {
17866
+ ok: false,
17867
+ error: `Token refresh failed: HTTP ${response.status} - ${body}`
17868
+ };
17869
+ }
17870
+ const data = await response.json();
17871
+ return {
17872
+ ok: true,
17873
+ tokens: {
17874
+ accessToken: data.access_token,
17875
+ refreshToken: data.refresh_token ?? refreshToken,
17876
+ idToken: data.id_token,
17877
+ expiresAt: Date.now() + data.expires_in * 1e3,
17878
+ roles: parseKeycloakRoles(data.access_token, additionalDefaultRoles)
17879
+ }
17880
+ };
17881
+ } catch (error) {
17882
+ return {
17883
+ ok: false,
17884
+ error: error instanceof Error ? error.message : "Token refresh failed"
17885
+ };
17886
+ }
17887
+ }
17888
+
17889
+ // src/auth/nextjs-keycloak.ts
17890
+ function buildAuthCookies(config = {}) {
17891
+ const { domain, sessionToken = true, callbackUrl = true } = config;
17892
+ const isProduction = process.env.NODE_ENV === "production";
17893
+ const cookieDomain = isProduction ? domain : void 0;
17894
+ const baseOptions = {
17895
+ httpOnly: true,
17896
+ sameSite: "lax",
17897
+ path: "/",
17898
+ secure: isProduction,
17899
+ domain: cookieDomain
17900
+ };
17901
+ const cookies = {
17902
+ pkceCodeVerifier: {
17903
+ name: "authjs.pkce.code_verifier",
17904
+ options: { ...baseOptions }
17905
+ },
17906
+ state: {
17907
+ name: "authjs.state",
17908
+ options: { ...baseOptions }
17909
+ }
17910
+ };
17911
+ if (sessionToken) {
17912
+ cookies.sessionToken = {
17913
+ name: isProduction ? "__Secure-authjs.session-token" : "authjs.session-token",
17914
+ options: { ...baseOptions }
17915
+ };
17916
+ }
17917
+ if (callbackUrl) {
17918
+ cookies.callbackUrl = {
17919
+ name: isProduction ? "__Secure-authjs.callback-url" : "authjs.callback-url",
17920
+ options: { ...baseOptions }
17921
+ };
17922
+ }
17923
+ return cookies;
17924
+ }
17925
+ function buildRedirectCallback(config = {}) {
17926
+ const { allowWwwVariant = false } = config;
17927
+ return async ({ url, baseUrl }) => {
17928
+ if (url.startsWith("/")) return `${baseUrl}${url}`;
17929
+ try {
17930
+ if (new URL(url).origin === baseUrl) return url;
17931
+ } catch {
17932
+ return baseUrl;
17933
+ }
17934
+ if (allowWwwVariant) {
17935
+ try {
17936
+ const urlHost = new URL(url).hostname;
17937
+ const baseHost = new URL(baseUrl).hostname;
17938
+ if (urlHost === `www.${baseHost}` || baseHost === `www.${urlHost}`) {
17939
+ return url;
17940
+ }
17941
+ } catch {
17942
+ }
17943
+ }
17944
+ return baseUrl;
17945
+ };
17946
+ }
17947
+ function buildKeycloakCallbacks(config) {
17948
+ const {
17949
+ issuer,
17950
+ clientId,
17951
+ clientSecret,
17952
+ defaultRoles = [],
17953
+ debug = process.env.NODE_ENV === "development"
17954
+ } = config;
17955
+ const kcConfig = { issuer, clientId, clientSecret };
17956
+ function log(message, meta) {
17957
+ if (debug) {
17958
+ console.log(`[Auth] ${message}`, meta ? JSON.stringify(meta) : "");
17959
+ }
17960
+ }
17961
+ return {
17962
+ /**
17963
+ * JWT callback — stores Keycloak tokens and handles refresh.
17964
+ *
17965
+ * Compatible with Auth.js v5 JWT callback signature.
17966
+ */
17967
+ async jwt({
17968
+ token,
17969
+ user,
17970
+ account
17971
+ }) {
17972
+ if (user) {
17973
+ token.id = token.sub ?? user.id;
17974
+ }
17975
+ if (account?.provider === "keycloak") {
17976
+ token.accessToken = account.access_token;
17977
+ token.refreshToken = account.refresh_token;
17978
+ token.idToken = account.id_token;
17979
+ token.roles = parseKeycloakRoles(
17980
+ account.access_token,
17981
+ defaultRoles
17982
+ );
17983
+ token.accessTokenExpires = account.expires_at ? account.expires_at * 1e3 : Date.now() + 3e5;
17984
+ return token;
17985
+ }
17986
+ if (!isTokenExpired(token.accessTokenExpires)) {
17987
+ return token;
17988
+ }
17989
+ if (token.refreshToken) {
17990
+ log("Token expired, attempting refresh...");
17991
+ const result = await refreshKeycloakToken(
17992
+ kcConfig,
17993
+ token.refreshToken,
17994
+ defaultRoles
17995
+ );
17996
+ if (result.ok) {
17997
+ token.accessToken = result.tokens.accessToken;
17998
+ token.idToken = result.tokens.idToken ?? token.idToken;
17999
+ token.refreshToken = result.tokens.refreshToken ?? token.refreshToken;
18000
+ token.accessTokenExpires = result.tokens.expiresAt;
18001
+ token.roles = result.tokens.roles;
18002
+ delete token.error;
18003
+ log("Token refreshed OK");
18004
+ return token;
18005
+ }
18006
+ log("Token refresh failed", { error: result.error });
18007
+ return { ...token, error: "RefreshTokenError" };
18008
+ }
18009
+ log("Token expired but no refresh token available");
18010
+ return { ...token, error: "RefreshTokenError" };
18011
+ },
18012
+ /**
18013
+ * Session callback — maps JWT fields to the session object.
18014
+ *
18015
+ * Compatible with Auth.js v5 session callback signature.
18016
+ */
18017
+ async session({
18018
+ session,
18019
+ token
18020
+ }) {
18021
+ const user = session.user;
18022
+ if (user) {
18023
+ user.id = token.id || token.sub;
18024
+ user.roles = token.roles || [];
18025
+ }
18026
+ session.idToken = token.idToken;
18027
+ session.accessToken = token.accessToken;
18028
+ if (token.error) {
18029
+ session.error = token.error;
18030
+ }
18031
+ return session;
18032
+ }
18033
+ };
18034
+ }
18035
+
18036
+ // src/auth/api-security.ts
18037
+ var StandardRateLimitPresets = {
18038
+ /** General API: 100/min, 200/min authenticated */
18039
+ apiGeneral: {
18040
+ limit: 100,
18041
+ windowSeconds: 60,
18042
+ authenticatedLimit: 200
18043
+ },
18044
+ /** Admin operations: 100/min (admins are trusted) */
18045
+ adminAction: {
18046
+ limit: 100,
18047
+ windowSeconds: 60
18048
+ },
18049
+ /** AI/expensive operations: 20/hour, 50/hour authenticated */
18050
+ aiRequest: {
18051
+ limit: 20,
18052
+ windowSeconds: 3600,
18053
+ authenticatedLimit: 50
18054
+ },
18055
+ /** Auth attempts: 5/15min with 15min block */
18056
+ authAttempt: {
18057
+ limit: 5,
18058
+ windowSeconds: 900,
18059
+ blockDurationSeconds: 900
18060
+ },
18061
+ /** Contact/public forms: 10/hour */
18062
+ publicForm: {
18063
+ limit: 10,
18064
+ windowSeconds: 3600,
18065
+ blockDurationSeconds: 1800
18066
+ },
18067
+ /** Checkout/billing: 10/hour with 1hr block */
18068
+ checkout: {
18069
+ limit: 10,
18070
+ windowSeconds: 3600,
18071
+ blockDurationSeconds: 3600
18072
+ }
18073
+ };
18074
+ function resolveRateLimitIdentifier(session, clientIp) {
18075
+ if (session?.user?.id) {
18076
+ return { identifier: `user:${session.user.id}`, isAuthenticated: true };
18077
+ }
18078
+ if (session?.user?.email) {
18079
+ return {
18080
+ identifier: `email:${session.user.email}`,
18081
+ isAuthenticated: true
18082
+ };
18083
+ }
18084
+ return { identifier: `ip:${clientIp}`, isAuthenticated: false };
18085
+ }
18086
+ function extractClientIp(getHeader) {
18087
+ return getHeader("cf-connecting-ip") || getHeader("x-real-ip") || getHeader("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
18088
+ }
18089
+ function buildRateLimitHeaders(limit, remaining, resetAtMs) {
18090
+ return {
18091
+ "X-RateLimit-Limit": String(limit),
18092
+ "X-RateLimit-Remaining": String(Math.max(0, remaining)),
18093
+ "X-RateLimit-Reset": String(Math.ceil(resetAtMs / 1e3))
18094
+ };
18095
+ }
18096
+ function buildErrorBody(error, extra) {
18097
+ return { error, ...extra };
18098
+ }
18099
+ var WrapperPresets = {
18100
+ /** Public route: no auth, rate limited */
18101
+ public: {
18102
+ requireAuth: false,
18103
+ requireAdmin: false,
18104
+ rateLimit: "apiGeneral"
18105
+ },
18106
+ /** Authenticated route: requires session */
18107
+ authenticated: {
18108
+ requireAuth: true,
18109
+ requireAdmin: false,
18110
+ rateLimit: "apiGeneral"
18111
+ },
18112
+ /** Admin route: requires session with admin role */
18113
+ admin: {
18114
+ requireAuth: true,
18115
+ requireAdmin: true,
18116
+ rateLimit: "adminAction"
18117
+ },
18118
+ /** Legacy admin: accepts session OR bearer token */
18119
+ legacyAdmin: {
18120
+ requireAuth: true,
18121
+ requireAdmin: true,
18122
+ allowBearerToken: true,
18123
+ rateLimit: "adminAction"
18124
+ },
18125
+ /** AI/expensive: requires auth, strict rate limit */
18126
+ ai: {
18127
+ requireAuth: true,
18128
+ requireAdmin: false,
18129
+ rateLimit: "aiRequest"
18130
+ },
18131
+ /** Cron: no rate limit, admin-level access */
18132
+ cron: {
18133
+ requireAuth: true,
18134
+ requireAdmin: false,
18135
+ skipRateLimit: true,
18136
+ skipAudit: false
18137
+ }
18138
+ };
18139
+
18140
+ // src/auth/schemas.ts
18141
+ import { z as z2 } from "zod";
18142
+ var EmailSchema = z2.string().trim().toLowerCase().email("Invalid email address");
18143
+ var PasswordSchema = z2.string().min(8, "Password must be at least 8 characters").max(100, "Password must be less than 100 characters");
18144
+ var SlugSchema = z2.string().min(1, "Slug is required").max(100, "Slug must be less than 100 characters").regex(
18145
+ /^[a-z0-9-]+$/,
18146
+ "Slug can only contain lowercase letters, numbers, and hyphens"
18147
+ );
18148
+ var PhoneSchema = z2.string().regex(/^[\d\s()+.\-]{7,20}$/, "Invalid phone number format");
18149
+ var PersonNameSchema = z2.string().min(2, "Name must be at least 2 characters").max(100, "Name must be less than 100 characters").regex(
18150
+ /^[a-zA-Z\s\-']+$/,
18151
+ "Name can only contain letters, spaces, hyphens and apostrophes"
18152
+ );
18153
+ function createSafeTextSchema(options) {
18154
+ const {
18155
+ min,
18156
+ max,
18157
+ allowHtml = false,
18158
+ allowUrls = false,
18159
+ fieldName = "Text"
18160
+ } = options ?? {};
18161
+ let schema = z2.string();
18162
+ if (min !== void 0)
18163
+ schema = schema.min(min, `${fieldName} must be at least ${min} characters`);
18164
+ if (max !== void 0)
18165
+ schema = schema.max(
18166
+ max,
18167
+ `${fieldName} must be less than ${max} characters`
18168
+ );
18169
+ if (!allowHtml && !allowUrls) {
18170
+ return schema.refine((val) => !HTML_TAG_PATTERN.test(val), "HTML tags are not allowed").refine(
18171
+ (val) => !URL_PROTOCOL_PATTERN.test(val),
18172
+ "Links are not allowed for security reasons"
18173
+ ).refine(
18174
+ (val) => !URL_DOMAIN_PATTERN.test(val),
18175
+ "Links are not allowed for security reasons"
18176
+ );
18177
+ }
18178
+ if (!allowHtml) {
18179
+ return schema.refine(
18180
+ (val) => !HTML_TAG_PATTERN.test(val),
18181
+ "HTML tags are not allowed"
18182
+ );
18183
+ }
18184
+ if (!allowUrls) {
18185
+ return schema.refine(
18186
+ (val) => !URL_PROTOCOL_PATTERN.test(val),
18187
+ "Links are not allowed for security reasons"
18188
+ ).refine(
18189
+ (val) => !URL_DOMAIN_PATTERN.test(val),
18190
+ "Links are not allowed for security reasons"
18191
+ );
18192
+ }
18193
+ return schema;
18194
+ }
18195
+ var PaginationSchema = z2.object({
18196
+ page: z2.coerce.number().int().positive().default(1),
18197
+ limit: z2.coerce.number().int().positive().max(100).default(20),
18198
+ sortBy: z2.string().optional(),
18199
+ sortOrder: z2.enum(["asc", "desc"]).default("desc")
18200
+ });
18201
+ var DateRangeSchema = z2.object({
18202
+ startDate: z2.string().datetime(),
18203
+ endDate: z2.string().datetime()
18204
+ }).refine((data) => new Date(data.startDate) <= new Date(data.endDate), {
18205
+ message: "Start date must be before end date"
18206
+ });
18207
+ var SearchQuerySchema = z2.object({
18208
+ query: z2.string().min(1).max(200).trim(),
18209
+ page: z2.coerce.number().int().positive().default(1),
18210
+ limit: z2.coerce.number().int().positive().max(50).default(10)
18211
+ });
18212
+ var LoginSchema = z2.object({
18213
+ email: EmailSchema,
18214
+ password: PasswordSchema
18215
+ });
18216
+ var SignupSchema = z2.object({
18217
+ email: EmailSchema,
18218
+ password: PasswordSchema,
18219
+ name: z2.string().min(2).max(100).optional()
18220
+ });
18221
+
18222
+ // src/auth/feature-flags.ts
18223
+ function detectStage() {
18224
+ const stage = process.env.DEPLOYMENT_STAGE;
18225
+ if (stage === "staging" || stage === "preview") return stage;
18226
+ if (process.env.NODE_ENV === "production") return "production";
18227
+ return "development";
18228
+ }
18229
+ function resolveFlagValue(value) {
18230
+ if (typeof value === "boolean") return value;
18231
+ const envValue = process.env[value.envVar];
18232
+ if (envValue === void 0 || envValue === "") {
18233
+ return value.default ?? false;
18234
+ }
18235
+ return envValue === "true" || envValue === "1";
18236
+ }
18237
+ function createFeatureFlags(definitions) {
18238
+ return {
18239
+ /**
18240
+ * Resolve all flags for the current environment.
18241
+ * Call this once at startup or per-request for dynamic flags.
18242
+ */
18243
+ resolve(stage) {
18244
+ const currentStage = stage ?? detectStage();
18245
+ const resolved = {};
18246
+ for (const [key, def] of Object.entries(definitions)) {
18247
+ const stageKey = currentStage === "preview" ? "staging" : currentStage;
18248
+ resolved[key] = resolveFlagValue(def[stageKey]);
18249
+ }
18250
+ return resolved;
18251
+ },
18252
+ /**
18253
+ * Check if a single flag is enabled.
18254
+ */
18255
+ isEnabled(flag, stage) {
18256
+ const currentStage = stage ?? detectStage();
18257
+ const def = definitions[flag];
18258
+ const stageKey = currentStage === "preview" ? "staging" : currentStage;
18259
+ return resolveFlagValue(def[stageKey]);
18260
+ },
18261
+ /**
18262
+ * Get the flag definitions (for introspection/admin UI).
18263
+ */
18264
+ definitions
18265
+ };
18266
+ }
18267
+ function buildAllowlist(config) {
18268
+ const fromEnv = config.envVar ? process.env[config.envVar]?.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean) ?? [] : [];
18269
+ const fallback = config.fallback?.map((e) => e.toLowerCase()) ?? [];
18270
+ return [.../* @__PURE__ */ new Set([...fromEnv, ...fallback])];
18271
+ }
18272
+ function isAllowlisted(email, allowlist) {
18273
+ return allowlist.includes(email.toLowerCase());
18274
+ }
18275
+
18276
+ // src/auth/rate-limiter.ts
18277
+ var CommonRateLimits = {
18278
+ /** General API: 100/min, 200/min authenticated */
18279
+ apiGeneral: {
18280
+ limit: 100,
18281
+ windowSeconds: 60,
18282
+ authenticatedLimit: 200
18283
+ },
18284
+ /** Admin actions: 100/min */
18285
+ adminAction: {
18286
+ limit: 100,
18287
+ windowSeconds: 60
18288
+ },
18289
+ /** Auth attempts: 10/15min with 30min block */
18290
+ authAttempt: {
18291
+ limit: 10,
18292
+ windowSeconds: 900,
18293
+ blockDurationSeconds: 1800
18294
+ },
18295
+ /** AI/expensive requests: 20/hour, 50/hour authenticated */
18296
+ aiRequest: {
18297
+ limit: 20,
18298
+ windowSeconds: 3600,
18299
+ authenticatedLimit: 50
18300
+ },
18301
+ /** Public form submissions: 5/hour with 1hr block */
18302
+ publicForm: {
18303
+ limit: 5,
18304
+ windowSeconds: 3600,
18305
+ blockDurationSeconds: 3600
18306
+ },
18307
+ /** Checkout/billing: 10/hour with 1hr block */
18308
+ checkout: {
18309
+ limit: 10,
18310
+ windowSeconds: 3600,
18311
+ blockDurationSeconds: 3600
18312
+ }
18313
+ };
18314
+ function createMemoryRateLimitStore() {
18315
+ const windows = /* @__PURE__ */ new Map();
18316
+ const blocks = /* @__PURE__ */ new Map();
18317
+ const cleanupInterval = setInterval(() => {
18318
+ const now = Date.now();
18319
+ for (const [key, entry] of windows) {
18320
+ if (entry.expiresAt < now) windows.delete(key);
18321
+ }
18322
+ for (const [key, expiry] of blocks) {
18323
+ if (expiry < now) blocks.delete(key);
18324
+ }
18325
+ }, 60 * 1e3);
18326
+ if (cleanupInterval.unref) {
18327
+ cleanupInterval.unref();
18328
+ }
18329
+ return {
18330
+ async increment(key, windowMs, now) {
18331
+ const windowStart = now - windowMs;
18332
+ let entry = windows.get(key);
18333
+ if (!entry) {
18334
+ entry = { timestamps: [], expiresAt: now + windowMs + 6e4 };
18335
+ windows.set(key, entry);
18336
+ }
18337
+ entry.timestamps = entry.timestamps.filter((t) => t > windowStart);
18338
+ entry.timestamps.push(now);
18339
+ entry.expiresAt = now + windowMs + 6e4;
18340
+ return { count: entry.timestamps.length };
18341
+ },
18342
+ async isBlocked(key) {
18343
+ const expiry = blocks.get(key);
18344
+ if (!expiry || expiry < Date.now()) {
18345
+ blocks.delete(key);
18346
+ return { blocked: false, ttlMs: 0 };
18347
+ }
18348
+ return { blocked: true, ttlMs: expiry - Date.now() };
18349
+ },
18350
+ async setBlock(key, durationSeconds) {
18351
+ blocks.set(key, Date.now() + durationSeconds * 1e3);
18352
+ },
18353
+ async reset(key) {
18354
+ windows.delete(key);
18355
+ blocks.delete(`block:${key}`);
18356
+ blocks.delete(key);
18357
+ }
18358
+ };
18359
+ }
18360
+ var defaultStore;
18361
+ function getDefaultStore() {
18362
+ if (!defaultStore) {
18363
+ defaultStore = createMemoryRateLimitStore();
18364
+ }
18365
+ return defaultStore;
18366
+ }
18367
+ async function checkRateLimit(operation, identifier, rule, options = {}) {
18368
+ const store = options.store ?? getDefaultStore();
18369
+ const limit = options.isAuthenticated && rule.authenticatedLimit ? rule.authenticatedLimit : rule.limit;
18370
+ const now = Date.now();
18371
+ const windowMs = rule.windowSeconds * 1e3;
18372
+ const resetAt = now + windowMs;
18373
+ const key = `ratelimit:${operation}:${identifier}`;
18374
+ const blockKey = `ratelimit:block:${operation}:${identifier}`;
18375
+ try {
18376
+ if (rule.blockDurationSeconds) {
18377
+ const blockStatus = await store.isBlocked(blockKey);
18378
+ if (blockStatus.blocked) {
18379
+ return {
18380
+ allowed: false,
18381
+ remaining: 0,
18382
+ resetAt: now + blockStatus.ttlMs,
18383
+ current: limit + 1,
18384
+ limit,
18385
+ retryAfterSeconds: Math.ceil(blockStatus.ttlMs / 1e3)
18386
+ };
18387
+ }
18388
+ }
18389
+ const { count } = await store.increment(key, windowMs, now);
18390
+ if (count > limit) {
18391
+ options.logger?.warn("Rate limit exceeded", {
18392
+ operation,
18393
+ identifier,
18394
+ current: count,
18395
+ limit
18396
+ });
18397
+ if (rule.blockDurationSeconds) {
18398
+ await store.setBlock(blockKey, rule.blockDurationSeconds);
18399
+ }
18400
+ return {
18401
+ allowed: false,
18402
+ remaining: 0,
18403
+ resetAt,
18404
+ current: count,
18405
+ limit,
18406
+ retryAfterSeconds: Math.ceil(windowMs / 1e3)
18407
+ };
18408
+ }
18409
+ return {
18410
+ allowed: true,
18411
+ remaining: limit - count,
18412
+ resetAt,
18413
+ current: count,
18414
+ limit,
18415
+ retryAfterSeconds: 0
18416
+ };
18417
+ } catch (error) {
18418
+ options.logger?.error("Rate limit check failed, allowing request", {
18419
+ error: error instanceof Error ? error.message : String(error),
18420
+ operation,
18421
+ identifier
18422
+ });
18423
+ return {
18424
+ allowed: true,
18425
+ remaining: limit,
18426
+ resetAt,
18427
+ current: 0,
18428
+ limit,
18429
+ retryAfterSeconds: 0
18430
+ };
18431
+ }
18432
+ }
18433
+ async function getRateLimitStatus(operation, identifier, rule, store) {
18434
+ const s = store ?? getDefaultStore();
18435
+ const key = `ratelimit:${operation}:${identifier}`;
18436
+ const now = Date.now();
18437
+ const windowMs = rule.windowSeconds * 1e3;
18438
+ try {
18439
+ const { count } = await s.increment(key, windowMs, now);
18440
+ return {
18441
+ allowed: count <= rule.limit,
18442
+ remaining: Math.max(0, rule.limit - count),
18443
+ resetAt: now + windowMs,
18444
+ current: count,
18445
+ limit: rule.limit,
18446
+ retryAfterSeconds: count > rule.limit ? Math.ceil(windowMs / 1e3) : 0
18447
+ };
18448
+ } catch {
18449
+ return null;
18450
+ }
18451
+ }
18452
+ async function resetRateLimitForKey(operation, identifier, store) {
18453
+ const s = store ?? getDefaultStore();
18454
+ const key = `ratelimit:${operation}:${identifier}`;
18455
+ const blockKey = `ratelimit:block:${operation}:${identifier}`;
18456
+ await s.reset(key);
18457
+ await s.reset(blockKey);
18458
+ }
18459
+ function buildRateLimitResponseHeaders(result) {
18460
+ const headers = {
18461
+ "X-RateLimit-Limit": String(result.limit),
18462
+ "X-RateLimit-Remaining": String(result.remaining),
18463
+ "X-RateLimit-Reset": String(Math.ceil(result.resetAt / 1e3))
18464
+ };
18465
+ if (!result.allowed) {
18466
+ headers["Retry-After"] = String(result.retryAfterSeconds);
18467
+ }
18468
+ return headers;
18469
+ }
18470
+ function resolveIdentifier(session, clientIp) {
18471
+ if (session?.user?.id) {
18472
+ return { identifier: `user:${session.user.id}`, isAuthenticated: true };
18473
+ }
18474
+ if (session?.user?.email) {
18475
+ return {
18476
+ identifier: `email:${session.user.email}`,
18477
+ isAuthenticated: true
18478
+ };
18479
+ }
18480
+ return { identifier: `ip:${clientIp ?? "unknown"}`, isAuthenticated: false };
18481
+ }
18482
+
18483
+ // src/auth/audit.ts
18484
+ var StandardAuditActions = {
18485
+ // Authentication
18486
+ LOGIN_SUCCESS: "auth.login.success",
18487
+ LOGIN_FAILURE: "auth.login.failure",
18488
+ LOGOUT: "auth.logout",
18489
+ SESSION_REFRESH: "auth.session.refresh",
18490
+ PASSWORD_CHANGE: "auth.password.change",
18491
+ PASSWORD_RESET: "auth.password.reset",
18492
+ // Billing
18493
+ CHECKOUT_START: "billing.checkout.start",
18494
+ CHECKOUT_COMPLETE: "billing.checkout.complete",
18495
+ SUBSCRIPTION_CREATE: "billing.subscription.create",
18496
+ SUBSCRIPTION_CANCEL: "billing.subscription.cancel",
18497
+ SUBSCRIPTION_UPDATE: "billing.subscription.update",
18498
+ PAYMENT_FAILED: "billing.payment.failed",
18499
+ // Admin
18500
+ ADMIN_LOGIN: "admin.login",
18501
+ ADMIN_USER_VIEW: "admin.user.view",
18502
+ ADMIN_USER_UPDATE: "admin.user.update",
18503
+ ADMIN_CONFIG_CHANGE: "admin.config.change",
18504
+ // Security Events
18505
+ RATE_LIMIT_EXCEEDED: "security.rate_limit.exceeded",
18506
+ INVALID_INPUT: "security.input.invalid",
18507
+ UNAUTHORIZED_ACCESS: "security.access.unauthorized",
18508
+ OWNERSHIP_VIOLATION: "security.ownership.violation",
18509
+ WEBHOOK_SIGNATURE_INVALID: "security.webhook.signature_invalid",
18510
+ // Data
18511
+ DATA_EXPORT: "data.export",
18512
+ DATA_DELETE: "data.delete",
18513
+ DATA_UPDATE: "data.update"
18514
+ };
18515
+ function extractAuditIp(request) {
18516
+ if (!request) return void 0;
18517
+ const headers = [
18518
+ "cf-connecting-ip",
18519
+ // Cloudflare
18520
+ "x-real-ip",
18521
+ // Nginx
18522
+ "x-forwarded-for",
18523
+ // Standard proxy
18524
+ "x-client-ip"
18525
+ // Apache
18526
+ ];
18527
+ for (const header of headers) {
18528
+ const value = request.headers.get(header);
18529
+ if (value) {
18530
+ return value.split(",")[0]?.trim();
18531
+ }
18532
+ }
18533
+ return void 0;
18534
+ }
18535
+ function extractAuditUserAgent(request) {
18536
+ return request?.headers.get("user-agent") ?? void 0;
18537
+ }
18538
+ function extractAuditRequestId(request) {
18539
+ return request?.headers.get("x-request-id") ?? crypto.randomUUID();
18540
+ }
18541
+ function createAuditActor(session) {
18542
+ if (!session?.user) {
18543
+ return { id: "anonymous", type: "anonymous" };
18544
+ }
18545
+ return {
18546
+ id: session.user.id ?? session.user.email ?? "unknown",
18547
+ email: session.user.email ?? void 0,
18548
+ type: "user"
18549
+ };
18550
+ }
18551
+ var defaultLogger = {
18552
+ info: (msg, meta) => console.log(msg, meta ? JSON.stringify(meta) : ""),
18553
+ warn: (msg, meta) => console.warn(msg, meta ? JSON.stringify(meta) : ""),
18554
+ error: (msg, meta) => console.error(msg, meta ? JSON.stringify(meta) : "")
18555
+ };
18556
+ function createAuditLogger(options = {}) {
18557
+ const { persist, logger = defaultLogger } = options;
18558
+ async function log(event, request) {
18559
+ const record = {
18560
+ id: crypto.randomUUID(),
18561
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
18562
+ ip: extractAuditIp(request),
18563
+ userAgent: extractAuditUserAgent(request),
18564
+ requestId: extractAuditRequestId(request),
18565
+ ...event
18566
+ };
18567
+ const logFn = event.outcome === "failure" || event.outcome === "blocked" ? logger.warn : logger.info;
18568
+ logFn(`[AUDIT] ${event.action}`, {
18569
+ auditId: record.id,
18570
+ actor: record.actor,
18571
+ action: record.action,
18572
+ resource: record.resource,
18573
+ outcome: record.outcome,
18574
+ ip: record.ip,
18575
+ metadata: record.metadata,
18576
+ reason: record.reason
18577
+ });
18578
+ if (persist) {
18579
+ try {
18580
+ await persist(record);
18581
+ } catch (error) {
18582
+ logger.error("Failed to persist audit log", {
18583
+ error: error instanceof Error ? error.message : String(error),
18584
+ auditId: record.id
18585
+ });
18586
+ }
18587
+ }
18588
+ return record;
18589
+ }
18590
+ function createTimedAudit(event, request) {
18591
+ const startTime = Date.now();
18592
+ return {
18593
+ success: async (metadata) => {
18594
+ return log(
18595
+ {
18596
+ ...event,
18597
+ outcome: "success",
18598
+ metadata: {
18599
+ ...event.metadata,
18600
+ ...metadata,
18601
+ durationMs: Date.now() - startTime
18602
+ }
18603
+ },
18604
+ request
18605
+ );
18606
+ },
18607
+ failure: async (reason, metadata) => {
18608
+ return log(
18609
+ {
18610
+ ...event,
18611
+ outcome: "failure",
18612
+ reason,
18613
+ metadata: {
18614
+ ...event.metadata,
18615
+ ...metadata,
18616
+ durationMs: Date.now() - startTime
18617
+ }
18618
+ },
18619
+ request
18620
+ );
18621
+ },
18622
+ blocked: async (reason, metadata) => {
18623
+ return log(
18624
+ {
18625
+ ...event,
18626
+ outcome: "blocked",
18627
+ reason,
18628
+ metadata: {
18629
+ ...event.metadata,
18630
+ ...metadata,
18631
+ durationMs: Date.now() - startTime
18632
+ }
18633
+ },
18634
+ request
18635
+ );
18636
+ }
18637
+ };
18638
+ }
18639
+ return { log, createTimedAudit };
18640
+ }
18641
+
17759
18642
  // src/http/health.ts
17760
18643
  function createHealthEndpoints(platform, options = {}) {
17761
18644
  const {
@@ -26311,7 +27194,7 @@ CREATE INDEX IF NOT EXISTS idx_${executionsTable}_started ON ${executionsTable}(
26311
27194
  };
26312
27195
 
26313
27196
  // src/adapters/webhook/HttpWebhook.ts
26314
- import { createHmac as createHmac4, timingSafeEqual, randomBytes as randomBytes35 } from "crypto";
27197
+ import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual2, randomBytes as randomBytes35 } from "crypto";
26315
27198
  var HttpWebhook = class {
26316
27199
  db;
26317
27200
  queue;
@@ -26660,7 +27543,7 @@ var HttpWebhook = class {
26660
27543
  if (providedBuffer.length !== expectedBuffer.length) {
26661
27544
  return { valid: false, error: "Invalid signature" };
26662
27545
  }
26663
- if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
27546
+ if (!timingSafeEqual2(providedBuffer, expectedBuffer)) {
26664
27547
  return { valid: false, error: "Invalid signature" };
26665
27548
  }
26666
27549
  } catch {
@@ -29716,6 +30599,272 @@ function generateId2() {
29716
30599
  return randomBytes36(8).toString("hex") + Date.now().toString(36);
29717
30600
  }
29718
30601
 
30602
+ // src/env.ts
30603
+ function getRequiredEnv(key) {
30604
+ const value = process.env[key];
30605
+ if (!value) {
30606
+ throw new Error(`Missing required environment variable: ${key}`);
30607
+ }
30608
+ return value;
30609
+ }
30610
+ function getOptionalEnv(key, defaultValue) {
30611
+ return process.env[key] || defaultValue;
30612
+ }
30613
+ function getBoolEnv(key, defaultValue = false) {
30614
+ const value = process.env[key];
30615
+ if (value === void 0 || value === "") return defaultValue;
30616
+ return value === "true" || value === "1";
30617
+ }
30618
+ function getIntEnv(key, defaultValue) {
30619
+ const value = process.env[key];
30620
+ if (value === void 0 || value === "") return defaultValue;
30621
+ const parsed = parseInt(value, 10);
30622
+ return isNaN(parsed) ? defaultValue : parsed;
30623
+ }
30624
+ function validateEnvVars(config) {
30625
+ const result = checkEnvVars(config);
30626
+ if (!result.valid) {
30627
+ const lines = [];
30628
+ if (result.missing.length > 0) {
30629
+ lines.push(
30630
+ "Missing required environment variables:",
30631
+ ...result.missing.map((v) => ` - ${v}`)
30632
+ );
30633
+ }
30634
+ if (result.missingOneOf.length > 0) {
30635
+ for (const group of result.missingOneOf) {
30636
+ lines.push(`Missing one of: ${group.join(" | ")}`);
30637
+ }
30638
+ }
30639
+ if (result.invalid.length > 0) {
30640
+ lines.push(
30641
+ "Invalid environment variables:",
30642
+ ...result.invalid.map((v) => ` - ${v.key}: ${v.reason}`)
30643
+ );
30644
+ }
30645
+ throw new Error(lines.join("\n"));
30646
+ }
30647
+ }
30648
+ function checkEnvVars(config) {
30649
+ const missing = [];
30650
+ const invalid = [];
30651
+ const missingOneOf = [];
30652
+ if (config.required) {
30653
+ for (const key of config.required) {
30654
+ if (!process.env[key]) {
30655
+ missing.push(key);
30656
+ }
30657
+ }
30658
+ }
30659
+ if (config.requireOneOf) {
30660
+ for (const group of config.requireOneOf) {
30661
+ const hasAny = group.some((key) => !!process.env[key]);
30662
+ if (!hasAny) {
30663
+ missingOneOf.push(group);
30664
+ }
30665
+ }
30666
+ }
30667
+ if (config.validators) {
30668
+ for (const [key, validator] of Object.entries(config.validators)) {
30669
+ const value = process.env[key];
30670
+ if (value) {
30671
+ const result = validator(value);
30672
+ if (result !== true) {
30673
+ invalid.push({ key, reason: result });
30674
+ }
30675
+ }
30676
+ }
30677
+ }
30678
+ return {
30679
+ valid: missing.length === 0 && invalid.length === 0 && missingOneOf.length === 0,
30680
+ missing,
30681
+ invalid,
30682
+ missingOneOf
30683
+ };
30684
+ }
30685
+ function getEnvSummary(keys) {
30686
+ const summary = {};
30687
+ for (const key of keys) {
30688
+ summary[key] = !!process.env[key];
30689
+ }
30690
+ return summary;
30691
+ }
30692
+
30693
+ // src/app-logger.ts
30694
+ var LEVEL_PRIORITY2 = {
30695
+ debug: 0,
30696
+ info: 1,
30697
+ warn: 2,
30698
+ error: 3
30699
+ };
30700
+ function defaultOutput(level, message, context) {
30701
+ const isProduction = process.env.NODE_ENV === "production";
30702
+ if (isProduction) {
30703
+ const entry = { level, message, ...context };
30704
+ const method = level === "error" ? "error" : level === "warn" ? "warn" : "log";
30705
+ console[method](JSON.stringify(entry));
30706
+ } else {
30707
+ const prefix = `[${level.toUpperCase()}]`;
30708
+ const ctx = Object.keys(context).length > 0 ? " " + JSON.stringify(context) : "";
30709
+ const method = level === "error" ? "error" : level === "warn" ? "warn" : level === "debug" ? "debug" : "log";
30710
+ console[method](`${prefix} ${message}${ctx}`);
30711
+ }
30712
+ }
30713
+ var AppLoggerImpl = class _AppLoggerImpl {
30714
+ baseContext;
30715
+ minLevelPriority;
30716
+ outputFn;
30717
+ onErrorFn;
30718
+ constructor(baseContext, options) {
30719
+ this.baseContext = baseContext;
30720
+ this.minLevelPriority = LEVEL_PRIORITY2[options.minLevel] ?? 0;
30721
+ this.outputFn = options.output;
30722
+ this.onErrorFn = options.onError;
30723
+ }
30724
+ log(level, message, context) {
30725
+ if ((LEVEL_PRIORITY2[level] ?? 0) < this.minLevelPriority) return;
30726
+ const merged = {
30727
+ ...this.baseContext,
30728
+ ...context,
30729
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
30730
+ };
30731
+ this.outputFn(level, message, merged);
30732
+ }
30733
+ debug(message, context) {
30734
+ this.log("debug", message, context);
30735
+ }
30736
+ info(message, context) {
30737
+ this.log("info", message, context);
30738
+ }
30739
+ warn(message, context) {
30740
+ this.log("warn", message, context);
30741
+ }
30742
+ error(message, context) {
30743
+ this.log("error", message, context);
30744
+ if (this.onErrorFn) {
30745
+ try {
30746
+ this.onErrorFn(message, { ...this.baseContext, ...context });
30747
+ } catch {
30748
+ }
30749
+ }
30750
+ }
30751
+ child(context) {
30752
+ return new _AppLoggerImpl(
30753
+ { ...this.baseContext, ...context },
30754
+ {
30755
+ minLevel: Object.entries(LEVEL_PRIORITY2).find(
30756
+ ([, v]) => v === this.minLevelPriority
30757
+ )?.[0] ?? "debug",
30758
+ output: this.outputFn,
30759
+ onError: this.onErrorFn
30760
+ }
30761
+ );
30762
+ }
30763
+ forRequest(request, operation) {
30764
+ const requestId = request.headers.get("x-request-id") ?? request.headers.get("x-correlation-id") ?? crypto.randomUUID();
30765
+ let path;
30766
+ try {
30767
+ path = new URL(request.url).pathname;
30768
+ } catch {
30769
+ path = request.url;
30770
+ }
30771
+ return this.child({
30772
+ requestId,
30773
+ operation,
30774
+ method: request.method,
30775
+ path
30776
+ });
30777
+ }
30778
+ forJob(jobType, jobId, context) {
30779
+ return this.child({ jobType, jobId, ...context });
30780
+ }
30781
+ async timed(operation, fn, context) {
30782
+ const opLogger = this.child({ operation, ...context });
30783
+ opLogger.debug("Operation started");
30784
+ const start = Date.now();
30785
+ try {
30786
+ const result = await fn();
30787
+ opLogger.info("Operation completed", {
30788
+ durationMs: Date.now() - start
30789
+ });
30790
+ return result;
30791
+ } catch (error) {
30792
+ opLogger.error("Operation failed", {
30793
+ durationMs: Date.now() - start,
30794
+ error: error instanceof Error ? error.message : String(error)
30795
+ });
30796
+ throw error;
30797
+ }
30798
+ }
30799
+ };
30800
+ function createAppLogger(options) {
30801
+ const {
30802
+ service,
30803
+ minLevel = process.env.NODE_ENV === "production" ? "info" : "debug",
30804
+ output = defaultOutput,
30805
+ onError
30806
+ } = options;
30807
+ return new AppLoggerImpl({ service }, { minLevel, output, onError });
30808
+ }
30809
+
30810
+ // src/redis-client.ts
30811
+ import Redis from "ioredis";
30812
+ function createRedisClient(options) {
30813
+ const {
30814
+ url,
30815
+ keyPrefix,
30816
+ maxRetries = 3,
30817
+ lazyConnect = true,
30818
+ silent = false
30819
+ } = options;
30820
+ const logger = options.logger ?? {
30821
+ error: console.error
30822
+ };
30823
+ const client = new Redis(url, {
30824
+ keyPrefix,
30825
+ maxRetriesPerRequest: maxRetries,
30826
+ retryStrategy(times) {
30827
+ if (times > maxRetries) return null;
30828
+ return Math.min(times * 200, 2e3);
30829
+ },
30830
+ lazyConnect
30831
+ });
30832
+ if (!silent) {
30833
+ client.on("error", (err) => {
30834
+ logger.error("[redis] Connection error", { message: err.message });
30835
+ });
30836
+ if (logger.info) {
30837
+ const logInfo = logger.info;
30838
+ client.on("connect", () => {
30839
+ logInfo("[redis] Connected");
30840
+ });
30841
+ }
30842
+ }
30843
+ if (lazyConnect) {
30844
+ client.connect().catch(() => {
30845
+ });
30846
+ }
30847
+ return client;
30848
+ }
30849
+ var sharedClient = null;
30850
+ function getSharedRedis() {
30851
+ if (sharedClient) return sharedClient;
30852
+ const url = process.env.REDIS_URL;
30853
+ if (!url) return null;
30854
+ sharedClient = createRedisClient({
30855
+ url,
30856
+ keyPrefix: process.env.REDIS_KEY_PREFIX,
30857
+ silent: process.env.NODE_ENV === "test"
30858
+ });
30859
+ return sharedClient;
30860
+ }
30861
+ async function closeSharedRedis() {
30862
+ if (sharedClient) {
30863
+ await sharedClient.quit();
30864
+ sharedClient = null;
30865
+ }
30866
+ }
30867
+
29719
30868
  // src/migrations/Migrator.ts
29720
30869
  var DEFAULT_CONFIG = {
29721
30870
  tableName: "_migrations",
@@ -30492,6 +31641,7 @@ export {
30492
31641
  CircuitBreakerRegistry,
30493
31642
  CircuitOpenError,
30494
31643
  CommonApiErrors,
31644
+ CommonRateLimits,
30495
31645
  ConsoleEmail,
30496
31646
  ConsoleLogger,
30497
31647
  CronPresets,
@@ -30508,17 +31658,21 @@ export {
30508
31658
  DatabaseNotification,
30509
31659
  DatabasePromptStore,
30510
31660
  DatabaseProviderSchema,
31661
+ DateRangeSchema,
30511
31662
  DefaultTimeouts,
30512
31663
  EmailConfigSchema,
30513
31664
  EmailProviderSchema,
31665
+ EmailSchema,
30514
31666
  EnvSecrets,
30515
31667
  FallbackStrategies,
30516
31668
  GenericOIDCAuthSSO,
30517
31669
  GoogleAIAdapter,
30518
31670
  HTML_TAG_PATTERN,
30519
31671
  HttpWebhook,
31672
+ KEYCLOAK_DEFAULT_ROLES,
30520
31673
  LogLevelSchema,
30521
31674
  LoggingConfigSchema,
31675
+ LoginSchema,
30522
31676
  MemoryAI,
30523
31677
  MemoryAIUsage,
30524
31678
  MemoryAuditLog,
@@ -30557,7 +31711,11 @@ export {
30557
31711
  ObservabilityConfigSchema,
30558
31712
  OpenAIAdapter,
30559
31713
  PG_ERROR_MAP,
31714
+ PaginationSchema,
31715
+ PasswordSchema,
30560
31716
  PaymentErrorMessages,
31717
+ PersonNameSchema,
31718
+ PhoneSchema,
30561
31719
  PineconeRAG,
30562
31720
  PlatformConfigSchema,
30563
31721
  PostgresDatabase,
@@ -30577,9 +31735,14 @@ export {
30577
31735
  RetryPredicates,
30578
31736
  S3Storage,
30579
31737
  SQL,
31738
+ SearchQuerySchema,
30580
31739
  SecurityConfigSchema,
30581
31740
  SecurityHeaderPresets,
31741
+ SignupSchema,
31742
+ SlugSchema,
30582
31743
  SmtpEmail,
31744
+ StandardAuditActions,
31745
+ StandardRateLimitPresets,
30583
31746
  StorageConfigSchema,
30584
31747
  StorageProviderSchema,
30585
31748
  StripePayment,
@@ -30595,16 +31758,32 @@ export {
30595
31758
  UpstashCache,
30596
31759
  WeaviateRAG,
30597
31760
  WebhookEventTypes,
31761
+ WrapperPresets,
31762
+ buildAllowlist,
31763
+ buildAuthCookies,
31764
+ buildErrorBody,
31765
+ buildKeycloakCallbacks,
30598
31766
  buildPagination,
31767
+ buildRateLimitHeaders,
31768
+ buildRateLimitResponseHeaders,
31769
+ buildRedirectCallback,
31770
+ buildTokenRefreshParams,
30599
31771
  calculateBackoff,
30600
31772
  calculateRetryDelay,
31773
+ checkEnvVars,
31774
+ checkRateLimit,
30601
31775
  classifyError,
31776
+ closeSharedRedis,
30602
31777
  composeHookRegistries,
31778
+ constantTimeEqual,
30603
31779
  containsHtml,
30604
31780
  containsUrls,
30605
31781
  correlationContext,
30606
31782
  createAIError,
30607
31783
  createAnthropicAdapter,
31784
+ createAppLogger,
31785
+ createAuditActor,
31786
+ createAuditLogger,
30608
31787
  createAuthError,
30609
31788
  createBulkhead,
30610
31789
  createCacheMiddleware,
@@ -30615,6 +31794,7 @@ export {
30615
31794
  createErrorReport,
30616
31795
  createExpressHealthHandlers,
30617
31796
  createExpressMetricsHandler,
31797
+ createFeatureFlags,
30618
31798
  createGoogleAIAdapter,
30619
31799
  createHealthEndpoints,
30620
31800
  createHealthServer,
@@ -30622,6 +31802,7 @@ export {
30622
31802
  createIpKeyGenerator,
30623
31803
  createJobContext,
30624
31804
  createLoggingMiddleware,
31805
+ createMemoryRateLimitStore,
30625
31806
  createMetricsEndpoint,
30626
31807
  createMetricsMiddleware,
30627
31808
  createMetricsServer,
@@ -30635,8 +31816,10 @@ export {
30635
31816
  createPlatform,
30636
31817
  createPlatformAsync,
30637
31818
  createRateLimitMiddleware,
31819
+ createRedisClient,
30638
31820
  createRequestContext,
30639
31821
  createRequestIdMiddleware,
31822
+ createSafeTextSchema,
30640
31823
  createScopedMetrics,
30641
31824
  createSlowQueryMiddleware,
30642
31825
  createSsoOidcConfigsTable,
@@ -30653,8 +31836,13 @@ export {
30653
31836
  defangUrl,
30654
31837
  defineMigration,
30655
31838
  describeCron,
31839
+ detectStage,
30656
31840
  enterpriseMigrations,
30657
31841
  escapeHtml,
31842
+ extractAuditIp,
31843
+ extractAuditRequestId,
31844
+ extractAuditUserAgent,
31845
+ extractClientIp,
30658
31846
  filterChannelsByPreferences,
30659
31847
  formatAmount,
30660
31848
  generateAuditId,
@@ -30672,37 +31860,58 @@ export {
30672
31860
  generateVersion,
30673
31861
  generateWebhookId,
30674
31862
  generateWebhookSecret,
31863
+ getBoolEnv,
30675
31864
  getContext,
30676
- getCorrelationId,
31865
+ getCorrelationId2 as getCorrelationId,
30677
31866
  getDefaultConfig,
31867
+ getEndSessionEndpoint,
30678
31868
  getEnterpriseMigrations,
31869
+ getEnvSummary,
31870
+ getIntEnv,
30679
31871
  getLogMeta,
30680
31872
  getNextCronRun,
31873
+ getOptionalEnv,
31874
+ getRateLimitStatus,
30681
31875
  getRequestId,
31876
+ getRequiredEnv,
31877
+ getSharedRedis,
30682
31878
  getTenantId,
31879
+ getTokenEndpoint,
30683
31880
  getTraceId,
30684
31881
  getUserId,
31882
+ hasAllRoles,
31883
+ hasAnyRole,
31884
+ hasRole,
30685
31885
  isAIError,
31886
+ isAllowlisted,
30686
31887
  isApiError,
30687
31888
  isAuthError,
30688
31889
  isInContext,
30689
31890
  isInQuietHours,
30690
31891
  isPaymentError,
31892
+ isTokenExpired,
30691
31893
  isValidCron,
30692
31894
  loadConfig,
30693
31895
  matchAction,
30694
31896
  matchEventType,
31897
+ parseKeycloakRoles,
30695
31898
  raceTimeout,
31899
+ refreshKeycloakToken,
31900
+ resetRateLimitForKey,
31901
+ resolveIdentifier,
31902
+ resolveRateLimitIdentifier,
30696
31903
  retryable,
30697
31904
  runWithContext,
30698
31905
  runWithContextAsync,
30699
31906
  safeValidateConfig,
31907
+ sanitizeApiError,
30700
31908
  sanitizeForEmail,
30701
31909
  sqlMigration,
30702
31910
  stripHtml,
30703
31911
  timedHealthCheck,
30704
31912
  toHealthCheckResult,
30705
31913
  validateConfig,
31914
+ validateEnvVars,
30706
31915
  withCorrelation,
30707
31916
  withCorrelationAsync,
30708
31917
  withFallback,