@learncard/partner-connect 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/dist/index.d.ts +383 -0
- package/dist/partner-connect.esm.js +324 -0
- package/dist/partner-connect.esm.js.map +1 -0
- package/dist/partner-connect.js +330 -0
- package/dist/partner-connect.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Learning Economy Foundation <sdk@learningeconomy.io>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
# @learncard/partner-connect
|
|
2
|
+
|
|
3
|
+
> Promise-based JavaScript SDK for managing cross-origin messaging between partner apps and LearnCard
|
|
4
|
+
|
|
5
|
+
The LearnCard Partner Connect SDK transforms complex `postMessage` communication into clean, modern Promise-based functions. It handles the entire cross-origin message lifecycle, including request queuing, message validation, and timeout management.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
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
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @learncard/partner-connect
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add @learncard/partner-connect
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add @learncard/partner-connect
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { createPartnerConnect } from '@learncard/partner-connect';
|
|
34
|
+
|
|
35
|
+
// Initialize the SDK
|
|
36
|
+
const learnCard = createPartnerConnect({
|
|
37
|
+
hostOrigin: 'https://learncard.app'
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Request user identity (SSO)
|
|
41
|
+
try {
|
|
42
|
+
const identity = await learnCard.requestIdentity();
|
|
43
|
+
console.log('User DID:', identity.user.did);
|
|
44
|
+
console.log('JWT Token:', identity.token);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error.code === 'LC_UNAUTHENTICATED') {
|
|
47
|
+
console.log('User is not logged in');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configuration
|
|
53
|
+
|
|
54
|
+
### Options
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
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;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Dynamic Origin Configuration
|
|
78
|
+
|
|
79
|
+
The SDK uses a hierarchical approach to determine the active host origin:
|
|
80
|
+
|
|
81
|
+
#### 1. **Hardcoded Default** (Security Anchor)
|
|
82
|
+
```typescript
|
|
83
|
+
PartnerConnect.DEFAULT_HOST_ORIGIN // 'https://learncard.app'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### 2. **Query Parameter Override** (Staging/Testing)
|
|
87
|
+
```typescript
|
|
88
|
+
// Your app URL: https://partner-app.com/?lc_host_override=https://staging.learncard.app
|
|
89
|
+
|
|
90
|
+
const learnCard = createPartnerConnect({
|
|
91
|
+
hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
|
|
92
|
+
});
|
|
93
|
+
// Active origin: https://staging.learncard.app (from query param)
|
|
94
|
+
// โ
Only accepts messages from: https://staging.learncard.app
|
|
95
|
+
// โ
Sends messages to: https://staging.learncard.app
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**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
|
|
102
|
+
|
|
103
|
+
#### 3. **Configured Origin** (Fallback)
|
|
104
|
+
```typescript
|
|
105
|
+
const learnCard = createPartnerConnect({
|
|
106
|
+
hostOrigin: 'https://learncard.app'
|
|
107
|
+
});
|
|
108
|
+
// Active origin: https://learncard.app (configured)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Origin Whitelist (Security Gate)
|
|
112
|
+
|
|
113
|
+
When providing multiple origins, they serve as a **whitelist** for the `lc_host_override` parameter:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const learnCard = createPartnerConnect({
|
|
117
|
+
hostOrigin: [
|
|
118
|
+
'https://learncard.app',
|
|
119
|
+
'https://staging.learncard.app',
|
|
120
|
+
'https://preview.learncard.app'
|
|
121
|
+
]
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Scenario 1: No query param
|
|
125
|
+
// โ Uses: https://learncard.app (first in array)
|
|
126
|
+
|
|
127
|
+
// Scenario 2: Valid override
|
|
128
|
+
// URL: ?lc_host_override=https://staging.learncard.app
|
|
129
|
+
// โ Uses: https://staging.learncard.app โ
|
|
130
|
+
|
|
131
|
+
// Scenario 3: Invalid override (not in whitelist)
|
|
132
|
+
// URL: ?lc_host_override=https://evil.com
|
|
133
|
+
// โ Uses: https://learncard.app (falls back to first) โ ๏ธ
|
|
134
|
+
// โ Logs warning about unauthorized override
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Security Model
|
|
138
|
+
|
|
139
|
+
**STRICT Origin Validation:**
|
|
140
|
+
```
|
|
141
|
+
Incoming Message Origin โก Configured Host Origin
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The SDK enforces an exact match between incoming message origins and the active host origin:
|
|
145
|
+
|
|
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
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// Active origin: https://staging.learncard.app
|
|
152
|
+
// โ
Accepts: messages from https://staging.learncard.app
|
|
153
|
+
// โ Rejects: messages from https://learncard.app
|
|
154
|
+
// โ Rejects: messages from https://evil.com
|
|
155
|
+
// โ Rejects: messages from any other origin
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## API Reference
|
|
159
|
+
|
|
160
|
+
### `requestIdentity()`
|
|
161
|
+
|
|
162
|
+
Request user identity information (Single Sign-On).
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
const identity = await learnCard.requestIdentity();
|
|
166
|
+
// Returns: { token: string, user: { did: string, ... } }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Error Codes:**
|
|
170
|
+
- `LC_UNAUTHENTICATED`: User is not logged in to LearnCard
|
|
171
|
+
- `LC_TIMEOUT`: Request timed out
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### `sendCredential(credential)`
|
|
176
|
+
|
|
177
|
+
Send a verifiable credential to the user's LearnCard wallet.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
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
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
console.log('Credential ID:', response.credentialId);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Returns:** `{ credentialId: string }`
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### `launchFeature(featurePath, initialPrompt?)`
|
|
200
|
+
|
|
201
|
+
Launch a feature in the LearnCard host application.
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
await learnCard.launchFeature(
|
|
205
|
+
'/ai/topics?shortCircuitStep=newTopic&selectedAppId=null',
|
|
206
|
+
'Explain the postMessage security model'
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Parameters:**
|
|
211
|
+
- `featurePath`: Path to the feature
|
|
212
|
+
- `initialPrompt`: Optional initial data or prompt
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### `askCredentialSearch(verifiablePresentationRequest)`
|
|
217
|
+
|
|
218
|
+
Request credentials from the user's wallet using a Verifiable Presentation Request.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
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
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (response.verifiablePresentation) {
|
|
236
|
+
const credentials = response.verifiablePresentation.verifiableCredential;
|
|
237
|
+
console.log(`Received ${credentials.length} credential(s)`);
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Returns:** `{ verifiablePresentation?: { verifiableCredential: unknown[], ... } }`
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### `askCredentialSpecific(credentialId)`
|
|
246
|
+
|
|
247
|
+
Request a specific credential by ID.
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
const response = await learnCard.askCredentialSpecific('credential-id-123');
|
|
251
|
+
|
|
252
|
+
if (response.credential) {
|
|
253
|
+
console.log('Received credential:', response.credential);
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Error Codes:**
|
|
258
|
+
- `CREDENTIAL_NOT_FOUND`: Credential doesn't exist
|
|
259
|
+
- `USER_REJECTED`: User declined to share
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### `requestConsent(contractUri)`
|
|
264
|
+
|
|
265
|
+
Request user consent for permissions.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const response = await learnCard.requestConsent(
|
|
269
|
+
'lc:network:network.learncard.com/trpc:contract:abc123'
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
if (response.granted) {
|
|
273
|
+
console.log('User granted consent');
|
|
274
|
+
} else {
|
|
275
|
+
console.log('User denied consent');
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Returns:** `{ granted: boolean }`
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
### `initiateTemplateIssue(templateId, draftRecipients?)`
|
|
284
|
+
|
|
285
|
+
Initiate a template-based credential issuance flow (e.g., Send Boost).
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const response = await learnCard.initiateTemplateIssue(
|
|
289
|
+
'lc:network:network.learncard.com/trpc:boost:xyz789',
|
|
290
|
+
['did:key:z6Mkr...', 'did:key:z6Mks...']
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (response.issued) {
|
|
294
|
+
console.log('Template issued successfully');
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**Error Codes:**
|
|
299
|
+
- `UNAUTHORIZED`: Not an admin of this template
|
|
300
|
+
- `TEMPLATE_NOT_FOUND`: Template doesn't exist
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
### `destroy()`
|
|
305
|
+
|
|
306
|
+
Clean up the SDK and remove event listeners. Call this when unmounting your component or closing your app.
|
|
307
|
+
|
|
308
|
+
```typescript
|
|
309
|
+
learnCard.destroy();
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Complete Example
|
|
313
|
+
|
|
314
|
+
Here's a complete example showing how to refactor a manual postMessage implementation to use the SDK:
|
|
315
|
+
|
|
316
|
+
### Before (Manual postMessage)
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// Manual setup - verbose and error-prone
|
|
320
|
+
const LEARNCARD_HOST_ORIGIN = 'https://learncard.app';
|
|
321
|
+
const PROTOCOL = 'LEARNCARD_V1';
|
|
322
|
+
const pendingRequests = new Map();
|
|
323
|
+
|
|
324
|
+
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
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
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;
|
|
349
|
+
|
|
350
|
+
const pending = pendingRequests.get(requestId);
|
|
351
|
+
if (!pending) return;
|
|
352
|
+
|
|
353
|
+
pendingRequests.delete(requestId);
|
|
354
|
+
if (type === 'SUCCESS') {
|
|
355
|
+
pending.resolve(data);
|
|
356
|
+
} else if (type === 'ERROR') {
|
|
357
|
+
pending.reject(error);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Usage
|
|
362
|
+
const identity = await sendPostMessage('REQUEST_IDENTITY');
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### After (With SDK)
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
import { createPartnerConnect } from '@learncard/partner-connect';
|
|
369
|
+
|
|
370
|
+
// Clean, one-line setup
|
|
371
|
+
const learnCard = createPartnerConnect({
|
|
372
|
+
hostOrigin: 'https://learncard.app'
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Usage - same result, much cleaner
|
|
376
|
+
const identity = await learnCard.requestIdentity();
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Error Handling
|
|
380
|
+
|
|
381
|
+
All methods return Promises that reject with a `LearnCardError` object:
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
interface LearnCardError {
|
|
385
|
+
code: string;
|
|
386
|
+
message: string;
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**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
|
|
399
|
+
|
|
400
|
+
**Example:**
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
try {
|
|
404
|
+
const identity = await learnCard.requestIdentity();
|
|
405
|
+
// Success
|
|
406
|
+
} 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
|
+
}
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Integration with Astro
|
|
421
|
+
|
|
422
|
+
```astro
|
|
423
|
+
---
|
|
424
|
+
// src/pages/index.astro
|
|
425
|
+
const config = {
|
|
426
|
+
learnCardHostOrigin: import.meta.env.PUBLIC_LEARNCARD_HOST || 'https://learncard.app'
|
|
427
|
+
};
|
|
428
|
+
---
|
|
429
|
+
|
|
430
|
+
<script>
|
|
431
|
+
import { createPartnerConnect } from '@learncard/partner-connect';
|
|
432
|
+
|
|
433
|
+
const config = window.__LC_CONFIG;
|
|
434
|
+
const learnCard = createPartnerConnect({
|
|
435
|
+
hostOrigin: config.learnCardHostOrigin
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
async function init() {
|
|
439
|
+
try {
|
|
440
|
+
const identity = await learnCard.requestIdentity();
|
|
441
|
+
console.log('Logged in as:', identity.user.did);
|
|
442
|
+
} catch (error) {
|
|
443
|
+
console.error('Not authenticated:', error);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
init();
|
|
448
|
+
</script>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Browser Support
|
|
452
|
+
|
|
453
|
+
- Chrome/Edge 90+
|
|
454
|
+
- Firefox 88+
|
|
455
|
+
- Safari 14+
|
|
456
|
+
|
|
457
|
+
Requires `postMessage` API and `Promise` support.
|
|
458
|
+
|
|
459
|
+
## Security
|
|
460
|
+
|
|
461
|
+
The SDK implements multiple security layers:
|
|
462
|
+
|
|
463
|
+
### 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`
|
|
467
|
+
|
|
468
|
+
### 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
|
|
472
|
+
|
|
473
|
+
### 3. **Anti-Spoofing Protection**
|
|
474
|
+
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
|
|
479
|
+
|
|
480
|
+
### 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
|
|
485
|
+
|
|
486
|
+
### Example Attack Scenario (Prevented)
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
// Attacker adds malicious query param
|
|
490
|
+
// URL: https://partner-app.com/?lc_host_override=https://evil.com
|
|
491
|
+
|
|
492
|
+
// SDK configuration
|
|
493
|
+
const learnCard = createPartnerConnect({
|
|
494
|
+
hostOrigin: ['https://learncard.app', 'https://staging.learncard.app']
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// What happens:
|
|
498
|
+
// 1. SDK detects lc_host_override=https://evil.com
|
|
499
|
+
// 2. Validates against whitelist: NOT FOUND
|
|
500
|
+
// 3. Falls back to: https://learncard.app
|
|
501
|
+
// 4. Sends messages to: https://learncard.app
|
|
502
|
+
// 5. Only accepts messages from: https://learncard.app
|
|
503
|
+
// 6. Attacker's messages from evil.com: REJECTED โ
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
## TypeScript
|
|
507
|
+
|
|
508
|
+
The SDK is written in TypeScript and includes comprehensive type definitions:
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import type {
|
|
512
|
+
PartnerConnectOptions,
|
|
513
|
+
IdentityResponse,
|
|
514
|
+
SendCredentialResponse,
|
|
515
|
+
VerifiablePresentationRequest,
|
|
516
|
+
CredentialSearchResponse,
|
|
517
|
+
ConsentResponse,
|
|
518
|
+
LearnCardError,
|
|
519
|
+
} from '@learncard/partner-connect';
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## License
|
|
523
|
+
|
|
524
|
+
MIT
|
|
525
|
+
|
|
526
|
+
## Contributing
|
|
527
|
+
|
|
528
|
+
Contributions are welcome! Please see the [main LearnCard repository](https://github.com/learningeconomy/LearnCard) for contribution guidelines.
|
|
529
|
+
|
|
530
|
+
## Support
|
|
531
|
+
|
|
532
|
+
For issues and questions:
|
|
533
|
+
- GitHub Issues: https://github.com/learningeconomy/LearnCard/issues
|
|
534
|
+
- Documentation: https://docs.learncard.com
|