@sagepilot-ai/web-events-addon 0.2.4 → 0.2.6

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.d.cts CHANGED
@@ -14,6 +14,7 @@ type SagepilotContactResult = {
14
14
  status: "verification_required" | "recorded";
15
15
  challenge_id?: string;
16
16
  expires_at?: string;
17
+ popup_submission_recorded?: boolean;
17
18
  };
18
19
  type SagepilotSDKObservation = {
19
20
  event_id: string;
package/dist/index.d.ts CHANGED
@@ -14,6 +14,7 @@ type SagepilotContactResult = {
14
14
  status: "verification_required" | "recorded";
15
15
  challenge_id?: string;
16
16
  expires_at?: string;
17
+ popup_submission_recorded?: boolean;
17
18
  };
18
19
  type SagepilotSDKObservation = {
19
20
  event_id: string;
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,25 @@ 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 customer 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 body = prior?.popup_submission_events === true ? { ...input, server_submission_event: true } : input;
222
+ const result = await this.authenticated("/contact", body);
223
+ 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.");
224
+ const { session, ...acknowledgement } = result;
225
+ if (session != null) {
226
+ if (result.status !== "recorded" || prior?.customer_id !== null) throw new Error("Invalid popup customer session.");
227
+ await this.adoptTransition(session, prior, generation, true);
228
+ }
229
+ return acknowledgement;
230
+ } finally {
231
+ this.transitioning = false;
232
+ }
206
233
  }
207
234
  /** Adopts customer authority only from the server's completed verification operation. */
208
235
  async verifyContact(challengeId, code) {
@@ -215,7 +242,7 @@ var SagepilotInstallationClient = class {
215
242
  }
216
243
  /** Keeps a bounded credential cache separate from observations so old anonymous batches retain their owner. */
217
244
  remember(session) {
218
- this.owners.set(session.session_id, { ...session });
245
+ this.owners.set(session.session_id, { ...session, browser_resume_token: void 0 });
219
246
  while (this.owners.size > 4) this.owners.delete(this.owners.keys().next().value);
220
247
  try {
221
248
  void Promise.resolve(this.options.storage?.setItem(this.storageKey + ":owners", JSON.stringify([...this.owners.values()]))).catch(() => {
@@ -237,24 +264,28 @@ var SagepilotInstallationClient = class {
237
264
  this.transitioning = true;
238
265
  const generation = this.generation, prior = this.current;
239
266
  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 };
267
+ return await this.adoptTransition(await this.authenticated(path, body), prior, generation, identified);
254
268
  } finally {
255
269
  this.transitioning = false;
256
270
  }
257
271
  }
272
+ /** Installs one new server owner and retains earlier credentials unless the owner logged out. */
273
+ async adoptTransition(value, prior, generation, identified) {
274
+ const next = this.validateSession(value);
275
+ 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.");
276
+ this.generation++;
277
+ this.cancelPending();
278
+ this.pending = null;
279
+ if (identified) this.remember(prior);
280
+ else this.owners.delete(prior.session_id);
281
+ this.current = next;
282
+ this.remember(next);
283
+ try {
284
+ await this.options.storage?.setItem(this.storageKey, JSON.stringify(next));
285
+ } catch {
286
+ }
287
+ return { ...next };
288
+ }
258
289
  /** Cancels requests on permission/ownership boundaries while preserving the current session for safe renewal. */
259
290
  cancelPending() {
260
291
  this.uploadRevision++;
@@ -270,10 +301,10 @@ var SagepilotInstallationClient = class {
270
301
  this.startup = null;
271
302
  this.owners.clear();
272
303
  }
273
- /** Rejects foreign, identified or malformed session responses before credentials are used. */
304
+ /** Rejects foreign or malformed session responses before credentials are used. */
274
305
  validateSession(value) {
275
306
  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)) {
307
+ 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
308
  throw new Error("SDK session response has invalid scope or ownership.");
278
309
  }
279
310
  const url = safeURL(session.ingestion_url);
@@ -338,7 +369,7 @@ function createSagepilotInstallationClient(options) {
338
369
 
339
370
  // package.json
340
371
  var name = "@sagepilot-ai/web-events-addon";
341
- var version = "0.2.4";
372
+ var version = "0.2.6";
342
373
 
343
374
  // src/projection.ts
344
375
  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 +2371,6 @@ function createPopup(options) {
2340
2371
  }
2341
2372
  const result = await options.submit(contact);
2342
2373
  if (revision !== generation) return;
2343
- options.emit("popup_submitted");
2344
2374
  for (const input of inputs.values()) input.value = "";
2345
2375
  if (result.status === "verification_required") {
2346
2376
  let restartForm2 = function() {
@@ -2638,13 +2668,14 @@ function createSagepilotWebEvents(options) {
2638
2668
  captureBytes -= bytes;
2639
2669
  });
2640
2670
  };
2671
+ function emitPopup(action) {
2672
+ if (enabled()) capture("lifecycle", { action }, epoch, Date.now());
2673
+ }
2641
2674
  const popup = createPopup({
2642
2675
  client,
2643
2676
  storage,
2644
2677
  report,
2645
- emit(action) {
2646
- if (enabled()) capture("lifecycle", { action }, epoch, Date.now());
2647
- },
2678
+ emit: emitPopup,
2648
2679
  submit(input) {
2649
2680
  return addon.submitContact(input);
2650
2681
  },
@@ -2661,7 +2692,7 @@ function createSagepilotWebEvents(options) {
2661
2692
  detach(true);
2662
2693
  client.cancelPending();
2663
2694
  try {
2664
- await operation();
2695
+ return await operation();
2665
2696
  } finally {
2666
2697
  identityChanging = false;
2667
2698
  if (!destroyed) {
@@ -2890,17 +2921,21 @@ function createSagepilotWebEvents(options) {
2890
2921
  }
2891
2922
  },
2892
2923
  flush,
2893
- /** Submits contact-only data through the explicit identity endpoint, never through autocapture. */
2924
+ /** Keeps contact on the identity endpoint and fences capture while adopting the customer. */
2894
2925
  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);
2926
+ const session = client.session();
2927
+ const result = session?.popup?.verification === "none" && session.customer_id === null ? await changeIdentity(() => client.submitContact(input)) : await client.submitContact(input);
2928
+ if (result.status === "recorded") {
2929
+ if (result.popup_submission_recorded !== true) emitPopup("popup_submitted");
2930
+ await completeVisitor(session?.anonymous_id);
2931
+ }
2898
2932
  return result;
2899
2933
  },
2900
2934
  /** Verifies proof behind a capture fence; buffered anonymous observations remain anonymous. */
2901
2935
  async verifyContact(challengeId, code) {
2902
2936
  const anonymousId = client.session()?.anonymous_id;
2903
- await changeIdentity(() => client.verifyContact(challengeId, code));
2937
+ const session = await changeIdentity(() => client.verifyContact(challengeId, code));
2938
+ if (session.popup_submission_recorded !== true) emitPopup("popup_submitted");
2904
2939
  await completeVisitor(anonymousId);
2905
2940
  },
2906
2941
  /** Applies an explicit logout boundary without relabeling earlier observations. */