@learncard/partner-connect 0.2.8 โ†’ 0.2.10

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/README.md CHANGED
@@ -6,12 +6,12 @@ The LearnCard Partner Connect SDK transforms complex `postMessage` communication
6
6
 
7
7
  ## Features
8
8
 
9
- - ๐Ÿ”’ **Secure**: Origin validation for all messages
10
- - ๐ŸŽฏ **Type-safe**: Full TypeScript support with comprehensive types
11
- - โšก **Promise-based**: Modern async/await API
12
- - ๐Ÿงน **Clean**: Abstracts away all postMessage complexity
13
- - ๐Ÿ“ฆ **Lightweight**: Zero dependencies
14
- - ๐Ÿ›ก๏ธ **Robust**: Built-in timeout handling and error management
9
+ - ๐Ÿ”’ **Secure**: Origin validation for all messages
10
+ - ๐ŸŽฏ **Type-safe**: Full TypeScript support with comprehensive types
11
+ - โšก **Promise-based**: Modern async/await API
12
+ - ๐Ÿงน **Clean**: Abstracts away all postMessage complexity
13
+ - ๐Ÿ“ฆ **Lightweight**: Zero runtime dependencies
14
+ - ๐Ÿ›ก๏ธ **Robust**: Built-in timeout handling and error management
15
15
 
16
16
  ## Installation
17
17
 
@@ -34,18 +34,18 @@ import { createPartnerConnect } from '@learncard/partner-connect';
34
34
 
35
35
  // Initialize the SDK
36
36
  const learnCard = createPartnerConnect({
37
- hostOrigin: 'https://learncard.app'
37
+ hostOrigin: 'https://learncard.app',
38
38
  });
39
39
 
40
40
  // Request user identity (SSO)
41
41
  try {
42
- const identity = await learnCard.requestIdentity();
43
- console.log('User DID:', identity.user.did);
44
- console.log('JWT Token:', identity.token);
42
+ const identity = await learnCard.requestIdentity();
43
+ console.log('User DID:', identity.user.did);
44
+ console.log('JWT Token:', identity.token);
45
45
  } catch (error) {
46
- if (error.code === 'LC_UNAUTHENTICATED') {
47
- console.log('User is not logged in');
48
- }
46
+ if (error.code === 'LC_UNAUTHENTICATED') {
47
+ console.log('User is not logged in');
48
+ }
49
49
  }
50
50
  ```
51
51
 
@@ -55,22 +55,28 @@ try {
55
55
 
56
56
  ```typescript
57
57
  interface PartnerConnectOptions {
58
- /**
59
- * The origin of the LearnCard host (e.g., 'https://learncard.app')
60
- * All messages will be validated against this origin for security
61
- * (default: 'https://learncard.app')
62
- */
63
- hostOrigin?: string;
64
-
65
- /**
66
- * Protocol identifier (default: 'LEARNCARD_V1')
67
- */
68
- protocol?: string;
69
-
70
- /**
71
- * Request timeout in milliseconds (default: 30000)
72
- */
73
- requestTimeout?: number;
58
+ /**
59
+ * The origin(s) of the LearnCard host
60
+ * Single string or array for query parameter whitelist
61
+ * @default 'https://learncard.app'
62
+ */
63
+ hostOrigin?: string | string[];
64
+
65
+ /**
66
+ * Whether to allow native app origins (Capacitor/Ionic)
67
+ * @default true
68
+ */
69
+ allowNativeAppOrigins?: boolean;
70
+
71
+ /**
72
+ * Protocol identifier (default: 'LEARNCARD_V1')
73
+ */
74
+ protocol?: string;
75
+
76
+ /**
77
+ * Request timeout in milliseconds (default: 30000)
78
+ */
79
+ requestTimeout?: number;
74
80
  }
75
81
  ```
76
82
 
@@ -79,16 +85,18 @@ interface PartnerConnectOptions {
79
85
  The SDK uses a hierarchical approach to determine the active host origin:
80
86
 
81
87
  #### 1. **Hardcoded Default** (Security Anchor)
88
+
82
89
  ```typescript
