@learncard/partner-connect 0.2.9 โ†’ 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 runtime 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,28 +55,28 @@ try {
55
55
 
56
56
  ```typescript
57
57
  interface PartnerConnectOptions {
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;
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;
80
80
  }
81
81
  ```
82
82
 
@@ -85,16 +85,18 @@ interface PartnerConnectOptions {
85
85
  The SDK uses a hierarchical approach to determine the active host origin:
86
86
 
87
87
  #### 1. **Hardcoded Default** (Security Anchor)
88
+
88
89
  ```typescript
89
- PartnerConnect.DEFAULT_HOST_ORIGIN // 'https://learncard.app'
90
+ PartnerConnect.DEFAULT_HOST_ORIGIN; // 'https://learncard.app'
90
91
  ```
91
92
 
92
93
  #### 2. **Query Parameter Override** (Staging/Testing)
94
+
93
95
  ```typescript
94
96
  // Your app URL: https://partner-app.com/?lc_host_override=https://staging.learncard.app
95
97
 
96
98
  const learnCard = createPartnerConnect({
97
- hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
99
+ hostOrigin: ['https://learncard.app', 'https://staging.learncard.app'],
98
100
  });
99
101
  // Active origin: https://staging.learncard.app (from query param)
100
102
  // โœ… Only accepts messages from: https://staging.learncard.app
@@ -102,14 +104,16 @@ const learnCard = createPartnerConnect({
102
104
  ```
103
105
 
104
106
  **How the LearnCard Host Uses This:**
105
- - Production: Iframe URL has no `lc_host_override` parameter
106
- - Staging: Iframe URL includes `?lc_host_override=https://staging.learncard.app`
107
- - 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
108
111
 
109
112
  #### 3. **Configured Origin** (Fallback)
113
+
110
114
  ```typescript
111
115
  const learnCard = createPartnerConnect({
112
- hostOrigin: 'https://learncard.app'
116
+ hostOrigin: 'https://learncard.app',
113
117
  });
114
118
  // Active origin: https://learncard.app (configured)
115
119
  ```
@@ -120,11 +124,11 @@ When providing multiple origins, they serve as a **whitelist** for the `lc_host_
120
124
 
121
125
  ```typescript
122
126
  const learnCard = createPartnerConnect({
123
- hostOrigin: [
124
- 'https://learncard.app',
125
- 'https://staging.learncard.app',
126
- 'https://preview.learncard.app'
127
- ]
127
+ hostOrigin: [
128
+ 'https://learncard.app',
129
+ 'https://staging.learncard.app',
130
+ 'https://preview.learncard.app',
131
+ ],
128
132
  });
129
133
 
130
134
  // Scenario 1: No query param
@@ -143,15 +147,16 @@ const learnCard = createPartnerConnect({
143
147
  ### Security Model
144
148
 
145
149
  **STRICT Origin Validation:**
150
+
146
151
  ```
147
152
  Incoming Message Origin โ‰ก Configured Host Origin
148
153
  ```
149
154
 
150
155
  The SDK enforces an exact match between incoming message origins and the active host origin:
151
156
 
152
- - โœ… **Secure**: Even if a malicious actor adds `?lc_host_override=https://evil.com`, messages from `evil.com` will be rejected
153
- - โœ… **Cannot be spoofed**: Browser security prevents malicious sites from faking their `event.origin`
154
- - โœ… **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
155
160
 
156
161
  ```typescript
157
162
  // Active origin: https://staging.learncard.app
@@ -173,8 +178,9 @@ const identity = await learnCard.requestIdentity();
173
178
  ```
174
179
 
175
180
  **Error Codes:**
176
- - `LC_UNAUTHENTICATED`: User is not logged in to LearnCard
177
- - `LC_TIMEOUT`: Request timed out
181
+
182
+ - `LC_UNAUTHENTICATED`: User is not logged in to LearnCard
183
+ - `LC_TIMEOUT`: Request timed out
178
184
 
179
185
  ---
180
186
 
@@ -184,38 +190,136 @@ Send a verifiable credential to the user's LearnCard wallet.
184
190
 
185
191
  ```typescript
186
192
  const response = await learnCard.sendCredential({
187
- '@context': ['https://www.w3.org/2018/credentials/v1'],
188
- type: ['VerifiableCredential', 'AchievementCredential'],
189
- credentialSubject: {
190
- id: identity.user.did,
191
- achievement: {
192
- name: 'JavaScript Expert',
193
- description: 'Mastered advanced JavaScript concepts'
194
- }
195
- }
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
+ },
196
202
  });
197
203
 
198
204
  console.log('Credential ID:', response.credentialId);
199
205
  ```
200
206
 
201
- **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
+ ```
202
222
 
203
223
  ---
204
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
+
205
308
  ### `launchFeature(featurePath, initialPrompt?)`
206
309
 
207
310
  Launch a feature in the LearnCard host application.
208
311
 
209
312
  ```typescript
210
313
  await learnCard.launchFeature(
211
- '/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
212
- 'Explain the postMessage security model'
314
+ '/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
315
+ 'Explain the postMessage security model'
213
316
  );
214
317
  ```
215
318
 
216
319
  **Parameters:**
217
- - `featurePath`: Path to the feature
218
- - `initialPrompt`: Optional initial data or prompt
320
+
321
+ - `featurePath`: Path to the feature
322
+ - `initialPrompt`: Optional initial data or prompt
219
323
 
220
324
  ---
221
325
 
@@ -225,22 +329,22 @@ Request credentials from the user's wallet using a Verifiable Presentation Reque
225
329
 
226
330
  ```typescript
227
331
  const response = await learnCard.askCredentialSearch({
228
- query: [
229
- {
230
- type: 'QueryByTitle',
231
- credentialQuery: {
232
- reason: 'We need to verify your teamwork skills',
233
- title: 'Capstone'
234
- }
235
- }
236
- ],
237
- challenge: `challenge-${Date.now()}`,
238
- 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,
239
343
  });
240
344
 
241
345
  if (response.verifiablePresentation) {
242
- const credentials = response.verifiablePresentation.verifiableCredential;
243
- console.log(`Received ${credentials.length} credential(s)`);
346
+ const credentials = response.verifiablePresentation.verifiableCredential;
347
+ console.log(`Received ${credentials.length} credential(s)`);
244
348
  }
245
349
  ```
246
350
 
@@ -256,13 +360,14 @@ Request a specific credential by ID.
256
360
  const response = await learnCard.askCredentialSpecific('credential-id-123');
257
361
 
258
362
  if (response.credential) {
259
- console.log('Received credential:', response.credential);
363
+ console.log('Received credential:', response.credential);
260
364
  }
261
365
  ```
262
366
 
263
367
  **Error Codes:**
264
- - `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
265
- - `USER_REJECTED`: User declined to share
368
+
369
+ - `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
370
+ - `USER_REJECTED`: User declined to share
266
371
 
267
372
  ---
268
373
 
@@ -272,13 +377,13 @@ Request user consent for permissions.
272
377
 
273
378
  ```typescript
274
379
  const response = await learnCard.requestConsent(
275
- 'lc:network:network.learncard.com/trpc:contract:abc123'
380
+ 'lc:network:network.learncard.com/trpc:contract:abc123'
276
381
  );
277
382
 
278
383
  if (response.granted) {
279
- console.log('User granted consent');
384
+ console.log('User granted consent');
280
385
  } else {
281
- console.log('User denied consent');
386
+ console.log('User denied consent');
282
387
  }
283
388
  ```
284
389
 
@@ -292,18 +397,19 @@ Initiate a template-based credential issuance flow (e.g., Send Boost).
292
397
 
293
398
  ```typescript
294
399
  const response = await learnCard.initiateTemplateIssue(
295
- 'lc:network:network.learncard.com/trpc:boost:xyz789',
296
- ['did:key:z6Mkr...', 'did:key:z6Mks...']
400
+ 'lc:network:network.learncard.com/trpc:boost:xyz789',
401
+ ['did:key:z6Mkr...', 'did:key:z6Mks...']
297
402
  );
298
403
 
299
404
  if (response.issued) {
300
- console.log('Template issued successfully');
405
+ console.log('Template issued successfully');
301
406
  }
302
407
  ```
303
408
 
304
409
  **Error Codes:**
305
- - `UNAUTHORIZED`: Not an admin of this template
306
- - `TEMPLATE_NOT_FOUND`: Template doesn't exist
410
+
411
+ - `UNAUTHORIZED`: Not an admin of this template
412
+ - `TEMPLATE_NOT_FOUND`: Template doesn't exist
307
413
 
308
414
  ---
309
415
 
@@ -328,40 +434,43 @@ const PROTOCOL = 'LEARNCARD_V1';
328
434
  const pendingRequests = new Map();
329
435
 
330
436
  function sendPostMessage(action, payload = {}) {
331
- return new Promise((resolve, reject) => {
332
- const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
333
- pendingRequests.set(requestId, { resolve, reject });
334
-
335
- window.parent.postMessage({
336
- protocol: PROTOCOL,
337
- action,
338
- requestId,
339
- payload,
340
- }, LEARNCARD_HOST_ORIGIN);
341
-
342
- setTimeout(() => {
343
- if (pendingRequests.has(requestId)) {
344
- pendingRequests.delete(requestId);
345
- reject({ code: 'LC_TIMEOUT', message: 'Request timed out' });
346
- }
347
- }, 30000);
348
- });
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
+ });
349
458
  }
350
459
 
351
- window.addEventListener('message', (event) => {
352
- if (event.origin !== LEARNCARD_HOST_ORIGIN) return;
353
- const { protocol, requestId, type, data, error } = event.data;
354
- 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;
355
464
 
356
- const pending = pendingRequests.get(requestId);
357
- if (!pending) return;
465
+ const pending = pendingRequests.get(requestId);
466
+ if (!pending) return;
358
467
 
359
- pendingRequests.delete(requestId);
360
- if (type === 'SUCCESS') {
361
- pending.resolve(data);
362
- } else if (type === 'ERROR') {
363
- pending.reject(error);
364
- }
468
+ pendingRequests.delete(requestId);
469
+ if (type === 'SUCCESS') {
470
+ pending.resolve(data);
471
+ } else if (type === 'ERROR') {
472
+ pending.reject(error);
473
+ }
365
474
  });
366
475
 
367
476
  // Usage
@@ -375,7 +484,7 @@ import { createPartnerConnect } from '@learncard/partner-connect';
375
484
 
376
485
  // Clean, one-line setup
377
486
  const learnCard = createPartnerConnect({
378
- hostOrigin: 'https://learncard.app'
487
+ hostOrigin: 'https://learncard.app',
379
488
  });
380
489
 
381
490
  // Usage - same result, much cleaner
@@ -388,38 +497,39 @@ All methods return Promises that reject with a `LearnCardError` object:
388
497
 
389
498
  ```typescript
390
499
  interface LearnCardError {
391
- code: string;
392
- message: string;
500
+ code: string;
501
+ message: string;
393
502
  }
394
503
  ```
395
504
 
396
505
  **Common Error Codes:**
397
- - `LC_TIMEOUT`: Request timed out
398
- - `LC_UNAUTHENTICATED`: User not logged in
399
- - `USER_REJECTED`: User declined the request
400
- - `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
401
- - `UNAUTHORIZED`: User lacks permission
402
- - `TEMPLATE_NOT_FOUND`: Template doesn't exist
403
- - `SDK_NOT_INITIALIZED`: SDK initialization failed
404
- - `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
405
515
 
406
516
  **Example:**
407
517
 
408
518
  ```typescript
409
519
  try {
410
- const identity = await learnCard.requestIdentity();
411
- // Success
520
+ const identity = await learnCard.requestIdentity();
521
+ // Success
412
522
  } catch (error) {
413
- switch (error.code) {
414
- case 'LC_UNAUTHENTICATED':
415
- console.log('Please log in to your LearnCard account');
416
- break;
417
- case 'LC_TIMEOUT':
418
- console.log('Request timed out. Please try again.');
419
- break;
420
- default:
421
- console.error('An error occurred:', error.message);
422
- }
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
+ }
423
533
  }
424
534
  ```
425
535
 
@@ -456,9 +566,9 @@ const config = {
456
566
 
457
567
  ## Browser Support
458
568
 
459
- - Chrome/Edge 90+
460
- - Firefox 88+
461
- - Safari 14+
569
+ - Chrome/Edge 90+
570
+ - Firefox 88+
571
+ - Safari 14+
462
572
 
463
573
  Requires `postMessage` API and `Promise` support.
464
574
 
@@ -467,27 +577,32 @@ Requires `postMessage` API and `Promise` support.
467
577
  The SDK implements multiple security layers:
468
578
 
469
579
  ### 1. **Strict Origin Validation**
470
- - Messages must come from the **exact** active host origin
471
- - No wildcards, no pattern matching, no exceptions
472
- - 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`
473
584
 
474
585
  ### 2. **Query Parameter Whitelist**
475
- - `lc_host_override` values are validated against configured `hostOrigin` array
476
- - Invalid overrides are rejected and logged
477
- - 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
478
590
 
479
591
  ### 3. **Anti-Spoofing Protection**
592
+
480
593
  Even if a malicious actor injects `?lc_host_override=https://evil.com`:
481
- - The SDK may adopt `evil.com` as the active origin (if not whitelisted)
482
- - **BUT** messages from `evil.com` will only be accepted if `event.origin === 'evil.com'`
483
- - Browser security prevents `evil.com` from spoofing another domain's origin
484
- - 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
485
599
 
486
600
  ### 4. **Additional Security Layers**
487
- - **Protocol Validation**: Messages must match the expected protocol identifier
488
- - **Request ID Tracking**: Only tracked requests with valid IDs are processed
489
- - **Timeout Protection**: Requests automatically timeout to prevent hanging
490
- - **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
491
606
 
492
607
  ### Example Attack Scenario (Prevented)
493
608
 
@@ -497,7 +612,7 @@ Even if a malicious actor injects `?lc_host_override=https://evil.com`:
497
612
 
498
613
  // SDK configuration
499
614
  const learnCard = createPartnerConnect({
500
- hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
615
+ hostOrigin: ['https://learncard.app', 'https://staging.learncard.app'],
501
616
  });
502
617
 
503
618
  // What happens:
@@ -515,13 +630,13 @@ The SDK is written in TypeScript and includes comprehensive type definitions:
515
630
 
516
631
  ```typescript
517
632
  import type {
518
- PartnerConnectOptions,
519
- IdentityResponse,
520
- SendCredentialResponse,
521
- VerifiablePresentationRequest,
522
- CredentialSearchResponse,
523
- ConsentResponse,
524
- LearnCardError,
633
+ PartnerConnectOptions,
634
+ IdentityResponse,
635
+ SendCredentialResponse,
636
+ VerifiablePresentationRequest,
637
+ CredentialSearchResponse,
638
+ ConsentResponse,
639
+ LearnCardError,
525
640
  } from '@learncard/partner-connect';
526
641
  ```
527
642
 
@@ -536,5 +651,6 @@ Contributions are welcome! Please see the [main LearnCard repository](https://gi
536
651
  ## Support
537
652
 
538
653
  For issues and questions:
539
- - GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
540
- - Documentation: https://docs.learncard.com
654
+
655
+ - GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
656
+ - Documentation: https://docs.learncard.com