@cross-deck/web 0.2.0 → 0.3.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/CHANGELOG.md +35 -0
- package/README.md +33 -16
- package/dist/index.d.mts +82 -11
- package/dist/index.d.ts +82 -11
- package/dist/index.js +199 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +199 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
package/dist/index.mjs
CHANGED
|
@@ -46,7 +46,7 @@ function typeMapForStatus(status) {
|
|
|
46
46
|
|
|
47
47
|
// src/http.ts
|
|
48
48
|
var SDK_NAME = "@cross-deck/web";
|
|
49
|
-
var SDK_VERSION = "0.
|
|
49
|
+
var SDK_VERSION = "0.3.0";
|
|
50
50
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
51
51
|
var HttpClient = class {
|
|
52
52
|
constructor(config) {
|
|
@@ -245,6 +245,7 @@ var EventQueue = class {
|
|
|
245
245
|
this.lastFlushAt = 0;
|
|
246
246
|
this.lastError = null;
|
|
247
247
|
this.cancelTimer = null;
|
|
248
|
+
this.firstFlushFired = false;
|
|
248
249
|
}
|
|
249
250
|
enqueue(event) {
|
|
250
251
|
this.buffer.push(event);
|
|
@@ -271,12 +272,25 @@ var EventQueue = class {
|
|
|
271
272
|
const batch = this.buffer.splice(0);
|
|
272
273
|
this.inFlight += batch.length;
|
|
273
274
|
try {
|
|
275
|
+
const env = this.cfg.envelope();
|
|
274
276
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
275
|
-
body: {
|
|
277
|
+
body: {
|
|
278
|
+
// NorthStar §13.1 batch envelope. The backend validates these
|
|
279
|
+
// against the API-key-resolved app and rejects mismatches loudly
|
|
280
|
+
// (env_mismatch).
|
|
281
|
+
appId: env.appId,
|
|
282
|
+
environment: env.environment,
|
|
283
|
+
sdk: env.sdk,
|
|
284
|
+
events: batch
|
|
285
|
+
}
|
|
276
286
|
});
|
|
277
287
|
this.lastFlushAt = Date.now();
|
|
278
288
|
this.lastError = null;
|
|
279
289
|
this.inFlight -= batch.length;
|
|
290
|
+
if (!this.firstFlushFired) {
|
|
291
|
+
this.firstFlushFired = true;
|
|
292
|
+
this.cfg.onFirstFlushSuccess?.();
|
|
293
|
+
}
|
|
280
294
|
return result;
|
|
281
295
|
} catch (err) {
|
|
282
296
|
this.buffer.unshift(...batch);
|
|
@@ -595,29 +609,104 @@ function mintSessionId() {
|
|
|
595
609
|
return `sess_${ts}${randomChars(10)}`;
|
|
596
610
|
}
|
|
597
611
|
|
|
612
|
+
// src/debug.ts
|
|
613
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
614
|
+
/^email$/i,
|
|
615
|
+
/^password$/i,
|
|
616
|
+
/^token$/i,
|
|
617
|
+
/^secret$/i,
|
|
618
|
+
/^card$/i,
|
|
619
|
+
/^phone$/i,
|
|
620
|
+
/password/i,
|
|
621
|
+
/credit_?card/i
|
|
622
|
+
];
|
|
623
|
+
function findSensitivePropertyKeys(properties) {
|
|
624
|
+
if (!properties) return [];
|
|
625
|
+
const hits = [];
|
|
626
|
+
for (const k of Object.keys(properties)) {
|
|
627
|
+
if (SENSITIVE_KEY_PATTERNS.some((re) => re.test(k))) hits.push(k);
|
|
628
|
+
}
|
|
629
|
+
return hits;
|
|
630
|
+
}
|
|
631
|
+
var ConsoleDebugLogger = class {
|
|
632
|
+
constructor() {
|
|
633
|
+
this.enabled = false;
|
|
634
|
+
this.seen = /* @__PURE__ */ new Set();
|
|
635
|
+
}
|
|
636
|
+
emit(signal, message, context) {
|
|
637
|
+
if (!this.enabled) return;
|
|
638
|
+
if (ONCE_SIGNALS.has(signal)) {
|
|
639
|
+
if (this.seen.has(signal)) return;
|
|
640
|
+
this.seen.add(signal);
|
|
641
|
+
}
|
|
642
|
+
const ctx = context ? ` ${safeJson(context)}` : "";
|
|
643
|
+
console.info(`[crossdeck:${signal}] ${message}${ctx}`);
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var ONCE_SIGNALS = /* @__PURE__ */ new Set([
|
|
647
|
+
"sdk.configured",
|
|
648
|
+
"sdk.first_event_sent",
|
|
649
|
+
"sdk.environment_mismatch"
|
|
650
|
+
]);
|
|
651
|
+
function safeJson(obj) {
|
|
652
|
+
try {
|
|
653
|
+
return JSON.stringify(obj);
|
|
654
|
+
} catch {
|
|
655
|
+
return "[unserialisable context]";
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
598
659
|
// src/crossdeck.ts
|
|
599
660
|
var CrossdeckClient = class {
|
|
600
661
|
constructor() {
|
|
601
662
|
this.state = null;
|
|
602
663
|
}
|
|
603
664
|
/**
|
|
604
|
-
* Boot the SDK. Idempotent — calling
|
|
665
|
+
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
605
666
|
* is a no-op; calling with different options replaces the previous
|
|
606
667
|
* configuration.
|
|
668
|
+
*
|
|
669
|
+
* NorthStar §11.1: signature is `Crossdeck.init({ appId, publicKey,
|
|
670
|
+
* environment })`. The trio is validated up-front so a typo'd key or a
|
|
671
|
+
* mismatched env fails fast at boot rather than at first event-flush.
|
|
607
672
|
*/
|
|
608
|
-
|
|
673
|
+
init(options) {
|
|
609
674
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
610
675
|
throw new CrossdeckError({
|
|
611
676
|
type: "configuration_error",
|
|
612
677
|
code: "invalid_public_key",
|
|
613
|
-
message: "Crossdeck.
|
|
678
|
+
message: "Crossdeck.init requires a publishable key starting with cd_pub_."
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
if (!options.appId) {
|
|
682
|
+
throw new CrossdeckError({
|
|
683
|
+
type: "configuration_error",
|
|
684
|
+
code: "missing_app_id",
|
|
685
|
+
message: "Crossdeck.init requires an appId. Find yours in the Crossdeck dashboard."
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
if (options.environment !== "production" && options.environment !== "sandbox") {
|
|
689
|
+
throw new CrossdeckError({
|
|
690
|
+
type: "configuration_error",
|
|
691
|
+
code: "invalid_environment",
|
|
692
|
+
message: 'Crossdeck.init requires environment: "production" | "sandbox".'
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
const keyEnv = inferEnvFromKey(options.publicKey);
|
|
696
|
+
if (keyEnv && keyEnv !== options.environment) {
|
|
697
|
+
throw new CrossdeckError({
|
|
698
|
+
type: "configuration_error",
|
|
699
|
+
code: "environment_mismatch",
|
|
700
|
+
message: `Crossdeck.init: environment "${options.environment}" disagrees with key prefix (${keyEnv}). Reconcile the publishable key with the environment declaration.`
|
|
614
701
|
});
|
|
615
702
|
}
|
|
616
703
|
const storage = options.storage ?? detectDefaultStorage();
|
|
617
704
|
const persistIdentity = options.persistIdentity ?? true;
|
|
618
705
|
const autoTrack = resolveAutoTrack(options.autoTrack);
|
|
619
706
|
const opts = {
|
|
707
|
+
appId: options.appId,
|
|
620
708
|
publicKey: options.publicKey,
|
|
709
|
+
environment: options.environment,
|
|
621
710
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
622
711
|
persistIdentity,
|
|
623
712
|
storagePrefix: options.storagePrefix ?? "crossdeck:",
|
|
@@ -628,6 +717,8 @@ var CrossdeckClient = class {
|
|
|
628
717
|
autoTrack,
|
|
629
718
|
appVersion: options.appVersion ?? null
|
|
630
719
|
};
|
|
720
|
+
const debug = new ConsoleDebugLogger();
|
|
721
|
+
debug.enabled = options.debug === true;
|
|
631
722
|
const http = new HttpClient({
|
|
632
723
|
publicKey: opts.publicKey,
|
|
633
724
|
baseUrl: opts.baseUrl,
|
|
@@ -639,7 +730,19 @@ var CrossdeckClient = class {
|
|
|
639
730
|
const events = new EventQueue({
|
|
640
731
|
http,
|
|
641
732
|
batchSize: opts.eventFlushBatchSize,
|
|
642
|
-
intervalMs: opts.eventFlushIntervalMs
|
|
733
|
+
intervalMs: opts.eventFlushIntervalMs,
|
|
734
|
+
envelope: () => ({
|
|
735
|
+
appId: opts.appId,
|
|
736
|
+
environment: opts.environment,
|
|
737
|
+
sdk: { name: SDK_NAME, version: opts.sdkVersion }
|
|
738
|
+
}),
|
|
739
|
+
onFirstFlushSuccess: () => {
|
|
740
|
+
debug.emit(
|
|
741
|
+
"sdk.first_event_sent",
|
|
742
|
+
"First telemetry event received. View it in Live Events.",
|
|
743
|
+
{ appId: opts.appId, environment: opts.environment }
|
|
744
|
+
);
|
|
745
|
+
}
|
|
643
746
|
});
|
|
644
747
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
645
748
|
this.state = {
|
|
@@ -650,8 +753,14 @@ var CrossdeckClient = class {
|
|
|
650
753
|
autoTracker: null,
|
|
651
754
|
deviceInfo,
|
|
652
755
|
options: opts,
|
|
756
|
+
debug,
|
|
653
757
|
developerUserId: null
|
|
654
758
|
};
|
|
759
|
+
debug.emit("sdk.configured", `Crossdeck connected to ${opts.appId} in ${opts.environment} mode.`, {
|
|
760
|
+
appId: opts.appId,
|
|
761
|
+
environment: opts.environment,
|
|
762
|
+
sdkVersion: opts.sdkVersion
|
|
763
|
+
});
|
|
655
764
|
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
656
765
|
const tracker = new AutoTracker(
|
|
657
766
|
autoTrack,
|
|
@@ -664,6 +773,19 @@ var CrossdeckClient = class {
|
|
|
664
773
|
void this.heartbeat().catch(() => void 0);
|
|
665
774
|
}
|
|
666
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* @deprecated Use `init()` instead. NorthStar §4 standardised the
|
|
778
|
+
* lifecycle method name across SDKs as `init` (formerly `start` /
|
|
779
|
+
* `configure`). `start` will be removed in a future major version.
|
|
780
|
+
*/
|
|
781
|
+
start(options) {
|
|
782
|
+
if (typeof console !== "undefined") {
|
|
783
|
+
console.warn(
|
|
784
|
+
"[crossdeck] Crossdeck.start() is deprecated \u2014 use Crossdeck.init() instead. The signature is the same."
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
this.init(options);
|
|
788
|
+
}
|
|
667
789
|
/**
|
|
668
790
|
* Link the anonymous device to a developer-supplied user ID. Cache
|
|
669
791
|
* the resolved Crossdeck customer for follow-up calls.
|
|
@@ -719,7 +841,7 @@ var CrossdeckClient = class {
|
|
|
719
841
|
/**
|
|
720
842
|
* Queue a telemetry event. Returns immediately — the network round-
|
|
721
843
|
* trip happens in the background. To flush before the page unloads,
|
|
722
|
-
* call
|
|
844
|
+
* call flush().
|
|
723
845
|
*/
|
|
724
846
|
track(name, properties) {
|
|
725
847
|
const s = this.requireStarted();
|
|
@@ -730,6 +852,22 @@ var CrossdeckClient = class {
|
|
|
730
852
|
message: "track(name) requires a non-empty name."
|
|
731
853
|
});
|
|
732
854
|
}
|
|
855
|
+
if (s.debug.enabled && properties) {
|
|
856
|
+
const flagged = findSensitivePropertyKeys(properties);
|
|
857
|
+
if (flagged.length > 0) {
|
|
858
|
+
s.debug.emit(
|
|
859
|
+
"sdk.sensitive_property_warning",
|
|
860
|
+
`Event "${name}" has potentially sensitive property names: ${flagged.join(", ")}. Crossdeck is privacy-first \u2014 avoid sending PII unless intentional.`,
|
|
861
|
+
{ eventName: name, flagged }
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
if (s.debug.enabled && !s.developerUserId && !s.identity.crossdeckCustomerId) {
|
|
866
|
+
s.debug.emit(
|
|
867
|
+
"sdk.no_identity",
|
|
868
|
+
"Using anonymous user until identify(userId) is called."
|
|
869
|
+
);
|
|
870
|
+
}
|
|
733
871
|
const enriched = { ...s.deviceInfo };
|
|
734
872
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
735
873
|
if (sessionId) enriched.sessionId = sessionId;
|
|
@@ -743,28 +881,68 @@ var CrossdeckClient = class {
|
|
|
743
881
|
Object.assign(event, this.identityHintForEvent());
|
|
744
882
|
s.events.enqueue(event);
|
|
745
883
|
}
|
|
746
|
-
/**
|
|
747
|
-
|
|
884
|
+
/**
|
|
885
|
+
* Force-flush queued events. Useful to call from page-unload handlers.
|
|
886
|
+
*
|
|
887
|
+
* NorthStar §4: standard method name across all Crossdeck SDKs.
|
|
888
|
+
*/
|
|
889
|
+
async flush() {
|
|
748
890
|
const s = this.requireStarted();
|
|
749
891
|
await s.events.flush();
|
|
750
892
|
}
|
|
751
|
-
/**
|
|
752
|
-
async
|
|
893
|
+
/** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
|
|
894
|
+
async flushEvents() {
|
|
895
|
+
return this.flush();
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Forward purchase evidence to the backend for verification + entitlement
|
|
899
|
+
* projection. NorthStar §4 + §13 canonical name.
|
|
900
|
+
*
|
|
901
|
+
* Today the web SDK only supports Apple StoreKit 2 forwarding (web apps
|
|
902
|
+
* that sit alongside an iOS app). Stripe doesn't need this method —
|
|
903
|
+
* Stripe webhooks deliver evidence server-side without a client round-trip.
|
|
904
|
+
*/
|
|
905
|
+
async syncPurchases(input) {
|
|
753
906
|
const s = this.requireStarted();
|
|
754
907
|
if (!input.signedTransactionInfo) {
|
|
755
908
|
throw new CrossdeckError({
|
|
756
909
|
type: "invalid_request_error",
|
|
757
910
|
code: "missing_signed_transaction_info",
|
|
758
|
-
message: "
|
|
911
|
+
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
759
912
|
});
|
|
760
913
|
}
|
|
761
|
-
const result = await s.http.request("POST", "/purchases", {
|
|
762
|
-
body: { rail: "apple", ...input }
|
|
914
|
+
const result = await s.http.request("POST", "/purchases/sync", {
|
|
915
|
+
body: { rail: input.rail ?? "apple", ...input }
|
|
763
916
|
});
|
|
764
917
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
765
918
|
s.entitlements.setFromList(result.entitlements);
|
|
919
|
+
s.debug.emit(
|
|
920
|
+
"sdk.purchase_evidence_sent",
|
|
921
|
+
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
922
|
+
{ rail: input.rail ?? "apple" }
|
|
923
|
+
);
|
|
766
924
|
return result;
|
|
767
925
|
}
|
|
926
|
+
/** @deprecated Use `syncPurchases()` instead. NorthStar §4 standardised the name. */
|
|
927
|
+
async purchaseApple(input) {
|
|
928
|
+
return this.syncPurchases({ rail: "apple", ...input });
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Toggle verbose diagnostic logging — NorthStar §16. When enabled, the
|
|
932
|
+
* SDK emits a fixed vocabulary of debug signals to console.info that the
|
|
933
|
+
* dashboard's onboarding checklist can also surface as live events.
|
|
934
|
+
*/
|
|
935
|
+
setDebugMode(enabled) {
|
|
936
|
+
const s = this.requireStarted();
|
|
937
|
+
s.debug.enabled = enabled;
|
|
938
|
+
if (enabled) {
|
|
939
|
+
s.debug.emit(
|
|
940
|
+
"sdk.configured",
|
|
941
|
+
`Debug mode enabled for ${s.options.appId} in ${s.options.environment} mode.`,
|
|
942
|
+
{ appId: s.options.appId, environment: s.options.environment }
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
768
946
|
/**
|
|
769
947
|
* Send the boot heartbeat. Called automatically by start() unless
|
|
770
948
|
* autoHeartbeat:false. Safe to call manually as a "we're still here" ping.
|
|
@@ -841,8 +1019,8 @@ var CrossdeckClient = class {
|
|
|
841
1019
|
if (!this.state) {
|
|
842
1020
|
throw new CrossdeckError({
|
|
843
1021
|
type: "configuration_error",
|
|
844
|
-
code: "
|
|
845
|
-
message: "Call Crossdeck.
|
|
1022
|
+
code: "not_initialized",
|
|
1023
|
+
message: "Call Crossdeck.init({ appId, publicKey, environment }) before any other method."
|
|
846
1024
|
});
|
|
847
1025
|
}
|
|
848
1026
|
return this.state;
|
|
@@ -875,6 +1053,11 @@ var CrossdeckClient = class {
|
|
|
875
1053
|
}
|
|
876
1054
|
};
|
|
877
1055
|
var Crossdeck = new CrossdeckClient();
|
|
1056
|
+
function inferEnvFromKey(publicKey) {
|
|
1057
|
+
if (publicKey.startsWith("cd_pub_test_")) return "sandbox";
|
|
1058
|
+
if (publicKey.startsWith("cd_pub_live_")) return "production";
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
878
1061
|
function resolveAutoTrack(input) {
|
|
879
1062
|
if (input === false) {
|
|
880
1063
|
return { sessions: false, pageViews: false, deviceInfo: false };
|