@offscript/sdk 0.1.1 → 0.1.2

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.
Files changed (37) hide show
  1. package/README.md +3 -3
  2. package/dist/adapters/nuxt.cjs +3 -3
  3. package/dist/adapters/nuxt.js +2 -2
  4. package/dist/adapters/vanilla.cjs +5 -5
  5. package/dist/adapters/vanilla.js +3 -3
  6. package/dist/adapters/vue.cjs +3 -3
  7. package/dist/adapters/vue.js +2 -2
  8. package/dist/{chunk-MBYNQSKA.js → chunk-HECPMGDC.js} +3 -3
  9. package/dist/{chunk-MBYNQSKA.js.map → chunk-HECPMGDC.js.map} +1 -1
  10. package/dist/{chunk-3NX2AGXM.cjs → chunk-I6JPZAQA.cjs} +738 -31
  11. package/dist/chunk-I6JPZAQA.cjs.map +1 -0
  12. package/dist/{chunk-3OSCH7J2.js → chunk-IBEPWI7S.js} +729 -22
  13. package/dist/chunk-IBEPWI7S.js.map +1 -0
  14. package/dist/{chunk-B6GO5FE5.cjs → chunk-KQVK5EJR.cjs} +4 -3
  15. package/dist/chunk-KQVK5EJR.cjs.map +1 -0
  16. package/dist/{chunk-HAPNSHFG.cjs → chunk-TBPD2LMI.cjs} +4 -4
  17. package/dist/{chunk-HAPNSHFG.cjs.map → chunk-TBPD2LMI.cjs.map} +1 -1
  18. package/dist/{chunk-LFXHB4PN.js → chunk-WUFW7TEL.js} +4 -3
  19. package/dist/chunk-WUFW7TEL.js.map +1 -0
  20. package/dist/core/OffscriptSDK.d.ts +19 -0
  21. package/dist/core/SessionManager.d.ts +1 -0
  22. package/dist/index.cjs +25 -25
  23. package/dist/index.js +3 -3
  24. package/dist/playwright.cjs +13 -13
  25. package/dist/playwright.js +1 -1
  26. package/dist/privacy/DataSanitizer.d.ts +1 -0
  27. package/dist/recorders/ContextRecorder.d.ts +1 -1
  28. package/dist/types/config.d.ts +11 -0
  29. package/dist/types/events.d.ts +2 -0
  30. package/dist/types/session.d.ts +2 -2
  31. package/dist/ui/RecordingControls.d.ts +60 -0
  32. package/docs/api-contract.md +1 -1
  33. package/package.json +1 -1
  34. package/dist/chunk-3NX2AGXM.cjs.map +0 -1
  35. package/dist/chunk-3OSCH7J2.js.map +0 -1
  36. package/dist/chunk-B6GO5FE5.cjs.map +0 -1
  37. package/dist/chunk-LFXHB4PN.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { buildPlaywrightContract, SDK_VERSION, SCHEMA_VERSION, SelectorGenerator, ACTION_SCHEMA_VERSION } from './chunk-LFXHB4PN.js';
1
+ import { buildPlaywrightContract, SDK_VERSION, SCHEMA_VERSION, SelectorGenerator, ACTION_SCHEMA_VERSION } from './chunk-WUFW7TEL.js';
2
2
 
3
3
  // src/types/config.ts
