@learncard/create-http-bridge 1.1.251 → 1.1.254

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
@@ -15,10 +15,10 @@ https://docs.learncard.com
15
15
 
16
16
  ## Install
17
17
 
18
- - Clone this repo
19
- - Set up AWS CLI
20
- - Add a .env file exporting a wallet seed (e.g. `WALLET_SEED=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`)
21
- - Run `bun run serverless-deploy`
18
+ - Clone this repo
19
+ - Set up AWS CLI
20
+ - Add a .env file exporting a wallet seed (e.g. `WALLET_SEED=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa`)
21
+ - Run `bun run serverless-deploy`
22
22
 
23
23
  ## Local VC-API conformance testing
24
24
 
@@ -34,8 +34,12 @@ bun --cwd ../../../../packages/plugins/didkit run build
34
34
 
35
35
  Stop and restart any running bridge after rebuilding; it loads the DIDKit WASM during
36
36
  initialization.
37
- When intentionally rebuilding DIDKit, review the new binary and update
38
- `EXPECTED_DIDKIT_SHA256` in `scripts/sync-didkit.ts`. To test an unreviewed local artifact
37
+ When intentionally rebuilding DIDKit, review the new binary and run
38
+ `bun scripts/sync-didkit.ts --update-integrity` from this package. Commit the updated
39
+ `EXPECTED_DIDKIT_SHA256` guard together with the plugin artifact. The repository's
40
+ `build-wasm-from-submodules.sh` helper does this explicitly after generation, and the
41
+ WASM update automation includes the guard and artifacts in its PR. Ordinary builds only verify
42
+ the pin; they never update it. To test an unreviewed local artifact
39
43
  without changing that pin, set `LOCAL_DIDKIT_PATH` as described below.
40
44
 
41
45
  Start the LearnCard HTTP bridge in the first terminal:
package/dist/index.js CHANGED
@@ -416,7 +416,7 @@ const App = ({ name = "LearnCardHTTPBridge" }) => {
416
416
  };
417
417
 
418
418
  var name = "@learncard/create-http-bridge";
419
- var version = "1.1.251";
419
+ var version = "1.1.254";
420
420
  var description = "Instantly create and deploy a Learn Card Bridge HTTP API via AWS Lambda!";
421
421
  var main = "dist/index.js";
422
422
  var bin = "dist/index.js";
