@sagepilot-ai/web-events-addon 0.2.3 → 0.2.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.
package/dist/index.js CHANGED
@@ -116,27 +116,40 @@ var SagepilotInstallationClient = class {
116
116
  this.startup = startup;
117
117
  return startup;
118
118
  }
119
- /** Renews the owner or, when allowed for web capture, starts fresh after an expired browser session. */
119
+ /** Renews capture, using separate browser proof to recover a linked web visitor after expiry. */
120
120
  async refresh(recoverAnonymous = false) {
121
121
  if (this.pending) return this.pending;
122
122
  if (!this.anonymousId) throw new Error("Start the installation client before renewing it.");
123
123
  const generation = this.generation, prior = this.current;
124
124
  const pending = (async () => {
125
125
  let renewing = prior;
126
+ let resumed = false;
126
127
  let value;
127
128
  try {
128
129
  value = await this.requestSession(prior?.session_token);
129
130
  } catch (error) {
130
131
  if (!recoverAnonymous || !prior || this.options.platform !== "web" || generation !== this.generation || !(error instanceof SagepilotInstallationError) || error.status !== 401) throw error;
131
132
  renewing = null;
132
- value = await this.requestSession();
133
+ if (prior.browser_resume_token) {
134
+ try {
135
+ value = await this.requestSession(prior.browser_resume_token, true);
136
+ resumed = true;
137
+ } catch (resumeError) {
138
+ if (generation !== this.generation || !(resumeError instanceof SagepilotInstallationError) || ![401, 403, 422].includes(resumeError.status ?? 0)) throw resumeError;
139
+ }
140
+ }
141
+ if (!resumed) value = await this.requestSession();
133
142
  }
134
143
  const next = this.validateSession(value);
135
144
  if (generation !== this.generation) throw new Error("SDK session startup was cancelled.");
136
- if (renewing && (next.session_id !== renewing.session_id || next.customer_id !== renewing.customer_id) || !renewing && (next.customer_id !== null || next.session_id === prior?.session_id)) {
145
+ if (renewing && (next.session_id !== renewing.session_id || next.customer_id !== renewing.customer_id) || !renewing && (next.customer_id !== null || !resumed && next.session_id === prior?.session_id)) {
137
146
  throw new Error("SDK session renewal changed capture ownership.");
138
147
  }
139
- if (prior && !renewing) this.owners.delete(prior.session_id);
148
+ if (resumed && next.browser_resume_token !== prior?.browser_resume_token) {
149
+ throw new Error("SDK browser recovery returned invalid proof.");
150
+ }
151
+ if (renewing && !next.browser_resume_token) next.browser_resume_token = renewing.browser_resume_token;
152
+ if (prior && next.session_id !== prior.session_id) this.owners.delete(prior.session_id);
140
153
  this.current = next;
141
154
  this.remember(next);
142
155
  try {
@@ -150,8 +163,8 @@ var SagepilotInstallationClient = class {
150
163
  this.pending = pending;
151
164
  return pending;
152
165
  }
153
- /** Requests policy for this browser or device with the original token when renewing. */
154
- requestSession(token) {
166
+ /** Sends only the capture token for renewal or the distinct credential for browser recovery. */
167
+ requestSession(token, resumeBrowser = false) {
155
168
  return this.request(`${this.apiHost}/platform-api/v1/sdk/sessions`, {
156
169
  method: "POST",
157
170
  headers: token ? { Authorization: `Bearer ${token}` } : {},
@@ -159,7 +172,8 @@ var SagepilotInstallationClient = class {
159
172
  key: `${this.workspaceId}:${this.installationId}`,
160
173
  anonymous_id: this.anonymousId,
161
174
  platform: this.options.platform,
162
- ...this.options.appId ? { app_id: this.options.appId } : {}
175
+ ...this.options.appId ? { app_id: this.options.appId } : {},
176
+ ...resumeBrowser ? { resume_browser: true } : {}
163
177
  })
164
178
  });
165
179
  }
@@ -197,12 +211,24 @@ var SagepilotInstallationClient = class {
197
211
  if (new TextEncoder().encode(body).byteLength > 128 * 1024) throw new Error("SDK batch exceeds the upload limit.");
198
212
  return validateSagepilotInstallationReceipt(await this.request(auth.url, { method: "POST", headers: auth.headers, body }), observations.map((item) => item.event_id));
199
213
  }
200
- /** Submits explicit popup contacts independently from ordinary observation payloads. */
214
+ /** Submits contact and adopts only a scoped lead session returned by the server. */
201
215
  async submitContact(input) {
202
216
  if (!UUID.test(input.submission_id) || !Number.isSafeInteger(input.popup_revision) || input.popup_revision < 1 || typeof input.marketing_consent !== "boolean") throw new Error("Invalid popup submission.");
203
- const result = await this.authenticated("/contact", input);
204
- if (!result || !["recorded", "verification_required"].includes(result.status) || result.status === "verification_required" && (!result.challenge_id || !UUID.test(result.challenge_id) || !Number.isFinite(Date.parse(result.expires_at ?? "")))) throw new Error("Invalid popup submission response.");
205
- return result;
217
+ if (this.transitioning) throw new Error("An SDK identity operation is already running.");
218
+ this.transitioning = true;
219
+ const generation = this.generation, prior = this.current;
220
+ try {
221
+ const result = await this.authenticated("/contact", input);
222
+ if (!result || !["recorded", "verification_required"].includes(result.status) || result.status === "verification_required" && (!result.challenge_id || !UUID.test(result.challenge_id) || !Number.isFinite(Date.parse(result.expires_at ?? "")))) throw new Error("Invalid popup submission response.");
223
+ const { session, ...acknowledgement } = result;
224
+ if (session != null) {
225
+ if (result.status !== "recorded" || prior?.customer_id !== null) throw new Error("Invalid popup lead session.");
226
+ await this.adoptTransition(session, prior, generation, true);
227
+ }
228
+ return acknowledgement;
229
+ } finally {
230
+ this.transitioning = false;
231
+ }
206
232
  }
207
233
  /** Adopts customer authority only from the server's completed verification operation. */
208
234
  async verifyContact(challengeId, code) {
@@ -215,7 +241,7 @@ var SagepilotInstallationClient = class {
215
241
  }
216
242
  /** Keeps a bounded credential cache separate from observations so old anonymous batches retain their owner. */
217
243
  remember(session) {
218
- this.owners.set(session.session_id, { ...session });
244
+ this.owners.set(session.session_id, { ...session, browser_resume_token: void 0 });
219
245
  while (this.owners.size > 4) this.owners.delete(this.owners.keys().next().value);
220
246
  try {
221
247
  void Promise.resolve(this.options.storage?.setItem(this.storageKey + ":owners", JSON.stringify([...this.owners.values()]))).catch(() => {
@@ -237,24 +263,28 @@ var SagepilotInstallationClient = class {
237
263
  this.transitioning = true;
238
264
  const generation = this.generation, prior = this.current;
239
265
  try {
240
- const next = this.validateSession(await this.authenticated(path, body));
241
- if (generation !== this.generation || !prior || next.session_id === prior.session_id || (identified ? next.customer_id === null : next.customer_id !== null)) throw new Error("Invalid SDK identity transition.");
242
- this.generation++;
243
- this.cancelPending();
244
- this.pending = null;
245
- if (path === "/logout") this.owners.delete(prior.session_id);
246
- else this.remember(prior);
247
- this.current = next;
248
- this.remember(next);
249
- try {
250
- await this.options.storage?.setItem(this.storageKey, JSON.stringify(next));
251
- } catch {
252
- }
253
- return { ...next };
266
+ return await this.adoptTransition(await this.authenticated(path, body), prior, generation, identified);
254
267
  } finally {
255
268
  this.transitioning = false;
256
269
  }
257
270
  }
271
+ /** Installs one new server owner and retains earlier credentials unless the owner logged out. */
272
+ async adoptTransition(value, prior, generation, identified) {
273
+ const next = this.validateSession(value);
274
+ if (generation !== this.generation || !prior || next.session_id === prior.session_id || (identified ? next.customer_id === null : next.customer_id !== null)) throw new Error("Invalid SDK identity transition.");
275
+ this.generation++;
276
+ this.cancelPending();
277
+ this.pending = null;
278
+ if (identified) this.remember(prior);
279
+ else this.owners.delete(prior.session_id);
280
+ this.current = next;
281
+ this.remember(next);
282
+ try {
283
+ await this.options.storage?.setItem(this.storageKey, JSON.stringify(next));
284
+ } catch {
285
+ }
286
+ return { ...next };
287
+ }
258
288
  /** Cancels requests on permission/ownership boundaries while preserving the current session for safe renewal. */
259
289
  cancelPending() {
260
290
  this.uploadRevision++;
@@ -270,10 +300,10 @@ var SagepilotInstallationClient = class {
270
300
  this.startup = null;
271
301
  this.owners.clear();
272
302
  }
273
- /** Rejects foreign, identified or malformed session responses before credentials are used. */
303
+ /** Rejects foreign or malformed session responses before credentials are used. */
274
304
  validateSession(value) {
275
305
  const session = value;
276
- if (!session || session.workspace_id !== this.workspaceId || session.installation_id !== this.installationId || session.platform !== this.options.platform || session.anonymous_id !== this.anonymousId || session.customer_id !== null && (typeof session.customer_id !== "string" || !UUID.test(session.customer_id)) || !UUID.test(session.session_id) || typeof session.session_token !== "string" || session.session_token.length > 16384 || !session.session_token || !Number.isFinite(Date.parse(session.expires_at)) || typeof session.collection_enabled !== "boolean" || session.visitor_cookie_enabled !== void 0 && typeof session.visitor_cookie_enabled !== "boolean" || !Number.isSafeInteger(session.revision)) {
306
+ if (!session || session.workspace_id !== this.workspaceId || session.installation_id !== this.installationId || session.platform !== this.options.platform || session.anonymous_id !== this.anonymousId || session.customer_id !== null && (typeof session.customer_id !== "string" || !UUID.test(session.customer_id)) || !UUID.test(session.session_id) || typeof session.session_token !== "string" || session.session_token.length > 16384 || !session.session_token || session.browser_resume_token != null && (session.platform !== "web" || typeof session.browser_resume_token !== "string" || !session.browser_resume_token || session.browser_resume_token.length > 8192) || !Number.isFinite(Date.parse(session.expires_at)) || typeof session.collection_enabled !== "boolean" || session.visitor_cookie_enabled !== void 0 && typeof session.visitor_cookie_enabled !== "boolean" || !Number.isSafeInteger(session.revision)) {
277
307
  throw new Error("SDK session response has invalid scope or ownership.");
278
308
  }
279
309
  const url = safeURL(session.ingestion_url);
@@ -338,7 +368,7 @@ function createSagepilotInstallationClient(options) {
338
368
 
339
369
  // package.json
340
370
  var name = "@sagepilot-ai/web-events-addon";
341
- var version = "0.2.3";
371
+ var version = "0.2.5";
342
372
 
343
373
  // src/projection.ts
344
374
  var sensitive = /password|passwd|secret|token|authorization|cookie|session|email|phone|contact|address|card|payment|cvv|ssn|dob|birth|customer|user|account|person|profile|name|description|message|content|text|search|query/i;
@@ -2340,7 +2370,6 @@ function createPopup(options) {
2340
2370
  }
2341
2371
  const result = await options.submit(contact);
2342
2372
  if (revision !== generation) return;
2343
- options.emit("popup_submitted");
2344
2373
  for (const input of inputs.values()) input.value = "";
2345
2374
  if (result.status === "verification_required") {
2346
2375
  let restartForm2 = function() {
@@ -2638,13 +2667,14 @@ function createSagepilotWebEvents(options) {
2638
2667
  captureBytes -= bytes;
2639
2668
  });
2640
2669
  };
2670
+ function emitPopup(action) {
2671
+ if (enabled()) capture("lifecycle", { action }, epoch, Date.now());
2672
+ }
2641
2673
  const popup = createPopup({
2642
2674
  client,
2643
2675
  storage,
2644
2676
  report,
2645
- emit(action) {
2646
- if (enabled()) capture("lifecycle", { action }, epoch, Date.now());
2647
- },
2677
+ emit: emitPopup,
2648
2678
  submit(input) {
2649
2679
  return addon.submitContact(input);
2650
2680
  },
@@ -2661,7 +2691,7 @@ function createSagepilotWebEvents(options) {
2661
2691
  detach(true);
2662
2692
  client.cancelPending();
2663
2693
  try {
2664
- await operation();
2694
+ return await operation();
2665
2695
  } finally {
2666
2696
  identityChanging = false;
2667
2697
  if (!destroyed) {
@@ -2890,17 +2920,21 @@ function createSagepilotWebEvents(options) {
2890
2920
  }
2891
2921
  },
2892
2922
  flush,
2893
- /** Submits contact-only data through the explicit identity endpoint, never through autocapture. */
2923
+ /** Keeps contact data on the identity endpoint and fences capture while a new lead session is adopted. */
2894
2924
  async submitContact(input) {
2895
- const anonymousId = client.session()?.anonymous_id;
2896
- const result = await client.submitContact(input);
2897
- if (result.status === "recorded") await completeVisitor(anonymousId);
2925
+ const session = client.session();
2926
+ const result = session?.popup?.verification === "none" && session.customer_id === null ? await changeIdentity(() => client.submitContact(input)) : await client.submitContact(input);
2927
+ if (result.status === "recorded") {
2928
+ emitPopup("popup_submitted");
2929
+ await completeVisitor(session?.anonymous_id);
2930
+ }
2898
2931
  return result;
2899
2932
  },
2900
2933
  /** Verifies proof behind a capture fence; buffered anonymous observations remain anonymous. */
2901
2934
  async verifyContact(challengeId, code) {
2902
2935
  const anonymousId = client.session()?.anonymous_id;
2903
2936
  await changeIdentity(() => client.verifyContact(challengeId, code));
2937
+ emitPopup("popup_submitted");
2904
2938
  await completeVisitor(anonymousId);
2905
2939
  },
2906
2940
  /** Applies an explicit logout boundary without relabeling earlier observations. */