@scalemule/nextjs 0.0.25 → 0.0.26

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.
@@ -94,6 +94,13 @@ declare function extractClientContextFromReq(req: IncomingMessageLike): ClientCo
94
94
  * @internal
95
95
  */
96
96
  declare function buildClientContextHeaders(context: ClientContext | undefined): Record<string, string>;
97
+ /**
98
+ * Build feature-flag evaluation context from forwarded client context.
99
+ *
100
+ * This keeps server-side flag evaluation aligned with targeting rules that use
101
+ * `ip_address`, while letting callers merge any additional attributes they need.
102
+ */
103
+ declare function buildFlagContext(clientContext: Pick<ClientContext, 'ip'> | undefined, extraContext?: Record<string, unknown>): Record<string, unknown>;
97
104
 
98
105
  /**
99
106
  * Cookie Utilities for Secure Session Management
@@ -262,6 +269,27 @@ declare function createAuthRoutes(config?: AuthRoutesConfig): {
262
269
  DELETE: RouteHandler;
263
270
  PATCH: RouteHandler;
264
271
  };
272
+ interface AnalyticsTrackingGateConfig {
273
+ /** Feature flag key used to decide whether to forward analytics */
274
+ flagKey: string;
275
+ /** Feature flag environment (default: 'prod') */
276
+ environment?: string;
277
+ /**
278
+ * When true, analytics continues if flag evaluation fails.
279
+ * Default: true
280
+ */
281
+ failOpen?: boolean;
282
+ /**
283
+ * Build feature-flag evaluation context from the inbound analytics request.
284
+ * Defaults to { ip_address, user_id, anonymous_id, session_id, event_name, page_url }
285
+ * when those values are present.
286
+ */
287
+ buildContext?: (args: {
288
+ body: Record<string, unknown>;
289
+ clientContext: ClientContext;
290
+ request: Request;
291
+ }) => Record<string, unknown>;
292
+ }
265
293
  interface AnalyticsRoutesConfig {
266
294
  /** Server client config (optional if using env vars) */
267
295
  client?: Partial<ServerConfig>;
@@ -276,6 +304,12 @@ interface AnalyticsRoutesConfig {
276
304
  * Default: false (uses catch-all route pattern)
277
305
  */
278
306
  simpleProxy?: boolean;
307
+ /**
308
+ * Optional feature flag gate that suppresses analytics forwarding when the
309
+ * evaluated flag returns false. Useful for test users, internal IPs, or
310
+ * staged rollouts without adding app-specific proxy logic.
311
+ */
312
+ trackingGate?: AnalyticsTrackingGateConfig;
279
313
  }
280
314
  /**
281
315
  * Create analytics API route handlers
@@ -906,4 +940,4 @@ declare function invalidateBundleCache(key?: string): void;
906
940
  */
907
941
  declare function prefetchBundles(keys: string[]): Promise<void>;
908
942
 
909
- export { type AnalyticsRoutesConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
943
+ export { type AnalyticsRoutesConfig, type AnalyticsTrackingGateConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
@@ -94,6 +94,13 @@ declare function extractClientContextFromReq(req: IncomingMessageLike): ClientCo
94
94
  * @internal
95
95
  */
96
96
  declare function buildClientContextHeaders(context: ClientContext | undefined): Record<string, string>;
97
+ /**
98
+ * Build feature-flag evaluation context from forwarded client context.
99
+ *
100
+ * This keeps server-side flag evaluation aligned with targeting rules that use
101
+ * `ip_address`, while letting callers merge any additional attributes they need.
102
+ */
103
+ declare function buildFlagContext(clientContext: Pick<ClientContext, 'ip'> | undefined, extraContext?: Record<string, unknown>): Record<string, unknown>;
97
104
 
98
105
  /**
99
106
  * Cookie Utilities for Secure Session Management
@@ -262,6 +269,27 @@ declare function createAuthRoutes(config?: AuthRoutesConfig): {
262
269
  DELETE: RouteHandler;
263
270
  PATCH: RouteHandler;
264
271
  };
272
+ interface AnalyticsTrackingGateConfig {
273
+ /** Feature flag key used to decide whether to forward analytics */
274
+ flagKey: string;
275
+ /** Feature flag environment (default: 'prod') */
276
+ environment?: string;
277
+ /**
278
+ * When true, analytics continues if flag evaluation fails.
279
+ * Default: true
280
+ */
281
+ failOpen?: boolean;
282
+ /**
283
+ * Build feature-flag evaluation context from the inbound analytics request.
284
+ * Defaults to { ip_address, user_id, anonymous_id, session_id, event_name, page_url }
285
+ * when those values are present.
286
+ */
287
+ buildContext?: (args: {
288
+ body: Record<string, unknown>;
289
+ clientContext: ClientContext;
290
+ request: Request;
291
+ }) => Record<string, unknown>;
292
+ }
265
293
  interface AnalyticsRoutesConfig {
266
294
  /** Server client config (optional if using env vars) */
267
295
  client?: Partial<ServerConfig>;
@@ -276,6 +304,12 @@ interface AnalyticsRoutesConfig {
276
304
  * Default: false (uses catch-all route pattern)
277
305
  */
278
306
  simpleProxy?: boolean;
307
+ /**
308
+ * Optional feature flag gate that suppresses analytics forwarding when the
309
+ * evaluated flag returns false. Useful for test users, internal IPs, or
310
+ * staged rollouts without adding app-specific proxy logic.
311
+ */
312
+ trackingGate?: AnalyticsTrackingGateConfig;
279
313
  }
280
314
  /**
281
315
  * Create analytics API route handlers
@@ -906,4 +940,4 @@ declare function invalidateBundleCache(key?: string): void;
906
940
  */
907
941
  declare function prefetchBundles(keys: string[]): Promise<void>;
908
942
 
909
- export { type AnalyticsRoutesConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
943
+ export { type AnalyticsRoutesConfig, type AnalyticsTrackingGateConfig, type AuthMiddlewareConfig, type AuthRoutesConfig, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, type HandlerContext, type HandlerOptions, type MySqlBundle, OAUTH_STATE_COOKIE_NAME, type OAuthBundle, type PostgresBundle, type RedisBundle, type S3Bundle, SESSION_COOKIE_NAME, ScaleMuleError, ServerConfig, type SessionCookieOptions, type SessionData, type SmtpBundle, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, prefetchBundles, prefetchSecrets, requireAppSecret, requireBundle, requireSession, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, withAuth, withCSRFProtection, withCSRFToken, withSession };
@@ -159,6 +159,13 @@ function buildClientContextHeaders(context) {
159
159
  }
160
160
  return headers2;
161
161
  }
162
+ function buildFlagContext(clientContext, extraContext = {}) {
163
+ const context = { ...extraContext };
164
+ if (clientContext?.ip && context.ip_address === void 0) {
165
+ context.ip_address = clientContext.ip;
166
+ }
167
+ return context;
168
+ }
162
169
 
163
170
  // src/server/client.ts
164
171
  var GATEWAY_URLS = {
@@ -1385,8 +1392,50 @@ function createAuthRoutes(config = {}) {
1385
1392
  };
1386
1393
  return { GET, POST, DELETE, PATCH };
1387
1394
  }
1395
+ function getPrimaryTrackingContextSource(body) {
1396
+ if (Array.isArray(body.events)) {
1397
+ const firstEvent = body.events.find((event) => event && typeof event === "object" && !Array.isArray(event));
1398
+ if (firstEvent && typeof firstEvent === "object" && !Array.isArray(firstEvent)) {
1399
+ return firstEvent;
1400
+ }
1401
+ }
1402
+ return body;
1403
+ }
1404
+ function buildDefaultTrackingGateContext(args) {
1405
+ const source = getPrimaryTrackingContextSource(args.body);
1406
+ const extraContext = {};
1407
+ for (const key of ["user_id", "anonymous_id", "session_id", "event_name", "page_url"]) {
1408
+ const value = source[key];
1409
+ if (typeof value === "string" && value.trim()) {
1410
+ extraContext[key] = value;
1411
+ }
1412
+ }
1413
+ return buildFlagContext(args.clientContext, extraContext);
1414
+ }
1388
1415
  function createAnalyticsRoutes(config = {}) {
1389
1416
  const sm = createServerClient(config.client);
1417
+ const shouldSuppressTracking = async (body, clientContext, request) => {
1418
+ const gate = config.trackingGate;
1419
+ if (!gate) {
1420
+ return false;
1421
+ }
1422
+ try {
1423
+ const contextBuilder = gate.buildContext || ((args) => buildDefaultTrackingGateContext(args));
1424
+ const evaluation = await sm.flags.evaluate(
1425
+ gate.flagKey,
1426
+ contextBuilder({ body, clientContext, request }) || {},
1427
+ gate.environment || "prod",
1428
+ { clientContext }
1429
+ );
1430
+ return evaluation?.value === false;
1431
+ } catch (err) {
1432
+ if (gate.failOpen === false) {
1433
+ console.warn(`[ScaleMule Analytics] Tracking gate blocked "${gate.flagKey}" after evaluation failure`, err);
1434
+ return true;
1435
+ }
1436
+ return false;
1437
+ }
1438
+ };
1390
1439
  const handleTrackEvent = async (body, clientContext) => {
1391
1440
  const {
1392
1441
  event_name,
@@ -1469,8 +1518,12 @@ function createAnalyticsRoutes(config = {}) {
1469
1518
  };
1470
1519
  const POST = async (request, context) => {
1471
1520
  try {
1472
- const body = await request.json().catch(() => ({}));
1521
+ const rawBody = await request.json().catch(() => ({}));
1522
+ const body = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody : {};
1473
1523
  const clientContext = extractClientContext(request);
1524
+ if (await shouldSuppressTracking(body, clientContext, request)) {
1525
+ return successResponse({ tracked: 0, suppressed: true }, 202);
1526
+ }
1474
1527
  if (config.simpleProxy) {
1475
1528
  return handleTrackEvent(body, clientContext);
1476
1529
  }
@@ -1515,7 +1568,13 @@ function createAnalyticsRoutes(config = {}) {
1515
1568
  let pageViewResult;
1516
1569
  try {
1517
1570
  pageViewResult = await sm.analytics.trackPageView(
1518
- { page_url, page_title, referrer, session_id, user_id },
1571
+ {
1572
+ page_url,
1573
+ page_title,
1574
+ referrer,
1575
+ session_id,
1576
+ user_id
1577
+ },
1519
1578
  { clientContext }
1520
1579
  );
1521
1580
  } catch (err) {
@@ -2153,6 +2212,7 @@ exports.ScaleMuleServer = ScaleMuleServer;
2153
2212
  exports.USER_ID_COOKIE_NAME = USER_ID_COOKIE_NAME;
2154
2213
  exports.apiHandler = apiHandler;
2155
2214
  exports.buildClientContextHeaders = buildClientContextHeaders;
2215
+ exports.buildFlagContext = buildFlagContext;
2156
2216
  exports.clearOAuthState = clearOAuthState;
2157
2217
  exports.clearSession = clearSession;
2158
2218
  exports.configureBundles = configureBundles;
@@ -157,6 +157,13 @@ function buildClientContextHeaders(context) {
157
157
  }
158
158
  return headers2;
159
159
  }
160
+ function buildFlagContext(clientContext, extraContext = {}) {
161
+ const context = { ...extraContext };
162
+ if (clientContext?.ip && context.ip_address === void 0) {
163
+ context.ip_address = clientContext.ip;
164
+ }
165
+ return context;
166
+ }
160
167
 
161
168
  // src/server/client.ts
162
169
  var GATEWAY_URLS = {
@@ -1383,8 +1390,50 @@ function createAuthRoutes(config = {}) {
1383
1390
  };
1384
1391
  return { GET, POST, DELETE, PATCH };
1385
1392
  }
1393
+ function getPrimaryTrackingContextSource(body) {
1394
+ if (Array.isArray(body.events)) {
1395
+ const firstEvent = body.events.find((event) => event && typeof event === "object" && !Array.isArray(event));
1396
+ if (firstEvent && typeof firstEvent === "object" && !Array.isArray(firstEvent)) {
1397
+ return firstEvent;
1398
+ }
1399
+ }
1400
+ return body;
1401
+ }
1402
+ function buildDefaultTrackingGateContext(args) {
1403
+ const source = getPrimaryTrackingContextSource(args.body);
1404
+ const extraContext = {};
1405
+ for (const key of ["user_id", "anonymous_id", "session_id", "event_name", "page_url"]) {
1406
+ const value = source[key];
1407
+ if (typeof value === "string" && value.trim()) {
1408
+ extraContext[key] = value;
1409
+ }
1410
+ }
1411
+ return buildFlagContext(args.clientContext, extraContext);
1412
+ }
1386
1413
  function createAnalyticsRoutes(config = {}) {
1387
1414
  const sm = createServerClient(config.client);
1415
+ const shouldSuppressTracking = async (body, clientContext, request) => {
1416
+ const gate = config.trackingGate;
1417
+ if (!gate) {
1418
+ return false;
1419
+ }
1420
+ try {
1421
+ const contextBuilder = gate.buildContext || ((args) => buildDefaultTrackingGateContext(args));
1422
+ const evaluation = await sm.flags.evaluate(
1423
+ gate.flagKey,
1424
+ contextBuilder({ body, clientContext, request }) || {},
1425
+ gate.environment || "prod",
1426
+ { clientContext }
1427
+ );
1428
+ return evaluation?.value === false;
1429
+ } catch (err) {
1430
+ if (gate.failOpen === false) {
1431
+ console.warn(`[ScaleMule Analytics] Tracking gate blocked "${gate.flagKey}" after evaluation failure`, err);
1432
+ return true;
1433
+ }
1434
+ return false;
1435
+ }
1436
+ };
1388
1437
  const handleTrackEvent = async (body, clientContext) => {
1389
1438
  const {
1390
1439
  event_name,
@@ -1467,8 +1516,12 @@ function createAnalyticsRoutes(config = {}) {
1467
1516
  };
1468
1517
  const POST = async (request, context) => {
1469
1518
  try {
1470
- const body = await request.json().catch(() => ({}));
1519
+ const rawBody = await request.json().catch(() => ({}));
1520
+ const body = rawBody && typeof rawBody === "object" && !Array.isArray(rawBody) ? rawBody : {};
1471
1521
  const clientContext = extractClientContext(request);
1522
+ if (await shouldSuppressTracking(body, clientContext, request)) {
1523
+ return successResponse({ tracked: 0, suppressed: true }, 202);
1524
+ }
1472
1525
  if (config.simpleProxy) {
1473
1526
  return handleTrackEvent(body, clientContext);
1474
1527
  }
@@ -1513,7 +1566,13 @@ function createAnalyticsRoutes(config = {}) {
1513
1566
  let pageViewResult;
1514
1567
  try {
1515
1568
  pageViewResult = await sm.analytics.trackPageView(
1516
- { page_url, page_title, referrer, session_id, user_id },
1569
+ {
1570
+ page_url,
1571
+ page_title,
1572
+ referrer,
1573
+ session_id,
1574
+ user_id
1575
+ },
1517
1576
  { clientContext }
1518
1577
  );
1519
1578
  } catch (err) {
@@ -2142,4 +2201,4 @@ async function prefetchBundles(keys) {
2142
2201
  await Promise.all(keys.map((key) => getBundle(key)));
2143
2202
  }
2144
2203
 
2145
- export { CSRF_COOKIE_NAME, CSRF_HEADER_NAME, OAUTH_STATE_COOKIE_NAME, SESSION_COOKIE_NAME, ScaleMuleError, ScaleMuleServer, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createServerClient, createWebhookHandler, createWebhookRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, parseWebhookEvent, prefetchBundles, prefetchSecrets, registerVideoWebhook, requireAppSecret, requireBundle, requireSession, resolveGatewayUrl, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, verifyWebhookSignature, withAuth, withCSRFProtection, withCSRFToken, withSession };
2204
+ export { CSRF_COOKIE_NAME, CSRF_HEADER_NAME, OAUTH_STATE_COOKIE_NAME, SESSION_COOKIE_NAME, ScaleMuleError, ScaleMuleServer, USER_ID_COOKIE_NAME, apiHandler, buildClientContextHeaders, buildFlagContext, clearOAuthState, clearSession, configureBundles, configureSecrets, createAnalyticsRoutes, createAuthMiddleware, createAuthRoutes, createServerClient, createWebhookHandler, createWebhookRoutes, errorCodeToStatus, extractClientContext, extractClientContextFromReq, generateCSRFToken, getAppSecret, getAppSecretOrDefault, getBootstrapFlags, getBundle, getCSRFToken, getMySqlBundle, getOAuthBundle, getPostgresBundle, getRedisBundle, getS3Bundle, getSession, getSessionFromRequest, getSmtpBundle, invalidateBundleCache, invalidateSecretCache, parseWebhookEvent, prefetchBundles, prefetchSecrets, registerVideoWebhook, requireAppSecret, requireBundle, requireSession, resolveGatewayUrl, setOAuthState, unwrap, validateCSRFToken, validateCSRFTokenAsync, validateOAuthState, validateOAuthStateAsync, verifyWebhookSignature, withAuth, withCSRFProtection, withCSRFToken, withSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalemule/nextjs",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "description": "ScaleMule SDK for Next.js applications - authentication, storage, and user management",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",