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