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