@learncard/partner-connect 0.2.9 โ 0.2.11
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 -169
- package/dist/index.d.ts +145 -5
- 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,28 +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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
-
|
|
107
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
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
|
-
|
|
177
|
-
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
212
|
-
|
|
314
|
+
'/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
|
|
315
|
+
'Explain the postMessage security model'
|
|
213
316
|
);
|
|
214
317
|
```
|
|
215
318
|
|
|
216
319
|
**Parameters:**
|
|
217
|
-
|
|
218
|
-
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
-
|
|
363
|
+
console.log('Received credential:', response.credential);
|
|
260
364
|
}
|
|
261
365
|
```
|
|
262
366
|
|
|
263
367
|
**Error Codes:**
|
|
264
|
-
|
|
265
|
-
-
|
|
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
|
-
|
|
380
|
+
'lc:network:network.learncard.com/trpc:contract:abc123'
|
|
276
381
|
);
|
|
277
382
|
|
|
278
383
|
if (response.granted) {
|
|
279
|
-
|
|
384
|
+
console.log('User granted consent');
|
|
280
385
|
} else {
|
|
281
|
-
|
|
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
|
-
|
|
296
|
-
|
|
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
|
-
|
|
405
|
+
console.log('Template issued successfully');
|
|
301
406
|
}
|
|
302
407
|
```
|
|
303
408
|
|
|
304
409
|
**Error Codes:**
|
|
305
|
-
|
|
306
|
-
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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',
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
465
|
+
const pending = pendingRequests.get(requestId);
|
|
466
|
+
if (!pending) return;
|
|
358
467
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
500
|
+
code: string;
|
|
501
|
+
message: string;
|
|
393
502
|
}
|
|
394
503
|
```
|
|
395
504
|
|
|
396
505
|
**Common Error Codes:**
|
|
397
|
-
|
|
398
|
-
-
|
|
399
|
-
-
|
|
400
|
-
-
|
|
401
|
-
-
|
|
402
|
-
-
|
|
403
|
-
-
|
|
404
|
-
-
|
|
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
|
-
|
|
411
|
-
|
|
520
|
+
const identity = await learnCard.requestIdentity();
|
|
521
|
+
// Success
|
|
412
522
|
} catch (error) {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
-
|
|
460
|
-
-
|
|
461
|
-
-
|
|
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
|
-
|
|
471
|
-
-
|
|
472
|
-
-
|
|
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
|
-
|
|
476
|
-
-
|
|
477
|
-
-
|
|
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
|
-
|
|
482
|
-
-
|
|
483
|
-
-
|
|
484
|
-
-
|
|
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
|
-
|
|
488
|
-
-
|
|
489
|
-
-
|
|
490
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
540
|
-
-
|
|
654
|
+
|
|
655
|
+
- GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
|
|
656
|
+
- Documentation: https://docs.learncard.com
|