@@ -436,6 +436,7 @@ var dependencies = {
436
436
  commander: "^9.3.0",
437
437
  cors: "^2.8.5",
438
438
  express: "~4.21.0",
439
+ "express-rate-limit": "^7.1.5",
439
440
  figlet: "^1.5.2",
440
441
  immer: "^9.0.15",
441
442
  ink: "^3.2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learncard/create-http-bridge",
3
- "version": "1.1.251",
3
+ "version": "1.1.254",
4
4
  "description": "Instantly create and deploy a Learn Card Bridge HTTP API via AWS Lambda!",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
@@ -14,12 +14,13 @@
14
14
  "author": "Learning Economy Foundation (www.learningeconomy.io)",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@learncard/init": "2.4.12",
18
- "@learncard/types": "5.18.2",
17
+ "@learncard/init": "2.4.14",
18
+ "@learncard/types": "5.18.3",
19
19
  "@rollup/plugin-json": "^4.1.0",
20
20
  "commander": "^9.3.0",
21
21
  "cors": "^2.8.5",
22
22
  "express": "~4.21.0",
23
+ "express-rate-limit": "^7.1.5",
23
24
  "figlet": "^1.5.2",
24
25
  "immer": "^9.0.15",
25
26
  "ink": "^3.2.0",
@@ -1,20 +1,42 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { readFile, writeFile } from 'fs/promises';
3
3
 
4
- const EXPECTED_DIDKIT_SHA256 = '60efb6027699fda135db0d817f7a541bbd660ae88bec9cbec9796feeb3740b96';
4
+ const EXPECTED_DIDKIT_SHA256 = '38e9e39677846cfec7f0521cc23d707e2c9e87ae9981f988d78747f023bf64ee';
5
5
  const sourceUrl = new URL(
6
6
  '../../plugins/didkit/src/didkit/pkg/didkit_wasm_bg.wasm',
7
7
  import.meta.url
8
8
  );
9
9
  const destinationUrl = new URL('../src/didkit_wasm_bg.wasm', import.meta.url);
10
+ const args = process.argv.slice(2);
11
+ const updateIntegrity = args.length === 1 && args[0] === '--update-integrity';
12
+
13
+ if (args.length > 0 && !updateIntegrity) {
14
+ throw new Error('Usage: bun scripts/sync-didkit.ts [--update-integrity]');
15
+ }
16
+ if (updateIntegrity && process.env.LOCAL_DIDKIT_PATH) {
17
+ throw new Error('Unset LOCAL_DIDKIT_PATH before updating the canonical DIDKit integrity pin.');
18
+ }
19
+
10
20
  if (!process.env.LOCAL_DIDKIT_PATH) {
11
21
  const didkit = await readFile(sourceUrl);
12
22
  const actualHash = createHash('sha256').update(didkit).digest('hex');
13
23
 
14
- if (actualHash !== EXPECTED_DIDKIT_SHA256) {
24
+ if (updateIntegrity && actualHash !== EXPECTED_DIDKIT_SHA256) {
25
+ // Only intentional artifact generation/review may change the committed guard.
26
+ const scriptUrl = new URL(import.meta.url);
27
+ const script = await readFile(scriptUrl, 'utf8');
28
+ const declaration = `const EXPECTED_DIDKIT_SHA256 = '${EXPECTED_DIDKIT_SHA256}';`;
29
+ if (!script.includes(declaration)) {
30
+ throw new Error('Could not locate the DIDKit integrity pin to update.');
31
+ }
32
+ await writeFile(
33
+ scriptUrl,
34
+ script.replace(declaration, `const EXPECTED_DIDKIT_SHA256 = '${actualHash}';`)
35
+ );
36
+ } else if (actualHash !== EXPECTED_DIDKIT_SHA256) {
15
37
  throw new Error(
16
38
  `DIDKit WASM hash changed: expected ${EXPECTED_DIDKIT_SHA256}, received ${actualHash}. ` +
17
- 'Review the rebuilt artifact and update EXPECTED_DIDKIT_SHA256 intentionally.'
39
+ 'Review the rebuilt artifact, then run sync-didkit.ts --update-integrity intentionally.'
18
40
  );
19
41
  }
20
42
 
package/src/app.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import express from 'express';
2
2
  import cors from 'cors';
3
+ import rateLimit from 'express-rate-limit';
3
4
  import { VCValidator, VPValidator, type UnsignedVC, type VP } from '@learncard/types';
4
5
 
5
6
  import { TypedRequest } from './types.helpers';
@@ -15,6 +16,15 @@ import {
15
16
  } from './validators';
16
17
  import { getLearnCard } from './learn-card';
17
18
 
19
+ // Rate limiter for routes that perform expensive signing/verification operations
20
+ const authRateLimiter = rateLimit({
21
+ windowMs: 60 * 1000, // 1 minute window
22
+ limit: 100, // limit each IP to 100 requests per windowMs
23
+ message: 'Too many requests, please try again later.',
24
+ standardHeaders: true,
25
+ legacyHeaders: false,
26
+ });
27
+
18
28
  const router = express.Router();
19
29
 
20
30
  const app = express();
@@ -22,6 +32,15 @@ const app = express();
22
32
  const W3C_V1_CREDENTIALS_CONTEXT = 'https://www.w3.org/2018/credentials/v1';
23
33
  const W3C_ALT_V1_CREDENTIALS_CONTEXT = 'https://w3.org/2018/credentials/v1';
24
34
 
35
+ // Trust proxy for correct IP detection behind load balancers.
36
+ // Only enable when running behind a reverse proxy (nginx, AWS ALB, etc.)
37
+ // Without a real proxy, this allows clients to spoof X-Forwarded-For and bypass rate limits.
38
+ // Set TRUST_PROXY=1 (or number of proxy hops) when behind a load balancer.
39
+ if (process.env.TRUST_PROXY) {
40
+ const trustProxy = parseInt(process.env.TRUST_PROXY, 10);
41
+ app.set('trust proxy', isNaN(trustProxy) ? process.env.TRUST_PROXY : trustProxy);
42
+ }
43
+
25
44
  app.use(cors());
26
45
  app.use(express.json());
27
46
 
@@ -49,7 +68,7 @@ app.delete('/credentials/:id', async (_req: TypedRequest<{}>, res) => {
49
68
  res.sendStatus(501);
50
69
  });
51
70
 
52
- app.post('/credentials/issue', async (req: TypedRequest<IssueEndpoint>, res) => {
71
+ app.post('/credentials/issue', authRateLimiter, async (req: TypedRequest<IssueEndpoint>, res) => {
53
72
  try {
54
73
  const validationResult = await IssueEndpointValidator.spa(req.body);
55
74
 
@@ -99,52 +118,56 @@ app.post('/credentials/status', async (_req: TypedRequest<UpdateStatusEndpoint>,
99
118
  res.sendStatus(501);
100
119
  });
101
120
 
102
- app.post('/credentials/verify', async (req: TypedRequest<VerifyCredentialEndpoint>, res) => {
103
- try {
104
- const validationResult = await VerifyCredentialEndpointValidator.spa(req.body);
105
-
106
- if (!validationResult.success) {
107
- console.error(
108
- '[/credentials/verify] Validation error: ',
109
- validationResult.error.message,
110
- '(received: ',
111
- req.body,
112
- ')'
121
+ app.post(
122
+ '/credentials/verify',
123
+ authRateLimiter,
124
+ async (req: TypedRequest<VerifyCredentialEndpoint>, res) => {
125
+ try {
126
+ const validationResult = await VerifyCredentialEndpointValidator.spa(req.body);
127
+
128
+ if (!validationResult.success) {
129
+ console.error(
130
+ '[/credentials/verify] Validation error: ',
131
+ validationResult.error.message,
132
+ '(received: ',
133
+ req.body,
134
+ ')'
135
+ );
136
+ return res.status(400).json(`Invalid input: ${validationResult.error.message}`);
137
+ }
138
+
139
+ const validatedBody = validationResult.data;
140
+ const learnCard = await getLearnCard();
141
+
142
+ const verificationResult = await learnCard.invoke.verifyCredential(
143
+ validatedBody.verifiableCredential,
144
+ validatedBody.options
113
145
  );
114
- return res.status(400).json(`Invalid input: ${validationResult.error.message}`);
115
- }
116
-
117
- const validatedBody = validationResult.data;
118
- const learnCard = await getLearnCard();
119
-
120
- const verificationResult = await learnCard.invoke.verifyCredential(
121
- validatedBody.verifiableCredential,
122
- validatedBody.options
123
- );
124
146
 
125
- if (verificationResult.errors.length > 0) {
126
- console.error(
127
- '[/credentials/verify] Verification error(s): ',
128
- verificationResult.errors,
129
- '(received: ',
130
- req.body
131
- );
132
- return res.status(400).json(verificationResult);
147
+ if (verificationResult.errors.length > 0) {
148
+ console.error(
149
+ '[/credentials/verify] Verification error(s): ',
150
+ verificationResult.errors,
151
+ '(received: ',
152
+ req.body
153
+ );
154
+ return res.status(400).json(verificationResult);
155
+ }
156
+
157
+ return res.status(200).json(verificationResult);
158
+ } catch (error) {
159
+ console.error('[/credentials/verify] Caught error: ', error, '(received: ', req.body);
160
+ return res.status(400).json(`Invalid input: ${error}`);
133
161
  }
134
-
135
- return res.status(200).json(verificationResult);
136
- } catch (error) {
137
- console.error('[/credentials/verify] Caught error: ', error, '(received: ', req.body);
138
- return res.status(400).json(`Invalid input: ${error}`);
139
162
  }
140
- });
163
+ );
141
164
 
142
165
  app.post('/credentials/derive', async (_req: TypedRequest<{}>, res) => {
143
166
  res.sendStatus(501);
144
167
  });
145
168
 
146
169
  // This is non-standard! But very helpful for the VC-API plugin
147
- app.post('/presentations/issue', async (req: TypedRequest<IssueEndpoint>, res) => {
170
+ app.post('/presentations/issue', authRateLimiter, async (req: TypedRequest<IssueEndpoint>, res) => {
148
171
  try {
149
172
  const validationResult = await IssuePresentationEndpointValidator.spa(req.body);
150
173
 
@@ -174,47 +197,51 @@ app.post('/presentations/issue', async (req: TypedRequest<IssueEndpoint>, res) =
174
197
  }
175
198
  });
176
199
 
177
- app.post('/presentations/verify', async (req: TypedRequest<VerifyPresentationEndpoint>, res) => {
178
- try {
179
- const validationResult = await VerifyPresentationEndpointValidator.spa(req.body);
180
-
181
- if (!validationResult.success) {
182
- console.error(
183
- '[/presentations/verify] Validation error: ',
184
- validationResult.error.message,
185
- '(received: ',
186
- req.body,
187
- ')'
200
+ app.post(
201
+ '/presentations/verify',
202
+ authRateLimiter,
203
+ async (req: TypedRequest<VerifyPresentationEndpoint>, res) => {
204
+ try {
205
+ const validationResult = await VerifyPresentationEndpointValidator.spa(req.body);
206
+
207
+ if (!validationResult.success) {
208
+ console.error(
209
+ '[/presentations/verify] Validation error: ',
210
+ validationResult.error.message,
211
+ '(received: ',
212
+ req.body,
213
+ ')'
214
+ );
215
+ return res.status(400).json(`Invalid input: ${validationResult.error.message}`);
216
+ }
217
+
218
+ const validatedBody = validationResult.data;
219
+ const learnCard = await getLearnCard();
220
+
221
+ if ('presentation' in validatedBody) return res.sendStatus(501);
222
+
223
+ const verificationResult = await learnCard.invoke.verifyPresentation(
224
+ validatedBody.verifiablePresentation,
225
+ validatedBody.options
188
226
  );
189
- return res.status(400).json(`Invalid input: ${validationResult.error.message}`);
190
- }
191
-
192
- const validatedBody = validationResult.data;
193
- const learnCard = await getLearnCard();
194
-
195
- if ('presentation' in validatedBody) return res.sendStatus(501);
196
-
197
- const verificationResult = await learnCard.invoke.verifyPresentation(
198
- validatedBody.verifiablePresentation,
199
- validatedBody.options
200
- );
201
227
 
202
- if (verificationResult.errors.length > 0) {
203
- console.error(
204
- '[/presentations/verify] Verification error(s): ',
205
- verificationResult.errors,
206
- '(received: ',
207
- req.body
208
- );
209
- return res.status(400).json(verificationResult);
228
+ if (verificationResult.errors.length > 0) {
229
+ console.error(
230
+ '[/presentations/verify] Verification error(s): ',
231
+ verificationResult.errors,
232
+ '(received: ',
233
+ req.body
234
+ );
235
+ return res.status(400).json(verificationResult);
236
+ }
237
+
238
+ return res.status(200).json(verificationResult);
239
+ } catch (error) {
240
+ console.error('[/presentations/verify] Caught error: ', error, '(received: ', req.body);
241
+ return res.status(400).json(`Invalid input: ${error}`);
210
242
  }
211
-
212
- return res.status(200).json(verificationResult);
213
- } catch (error) {
214
- console.error('[/presentations/verify] Caught error: ', error, '(received: ', req.body);
215
- return res.status(400).json(`Invalid input: ${error}`);
216
243
  }
217
- });
244
+ );
218
245
 
219
246
  app.post('/presentations/prove', async (_req: TypedRequest<{}>, res) => {
220
247
  res.sendStatus(501);
@@ -232,74 +259,80 @@ app.delete('/presentations/:id', async (_req: TypedRequest<{}>, res) => {
232
259
  res.sendStatus(501);
233
260
  });
234
261
 
235
- app.post('/exchanges/:uri', async (req: TypedRequest<VP, { challenge?: string }>, res) => {
236
- try {
237
- const validationResult = await VPValidator.spa(req.body);
238
-
239
- if (!validationResult.success) {
240
- console.error(
241
- '[/exchanges/:uri] Validation error: ',
242
- validationResult.error.message,
243
- '(received: ',
244
- req.body,
245
- ')'
262
+ app.post(
263
+ '/exchanges/:uri',
264
+ authRateLimiter,
265
+ async (req: TypedRequest<VP, { challenge?: string }>, res) => {
266
+ try {
267
+ const validationResult = await VPValidator.spa(req.body);
268
+
269
+ if (!validationResult.success) {
270
+ console.error(
271
+ '[/exchanges/:uri] Validation error: ',
272
+ validationResult.error.message,
273
+ '(received: ',
274
+ req.body,
275
+ ')'
276
+ );
277
+ return res.status(400).json(`Invalid input: ${validationResult.error.message}`);
278
+ }
279
+
280
+ const validatedBody = validationResult.data;
281
+ const learnCard = await getLearnCard();
282
+ const challenge = Array.isArray(validatedBody.proof)
283
+ ? validatedBody.proof[0]?.challenge
284
+ : validatedBody.proof.challenge;
285
+
286
+ const verification = await learnCard.invoke.verifyPresentation(validatedBody, {
287
+ challenge,
288
+ proofPurpose: 'authentication',
289
+ });
290
+
291
+ if (verification.warnings.length > 0 || verification.errors.length > 0) {
292
+ console.error(
293
+ '[/exchanges/:uri] Validation error: ',
294
+ verification,
295
+ '(received: ',
296
+ req.body,
297
+ ')'
298
+ );
299
+ return res
300
+ .status(400)
301
+ .json({ reason: 'Invalid DID Auth VP', verificationResult: verification });
302
+ }
303
+
304
+ const subject = validatedBody.holder;
305
+
306
+ const credentialResult = VCValidator.safeParse(
307
+ await learnCard.read.get(req.params.uri)
246
308
  );
247
- return res.status(400).json(`Invalid input: ${validationResult.error.message}`);
248
- }
249
309
 
250
- const validatedBody = validationResult.data;
251
- const learnCard = await getLearnCard();
252
- const challenge = Array.isArray(validatedBody.proof)
253
- ? validatedBody.proof[0]?.challenge
254
- : validatedBody.proof.challenge;
310
+ if (!credentialResult.success) return res.status(404).json('Credential not found');
255
311
 
256
- const verification = await learnCard.invoke.verifyPresentation(validatedBody, {
257
- challenge,
258
- proofPurpose: 'authentication',
259
- });
312
+ const credential = credentialResult.data;
260
313
 
261
- if (verification.warnings.length > 0 || verification.errors.length > 0) {
262
- console.error(
263
- '[/exchanges/:uri] Validation error: ',
264
- verification,
265
- '(received: ',
266
- req.body,
267
- ')'
268
- );
269
- return res
270
- .status(400)
271
- .json({ reason: 'Invalid DID Auth VP', verificationResult: verification });
272
- }
273
-
274
- const subject = validatedBody.holder;
275
-
276
- const credentialResult = VCValidator.safeParse(await learnCard.read.get(req.params.uri));
314
+ credential.issuer = {
315
+ ...(typeof credential.issuer === 'string' ? {} : credential.issuer),
316
+ id: learnCard.id.did(),
317
+ };
318
+ credential.issuanceDate = new Date().toISOString();
277
319
 
278
- if (!credentialResult.success) return res.status(404).json('Credential not found');
320
+ if (!Array.isArray(credential.credentialSubject) && subject) {
321
+ credential.credentialSubject.id = subject;
322
+ }
279
323
 
280
- const credential = credentialResult.data;
324
+ const { proof, ...unsignedCredential } = credential;
325
+ void proof;
281
326
 
282
- credential.issuer = {
283
- ...(typeof credential.issuer === 'string' ? {} : credential.issuer),
284
- id: learnCard.id.did(),
285
- };
286
- credential.issuanceDate = new Date().toISOString();
327
+ const newVc = await learnCard.invoke.issueCredential(unsignedCredential);
287
328
 
288
- if (!Array.isArray(credential.credentialSubject) && subject) {
289
- credential.credentialSubject.id = subject;
329
+ return res.status(201).json(newVc);
330
+ } catch (error) {
331
+ console.error('[/exchanges/:uri] Caught error: ', error, '(received: ', req.body);
332
+ return res.status(400).json(`Invalid input: ${error}`);
290
333
  }
291
-
292
- const { proof, ...unsignedCredential } = credential;
293
- void proof;
294
-
295
- const newVc = await learnCard.invoke.issueCredential(unsignedCredential);
296
-
297
- return res.status(201).json(newVc);
298
- } catch (error) {
299
- console.error('[/exchanges/:uri] Caught error: ', error, '(received: ', req.body);
300
- return res.status(400).json(`Invalid input: ${error}`);
301
334
  }
302
- });
335
+ );
303
336
 
304
337
  app.post('/exchanges/:exchangeId/:transactionId', async (_req: TypedRequest<{}>, res) => {
305
338
  res.sendStatus(501);
Binary file
package/tsconfig.json CHANGED
@@ -1,4 +1,5 @@
1
1
  {
2
+ "extends": "../../tsconfig.library.json",
2
3
  "compilerOptions": {
3
4
  "lib": ["es2017"],
4
5
  "module": "esnext",