@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.
@@ -142,27 +142,40 @@ var SagepilotWebEvents = (() => {
142
142
  this.startup = startup;
143
143
  return startup;
144
144
  }
145
- /** Renews the owner or, when allowed for web capture, starts fresh after an expired browser session. */
145
+ /** Renews capture, using separate browser proof to recover a linked web visitor after expiry. */
146
146
  async refresh(recoverAnonymous = false) {
147
147
  if (this.pending) return this.pending;
148
148
  if (!this.anonymousId) throw new Error("Start the installation client before renewing it.");
149
149
  const generation = this.generation, prior = this.current;
150
150
  const pending = (async () => {
151
151
  let renewing = prior;
152
+ let resumed = false;
152
153
  let value;
153
154
  try {
154
155
  value = await this.requestSession(prior?.session_token);
155
156
  } catch (error) {
156
157
  if (!recoverAnonymous || !prior || this.options.platform !== "web" || generation !== this.generation || !(error instanceof SagepilotInstallationError) || error.status !== 401) throw error;
157
158
  renewing = null;
158
- value = await this.requestSession();
159
+ if (prior.browser_resume_token) {
160
+ try {
161
+ value = await this.requestSession(prior.browser_resume_token, true);
162
+ resumed = true;
163
+ } catch (resumeError) {
164
+ if (generation !== this.generation || !(resumeError instanceof SagepilotInstallationError) || ![401, 403, 422].includes(resumeError.status ?? 0)) throw resumeError;
165
+ }
166
+ }
167
+ if (!resumed) value = await this.requestSession();
159
168
  }
160
169
  const next = this.validateSession(value);
161
170
  if (generation !== this.generation) throw new Error("SDK session startup was cancelled.");
162
- 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)) {
171
+ 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)) {
163
172
  throw new Error("SDK session renewal changed capture ownership.");
164
173
  }
165
- if (prior && !renewing) this.owners.delete(prior.session_id);
174
+ if (resumed && next.browser_resume_token !== prior?.browser_resume_token) {
175
+ throw new Error("SDK browser recovery returned invalid proof.");
176
+ }
177
+ if (renewing && !next.browser_resume_token) next.browser_resume_token = renewing.browser_resume_token;
178
+ if (prior && next.session_id !== prior.session_id) this.owners.delete(prior.session_id);
166
179
  this.current = next;
167
180
  this.remember(next);
168
181
  try {
@@ -176,8 +189,8 @@ var SagepilotWebEvents = (() => {
176
189
  this.pending = pending;
177
190
  return pending;
178
191
  }
179
- /** Requests policy for this browser or device with the original token when renewing. */
180
- requestSession(token) {
192
+ /** Sends only the capture token for renewal or the distinct credential for browser recovery. */
193
+ requestSession(token, resumeBrowser = false) {
181
194
  return this.request(`${this.apiHost}/platform-api/v1/sdk/sessions`, {
182
195
  method: "POST",
183
196
  headers: token ? { Authorization: `Bearer ${token}` } : {},
@@ -185,7 +198,8 @@ var SagepilotWebEvents = (() => {
185
198
  key: `${this.workspaceId}:${this.installationId}`,
186
199
  anonymous_id: this.anonymousId,
187
200
  platform: this.options.platform,
188
- ...this.options.appId ? { app_id: this.options.appId } : {}
201
+ ...this.options.appId ? { app_id: this.options.appId } : {},
202
+ ...resumeBrowser ? { resume_browser: true } : {}
189
203
  })
190
204
  });
191
205
  }
@@ -223,12 +237,24 @@ var SagepilotWebEvents = (() => {
223
237
  if (new TextEncoder().encode(body).byteLength > 128 * 1024) throw new Error("SDK batch exceeds the upload limit.");
224
238
  return validateSagepilotInstallationReceipt(await this.request(auth.url, { method: "POST", headers: auth.headers, body }), observations.map((item) => item.event_id));
225
239
  }
226
- /** Submits explicit popup contacts independently from ordinary observation payloads. */
240
+ /** Submits contact and adopts only a scoped lead session returned by the server. */
227
241
  async submitContact(input) {
228
242
  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.");
229
- const result = await this.authenticated("/contact", input);
230
- 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.");
231
- return result;
243
+ if (this.transitioning) throw new Error("An SDK identity operation is already running.");
244
+ this.transitioning = true;
245
+ const generation = this.generation, prior = this.current;
246
+ try {
247
+ const result = await this.authenticated("/contact", input);
248
+ 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.");
249
+ const { session, ...acknowledgement } = result;
250
+ if (session != null) {
251
+ if (result.status !== "recorded" || prior?.customer_id !== null) throw new Error("Invalid popup lead session.");
252
+ await this.adoptTransition(session, prior, generation, true);
253
+ }
254
+ return acknowledgement;
255
+ } finally {
256
+ this.transitioning = false;
257
+ }
232
258
  }
233
259
  /** Adopts customer authority only from the server's completed verification operation. */
234
260
  async verifyContact(challengeId, code) {
@@ -241,7 +267,7 @@ var SagepilotWebEvents = (() => {
241
267
  }
242
268
  /** Keeps a bounded credential cache separate from observations so old anonymous batches retain their owner. */
243
269
  remember(session) {
244
- this.owners.set(session.session_id, { ...session });
270
+ this.owners.set(session.session_id, { ...session, browser_resume_token: void 0 });
245
271
  while (this.owners.size > 4) this.owners.delete(this.owners.keys().next().value);
246
272
  try {
247
273
  void Promise.resolve(this.options.storage?.setItem(this.storageKey + ":owners", JSON.stringify([...this.owners.values()]))).catch(() => {
@@ -263,24 +289,28 @@ var SagepilotWebEvents = (() => {
263
289
  this.transitioning = true;
264
290
  const generation = this.generation, prior = this.current;
265
291
  try {
266
- const next = this.validateSession(await this.authenticated(path, body));
267
- 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.");
268
- this.generation++;
269
- this.cancelPending();
270
- this.pending = null;
271
- if (path === "/logout") this.owners.delete(prior.session_id);
272
- else this.remember(prior);
273
- this.current = next;
274
- this.remember(next);
275
- try {
276
- await this.options.storage?.setItem(this.storageKey, JSON.stringify(next));
277
- } catch {
278
- }
279
- return { ...next };
292
+ return await this.adoptTransition(await this.authenticated(path, body), prior, generation, identified);
280
293
  } finally {
281
294
  this.transitioning = false;
282
295
  }
283
296
  }
297
+ /** Installs one new server owner and retains earlier credentials unless the owner logged out. */
298
+ async adoptTransition(value, prior, generation, identified) {
299
+ const next = this.validateSession(value);
300
+ 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.");
301
+ this.generation++;
302
+ this.cancelPending();
303
+ this.pending = null;
304
+ if (identified) this.remember(prior);
305
+ else this.owners.delete(prior.session_id);
306
+ this.current = next;
307
+ this.remember(next);
308
+ try {
309
+ await this.options.storage?.setItem(this.storageKey, JSON.stringify(next));
310
+ } catch {
311
+ }
312
+ return { ...next };
313
+ }
284
314
  /** Cancels requests on permission/ownership boundaries while preserving the current session for safe renewal. */
285
315
  cancelPending() {
286
316
  this.uploadRevision++;
@@ -296,10 +326,10 @@ var SagepilotWebEvents = (() => {
296
326
  this.startup = null;
297
327
  this.owners.clear();
298
328
  }
299
- /** Rejects foreign, identified or malformed session responses before credentials are used. */
329
+ /** Rejects foreign or malformed session responses before credentials are used. */
300
330
  validateSession(value) {
301
331
  const session = value;
302
- 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)) {
332
+ 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)) {
303
333
  throw new Error("SDK session response has invalid scope or ownership.");
304
334
  }
305
335
  const url = safeURL(session.ingestion_url);
@@ -364,7 +394,7 @@ var SagepilotWebEvents = (() => {
364
394
 
365
395
  // package.json
366
396
  var name = "@sagepilot-ai/web-events-addon";
367
- var version = "0.2.3";
397
+ var version = "0.2.5";
368
398
 
369
399
  // src/projection.ts
370
400
  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;
@@ -2366,7 +2396,6 @@ var SagepilotWebEvents = (() => {
2366
2396
  }
2367
2397
  const result = await options.submit(contact);
2368
2398
  if (revision !== generation) return;
2369
- options.emit("popup_submitted");
2370
2399
  for (const input of inputs.values()) input.value = "";
2371
2400
  if (result.status === "verification_required") {
2372
2401
  let restartForm2 = function() {
@@ -2664,13 +2693,14 @@ var SagepilotWebEvents = (() => {
2664
2693
  captureBytes -= bytes;
2665
2694
  });
2666
2695
  };
2696
+ function emitPopup(action) {
2697
+ if (enabled()) capture("lifecycle", { action }, epoch, Date.now());
2698
+ }
2667
2699
  const popup = createPopup({
2668
2700
  client,
2669
2701
  storage,
2670
2702
  report,
2671
- emit(action) {
2672
- if (enabled()) capture("lifecycle", { action }, epoch, Date.now());
2673
- },
2703
+ emit: emitPopup,
2674
2704
  submit(input) {
2675
2705
  return addon.submitContact(input);
2676
2706
  },
@@ -2687,7 +2717,7 @@ var SagepilotWebEvents = (() => {
2687
2717
  detach(true);
2688
2718
  client.cancelPending();
2689
2719
  try {
2690
- await operation();
2720
+ return await operation();
2691
2721
  } finally {
2692
2722
  identityChanging = false;
2693
2723
  if (!destroyed) {
@@ -2916,17 +2946,21 @@ var SagepilotWebEvents = (() => {
2916
2946
  }
2917
2947
  },
2918
2948
  flush,
2919
- /** Submits contact-only data through the explicit identity endpoint, never through autocapture. */
2949
+ /** Keeps contact data on the identity endpoint and fences capture while a new lead session is adopted. */
2920
2950
  async submitContact(input) {
2921
- const anonymousId = client.session()?.anonymous_id;
2922
- const result = await client.submitContact(input);
2923
- if (result.status === "recorded") await completeVisitor(anonymousId);
2951
+ const session = client.session();
2952
+ const result = session?.popup?.verification === "none" && session.customer_id === null ? await changeIdentity(() => client.submitContact(input)) : await client.submitContact(input);
2953
+ if (result.status === "recorded") {
2954
+ emitPopup("popup_submitted");
2955
+ await completeVisitor(session?.anonymous_id);
2956
+ }
2924
2957
  return result;
2925
2958
  },
2926
2959
  /** Verifies proof behind a capture fence; buffered anonymous observations remain anonymous. */
2927
2960
  async verifyContact(challengeId, code) {
2928
2961
  const anonymousId = client.session()?.anonymous_id;
2929
2962
  await changeIdentity(() => client.verifyContact(challengeId, code));
2963
+ emitPopup("popup_submitted");
2930
2964
  await completeVisitor(anonymousId);
2931
2965
  },
2932
2966
  /** Applies an explicit logout boundary without relabeling earlier observations. */