@learncard/partner-connect 0.2.16 → 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.d.ts +165 -39
- package/dist/partner-connect.esm.js +217 -54
- package/dist/partner-connect.esm.js.map +1 -1
- package/dist/partner-connect.js +217 -53
- package/dist/partner-connect.js.map +1 -1
- package/package.json +6 -4
package/dist/index.d.ts
CHANGED
|
@@ -10,44 +10,73 @@ export { AppEvent, AppEventResponse, CheckCredentialEvent, SendCredentialEvent }
|
|
|
10
10
|
*/
|
|
11
11
|
interface PartnerConnectOptions {
|
|
12
12
|
/**
|
|
13
|
-
* The origin(s) of the LearnCard host
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* **
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
13
|
+
* The origin(s) of the LearnCard host.
|
|
14
|
+
*
|
|
15
|
+
* Each entry may be either an **exact origin** (`https://learncard.app`) or a
|
|
16
|
+
* **wildcard pattern** where `*` stands in for a single DNS label portion in
|
|
17
|
+
* the host portion of the origin (e.g. `https://*.learncard.app`,
|
|
18
|
+
* `https://*.vetpass.app`). Wildcards are **only** allowed in the host and
|
|
19
|
+
* only as label(s); the protocol and port must always match exactly.
|
|
20
|
+
*
|
|
21
|
+
* Wildcard patterns match any non-empty chain of labels. So
|
|
22
|
+
* `https://*.learncard.app` matches both `https://staging.learncard.app` and
|
|
23
|
+
* `https://pr-123.preview.learncard.app`, but **not** `https://learncard.app`
|
|
24
|
+
* itself (include the bare origin explicitly if you need it) and **not**
|
|
25
|
+
* `https://evil.learncard.app.attacker.com` (the suffix must match).
|
|
26
|
+
*
|
|
27
|
+
* **Origin Configuration Hierarchy at runtime:**
|
|
28
|
+
* 1. `window.location.ancestorOrigins[0]` (when available) — the real parent
|
|
29
|
+
* origin as reported by the browser, validated against the effective
|
|
30
|
+
* whitelist. This source cannot be spoofed by a malicious query param.
|
|
31
|
+
* 2. `?lc_host_override=<origin>` query parameter — validated against the
|
|
32
|
+
* whitelist. Used by the LearnCard host to tell the SDK which origin it
|
|
33
|
+
* is loading from.
|
|
34
|
+
* 3. `sessionStorage['lc_host_override']` — a previously-validated override
|
|
35
|
+
* persisted across in-iframe navigation.
|
|
36
|
+
* 4. First value in the configured `hostOrigin` array / single string.
|
|
37
|
+
* 5. `PartnerConnect.DEFAULT_HOST_ORIGIN` (`https://learncard.app`).
|
|
38
|
+
*
|
|
39
|
+
* The partner app's configured whitelist is combined with a small built-in
|
|
40
|
+
* list of LearnCard tenant domains (see `disableDefaultTenants` to opt out).
|
|
41
|
+
* This lets a partner app work out-of-the-box inside any current or future
|
|
42
|
+
* `*.learncard.app`, `*.learncard.ai`, or `*.vetpass.app` tenant without a
|
|
43
|
+
* re-deploy.
|
|
30
44
|
*
|
|
31
45
|
* **Examples:**
|
|
32
46
|
*
|
|
33
|
-
* Single origin (production):
|
|
47
|
+
* Single origin (production only):
|
|
34
48
|
* ```typescript
|
|
35
49
|
* hostOrigin: 'https://learncard.app'
|
|
36
|
-
* // Uses: https://learncard.app
|
|
37
|
-
* // Override: ?lc_host_override=https://staging.learncard.app (not validated)
|
|
38
50
|
* ```
|
|
39
51
|
*
|
|
40
|
-
*
|
|
52
|
+
* Wildcard whitelist (covers staging + preview):
|
|
53
|
+
* ```typescript
|
|
54
|
+
* hostOrigin: ['https://learncard.app', 'https://*.learncard.app']
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Custom tenant alongside the built-in LearnCard defaults:
|
|
41
58
|
* ```typescript
|
|
42
|
-
* hostOrigin: ['https://
|
|
43
|
-
* //
|
|
44
|
-
* //
|
|
45
|
-
* // Invalid: ?lc_host_override=https://evil.com (rejected)
|
|
59
|
+
* hostOrigin: ['https://partner.example.com']
|
|
60
|
+
* // learncard.app / *.learncard.app / *.learncard.ai / vetpass.app / *.vetpass.app
|
|
61
|
+
* // are ALSO trusted because disableDefaultTenants is false.
|
|
46
62
|
* ```
|
|
47
63
|
*
|
|
48
64
|
* @default 'https://learncard.app'
|
|
49
65
|
*/
|
|
50
66
|
hostOrigin?: string | string[];
|
|
67
|
+
/**
|
|
68
|
+
* Opt out of the built-in LearnCard tenant whitelist.
|
|
69
|
+
*
|
|
70
|
+
* By default, the SDK merges `hostOrigin` with a curated list of LearnCard
|
|
71
|
+
* and tenant domains (see `PartnerConnect.DEFAULT_TRUSTED_TENANTS`) so that
|
|
72
|
+
* partner apps work on any LearnCard-managed tenant without reconfiguration.
|
|
73
|
+
*
|
|
74
|
+
* Set to `true` if you want the partner app to **only** trust the origins
|
|
75
|
+
* you pass in `hostOrigin`.
|
|
76
|
+
*
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
disableDefaultTenants?: boolean;
|
|
51
80
|
/**
|
|
52
81
|
* Whether to allow native app origins (default: true)
|
|
53
82
|
*
|
|
@@ -325,11 +354,14 @@ interface SummaryCredentialSkill {
|
|
|
325
354
|
}
|
|
326
355
|
/**
|
|
327
356
|
* Next step item in summary credential data
|
|
357
|
+
*
|
|
358
|
+
* `keywords` is optional. Omit it entirely (or pass `undefined`) when you do not
|
|
359
|
+
* have taxonomy data — you no longer need to pass a struct of `null` fields.
|
|
328
360
|
*/
|
|
329
361
|
interface SummaryCredentialNextStep {
|
|
330
362
|
title: string;
|
|
331
363
|
description: string;
|
|
332
|
-
keywords
|
|
364
|
+
keywords?: SummaryCredentialKeyword;
|
|
333
365
|
}
|
|
334
366
|
/**
|
|
335
367
|
* Reflection item in summary credential data
|
|
@@ -431,12 +463,55 @@ interface GetCountersResponse {
|
|
|
431
463
|
*/
|
|
432
464
|
type ErrorCode = 'LC_TIMEOUT' | 'LC_UNAUTHENTICATED' | 'CREDENTIAL_NOT_FOUND' | 'USER_REJECTED' | 'UNAUTHORIZED' | 'TEMPLATE_NOT_FOUND' | 'BOOST_NOT_FOUND' | 'INSUFFICIENT_PERMISSIONS' | string;
|
|
433
465
|
/**
|
|
434
|
-
* Error object returned when a request fails
|
|
466
|
+
* Error object returned when a request fails.
|
|
467
|
+
*
|
|
468
|
+
* Historically the SDK rejected with a plain `{ code, message }` object. As of
|
|
469
|
+
* v0.3.0 we reject with a {@link PartnerConnectError} instance instead, which
|
|
470
|
+
* still satisfies this interface (it has both `code` and `message` fields), so
|
|
471
|
+
* existing consumers that do `if (err.code === '...')` continue to work
|
|
472
|
+
* unchanged.
|
|
435
473
|
*/
|
|
436
474
|
interface LearnCardError {
|
|
437
475
|
code: ErrorCode;
|
|
438
476
|
message: string;
|
|
439
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Typed error class for all Partner Connect SDK rejections.
|
|
480
|
+
*
|
|
481
|
+
* Use `instanceof PartnerConnectError` to narrow caught errors and unlock
|
|
482
|
+
* exhaustive `switch` checks on `code`. Both `code` and `message` are present
|
|
483
|
+
* (so the legacy `LearnCardError` object shape is preserved), and `name` is
|
|
484
|
+
* always `'PartnerConnectError'`.
|
|
485
|
+
*
|
|
486
|
+
* @example
|
|
487
|
+
* ```typescript
|
|
488
|
+
* try {
|
|
489
|
+
* await learnCard.requestLearnerContext();
|
|
490
|
+
* } catch (err) {
|
|
491
|
+
* if (err instanceof PartnerConnectError) {
|
|
492
|
+
* switch (err.code) {
|
|
493
|
+
* case 'LC_UNAUTHENTICATED': showLogin(); break;
|
|
494
|
+
* case 'USER_REJECTED': showPrivacyNotice(); break;
|
|
495
|
+
* case 'UNAUTHORIZED': showPermissionsError(); break;
|
|
496
|
+
* default: console.error(err);
|
|
497
|
+
* }
|
|
498
|
+
* }
|
|
499
|
+
* }
|
|
500
|
+
* ```
|
|
501
|
+
*/
|
|
502
|
+
declare class PartnerConnectError extends Error implements LearnCardError {
|
|
503
|
+
readonly code: ErrorCode;
|
|
504
|
+
constructor(code: ErrorCode, message: string);
|
|
505
|
+
/**
|
|
506
|
+
* Wrap any incoming `LearnCardError`-shaped value into a `PartnerConnectError`.
|
|
507
|
+
* Returns the value unchanged if it is already an instance.
|
|
508
|
+
*
|
|
509
|
+
* Used internally at every reject site so callers always receive a typed
|
|
510
|
+
* `PartnerConnectError`, regardless of whether the failure originated from
|
|
511
|
+
* the host (over postMessage), an SDK timeout, or `destroy()`.
|
|
512
|
+
*/
|
|
513
|
+
static from(input: LearnCardError | unknown): PartnerConnectError;
|
|
514
|
+
}
|
|
440
515
|
/**
|
|
441
516
|
* Internal message structure sent via postMessage
|
|
442
517
|
*/
|
|
@@ -491,6 +566,19 @@ interface PendingRequest {
|
|
|
491
566
|
declare class PartnerConnect {
|
|
492
567
|
/** Default host origin (security anchor) */
|
|
493
568
|
static readonly DEFAULT_HOST_ORIGIN = "https://learncard.app";
|
|
569
|
+
/**
|
|
570
|
+
* Built-in list of LearnCard-managed tenant origins.
|
|
571
|
+
*
|
|
572
|
+
* These are merged with the partner app's configured `hostOrigin` whitelist
|
|
573
|
+
* unless `disableDefaultTenants: true` is passed. This lets a partner app
|
|
574
|
+
* run inside any current or future LearnCard tenant (staging, preview,
|
|
575
|
+
* VetPass, etc.) without needing a re-deploy each time a new tenant is
|
|
576
|
+
* onboarded.
|
|
577
|
+
*
|
|
578
|
+
* Patterns follow the same rules as user-supplied `hostOrigin` entries:
|
|
579
|
+
* `*` is a wildcard for one or more DNS labels in the host portion.
|
|
580
|
+
*/
|
|
581
|
+
static readonly DEFAULT_TRUSTED_TENANTS: readonly string[];
|
|
494
582
|
private hostOrigins;
|
|
495
583
|
private activeHostOrigin;
|
|
496
584
|
private allowNativeAppOrigins;
|
|
@@ -502,22 +590,60 @@ declare class PartnerConnect {
|
|
|
502
590
|
constructor(options?: PartnerConnectOptions);
|
|
503
591
|
/**
|
|
504
592
|
* Configure the active host origin using the following hierarchy:
|
|
505
|
-
* 1.
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
593
|
+
* 1. `window.location.ancestorOrigins[0]` (when supported) — the browser's
|
|
594
|
+
* view of who our parent frame is. Cannot be forged by a malicious
|
|
595
|
+
* `lc_host_override` query param and therefore takes precedence.
|
|
596
|
+
* 2. `?lc_host_override=<origin>` query param (for staging / cross-tenant).
|
|
597
|
+
* 3. `sessionStorage` value saved from a previously-validated override.
|
|
598
|
+
* 4. First configured origin.
|
|
599
|
+
* 5. `DEFAULT_HOST_ORIGIN`.
|
|
600
|
+
*
|
|
601
|
+
* When a valid override is found in the query parameter, it is persisted
|
|
602
|
+
* to sessionStorage so subsequent in-iframe navigations in the same tab
|
|
603
|
+
* continue to use the same active origin.
|
|
515
604
|
*/
|
|
516
605
|
private static readonly SESSION_STORAGE_KEY;
|
|
606
|
+
/**
|
|
607
|
+
* Read `window.location.ancestorOrigins[0]` without throwing if the
|
|
608
|
+
* property is unavailable (Firefox) or the list is empty (top-level
|
|
609
|
+
* context, e.g. running outside of an iframe).
|
|
610
|
+
*/
|
|
611
|
+
private readAncestorOrigin;
|
|
517
612
|
private configureActiveOrigin;
|
|
613
|
+
private persistOverride;
|
|
518
614
|
private isOriginNativeApp;
|
|
519
615
|
/**
|
|
520
|
-
*
|
|
616
|
+
* Internal placeholder substituted in for `*` so that `new URL(...)` can
|
|
617
|
+
* parse a wildcard pattern. Chosen to be a syntactically-valid DNS label
|
|
618
|
+
* that cannot collide with a real hostname.
|
|
619
|
+
*/
|
|
620
|
+
private static readonly WILDCARD_PLACEHOLDER;
|
|
621
|
+
/** `*` (any number of occurrences) for replacement in the pattern. */
|
|
622
|
+
private static readonly WILDCARD_REGEX;
|
|
623
|
+
/** The required leading-label form a wildcard pattern must take. */
|
|
624
|
+
private static readonly WILDCARD_LEADING_PREFIX;
|
|
625
|
+
/**
|
|
626
|
+
* Check whether a candidate origin matches a configured whitelist entry.
|
|
627
|
+
*
|
|
628
|
+
* Supports exact matches and wildcard patterns. A wildcard entry has the
|
|
629
|
+
* form `<protocol>://*.<domain>` and matches any origin with the same
|
|
630
|
+
* protocol, same port, and a host ending in `.<domain>` with at least
|
|
631
|
+
* one non-empty DNS label in place of the `*`.
|
|
632
|
+
*
|
|
633
|
+
* Examples with pattern `https://*.learncard.app`:
|
|
634
|
+
* - `https://staging.learncard.app` → match
|
|
635
|
+
* - `https://pr-1.preview.learncard.app` → match
|
|
636
|
+
* - `https://learncard.app` → no match (no subdomain)
|
|
637
|
+
* - `http://staging.learncard.app` → no match (protocol mismatch)
|
|
638
|
+
* - `https://learncard.app.attacker.com` → no match (suffix mismatch)
|
|
639
|
+
*
|
|
640
|
+
* Exposed as a public static so it can be unit-tested directly without
|
|
641
|
+
* standing up a full SDK instance.
|
|
642
|
+
*/
|
|
643
|
+
static matchesOriginPattern(candidate: string, pattern: string): boolean;
|
|
644
|
+
/**
|
|
645
|
+
* Check if an origin is in the effective whitelist (exact origins +
|
|
646
|
+
* wildcard patterns + optional native-app origins).
|
|
521
647
|
*/
|
|
522
648
|
private isOriginInWhitelist;
|
|
523
649
|
/**
|
|
@@ -851,4 +977,4 @@ declare class PartnerConnect {
|
|
|
851
977
|
*/
|
|
852
978
|
declare function createPartnerConnect(options?: PartnerConnectOptions): PartnerConnect;
|
|
853
979
|
|
|
854
|
-
export { AppNotificationInput, AppNotificationResponse, CheckCredentialInput, CheckCredentialResponse, CheckIssuanceStatusInput, ConsentResponse, CredentialSearchResponse, CredentialSpecificResponse, ErrorCode, GetCounterResponse, GetCountersResponse, GetTemplateRecipientsInput, IdentityResponse, IncrementCounterResponse, LearnCardError, LearnerContextRawData, LearnerContextResponse, PartnerConnect, PartnerConnectOptions, PendingRequest, PostMessageRequest, PostMessageResponse, RequestConsentOptions, RequestConsentPayload, RequestLearnerContextOptions, SendAiSessionCredentialInput, SendAiSessionCredentialResponse, SendCredentialResponse, SummaryCredentialData, SummaryCredentialKeyword, SummaryCredentialNextStep, SummaryCredentialReflection, SummaryCredentialSkill, TemplateCredentialInput, TemplateCredentialResponse, TemplateIssuanceStatusResponse, TemplateIssueResponse, TemplateRecipientRecord, TemplateRecipientsResponse, VPRQuery, VerifiablePresentationRequest, createPartnerConnect, createPartnerConnect as default };
|
|
980
|
+
export { AppNotificationInput, AppNotificationResponse, CheckCredentialInput, CheckCredentialResponse, CheckIssuanceStatusInput, ConsentResponse, CredentialSearchResponse, CredentialSpecificResponse, ErrorCode, GetCounterResponse, GetCountersResponse, GetTemplateRecipientsInput, IdentityResponse, IncrementCounterResponse, LearnCardError, LearnerContextRawData, LearnerContextResponse, PartnerConnect, PartnerConnectError, PartnerConnectOptions, PendingRequest, PostMessageRequest, PostMessageResponse, RequestConsentOptions, RequestConsentPayload, RequestLearnerContextOptions, SendAiSessionCredentialInput, SendAiSessionCredentialResponse, SendCredentialResponse, SummaryCredentialData, SummaryCredentialKeyword, SummaryCredentialNextStep, SummaryCredentialReflection, SummaryCredentialSkill, TemplateCredentialInput, TemplateCredentialResponse, TemplateIssuanceStatusResponse, TemplateIssueResponse, TemplateRecipientRecord, TemplateRecipientsResponse, VPRQuery, VerifiablePresentationRequest, createPartnerConnect, createPartnerConnect as default };
|
|
@@ -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,68 +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)
|
|
16
|
-
|
|
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 = (
|
|
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
|
}
|
|
60
|
+
/**
|
|
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).
|
|
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
|
+
}
|
|
24
79
|
configureActiveOrigin() {
|
|
25
80
|
if (typeof window === "undefined") {
|
|
26
81
|
this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
|
|
27
82
|
return;
|
|
28
83
|
}
|
|
29
84
|
try {
|
|
85
|
+
const ancestorOrigin = this.readAncestorOrigin();
|
|
30
86
|
const urlParams = new URLSearchParams(window.location.search);
|
|
31
87
|
const hostOverride = urlParams.get("lc_host_override");
|
|
32
|
-
if (
|
|
33
|
-
if (
|
|
88
|
+
if (ancestorOrigin && this.isOriginInWhitelist(ancestorOrigin)) {
|
|
89
|
+
if (hostOverride && hostOverride !== ancestorOrigin) {
|
|
34
90
|
console.warn(
|
|
35
|
-
"[LearnCard SDK] lc_host_override
|
|
36
|
-
hostOverride,
|
|
37
|
-
"Allowed:",
|
|
38
|
-
this.hostOrigins
|
|
91
|
+
"[LearnCard SDK] lc_host_override does not match the real parent origin; preferring parent.",
|
|
92
|
+
{ override: hostOverride, parent: ancestorOrigin }
|
|
39
93
|
);
|
|
40
|
-
|
|
41
|
-
|
|
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)) {
|
|
42
102
|
this.activeHostOrigin = hostOverride;
|
|
43
|
-
|
|
44
|
-
sessionStorage.setItem(_PartnerConnect.SESSION_STORAGE_KEY, hostOverride);
|
|
45
|
-
} catch {
|
|
46
|
-
}
|
|
103
|
+
this.persistOverride(hostOverride);
|
|
47
104
|
console.log("[LearnCard SDK] Using lc_host_override:", hostOverride);
|
|
105
|
+
return;
|
|
48
106
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (storedOverride && this.isOriginInWhitelist(storedOverride)) {
|
|
56
|
-
this.activeHostOrigin = storedOverride;
|
|
57
|
-
console.log("[LearnCard SDK] Using stored lc_host_override:", storedOverride);
|
|
58
|
-
} else {
|
|
59
|
-
this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
|
|
60
|
-
console.log("[LearnCard SDK] Using configured origin:", this.activeHostOrigin);
|
|
61
|
-
}
|
|
107
|
+
console.warn(
|
|
108
|
+
"[LearnCard SDK] lc_host_override value is not in the configured whitelist:",
|
|
109
|
+
hostOverride,
|
|
110
|
+
"Allowed:",
|
|
111
|
+
this.hostOrigins
|
|
112
|
+
);
|
|
62
113
|
}
|
|
114
|
+
let storedOverride = null;
|
|
115
|
+
try {
|
|
116
|
+
storedOverride = sessionStorage.getItem(_PartnerConnect.SESSION_STORAGE_KEY);
|
|
117
|
+
} catch {
|
|
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);
|
|
63
126
|
} catch (error) {
|
|
64
127
|
console.error("[LearnCard SDK] Error configuring active origin:", error);
|
|
65
128
|
this.activeHostOrigin = this.hostOrigins[0] || _PartnerConnect.DEFAULT_HOST_ORIGIN;
|
|
66
129
|
}
|
|
67
130
|
}
|
|
131
|
+
persistOverride(origin) {
|
|
132
|
+
try {
|
|
133
|
+
sessionStorage.setItem(_PartnerConnect.SESSION_STORAGE_KEY, origin);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
68
137
|
isOriginNativeApp(origin) {
|
|
69
138
|
return origin.startsWith("capacitor://") || origin.startsWith("ionic://") || origin.startsWith("https://localhost") || origin.startsWith("http://localhost") || origin.startsWith("http://127.0.0.1");
|
|
70
139
|
}
|
|
71
140
|
/**
|
|
72
|
-
* Check
|
|
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).
|
|
73
193
|
*/
|
|
74
194
|
isOriginInWhitelist(origin) {
|
|
75
|
-
|
|
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;
|
|
76
201
|
}
|
|
77
202
|
/**
|
|
78
203
|
* Check if an event origin is valid against the active host origin
|
|
@@ -111,7 +236,12 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
111
236
|
pending.resolve(data.data);
|
|
112
237
|
} else if (data.type === "ERROR") {
|
|
113
238
|
pending.reject(
|
|
114
|
-
|
|
239
|
+
PartnerConnectError.from(
|
|
240
|
+
data.error || {
|
|
241
|
+
code: "UNKNOWN_ERROR",
|
|
242
|
+
message: "An unknown error occurred"
|
|
243
|
+
}
|
|
244
|
+
)
|
|
115
245
|
);
|
|
116
246
|
}
|
|
117
247
|
};
|
|
@@ -129,20 +259,21 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
129
259
|
*/
|
|
130
260
|
sendMessage(action, payload) {
|
|
131
261
|
if (!this.isInitialized) {
|
|
132
|
-
return Promise.reject(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
262
|
+
return Promise.reject(
|
|
263
|
+
new PartnerConnectError("SDK_NOT_INITIALIZED", "SDK is not initialized")
|
|
264
|
+
);
|
|
136
265
|
}
|
|
137
266
|
return new Promise((resolve, reject) => {
|
|
138
267
|
const requestId = this.generateRequestId(action);
|
|
139
268
|
const timeoutId = setTimeout(() => {
|
|
140
269
|
if (this.pendingRequests.has(requestId)) {
|
|
141
270
|
this.pendingRequests.delete(requestId);
|
|
142
|
-
reject(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
271
|
+
reject(
|
|
272
|
+
new PartnerConnectError(
|
|
273
|
+
"LC_TIMEOUT",
|
|
274
|
+
`Request ${action} timed out after ${this.requestTimeout}ms`
|
|
275
|
+
)
|
|
276
|
+
);
|
|
146
277
|
}
|
|
147
278
|
}, this.requestTimeout);
|
|
148
279
|
this.pendingRequests.set(requestId, {
|
|
@@ -544,10 +675,12 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
544
675
|
}
|
|
545
676
|
for (const [requestId, pending] of this.pendingRequests.entries()) {
|
|
546
677
|
clearTimeout(pending.timeoutId);
|
|
547
|
-
pending.reject(
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
678
|
+
pending.reject(
|
|
679
|
+
new PartnerConnectError(
|
|
680
|
+
"SDK_DESTROYED",
|
|
681
|
+
"SDK was destroyed before request completed"
|
|
682
|
+
)
|
|
683
|
+
);
|
|
551
684
|
}
|
|
552
685
|
this.pendingRequests.clear();
|
|
553
686
|
this.isInitialized = false;
|
|
@@ -556,23 +689,53 @@ const _PartnerConnect = class _PartnerConnect {
|
|
|
556
689
|
/** Default host origin (security anchor) */
|
|
557
690
|
__publicField(_PartnerConnect, "DEFAULT_HOST_ORIGIN", "https://learncard.app");
|
|
558
691
|
/**
|
|
559
|
-
*
|
|
560
|
-
* 1. Check for `lc_host_override` query parameter (for staging/testing)
|
|
561
|
-
* 2. Check sessionStorage for a previously stored override (survives in-app navigation)
|
|
562
|
-
* 3. Fall back to first configured origin
|
|
563
|
-
* 4. Fall back to DEFAULT_HOST_ORIGIN
|
|
692
|
+
* Built-in list of LearnCard-managed tenant origins.
|
|
564
693
|
*
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
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.
|
|
568
699
|
*
|
|
569
|
-
*
|
|
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.
|
|
570
723
|
*/
|
|
571
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}.`);
|
|
572
735
|
let PartnerConnect = _PartnerConnect;
|
|
573
736
|
function createPartnerConnect(options) {
|
|
574
737
|
return new PartnerConnect(options);
|
|
575
738
|
}
|
|
576
739
|
|
|
577
|
-
export { PartnerConnect, createPartnerConnect, createPartnerConnect as default };
|
|
740
|
+
export { PartnerConnect, PartnerConnectError, createPartnerConnect, createPartnerConnect as default };
|
|
578
741
|
//# sourceMappingURL=partner-connect.esm.js.map
|