@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/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.2.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: { events: batch }
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 start twice with the same options
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
- start(options) {
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.start requires a publishable key starting with cd_pub_."
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 flushEvents().
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
- /** Force-flush queued events. Useful to call from page-unload handlers. */
779
- async flushEvents() {
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
- /** Forward an Apple StoreKit 2 transaction for verification + projection. */
784
- async purchaseApple(input) {
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: "purchaseApple requires a signedTransactionInfo string from StoreKit 2."
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: "not_started",
877
- message: "Call Crossdeck.start({ publicKey }) before any other method."
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 };