@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 +285 -163
- package/dist/index.d.ts +146 -6
- package/dist/partner-connect.esm.js +75 -1
- package/dist/partner-connect.esm.js.map +1 -1
- package/dist/partner-connect.js +75 -1
- package/dist/partner-connect.js.map +1 -1
- package/package.json +2 -2
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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
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
|
-
|
|
37
|
+
hostOrigin: 'https://learncard.app',
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
// Request user identity (SSO)
|
|
41
41
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
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
|
-
|
|
171
|
-
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
206
|
-
|
|
314
|
+
'/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
|
|
315
|
+
'Explain the postMessage security model'
|
|
207
316
|
);
|
|
208
317
|
```
|
|
209
318
|
|
|
210
319
|
**Parameters:**
|
|
211
|
-
|
|
212
|
-
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
363
|
+
console.log('Received credential:', response.credential);
|
|
254
364
|
}
|
|
255
365
|
```
|
|
256
366
|
|
|
257
367
|
**Error Codes:**
|
|
258
|
-
|
|
259
|
-
-
|
|
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
|
-
|
|
380
|
+
'lc:network:network.learncard.com/trpc:contract:abc123'
|
|
270
381
|
);
|
|
271
382
|
|
|
272
383
|
if (response.granted) {
|
|
273
|
-
|
|
384
|
+
console.log('User granted consent');
|
|
274
385
|
} else {
|
|
275
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
405
|
+
console.log('Template issued successfully');
|
|
295
406
|
}
|
|
296
407
|
```
|
|
297
408
|
|
|
298
409
|
**Error Codes:**
|
|
299
|
-
|
|
300
|
-
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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',
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
351
|
-
|
|
465
|
+
const pending = pendingRequests.get(requestId);
|
|
466
|
+
if (!pending) return;
|
|
352
467
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
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
|
-
|
|
386
|
-
|
|
500
|
+
code: string;
|
|
501
|
+
message: string;
|
|
387
502
|
}
|
|
388
503
|
```
|
|
389
504
|
|
|
390
505
|
**Common Error Codes:**
|
|
391
|
-
|
|
392
|
-
-
|
|
393
|
-
-
|
|
394
|
-
-
|
|
395
|
-
-
|
|
396
|
-
-
|
|
397
|
-
-
|
|
398
|
-
-
|
|
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
|
-
|
|
405
|
-
|
|
520
|
+
const identity = await learnCard.requestIdentity();
|
|
521
|
+
// Success
|
|
406
522
|
} catch (error) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
-
|
|
454
|
-
-
|
|
455
|
-
-
|
|
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
|
-
|
|
465
|
-
-
|
|
466
|
-
-
|
|
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
|
-
|
|
470
|
-
-
|
|
471
|
-
-
|
|
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
|
-
|
|
476
|
-
-
|
|
477
|
-
-
|
|
478
|
-
-
|
|
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
|
-
|
|
482
|
-
-
|
|
483
|
-
-
|
|
484
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
-
|
|
534
|
-
-
|
|
654
|
+
|
|
655
|
+
- GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
|
|
656
|
+
- Documentation: https://docs.learncard.com
|