@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.js
CHANGED
|
@@ -78,7 +78,7 @@ function typeMapForStatus(status) {
|
|
|
78
78
|
|
|
79
79
|
// src/http.ts
|
|
80
80
|
var SDK_NAME = "@cross-deck/web";
|
|
81
|
-
var SDK_VERSION = "0.
|
|
81
|
+
var SDK_VERSION = "0.3.0";
|
|
82
82
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
83
83
|
var HttpClient = class {
|
|
84
84
|
constructor(config) {
|
|
@@ -277,6 +277,7 @@ var EventQueue = class {
|
|
|
277
277
|
this.lastFlushAt = 0;
|
|
278
278
|
this.lastError = null;
|
|
279
279
|
this.cancelTimer = null;
|
|
280
|
+
this.firstFlushFired = false;
|
|
280
281
|
}
|
|
281
282
|
enqueue(event) {
|
|
282
283
|
this.buffer.push(event);
|
|
@@ -303,12 +304,25 @@ var EventQueue = class {
|
|
|
303
304
|
const batch = this.buffer.splice(0);
|
|
304
305
|
this.inFlight += batch.length;
|
|
305
306
|
try {
|
|
307
|
+
const env = this.cfg.envelope();
|
|
306
308
|
const result = await this.cfg.http.request("POST", "/events", {
|
|
307
|
-
body: {
|
|
309
|
+
body: {
|
|
310
|
+
// NorthStar §13.1 batch envelope. The backend validates these
|
|
311
|
+
// against the API-key-resolved app and rejects mismatches loudly
|
|
312
|
+
// (env_mismatch).
|
|
313
|
+
appId: env.appId,
|
|
314
|
+
environment: env.environment,
|
|
315
|
+
sdk: env.sdk,
|
|
316
|
+
events: batch
|
|
317
|
+
}
|
|
308
318
|
});
|
|
309
319
|
this.lastFlushAt = Date.now();
|
|
310
320
|
this.lastError = null;
|
|
311
321
|
this.inFlight -= batch.length;
|
|
322
|
+
if (!this.firstFlushFired) {
|
|
323
|
+
this.firstFlushFired = true;
|
|
324
|
+
this.cfg.onFirstFlushSuccess?.();
|
|
325
|
+
}
|
|
312
326
|
return result;
|
|
313
327
|
} catch (err) {
|
|
314
328
|
this.buffer.unshift(...batch);
|
|
@@ -627,29 +641,104 @@ function mintSessionId() {
|
|
|
627
641
|
return `sess_${ts}${randomChars(10)}`;
|
|
628
642
|
}
|
|
629
643
|
|
|
644
|
+
// src/debug.ts
|
|
645
|
+
var SENSITIVE_KEY_PATTERNS = [
|
|
646
|
+
/^email$/i,
|
|
647
|
+
/^password$/i,
|
|
648
|
+
/^token$/i,
|
|
649
|
+
/^secret$/i,
|
|
650
|
+
/^card$/i,
|
|
651
|
+
/^phone$/i,
|
|
652
|
+
/password/i,
|
|
653
|
+
/credit_?card/i
|
|
654
|
+
];
|
|
655
|
+
function findSensitivePropertyKeys(properties) {
|
|
656
|
+
if (!properties) return [];
|
|
657
|
+
const hits = [];
|
|
658
|
+
for (const k of Object.keys(properties)) {
|
|
659
|
+
if (SENSITIVE_KEY_PATTERNS.some((re) => re.test(k))) hits.push(k);
|
|
660
|
+
}
|
|
661
|
+
return hits;
|
|
662
|
+
}
|
|
663
|
+
var ConsoleDebugLogger = class {
|
|
664
|
+
constructor() {
|
|
665
|
+
this.enabled = false;
|
|
666
|
+
this.seen = /* @__PURE__ */ new Set();
|
|
667
|
+
}
|
|
668
|
+
emit(signal, message, context) {
|
|
669
|
+
if (!this.enabled) return;
|
|
670
|
+
if (ONCE_SIGNALS.has(signal)) {
|
|
671
|
+
if (this.seen.has(signal)) return;
|
|
672
|
+
this.seen.add(signal);
|
|
673
|
+
}
|
|
674
|
+
const ctx = context ? ` ${safeJson(context)}` : "";
|
|
675
|
+
console.info(`[crossdeck:${signal}] ${message}${ctx}`);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
var ONCE_SIGNALS = /* @__PURE__ */ new Set([
|
|
679
|
+
"sdk.configured",
|
|
680
|
+
"sdk.first_event_sent",
|
|
681
|
+
"sdk.environment_mismatch"
|
|
682
|
+
]);
|
|
683
|
+
function safeJson(obj) {
|
|
684
|
+
try {
|
|
685
|
+
return JSON.stringify(obj);
|
|
686
|
+
} catch {
|
|
687
|
+
return "[unserialisable context]";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
630
691
|
// src/crossdeck.ts
|
|
631
692
|
var CrossdeckClient = class {
|
|
632
693
|
constructor() {
|
|
633
694
|
this.state = null;
|
|
634
695
|
}
|
|
635
696
|
/**
|
|
636
|
-
* Boot the SDK. Idempotent — calling
|
|
697
|
+
* Boot the SDK. Idempotent — calling init twice with the same options
|
|
637
698
|
* is a no-op; calling with different options replaces the previous
|
|
638
699
|
* configuration.
|
|
700
|
+
*
|
|
701
|
+
* NorthStar §11.1: signature is `Crossdeck.init({ appId, publicKey,
|
|
702
|
+
* environment })`. The trio is validated up-front so a typo'd key or a
|
|
703
|
+
* mismatched env fails fast at boot rather than at first event-flush.
|
|
639
704
|
*/
|
|
640
|
-
|
|
705
|
+
init(options) {
|
|
641
706
|
if (!options.publicKey || !options.publicKey.startsWith("cd_pub_")) {
|
|
642
707
|
throw new CrossdeckError({
|
|
643
708
|
type: "configuration_error",
|
|
644
709
|
code: "invalid_public_key",
|
|
645
|
-
message: "Crossdeck.
|
|
710
|
+
message: "Crossdeck.init requires a publishable key starting with cd_pub_."
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
if (!options.appId) {
|
|
714
|
+
throw new CrossdeckError({
|
|
715
|
+
type: "configuration_error",
|
|
716
|
+
code: "missing_app_id",
|
|
717
|
+
message: "Crossdeck.init requires an appId. Find yours in the Crossdeck dashboard."
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
if (options.environment !== "production" && options.environment !== "sandbox") {
|
|
721
|
+
throw new CrossdeckError({
|
|
722
|
+
type: "configuration_error",
|
|
723
|
+
code: "invalid_environment",
|
|
724
|
+
message: 'Crossdeck.init requires environment: "production" | "sandbox".'
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
const keyEnv = inferEnvFromKey(options.publicKey);
|
|
728
|
+
if (keyEnv && keyEnv !== options.environment) {
|
|
729
|
+
throw new CrossdeckError({
|
|
730
|
+
type: "configuration_error",
|
|
731
|
+
code: "environment_mismatch",
|
|
732
|
+
message: `Crossdeck.init: environment "${options.environment}" disagrees with key prefix (${keyEnv}). Reconcile the publishable key with the environment declaration.`
|
|
646
733
|
});
|
|
647
734
|
}
|
|
648
735
|
const storage = options.storage ?? detectDefaultStorage();
|
|
649
736
|
const persistIdentity = options.persistIdentity ?? true;
|
|
650
737
|
const autoTrack = resolveAutoTrack(options.autoTrack);
|
|
651
738
|
const opts = {
|
|
739
|
+
appId: options.appId,
|
|
652
740
|
publicKey: options.publicKey,
|
|
741
|
+
environment: options.environment,
|
|
653
742
|
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
654
743
|
persistIdentity,
|
|
655
744
|
storagePrefix: options.storagePrefix ?? "crossdeck:",
|
|
@@ -660,6 +749,8 @@ var CrossdeckClient = class {
|
|
|
660
749
|
autoTrack,
|
|
661
750
|
appVersion: options.appVersion ?? null
|
|
662
751
|
};
|
|
752
|
+
const debug = new ConsoleDebugLogger();
|
|
753
|
+
debug.enabled = options.debug === true;
|
|
663
754
|
const http = new HttpClient({
|
|
664
755
|
publicKey: opts.publicKey,
|
|
665
756
|
baseUrl: opts.baseUrl,
|
|
@@ -671,7 +762,19 @@ var CrossdeckClient = class {
|
|
|
671
762
|
const events = new EventQueue({
|
|
672
763
|
http,
|
|
673
764
|
batchSize: opts.eventFlushBatchSize,
|
|
674
|
-
intervalMs: opts.eventFlushIntervalMs
|
|
765
|
+
intervalMs: opts.eventFlushIntervalMs,
|
|
766
|
+
envelope: () => ({
|
|
767
|
+
appId: opts.appId,
|
|
768
|
+
environment: opts.environment,
|
|
769
|
+
sdk: { name: SDK_NAME, version: opts.sdkVersion }
|
|
770
|
+
}),
|
|
771
|
+
onFirstFlushSuccess: () => {
|
|
772
|
+
debug.emit(
|
|
773
|
+
"sdk.first_event_sent",
|
|
774
|
+
"First telemetry event received. View it in Live Events.",
|
|
775
|
+
{ appId: opts.appId, environment: opts.environment }
|
|
776
|
+
);
|
|
777
|
+
}
|
|
675
778
|
});
|
|
676
779
|
const deviceInfo = autoTrack.deviceInfo ? collectDeviceInfo({ appVersion: opts.appVersion ?? void 0 }) : opts.appVersion ? { appVersion: opts.appVersion } : {};
|
|
677
780
|
this.state = {
|
|
@@ -682,8 +785,14 @@ var CrossdeckClient = class {
|
|
|
682
785
|
autoTracker: null,
|
|
683
786
|
deviceInfo,
|
|
684
787
|
options: opts,
|
|
788
|
+
debug,
|
|
685
789
|
developerUserId: null
|
|
686
790
|
};
|
|
791
|
+
debug.emit("sdk.configured", `Crossdeck connected to ${opts.appId} in ${opts.environment} mode.`, {
|
|
792
|
+
appId: opts.appId,
|
|
793
|
+
environment: opts.environment,
|
|
794
|
+
sdkVersion: opts.sdkVersion
|
|
795
|
+
});
|
|
687
796
|
if (autoTrack.sessions || autoTrack.pageViews) {
|
|
688
797
|
const tracker = new AutoTracker(
|
|
689
798
|
autoTrack,
|
|
@@ -696,6 +805,19 @@ var CrossdeckClient = class {
|
|
|
696
805
|
void this.heartbeat().catch(() => void 0);
|
|
697
806
|
}
|
|
698
807
|
}
|
|
808
|
+
/**
|
|
809
|
+
* @deprecated Use `init()` instead. NorthStar §4 standardised the
|
|
810
|
+
* lifecycle method name across SDKs as `init` (formerly `start` /
|
|
811
|
+
* `configure`). `start` will be removed in a future major version.
|
|
812
|
+
*/
|
|
813
|
+
start(options) {
|
|
814
|
+
if (typeof console !== "undefined") {
|
|
815
|
+
console.warn(
|
|
816
|
+
"[crossdeck] Crossdeck.start() is deprecated \u2014 use Crossdeck.init() instead. The signature is the same."
|
|
817
|
+
);
|
|
818
|
+
}
|
|
819
|
+
this.init(options);
|
|
820
|
+
}
|
|
699
821
|
/**
|
|
700
822
|
* Link the anonymous device to a developer-supplied user ID. Cache
|
|
701
823
|
* the resolved Crossdeck customer for follow-up calls.
|
|
@@ -751,7 +873,7 @@ var CrossdeckClient = class {
|
|
|
751
873
|
/**
|
|
752
874
|
* Queue a telemetry event. Returns immediately — the network round-
|
|
753
875
|
* trip happens in the background. To flush before the page unloads,
|
|
754
|
-
* call
|
|
876
|
+
* call flush().
|
|
755
877
|
*/
|
|
756
878
|
track(name, properties) {
|
|
757
879
|
const s = this.requireStarted();
|
|
@@ -762,6 +884,22 @@ var CrossdeckClient = class {
|
|
|
762
884
|
message: "track(name) requires a non-empty name."
|
|
763
885
|
});
|
|
764
886
|
}
|
|
887
|
+
if (s.debug.enabled && properties) {
|
|
888
|
+
const flagged = findSensitivePropertyKeys(properties);
|
|
889
|
+
if (flagged.length > 0) {
|
|
890
|
+
s.debug.emit(
|
|
891
|
+
"sdk.sensitive_property_warning",
|
|
892
|
+
`Event "${name}" has potentially sensitive property names: ${flagged.join(", ")}. Crossdeck is privacy-first \u2014 avoid sending PII unless intentional.`,
|
|
893
|
+
{ eventName: name, flagged }
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (s.debug.enabled && !s.developerUserId && !s.identity.crossdeckCustomerId) {
|
|
898
|
+
s.debug.emit(
|
|
899
|
+
"sdk.no_identity",
|
|
900
|
+
"Using anonymous user until identify(userId) is called."
|
|
901
|
+
);
|
|
902
|
+
}
|
|
765
903
|
const enriched = { ...s.deviceInfo };
|
|
766
904
|
const sessionId = s.autoTracker?.currentSessionId;
|
|
767
905
|
if (sessionId) enriched.sessionId = sessionId;
|
|
@@ -775,28 +913,68 @@ var CrossdeckClient = class {
|
|
|
775
913
|
Object.assign(event, this.identityHintForEvent());
|
|
776
914
|
s.events.enqueue(event);
|
|
777
915
|
}
|
|
778
|
-
/**
|
|
779
|
-
|
|
916
|
+
/**
|
|
917
|
+
* Force-flush queued events. Useful to call from page-unload handlers.
|
|
918
|
+
*
|
|
919
|
+
* NorthStar §4: standard method name across all Crossdeck SDKs.
|
|
920
|
+
*/
|
|
921
|
+
async flush() {
|
|
780
922
|
const s = this.requireStarted();
|
|
781
923
|
await s.events.flush();
|
|
782
924
|
}
|
|
783
|
-
/**
|
|
784
|
-
async
|
|
925
|
+
/** @deprecated Use `flush()` instead. NorthStar §4 standardised the name. */
|
|
926
|
+
async flushEvents() {
|
|
927
|
+
return this.flush();
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Forward purchase evidence to the backend for verification + entitlement
|
|
931
|
+
* projection. NorthStar §4 + §13 canonical name.
|
|
932
|
+
*
|
|
933
|
+
* Today the web SDK only supports Apple StoreKit 2 forwarding (web apps
|
|
934
|
+
* that sit alongside an iOS app). Stripe doesn't need this method —
|
|
935
|
+
* Stripe webhooks deliver evidence server-side without a client round-trip.
|
|
936
|
+
*/
|
|
937
|
+
async syncPurchases(input) {
|
|
785
938
|
const s = this.requireStarted();
|
|
786
939
|
if (!input.signedTransactionInfo) {
|
|
787
940
|
throw new CrossdeckError({
|
|
788
941
|
type: "invalid_request_error",
|
|
789
942
|
code: "missing_signed_transaction_info",
|
|
790
|
-
message: "
|
|
943
|
+
message: "syncPurchases requires a signedTransactionInfo string from StoreKit 2."
|
|
791
944
|
});
|
|
792
945
|
}
|
|
793
|
-
const result = await s.http.request("POST", "/purchases", {
|
|
794
|
-
body: { rail: "apple", ...input }
|
|
946
|
+
const result = await s.http.request("POST", "/purchases/sync", {
|
|
947
|
+
body: { rail: input.rail ?? "apple", ...input }
|
|
795
948
|
});
|
|
796
949
|
s.identity.setCrossdeckCustomerId(result.crossdeckCustomerId);
|
|
797
950
|
s.entitlements.setFromList(result.entitlements);
|
|
951
|
+
s.debug.emit(
|
|
952
|
+
"sdk.purchase_evidence_sent",
|
|
953
|
+
"StoreKit transaction forwarded. Waiting for backend verification.",
|
|
954
|
+
{ rail: input.rail ?? "apple" }
|
|
955
|
+
);
|
|
798
956
|
return result;
|
|
799
957
|
}
|
|
958
|
+
/** @deprecated Use `syncPurchases()` instead. NorthStar §4 standardised the name. */
|
|
959
|
+
async purchaseApple(input) {
|
|
960
|
+
return this.syncPurchases({ rail: "apple", ...input });
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Toggle verbose diagnostic logging — NorthStar §16. When enabled, the
|
|
964
|
+
* SDK emits a fixed vocabulary of debug signals to console.info that the
|
|
965
|
+
* dashboard's onboarding checklist can also surface as live events.
|
|
966
|
+
*/
|
|
967
|
+
setDebugMode(enabled) {
|
|
968
|
+
const s = this.requireStarted();
|
|
969
|
+
s.debug.enabled = enabled;
|
|
970
|
+
if (enabled) {
|
|
971
|
+
s.debug.emit(
|
|
972
|
+
"sdk.configured",
|
|
973
|
+
`Debug mode enabled for ${s.options.appId} in ${s.options.environment} mode.`,
|
|
974
|
+
{ appId: s.options.appId, environment: s.options.environment }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
800
978
|
/**
|
|
801
979
|
* Send the boot heartbeat. Called automatically by start() unless
|
|
802
980
|
* autoHeartbeat:false. Safe to call manually as a "we're still here" ping.
|
|
@@ -873,8 +1051,8 @@ var CrossdeckClient = class {
|
|
|
873
1051
|
if (!this.state) {
|
|
874
1052
|
throw new CrossdeckError({
|
|
875
1053
|
type: "configuration_error",
|
|
876
|
-
code: "
|
|
877
|
-
message: "Call Crossdeck.
|
|
1054
|
+
code: "not_initialized",
|
|
1055
|
+
message: "Call Crossdeck.init({ appId, publicKey, environment }) before any other method."
|
|
878
1056
|
});
|
|
879
1057
|
}
|
|
880
1058
|
return this.state;
|
|
@@ -907,6 +1085,11 @@ var CrossdeckClient = class {
|
|
|
907
1085
|
}
|
|
908
1086
|
};
|
|
909
1087
|
var Crossdeck = new CrossdeckClient();
|
|
1088
|
+
function inferEnvFromKey(publicKey) {
|
|
1089
|
+
if (publicKey.startsWith("cd_pub_test_")) return "sandbox";
|
|
1090
|
+
if (publicKey.startsWith("cd_pub_live_")) return "production";
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
910
1093
|
function resolveAutoTrack(input) {
|
|
911
1094
|
if (input === false) {
|
|
912
1095
|
return { sessions: false, pageViews: false, deviceInfo: false };
|