4
4
  var DEFAULT_RECORDING = {
@@ -18,7 +18,7 @@ var DEFAULT_RECORDING = {
18
18
  stableAttributes: ["name", "data-id", "aria-label"]
19
19
  };
20
20
  var DEFAULT_PRIVACY = {
21
- maskSensitiveInputs: true,
21
+ maskSensitiveInputs: false,
22
22
  maskAllText: false,
23
23
  captureRequestBodies: false,
24
24
  captureResponseBodies: false,
@@ -30,6 +30,7 @@ var DEFAULT_PRIVACY = {
30
30
  ignoreUrls: [],
31
31
  redactHeaders: [],
32
32
  redactKeys: [],
33
+ captureSensitiveValues: false,
33
34
  maxBodyBytes: 16 * 1024,
34
35
  textContentTypes: [/^text\//i, /json/i, /xml/i, /x-www-form-urlencoded/i, /javascript/i],
35
36
  localRetentionMs: 24 * 36e5
@@ -159,13 +160,15 @@ var DataSanitizer = class {
159
160
  this.cfg = cfg;
160
161
  this.headerSet = /* @__PURE__ */ new Set([...BASE_SENSITIVE_HEADERS, ...cfg.redactHeaders.map((h) => h.toLowerCase())]);
161
162
  this.extraKeys = new Set(cfg.redactKeys.map((k) => k.toLowerCase()));
163
+ this.open = cfg.captureSensitiveValues === true;
162
164
  }
163
165
  isSensitiveKey(key) {
166
+ if (this.open) return false;
164
167
  return BASE_SENSITIVE_KEY_PATTERN.test(key) || this.extraKeys.has(key.toLowerCase());
165
168
  }
166
169
  /** Mascara padrões sensíveis em texto livre (JWT, Bearer, CPF, CNPJ, cartões). */
167
170
  scrubString(input) {
168
- if (!input) return input;
171
+ if (this.open || !input) return input;
169
172
  let s = input.replace(JWT, REDACTED).replace(BEARER, (_m, scheme) => `${scheme} ${REDACTED}`);
170
173
  s = s.replace(CNPJ, REDACTED).replace(CPF, REDACTED);
171
174
  s = s.replace(CARD, (m) => {
@@ -175,6 +178,7 @@ var DataSanitizer = class {
175
178
  return s;
176
179
  }
177
180
  sanitizeHeaders(headers) {
181
+ if (this.open) return { ...headers };
178
182
  const out = {};
179
183
  for (const [k, v] of Object.entries(headers)) {
180
184
  out[k] = this.headerSet.has(k.toLowerCase()) || this.isSensitiveKey(k) ? REDACTED : this.scrubString(v);
@@ -183,6 +187,7 @@ var DataSanitizer = class {
183
187
  }
184
188
  /** Redige query params sensíveis e credenciais embutidas (user:pass@). */
185
189
  sanitizeUrl(raw, base) {
190
+ if (this.open) return raw;
186
191
  try {
187
192
  const u = new URL(raw, base ?? (typeof location !== "undefined" ? location.href : void 0));
188
193
  if (u.username || u.password) {
@@ -201,7 +206,7 @@ var DataSanitizer = class {
201
206
  }
202
207
  /** Redige query/fragmento de um valor de atributo de URL preservando a forma original (relativa ou absoluta). */
203
208
  redactUrlAttribute(value) {
204
- if (typeof value !== "string" || !/[?#@]/.test(value)) return value;
209
+ if (this.open || typeof value !== "string" || !/[?#@]/.test(value)) return value;
205
210
  const hashAt = value.indexOf("#");
206
211
  const hash = hashAt >= 0 ? value.slice(hashAt) : "";
207
212
  const noHash = hashAt >= 0 ? value.slice(0, hashAt) : value;
@@ -958,6 +963,7 @@ function guard(sink, name, fn) {
958
963
 
959
964
  // src/recorders/ActionRecorder.ts
960
965
  var KEYS = /* @__PURE__ */ new Set(["Enter", "Escape", "Tab", "Backspace", "Delete", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"]);
966
+ var MODIFIERS = /* @__PURE__ */ new Set(["Control", "Shift", "Alt", "Meta"]);
961
967
  var INTERACTIVE = "a,button,input,select,textarea,summary,[role=button],[role=link],[role=menuitem],[role=tab],[role=option],[data-testid],[data-test],[data-cy]";
962
968
  var INPUT_IDLE_MS = 700;
963
969
  var DRAG_MIN_PX = 8;
@@ -1083,7 +1089,7 @@ var ActionRecorder = class {
1083
1089
  onInput(e) {
1084
1090
  const el = this.target(e);
1085
1091
  if (!el || !(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) return;
1086
- if (el instanceof HTMLInputElement && ["checkbox", "radio", "file", "range", "color"].includes(el.type)) return;
1092
+ if (el instanceof HTMLInputElement && ["checkbox", "radio", "file"].includes(el.type)) return;
1087
1093
  if (this.pendingInput && this.pendingInput.el !== el) this.flushInput();
1088
1094
  this.pendingInput ?? (this.pendingInput = { el, inputType: e.inputType ?? "input", startedAt: Date.now() });
1089
1095
  if (this.inputTimer) clearTimeout(this.inputTimer);
@@ -1101,8 +1107,9 @@ var ActionRecorder = class {
1101
1107
  const policy = this.sink.config.privacy;
1102
1108
  const allowed = policy.allowedInputSelectors.join(",");
1103
1109
  const isPassword = el instanceof HTMLInputElement && el.type === "password";
1104
- if (!isPassword && allowed && safe(() => el.matches(allowed))) {
1105
- payload.value = this.sink.sanitizer.scrubString(value);
1110
+ const listed = Boolean(allowed) && safe(() => el.matches(allowed));
1111
+ if (policy.captureSensitiveValues || !isPassword && (!policy.maskSensitiveInputs || listed)) {
1112
+ payload.value = policy.captureSensitiveValues ? value : this.sink.sanitizer.scrubString(value);
1106
1113
  payload.valueCaptured = true;
1107
1114
  } else {
1108
1115
  payload.valueCaptured = false;
@@ -1130,13 +1137,15 @@ var ActionRecorder = class {
1130
1137
  }
1131
1138
  }
1132
1139
  onKey(e) {
1133
- const el = this.target(e);
1140
+ const el = this.target(e) ?? document.body;
1134
1141
  if (!el) return;
1135
1142
  const modified = e.ctrlKey || e.metaKey || e.altKey;
1136
- const relevant = KEYS.has(e.key) || /^F\d{1,2}$/.test(e.key) || modified && e.key.length === 1;
1137
- if (!relevant) return;
1143
+ const isModifier = MODIFIERS.has(e.key);
1144
+ const relevant = KEYS.has(e.key) || isModifier || /^F\d{1,2}$/.test(e.key) || modified && e.key.length === 1;
1145
+ if (!relevant || isModifier && e.repeat) return;
1138
1146
  if (e.key === "Enter" || e.key === "Tab") this.flushInput();
1139
- this.emit({ type: "keydown", payload: { key: e.key, code: e.code, modifiers: mods(e), repeat: e.repeat } }, el);
1147
+ const modifiers = mods(e).filter((name) => name !== e.key);
1148
+ this.emit({ type: "keydown", payload: { key: e.key, code: e.code, modifiers, repeat: e.repeat } }, el);
1140
1149
  }
1141
1150
  onFocus(type, e) {
1142
1151
  const el = this.target(e);
@@ -1269,7 +1278,7 @@ function parseBrowser(ua) {
1269
1278
  }
1270
1279
  return {};
1271
1280
  }
1272
- function collectContext(id, reason, appVersion) {
1281
+ function collectContext(id, reason, appVersion, captureSensitiveValues = false) {
1273
1282
  const nav = navigator;
1274
1283
  const ua = nav.userAgent ?? "";
1275
1284
  let browser = parseBrowser(ua);
@@ -1297,7 +1306,8 @@ function collectContext(id, reason, appVersion) {
1297
1306
  testCaseId: id.testCaseId,
1298
1307
  executionId: id.executionId,
1299
1308
  sdkVersion: SDK_VERSION,
1300
- appVersion
1309
+ appVersion,
1310
+ captureSensitiveValues
1301
1311
  };
1302
1312
  }
1303
1313
  var ContextRecorder = class {
@@ -1316,7 +1326,7 @@ var ContextRecorder = class {
1316
1326
  }
1317
1327
  record(reason) {
1318
1328
  guard(this.sink, this.name, () => {
1319
- const data = collectContext(this.identity(), reason, this.sink.config.appVersion);
1329
+ const data = collectContext(this.identity(), reason, this.sink.config.appVersion, this.sink.config.privacy.captureSensitiveValues);
1320
1330
  data.url = this.sink.sanitizer.sanitizeUrl(data.url);
1321
1331
  if (data.referrer) data.referrer = this.sink.sanitizer.sanitizeUrl(data.referrer);
1322
1332
  this.sink.emit("context", data, { priority: "normal" });
@@ -1870,9 +1880,10 @@ var RRWebRecorder = class {
1870
1880
  emit: (event) => this.onEvent(event),
1871
1881
  blockSelector: block || void 0,
1872
1882
  maskTextSelector: maskText || void 0,
1873
- maskAllInputs: privacy.maskSensitiveInputs,
1874
- maskInputOptions: privacy.maskSensitiveInputs ? { password: true, email: true, tel: true, text: true, textarea: true, search: true, url: true, number: true, select: true, date: true, "datetime-local": true, month: true, time: true, week: true } : { password: true },
1883
+ maskAllInputs: privacy.maskSensitiveInputs && !privacy.captureSensitiveValues,
1884
+ maskInputOptions: privacy.captureSensitiveValues ? { password: false } : privacy.maskSensitiveInputs ? { password: true, email: true, tel: true, text: true, textarea: true, search: true, url: true, number: true, select: true, date: true, "datetime-local": true, month: true, time: true, week: true } : { password: true },
1875
1885
  maskInputFn: (text, el) => {
1886
+ if (privacy.captureSensitiveValues) return text;
1876
1887
  if (el?.type === "password") return "*".repeat(text.length);
1877
1888
  if (allowed && safe(() => el.matches(allowed))) return text;
1878
1889
  return "*".repeat(text.length);
@@ -2036,6 +2047,9 @@ var SessionManager = class {
2036
2047
  get currentToken() {
2037
2048
  return this.token;
2038
2049
  }
2050
+ hasTokenAvailable() {
2051
+ return Boolean(this.token || readQueryToken());
2052
+ }
2039
2053
  // ---- persistência -------------------------------------------------------
2040
2054
  loadPersisted() {
2041
2055
  const raw = safe(() => sessionStorage.getItem(this.storageKey));
@@ -2104,9 +2118,11 @@ var SessionManager = class {
2104
2118
  // ---- ciclo de vida ------------------------------------------------------
2105
2119
  /** Cria (ou obtém, de forma idempotente) a sessão no backend. */
2106
2120
  async create(opts, context) {
2107
- if (!opts?.sessionToken || !opts.executionId || !opts.testCaseId)
2121
+ const queryToken = consumeQueryToken();
2122
+ const sessionToken = opts?.sessionToken || this.token || queryToken;
2123
+ if (!sessionToken || !opts.executionId || !opts.testCaseId)
2108
2124
  throw new AuthError("sessionToken, executionId e testCaseId s\xE3o obrigat\xF3rios");
2109
- this.token = opts.sessionToken;
2125
+ this.token = sessionToken;
2110
2126
  const tabId = await resolveTabId(this.cfg.storagePrefix, this.loadPersisted()?.tabId);
2111
2127
  const res = await this.api.createSession({
2112
2128
  platformId: this.cfg.platformId,
@@ -2204,6 +2220,27 @@ var SessionManager = class {
2204
2220
  this.seq = 0;
2205
2221
  }
2206
2222
  };
2223
+ function consumeQueryToken() {
2224
+ if (typeof window === "undefined") return "";
2225
+ try {
2226
+ const token = readQueryToken();
2227
+ if (!token) return "";
2228
+ const url = new URL(window.location.href);
2229
+ url.searchParams.delete("offscript_token");
2230
+ window.history.replaceState(window.history.state, typeof document === "undefined" ? "" : document.title, `${url.pathname}${url.search}${url.hash}`);
2231
+ return token;
2232
+ } catch {
2233
+ return "";
2234
+ }
2235
+ }
2236
+ function readQueryToken() {
2237
+ if (typeof window === "undefined") return "";
2238
+ try {
2239
+ return new URL(window.location.href).searchParams.get("offscript_token")?.trim() ?? "";
2240
+ } catch {
2241
+ return "";
2242
+ }
2243
+ }
2207
2244
  var heldLocks = [];
2208
2245
  async function resolveTabId(prefix, previous) {
2209
2246
  const candidate = previous ?? uid("tab_");
@@ -2262,10 +2299,628 @@ function resolveConfig(input) {
2262
2299
  autoResume: input.autoResume ?? true,
2263
2300
  heartbeatIntervalMs: input.heartbeatIntervalMs ?? 3e4,
2264
2301
  storagePrefix: input.storagePrefix ?? "offscript",
2265
- debug: input.debug ?? false
2302
+ debug: input.debug ?? false,
2303
+ controls: input.controls ?? true
2266
2304
  };
2267
2305
  }
2268
2306
 
2307
+ // src/ui/RecordingControls.ts
2308
+ var HOST_ATTR = "data-offscript-console";
2309
+ var EYE_SEQUENCE = [
2310
+ { pose: "open", duration: 2600 },
2311
+ { pose: "blink", duration: 160 },
2312
+ { pose: "open", duration: 1800 },
2313
+ { pose: "left", duration: 700 },
2314
+ { pose: "open", duration: 1500 },
2315
+ { pose: "wink", duration: 480 },
2316
+ { pose: "open", duration: 1900 },
2317
+ { pose: "right", duration: 700 },
2318
+ { pose: "open", duration: 1600 },
2319
+ { pose: "up", duration: 640 },
2320
+ { pose: "happy", duration: 720 }
2321
+ ];
2322
+ var EYE_PATHS = {
2323
+ open: ["M59.5 79.5v15", "M93 79.5v15"],
2324
+ blink: ["M52 89q7.5 5 15 0", "M85.5 89q7.5 5 15 0"],
2325
+ wink: ["M52 89q7.5 5 15 0", "M93 79.5v15"],
2326
+ happy: ["M52 91q7.5-12 15 0", "M85.5 91q7.5-12 15 0"]
2327
+ };
2328
+ var RecordingControls = class {
2329
+ constructor(sdk) {
2330
+ this.sdk = sdk;
2331
+ this.eyeTimer = 0;
2332
+ this.placeFrame = 0;
2333
+ this.eyeIndex = 0;
2334
+ this.busy = false;
2335
+ this.expanded = true;
2336
+ this.privacyOpen = true;
2337
+ this.failure = "";
2338
+ this.onVisibility = () => {
2339
+ window.clearTimeout(this.eyeTimer);
2340
+ if (!document.hidden) this.scheduleEye();
2341
+ };
2342
+ this.onViewport = () => {
2343
+ this.placeConfirm();
2344
+ this.schedulePlacePrivacy();
2345
+ };
2346
+ }
2347
+ mount() {
2348
+ if (this.host || typeof document === "undefined") return;
2349
+ const host = document.createElement("div");
2350
+ host.setAttribute(HOST_ATTR, "");
2351
+ host.setAttribute("data-offscript-ignore", "");
2352
+ host.className = "offscript-ignore";
2353
+ host.style.cssText = "position:fixed;left:0;top:0;width:0;height:0;margin:0;padding:0;border:0;overflow:visible;z-index:2147483646;pointer-events:none;";
2354
+ const shadow = host.attachShadow({ mode: "closed" });
2355
+ shadow.innerHTML = TEMPLATE;
2356
+ this.bind(shadow);
2357
+ this.host = host;
2358
+ this.shadow = shadow;
2359
+ this.note = shadow.querySelector("[data-note]") ?? void 0;
2360
+ this.place();
2361
+ this.watchDom();
2362
+ this.unsubscribe = this.sdk.on("statusChange", () => this.render());
2363
+ window.addEventListener("resize", this.onViewport);
2364
+ window.visualViewport?.addEventListener("resize", this.onViewport);
2365
+ document.fonts?.ready.then(() => {
2366
+ if (this.host) this.onViewport();
2367
+ });
2368
+ this.watchDock();
2369
+ this.render();
2370
+ this.startEyes();
2371
+ }
2372
+ unmount() {
2373
+ this.unsubscribe?.();
2374
+ this.unsubscribe = void 0;
2375
+ this.observer?.disconnect();
2376
+ this.observer = void 0;
2377
+ this.dockWatch?.disconnect();
2378
+ this.dockWatch = void 0;
2379
+ window.cancelAnimationFrame(this.placeFrame);
2380
+ this.placeFrame = 0;
2381
+ window.clearTimeout(this.eyeTimer);
2382
+ window.removeEventListener("resize", this.onViewport);
2383
+ window.visualViewport?.removeEventListener("resize", this.onViewport);
2384
+ document.removeEventListener("visibilitychange", this.onVisibility);
2385
+ this.host?.remove();
2386
+ this.host = void 0;
2387
+ this.shadow = void 0;
2388
+ }
2389
+ place() {
2390
+ const host = this.host;
2391
+ const parent = document.body ?? document.documentElement;
2392
+ if (!host || !parent || host.parentNode === parent) return;
2393
+ parent.appendChild(host);
2394
+ }
2395
+ watchDom() {
2396
+ if (typeof MutationObserver === "undefined") return;
2397
+ this.observer?.disconnect();
2398
+ this.observer = new MutationObserver(() => {
2399
+ this.place();
2400
+ if (document.body) this.observer?.observe(document.body, { childList: true });
2401
+ });
2402
+ this.observer.observe(document.documentElement, { childList: true });
2403
+ if (document.body) this.observer.observe(document.body, { childList: true });
2404
+ }
2405
+ watchDock() {
2406
+ const dock = this.shadow?.querySelector(".dock");
2407
+ if (!(dock instanceof HTMLElement) || typeof ResizeObserver === "undefined") return;
2408
+ this.dockWatch?.disconnect();
2409
+ this.dockWatch = new ResizeObserver(() => this.placePrivacy());
2410
+ this.dockWatch.observe(dock);
2411
+ }
2412
+ bind(shadow) {
2413
+ const stop = (event) => event.stopPropagation();
2414
+ shadow.addEventListener("click", (event) => {
2415
+ const actionNode = event.composedPath().find((node) => node instanceof Element && node.hasAttribute("data-action"));
2416
+ const action = actionNode instanceof Element ? actionNode.getAttribute("data-action") : null;
2417
+ if (action === "open") {
2418
+ this.expanded = true;
2419
+ this.render();
2420
+ } else if (action === "close") {
2421
+ this.expanded = false;
2422
+ this.render();
2423
+ } else if (action === "record") void this.run(() => this.sdk.startArmedRecording());
2424
+ else if (action === "pause") void this.run(() => this.sdk.pauseRecording());
2425
+ else if (action === "resume") void this.run(() => this.sdk.resumeRecording());
2426
+ else if (action === "stop") void this.run(() => this.sdk.stopRecording());
2427
+ else if (action === "new") this.openConfirm();
2428
+ else if (action === "cancel") this.closeConfirm();
2429
+ else if (action === "confirm") {
2430
+ this.closeConfirm();
2431
+ void this.run(() => this.sdk.startNewSession());
2432
+ } else if (action === "privacy-show") {
2433
+ this.privacyOpen = true;
2434
+ this.render();
2435
+ } else if (action === "privacy-hide") {
2436
+ this.privacyOpen = false;
2437
+ this.render();
2438
+ }
2439
+ }, true);
2440
+ for (const type of ["click", "dblclick", "auxclick", "contextmenu", "pointerdown", "pointerup", "mousedown", "mouseup", "keydown", "keyup", "keypress", "input", "change", "mouseover", "mouseout"]) {
2441
+ shadow.addEventListener(type, stop);
2442
+ }
2443
+ shadow.querySelector("[data-confirm]")?.addEventListener("keydown", (event) => {
2444
+ if (event instanceof KeyboardEvent && event.key === "Escape") this.closeConfirm();
2445
+ });
2446
+ shadow.querySelector("[data-privacy]")?.addEventListener("keydown", (event) => {
2447
+ if (event instanceof KeyboardEvent && event.key === "Escape") {
2448
+ this.privacyOpen = false;
2449
+ this.render();
2450
+ }
2451
+ });
2452
+ }
2453
+ openConfirm() {
2454
+ const bubble = this.shadow?.querySelector("[data-confirm]");
2455
+ if (!(bubble instanceof HTMLElement) || this.busy || !bubble.hidden) return;
2456
+ bubble.hidden = false;
2457
+ this.placeConfirm();
2458
+ this.paintPrivacy();
2459
+ const cancel = bubble.querySelector("[data-action='cancel']");
2460
+ if (cancel instanceof HTMLElement) cancel.focus();
2461
+ }
2462
+ placeConfirm() {
2463
+ const bubble = this.shadow?.querySelector("[data-confirm]");
2464
+ const card = bubble?.querySelector(".bubble");
2465
+ const button = this.shadow?.querySelector("[data-action='new']");
2466
+ if (!(bubble instanceof HTMLElement) || !(card instanceof HTMLElement) || !(button instanceof HTMLElement)) return;
2467
+ const buttonBox = button.getBoundingClientRect();
2468
+ const buttonCenter = buttonBox.left + buttonBox.width / 2;
2469
+ const tailHalf = 4.5;
2470
+ const margin = 12;
2471
+ const viewport = document.documentElement.clientWidth;
2472
+ let rightEdge = buttonCenter + 22 + tailHalf;
2473
+ rightEdge = Math.min(viewport - margin, Math.max(margin + card.offsetWidth, rightEdge));
2474
+ bubble.style.right = `${Math.round(viewport - rightEdge)}px`;
2475
+ const tail = Math.min(Math.max(12, rightEdge - buttonCenter - tailHalf), Math.max(12, card.offsetWidth - 18));
2476
+ card.style.setProperty("--tail", `${Math.round(tail)}px`);
2477
+ }
2478
+ closeConfirm() {
2479
+ const bubble = this.shadow?.querySelector("[data-confirm]");
2480
+ if (bubble instanceof HTMLElement) bubble.hidden = true;
2481
+ this.paintPrivacy();
2482
+ }
2483
+ async run(task) {
2484
+ if (this.busy) return;
2485
+ this.busy = true;
2486
+ this.failure = "";
2487
+ this.render();
2488
+ try {
2489
+ await task();
2490
+ } catch {
2491
+ this.failure = "N\xE3o foi poss\xEDvel concluir";
2492
+ } finally {
2493
+ this.busy = false;
2494
+ this.render();
2495
+ }
2496
+ }
2497
+ render() {
2498
+ this.place();
2499
+ const status = this.sdk.getState().status;
2500
+ const shell = this.shadow?.querySelector("[data-shell]");
2501
+ shell?.setAttribute("data-open", this.expanded ? "true" : "false");
2502
+ shell?.setAttribute("data-state", status);
2503
+ if (this.note) this.note.textContent = this.failure;
2504
+ const live = status === "recording" || status === "paused";
2505
+ if (!live) this.closeConfirm();
2506
+ this.paintPrivacy();
2507
+ this.setHidden("record", live || status === "initializing" || status === "stopping");
2508
+ this.setHidden("pause", status !== "recording");
2509
+ this.setHidden("resume", status !== "paused");
2510
+ this.setHidden("new", !live);
2511
+ this.setHidden("stop", !live);
2512
+ this.setEnabled("record", !this.busy && !live && status !== "initializing" && status !== "stopping");
2513
+ this.setEnabled("pause", !this.busy && status === "recording");
2514
+ this.setEnabled("resume", !this.busy && status === "paused");
2515
+ this.setEnabled("stop", !this.busy && live);
2516
+ this.setEnabled("new", !this.busy && live);
2517
+ }
2518
+ paintPrivacy() {
2519
+ const flag = this.sdk.capturesSensitiveValues();
2520
+ const confirm = this.shadow?.querySelector("[data-confirm]");
2521
+ const confirmOpen = confirm instanceof HTMLElement && !confirm.hidden;
2522
+ const bubble = this.shadow?.querySelector("[data-privacy]");
2523
+ const show = flag && this.privacyOpen && this.expanded && !confirmOpen;
2524
+ if (bubble instanceof HTMLElement) {
2525
+ bubble.hidden = !show;
2526
+ if (!show) this.clearPrivacyPlace(bubble);
2527
+ }
2528
+ if (show) this.schedulePlacePrivacy();
2529
+ this.setHidden("privacy-show", !flag || this.privacyOpen || !this.expanded);
2530
+ }
2531
+ schedulePlacePrivacy() {
2532
+ this.placePrivacy();
2533
+ window.cancelAnimationFrame(this.placeFrame);
2534
+ this.placeFrame = window.requestAnimationFrame(() => {
2535
+ this.placePrivacy();
2536
+ this.placeFrame = window.requestAnimationFrame(() => {
2537
+ this.placeFrame = 0;
2538
+ this.placePrivacy();
2539
+ });
2540
+ });
2541
+ }
2542
+ clearPrivacyPlace(wrap) {
2543
+ wrap.style.position = "";
2544
+ wrap.style.transform = "";
2545
+ wrap.style.width = "";
2546
+ wrap.style.maxWidth = "";
2547
+ wrap.style.left = "";
2548
+ wrap.style.right = "";
2549
+ wrap.style.top = "";
2550
+ wrap.style.bottom = "";
2551
+ }
2552
+ placePrivacy() {
2553
+ const wrap = this.shadow?.querySelector("[data-privacy]");
2554
+ const card = wrap?.querySelector(".bubble");
2555
+ const face = this.shadow?.querySelector(".face");
2556
+ if (!(wrap instanceof HTMLElement) || !(card instanceof HTMLElement) || !(face instanceof HTMLElement) || wrap.hidden) return;
2557
+ const box = face.getBoundingClientRect();
2558
+ if (box.width < 8 || box.height < 8 || box.top < 1 && box.left < 1) return;
2559
+ const margin = 12;
2560
+ const viewport = document.documentElement.clientWidth;
2561
+ const maxWidth = Math.min(264, viewport - margin * 2);
2562
+ wrap.style.position = "fixed";
2563
+ wrap.style.transform = "none";
2564
+ wrap.style.width = `${maxWidth}px`;
2565
+ wrap.style.maxWidth = `${maxWidth}px`;
2566
+ wrap.style.left = "auto";
2567
+ wrap.style.bottom = "auto";
2568
+ const faceCenter = box.left + box.width / 2;
2569
+ const tailHalf = 5;
2570
+ let rightEdge = faceCenter + 12 + tailHalf;
2571
+ rightEdge = Math.min(viewport - margin, Math.max(margin + maxWidth, rightEdge));
2572
+ wrap.style.right = `${Math.round(viewport - rightEdge)}px`;
2573
+ wrap.style.top = `${Math.round(Math.max(margin, box.top - 10 - wrap.offsetHeight))}px`;
2574
+ const tail = Math.min(Math.max(12, rightEdge - faceCenter - tailHalf), Math.max(12, maxWidth - 18));
2575
+ card.style.setProperty("--tail", `${Math.round(tail)}px`);
2576
+ }
2577
+ setEnabled(action, enabled) {
2578
+ const button = this.shadow?.querySelector(`[data-action='${action}']`);
2579
+ if (button instanceof HTMLButtonElement) button.disabled = !enabled;
2580
+ }
2581
+ setHidden(action, hidden) {
2582
+ const button = this.shadow?.querySelector(`[data-action='${action}']`);
2583
+ if (button instanceof HTMLButtonElement) button.hidden = hidden;
2584
+ }
2585
+ startEyes() {
2586
+ const media = window.matchMedia?.("(prefers-reduced-motion: reduce)");
2587
+ if (!media || media.matches) return;
2588
+ document.addEventListener("visibilitychange", this.onVisibility);
2589
+ this.scheduleEye();
2590
+ }
2591
+ scheduleEye() {
2592
+ const beat = EYE_SEQUENCE[this.eyeIndex % EYE_SEQUENCE.length];
2593
+ this.applyPose(beat.pose);
2594
+ this.eyeIndex = (this.eyeIndex + 1) % EYE_SEQUENCE.length;
2595
+ this.eyeTimer = window.setTimeout(() => this.scheduleEye(), beat.duration);
2596
+ }
2597
+ applyPose(pose) {
2598
+ const [left, right] = EYE_PATHS[pose] ?? EYE_PATHS.open;
2599
+ this.shadow?.querySelectorAll("[data-eye='left']").forEach((node) => node.setAttribute("d", left));
2600
+ this.shadow?.querySelectorAll("[data-eye='right']").forEach((node) => node.setAttribute("d", right));
2601
+ this.shadow?.querySelectorAll("[data-eyes]").forEach((node) => node.setAttribute("data-pose", pose));
2602
+ }
2603
+ };
2604
+ var ICON = {
2605
+ pause: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9M11 3.5v9"/></svg>`,
2606
+ play: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 3.8 12.2 8 6 12.2z" fill="currentColor" stroke="none"/></svg>`,
2607
+ close: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4 4 12"/></svg>`,
2608
+ speak: `<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 3.5h9a1.5 1.5 0 0 1 1.5 1.5v5a1.5 1.5 0 0 1-1.5 1.5H7.2L4.5 13.5v-2H3.5A1.5 1.5 0 0 1 2 10V5a1.5 1.5 0 0 1 1.5-1.5Z"/></svg>`
2609
+ };
2610
+ function mark(gradientId) {
2611
+ return `<svg class="mark" viewBox="14 9 138 126" fill="none" aria-hidden="true">
2612
+ <defs>
2613
+ <linearGradient id="${gradientId}" x1="112" y1="41" x2="143" y2="17" gradientUnits="userSpaceOnUse">
2614
+ <stop offset="0" stop-color="#753eff"></stop>
2615
+ <stop offset="1" stop-color="#1772ff"></stop>
2616
+ </linearGradient>
2617
+ </defs>
2618
+ <path d="M72 23C88 23 101 26 109 32L112 40C122 50 128 64 128 80C128 109 104 131 72 131C41 131 16 108 16 80C16 46 40 23 72 23Z" fill="#eeeef7"></path>
2619
+ <path d="M112 41C119 37 123 27 131 22C135 19 139 18 143 17" stroke="url(#${gradientId})" stroke-width="8" stroke-linecap="round"></path>
2620
+ <g class="eyes" data-eyes data-pose="open" stroke="#10182e" stroke-width="7" stroke-linecap="round">
2621
+ <path data-eye="left" d="M59.5 79.5v15"></path>
2622
+ <path data-eye="right" d="M93 79.5v15"></path>
2623
+ </g>
2624
+ </svg>`;
2625
+ }
2626
+ var TEMPLATE = `
2627
+ <style>
2628
+ :host { color-scheme: dark; }
2629
+ button {
2630
+ display: grid;
2631
+ place-items: center;
2632
+ width: 32px;
2633
+ height: 32px;
2634
+ margin: 0;
2635
+ padding: 0;
2636
+ border: 1px solid #3a4458;
2637
+ border-radius: 999px;
2638
+ background: transparent;
2639
+ color: #f4f3fc;
2640
+ cursor: pointer;
2641
+ }
2642
+ button svg {
2643
+ width: 15px;
2644
+ height: 15px;
2645
+ fill: none;
2646
+ stroke: currentColor;
2647
+ stroke-width: 1.7;
2648
+ stroke-linecap: round;
2649
+ stroke-linejoin: round;
2650
+ }
2651
+ button:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
2652
+ button:focus-visible { outline: 2px solid #a596ff; outline-offset: 2px; }
2653
+ button:disabled { cursor: not-allowed; opacity: 0.4; }
2654
+ button[hidden] { display: none; }
2655
+ button.text {
2656
+ width: auto;
2657
+ padding: 0 12px;
2658
+ font: 600 12px/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
2659
+ }
2660
+ button.record {
2661
+ border: 0;
2662
+ background: linear-gradient(100deg, #6847ff, #1673ff);
2663
+ color: #fff;
2664
+ }
2665
+ button.finish {
2666
+ border: 0;
2667
+ background: #e5485f;
2668
+ color: #fff;
2669
+ }
2670
+ button.record:hover:not(:disabled),
2671
+ button.finish:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
2672
+ button.primary {
2673
+ border: 0;
2674
+ background: linear-gradient(100deg, #6847ff, #1673ff);
2675
+ color: #fff;
2676
+ }
2677
+ button.primary:hover:not(:disabled) { color: #fff; filter: brightness(1.08); }
2678
+ .launcher, .dock {
2679
+ position: fixed;
2680
+ right: 16px;
2681
+ bottom: 16px;
2682
+ z-index: 1;
2683
+ border: 1px solid #232b3b;
2684
+ background: #151b27;
2685
+ box-shadow: 0 18px 40px rgba(3, 5, 12, 0.5);
2686
+ pointer-events: auto;
2687
+ font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
2688
+ }
2689
+ .launcher {
2690
+ width: 56px;
2691
+ height: 56px;
2692
+ padding: 8px;
2693
+ border-radius: 999px;
2694
+ cursor: pointer;
2695
+ }
2696
+ .launcher .mark { width: 100%; height: 100%; }
2697
+ .launcher::after {
2698
+ content: "";
2699
+ position: absolute;
2700
+ right: 1px;
2701
+ bottom: 1px;
2702
+ width: 10px;
2703
+ height: 10px;
2704
+ border-radius: 999px;
2705
+ background: #929bb1;
2706
+ box-shadow: 0 0 0 2px #151b27;
2707
+ }
2708
+ [data-state="recording"] .launcher::after { background: #1673ff; }
2709
+ [data-state="paused"] .launcher::after { background: #d0c8ff; }
2710
+ [data-state="error"] .launcher::after { background: #e5485f; }
2711
+ .dock {
2712
+ display: flex;
2713
+ align-items: center;
2714
+ gap: 10px;
2715
+ max-width: calc(100vw - 32px);
2716
+ padding: 6px 6px 6px 12px;
2717
+ border-radius: 999px;
2718
+ color: #f4f3fc;
2719
+ overflow: visible;
2720
+ }
2721
+ [data-open="false"] .dock, [data-open="true"] .launcher { display: none; }
2722
+ .brand { display: flex; align-items: center; gap: 8px; min-width: 0; overflow: visible; }
2723
+ .face { position: relative; flex: none; width: 28px; height: 28px; overflow: visible; }
2724
+ .dock .mark { width: 28px; height: 28px; }
2725
+ .word {
2726
+ color: #f4f3fc;
2727
+ font-size: 14px;
2728
+ font-weight: 600;
2729
+ letter-spacing: -0.03em;
2730
+ line-height: 1;
2731
+ }
2732
+ .actions { display: flex; align-items: center; gap: 6px; }
2733
+ .note {
2734
+ position: fixed;
2735
+ right: 16px;
2736
+ bottom: 78px;
2737
+ z-index: 1;
2738
+ margin: 0;
2739
+ padding: 4px 10px;
2740
+ border-radius: 999px;
2741
+ background: #151b27;
2742
+ color: #ff9aa8;
2743
+ font: 600 11px/1.3 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
2744
+ pointer-events: none;
2745
+ }
2746
+ .note:empty { display: none; }
2747
+ [data-shell]:has(.note:not(:empty)) .privacy { bottom: calc(100% + 48px); }
2748
+ .eyes {
2749
+ transform-box: view-box;
2750
+ transform-origin: 0 0;
2751
+ transition: transform 280ms cubic-bezier(0.22, 1, 0.36, 1);
2752
+ }
2753
+ .eyes[data-pose="left"] { transform: translate(-4.5%, 0.8%); }
2754
+ .eyes[data-pose="right"] { transform: translate(4.5%, 0.8%); }
2755
+ .eyes[data-pose="up"] { transform: translate(0, -3.5%); }
2756
+ @media (prefers-reduced-motion: reduce) {
2757
+ .eyes { transition: none; }
2758
+ .eyes[data-pose] { transform: none; }
2759
+ .speak:not([hidden]) .bubble { animation: none; }
2760
+ }
2761
+ .speak {
2762
+ position: fixed;
2763
+ right: 16px;
2764
+ bottom: 76px;
2765
+ z-index: 2;
2766
+ width: max-content;
2767
+ max-width: min(16rem, calc(100vw - 32px));
2768
+ pointer-events: auto;
2769
+ font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
2770
+ }
2771
+ .speak[hidden] { display: none; }
2772
+ .bubble {
2773
+ position: relative;
2774
+ padding: 0.6rem 0.7rem 0.55rem;
2775
+ border: 1px solid #3d3590;
2776
+ border-radius: 1rem;
2777
+ background: #1b1a3f;
2778
+ color: #c7cadc;
2779
+ box-shadow: 0 16px 40px rgba(3, 5, 12, 0.45);
2780
+ animation: bubble-in 320ms cubic-bezier(0.22, 1, 0.36, 1);
2781
+ }
2782
+ .bubble::after {
2783
+ content: "";
2784
+ position: absolute;
2785
+ right: var(--tail, 22px);
2786
+ bottom: -5px;
2787
+ width: 9px;
2788
+ height: 9px;
2789
+ border-right: 1px solid #3d3590;
2790
+ border-bottom: 1px solid #3d3590;
2791
+ background: #1b1a3f;
2792
+ transform: rotate(45deg);
2793
+ }
2794
+ .bubble .mark {
2795
+ display: block;
2796
+ width: 22px;
2797
+ height: 22px;
2798
+ margin: 0 0 0.35rem;
2799
+ }
2800
+ @keyframes bubble-in {
2801
+ from { opacity: 0; transform: translateY(8px) scale(0.96); }
2802
+ to { opacity: 1; transform: none; }
2803
+ }
2804
+ .bubble p {
2805
+ margin: 0;
2806
+ color: #c7cadc;
2807
+ font-size: 0.8125rem;
2808
+ font-weight: 400;
2809
+ line-height: 1.45;
2810
+ }
2811
+ .privacy {
2812
+ position: absolute;
2813
+ left: auto;
2814
+ right: 50%;
2815
+ bottom: calc(100% + 18px);
2816
+ z-index: 3;
2817
+ width: min(16.5rem, calc(100vw - 24px));
2818
+ max-width: min(16.5rem, calc(100vw - 24px));
2819
+ pointer-events: auto;
2820
+ font-family: "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
2821
+ transform: translateX(20px);
2822
+ }
2823
+ .privacy[hidden] { display: none; }
2824
+ .privacy .bubble {
2825
+ padding: 0.75rem 2.15rem 0.75rem 0.9rem;
2826
+ border-radius: 16px;
2827
+ }
2828
+ .privacy .bubble::after {
2829
+ right: var(--tail, 12px);
2830
+ bottom: -5px;
2831
+ width: 10px;
2832
+ height: 10px;
2833
+ }
2834
+ @media (max-width: 560px) {
2835
+ .word { display: none; }
2836
+ .privacy {
2837
+ position: fixed;
2838
+ left: 12px;
2839
+ right: 12px;
2840
+ bottom: 76px;
2841
+ width: auto;
2842
+ max-width: none;
2843
+ transform: none;
2844
+ }
2845
+ .privacy .bubble {
2846
+ padding: 0.65rem 1.85rem 0.6rem 0.75rem;
2847
+ }
2848
+ .privacy .bubble p { font-size: 0.75rem; }
2849
+ }
2850
+ .privacy button.hide {
2851
+ position: absolute;
2852
+ top: 0.4rem;
2853
+ right: 0.4rem;
2854
+ width: 24px;
2855
+ height: 24px;
2856
+ border: 0;
2857
+ background: transparent;
2858
+ color: #c7cadc;
2859
+ }
2860
+ .privacy button.hide:hover:not(:disabled) { color: #f4f3fc; }
2861
+ .choices { display: flex; gap: 6px; margin-top: 0.7rem; }
2862
+ .speak button.choice {
2863
+ width: auto;
2864
+ height: auto;
2865
+ min-height: 1.75rem;
2866
+ padding: 0 0.7rem;
2867
+ border-radius: 999px;
2868
+ font: 600 0.75rem/1 "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
2869
+ }
2870
+ .speak button.secondary {
2871
+ border: 1px solid #3a4458;
2872
+ background: transparent;
2873
+ color: #f4f3fc;
2874
+ }
2875
+ .speak button.secondary:hover:not(:disabled) { border-color: #a596ff; color: #d0c8ff; }
2876
+ .speak button.danger {
2877
+ border: 0;
2878
+ background: #e5485f;
2879
+ color: #fff;
2880
+ }
2881
+ .speak button.danger:hover:not(:disabled) { color: #fff; background: color-mix(in srgb, #e5485f 88%, #fff); }
2882
+ </style>
2883
+ <div data-shell data-open="true" data-state="recording">
2884
+ <button type="button" class="launcher" data-action="open" aria-label="Abrir controles da grava\xE7\xE3o">
2885
+ ${mark("offscript-trail-launcher")}
2886
+ </button>
2887
+ <div class="dock" role="toolbar" aria-label="Controles da grava\xE7\xE3o">
2888
+ <div class="brand">
2889
+ <span class="face">
2890
+ ${mark("offscript-trail-dock")}
2891
+ <div class="privacy" data-privacy hidden>
2892
+ <div class="bubble" role="status">
2893
+ <button type="button" class="hide" data-action="privacy-hide" aria-label="Fechar aviso">${ICON.close}</button>
2894
+ <p>Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado \u2014 senha e dado sens\xEDvel tamb\xE9m.</p>
2895
+ </div>
2896
+ </div>
2897
+ </span>
2898
+ <span class="word">Offscript</span>
2899
+ </div>
2900
+ <div class="actions">
2901
+ <button type="button" data-action="privacy-show" aria-label="Abrir aviso" title="Tudo vis\xEDvel na tela ser\xE1 gravado, inclusive o que for digitado" hidden>${ICON.speak}</button>
2902
+ <button type="button" class="text record" data-action="record">Iniciar</button>
2903
+ <button type="button" data-action="pause" aria-label="Pausar" title="Pausar">${ICON.pause}</button>
2904
+ <button type="button" data-action="resume" aria-label="Retomar" title="Retomar">${ICON.play}</button>
2905
+ <button type="button" class="text" data-action="new" title="Descarta o que foi gravado e volta ao in\xEDcio">Nova sess\xE3o</button>
2906
+ <button type="button" class="text finish" data-action="stop" title="Encerra a coleta e envia o que foi gravado">Finalizar</button>
2907
+ <button type="button" data-action="close" aria-label="Fechar" title="Fechar">${ICON.close}</button>
2908
+ </div>
2909
+ </div>
2910
+ <div class="speak" data-confirm hidden>
2911
+ <div class="bubble" role="dialog" aria-labelledby="offscript-confirm-title">
2912
+ ${mark("offscript-trail-speak")}
2913
+ <p id="offscript-confirm-title">Descartar esta sess\xE3o? O que foi gravado ser\xE1 ignorado.</p>
2914
+ <div class="choices">
2915
+ <button type="button" class="choice secondary" data-action="cancel">Cancelar</button>
2916
+ <button type="button" class="choice danger" data-action="confirm">Descartar</button>
2917
+ </div>
2918
+ </div>
2919
+ </div>
2920
+ <p class="note" data-note></p>
2921
+ </div>
2922
+ `;
2923
+
2269
2924
  // src/core/OffscriptSDK.ts
2270
2925
  var MAX_LOG = 5e3;
2271
2926
  var Offscript = class {
@@ -2288,6 +2943,9 @@ var Offscript = class {
2288
2943
  getState() {
2289
2944
  return { status: this.status, session: this.sessions.session, pending: this.transport?.pending ?? 0, lastError: this.lastError };
2290
2945
  }
2946
+ capturesSensitiveValues() {
2947
+ return this.config.privacy.captureSensitiveValues === true;
2948
+ }
2291
2949
  getMetrics() {
2292
2950
  return { emitted: this.metrics.emitted, dropped: this.metrics.dropped, avgEmitMs: this.metrics.emitted ? this.metrics.emitMs / this.metrics.emitted : 0, bufferKind: this.transport?.bufferKind };
2293
2951
  }
@@ -2299,6 +2957,7 @@ var Offscript = class {
2299
2957
  }
2300
2958
  async doInit() {
2301
2959
  if (typeof window === "undefined") throw new OffscriptError("NOT_SUPPORTED", "O SDK requer um ambiente de navegador");
2960
+ this.ensureControls();
2302
2961
  this.setStatus("initializing");
2303
2962
  try {
2304
2963
  const buffer = await openBuffer(`${this.config.storagePrefix}-${this.config.platformId}`, this.config.transport.persist);
@@ -2331,9 +2990,10 @@ var Offscript = class {
2331
2990
  throw new InvalidStateError(`J\xE1 existe grava\xE7\xE3o ativa para a execu\xE7\xE3o ${cur.executionId}. Encerre-a antes.`);
2332
2991
  }
2333
2992
  if (this.status === "stopping") throw new InvalidStateError("Grava\xE7\xE3o em encerramento");
2993
+ this.lastStart = { testCaseId: opts.testCaseId, executionId: opts.executionId, sessionToken: opts.sessionToken };
2334
2994
  this.setStatus("initializing");
2335
2995
  try {
2336
- const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion);
2996
+ const ctx = collectContext({ platformId: this.config.platformId, testCaseId: opts.testCaseId, executionId: opts.executionId }, "initial", this.config.appVersion, this.config.privacy.captureSensitiveValues);
2337
2997
  await this.sessions.create(opts, { ...ctx, url: this.sanitizer.sanitizeUrl(ctx.url), referrer: void 0, title: void 0 });
2338
2998
  const s = this.sessions.session;
2339
2999
  await this.transport.start({ sessionId: s.sessionId, executionId: s.executionId, tabId: s.tabId });
@@ -2368,6 +3028,11 @@ var Offscript = class {
2368
3028
  if (state.status === "paused") this.recorders.forEach((r) => r.pause?.());
2369
3029
  this.actions?.recordPageLoad(safe(() => performance.getEntriesByType("navigation")[0]?.type), document.referrer);
2370
3030
  this.sessions.startHeartbeat((r) => this.onHeartbeat(r.status), (e) => void this.onFatal(e), () => this.transport?.pending ?? 0, () => this.status);
3031
+ const token = this.sessions.currentToken;
3032
+ if (token) {
3033
+ this.lastStart = { testCaseId: s.testCaseId, executionId: s.executionId, sessionToken: token };
3034
+ this.armed ?? (this.armed = { testCaseId: s.testCaseId, sessionToken: token });
3035
+ }
2371
3036
  this.bus.emit("sessionStart", s);
2372
3037
  } catch (e) {
2373
3038
  await this.teardown();
@@ -2377,6 +3042,38 @@ var Offscript = class {
2377
3042
  if (!(e instanceof SessionExpiredError || e instanceof SessionDisabledError || e instanceof AuthError)) throw e;
2378
3043
  }
2379
3044
  }
3045
+ /** Guarda caso de teste e token para o botão de gravar, sem abrir a sessão. */
3046
+ armRecording(opts) {
3047
+ this.armed = { testCaseId: opts.testCaseId, sessionToken: opts.sessionToken };
3048
+ }
3049
+ /** Abre a coleta com os dados armados, ou com os da última sessão. */
3050
+ startArmedRecording() {
3051
+ if ((this.status === "recording" || this.status === "paused") && this.sessions.session) return Promise.resolve(this.sessions.session);
3052
+ return this.startRecording(this.freshStart());
3053
+ }
3054
+ /** Descarta a coleta atual e volta ao estado inicial, sem abrir outra. */
3055
+ async startNewSession() {
3056
+ if (this.status !== "recording" && this.status !== "paused") return;
3057
+ await this.discardRecording();
3058
+ }
3059
+ freshStart() {
3060
+ const base = this.armed ?? (this.lastStart ? { testCaseId: this.lastStart.testCaseId, sessionToken: this.lastStart.sessionToken } : void 0);
3061
+ if (!base?.testCaseId || !base.sessionToken) throw new InvalidStateError("N\xE3o h\xE1 dados para iniciar a grava\xE7\xE3o");
3062
+ return { testCaseId: base.testCaseId, sessionToken: base.sessionToken, executionId: uid("exec_") };
3063
+ }
3064
+ async discardRecording() {
3065
+ const sessionId = this.sessions.session?.sessionId ?? "";
3066
+ this.accepting = false;
3067
+ this.stopRecorders();
3068
+ this.sessions.stopHeartbeat();
3069
+ this.transport?.stop();
3070
+ if (sessionId) await this.transport?.clearSession(sessionId);
3071
+ this.lifecycle?.uninstall();
3072
+ this.sessions.clearPersisted();
3073
+ this.sessions.reset();
3074
+ this.setStatus("idle");
3075
+ if (sessionId) this.bus.emit("sessionDiscard", { sessionId });
3076
+ }
2380
3077
  async pauseRecording() {
2381
3078
  if (this.status === "paused") return;
2382
3079
  if (this.status !== "recording") throw new InvalidStateError(`N\xE3o \xE9 poss\xEDvel pausar no estado ${this.status}`);
@@ -2453,6 +3150,8 @@ var Offscript = class {
2453
3150
  }
2454
3151
  this.destroyed = true;
2455
3152
  await this.teardown();
3153
+ this.controls?.unmount();
3154
+ this.controls = void 0;
2456
3155
  this.sessions.reset();
2457
3156
  this.transport?.close();
2458
3157
  this.bus.clear();
@@ -2578,6 +3277,7 @@ var Offscript = class {
2578
3277
  this.transport?.setOnline(typeof navigator === "undefined" ? true : navigator.onLine !== false);
2579
3278
  }
2580
3279
  async startRecorders() {
3280
+ this.ensureControls();
2581
3281
  const r = this.config.recording;
2582
3282
  const list = [];
2583
3283
  if (r.rrweb) list.push(this.rrweb = new RRWebRecorder(this));
@@ -2600,6 +3300,13 @@ var Offscript = class {
2600
3300
  for (const r of this.recorders.splice(0).reverse()) safe(() => r.stop());
2601
3301
  this.rrweb = this.actions = this.context = void 0;
2602
3302
  }
3303
+ ensureControls() {
3304
+ if (!this.config.controls || typeof document === "undefined" || !this.sessions.hasTokenAvailable()) return;
3305
+ safe(() => {
3306
+ this.controls ?? (this.controls = new RecordingControls(this));
3307
+ this.controls.mount();
3308
+ });
3309
+ }
2603
3310
  async teardown() {
2604
3311
  this.accepting = false;
2605
3312
  this.stopRecorders();
@@ -2654,5 +3361,5 @@ var Offscript = class {
2654
3361
  Offscript.version = SDK_VERSION;
2655
3362
 
2656
3363
  export { AuthError, ConfigError, DEFAULT_PRIVACY, DEFAULT_RECORDING, DEFAULT_TRANSPORT, DataSanitizer, InvalidStateError, Offscript, OffscriptError, SessionDisabledError, SessionExpiredError, TransportError };
2657
- //# sourceMappingURL=chunk-3OSCH7J2.js.map
2658
- //# sourceMappingURL=chunk-3OSCH7J2.js.map
3364
+ //# sourceMappingURL=chunk-IBEPWI7S.js.map
3365
+ //# sourceMappingURL=chunk-IBEPWI7S.js.map