@sagepilot-ai/web-events-addon 0.2.4 → 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/README.md +2 -2
- package/dist/index.cjs +72 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +72 -38
- package/dist/index.js.map +1 -1
- package/dist/sagepilot-web-events.global.js +72 -38
- package/dist/sagepilot-web-events.global.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -87,11 +87,11 @@ In Settings → SDK, select a web installation, turn on **First-party visitor co
|
|
|
87
87
|
|
|
88
88
|
Records retain event ID, occurrence time, workspace/registered installation, original signed session owner and local identity epoch. A separate credential cache retains at most four owner generations across verification/reload so anonymous backlog stays anonymous. Records whose authority is unavailable or explicitly revoked are dropped with diagnostics; they never block newer owners or get relabeled.
|
|
89
89
|
|
|
90
|
-
## Popup and
|
|
90
|
+
## Popup and visitor identity
|
|
91
91
|
|
|
92
92
|
Settings → SDK publishes a text-only popup template independently from mapping versions. The installed runtime renders the published revision inside a shadow DOM, with exact page paths, delay/scroll conditions, frequency limits, required fields and keyboard dismissal/focus management. Submitting the form sets the existing contact endpoint's `marketing_consent` field to `true`. The renderer and dashboard preview have no consent checkbox or label. Drafts never render. Disabled publication removes an open popup after refresh. Native app registrations do not render website popups.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
A contact-only form saves self-reported details on a new lead and adopts its signed session. Earlier events retain their original credentials and resolve to the lead through the browser link. When email verification is enabled, the SDK adopts a customer session after a successful challenge. Phone numbers remain unverified. Form values and verification codes stay on the identity endpoints. Popup lifecycle observations obey collection permission.
|
|
95
95
|
|
|
96
96
|
The popup handles submission/verification itself. Host-owned forms can use `submitContact({submission_id,popup_revision,name?,email?,phone?,marketing_consent})` and `verifyContact(challengeId,code)`. `logout()` immediately pauses capture and cancels pending uploads before revoking the API grant and establishing a fresh anonymous owner. Queued events retain their original ownership. The collector discards any records whose credentials the logout revoked. Existing signed ingress credentials expire within 15 minutes.
|
|
97
97
|
|
package/dist/index.cjs
CHANGED
|
@@ -142,27 +142,40 @@ var SagepilotInstallationClient = class {
|
|
|
142
142
|
this.startup = startup;
|
|
143
143
|
return startup;
|
|
144
144
|
}
|
|
145
|
-
/** Renews
|
|
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
|
-
|
|
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 (
|
|
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 SagepilotInstallationClient = class {
|
|
|
176
189
|
this.pending = pending;
|
|
177
190
|
return pending;
|
|
178
191
|
}
|
|
179
|
-
/**
|
|
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 SagepilotInstallationClient = class {
|
|
|
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 SagepilotInstallationClient = class {
|
|
|
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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 SagepilotInstallationClient = class {
|
|
|
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 SagepilotInstallationClient = class {
|
|
|
263
289
|
this.transitioning = true;
|
|
264
290
|
const generation = this.generation, prior = this.current;
|
|
265
291
|
try {
|
|
266
|
-
|
|
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 SagepilotInstallationClient = class {
|
|
|
296
326
|
this.startup = null;
|
|
297
327
|
this.owners.clear();
|
|
298
328
|
}
|
|
299
|
-
/** Rejects foreign
|
|
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 @@ function createSagepilotInstallationClient(options) {
|
|
|
364
394
|
|
|
365
395
|
// package.json
|
|
366
396
|
var name = "@sagepilot-ai/web-events-addon";
|
|
367
|
-
var version = "0.2.
|
|
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 @@ function createPopup(options) {
|
|
|
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 @@ function createSagepilotWebEvents(options) {
|
|
|
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
|
|
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 @@ function createSagepilotWebEvents(options) {
|
|
|
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 @@ function createSagepilotWebEvents(options) {
|
|
|
2916
2946
|
}
|
|
2917
2947
|
},
|
|
2918
2948
|
flush,
|
|
2919
|
-
/**
|
|
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
|
|
2922
|
-
const result = await client.submitContact(input);
|
|
2923
|
-
if (result.status === "recorded")
|
|
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. */
|