@active-reach/web-sdk 1.10.0 → 1.11.1

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.
@@ -2034,6 +2034,187 @@ class NameGovernor {
2034
2034
  };
2035
2035
  }
2036
2036
  }
2037
+ const RESERVED_PREFIXES = [
2038
+ "system.",
2039
+ "user.",
2040
+ "loyalty.",
2041
+ "review.",
2042
+ "cart.",
2043
+ "checkout.",
2044
+ "product.",
2045
+ "pos.",
2046
+ "bill.",
2047
+ "feedback.",
2048
+ "chat.",
2049
+ "delivery.",
2050
+ "event.",
2051
+ "$",
2052
+ "_"
2053
+ ];
2054
+ const KEY_SOFT_VALUE_CAP = 512;
2055
+ const KEY_HARD_VALUE_CAP = 1e4;
2056
+ const CAMEL_BOUNDARY_1 = /(.)([A-Z][a-z]+)/g;
2057
+ const CAMEL_BOUNDARY_2 = /([a-z0-9])([A-Z])/g;
2058
+ const NON_SNAKE = /[\s\-.]+/g;
2059
+ const DUPE_UNDERSCORE = /_+/g;
2060
+ const ISO_8601 = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:?\d{2})?)?$/;
2061
+ const DATE_KEY_HINTS = /* @__PURE__ */ new Set([
2062
+ "at",
2063
+ "on",
2064
+ "date",
2065
+ "time",
2066
+ "timestamp",
2067
+ "dob",
2068
+ "birthday",
2069
+ "joined",
2070
+ "expired",
2071
+ "expires",
2072
+ "created",
2073
+ "updated",
2074
+ "started",
2075
+ "ended"
2076
+ ]);
2077
+ function normalizeKey(key) {
2078
+ if (!key) return null;
2079
+ const stage1 = key.replace(CAMEL_BOUNDARY_1, "$1_$2").replace(CAMEL_BOUNDARY_2, "$1_$2");
2080
+ const stage2 = stage1.replace(NON_SNAKE, "_").toLowerCase();
2081
+ const result = stage2.replace(DUPE_UNDERSCORE, "_").replace(/^_+|_+$/g, "");
2082
+ return result || null;
2083
+ }
2084
+ function startsWithReserved(key) {
2085
+ const lower = key.toLowerCase();
2086
+ for (const prefix of RESERVED_PREFIXES) {
2087
+ if (lower.startsWith(prefix)) return prefix;
2088
+ }
2089
+ return null;
2090
+ }
2091
+ function looksLikeDateKey(key) {
2092
+ for (const part of key.toLowerCase().split(/[_\-.\s]+/)) {
2093
+ if (DATE_KEY_HINTS.has(part)) return true;
2094
+ }
2095
+ return false;
2096
+ }
2097
+ function parseDateValue(value) {
2098
+ if (typeof value === "number" && Number.isFinite(value)) {
2099
+ return value < 1e11 ? Math.floor(value * 1e3) : Math.floor(value);
2100
+ }
2101
+ if (typeof value === "string" && value) {
2102
+ const s = value.trim();
2103
+ if (ISO_8601.test(s)) {
2104
+ const ms = Date.parse(s);
2105
+ if (!Number.isNaN(ms)) return ms;
2106
+ }
2107
+ const asNum = Number(s);
2108
+ if (Number.isFinite(asNum)) {
2109
+ return asNum < 1e11 ? Math.floor(asNum * 1e3) : Math.floor(asNum);
2110
+ }
2111
+ }
2112
+ return null;
2113
+ }
2114
+ function warnOnConsole(message) {
2115
+ if (typeof console === "undefined" || typeof console.warn !== "function") return;
2116
+ console.warn(message);
2117
+ }
2118
+ const _TraitGovernor = class _TraitGovernor {
2119
+ constructor() {
2120
+ this.warnCounts = /* @__PURE__ */ new Map();
2121
+ }
2122
+ /**
2123
+ * Apply all guards to a traits object. Returns the sanitized payload
2124
+ * to send + a list of drops/modifications.
2125
+ *
2126
+ * Pass `workspace_id` so warning rate-limits scope per workspace; an
2127
+ * agency user switching workspaces won't have their warning budget
2128
+ * consumed cross-workspace.
2129
+ */
2130
+ process(traits, workspace_id) {
2131
+ const sanitized = {};
2132
+ const drops = [];
2133
+ if (!traits || typeof traits !== "object") {
2134
+ return { sanitized, drops };
2135
+ }
2136
+ for (const rawKey of Object.keys(traits)) {
2137
+ const prefix = startsWithReserved(rawKey);
2138
+ if (prefix !== null) {
2139
+ drops.push({
2140
+ originalKey: rawKey,
2141
+ verdict: "reserved_prefix",
2142
+ reason: `key uses reserved namespace ${JSON.stringify(prefix)}`
2143
+ });
2144
+ continue;
2145
+ }
2146
+ const normalizedKey = normalizeKey(rawKey);
2147
+ if (normalizedKey === null) {
2148
+ drops.push({
2149
+ originalKey: rawKey,
2150
+ verdict: "bad_key_format",
2151
+ reason: `key reduced to empty after normalization`
2152
+ });
2153
+ continue;
2154
+ }
2155
+ if (startsWithReserved(normalizedKey) !== null) {
2156
+ drops.push({
2157
+ originalKey: rawKey,
2158
+ verdict: "reserved_prefix",
2159
+ reason: `normalized key ${JSON.stringify(normalizedKey)} still uses a reserved namespace`
2160
+ });
2161
+ continue;
2162
+ }
2163
+ let value = traits[rawKey];
2164
+ if (typeof value === "string") {
2165
+ if (value.length > KEY_HARD_VALUE_CAP) {
2166
+ drops.push({
2167
+ originalKey: rawKey,
2168
+ verdict: "value_too_long",
2169
+ reason: `value length ${value.length} exceeds hard cap ${KEY_HARD_VALUE_CAP}`
2170
+ });
2171
+ continue;
2172
+ }
2173
+ if (value.length > KEY_SOFT_VALUE_CAP) {
2174
+ drops.push({
2175
+ originalKey: rawKey,
2176
+ verdict: "value_too_long",
2177
+ reason: `value truncated from ${value.length} to ${KEY_SOFT_VALUE_CAP} chars`
2178
+ });
2179
+ value = value.slice(0, KEY_SOFT_VALUE_CAP);
2180
+ }
2181
+ }
2182
+ if (looksLikeDateKey(normalizedKey) && typeof value === "string") {
2183
+ const parsed = parseDateValue(value);
2184
+ if (parsed === null) {
2185
+ drops.push({
2186
+ originalKey: rawKey,
2187
+ verdict: "bad_date_format",
2188
+ reason: `value ${JSON.stringify(value)} on date-keyed field ${JSON.stringify(normalizedKey)} didn't parse as ISO-8601 / epoch`
2189
+ });
2190
+ continue;
2191
+ }
2192
+ value = parsed;
2193
+ }
2194
+ sanitized[normalizedKey] = value;
2195
+ }
2196
+ for (const drop of drops) {
2197
+ this.maybeWarn(workspace_id ?? null, drop);
2198
+ }
2199
+ return { sanitized, drops };
2200
+ }
2201
+ maybeWarn(workspace_id, drop) {
2202
+ const ws = workspace_id ?? "__no_workspace__";
2203
+ let perVerdict = this.warnCounts.get(ws);
2204
+ if (!perVerdict) {
2205
+ perVerdict = /* @__PURE__ */ new Map();
2206
+ this.warnCounts.set(ws, perVerdict);
2207
+ }
2208
+ const count = perVerdict.get(drop.verdict) ?? 0;
2209
+ if (count >= _TraitGovernor.WARN_CAP) return;
2210
+ perVerdict.set(drop.verdict, count + 1);
2211
+ warnOnConsole(
2212
+ `[Aegis SDK] trait ${drop.verdict}: ${drop.reason} (original key: ${JSON.stringify(drop.originalKey)}). Backend will reject; fix the SDK call to silence this warning.`
2213
+ );
2214
+ }
2215
+ };
2216
+ _TraitGovernor.WARN_CAP = 3;
2217
+ let TraitGovernor = _TraitGovernor;
2037
2218
  class Aegis {
2038
2219
  constructor() {
2039
2220
  this.config = null;
@@ -2046,6 +2227,7 @@ class Aegis {
2046
2227
  this._ecommerce = null;
2047
2228
  this.rateLimiter = null;
2048
2229
  this.nameGovernor = new NameGovernor();
2230
+ this.traitGovernor = new TraitGovernor();
2049
2231
  this._lastPageUrl = null;
2050
2232
  this._popstateHandler = null;
2051
2233
  this._originalPushState = null;
@@ -2238,10 +2420,12 @@ class Aegis {
2238
2420
  }
2239
2421
  identify(userId, traits) {
2240
2422
  if (!this.assertInitialized()) return;
2241
- this.identity.setUserId(userId, traits);
2423
+ const wsForGovernor = this.config.workspace_id || null;
2424
+ const { sanitized: governedTraits } = this.traitGovernor.process(traits, wsForGovernor);
2425
+ this.identity.setUserId(userId, governedTraits);
2242
2426
  const event = {
2243
2427
  type: "identify",
2244
- traits: traits || {},
2428
+ traits: governedTraits,
2245
2429
  messageId: generateMessageId(),
2246
2430
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2247
2431
  anonymousId: this.identity.getAnonymousId(),
@@ -2270,10 +2454,12 @@ class Aegis {
2270
2454
  }
2271
2455
  group(groupId, traits) {
2272
2456
  if (!this.assertInitialized()) return;
2457
+ const wsForGovernor = this.config.workspace_id || null;
2458
+ const { sanitized: governedTraits } = this.traitGovernor.process(traits, wsForGovernor);
2273
2459
  const event = {
2274
2460
  type: "group",
2275
2461
  groupId,
2276
- traits: traits || {},
2462
+ traits: governedTraits,
2277
2463
  messageId: generateMessageId(),
2278
2464
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2279
2465
  anonymousId: this.identity.getAnonymousId(),
@@ -2597,4 +2783,4 @@ export {
2597
2783
  logger as l,
2598
2784
  murmurhash3_x86_32 as m
2599
2785
  };
2600
- //# sourceMappingURL=analytics-Cc4-QQBf.mjs.map
2786
+ //# sourceMappingURL=analytics-6PR9ERDS.mjs.map