@learncard/partner-connect 0.2.14 → 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.
@@ -2,6 +2,39 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var __defProp$1 = Object.defineProperty;
6
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$1(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
+ var __publicField$1 = (obj, key, value) => __defNormalProp$1(obj, typeof key !== "symbol" ? key + "" : key, value);
8
+ class PartnerConnectError extends Error {
9
+ constructor(code, message) {
10
+ super(message);
11
+ __publicField$1(this, "code");
12
+ this.name = "PartnerConnectError";
13
+ this.code = code;
14
+ Object.setPrototypeOf(this, PartnerConnectError.prototype);
15
+ }
16
+ /**
17
+ * Wrap any incoming `LearnCardError`-shaped value into a `PartnerConnectError`.
18
+ * Returns the value unchanged if it is already an instance.
19
+ *
20
+ * Used internally at every reject site so callers always receive a typed
21
+ * `PartnerConnectError`, regardless of whether the failure originated from
22
+ * the host (over postMessage), an SDK timeout, or `destroy()`.
23
+ */
24
+ static from(input) {
25
+ if (input instanceof PartnerConnectError) return input;
26
+ if (input && typeof input === "object" && "code" in input && typeof input.code === "string") {
27
+ const candidate = input;
28
+ const message = typeof candidate.message === "string" ? candidate.message : "Partner Connect request failed";
29
+ return new PartnerConnectError(candidate.code, message);
30
+ }
31
+ return new PartnerConnectError(
32
+ "UNKNOWN_ERROR",
33
+ input instanceof Error ? input.message : "An unknown error occurred"
34
+ );
35
+ }
36
+ }
37
+
5
38
  var __defProp = Object.defineProperty;
6
39
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
7
40
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -15,62 +48,160 @@ const _PartnerConnect = class _PartnerConnect {
15
48
  __publicField(this, "pendingRequests");
16
49
  __publicField(this, "messageListener", null);
17
50
  __publicField(this, "isInitialized", false);
18
- var _a;
19
- const hostOrigin = (options == null ? void 0 : options.hostOrigin) || _PartnerConnect.DEFAULT_HOST_ORIGIN;
20
- this.hostOrigins = Array.isArray(hostOrigin) ? hostOrigin : [hostOrigin];
51
+ var _a, _b;
52
+ const hostOrigin = (_a = options == null ? void 0 : options.hostOrigin) != null ? _a : _PartnerConnect.DEFAULT_HOST_ORIGIN;
53
+ const configured = Array.isArray(hostOrigin) ? hostOrigin : [hostOrigin];
54
+ const disableDefaults = (options == null ? void 0 : options.disableDefaultTenants) === true;
55
+ const merged = disableDefaults ? [...configured] : [...configured, ..._PartnerConnect.DEFAULT_TRUSTED_TENANTS];
56
+ this.hostOrigins = Array.from(new Set(merged));
21
57
  this.protocol = (options == null ? void 0 : options.protocol) || "LEARNCARD_V1";
22
58
  this.requestTimeout = (options == null ? void 0 : options.requestTimeout) || 3e4;
23
- this.allowNativeAppOrigins = (_a = options == null ? void 0 : options.allowNativeAppOrigins) != null ? _a : true;
59
+ this.allowNativeAppOrigins = (_b = options == null ? void 0 : options.allowNativeAppOrigins) != null ? _b : true;
24
60
  this.pendingRequests = /* @__PURE__ */ new Map();
25
61
  this.configureActiveOrigin();
26
62
  this.setupMessageListener();
27
63
  }
28
64
  /**
29
- * Configure the active host origin using the following hierarchy:
30
- * 1. Check for `lc_host_override` query parameter (for staging/testing)
31
- * 2. Fall back to first configured origin
32
- * 3. Fall back to DEFAULT_HOST_ORIGIN
33
- *
34
- * This origin will be used for all outgoing messages and incoming message validation.
65
+ * Read `window.location.ancestorOrigins[0]` without throwing if the
66
+ * property is unavailable (Firefox) or the list is empty (top-level
67
+ * context, e.g. running outside of an iframe).
35
68
  */
69
+ readAncestorOrigin() {
70
+ if (typeof window === "undefined") return null;
71
+ try {
72
+ const ancestors = window.location.ancestorOrigins;
73
+ if (ancestors && ancestors.length > 0) {
74
+ const parent = ancestors[0];
75
+ if (typeof parent === "string" && parent.length > 0) {
76
+ return parent;
77
+ }
78
+ }
79
+ } catch {
80
+ }
81
+ return null;
82
+ }
36
83
  configureActiveOrigin() {
37
84
  if (typeof window === "undefined") {
38
85
  this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
39
86
  return;
40
87
  }
41
88
  try {
89
+ const ancestorOrigin = this.readAncestorOrigin();
42
90
  const urlParams = new URLSearchParams(window.location.search);
43
91
  const hostOverride = urlParams.get("lc_host_override");
44
- if (hostOverride) {
45
- if (this.hostOrigins.length > 0 && !this.isOriginInWhitelist(hostOverride)) {
92
+ if (ancestorOrigin && this.isOriginInWhitelist(ancestorOrigin)) {
93
+ if (hostOverride && hostOverride !== ancestorOrigin) {
46
94
  console.warn(
47
- "[LearnCard SDK] lc_host_override value is not in the configured whitelist:",
48
- hostOverride,
49
- "Allowed:",
50
- this.hostOrigins
95
+ "[LearnCard SDK] lc_host_override does not match the real parent origin; preferring parent.",
96
+ { override: hostOverride, parent: ancestorOrigin }
51
97
  );
52
- this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
53
- } else {
98
+ }
99
+ this.activeHostOrigin = ancestorOrigin;
100
+ this.persistOverride(ancestorOrigin);
101
+ console.log("[LearnCard SDK] Using parent origin:", ancestorOrigin);
102
+ return;
103
+ }
104
+ if (hostOverride) {
105
+ if (this.isOriginInWhitelist(hostOverride)) {
54
106
  this.activeHostOrigin = hostOverride;
107
+ this.persistOverride(hostOverride);
55
108
  console.log("[LearnCard SDK] Using lc_host_override:", hostOverride);
109
+ return;
56
110
  }
57
- } else {
58
- this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
59
- console.log("[LearnCard SDK] Using configured origin:", this.activeHostOrigin);
111
+ console.warn(
112
+ "[LearnCard SDK] lc_host_override value is not in the configured whitelist:",
113
+ hostOverride,
114
+ "Allowed:",
115
+ this.hostOrigins
116
+ );
117
+ }
118
+ let storedOverride = null;
119
+ try {
120
+ storedOverride = sessionStorage.getItem(_PartnerConnect.SESSION_STORAGE_KEY);
121
+ } catch {
60
122
  }
123
+ if (storedOverride && this.isOriginInWhitelist(storedOverride)) {
124
+ this.activeHostOrigin = storedOverride;
125
+ console.log("[LearnCard SDK] Using stored lc_host_override:", storedOverride);
126
+ return;
127
+ }
128
+ this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
129
+ console.log("[LearnCard SDK] Using configured origin:", this.activeHostOrigin);
61
130
  } catch (error) {
62
131
  console.error("[LearnCard SDK] Error configuring active origin:", error);
63
132
  this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
64
133
  }
65
134
  }
135
+ persistOverride(origin) {
136
+ try {
137
+ sessionStorage.setItem(_PartnerConnect.SESSION_STORAGE_KEY, origin);
138
+ } catch {
139
+ }
140
+ }
66
141
  isOriginNativeApp(origin) {
67
142
  return origin.startsWith("capacitor://") || origin.startsWith("ionic://") || origin.startsWith("https://localhost") || origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1");
68
143
  }
69
144
  /**
70
- * Check if an origin is in the configured whitelist
145
+ * Check whether a candidate origin matches a configured whitelist entry.
146
+ *
147
+ * Supports exact matches and wildcard patterns. A wildcard entry has the
148
+ * form `<protocol>://*.<domain>` and matches any origin with the same
149
+ * protocol, same port, and a host ending in `.<domain>` with at least
150
+ * one non-empty DNS label in place of the `*`.
151
+ *
152
+ * Examples with pattern `https://*.learncard.app`:
153
+ * - `https://staging.learncard.app` → match
154
+ * - `https://pr-1.preview.learncard.app` → match
155
+ * - `https://learncard.app` → no match (no subdomain)
156
+ * - `http://staging.learncard.app` → no match (protocol mismatch)
157
+ * - `https://learncard.app.attacker.com` → no match (suffix mismatch)
158
+ *
159
+ * Exposed as a public static so it can be unit-tested directly without
160
+ * standing up a full SDK instance.
161
+ */
162
+ static matchesOriginPattern(candidate, pattern) {
163
+ if (candidate === pattern) return true;
164
+ if (!pattern.includes("*")) return false;
165
+ let patternUrl;
166
+ let candidateUrl;
167
+ try {
168
+ patternUrl = new URL(
169
+ pattern.replace(
170
+ _PartnerConnect.WILDCARD_REGEX,
171
+ _PartnerConnect.WILDCARD_PLACEHOLDER
172
+ )
173
+ );
174
+ candidateUrl = new URL(candidate);
175
+ } catch {
176
+ return false;
177
+ }
178
+ if (patternUrl.protocol !== candidateUrl.protocol) return false;
179
+ if (patternUrl.port !== candidateUrl.port) return false;
180
+ const patternHost = patternUrl.hostname;
181
+ const candidateHost = candidateUrl.hostname;
182
+ if (!patternHost.startsWith(_PartnerConnect.WILDCARD_LEADING_PREFIX)) return false;
183
+ const patternSuffix = patternHost.slice(_PartnerConnect.WILDCARD_LEADING_PREFIX.length);
184
+ if (patternSuffix.length === 0) return false;
185
+ if (patternSuffix.includes(_PartnerConnect.WILDCARD_PLACEHOLDER)) return false;
186
+ const required = "." + patternSuffix;
187
+ if (!candidateHost.endsWith(required)) return false;
188
+ const prefix = candidateHost.slice(0, candidateHost.length - required.length);
189
+ if (prefix.length === 0) return false;
190
+ if (prefix.startsWith(".") || prefix.endsWith(".")) return false;
191
+ if (prefix.split(".").some((label) => label.length === 0)) return false;
192
+ return true;
193
+ }
194
+ /**
195
+ * Check if an origin is in the effective whitelist (exact origins +
196
+ * wildcard patterns + optional native-app origins).
71
197
  */
72
198
  isOriginInWhitelist(origin) {
73
- return this.hostOrigins.includes(origin) || this.allowNativeAppOrigins && this.isOriginNativeApp(origin);
199
+ if (!origin) return false;
200
+ for (const entry of this.hostOrigins) {
201
+ if (_PartnerConnect.matchesOriginPattern(origin, entry)) return true;
202
+ }
203
+ if (this.allowNativeAppOrigins && this.isOriginNativeApp(origin)) return true;
204
+ return false;
74
205
  }
75
206
  /**
76
207
  * Check if an event origin is valid against the active host origin
@@ -109,7 +240,12 @@ const _PartnerConnect = class _PartnerConnect {
109
240
  pending.resolve(data.data);
110
241
  } else if (data.type === "ERROR") {
111
242
  pending.reject(
112
- data.error || { code: "UNKNOWN_ERROR", message: "An unknown error occurred" }
243
+ PartnerConnectError.from(
244
+ data.error || {
245
+ code: "UNKNOWN_ERROR",
246
+ message: "An unknown error occurred"
247
+ }
248
+ )
113
249
  );
114
250
  }
115
251
  };
@@ -127,20 +263,21 @@ const _PartnerConnect = class _PartnerConnect {
127
263
  */
128
264
  sendMessage(action, payload) {
129
265
  if (!this.isInitialized) {
130
- return Promise.reject({
131
- code: "SDK_NOT_INITIALIZED",
132
- message: "SDK is not initialized"
133
- });
266
+ return Promise.reject(
267
+ new PartnerConnectError("SDK_NOT_INITIALIZED", "SDK is not initialized")
268
+ );
134
269
  }
135
270
  return new Promise((resolve, reject) => {
136
271
  const requestId = this.generateRequestId(action);
137
272
  const timeoutId = setTimeout(() => {
138
273
  if (this.pendingRequests.has(requestId)) {
139
274
  this.pendingRequests.delete(requestId);
140
- reject({
141
- code: "LC_TIMEOUT",
142
- message: `Request ${action} timed out after ${this.requestTimeout}ms`
143
- });
275
+ reject(
276
+ new PartnerConnectError(
277
+ "LC_TIMEOUT",
278
+ `Request ${action} timed out after ${this.requestTimeout}ms`
279
+ )
280
+ );
144
281
  }
145
282
  }, this.requestTimeout);
146
283
  this.pendingRequests.set(requestId, {
@@ -358,22 +495,27 @@ const _PartnerConnect = class _PartnerConnect {
358
495
  /**
359
496
  * Request user consent for permissions
360
497
  *
361
- * @param contractUri - URI of the consent contract
498
+ * @param contractUri - URI of the consent contract (optional for App Store apps with configured contracts)
499
+ * @param options - Additional options including redirect behavior
362
500
  * @returns Promise resolving to consent response
363
501
  *
364
502
  * @example
365
503
  * ```typescript
366
- * // Without redirect (default) - returns VP in response if app owns the contract
504
+ * // With explicit contract URI (for external/non-app store integrations)
367
505
  * const response = await learnCard.requestConsent('lc:network:network.learncard.com/trpc:contract:abc123');
368
506
  * if (response.granted) {
369
507
  * console.log('User granted consent');
370
- * if (response.vp) {
371
- * console.log('VP:', response.vp);
372
- * }
508
+ * }
509
+ *
510
+ * // Without contract URI (uses app's configured contract from integration)
511
+ * // This works for App Store apps that have configured a contract in their integration
512
+ * const response = await learnCard.requestConsent();
513
+ * if (response.granted) {
514
+ * console.log('User granted consent using listing contract');
373
515
  * }
374
516
  *
375
517
  * // With redirect - redirects to contract's redirectUrl with VP in URL params
376
- * const response = await learnCard.requestConsent('lc:network:network.learncard.com/trpc:contract:abc123', { redirect: true });
518
+ * const response = await learnCard.requestConsent(undefined, { redirect: true });
377
519
  * ```
378
520
  */
379
521
  requestConsent(contractUri, options = {}) {
@@ -408,6 +550,45 @@ const _PartnerConnect = class _PartnerConnect {
408
550
  draftRecipients: draftRecipients || []
409
551
  });
410
552
  }
553
+ /**
554
+ * Request comprehensive learner context for AI tutoring systems.
555
+ *
556
+ * This method retrieves the user's credentials and personal data,
557
+ * then formats them into an LLM-ready prompt that can be injected directly into
558
+ * an AI system prompt.
559
+ *
560
+ * @param options - Configuration options for what data to include and how to format it
561
+ * @returns Promise resolving to learner context with prompt and optional raw data
562
+ *
563
+ * @example
564
+ * ```typescript
565
+ * // Get LLM-ready prompt with credentials and personal data
566
+ * const context = await learnCard.requestLearnerContext({
567
+ * includeCredentials: true,
568
+ * includePersonalData: true,
569
+ * format: 'prompt',
570
+ * instructions: 'Focus on technical skills and certifications',
571
+ * detailLevel: 'expanded'
572
+ * });
573
+ *
574
+ * // Use in AI system prompt
575
+ * const systemPrompt = `You are a helpful tutor. ${context.prompt}`;
576
+ *
577
+ * // Access structured data if needed
578
+ * console.log('User DID:', context.did);
579
+ * console.log('Credentials count:', context.raw?.credentials.length);
580
+ * ```
581
+ */
582
+ requestLearnerContext(options) {
583
+ var _a, _b, _c, _d;
584
+ return this.sendMessage("REQUEST_LEARNER_CONTEXT", {
585
+ includeCredentials: (_a = options == null ? void 0 : options.includeCredentials) != null ? _a : true,
586
+ includePersonalData: (_b = options == null ? void 0 : options.includePersonalData) != null ? _b : false,
587
+ format: (_c = options == null ? void 0 : options.format) != null ? _c : "prompt",
588
+ instructions: options == null ? void 0 : options.instructions,
589
+ detailLevel: (_d = options == null ? void 0 : options.detailLevel) != null ? _d : "compact"
590
+ });
591
+ }
411
592
  /**
412
593
  * Send a generic event to be processed by the brain service on behalf of this app.
413
594
  * This is used for backend-like operations such as issuing credentials.
@@ -432,6 +613,62 @@ const _PartnerConnect = class _PartnerConnect {
432
613
  sendAppEvent(event) {
433
614
  return this.sendMessage("APP_EVENT", event);
434
615
  }
616
+ /**
617
+ * Create and send an AI Session credential to the user.
618
+ *
619
+ * This method manages the AI Topic → AI Session hierarchy:
620
+ * - Ensures an AI Topic exists for this app (creates one if needed)
621
+ * - Creates a new AI Session as a child of the topic
622
+ * - The topic appears in the user's AI Sessions page with the app's name
623
+ * - All sessions from this app are organized under that topic
624
+ *
625
+ * @param input - Session details including title and optional metadata
626
+ * @returns Promise resolving to topic and session URIs
627
+ */
628
+ sendAiSessionCredential(input) {
629
+ return this.sendAppEvent({
630
+ type: "send-ai-session-credential",
631
+ ...input
632
+ });
633
+ }
634
+ /**
635
+ * Send a notification to the current user from this app.
636
+ * The notification appears in the user's LearnCard notification inbox.
637
+ */
638
+ sendNotification(input) {
639
+ return this.sendAppEvent({
640
+ type: "send-notification",
641
+ ...input
642
+ });
643
+ }
644
+ /**
645
+ * Increment or decrement an app-scoped counter for the current user.
646
+ */
647
+ incrementCounter(key, amount) {
648
+ return this.sendAppEvent({
649
+ type: "increment-counter",
650
+ key,
651
+ amount
652
+ });
653
+ }
654
+ /**
655
+ * Read the current value of an app-scoped counter for the current user.
656
+ */
657
+ getCounter(key) {
658
+ return this.sendAppEvent({
659
+ type: "get-counter",
660
+ key
661
+ });
662
+ }
663
+ /**
664
+ * Read multiple app-scoped counters at once for the current user.
665
+ */
666
+ getCounters(keys) {
667
+ return this.sendAppEvent({
668
+ type: "get-counters",
669
+ ...keys ? { keys } : {}
670
+ });
671
+ }
435
672
  /**
436
673
  * Clean up the SDK and remove event listeners
437
674
  */
@@ -442,10 +679,12 @@ const _PartnerConnect = class _PartnerConnect {
442
679
  }
443
680
  for (const [requestId, pending] of this.pendingRequests.entries()) {
444
681
  clearTimeout(pending.timeoutId);
445
- pending.reject({
446
- code: "SDK_DESTROYED",
447
- message: "SDK was destroyed before request completed"
448
- });
682
+ pending.reject(
683
+ new PartnerConnectError(
684
+ "SDK_DESTROYED",
685
+ "SDK was destroyed before request completed"
686
+ )
687
+ );
449
688
  }
450
689
  this.pendingRequests.clear();
451
690
  this.isInitialized = false;
@@ -453,12 +692,57 @@ const _PartnerConnect = class _PartnerConnect {
453
692
  };
454
693
  /** Default host origin (security anchor) */
455
694
  __publicField(_PartnerConnect, "DEFAULT_HOST_ORIGIN", "https://learncard.app");
695
+ /**
696
+ * Built-in list of LearnCard-managed tenant origins.
697
+ *
698
+ * These are merged with the partner app's configured `hostOrigin` whitelist
699
+ * unless `disableDefaultTenants: true` is passed. This lets a partner app
700
+ * run inside any current or future LearnCard tenant (staging, preview,
701
+ * VetPass, etc.) without needing a re-deploy each time a new tenant is
702
+ * onboarded.
703
+ *
704
+ * Patterns follow the same rules as user-supplied `hostOrigin` entries:
705
+ * `*` is a wildcard for one or more DNS labels in the host portion.
706
+ */
707
+ __publicField(_PartnerConnect, "DEFAULT_TRUSTED_TENANTS", [
708
+ "https://learncard.app",
709
+ "https://*.learncard.app",
710
+ "https://*.learncard.ai",
711
+ "https://vetpass.app",
712
+ "https://*.vetpass.app"
713
+ ]);
714
+ /**
715
+ * Configure the active host origin using the following hierarchy:
716
+ * 1. `window.location.ancestorOrigins[0]` (when supported) — the browser's
717
+ * view of who our parent frame is. Cannot be forged by a malicious
718
+ * `lc_host_override` query param and therefore takes precedence.
719
+ * 2. `?lc_host_override=<origin>` query param (for staging / cross-tenant).
720
+ * 3. `sessionStorage` value saved from a previously-validated override.
721
+ * 4. First configured origin.
722
+ * 5. `DEFAULT_HOST_ORIGIN`.
723
+ *
724
+ * When a valid override is found in the query parameter, it is persisted
725
+ * to sessionStorage so subsequent in-iframe navigations in the same tab
726
+ * continue to use the same active origin.
727
+ */
728
+ __publicField(_PartnerConnect, "SESSION_STORAGE_KEY", "lc_host_override");
729
+ /**
730
+ * Internal placeholder substituted in for `*` so that `new URL(...)` can
731
+ * parse a wildcard pattern. Chosen to be a syntactically-valid DNS label
732
+ * that cannot collide with a real hostname.
733
+ */
734
+ __publicField(_PartnerConnect, "WILDCARD_PLACEHOLDER", "__lc_wildcard__");
735
+ /** `*` (any number of occurrences) for replacement in the pattern. */
736
+ __publicField(_PartnerConnect, "WILDCARD_REGEX", /\*/g);
737
+ /** The required leading-label form a wildcard pattern must take. */
738
+ __publicField(_PartnerConnect, "WILDCARD_LEADING_PREFIX", `${_PartnerConnect.WILDCARD_PLACEHOLDER}.`);
456
739
  let PartnerConnect = _PartnerConnect;
457
740
  function createPartnerConnect(options) {
458
741
  return new PartnerConnect(options);
459
742
  }
460
743
 
461
744
  exports.PartnerConnect = PartnerConnect;
745
+ exports.PartnerConnectError = PartnerConnectError;
462
746
  exports.createPartnerConnect = createPartnerConnect;
463
747
  exports["default"] = createPartnerConnect;
464
748
  //# sourceMappingURL=partner-connect.js.map