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