@reform-society/agera-core 0.2.0 → 0.4.0
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.ts +168 -1
- package/dist/index.js +298 -1
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -274,6 +274,10 @@ type CrmFetchResult = {
|
|
|
274
274
|
ok: boolean;
|
|
275
275
|
status: number;
|
|
276
276
|
data: Record<string, unknown> | null;
|
|
277
|
+
raw?: unknown;
|
|
278
|
+
error?: unknown;
|
|
279
|
+
cancelled?: boolean;
|
|
280
|
+
normalized?: unknown;
|
|
277
281
|
};
|
|
278
282
|
type CrmCommand = {
|
|
279
283
|
client: string;
|
|
@@ -317,6 +321,156 @@ declare function executeCrm(gateway: CrmGateway, command: CrmCommand, options?:
|
|
|
317
321
|
signal?: AbortSignal;
|
|
318
322
|
}): Promise<CrmOutcome>;
|
|
319
323
|
//#endregion
|
|
324
|
+
//#region src/lifecycle.d.ts
|
|
325
|
+
type NormalizedResult = {
|
|
326
|
+
delivery: "observed" | "unobserved";
|
|
327
|
+
operation: string;
|
|
328
|
+
operationStatus: "acknowledged" | "pending" | "completed" | "rejected" | "unknown";
|
|
329
|
+
contactStatus: "created" | "updated" | "unknown";
|
|
330
|
+
contactEvidence: "provider" | "inferred" | "unknown";
|
|
331
|
+
paymentStatus: "unknown" | "pending" | "succeeded" | "failed";
|
|
332
|
+
paymentEvidence: "provider" | "inferred" | "unknown";
|
|
333
|
+
nextAction?: {
|
|
334
|
+
type: "redirect" | "poll" | "verify";
|
|
335
|
+
url?: string;
|
|
336
|
+
};
|
|
337
|
+
outcome?: string;
|
|
338
|
+
extensions?: Record<string, unknown>;
|
|
339
|
+
};
|
|
340
|
+
type EventSource = {
|
|
341
|
+
client: string;
|
|
342
|
+
integration?: string;
|
|
343
|
+
formId?: string;
|
|
344
|
+
triggerId?: string;
|
|
345
|
+
pagePath: string;
|
|
346
|
+
};
|
|
347
|
+
type RequestSnapshot = {
|
|
348
|
+
fields: Record<string, unknown>;
|
|
349
|
+
context: Record<string, unknown>;
|
|
350
|
+
config: Record<string, unknown>;
|
|
351
|
+
method: string;
|
|
352
|
+
delivery: "wait" | "background";
|
|
353
|
+
body?: unknown;
|
|
354
|
+
};
|
|
355
|
+
type EventEnvelope = {
|
|
356
|
+
schemaVersion: 1;
|
|
357
|
+
eventId: string;
|
|
358
|
+
occurredAt: string;
|
|
359
|
+
source: EventSource;
|
|
360
|
+
context: Record<string, unknown>;
|
|
361
|
+
result: NormalizedResult;
|
|
362
|
+
submissionId?: string;
|
|
363
|
+
flowId?: string;
|
|
364
|
+
attemptId?: string;
|
|
365
|
+
conversionId?: string;
|
|
366
|
+
request?: RequestSnapshot;
|
|
367
|
+
};
|
|
368
|
+
type LifecycleEvent = EventEnvelope & ({
|
|
369
|
+
type: "crm.dispatched";
|
|
370
|
+
submissionId: string;
|
|
371
|
+
request: RequestSnapshot;
|
|
372
|
+
} | {
|
|
373
|
+
type: "crm.response";
|
|
374
|
+
submissionId: string;
|
|
375
|
+
request: RequestSnapshot;
|
|
376
|
+
response: {
|
|
377
|
+
httpStatus: number;
|
|
378
|
+
raw: unknown;
|
|
379
|
+
};
|
|
380
|
+
} | {
|
|
381
|
+
type: "crm.failed" | "crm.cancelled";
|
|
382
|
+
submissionId: string;
|
|
383
|
+
request: RequestSnapshot;
|
|
384
|
+
} | {
|
|
385
|
+
type: "element.entered" | "element.clicked" | "element.viewed" | "step.changed";
|
|
386
|
+
} | {
|
|
387
|
+
type: "flow.started" | "flow.returned" | "flow.verified" | "flow.failed";
|
|
388
|
+
flowId: string;
|
|
389
|
+
attemptId: string;
|
|
390
|
+
});
|
|
391
|
+
type LifecycleType = LifecycleEvent["type"];
|
|
392
|
+
declare function unknownResult(delivery?: NormalizedResult["delivery"]): NormalizedResult;
|
|
393
|
+
/** Serializable copies omit functions, DOM handles, and cyclic references. */
|
|
394
|
+
declare function snapshot<T>(value: T): T;
|
|
395
|
+
declare function createLifecycleBus(onError?: () => void): {
|
|
396
|
+
on: <T extends LifecycleType>(type: T | "*", listener: (event: LifecycleEvent & {
|
|
397
|
+
type: T;
|
|
398
|
+
}) => unknown) => () => void;
|
|
399
|
+
emit: (event: LifecycleEvent) => void;
|
|
400
|
+
clear(): void;
|
|
401
|
+
};
|
|
402
|
+
declare function normalizeResult(value: unknown, httpStatus?: number): NormalizedResult;
|
|
403
|
+
//#endregion
|
|
404
|
+
//#region src/tracking.d.ts
|
|
405
|
+
type ValueResolver = string | number | boolean | null | undefined | ValueResolver[] | {
|
|
406
|
+
[key: string]: ValueResolver;
|
|
407
|
+
} | ((event: LifecycleEvent) => unknown);
|
|
408
|
+
type TrackingConfig = {
|
|
409
|
+
destination: string | ((payload: Record<string, unknown>, event: LifecycleEvent) => unknown);
|
|
410
|
+
eventKey?: string;
|
|
411
|
+
reset?: Record<string, unknown>;
|
|
412
|
+
events: Partial<Record<LifecycleType, (event: LifecycleEvent) => string | null | undefined>>;
|
|
413
|
+
payload?: Record<string, ValueResolver>;
|
|
414
|
+
userData?: {
|
|
415
|
+
key: string;
|
|
416
|
+
fields: Record<string, string[] | ((event: LifecycleEvent) => unknown)>;
|
|
417
|
+
};
|
|
418
|
+
consent?: (event: LifecycleEvent) => boolean;
|
|
419
|
+
dedupe?: {
|
|
420
|
+
key: (event: LifecycleEvent, name: string) => string | undefined;
|
|
421
|
+
ttlMs: number;
|
|
422
|
+
maxEntries?: number;
|
|
423
|
+
};
|
|
424
|
+
envelope?: (payload: Record<string, unknown>, event: LifecycleEvent) => Record<string, unknown> | null;
|
|
425
|
+
};
|
|
426
|
+
declare function firstDefined(values: unknown[]): unknown;
|
|
427
|
+
declare function mapTrackingEvent(config: TrackingConfig, event: LifecycleEvent): {
|
|
428
|
+
name: string;
|
|
429
|
+
payload: any;
|
|
430
|
+
} | null;
|
|
431
|
+
declare function createTrackingConsumer(options: {
|
|
432
|
+
config: TrackingConfig;
|
|
433
|
+
now: () => number;
|
|
434
|
+
queue: (name: string, payload: Record<string, unknown>) => void;
|
|
435
|
+
diagnostic?: (status: "queued" | "submitted" | "deduplicated" | "failed") => void;
|
|
436
|
+
}): (event: LifecycleEvent) => void;
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/flow-attempts.d.ts
|
|
439
|
+
type FlowAttempt = {
|
|
440
|
+
schemaVersion: 1;
|
|
441
|
+
flowId: string;
|
|
442
|
+
attemptId: string;
|
|
443
|
+
submissionId?: string;
|
|
444
|
+
token: string;
|
|
445
|
+
expectedReturn: string;
|
|
446
|
+
expiresAt: number;
|
|
447
|
+
acknowledged: boolean;
|
|
448
|
+
source: EventSource;
|
|
449
|
+
context: Record<string, unknown>;
|
|
450
|
+
request?: RequestSnapshot;
|
|
451
|
+
};
|
|
452
|
+
type AttemptStorage = {
|
|
453
|
+
getItem(key: string): string | null;
|
|
454
|
+
setItem(key: string, value: string): void;
|
|
455
|
+
removeItem(key: string): void;
|
|
456
|
+
};
|
|
457
|
+
declare function createAttemptStore(options: {
|
|
458
|
+
namespace: string;
|
|
459
|
+
storage: AttemptStorage;
|
|
460
|
+
now: () => number;
|
|
461
|
+
id: () => string;
|
|
462
|
+
ttlMs: number;
|
|
463
|
+
maxEntries?: number;
|
|
464
|
+
diagnostic?: () => void;
|
|
465
|
+
}): {
|
|
466
|
+
begin: (input: Omit<FlowAttempt, "schemaVersion" | "attemptId" | "token" | "expiresAt" | "acknowledged">) => FlowAttempt | null;
|
|
467
|
+
acknowledge: (token: string) => boolean;
|
|
468
|
+
consume: (token: string, destination: string) => FlowAttempt | null;
|
|
469
|
+
cancel: (token: string) => boolean;
|
|
470
|
+
read: (token: string) => FlowAttempt | null;
|
|
471
|
+
purge: () => void;
|
|
472
|
+
};
|
|
473
|
+
//#endregion
|
|
320
474
|
//#region src/utm.d.ts
|
|
321
475
|
declare const UTM_KEYS: readonly ["source", "campaign", "medium", "content", "term", "wec"];
|
|
322
476
|
type Utms = Partial<Record<(typeof UTM_KEYS)[number], string>>;
|
|
@@ -396,6 +550,19 @@ type DeviceInput = {
|
|
|
396
550
|
};
|
|
397
551
|
declare function detectDeviceType(input: DeviceInput): DeviceType;
|
|
398
552
|
//#endregion
|
|
553
|
+
//#region src/visitor.d.ts
|
|
554
|
+
declare const VISITOR_ID_STORAGE_KEY = "agera:vid";
|
|
555
|
+
/**
|
|
556
|
+
* Read the semi-persistent pseudonymous visitor id from storage, generating
|
|
557
|
+
* and persisting a new one when absent. Used only to correlate requests from
|
|
558
|
+
* the same browser in crm-api logs — never stored server-side.
|
|
559
|
+
*
|
|
560
|
+
* Returns null when storage can't be read or written (private mode, quota,
|
|
561
|
+
* blocked storage): an unpersisted id would look like a new visitor on every
|
|
562
|
+
* page load, so callers omit the header instead.
|
|
563
|
+
*/
|
|
564
|
+
declare function getOrCreateVisitorId(storage: StorageAccessor): string | null;
|
|
565
|
+
//#endregion
|
|
399
566
|
//#region src/crm-fetch.d.ts
|
|
400
567
|
type CrmFetchOptions = {
|
|
401
568
|
waitForResponse?: boolean;
|
|
@@ -446,5 +613,5 @@ declare function assembleCrmRequest({
|
|
|
446
613
|
//#region src/polling.d.ts
|
|
447
614
|
declare function matchesFormat(example: unknown, value: unknown): boolean;
|
|
448
615
|
//#endregion
|
|
449
|
-
export { type AssembleCrmRequestParams, type BandQuizResults, CRM_CONTACT_KEYS, type CompositePredicate, type CounterDisplayConfig, type CounterPayload, type CounterResponse, type CounterUpdateConfig, type CrmCommand, type CrmConfig, type CrmContactKey, type CrmDispatch, type CrmFetchOptions, type CrmFetchResult, type CrmFrontendKey, type CrmGateway, type CrmGatewayResult, type CrmGroupedAnswer, type CrmJsonGroups, type CrmOutcome, type CrmV2PatchRequest, type CrmV2Payload, type CrmV2Request, type DeviceInput, type DeviceType, type ErrorMessages, type FetchBasinJWTOptions, type InsertDirective, type JsonValue, type JwtPayload, type MemoryCache, type MemoryCacheOptions, type MemoryCacheSetOptions, type PassQuizResults, type PlacesMessageKey, type PlacesMessages, type Predicate, type PredicateOp, type ProgressInfo, type ProgressModeInfo, type QuestionMetrics, type QuizBandDefinition, type QuizConfig, type QuizOutcomeModel, type QuizPassFailConfig, type QuizQuestionInput, type QuizQuestionModel, type QuizQuestionResult, type QuizQuestionState, type QuizResults, type QuizScoringModel, type QuizSectionConfig, type QuizSectionResults, type ResolveValueSources, type ScoreQuizResults, type SessionState, type ShareConfig, type SimplePredicate, type SplitConfig, type StepState, type StorageAccessor, type SurveyOptions, type Utms, type ValidationRule, type Validators, type WeightedQuizResults, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildContext, buildCounterPayload, buildFcrmUtms, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, createMemoryCache, createSessionStore, createStepsStore, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, matchesFormat, mergeAnswers, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, updateUrlWithParams, validators };
|
|
616
|
+
export { type AssembleCrmRequestParams, type AttemptStorage, type BandQuizResults, CRM_CONTACT_KEYS, type CompositePredicate, type CounterDisplayConfig, type CounterPayload, type CounterResponse, type CounterUpdateConfig, type CrmCommand, type CrmConfig, type CrmContactKey, type CrmDispatch, type CrmFetchOptions, type CrmFetchResult, type CrmFrontendKey, type CrmGateway, type CrmGatewayResult, type CrmGroupedAnswer, type CrmJsonGroups, type CrmOutcome, type CrmV2PatchRequest, type CrmV2Payload, type CrmV2Request, type DeviceInput, type DeviceType, type ErrorMessages, type EventSource, type FetchBasinJWTOptions, type FlowAttempt, type InsertDirective, type JsonValue, type JwtPayload, type LifecycleEvent, type LifecycleType, type MemoryCache, type MemoryCacheOptions, type MemoryCacheSetOptions, type NormalizedResult, type PassQuizResults, type PlacesMessageKey, type PlacesMessages, type Predicate, type PredicateOp, type ProgressInfo, type ProgressModeInfo, type QuestionMetrics, type QuizBandDefinition, type QuizConfig, type QuizOutcomeModel, type QuizPassFailConfig, type QuizQuestionInput, type QuizQuestionModel, type QuizQuestionResult, type QuizQuestionState, type QuizResults, type QuizScoringModel, type QuizSectionConfig, type QuizSectionResults, type RequestSnapshot, type ResolveValueSources, type ScoreQuizResults, type SessionState, type ShareConfig, type SimplePredicate, type SplitConfig, type StepState, type StorageAccessor, type SurveyOptions, type TrackingConfig, type Utms, VISITOR_ID_STORAGE_KEY, type ValidationRule, type Validators, type ValueResolver, type WeightedQuizResults, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildContext, buildCounterPayload, buildFcrmUtms, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, createAttemptStore, createLifecycleBus, createMemoryCache, createSessionStore, createStepsStore, createTrackingConsumer, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, firstDefined, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getOrCreateVisitorId, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, mapTrackingEvent, matchesFormat, mergeAnswers, normalizeResult, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, snapshot, unknownResult, updateUrlWithParams, validators };
|
|
450
617
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -789,6 +789,277 @@ async function executeCrm(gateway, command, options = {}) {
|
|
|
789
789
|
}
|
|
790
790
|
}
|
|
791
791
|
//#endregion
|
|
792
|
+
//#region src/lifecycle.ts
|
|
793
|
+
function unknownResult(delivery = "unobserved") {
|
|
794
|
+
return {
|
|
795
|
+
delivery,
|
|
796
|
+
operation: "unknown",
|
|
797
|
+
operationStatus: "unknown",
|
|
798
|
+
contactStatus: "unknown",
|
|
799
|
+
contactEvidence: "unknown",
|
|
800
|
+
paymentStatus: "unknown",
|
|
801
|
+
paymentEvidence: "unknown"
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
/** Serializable copies omit functions, DOM handles, and cyclic references. */
|
|
805
|
+
function snapshot(value) {
|
|
806
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
807
|
+
function copy(input) {
|
|
808
|
+
if (input === null || typeof input === "string" || typeof input === "boolean") return input;
|
|
809
|
+
if (typeof input === "number") return Number.isFinite(input) ? input : null;
|
|
810
|
+
if (typeof input !== "object" || seen.has(input)) return void 0;
|
|
811
|
+
if (!Array.isArray(input) && Object.getPrototypeOf(input) !== Object.prototype && Object.getPrototypeOf(input) !== null) return void 0;
|
|
812
|
+
seen.add(input);
|
|
813
|
+
const result = Array.isArray(input) ? input.map(copy) : Object.fromEntries(Object.entries(input).map(([key, item]) => [key, copy(item)]).filter(([, item]) => item !== void 0));
|
|
814
|
+
seen.delete(input);
|
|
815
|
+
return result;
|
|
816
|
+
}
|
|
817
|
+
return copy(value);
|
|
818
|
+
}
|
|
819
|
+
function createLifecycleBus(onError = () => {}) {
|
|
820
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
821
|
+
function report() {
|
|
822
|
+
try {
|
|
823
|
+
onError();
|
|
824
|
+
} catch {}
|
|
825
|
+
}
|
|
826
|
+
function on(type, listener) {
|
|
827
|
+
const set = listeners.get(type) ?? /* @__PURE__ */ new Set();
|
|
828
|
+
set.add(listener);
|
|
829
|
+
listeners.set(type, set);
|
|
830
|
+
return () => {
|
|
831
|
+
set.delete(listener);
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function emit(event) {
|
|
835
|
+
const captured = snapshot(event);
|
|
836
|
+
for (const listener of [...listeners.get(event.type) ?? [], ...listeners.get("*") ?? []]) try {
|
|
837
|
+
Promise.resolve(listener(snapshot(captured))).catch(report);
|
|
838
|
+
} catch {
|
|
839
|
+
report();
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return {
|
|
843
|
+
on,
|
|
844
|
+
emit,
|
|
845
|
+
clear() {
|
|
846
|
+
listeners.clear();
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
function normalizeResult(value, httpStatus) {
|
|
851
|
+
const result = unknownResult(httpStatus === void 0 ? "unobserved" : "observed");
|
|
852
|
+
if (httpStatus !== void 0 && httpStatus >= 400) result.operationStatus = "rejected";
|
|
853
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return result;
|
|
854
|
+
const record = value;
|
|
855
|
+
if (typeof record.operation === "string") result.operation = record.operation;
|
|
856
|
+
for (const [key, allowed] of Object.entries({
|
|
857
|
+
operationStatus: [
|
|
858
|
+
"acknowledged",
|
|
859
|
+
"pending",
|
|
860
|
+
"completed",
|
|
861
|
+
"rejected",
|
|
862
|
+
"unknown"
|
|
863
|
+
],
|
|
864
|
+
contactStatus: [
|
|
865
|
+
"created",
|
|
866
|
+
"updated",
|
|
867
|
+
"unknown"
|
|
868
|
+
],
|
|
869
|
+
contactEvidence: [
|
|
870
|
+
"provider",
|
|
871
|
+
"inferred",
|
|
872
|
+
"unknown"
|
|
873
|
+
],
|
|
874
|
+
paymentStatus: [
|
|
875
|
+
"unknown",
|
|
876
|
+
"pending",
|
|
877
|
+
"succeeded",
|
|
878
|
+
"failed"
|
|
879
|
+
],
|
|
880
|
+
paymentEvidence: [
|
|
881
|
+
"provider",
|
|
882
|
+
"inferred",
|
|
883
|
+
"unknown"
|
|
884
|
+
]
|
|
885
|
+
})) if (typeof record[key] === "string" && allowed.includes(record[key])) Object.assign(result, { [key]: record[key] });
|
|
886
|
+
if (record.nextAction && typeof record.nextAction === "object") {
|
|
887
|
+
const action = record.nextAction;
|
|
888
|
+
if (action.type === "redirect" || action.type === "poll" || action.type === "verify") result.nextAction = {
|
|
889
|
+
type: action.type,
|
|
890
|
+
...typeof action.url === "string" ? { url: action.url } : {}
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
if (record.extensions && typeof record.extensions === "object" && !Array.isArray(record.extensions)) result.extensions = snapshot(record.extensions);
|
|
894
|
+
return result;
|
|
895
|
+
}
|
|
896
|
+
//#endregion
|
|
897
|
+
//#region src/tracking.ts
|
|
898
|
+
function firstDefined(values) {
|
|
899
|
+
return values.find((value) => value !== void 0 && value !== "");
|
|
900
|
+
}
|
|
901
|
+
function resolveValue$1(value, event) {
|
|
902
|
+
if (typeof value === "function") return value(snapshot(event));
|
|
903
|
+
if (Array.isArray(value)) return value.map((item) => resolveValue$1(item, event));
|
|
904
|
+
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveValue$1(item, event)]).filter(([, item]) => item !== void 0));
|
|
905
|
+
return value;
|
|
906
|
+
}
|
|
907
|
+
function mapTrackingEvent(config, event) {
|
|
908
|
+
if (config.consent && !config.consent(snapshot(event))) return null;
|
|
909
|
+
const name = config.events[event.type]?.(snapshot(event));
|
|
910
|
+
if (typeof name !== "string" || name === "") return null;
|
|
911
|
+
const payload = Object.fromEntries(Object.entries(config.payload ?? {}).map(([key, value]) => [key, resolveValue$1(value, event)]).filter(([, value]) => value !== void 0));
|
|
912
|
+
payload[config.eventKey ?? "event"] = name;
|
|
913
|
+
if (config.userData) {
|
|
914
|
+
const fields = event.request?.fields ?? {};
|
|
915
|
+
const eligible = event.type.startsWith("crm.") || event.type.startsWith("flow.");
|
|
916
|
+
payload[config.userData.key] = Object.fromEntries(Object.entries(config.userData.fields).map(([key, aliases]) => {
|
|
917
|
+
return [key, !eligible ? void 0 : typeof aliases === "function" ? aliases(snapshot(event)) : firstDefined(aliases.map((alias) => fields[alias]))];
|
|
918
|
+
}).filter(([, value]) => value !== void 0 && value !== null && value !== ""));
|
|
919
|
+
}
|
|
920
|
+
const output = config.envelope ? config.envelope(snapshot(payload), snapshot(event)) : payload;
|
|
921
|
+
return output ? {
|
|
922
|
+
name,
|
|
923
|
+
payload: snapshot(output)
|
|
924
|
+
} : null;
|
|
925
|
+
}
|
|
926
|
+
function createTrackingConsumer(options) {
|
|
927
|
+
const seen = /* @__PURE__ */ new Map();
|
|
928
|
+
function report(status) {
|
|
929
|
+
try {
|
|
930
|
+
options.diagnostic?.(status);
|
|
931
|
+
} catch {}
|
|
932
|
+
}
|
|
933
|
+
return function consume(event) {
|
|
934
|
+
let key;
|
|
935
|
+
try {
|
|
936
|
+
const { config } = options;
|
|
937
|
+
const mapped = mapTrackingEvent(config, event);
|
|
938
|
+
if (!mapped) return;
|
|
939
|
+
const now = options.now();
|
|
940
|
+
for (const [key, expires] of seen) if (expires <= now) seen.delete(key);
|
|
941
|
+
key = config.dedupe?.key(snapshot(event), mapped.name);
|
|
942
|
+
if (key !== void 0 && seen.has(key)) {
|
|
943
|
+
report("deduplicated");
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
if (key !== void 0 && config.dedupe && config.dedupe.ttlMs > 0) {
|
|
947
|
+
while (seen.size >= Math.max(1, config.dedupe.maxEntries ?? 1e3)) seen.delete(seen.keys().next().value);
|
|
948
|
+
seen.set(key, now + config.dedupe.ttlMs);
|
|
949
|
+
}
|
|
950
|
+
if (typeof config.destination === "string") {
|
|
951
|
+
if (config.reset) options.queue(config.destination, snapshot(config.reset));
|
|
952
|
+
options.queue(config.destination, mapped.payload);
|
|
953
|
+
report("queued");
|
|
954
|
+
} else Promise.resolve(config.destination(mapped.payload, snapshot(event))).then(() => report("submitted"), () => {
|
|
955
|
+
if (key !== void 0) seen.delete(key);
|
|
956
|
+
report("failed");
|
|
957
|
+
});
|
|
958
|
+
} catch {
|
|
959
|
+
if (key !== void 0) seen.delete(key);
|
|
960
|
+
report("failed");
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
//#endregion
|
|
965
|
+
//#region src/flow-attempts.ts
|
|
966
|
+
function createAttemptStore(options) {
|
|
967
|
+
const prefix = `agera:attempts:${options.namespace}:`;
|
|
968
|
+
const indexKey = `${prefix}index`;
|
|
969
|
+
function report() {
|
|
970
|
+
try {
|
|
971
|
+
options.diagnostic?.();
|
|
972
|
+
} catch {}
|
|
973
|
+
}
|
|
974
|
+
function readIds() {
|
|
975
|
+
try {
|
|
976
|
+
const ids = JSON.parse(options.storage.getItem(indexKey) ?? "[]");
|
|
977
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string").slice(-Math.max(1, options.maxEntries ?? 20)) : [];
|
|
978
|
+
} catch {
|
|
979
|
+
report();
|
|
980
|
+
return [];
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
function read(token) {
|
|
984
|
+
try {
|
|
985
|
+
const value = JSON.parse(options.storage.getItem(prefix + token) ?? "null");
|
|
986
|
+
if (!value || typeof value !== "object") return null;
|
|
987
|
+
const item = value;
|
|
988
|
+
if (item.schemaVersion !== 1 || item.token !== token || typeof item.attemptId !== "string" || typeof item.flowId !== "string" || typeof item.expectedReturn !== "string" || typeof item.expiresAt !== "number" || !Number.isFinite(item.expiresAt) || item.expiresAt <= options.now() || typeof item.acknowledged !== "boolean" || !item.source || typeof item.source.client !== "string" || !item.context || typeof item.context !== "object") {
|
|
989
|
+
options.storage.removeItem(prefix + token);
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
return snapshot(item);
|
|
993
|
+
} catch {
|
|
994
|
+
report();
|
|
995
|
+
return null;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function remove(token) {
|
|
999
|
+
try {
|
|
1000
|
+
options.storage.removeItem(prefix + token);
|
|
1001
|
+
return true;
|
|
1002
|
+
} catch {
|
|
1003
|
+
report();
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function write(item) {
|
|
1008
|
+
try {
|
|
1009
|
+
options.storage.setItem(prefix + item.token, JSON.stringify(item));
|
|
1010
|
+
return true;
|
|
1011
|
+
} catch {
|
|
1012
|
+
report();
|
|
1013
|
+
return false;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function begin(input) {
|
|
1017
|
+
if (!Number.isFinite(options.ttlMs) || options.ttlMs <= 0) return null;
|
|
1018
|
+
const attempt = {
|
|
1019
|
+
...snapshot(input),
|
|
1020
|
+
schemaVersion: 1,
|
|
1021
|
+
attemptId: options.id(),
|
|
1022
|
+
token: options.id(),
|
|
1023
|
+
expiresAt: options.now() + options.ttlMs,
|
|
1024
|
+
acknowledged: false
|
|
1025
|
+
};
|
|
1026
|
+
const ids = readIds().filter((token) => read(token) !== null);
|
|
1027
|
+
while (ids.length >= Math.max(1, options.maxEntries ?? 20)) remove(ids.shift());
|
|
1028
|
+
if (!write(attempt)) return null;
|
|
1029
|
+
try {
|
|
1030
|
+
options.storage.setItem(indexKey, JSON.stringify([...ids, attempt.token]));
|
|
1031
|
+
} catch {
|
|
1032
|
+
remove(attempt.token);
|
|
1033
|
+
report();
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
return snapshot(attempt);
|
|
1037
|
+
}
|
|
1038
|
+
function acknowledge(token) {
|
|
1039
|
+
const attempt = read(token);
|
|
1040
|
+
return attempt ? write({
|
|
1041
|
+
...attempt,
|
|
1042
|
+
acknowledged: true
|
|
1043
|
+
}) : false;
|
|
1044
|
+
}
|
|
1045
|
+
function consume(token, destination) {
|
|
1046
|
+
const attempt = read(token);
|
|
1047
|
+
if (!attempt?.acknowledged || attempt.expectedReturn !== destination) return null;
|
|
1048
|
+
return remove(token) ? attempt : null;
|
|
1049
|
+
}
|
|
1050
|
+
function purge() {
|
|
1051
|
+
for (const token of readIds()) read(token);
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
begin,
|
|
1055
|
+
acknowledge,
|
|
1056
|
+
consume,
|
|
1057
|
+
cancel: remove,
|
|
1058
|
+
read,
|
|
1059
|
+
purge
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
//#endregion
|
|
792
1063
|
//#region src/utm.ts
|
|
793
1064
|
const UTM_KEYS = [
|
|
794
1065
|
"source",
|
|
@@ -1115,6 +1386,32 @@ function detectDeviceType(input) {
|
|
|
1115
1386
|
return "desktop";
|
|
1116
1387
|
}
|
|
1117
1388
|
//#endregion
|
|
1389
|
+
//#region src/visitor.ts
|
|
1390
|
+
const VISITOR_ID_STORAGE_KEY = "agera:vid";
|
|
1391
|
+
function generateVisitorId() {
|
|
1392
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Read the semi-persistent pseudonymous visitor id from storage, generating
|
|
1396
|
+
* and persisting a new one when absent. Used only to correlate requests from
|
|
1397
|
+
* the same browser in crm-api logs — never stored server-side.
|
|
1398
|
+
*
|
|
1399
|
+
* Returns null when storage can't be read or written (private mode, quota,
|
|
1400
|
+
* blocked storage): an unpersisted id would look like a new visitor on every
|
|
1401
|
+
* page load, so callers omit the header instead.
|
|
1402
|
+
*/
|
|
1403
|
+
function getOrCreateVisitorId(storage) {
|
|
1404
|
+
try {
|
|
1405
|
+
const existing = storage.getItem(VISITOR_ID_STORAGE_KEY);
|
|
1406
|
+
if (existing) return existing;
|
|
1407
|
+
const id = generateVisitorId();
|
|
1408
|
+
storage.setItem(VISITOR_ID_STORAGE_KEY, id);
|
|
1409
|
+
return id;
|
|
1410
|
+
} catch {
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
//#endregion
|
|
1118
1415
|
//#region src/crm-fetch.ts
|
|
1119
1416
|
const noop = () => {};
|
|
1120
1417
|
async function crmFetch(url, payload, options = {}) {
|
|
@@ -1234,6 +1531,6 @@ function matchesFormat(example, value) {
|
|
|
1234
1531
|
return example === value;
|
|
1235
1532
|
}
|
|
1236
1533
|
//#endregion
|
|
1237
|
-
export { CRM_CONTACT_KEYS, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildContext, buildCounterPayload, buildFcrmUtms, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, createMemoryCache, createSessionStore, createStepsStore, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, matchesFormat, mergeAnswers, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, updateUrlWithParams, validators };
|
|
1534
|
+
export { CRM_CONTACT_KEYS, VISITOR_ID_STORAGE_KEY, aggregateWeightedScores, answersChanged, assembleCrmRequest, buildContext, buildCounterPayload, buildFcrmUtms, calculateScoreResults, calculateSectionResults, calculateWinner, computeAutoTarget, createAttemptStore, createLifecycleBus, createMemoryCache, createSessionStore, createStepsStore, createTrackingConsumer, crmFetch, decodeJwtPayload, deriveBandResults, derivePassResults, deriveQuestionState, detectDeviceType, errorMessages, evaluateNextMap, evaluatePredicate, evaluateShowIf, executeCrm, extractUtms, fetchBasinJWT, fetchCounter, firstDefined, flattenObject, formatCounterValue, getErrorMessagesForLanguage, getOrCreateVisitorId, getPhoneExample, getPlacesMessagesForLanguage, getPostalExample, groupCrmAnswers, inferCounterName, isCounterResponse, isCrmContactKey, isJwtExpired, isSafeRedirectUrl, mapTrackingEvent, matchesFormat, mergeAnswers, normalizeResult, parseCounterUpdateAttr, parseInsertDirective, parseInsertDirectives, resolveValue, scoreExactQuestion, scorePartialQuestion, scoreQuiz, scoreQuizQuestions, serializeCrmJsonGroups, snapshot, unknownResult, updateUrlWithParams, validators };
|
|
1238
1535
|
|
|
1239
1536
|
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reform-society/agera-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DOM-free core logic for Agera",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
38
|
-
"tsdown": "^0.21.
|
|
38
|
+
"tsdown": "^0.21.10",
|
|
39
39
|
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
40
|
-
"vite": "
|
|
41
|
-
"vitest": "^4.1.
|
|
40
|
+
"vite": "7.3.5",
|
|
41
|
+
"vitest": "^4.1.11"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"loglevel": "^1.9.2",
|
|
45
|
-
"nanostores": "^1.
|
|
45
|
+
"nanostores": "^1.5.2"
|
|
46
46
|
}
|
|
47
47
|
}
|