83
- PartnerConnect.DEFAULT_HOST_ORIGIN // 'https://learncard.app'
90
+ PartnerConnect.DEFAULT_HOST_ORIGIN; // 'https://learncard.app'
84
91
  ```
85
92
 
86
93
  #### 2. **Query Parameter Override** (Staging/Testing)
94
+
87
95
  ```typescript
88
96
  // Your app URL: https://partner-app.com/?lc_host_override=https://staging.learncard.app
89
97
 
90
98
  const learnCard = createPartnerConnect({
91
- hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
99
+ hostOrigin: ['https://learncard.app', 'https://staging.learncard.app'],
92
100
  });
93
101
  // Active origin: https://staging.learncard.app (from query param)
94
102
  // โœ… Only accepts messages from: https://staging.learncard.app
@@ -96,14 +104,16 @@ const learnCard = createPartnerConnect({
96
104
  ```
97
105
 
98
106
  **How the LearnCard Host Uses This:**
99
- - Production: Iframe URL has no `lc_host_override` parameter
100
- - Staging: Iframe URL includes `?lc_host_override=https://staging.learncard.app`
101
- - This allows testing against non-production environments without recompiling partner code
107
+
108
+ - Production: Iframe URL has no `lc_host_override` parameter
109
+ - Staging: Iframe URL includes `?lc_host_override=https://staging.learncard.app`
110
+ - This allows testing against non-production environments without recompiling partner code
102
111
 
103
112
  #### 3. **Configured Origin** (Fallback)
113
+
104
114
  ```typescript
105
115
  const learnCard = createPartnerConnect({
106
- hostOrigin: 'https://learncard.app'
116
+ hostOrigin: 'https://learncard.app',
107
117
  });
108
118
  // Active origin: https://learncard.app (configured)
109
119
  ```
@@ -114,11 +124,11 @@ When providing multiple origins, they serve as a **whitelist** for the `lc_host_
114
124
 
115
125
  ```typescript
116
126
  const learnCard = createPartnerConnect({
117
- hostOrigin: [
118
- 'https://learncard.app',
119
- 'https://staging.learncard.app',
120
- 'https://preview.learncard.app'
121
- ]
127
+ hostOrigin: [
128
+ 'https://learncard.app',
129
+ 'https://staging.learncard.app',
130
+ 'https://preview.learncard.app',
131
+ ],
122
132
  });
123
133
 
124
134
  // Scenario 1: No query param
@@ -137,15 +147,16 @@ const learnCard = createPartnerConnect({
137
147
  ### Security Model
138
148
 
139
149
  **STRICT Origin Validation:**
150
+
140
151
  ```
141
152
  Incoming Message Origin โ‰ก Configured Host Origin
142
153
  ```
143
154
 
144
155
  The SDK enforces an exact match between incoming message origins and the active host origin:
145
156
 
146
- - โœ… **Secure**: Even if a malicious actor adds `?lc_host_override=https://evil.com`, messages from `evil.com` will be rejected
147
- - โœ… **Cannot be spoofed**: Browser security prevents malicious sites from faking their `event.origin`
148
- - โœ… **No wildcards**: Only exact matches are accepted
157
+ - โœ… **Secure**: Even if a malicious actor adds `?lc_host_override=https://evil.com`, messages from `evil.com` will be rejected
158
+ - โœ… **Cannot be spoofed**: Browser security prevents malicious sites from faking their `event.origin`
159
+ - โœ… **No wildcards**: Only exact matches are accepted
149
160
 
150
161
  ```typescript
151
162
  // Active origin: https://staging.learncard.app
@@ -167,8 +178,9 @@ const identity = await learnCard.requestIdentity();
167
178
  ```
168
179
 
169
180
  **Error Codes:**
170
- - `LC_UNAUTHENTICATED`: User is not logged in to LearnCard
171
- - `LC_TIMEOUT`: Request timed out
181
+
182
+ - `LC_UNAUTHENTICATED`: User is not logged in to LearnCard
183
+ - `LC_TIMEOUT`: Request timed out
172
184
 
173
185
  ---
174
186
 
@@ -178,38 +190,136 @@ Send a verifiable credential to the user's LearnCard wallet.
178
190
 
179
191
  ```typescript
180
192
  const response = await learnCard.sendCredential({
181
- '@context': ['https://www.w3.org/2018/credentials/v1'],
182
- type: ['VerifiableCredential', 'AchievementCredential'],
183
- credentialSubject: {
184
- id: identity.user.did,
185
- achievement: {
186
- name: 'JavaScript Expert',
187
- description: 'Mastered advanced JavaScript concepts'
188
- }
189
- }
193
+ '@context': ['https://www.w3.org/2018/credentials/v1'],
194
+ type: ['VerifiableCredential', 'AchievementCredential'],
195
+ credentialSubject: {
196
+ id: identity.user.did,
197
+ achievement: {
198
+ name: 'JavaScript Expert',
199
+ description: 'Mastered advanced JavaScript concepts',
200
+ },
201
+ },
190
202
  });
191
203
 
192
204
  console.log('Credential ID:', response.credentialId);
193
205
  ```
194
206
 
195
- **Returns:** `{ credentialId: string }`
207
+ **Returns:** `{ credentialId: string }` (raw credential mode) or `{ credentialUri: string, boostUri: string }` (template mode).
208
+
209
+ Template mode example with duplicate prevention:
210
+
211
+ ```typescript
212
+ const response = await learnCard.sendCredential({
213
+ templateAlias: 'achievement',
214
+ templateData: { score: 95 },
215
+ preventDuplicateClaim: true,
216
+ });
217
+
218
+ if (response.alreadyClaimed) {
219
+ console.log('User already has this credential:', response.credentialUri);
220
+ }
221
+ ```
196
222
 
197
223
  ---
198
224
 
225
+ ### `checkUserHasCredential(input)`
226
+
227
+ Silently check whether the current user already has a credential for a given app boost template.
228
+
229
+ ```typescript
230
+ const result = await learnCard.checkUserHasCredential({
231
+ templateAlias: 'achievement',
232
+ });
233
+
234
+ if (result.hasCredential) {
235
+ console.log('Already earned:', result.credentialUri, result.receivedDate);
236
+ } else {
237
+ console.log('Not earned yet');
238
+ }
239
+ ```
240
+
241
+ You can also query directly by boost URI:
242
+
243
+ ```typescript
244
+ await learnCard.checkUserHasCredential({
245
+ boostUri: 'lc:network:network.learncard.com/trpc:boost:abc123',
246
+ });
247
+ ```
248
+
249
+ **Returns:** `{ hasCredential: boolean, credentialUri?: string, receivedDate?: string, status?: 'pending' | 'claimed' | 'revoked' }`
250
+
251
+ ### `getTemplateIssuanceStatus(input)`
252
+
253
+ Check if the current user has issued/sent a specific template to someone. Returns issuance status including sent date and claim status.
254
+
255
+ ```typescript
256
+ const status = await learnCard.getTemplateIssuanceStatus({
257
+ templateAlias: 'achievement-badge',
258
+ recipient: 'user123', // Can be a profileId or DID (did:web:...)
259
+ });
260
+
261
+ if (status.sent) {
262
+ console.log('Issued on:', status.sentDate);
263
+ console.log('Status:', status.status); // 'pending', 'claimed', or 'revoked'
264
+ if (status.claimedDate) {
265
+ console.log('Claimed on:', status.claimedDate);
266
+ }
267
+ }
268
+ ```
269
+
270
+ You can also use a DID as the recipient:
271
+
272
+ ```typescript
273
+ await learnCard.getTemplateIssuanceStatus({
274
+ boostUri: 'lc:network:network.learncard.com/trpc:boost:abc123',
275
+ recipient: 'did:web:network.learncard.com:users:user456',
276
+ });
277
+ ```
278
+
279
+ **Returns:** `{ sent: boolean, credentialUri?: string, sentDate?: string, claimedDate?: string, status?: 'pending' | 'claimed' | 'revoked' }`
280
+
281
+ ### `getTemplateRecipients(input)`
282
+
283
+ Get the list of all recipients for a specific template/boost. Useful for dashboards showing who has received a credential.
284
+
285
+ ```typescript
286
+ const recipients = await learnCard.getTemplateRecipients({
287
+ templateAlias: 'achievement-badge',
288
+ limit: 10,
289
+ });
290
+
291
+ console.log(`Found ${recipients.records.length} recipients`);
292
+ recipients.records.forEach(r => {
293
+ console.log(`${r.recipientDisplayName}: ${r.status}`);
294
+ });
295
+
296
+ // Paginate if more results available
297
+ if (recipients.hasMore) {
298
+ const nextPage = await learnCard.getTemplateRecipients({
299
+ templateAlias: 'achievement-badge',
300
+ limit: 10,
301
+ cursor: recipients.cursor,
302
+ });
303
+ }
304
+ ```
305
+
306
+ **Returns:** `{ records: TemplateRecipientRecord[], hasMore: boolean, cursor?: string, total?: number }`
307
+
199
308
  ### `launchFeature(featurePath, initialPrompt?)`
200
309
 
201
310
  Launch a feature in the LearnCard host application.
202
311
 
203
312
  ```typescript
204
313
  await learnCard.launchFeature(
205
- '/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
206
- 'Explain the postMessage security model'
314
+ '/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
315
+ 'Explain the postMessage security model'
207
316
  );
208
317
  ```
209
318
 
210
319
  **Parameters:**
211
- - `featurePath`: Path to the feature
212
- - `initialPrompt`: Optional initial data or prompt
320
+
321
+ - `featurePath`: Path to the feature
322
+ - `initialPrompt`: Optional initial data or prompt
213
323
 
214
324
  ---
215
325
 
@@ -219,22 +329,22 @@ Request credentials from the user's wallet using a Verifiable Presentation Reque
219
329
 
220
330
  ```typescript
221
331
  const response = await learnCard.askCredentialSearch({
222
- query: [
223
- {
224
- type: 'QueryByTitle',
225
- credentialQuery: {
226
- reason: 'We need to verify your teamwork skills',
227
- title: 'Capstone'
228
- }
229
- }
230
- ],
231
- challenge: `challenge-${Date.now()}`,
232
- domain: window.location.hostname
332
+ query: [
333
+ {
334
+ type: 'QueryByTitle',
335
+ credentialQuery: {
336
+ reason: 'We need to verify your teamwork skills',
337
+ title: 'Capstone',
338
+ },
339
+ },
340
+ ],
341
+ challenge: `challenge-${Date.now()}`,
342
+ domain: window.location.hostname,
233
343
  });
234
344
 
235
345
  if (response.verifiablePresentation) {
236
- const credentials = response.verifiablePresentation.verifiableCredential;
237
- console.log(`Received ${credentials.length} credential(s)`);
346
+ const credentials = response.verifiablePresentation.verifiableCredential;
347
+ console.log(`Received ${credentials.length} credential(s)`);
238
348
  }
239
349
  ```
240
350
 
@@ -250,13 +360,14 @@ Request a specific credential by ID.
250
360
  const response = await learnCard.askCredentialSpecific('credential-id-123');
251
361
 
252
362
  if (response.credential) {
253
- console.log('Received credential:', response.credential);
363
+ console.log('Received credential:', response.credential);
254
364
  }
255
365
  ```
256
366
 
257
367
  **Error Codes:**
258
- - `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
259
- - `USER_REJECTED`: User declined to share
368
+
369
+ - `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
370
+ - `USER_REJECTED`: User declined to share
260
371
 
261
372
  ---
262
373
 
@@ -266,13 +377,13 @@ Request user consent for permissions.
266
377
 
267
378
  ```typescript
268
379
  const response = await learnCard.requestConsent(
269
- 'lc:network:network.learncard.com/trpc:contract:abc123'
380
+ 'lc:network:network.learncard.com/trpc:contract:abc123'
270
381
  );
271
382
 
272
383
  if (response.granted) {
273
- console.log('User granted consent');
384
+ console.log('User granted consent');
274
385
  } else {
275
- console.log('User denied consent');
386
+ console.log('User denied consent');
276
387
  }
277
388
  ```
278
389
 
@@ -286,18 +397,19 @@ Initiate a template-based credential issuance flow (e.g., Send Boost).
286
397
 
287
398
  ```typescript
288
399
  const response = await learnCard.initiateTemplateIssue(
289
- 'lc:network:network.learncard.com/trpc:boost:xyz789',
290
- ['did:key:z6Mkr...', 'did:key:z6Mks...']
400
+ 'lc:network:network.learncard.com/trpc:boost:xyz789',
401
+ ['did:key:z6Mkr...', 'did:key:z6Mks...']
291
402
  );
292
403
 
293
404
  if (response.issued) {
294
- console.log('Template issued successfully');
405
+ console.log('Template issued successfully');
295
406
  }
296
407
  ```
297
408
 
298
409
  **Error Codes:**
299
- - `UNAUTHORIZED`: Not an admin of this template
300
- - `TEMPLATE_NOT_FOUND`: Template doesn't exist
410
+
411
+ - `UNAUTHORIZED`: Not an admin of this template
412
+ - `TEMPLATE_NOT_FOUND`: Template doesn't exist
301
413
 
302
414
  ---
303
415
 
@@ -322,40 +434,43 @@ const PROTOCOL = 'LEARNCARD_V1';
322
434
  const pendingRequests = new Map();
323
435
 
324
436
  function sendPostMessage(action, payload = {}) {
325
- return new Promise((resolve, reject) => {
326
- const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
327
- pendingRequests.set(requestId, { resolve, reject });
328
-
329
- window.parent.postMessage({
330
- protocol: PROTOCOL,
331
- action,
332
- requestId,
333
- payload,
334
- }, LEARNCARD_HOST_ORIGIN);
335
-
336
- setTimeout(() => {
337
- if (pendingRequests.has(requestId)) {
338
- pendingRequests.delete(requestId);
339
- reject({ code: 'LC_TIMEOUT', message: 'Request timed out' });
340
- }
341
- }, 30000);
342
- });
437
+ return new Promise((resolve, reject) => {
438
+ const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
439
+ pendingRequests.set(requestId, { resolve, reject });
440
+
441
+ window.parent.postMessage(
442
+ {
443
+ protocol: PROTOCOL,
444
+ action,
445
+ requestId,
446
+ payload,
447
+ },
448
+ LEARNCARD_HOST_ORIGIN
449
+ );
450
+
451
+ setTimeout(() => {
452
+ if (pendingRequests.has(requestId)) {
453
+ pendingRequests.delete(requestId);
454
+ reject({ code: 'LC_TIMEOUT', message: 'Request timed out' });
455
+ }
456
+ }, 30000);
457
+ });
343
458
  }
344
459
 
345
- window.addEventListener('message', (event) => {
346
- if (event.origin !== LEARNCARD_HOST_ORIGIN) return;
347
- const { protocol, requestId, type, data, error } = event.data;
348
- if (protocol !== PROTOCOL || !requestId) return;
460
+ window.addEventListener('message', event => {
461
+ if (event.origin !== LEARNCARD_HOST_ORIGIN) return;
462
+ const { protocol, requestId, type, data, error } = event.data;
463
+ if (protocol !== PROTOCOL || !requestId) return;
349
464
 
350
- const pending = pendingRequests.get(requestId);
351
- if (!pending) return;
465
+ const pending = pendingRequests.get(requestId);
466
+ if (!pending) return;
352
467
 
353
- pendingRequests.delete(requestId);
354
- if (type === 'SUCCESS') {
355
- pending.resolve(data);
356
- } else if (type === 'ERROR') {
357
- pending.reject(error);
358
- }
468
+ pendingRequests.delete(requestId);
469
+ if (type === 'SUCCESS') {
470
+ pending.resolve(data);
471
+ } else if (type === 'ERROR') {
472
+ pending.reject(error);
473
+ }
359
474
  });
360
475
 
361
476
  // Usage
@@ -369,7 +484,7 @@ import { createPartnerConnect } from '@learncard/partner-connect';
369
484
 
370
485
  // Clean, one-line setup
371
486
  const learnCard = createPartnerConnect({
372
- hostOrigin: 'https://learncard.app'
487
+ hostOrigin: 'https://learncard.app',
373
488
  });
374
489
 
375
490
  // Usage - same result, much cleaner
@@ -382,38 +497,39 @@ All methods return Promises that reject with a `LearnCardError` object:
382
497
 
383
498
  ```typescript
384
499
  interface LearnCardError {
385
- code: string;
386
- message: string;
500
+ code: string;
501
+ message: string;
387
502
  }
388
503
  ```
389
504
 
390
505
  **Common Error Codes:**
391
- - `LC_TIMEOUT`: Request timed out
392
- - `LC_UNAUTHENTICATED`: User not logged in
393
- - `USER_REJECTED`: User declined the request
394
- - `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
395
- - `UNAUTHORIZED`: User lacks permission
396
- - `TEMPLATE_NOT_FOUND`: Template doesn't exist
397
- - `SDK_NOT_INITIALIZED`: SDK initialization failed
398
- - `SDK_DESTROYED`: SDK was destroyed before completion
506
+
507
+ - `LC_TIMEOUT`: Request timed out
508
+ - `LC_UNAUTHENTICATED`: User not logged in
509
+ - `USER_REJECTED`: User declined the request
510
+ - `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
511
+ - `UNAUTHORIZED`: User lacks permission
512
+ - `TEMPLATE_NOT_FOUND`: Template doesn't exist
513
+ - `SDK_NOT_INITIALIZED`: SDK initialization failed
514
+ - `SDK_DESTROYED`: SDK was destroyed before completion
399
515
 
400
516
  **Example:**
401
517
 
402
518
  ```typescript
403
519
  try {
404
- const identity = await learnCard.requestIdentity();
405
- // Success
520
+ const identity = await learnCard.requestIdentity();
521
+ // Success
406
522
  } catch (error) {
407
- switch (error.code) {
408
- case 'LC_UNAUTHENTICATED':
409
- console.log('Please log in to your LearnCard account');
410
- break;
411
- case 'LC_TIMEOUT':
412
- console.log('Request timed out. Please try again.');
413
- break;
414
- default:
415
- console.error('An error occurred:', error.message);
416
- }
523
+ switch (error.code) {
524
+ case 'LC_UNAUTHENTICATED':
525
+ console.log('Please log in to your LearnCard account');
526
+ break;
527
+ case 'LC_TIMEOUT':
528
+ console.log('Request timed out. Please try again.');
529
+ break;
530
+ default:
531
+ console.error('An error occurred:', error.message);
532
+ }
417
533
  }
418
534
  ```
419
535
 
@@ -450,9 +566,9 @@ const config = {
450
566
 
451
567
  ## Browser Support
452
568
 
453
- - Chrome/Edge 90+
454
- - Firefox 88+
455
- - Safari 14+
569
+ - Chrome/Edge 90+
570
+ - Firefox 88+
571
+ - Safari 14+
456
572
 
457
573
  Requires `postMessage` API and `Promise` support.
458
574
 
@@ -461,27 +577,32 @@ Requires `postMessage` API and `Promise` support.
461
577
  The SDK implements multiple security layers:
462
578
 
463
579
  ### 1. **Strict Origin Validation**
464
- - Messages must come from the **exact** active host origin
465
- - No wildcards, no pattern matching, no exceptions
466
- - Mathematical equivalence: `event.origin === activeHostOrigin`
580
+
581
+ - Messages must come from the **exact** active host origin
582
+ - No wildcards, no pattern matching, no exceptions
583
+ - Mathematical equivalence: `event.origin === activeHostOrigin`
467
584
 
468
585
  ### 2. **Query Parameter Whitelist**
469
- - `lc_host_override` values are validated against configured `hostOrigin` array
470
- - Invalid overrides are rejected and logged
471
- - Falls back to first configured origin on validation failure
586
+
587
+ - `lc_host_override` values are validated against configured `hostOrigin` array
588
+ - Invalid overrides are rejected and logged
589
+ - Falls back to first configured origin on validation failure
472
590
 
473
591
  ### 3. **Anti-Spoofing Protection**
592
+
474
593
  Even if a malicious actor injects `?lc_host_override=https://evil.com`:
475
- - The SDK may adopt `evil.com` as the active origin (if not whitelisted)
476
- - **BUT** messages from `evil.com` will only be accepted if `event.origin === 'evil.com'`
477
- - Browser security prevents `evil.com` from spoofing another domain's origin
478
- - Malicious messages are silently rejected
594
+
595
+ - The SDK may adopt `evil.com` as the active origin (if not whitelisted)
596
+ - **BUT** messages from `evil.com` will only be accepted if `event.origin === 'evil.com'`
597
+ - Browser security prevents `evil.com` from spoofing another domain's origin
598
+ - Malicious messages are silently rejected
479
599
 
480
600
  ### 4. **Additional Security Layers**
481
- - **Protocol Validation**: Messages must match the expected protocol identifier
482
- - **Request ID Tracking**: Only tracked requests with valid IDs are processed
483
- - **Timeout Protection**: Requests automatically timeout to prevent hanging
484
- - **Explicit targetOrigin**: Never uses `'*'` in postMessage calls
601
+
602
+ - **Protocol Validation**: Messages must match the expected protocol identifier
603
+ - **Request ID Tracking**: Only tracked requests with valid IDs are processed
604
+ - **Timeout Protection**: Requests automatically timeout to prevent hanging
605
+ - **Explicit targetOrigin**: Never uses `'*'` in postMessage calls
485
606
 
486
607
  ### Example Attack Scenario (Prevented)
487
608
 
@@ -491,7 +612,7 @@ Even if a malicious actor injects `?lc_host_override=https://evil.com`:
491
612
 
492
613
  // SDK configuration
493
614
  const learnCard = createPartnerConnect({
494
- hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
615
+ hostOrigin: ['https://learncard.app', 'https://staging.learncard.app'],
495
616
  });
496
617
 
497
618
  // What happens:
@@ -509,13 +630,13 @@ The SDK is written in TypeScript and includes comprehensive type definitions:
509
630
 
510
631
  ```typescript
511
632
  import type {
512
- PartnerConnectOptions,
513
- IdentityResponse,
514
- SendCredentialResponse,
515
- VerifiablePresentationRequest,
516
- CredentialSearchResponse,
517
- ConsentResponse,
518
- LearnCardError,
633
+ PartnerConnectOptions,
634
+ IdentityResponse,
635
+ SendCredentialResponse,
636
+ VerifiablePresentationRequest,
637
+ CredentialSearchResponse,
638
+ ConsentResponse,
639
+ LearnCardError,
519
640
  } from '@learncard/partner-connect';
520
641
  ```
521
642
 
@@ -530,5 +651,6 @@ Contributions are welcome! Please see the [main LearnCard repository](https://gi
530
651
  ## Support
531
652
 
532
653
  For issues and questions:
533
- - GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
534
- - Documentation: https://docs.learncard.com
654
+
655
+ - GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
656
+ - Documentation: https://docs.learncard.com