@agent-native/core 0.92.5 → 0.92.7
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/template-assets.mdx +139 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +96 -6
- package/corpus/core/src/cli/sync-builder-starter-manifest.ts +1 -1
- package/corpus/core/src/client/AssistantChat.tsx +127 -15
- package/corpus/core/src/client/chat/tool-call-display.tsx +9 -3
- package/corpus/core/src/client/org/TeamPage.tsx +274 -177
- package/corpus/core/src/client/settings/DemoModeSection.tsx +6 -6
- package/corpus/core/src/client/settings/SettingsPanel.tsx +1 -1
- package/corpus/core/src/client/settings/SettingsTabsPage.tsx +49 -1
- package/corpus/core/src/demo/actions/toggle-demo-mode.ts +2 -2
- package/corpus/core/src/demo/config.ts +3 -3
- package/corpus/core/src/demo/redact.ts +720 -0
- package/corpus/core/src/extensions/url-safety.ts +13 -1
- package/corpus/core/src/localization/default-messages.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +7 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +7 -0
- package/corpus/templates/analytics/AGENTS.md +6 -3
- package/corpus/templates/analytics/changelog/2026-07-09-uptime-monitors-use-scheduled-workers-in-production-serverless.md +6 -0
- package/corpus/templates/analytics/docs/uptime-monitoring.md +5 -0
- package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +14 -2
- package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +35 -0
- package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +19 -0
- package/corpus/templates/assets/AGENTS.md +13 -0
- package/corpus/templates/assets/actions/_generation-preset-settings.ts +73 -0
- package/corpus/templates/assets/actions/_preset-skeleton-validation.ts +63 -0
- package/corpus/templates/assets/actions/create-generation-preset.ts +22 -8
- package/corpus/templates/assets/actions/duplicate-library.ts +49 -1
- package/corpus/templates/assets/actions/generate-image-batch.ts +14 -1
- package/corpus/templates/assets/actions/generate-image.ts +195 -26
- package/corpus/templates/assets/actions/import-asset-from-url.ts +323 -0
- package/corpus/templates/assets/actions/list-generation-presets.ts +1 -1
- package/corpus/templates/assets/actions/rerun-generation-run.ts +37 -0
- package/corpus/templates/assets/actions/update-generation-preset.ts +19 -1
- package/corpus/templates/assets/app/i18n/ar-SA.ts +1 -0
- package/corpus/templates/assets/app/i18n/de-DE.ts +1 -0
- package/corpus/templates/assets/app/i18n/es-ES.ts +1 -0
- package/corpus/templates/assets/app/i18n/fr-FR.ts +1 -0
- package/corpus/templates/assets/app/i18n/hi-IN.ts +1 -0
- package/corpus/templates/assets/app/i18n/ja-JP.ts +1 -0
- package/corpus/templates/assets/app/i18n/ko-KR.ts +1 -0
- package/corpus/templates/assets/app/i18n/pt-BR.ts +1 -0
- package/corpus/templates/assets/app/i18n/zh-CN.ts +1 -0
- package/corpus/templates/assets/app/i18n/zh-TW.ts +41 -0
- package/corpus/templates/assets/app/i18n-data.ts +93 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +66 -11
- package/corpus/templates/assets/app/routes/brand-kits.$id_.presets.$presetId.tsx +959 -24
- package/corpus/templates/assets/changelog/2026-07-07-preset-skeleton-controls-fit-correctly-on-narrower-edit-page.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-09-import-reference-images-from-url.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-09-preset-reference-board.md +6 -0
- package/corpus/templates/assets/server/handlers/assets.ts +7 -27
- package/corpus/templates/assets/server/lib/generation.ts +83 -2
- package/corpus/templates/assets/server/lib/preset-chat-context.ts +17 -4
- package/corpus/templates/assets/server/lib/preset-references.ts +182 -0
- package/corpus/templates/assets/server/lib/upload-validation.ts +38 -0
- package/corpus/templates/assets/shared/api.ts +20 -0
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +6 -0
- package/corpus/templates/calendar/actions/create-event.ts +8 -2
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +22 -7
- package/corpus/templates/calendar/changelog/2026-07-09-calendar-events-with-guests-now-automatically-get-a-google-m.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-09-new-events-now-mark-the-creator-rsvp-as-yes.md +6 -0
- package/corpus/templates/calendar/server/handlers/events.ts +9 -2
- package/corpus/templates/calendar/server/lib/event-video-conferencing.ts +58 -0
- package/corpus/templates/calendar/server/lib/google-calendar.ts +1 -0
- package/corpus/templates/clips/changelog/2026-07-09-meeting-reminder-popovers-can-now-be-dismissed-with-a-top-le.md +6 -0
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +3 -1
- package/corpus/templates/clips/desktop/src/styles.css +13 -9
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +5 -2
- package/dist/agent/production-agent.d.ts +34 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +58 -5
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/sync-builder-starter-manifest.js +1 -1
- package/dist/cli/sync-builder-starter-manifest.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +3 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +96 -15
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +5 -1
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/org/TeamPage.d.ts.map +1 -1
- package/dist/client/org/TeamPage.js +32 -13
- package/dist/client/org/TeamPage.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts +3 -3
- package/dist/client/settings/DemoModeSection.js +4 -4
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/SettingsTabsPage.d.ts.map +1 -1
- package/dist/client/settings/SettingsTabsPage.js +38 -2
- package/dist/client/settings/SettingsTabsPage.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/demo/actions/toggle-demo-mode.js +2 -2
- package/dist/demo/actions/toggle-demo-mode.js.map +1 -1
- package/dist/demo/config.js +3 -3
- package/dist/demo/config.js.map +1 -1
- package/dist/demo/redact.d.ts +1 -1
- package/dist/demo/redact.d.ts.map +1 -1
- package/dist/demo/redact.js +0 -0
- package/dist/demo/redact.js.map +1 -1
- package/dist/extensions/url-safety.d.ts +5 -0
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +10 -0
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/localization/default-messages.d.ts +7 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +7 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +7 -0
- package/dist/templates/default/app/i18n/de-DE.ts +7 -0
- package/dist/templates/default/app/i18n/en-US.ts +7 -0
- package/dist/templates/default/app/i18n/es-ES.ts +7 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +7 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +7 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +7 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +7 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +7 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +7 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +7 -0
- package/docs/content/template-assets.mdx +139 -0
- package/package.json +1 -1
- package/src/templates/default/app/i18n/ar-SA.ts +7 -0
- package/src/templates/default/app/i18n/de-DE.ts +7 -0
- package/src/templates/default/app/i18n/en-US.ts +7 -0
- package/src/templates/default/app/i18n/es-ES.ts +7 -0
- package/src/templates/default/app/i18n/fr-FR.ts +7 -0
- package/src/templates/default/app/i18n/hi-IN.ts +7 -0
- package/src/templates/default/app/i18n/ja-JP.ts +7 -0
- package/src/templates/default/app/i18n/ko-KR.ts +7 -0
- package/src/templates/default/app/i18n/pt-BR.ts +7 -0
- package/src/templates/default/app/i18n/zh-CN.ts +7 -0
- package/src/templates/default/app/i18n/zh-TW.ts +7 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, dependency-free, deterministic demo-mode redactor.
|
|
3
|
+
*
|
|
4
|
+
* Replaces sensitive values (names, emails, and free numbers) with stable fake
|
|
5
|
+
* substitutes so a demo looks coherent — the same input always maps to the
|
|
6
|
+
* same fake. Crucially, it NEVER rewrites identifiers, structural tokens, or
|
|
7
|
+
* timestamps: under-redaction is safe, corrupting an ID is not. The string
|
|
8
|
+
* redactor uses a protect-first strategy (mask IDs with opaque placeholders
|
|
9
|
+
* before any transform runs, restore them byte-identical afterwards), and the
|
|
10
|
+
* structure-aware walker additionally protects leaf values by key name.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface RedactOptions {
|
|
14
|
+
salt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* ------------------------------------------------------------------ *
|
|
18
|
+
* Seeded hash + PRNG (xmur3 seed → mulberry32 stream)
|
|
19
|
+
* ------------------------------------------------------------------ */
|
|
20
|
+
|
|
21
|
+
function xmur3(str: string): () => number {
|
|
22
|
+
let h = 1779033703 ^ str.length;
|
|
23
|
+
for (let i = 0; i < str.length; i++) {
|
|
24
|
+
h = Math.imul(h ^ str.charCodeAt(i), 3432918353);
|
|
25
|
+
h = (h << 13) | (h >>> 19);
|
|
26
|
+
}
|
|
27
|
+
return function () {
|
|
28
|
+
h = Math.imul(h ^ (h >>> 16), 2246822507);
|
|
29
|
+
h = Math.imul(h ^ (h >>> 13), 3266489909);
|
|
30
|
+
h ^= h >>> 16;
|
|
31
|
+
return h >>> 0;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mulberry32(seed: number): () => number {
|
|
36
|
+
let a = seed >>> 0;
|
|
37
|
+
return function () {
|
|
38
|
+
a = (a + 0x6d2b79f5) >>> 0;
|
|
39
|
+
let t = a;
|
|
40
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
41
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
42
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Deterministic PRNG seeded by `value + salt`. */
|
|
47
|
+
function seededRng(value: string, salt: string): () => number {
|
|
48
|
+
const seedFn = xmur3(`${value}${salt}`);
|
|
49
|
+
return mulberry32(seedFn());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function pick<T>(rng: () => number, pool: readonly T[]): T {
|
|
53
|
+
return pool[Math.floor(rng() * pool.length) % pool.length];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ------------------------------------------------------------------ *
|
|
57
|
+
* Stable mapping cache (bounded, TTL, leak-free)
|
|
58
|
+
*
|
|
59
|
+
* The fake values are already a pure deterministic function of
|
|
60
|
+
* (kind, salt, original), so identical input is always stable. This cache
|
|
61
|
+
* adds two things on top:
|
|
62
|
+
*
|
|
63
|
+
* 1. A consistent forward map so the same original keeps the same fake for
|
|
64
|
+
* the life of the entry, even if the algorithm/salt is ever tuned.
|
|
65
|
+
* 2. Idempotency: every fake we emit is remembered, so when a fake value
|
|
66
|
+
* round-trips back through redaction (e.g. you edit a draft that's
|
|
67
|
+
* already showing fake text, it autosaves, then refetches) it is passed
|
|
68
|
+
* through UNCHANGED instead of being re-faked into something new. This
|
|
69
|
+
* is what stops names/emails drifting on every edit.
|
|
70
|
+
*
|
|
71
|
+
* Leak-free by construction: no timers, a hard size cap, and lazy TTL purge
|
|
72
|
+
* on write. Works the same per-tab in the browser and per-process on the
|
|
73
|
+
* server. Memoizing a pure function across users is safe — output depends
|
|
74
|
+
* only on input.
|
|
75
|
+
* ------------------------------------------------------------------ */
|
|
76
|
+
|
|
77
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
78
|
+
const CACHE_MAX = 5000;
|
|
79
|
+
|
|
80
|
+
// original-key → { fake, at }. Insertion-ordered (Map) so the oldest key is
|
|
81
|
+
// first — used for cap eviction.
|
|
82
|
+
const forwardCache = new Map<string, { value: string; at: number }>();
|
|
83
|
+
// produced fake → last-seen timestamp (same bound/TTL policy).
|
|
84
|
+
const producedFakes = new Map<string, number>();
|
|
85
|
+
|
|
86
|
+
function purge(map: Map<string, { at: number } | number>): void {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
for (const [k, v] of map) {
|
|
89
|
+
const at = typeof v === "number" ? v : v.at;
|
|
90
|
+
if (now - at > CACHE_TTL_MS) map.delete(k);
|
|
91
|
+
else break; // insertion-ordered: first fresh entry ⇒ rest are fresher
|
|
92
|
+
}
|
|
93
|
+
while (map.size > CACHE_MAX) {
|
|
94
|
+
const oldest = map.keys().next().value as string | undefined;
|
|
95
|
+
if (oldest === undefined) break;
|
|
96
|
+
map.delete(oldest);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function rememberProduced(fake: string): string {
|
|
101
|
+
if (fake.length === 0) return fake;
|
|
102
|
+
producedFakes.delete(fake);
|
|
103
|
+
producedFakes.set(fake, Date.now());
|
|
104
|
+
if (producedFakes.size > CACHE_MAX) purge(producedFakes);
|
|
105
|
+
return fake;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isProducedFake(value: string): boolean {
|
|
109
|
+
const at = producedFakes.get(value);
|
|
110
|
+
if (at === undefined) return false;
|
|
111
|
+
if (Date.now() - at > CACHE_TTL_MS) {
|
|
112
|
+
producedFakes.delete(value);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Memoize a deterministic fake by (kind, salt, original). On a hit, the
|
|
120
|
+
* entry's recency is refreshed (LRU-ish). The generated value is also
|
|
121
|
+
* registered as a produced fake so it survives a round-trip unchanged.
|
|
122
|
+
*/
|
|
123
|
+
function memoFake(
|
|
124
|
+
kind: string,
|
|
125
|
+
original: string,
|
|
126
|
+
salt: string,
|
|
127
|
+
gen: () => string,
|
|
128
|
+
// Numbers opt out: a fake number collides with real numbers far too often
|
|
129
|
+
// to safely treat "looks like one we emitted" as "leave it alone".
|
|
130
|
+
idempotent = true,
|
|
131
|
+
): string {
|
|
132
|
+
// Already one of our fakes? Leave it exactly as-is (round-trip stable).
|
|
133
|
+
if (idempotent && isProducedFake(original)) return original;
|
|
134
|
+
|
|
135
|
+
const key = `${kind}${salt}${original}`;
|
|
136
|
+
const hit = forwardCache.get(key);
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
if (hit && now - hit.at <= CACHE_TTL_MS) {
|
|
139
|
+
forwardCache.delete(key);
|
|
140
|
+
forwardCache.set(key, { value: hit.value, at: now });
|
|
141
|
+
return hit.value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const value = gen();
|
|
145
|
+
forwardCache.set(key, { value, at: now });
|
|
146
|
+
if (forwardCache.size > CACHE_MAX) purge(forwardCache);
|
|
147
|
+
if (idempotent) rememberProduced(value);
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ------------------------------------------------------------------ *
|
|
152
|
+
* Curated name pools
|
|
153
|
+
* ------------------------------------------------------------------ */
|
|
154
|
+
|
|
155
|
+
const FIRST_NAMES: readonly string[] = [
|
|
156
|
+
"Jane",
|
|
157
|
+
"John",
|
|
158
|
+
"Alex",
|
|
159
|
+
"Maria",
|
|
160
|
+
"Sam",
|
|
161
|
+
"Olivia",
|
|
162
|
+
"Liam",
|
|
163
|
+
"Emma",
|
|
164
|
+
"Noah",
|
|
165
|
+
"Ava",
|
|
166
|
+
"Lucas",
|
|
167
|
+
"Mia",
|
|
168
|
+
"Ethan",
|
|
169
|
+
"Sofia",
|
|
170
|
+
"Mason",
|
|
171
|
+
"Isla",
|
|
172
|
+
"Leo",
|
|
173
|
+
"Aria",
|
|
174
|
+
"Henry",
|
|
175
|
+
"Zoe",
|
|
176
|
+
"Owen",
|
|
177
|
+
"Nora",
|
|
178
|
+
"Caleb",
|
|
179
|
+
"Lily",
|
|
180
|
+
"Ryan",
|
|
181
|
+
"Ruby",
|
|
182
|
+
"Adam",
|
|
183
|
+
"Chloe",
|
|
184
|
+
"Eli",
|
|
185
|
+
"Hazel",
|
|
186
|
+
"Max",
|
|
187
|
+
"Iris",
|
|
188
|
+
"Ben",
|
|
189
|
+
"Clara",
|
|
190
|
+
"Theo",
|
|
191
|
+
"Maya",
|
|
192
|
+
"Felix",
|
|
193
|
+
"Elena",
|
|
194
|
+
"Jonah",
|
|
195
|
+
"Greta",
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
const LAST_NAMES: readonly string[] = [
|
|
199
|
+
"Doe",
|
|
200
|
+
"Smith",
|
|
201
|
+
"Johnson",
|
|
202
|
+
"Lee",
|
|
203
|
+
"Brown",
|
|
204
|
+
"Garcia",
|
|
205
|
+
"Miller",
|
|
206
|
+
"Davis",
|
|
207
|
+
"Wilson",
|
|
208
|
+
"Moore",
|
|
209
|
+
"Taylor",
|
|
210
|
+
"Anderson",
|
|
211
|
+
"Thomas",
|
|
212
|
+
"Jackson",
|
|
213
|
+
"White",
|
|
214
|
+
"Harris",
|
|
215
|
+
"Martin",
|
|
216
|
+
"Clark",
|
|
217
|
+
"Lewis",
|
|
218
|
+
"Walker",
|
|
219
|
+
"Hall",
|
|
220
|
+
"Young",
|
|
221
|
+
"King",
|
|
222
|
+
"Wright",
|
|
223
|
+
"Hill",
|
|
224
|
+
"Green",
|
|
225
|
+
"Adams",
|
|
226
|
+
"Baker",
|
|
227
|
+
"Nelson",
|
|
228
|
+
"Carter",
|
|
229
|
+
"Mitchell",
|
|
230
|
+
"Perez",
|
|
231
|
+
"Roberts",
|
|
232
|
+
"Turner",
|
|
233
|
+
"Phillips",
|
|
234
|
+
"Campbell",
|
|
235
|
+
"Parker",
|
|
236
|
+
"Evans",
|
|
237
|
+
"Edwards",
|
|
238
|
+
"Collins",
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
/* ------------------------------------------------------------------ *
|
|
242
|
+
* Fake value generators (deterministic in value + salt)
|
|
243
|
+
* ------------------------------------------------------------------ */
|
|
244
|
+
|
|
245
|
+
function fakeName(original: string, salt: string): string {
|
|
246
|
+
return memoFake("name", original, salt, () => {
|
|
247
|
+
const rng = seededRng(`name:${original}`, salt);
|
|
248
|
+
const first = pick(rng, FIRST_NAMES);
|
|
249
|
+
const last = pick(rng, LAST_NAMES);
|
|
250
|
+
return `${first} ${last}`;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Realistic-looking but safe stand-in domains: real consumer providers (the
|
|
255
|
+
// local part is fake, so no real mailbox is implied) plus the canonical
|
|
256
|
+
// reserved fictional-company domains (Contoso/Fabrikam/Northwind), which read
|
|
257
|
+
// as real businesses but are intentional placeholders. No example.com.
|
|
258
|
+
const FAKE_EMAIL_DOMAINS: readonly string[] = [
|
|
259
|
+
"gmail.com",
|
|
260
|
+
"outlook.com",
|
|
261
|
+
"icloud.com",
|
|
262
|
+
"yahoo.com",
|
|
263
|
+
"proton.me",
|
|
264
|
+
"hey.com",
|
|
265
|
+
"fastmail.com",
|
|
266
|
+
"contoso.com",
|
|
267
|
+
"fabrikam.com",
|
|
268
|
+
"northwind.com",
|
|
269
|
+
"acme.io",
|
|
270
|
+
"globex.net",
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
function fakeEmail(original: string, salt: string): string {
|
|
274
|
+
return memoFake("email", original.toLowerCase(), salt, () => {
|
|
275
|
+
const rng = seededRng(`email:${original.toLowerCase()}`, salt);
|
|
276
|
+
const first = pick(rng, FIRST_NAMES).toLowerCase();
|
|
277
|
+
const last = pick(rng, LAST_NAMES).toLowerCase();
|
|
278
|
+
const domain = pick(rng, FAKE_EMAIL_DOMAINS);
|
|
279
|
+
// Vary the local part so addresses don't all look templated.
|
|
280
|
+
const shape = Math.floor(rng() * 6);
|
|
281
|
+
let local: string;
|
|
282
|
+
switch (shape) {
|
|
283
|
+
case 0:
|
|
284
|
+
local = `${first}.${last}`;
|
|
285
|
+
break;
|
|
286
|
+
case 1:
|
|
287
|
+
local = `${first}${last}`;
|
|
288
|
+
break;
|
|
289
|
+
case 2:
|
|
290
|
+
local = `${first[0]}${last}`;
|
|
291
|
+
break;
|
|
292
|
+
case 3:
|
|
293
|
+
local = `${first}.${last}${1 + Math.floor(rng() * 89)}`;
|
|
294
|
+
break;
|
|
295
|
+
case 4:
|
|
296
|
+
local = `${first}_${last}`;
|
|
297
|
+
break;
|
|
298
|
+
default:
|
|
299
|
+
local = `${last}.${first}`;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
return `${local}@${domain}`;
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Replace every digit in `numericBody` with a freshly-generated digit while
|
|
308
|
+
* preserving non-digit characters (grouping commas, decimal points) exactly.
|
|
309
|
+
* The first digit is forced non-zero so the digit count is observable.
|
|
310
|
+
*/
|
|
311
|
+
function fakeNumberBody(
|
|
312
|
+
numericBody: string,
|
|
313
|
+
original: string,
|
|
314
|
+
salt: string,
|
|
315
|
+
): string {
|
|
316
|
+
// Stable per original token, but NOT registered as a produced fake — a
|
|
317
|
+
// fake number coincides with real numbers too often to skip on round-trip.
|
|
318
|
+
return memoFake(
|
|
319
|
+
"num",
|
|
320
|
+
original,
|
|
321
|
+
salt,
|
|
322
|
+
() => {
|
|
323
|
+
const rng = seededRng(`num:${original}`, salt);
|
|
324
|
+
let out = "";
|
|
325
|
+
let seenDigit = false;
|
|
326
|
+
for (const ch of numericBody) {
|
|
327
|
+
if (ch >= "0" && ch <= "9") {
|
|
328
|
+
let d: number;
|
|
329
|
+
if (!seenDigit) {
|
|
330
|
+
// Leading digit: 1-9 so length is preserved.
|
|
331
|
+
d = 1 + Math.floor(rng() * 9);
|
|
332
|
+
seenDigit = true;
|
|
333
|
+
} else {
|
|
334
|
+
d = Math.floor(rng() * 10);
|
|
335
|
+
}
|
|
336
|
+
out += String(d);
|
|
337
|
+
} else {
|
|
338
|
+
out += ch;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
},
|
|
343
|
+
false,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ------------------------------------------------------------------ *
|
|
348
|
+
* Protect-first tokenizer (ID-safety core)
|
|
349
|
+
* ------------------------------------------------------------------ */
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Patterns whose matches must be protected from ANY transform. Order matters:
|
|
353
|
+
* the most structural / specific shapes come first so they win the scan.
|
|
354
|
+
*/
|
|
355
|
+
const PROTECT_PATTERNS: RegExp[] = [
|
|
356
|
+
// URLs / URIs with a scheme.
|
|
357
|
+
/\b[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s]+/g,
|
|
358
|
+
/\b(?:mailto|data|tel|urn):[^\s]+/gi,
|
|
359
|
+
// UUID.
|
|
360
|
+
/\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g,
|
|
361
|
+
// JWT — three base64url segments separated by dots.
|
|
362
|
+
/\b[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}\b/g,
|
|
363
|
+
// ISO datetime / date.
|
|
364
|
+
/\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?Z?)?\b/g,
|
|
365
|
+
// Bare clock time.
|
|
366
|
+
/\b\d{1,2}:\d{2}(?::\d{2})?\b/g,
|
|
367
|
+
// Path-like tokens containing a slash (but not bare prose with slashes —
|
|
368
|
+
// require no spaces and at least one slash with adjacent non-space).
|
|
369
|
+
/(?:\S*\/\S+)+/g,
|
|
370
|
+
// nanoid / hex / base64-ish blobs: an unbroken [A-Za-z0-9_-] run that is
|
|
371
|
+
// either long-with-a-digit, mixes letters AND digits at length >= 10, or
|
|
372
|
+
// contains a `_`/`-` inside the run (real names/numbers never look so).
|
|
373
|
+
/[A-Za-z0-9_-]+/g,
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
const PLACEHOLDER_PREFIX = "P";
|
|
377
|
+
const PLACEHOLDER_SUFFIX = "";
|
|
378
|
+
|
|
379
|
+
function makePlaceholder(index: number): string {
|
|
380
|
+
return `${PLACEHOLDER_PREFIX}${index}${PLACEHOLDER_SUFFIX}`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** True if a `[A-Za-z0-9_-]+` run is identifier-shaped (must be protected). */
|
|
384
|
+
function looksLikeIdentifierToken(tok: string): boolean {
|
|
385
|
+
if (tok.length < 3) return false;
|
|
386
|
+
const hasLetter = /[A-Za-z]/.test(tok);
|
|
387
|
+
const hasDigit = /[0-9]/.test(tok);
|
|
388
|
+
const hasSep = /[_-]/.test(tok);
|
|
389
|
+
|
|
390
|
+
// A pure number (optionally with separators handled elsewhere) is NOT an
|
|
391
|
+
// identifier here — the number rule handles those.
|
|
392
|
+
if (!hasLetter && !hasSep) return false;
|
|
393
|
+
|
|
394
|
+
// Long hex/base64-ish blob with a digit.
|
|
395
|
+
if (tok.length >= 16 && hasDigit) return true;
|
|
396
|
+
// nanoid-ish: length >= 10 mixing letters AND digits.
|
|
397
|
+
if (tok.length >= 10 && hasLetter && hasDigit) return true;
|
|
398
|
+
// Any token that has a `_`/`-` joined inside an unbroken run AND also
|
|
399
|
+
// contains a digit or is long — e.g. `order-2024-abc`, `api_key_v2`.
|
|
400
|
+
if (hasSep && (hasDigit || tok.length >= 10)) return true;
|
|
401
|
+
// Mixed letters+digits adjacency (e.g. `abc123`, `v2`, `step3`) — protect so
|
|
402
|
+
// the number rule never bites an embedded number.
|
|
403
|
+
if (hasLetter && hasDigit) return true;
|
|
404
|
+
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
interface Protection {
|
|
409
|
+
text: string;
|
|
410
|
+
restore: Map<string, string>;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Walk `text`, replace every protected substring with an opaque placeholder,
|
|
415
|
+
* and return the masked text plus a restore map. Non-overlapping, left-to-right
|
|
416
|
+
* earliest-match-wins so a transform literally cannot see a protected value.
|
|
417
|
+
*/
|
|
418
|
+
function protect(text: string): Protection {
|
|
419
|
+
const restore = new Map<string, string>();
|
|
420
|
+
let counter = 0;
|
|
421
|
+
|
|
422
|
+
// Collect all candidate matches across patterns, then resolve overlaps by
|
|
423
|
+
// earliest start (and longest on tie).
|
|
424
|
+
interface Span {
|
|
425
|
+
start: number;
|
|
426
|
+
end: number;
|
|
427
|
+
value: string;
|
|
428
|
+
}
|
|
429
|
+
const spans: Span[] = [];
|
|
430
|
+
|
|
431
|
+
for (let p = 0; p < PROTECT_PATTERNS.length; p++) {
|
|
432
|
+
const re = new RegExp(
|
|
433
|
+
PROTECT_PATTERNS[p].source,
|
|
434
|
+
PROTECT_PATTERNS[p].flags,
|
|
435
|
+
);
|
|
436
|
+
let m: RegExpExecArray | null;
|
|
437
|
+
while ((m = re.exec(text)) !== null) {
|
|
438
|
+
const value = m[0];
|
|
439
|
+
if (value.length === 0) {
|
|
440
|
+
re.lastIndex++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const isTokenPattern = p === PROTECT_PATTERNS.length - 1;
|
|
444
|
+
if (isTokenPattern && !looksLikeIdentifierToken(value)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
spans.push({ start: m.index, end: m.index + value.length, value });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (spans.length === 0) return { text, restore };
|
|
452
|
+
|
|
453
|
+
spans.sort((a, b) =>
|
|
454
|
+
a.start !== b.start ? a.start - b.start : b.end - a.end,
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
let out = "";
|
|
458
|
+
let cursor = 0;
|
|
459
|
+
for (const span of spans) {
|
|
460
|
+
if (span.start < cursor) continue; // overlapped by an earlier protection
|
|
461
|
+
out += text.slice(cursor, span.start);
|
|
462
|
+
const ph = makePlaceholder(counter++);
|
|
463
|
+
restore.set(ph, span.value);
|
|
464
|
+
out += ph;
|
|
465
|
+
cursor = span.end;
|
|
466
|
+
}
|
|
467
|
+
out += text.slice(cursor);
|
|
468
|
+
|
|
469
|
+
return { text: out, restore };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function unprotect(text: string, restore: Map<string, string>): string {
|
|
473
|
+
if (restore.size === 0) return text;
|
|
474
|
+
let out = text;
|
|
475
|
+
for (const [ph, original] of restore) {
|
|
476
|
+
out = out.split(ph).join(original);
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* ------------------------------------------------------------------ *
|
|
482
|
+
* Transforms (run only on protected text)
|
|
483
|
+
* ------------------------------------------------------------------ */
|
|
484
|
+
|
|
485
|
+
const EMAIL_RE = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g;
|
|
486
|
+
|
|
487
|
+
// 2+ capitalized words (each starts uppercase, allows internal lowercase /
|
|
488
|
+
// apostrophes / hyphens). Single-letter middle initials allowed: `Sarah J Connor`.
|
|
489
|
+
const FULL_NAME_RE = /\b[A-Z][a-zA-Z'’-]*(?:\s+[A-Z](?:[a-zA-Z'’-]*)?){1,3}\b/g;
|
|
490
|
+
|
|
491
|
+
// A standalone numeric token: optional currency + sign, digits with optional
|
|
492
|
+
// comma grouping and a single optional decimal part. Bounded so it is NOT
|
|
493
|
+
// adjacent to a letter (placeholders already removed letter-mixed tokens, but
|
|
494
|
+
// this keeps the rule self-contained and safe on raw structured values too).
|
|
495
|
+
const NUMBER_RE =
|
|
496
|
+
/(^|[^A-Za-z0-9_])([$€£]?)([+-]?)(\d[\d,]*(?:\.\d+)?)(?![A-Za-z0-9_])/g;
|
|
497
|
+
|
|
498
|
+
function transformEmails(text: string, salt: string): string {
|
|
499
|
+
return text.replace(EMAIL_RE, (match) => fakeEmail(match, salt));
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function transformNames(text: string, salt: string): string {
|
|
503
|
+
return text.replace(FULL_NAME_RE, (match) => fakeName(match, salt));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function isProbablyYear(digits: string): boolean {
|
|
507
|
+
if (digits.length !== 4) return false;
|
|
508
|
+
const n = Number(digits);
|
|
509
|
+
return n >= 1900 && n <= 2099;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function transformNumbers(text: string, salt: string): string {
|
|
513
|
+
return text.replace(
|
|
514
|
+
NUMBER_RE,
|
|
515
|
+
(_full, pre: string, currency: string, sign: string, body: string) => {
|
|
516
|
+
const hasGrouping = body.includes(",");
|
|
517
|
+
const hasDecimal = body.includes(".");
|
|
518
|
+
const digitsOnly = body.replace(/[^0-9]/g, "");
|
|
519
|
+
|
|
520
|
+
// Skip bare years like 2026.
|
|
521
|
+
if (
|
|
522
|
+
!currency &&
|
|
523
|
+
!sign &&
|
|
524
|
+
!hasGrouping &&
|
|
525
|
+
!hasDecimal &&
|
|
526
|
+
isProbablyYear(digitsOnly)
|
|
527
|
+
) {
|
|
528
|
+
return `${pre}${currency}${sign}${body}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Skip standalone integers < 1000 with no currency and no grouping
|
|
532
|
+
// (rewriting "3 unread" / "page 2" looks broken and isn't sensitive).
|
|
533
|
+
if (!currency && !hasGrouping && !hasDecimal) {
|
|
534
|
+
const n = Number(digitsOnly);
|
|
535
|
+
if (Number.isFinite(n) && n < 1000) {
|
|
536
|
+
return `${pre}${currency}${sign}${body}`;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const fakeBody = fakeNumberBody(body, `${currency}${sign}${body}`, salt);
|
|
541
|
+
return `${pre}${currency}${sign}${fakeBody}`;
|
|
542
|
+
},
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/* ------------------------------------------------------------------ *
|
|
547
|
+
* Public: string redactor
|
|
548
|
+
* ------------------------------------------------------------------ */
|
|
549
|
+
|
|
550
|
+
function redactDemoStringInternal(
|
|
551
|
+
text: string,
|
|
552
|
+
salt: string,
|
|
553
|
+
redactNames: boolean,
|
|
554
|
+
): string {
|
|
555
|
+
if (typeof text !== "string" || text.length === 0) return text;
|
|
556
|
+
|
|
557
|
+
const { text: masked, restore } = protect(text);
|
|
558
|
+
let out = masked;
|
|
559
|
+
out = transformEmails(out, salt);
|
|
560
|
+
if (redactNames) out = transformNames(out, salt);
|
|
561
|
+
out = transformNumbers(out, salt);
|
|
562
|
+
return unprotect(out, restore);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
export function redactDemoString(text: string, opts?: RedactOptions): string {
|
|
566
|
+
return redactDemoStringInternal(text, opts?.salt ?? "", true);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/* ------------------------------------------------------------------ *
|
|
570
|
+
* Public: structure-aware redactor
|
|
571
|
+
* ------------------------------------------------------------------ */
|
|
572
|
+
|
|
573
|
+
// Keys whose leaf values must NEVER be transformed (still recurse into nested
|
|
574
|
+
// objects/arrays under them). Besides ids/urls/timestamps this also covers
|
|
575
|
+
// code/query-bearing keys (`sql`, `query`, `expression`, `formula`, `code`):
|
|
576
|
+
// prose-redacting a SQL string injects fake emails/numbers into it and rewrites
|
|
577
|
+
// its leading tokens, producing `syntax error at or near "Henry"` /
|
|
578
|
+
// `queries must start with SELECT or WITH`. The query must run untouched; its
|
|
579
|
+
// RESULTS are what get redacted so the chart still shows fake values.
|
|
580
|
+
const PROTECTED_KEY_RE =
|
|
581
|
+
/^id$|(^|_)id$|Id$|Ids$|uuid|guid|slug|token|secret|password|passwd|apikey|api_key|hash|sha\d*|etag|cursor|nonce|sessionid|messageid|threadid|nodeid|(^|_)key$|keyid|(^|_)ref$|url$|uri$|href$|src$|path$|filename$|mimetype|mime|^sql$|sql$|query|expression|formula|^code$|createdat|updatedat|deletedat|expiresat|timestamp|.+at$|.+_at$/i;
|
|
582
|
+
|
|
583
|
+
// UI labels and chart/dashboard titles often look like person names ("Maya
|
|
584
|
+
// Davis (First-party)", "Clicks by Henry Moore") but are structural labels. Keep
|
|
585
|
+
// them stable while still redacting free-text and contact-style person fields.
|
|
586
|
+
const LABEL_TEXT_KEY_RE = /^(name|title|label)$/i;
|
|
587
|
+
|
|
588
|
+
const MAX_DEPTH = 64;
|
|
589
|
+
|
|
590
|
+
function isStandalonePersonNameLabel(text: string): boolean {
|
|
591
|
+
const trimmed = text.trim();
|
|
592
|
+
if (!trimmed) return false;
|
|
593
|
+
return /^[A-Z][a-zA-Z'’-]+(?:\s+[A-Z]\.?)?\s+[A-Z][a-zA-Z'’-]+$/.test(
|
|
594
|
+
trimmed,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function shouldRedactNamesForChild(key: string, entry: unknown): boolean {
|
|
599
|
+
if (!LABEL_TEXT_KEY_RE.test(key)) return true;
|
|
600
|
+
// Preserve structural labels/titles, but do not let common contact-shaped
|
|
601
|
+
// fields like `{ name: "Olivia Parker" }` leak real person names.
|
|
602
|
+
return typeof entry === "string" ? isStandalonePersonNameLabel(entry) : true;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
606
|
+
if (value === null || typeof value !== "object") return false;
|
|
607
|
+
if (Array.isArray(value)) return false;
|
|
608
|
+
if (value instanceof Date) return false;
|
|
609
|
+
const proto = Object.getPrototypeOf(value);
|
|
610
|
+
return proto === Object.prototype || proto === null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function redactDemoStringWithSalt(
|
|
614
|
+
text: string,
|
|
615
|
+
salt: string,
|
|
616
|
+
redactNames: boolean,
|
|
617
|
+
): string {
|
|
618
|
+
return redactDemoStringInternal(text, salt, redactNames);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function redactNumberLeaf(value: number, salt: string): number {
|
|
622
|
+
if (!Number.isFinite(value)) return value;
|
|
623
|
+
const repr = String(value);
|
|
624
|
+
// Re-use the string number transform but only if the representation is a
|
|
625
|
+
// clean numeric token we can round-trip back to a JS number.
|
|
626
|
+
const redacted = transformNumbers(repr, salt);
|
|
627
|
+
if (redacted === repr) return value;
|
|
628
|
+
const n = Number(redacted);
|
|
629
|
+
if (!Number.isFinite(n)) return value;
|
|
630
|
+
return n;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function walk(
|
|
634
|
+
value: unknown,
|
|
635
|
+
salt: string,
|
|
636
|
+
depth: number,
|
|
637
|
+
seen: WeakSet<object>,
|
|
638
|
+
redactNames: boolean,
|
|
639
|
+
): unknown {
|
|
640
|
+
if (depth > MAX_DEPTH) return value;
|
|
641
|
+
|
|
642
|
+
if (value === null || value === undefined) return value;
|
|
643
|
+
|
|
644
|
+
const t = typeof value;
|
|
645
|
+
|
|
646
|
+
if (t === "string") {
|
|
647
|
+
return redactDemoStringWithSalt(value as string, salt, redactNames);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (t === "number") {
|
|
651
|
+
return redactNumberLeaf(value as number, salt);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (t === "boolean" || t === "bigint" || t === "function" || t === "symbol") {
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (value instanceof Date) return value;
|
|
659
|
+
|
|
660
|
+
if (Array.isArray(value)) {
|
|
661
|
+
if (seen.has(value)) return value;
|
|
662
|
+
seen.add(value);
|
|
663
|
+
const out = value.map((item) =>
|
|
664
|
+
walk(item, salt, depth + 1, seen, redactNames),
|
|
665
|
+
);
|
|
666
|
+
seen.delete(value);
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (isPlainObject(value)) {
|
|
671
|
+
if (seen.has(value)) return value;
|
|
672
|
+
seen.add(value);
|
|
673
|
+
const out: Record<string, unknown> = {};
|
|
674
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
675
|
+
const keyIsProtected = PROTECTED_KEY_RE.test(key);
|
|
676
|
+
if (keyIsProtected) {
|
|
677
|
+
if (
|
|
678
|
+
entry !== null &&
|
|
679
|
+
typeof entry === "object" &&
|
|
680
|
+
!(entry instanceof Date)
|
|
681
|
+
) {
|
|
682
|
+
// Still recurse into nested structures, but the protected key does
|
|
683
|
+
// not transform its own leaf value.
|
|
684
|
+
out[key] = walk(entry, salt, depth + 1, seen, redactNames);
|
|
685
|
+
} else {
|
|
686
|
+
// Leaf under a protected key: pass through completely untouched.
|
|
687
|
+
out[key] = entry;
|
|
688
|
+
}
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
out[key] = walk(
|
|
692
|
+
entry,
|
|
693
|
+
salt,
|
|
694
|
+
depth + 1,
|
|
695
|
+
seen,
|
|
696
|
+
redactNames && shouldRedactNamesForChild(key, entry),
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
seen.delete(value);
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Unknown object kind (Map, Set, class instance, etc.) — leave untouched.
|
|
704
|
+
return value;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function redactDemoData<T>(value: T, opts?: RedactOptions): T {
|
|
708
|
+
const salt = opts?.salt ?? "";
|
|
709
|
+
return walk(value, salt, 0, new WeakSet<object>(), true) as T;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Clear the stable-mapping caches. Test-only — the caches are process-global
|
|
714
|
+
* (intentionally, so mappings stay stable for a tab's session), which would
|
|
715
|
+
* otherwise let one test's produced fakes leak into another's assertions.
|
|
716
|
+
*/
|
|
717
|
+
export function __resetDemoRedactCacheForTests(): void {
|
|
718
|
+
forwardCache.clear();
|
|
719
|
+
producedFakes.clear();
|
|
720
|
+
}
|