@leadping/consent 1.0.3 → 1.0.5

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.
@@ -22,9 +22,14 @@ var LeadpingConsent = (() => {
22
22
  var embed_exports = {};
23
23
  __export(embed_exports, {
24
24
  ConsentCapture: () => ConsentCapture,
25
+ ReplayLimitExceededError: () => ReplayLimitExceededError,
25
26
  attach: () => attach
26
27
  });
27
28
 
29
+ // src/capture-rejected-error.ts
30
+ var CaptureRejectedError = class extends Error {
31
+ };
32
+
28
33
  // node_modules/rrweb/dist/rrweb.js
29
34
  var __defProp2 = Object.defineProperty;
30
35
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -12759,7 +12764,7 @@ var LeadpingConsent = (() => {
12759
12764
  }
12760
12765
  seal() {
12761
12766
  if (this.events.length === 0) return;
12762
- if (this.nextBatch >= 256) throw new Error("Recording exceeded the session batch limit.");
12767
+ if (this.nextBatch >= 1024) throw new Error("Recording exceeded the session batch limit.");
12763
12768
  const first = this.nextEvent - this.events.length;
12764
12769
  const body = `{"firstEventNumber":${first},"events":[${this.events.join(",")}]}`;
12765
12770
  this.events.length = 0;
@@ -12768,8 +12773,13 @@ var LeadpingConsent = (() => {
12768
12773
  }
12769
12774
  };
12770
12775
 
12771
- // src/capture-rejected-error.ts
12772
- var CaptureRejectedError = class extends Error {
12776
+ // src/replay-limit-exceeded-error.ts
12777
+ var ReplayLimitExceededError = class extends Error {
12778
+ code = "replay_limit_exceeded";
12779
+ constructor() {
12780
+ super("The recording time limit was reached. This submission wasn't captured. Start a new recording and submit again.");
12781
+ this.name = "ReplayLimitExceededError";
12782
+ }
12773
12783
  };
12774
12784
 
12775
12785
  // src/transport.ts
@@ -12802,10 +12812,11 @@ var LeadpingConsent = (() => {
12802
12812
  signal: AbortSignal.any([this.abort.signal, AbortSignal.timeout(15e3)])
12803
12813
  });
12804
12814
  if (response.ok) return;
12815
+ if (response.status === 410) throw new ReplayLimitExceededError();
12805
12816
  if (response.status < 500 && response.status !== 408 && response.status !== 429)
12806
12817
  throw new CaptureRejectedError(`Capture rejected (${response.status}).`);
12807
12818
  } catch (error) {
12808
- if (error instanceof CaptureRejectedError || this.abort.signal.aborted) throw error;
12819
+ if (error instanceof CaptureRejectedError || error instanceof ReplayLimitExceededError || this.abort.signal.aborted) throw error;
12809
12820
  }
12810
12821
  if (attempt === 4) break;
12811
12822
  await this.delay(Math.min(8e3, 500 * 2 ** attempt) + Math.random() * 250);
@@ -12831,6 +12842,32 @@ var LeadpingConsent = (() => {
12831
12842
  }
12832
12843
  };
12833
12844
 
12845
+ // src/replay-deadline.ts
12846
+ var ReplayDeadline = class {
12847
+ startedAt = Date.now();
12848
+ startedTick = performance.now();
12849
+ expiresAt = Infinity;
12850
+ maximumMs = 18e5;
12851
+ configure(expiresAt, maxReplaySeconds) {
12852
+ const deadline = Date.parse(expiresAt);
12853
+ if (!Number.isFinite(deadline) || !Number.isInteger(maxReplaySeconds) || maxReplaySeconds < 1 || maxReplaySeconds > 1800)
12854
+ throw new Error("Invalid recording time limit.");
12855
+ this.expiresAt = deadline;
12856
+ this.maximumMs = maxReplaySeconds * 1e3;
12857
+ this.assertWithinLimit();
12858
+ }
12859
+ get remainingMilliseconds() {
12860
+ return Math.min(
12861
+ this.expiresAt - Date.now(),
12862
+ this.maximumMs - (Date.now() - this.startedAt),
12863
+ this.maximumMs - (performance.now() - this.startedTick)
12864
+ );
12865
+ }
12866
+ assertWithinLimit() {
12867
+ if (this.remainingMilliseconds <= 0) throw new ReplayLimitExceededError();
12868
+ }
12869
+ };
12870
+
12834
12871
  // src/capture.ts
12835
12872
  var ConsentCapture = class _ConsentCapture {
12836
12873
  constructor(options) {
@@ -12839,12 +12876,19 @@ var LeadpingConsent = (() => {
12839
12876
  const url = new URL(options.apiUrl);
12840
12877
  if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash)
12841
12878
  throw new Error("Capture API requires an HTTPS URL without credentials.");
12842
- if (!/^[0-9a-f-]{36}$/i.test(options.session.sessionId) || options.session.form.origin !== location.origin || !options.form.contains(options.checkbox) || !options.form.contains(options.disclosure) || options.checkbox.type !== "checkbox" || options.checkbox.checked || options.checkbox.defaultChecked || options.disclosure.textContent !== options.session.form.disclosure)
12843
- throw new Error("Form does not match the approved disclosure and unchecked consent control.");
12844
- const remaining = Date.parse(options.session.expiresAt) - Date.now();
12845
- if (!Number.isFinite(remaining) || remaining <= 0 || remaining > 60 * 60 * 1e3)
12846
- throw new Error("Invalid or expired capture session.");
12847
- this.transport = new CaptureTransport(url.href.replace(/\/$/, "") + "/consent/sessions/" + options.session.sessionId, options.session.uploadToken);
12879
+ if (!options.form.contains(options.checkbox) || !options.form.contains(options.disclosure) || options.checkbox.type !== "checkbox" || options.checkbox.checked || options.checkbox.defaultChecked)
12880
+ throw new Error("Form requires an unchecked consent control and disclosure.");
12881
+ this.observedDisclosure = options.disclosure.textContent ?? "";
12882
+ this.ready = Promise.resolve().then(() => typeof options.session === "function" ? options.session() : options.session).then((session) => {
12883
+ if (this.disposed || this.failure) throw this.failure ?? new Error("Recorder is disposed.");
12884
+ if (!/^[0-9a-f-]{36}$/i.test(session.sessionId) || session.form.origin !== location.origin || session.form.disclosure !== this.observedDisclosure)
12885
+ throw new Error("Form does not match the approved disclosure.");
12886
+ this.deadline.configure(session.expiresAt, session.maxReplaySeconds);
12887
+ this.session = session;
12888
+ this.transport = new CaptureTransport(url.href.replace(/\/$/, "") + "/consent/sessions/" + session.sessionId, session.uploadToken);
12889
+ if (!this.stopped) this.expiry = setTimeout(() => this.fail(new ReplayLimitExceededError()), this.deadline.remainingMilliseconds);
12890
+ });
12891
+ void this.ready.then(this.backgroundFlush).catch((error) => this.fail(error));
12848
12892
  _ConsentCapture.active = this;
12849
12893
  try {
12850
12894
  this.stopRecording = record({
@@ -12871,10 +12915,9 @@ var LeadpingConsent = (() => {
12871
12915
  options.checkbox.addEventListener("change", this.consentChanged);
12872
12916
  document.addEventListener("visibilitychange", this.visibilityChanged);
12873
12917
  this.interval = setInterval(this.backgroundFlush, 3e3);
12874
- this.expiry = setTimeout(() => this.fail(new Error("Capture session expired.")), remaining);
12875
- record.addCustomEvent("leadping.disclosure", { formId: options.session.form.id });
12918
+ record.addCustomEvent("leadping.disclosure", { disclosure: this.observedDisclosure });
12876
12919
  } catch (error) {
12877
- this.stop();
12920
+ this.dispose();
12878
12921
  throw error;
12879
12922
  }
12880
12923
  }
@@ -12893,7 +12936,21 @@ var LeadpingConsent = (() => {
12893
12936
  maxTouchPoints: navigator.maxTouchPoints
12894
12937
  };
12895
12938
  buffer = new EventBuffer();
12939
+ deadline = new ReplayDeadline();
12896
12940
  transport;
12941
+ /** Resolves when the service session is available and validated. Recording starts before this. */
12942
+ ready;
12943
+ /** Allocated as soon as the session is ready; this does not assert that submission is saved. */
12944
+ get sessionId() {
12945
+ return this.session?.sessionId;
12946
+ }
12947
+ /** The server-selected duration limit, available after ready resolves. */
12948
+ get maxReplaySeconds() {
12949
+ return this.session?.maxReplaySeconds;
12950
+ }
12951
+ session;
12952
+ observedDisclosure;
12953
+ disposed = false;
12897
12954
  pending = [];
12898
12955
  head = 0;
12899
12956
  queuedBytes = 0;
@@ -12912,6 +12969,8 @@ var LeadpingConsent = (() => {
12912
12969
  async flush() {
12913
12970
  if (this.failure) throw this.failure;
12914
12971
  this.seal();
12972
+ await this.ready;
12973
+ if (this.failure || this.disposed) throw this.failure ?? new Error("Recorder is disposed.");
12915
12974
  while (this.head < this.pending.length || this.sending) {
12916
12975
  this.sending ??= this.send();
12917
12976
  await this.sending;
@@ -12919,19 +12978,29 @@ var LeadpingConsent = (() => {
12919
12978
  if (this.failure) throw this.failure;
12920
12979
  }
12921
12980
  dispose() {
12981
+ this.disposed = true;
12922
12982
  this.stop();
12923
- this.transport.dispose();
12983
+ this.transport?.dispose();
12924
12984
  }
12925
12985
  async finishCore(recipient) {
12926
12986
  if (this.stopped || this.failure) throw this.failure ?? new Error("Recorder is stopped.");
12987
+ try {
12988
+ this.deadline.assertWithinLimit();
12989
+ } catch (error) {
12990
+ this.fail(error);
12991
+ throw error;
12992
+ }
12927
12993
  const observedDisclosure = this.options.disclosure.textContent ?? "";
12928
- if (observedDisclosure !== this.options.session.form.disclosure)
12994
+ if (observedDisclosure !== this.observedDisclosure)
12929
12995
  throw new Error("The disclosure changed during capture. Start a new approved form session.");
12930
12996
  const accepted = this.options.checkbox.checked;
12931
12997
  record.addCustomEvent("leadping.submit", { accepted });
12932
12998
  this.stop();
12999
+ this.options.onProgress?.("uploading");
12933
13000
  await this.flush();
12934
- await this.transport.put("/submission", JSON.stringify({
13001
+ this.deadline.assertWithinLimit();
13002
+ this.options.onProgress?.("submitting");
13003
+ await this.transport.put(this.options.issueCertificate ? "/submission/certificate" : "/submission", JSON.stringify({
12935
13004
  lastBatchNumber: this.buffer.lastBatchNumber,
12936
13005
  lastEventNumber: this.buffer.lastEventNumber,
12937
13006
  accepted,
@@ -12939,7 +13008,7 @@ var LeadpingConsent = (() => {
12939
13008
  recipient,
12940
13009
  browser: this.browser
12941
13010
  }));
12942
- return { sessionId: this.options.session.sessionId, uploadToken: this.options.session.uploadToken };
13011
+ return { sessionId: this.session.sessionId, uploadToken: this.session.uploadToken };
12943
13012
  }
12944
13013
  emit = (event) => {
12945
13014
  if (this.stopped || this.failure) return;
@@ -12963,25 +13032,31 @@ var LeadpingConsent = (() => {
12963
13032
  async send() {
12964
13033
  await Promise.resolve();
12965
13034
  try {
12966
- while (this.head < this.pending.length) {
12967
- const batch = this.pending[this.head];
12968
- await this.transport.put("/batches/" + batch.number, batch.body);
12969
- this.queuedBytes -= batch.body.length * 2;
12970
- this.pending[this.head] = void 0;
12971
- this.head++;
12972
- if (this.head === this.pending.length) {
12973
- this.pending.length = 0;
12974
- this.head = 0;
12975
- }
13035
+ await Promise.all(Array.from({ length: 3 }, async () => {
13036
+ while (!this.failure && !this.disposed && this.head < this.pending.length) {
13037
+ const index2 = this.head++;
13038
+ const batch = this.pending[index2];
13039
+ await this.transport.put("/batches/" + batch.number, batch.body);
13040
+ this.queuedBytes -= batch.body.length * 2;
13041
+ this.pending[index2] = void 0;
13042
+ }
13043
+ }));
13044
+ if (this.head === this.pending.length) {
13045
+ this.pending.length = 0;
13046
+ this.head = 0;
12976
13047
  }
12977
13048
  } catch (error) {
13049
+ this.transport?.dispose();
12978
13050
  this.fail(error instanceof Error ? error : new Error("Capture upload failed."));
12979
13051
  throw this.failure;
12980
13052
  } finally {
12981
13053
  this.sending = void 0;
12982
13054
  }
12983
13055
  }
12984
- consentChanged = () => record.addCustomEvent("leadping.consent", { accepted: this.options.checkbox.checked });
13056
+ consentChanged = () => {
13057
+ record.addCustomEvent("leadping.consent", { accepted: this.options.checkbox.checked });
13058
+ if (this.options.checkbox.checked) this.backgroundFlush();
13059
+ };
12985
13060
  visibilityChanged = () => {
12986
13061
  if (document.visibilityState === "hidden") this.backgroundFlush();
12987
13062
  };
@@ -13020,11 +13095,9 @@ var LeadpingConsent = (() => {
13020
13095
  status.setAttribute("role", "status");
13021
13096
  form.append(status);
13022
13097
  let capture;
13023
- let ready = false;
13024
13098
  let submitted = false;
13025
13099
  const fail = (error) => {
13026
- ready = false;
13027
- status.textContent = "Unable to complete consent capture. Please reload to start again.";
13100
+ status.textContent = error instanceof ReplayLimitExceededError ? error.message : "Unable to complete consent capture. Please reload to start again.";
13028
13101
  form.dispatchEvent(new CustomEvent("leadping:error", { detail: error }));
13029
13102
  };
13030
13103
  const origin = new URL(script2.dataset.apiUrl ?? "https://consent.leadping.ai");
@@ -13034,27 +13107,40 @@ var LeadpingConsent = (() => {
13034
13107
  if (portal.protocol !== "https:" || portal.username || portal.password || portal.search || portal.hash || portal.pathname !== "/")
13035
13108
  throw new Error("Leadping requires an HTTPS portal origin.");
13036
13109
  const endpoint = `${origin.origin}/api/consent`;
13110
+ const progress = (stage) => {
13111
+ form.dispatchEvent(new CustomEvent("leadping:progress", { detail: { stage } }));
13112
+ };
13037
13113
  const post = async (path, body) => {
13038
- const response = await fetch(endpoint + path, {
13039
- method: "POST",
13040
- headers: { "Content-Type": "application/json" },
13041
- credentials: "omit",
13042
- redirect: "error",
13043
- body: JSON.stringify(body),
13044
- signal: AbortSignal.timeout(3e4)
13045
- });
13046
- if (!response.ok) throw new Error(`Leadping request failed (${response.status}).`);
13047
- return response;
13114
+ const attempts = path.endsWith("/sessions") ? 5 : 1;
13115
+ for (let attempt = 0; attempt < attempts; attempt++) {
13116
+ try {
13117
+ const response = await fetch(endpoint + path, {
13118
+ method: "POST",
13119
+ headers: { "Content-Type": "application/json" },
13120
+ credentials: "omit",
13121
+ redirect: "error",
13122
+ body: JSON.stringify(body),
13123
+ signal: AbortSignal.timeout(attempts > 1 ? 15e3 : 3e4)
13124
+ });
13125
+ if (response.ok) return response;
13126
+ if (response.status < 500 && response.status !== 408 && response.status !== 429)
13127
+ throw new CaptureRejectedError(`Leadping request failed (${response.status}).`);
13128
+ throw new Error(`Leadping request failed (${response.status}).`);
13129
+ } catch (error) {
13130
+ if (error instanceof CaptureRejectedError || attempt === attempts - 1) throw error;
13131
+ }
13132
+ await new Promise((resolve) => setTimeout(resolve, 500 * 2 ** attempt));
13133
+ }
13134
+ throw new Error("Leadping session could not be started.");
13048
13135
  };
13049
13136
  form.addEventListener("submit", async (event) => {
13050
13137
  event.preventDefault();
13051
- if (!ready || submitted || !capture || !form.reportValidity()) return;
13138
+ if (submitted || !capture || !form.reportValidity()) return;
13052
13139
  if (!(checkbox instanceof HTMLInputElement) || !checkbox.checked) {
13053
13140
  status.textContent = "Please check the consent box before submitting.";
13054
13141
  return;
13055
13142
  }
13056
13143
  submitted = true;
13057
- ready = false;
13058
13144
  status.textContent = "Submitting\u2026";
13059
13145
  const values = new FormData(form);
13060
13146
  const recipient = {};
@@ -13062,14 +13148,11 @@ var LeadpingConsent = (() => {
13062
13148
  const value = values.get(key);
13063
13149
  if (typeof value === "string" && value) recipient[key] = value;
13064
13150
  }
13065
- let issuingCertificate = false;
13066
13151
  try {
13067
13152
  const consentCapture = await capture.finish(recipient);
13068
- issuingCertificate = true;
13069
- const certificate = await (await post("/certificates", consentCapture)).json();
13070
- if (!/^[0-9a-f-]{36}$/i.test(certificate.certificateId)) throw new Error("Invalid certificate response.");
13153
+ const certificate = { certificateId: consentCapture.sessionId };
13071
13154
  const certificateUrl = `${portal.origin}/${certificate.certificateId}`;
13072
- status.textContent = `Certificate received: ${certificate.certificateId}. `;
13155
+ status.textContent = `Submission saved. Certificate: ${certificate.certificateId}. `;
13073
13156
  const link = document.createElement("a");
13074
13157
  link.href = certificateUrl;
13075
13158
  link.textContent = "View certificate and replay";
@@ -13079,7 +13162,8 @@ var LeadpingConsent = (() => {
13079
13162
  form.dispatchEvent(new CustomEvent("leadping:success", { detail: { certificateId: certificate.certificateId, certificateUrl } }));
13080
13163
  } catch (error) {
13081
13164
  fail(error);
13082
- if (issuingCertificate) status.textContent = "Certificate issuance could not be confirmed. Please check before starting another recording.";
13165
+ if (!(error instanceof ReplayLimitExceededError))
13166
+ status.textContent = "Submission could not be confirmed. Keep the certificate ID and check before starting another recording.";
13083
13167
  } finally {
13084
13168
  capture.dispose();
13085
13169
  }
@@ -13088,16 +13172,40 @@ var LeadpingConsent = (() => {
13088
13172
  try {
13089
13173
  if (!(disclosure instanceof HTMLElement) || !(checkbox instanceof HTMLInputElement))
13090
13174
  throw new Error("Add data-leadping-disclosure and data-leadping-consent inside the form.");
13091
- const session = await (await post(`/domains/${encodeURIComponent(domainId)}/sessions`, { pageUrl: location.href, disclosure: disclosure.textContent ?? "" })).json();
13092
- checkbox.checked = false;
13093
- capture = new ConsentCapture({ apiUrl: origin.origin + "/api", session, form, disclosure, checkbox, onError: fail });
13175
+ const pageUrl = location.href;
13176
+ const observedDisclosure = disclosure.textContent ?? "";
13177
+ capture = new ConsentCapture({
13178
+ apiUrl: origin.origin + "/api",
13179
+ session: async () => (await post(`/domains/${encodeURIComponent(domainId)}/sessions`, { pageUrl, disclosure: observedDisclosure })).json(),
13180
+ form,
13181
+ disclosure,
13182
+ checkbox,
13183
+ issueCertificate: true,
13184
+ onError: fail,
13185
+ onProgress: (stage) => {
13186
+ status.textContent = stage === "uploading" ? "Saving recording\u2026" : "Saving submission and requesting certificate\u2026";
13187
+ progress(stage);
13188
+ }
13189
+ });
13094
13190
  window.addEventListener("pagehide", () => {
13095
- ready = false;
13096
13191
  capture?.dispose();
13097
13192
  }, { once: true });
13098
- ready = true;
13099
- status.textContent = "";
13100
- form.dispatchEvent(new CustomEvent("leadping:ready"));
13193
+ await capture.ready;
13194
+ const certificateId = capture.sessionId;
13195
+ const certificateUrl = `${portal.origin}/${certificateId}`;
13196
+ for (const [name, value] of Object.entries({ leadping_certificate_id: certificateId, leadping_certificate_url: certificateUrl })) {
13197
+ const input2 = document.createElement("input");
13198
+ input2.type = "hidden";
13199
+ input2.name = name;
13200
+ input2.value = value;
13201
+ form.append(input2);
13202
+ }
13203
+ if (!submitted) status.textContent = "";
13204
+ form.dispatchEvent(new CustomEvent("leadping:ready", { detail: {
13205
+ certificateId,
13206
+ certificateUrl,
13207
+ maxReplaySeconds: capture.maxReplaySeconds
13208
+ } }));
13101
13209
  } catch (error) {
13102
13210
  capture?.dispose();
13103
13211
  fail(error);