@learncard/cli 3.6.8 → 4.0.1

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +36 -12
  3. package/dist/index.js +6673 -2038
  4. package/package.json +27 -15
  5. package/project.json +0 -42
  6. package/rollup.config.js +0 -34
  7. package/scripts/embed-snippets.ts +0 -47
  8. package/src/auth-grant.test.ts +0 -54
  9. package/src/auth-grant.ts +0 -34
  10. package/src/clr/validate.test.ts +0 -65
  11. package/src/clr/validate.ts +0 -242
  12. package/src/clr.ts +0 -119
  13. package/src/consent-contract.ts +0 -116
  14. package/src/demo-inbox-refresh.test.ts +0 -737
  15. package/src/demo-inbox-refresh.ts +0 -804
  16. package/src/demo-refresh-command.test.ts +0 -57
  17. package/src/demo-refresh-command.ts +0 -22
  18. package/src/demo-refresh-ui.test.ts +0 -66
  19. package/src/demo-refresh-ui.ts +0 -65
  20. package/src/demo-refresh.test.ts +0 -140
  21. package/src/demo-refresh.ts +0 -309
  22. package/src/doctor/checks.test.ts +0 -448
  23. package/src/doctor/checks.ts +0 -497
  24. package/src/doctor.test.ts +0 -67
  25. package/src/doctor.ts +0 -118
  26. package/src/embed.ts +0 -102
  27. package/src/inbox.test.ts +0 -257
  28. package/src/inbox.ts +0 -221
  29. package/src/index.tsx +0 -876
  30. package/src/init.ts +0 -26
  31. package/src/open.test.ts +0 -40
  32. package/src/open.ts +0 -186
  33. package/src/org/apply.test.ts +0 -1108
  34. package/src/org/apply.ts +0 -924
  35. package/src/org/branding.test.ts +0 -60
  36. package/src/org/diff.ts +0 -14
  37. package/src/org/load.ts +0 -50
  38. package/src/org/schema.test.ts +0 -256
  39. package/src/org/schema.ts +0 -216
  40. package/src/org.ts +0 -124
  41. package/src/out.test.ts +0 -40
  42. package/src/out.ts +0 -16
  43. package/src/phase-two.test.ts +0 -154
  44. package/src/project.test.ts +0 -232
  45. package/src/project.ts +0 -489
  46. package/src/promote.test.ts +0 -142
  47. package/src/promote.ts +0 -202
  48. package/src/random.ts +0 -3
  49. package/src/refresh.test.ts +0 -86
  50. package/src/refresh.ts +0 -93
  51. package/src/replHelpers.test.ts +0 -80
  52. package/src/replHelpers.ts +0 -45
  53. package/src/revoke.test.ts +0 -29
  54. package/src/revoke.ts +0 -65
  55. package/src/send-template.test.ts +0 -20
  56. package/src/send.test.ts +0 -321
  57. package/src/send.ts +0 -373
  58. package/src/setup-signing.test.ts +0 -62
  59. package/src/setup-signing.ts +0 -185
  60. package/src/snippet-files.ts +0 -16
  61. package/src/status.test.ts +0 -33
  62. package/src/status.ts +0 -94
  63. package/src/token.test.ts +0 -28
  64. package/src/token.ts +0 -139
  65. package/src/types.d.ts +0 -17
  66. package/src/verify.test.ts +0 -35
  67. package/src/verify.ts +0 -66
  68. package/src/webhook.ts +0 -247
  69. package/src/whoami.test.ts +0 -67
  70. package/src/whoami.ts +0 -129
  71. package/tsconfig.json +0 -38
  72. package/vitest.config.ts +0 -8
package/src/index.tsx DELETED
@@ -1,876 +0,0 @@
1
- import fs from 'fs/promises';
2
- import dns from 'node:dns';
3
- import { createInterface } from 'node:readline/promises';
4
- import { Writable } from 'node:stream';
5
- import { inspect } from 'node:util';
6
-
7
- import { getTestCache } from '@learncard/core';
8
- import { initLearnCard, emptyLearnCard, learnCardFromSeed } from '@learncard/init';
9
- import { getLCAPlugin } from '@learncard/lca-api-plugin';
10
- import { openBadgeV2Plugin } from '@learncard/open-badge-v2-plugin';
11
- import * as types from '@learncard/types';
12
- import { getLinkedClaimsPlugin } from '@learncard/linked-claims-plugin';
13
- import gradient from 'gradient-string';
14
- import figlet from 'figlet';
15
- import { Option, program } from 'commander';
16
- import clipboard from 'clipboardy';
17
-
18
- import { getLerRsPlugin } from '@learncard/ler-rs-plugin';
19
- import { getRenderMethodPlugin } from '@learncard/render-method-plugin';
20
-
21
- import { generateRandomSeed } from './random';
22
- import { runSend } from './send';
23
- import { out } from './out';
24
- import {
25
- createLearnCardBundle,
26
- exportLearnCardBundle as writeLearnCardBundle,
27
- importLearnCardBundle,
28
- readLearnCardBundle,
29
- restoreLearnCardFromBundle as restoreBundle,
30
- } from '@learncard/holder-continuity';
31
- import {
32
- createExportLearnCardBundleHelper,
33
- createRestoreLearnCardFromBundleHelper,
34
- } from './replHelpers';
35
- import { registerOrgCommand } from './org';
36
- import { registerDoctorCommand } from './doctor';
37
- import { registerClrCommand } from './clr';
38
- import { registerInboxCommand } from './inbox';
39
- import { registerRefreshCommand } from './refresh';
40
- import { registerWhoamiCommand } from './whoami';
41
- import { registerPromoteCommand } from './promote';
42
-
43
- import packageJson from '../package.json';
44
-
45
- dns.setDefaultResultOrder('ipv4first');
46
-
47
- type CliGlobals = {
48
- seed: string;
49
- generateRandomSeed: typeof generateRandomSeed;
50
- emptyLearnCard: typeof emptyLearnCard;
51
- learnCardFromSeed: typeof learnCardFromSeed;
52
- initLearnCard: typeof initLearnCard;
53
- learnCard: any;
54
- types: typeof types;
55
- getTestCache: typeof getTestCache;
56
- copy: typeof copyFunction;
57
- getLearnCardBundlePassword: typeof getLearnCardBundlePassword;
58
- exportLearnCardBundle: ReturnType<typeof createExportLearnCardBundleHelper>;
59
- importLearnCardBundle: typeof importLearnCardBundle;
60
- createLearnCardBundle: typeof createLearnCardBundle;
61
- readLearnCardBundle: typeof readLearnCardBundle;
62
- restoreLearnCardFromBundle: ReturnType<typeof createRestoreLearnCardFromBundleHelper>;
63
- };
64
-
65
- const cliGlobals = globalThis as typeof globalThis & CliGlobals;
66
-
67
- type ReplCompletion = [string[], string];
68
-
69
- const replGlobals = [
70
- 'learnCard',
71
- 'emptyLearnCard',
72
- 'learnCardFromSeed',
73
- 'initLearnCard',
74
- 'seed',
75
- 'generateRandomSeed',
76
- 'types',
77
- 'getTestCache',
78
- 'copy',
79
- 'getLearnCardBundlePassword',
80
- 'exportLearnCardBundle',
81
- 'importLearnCardBundle',
82
- 'createLearnCardBundle',
83
- 'readLearnCardBundle',
84
- 'restoreLearnCardFromBundle',
85
- ];
86
-
87
- const replCommands = ['.exit'];
88
-
89
- const identifierPattern = /^[A-Za-z_$][\w$]*$/;
90
-
91
- const colorizeName = (name: string): string => {
92
- const colorizedName = gradient(['cyan', 'green'])(name);
93
-
94
- if (colorizedName !== name || !process.stdout.isTTY) return colorizedName;
95
-
96
- return `\x1b[36m${name}\x1b[39m`;
97
- };
98
-
99
- const g = {
100
- learnCard: colorizeName('learnCard'),
101
- emptyLearnCard: colorizeName('emptyLearnCard'),
102
- learnCardFromSeed: colorizeName('learnCardFromSeed'),
103
- initLearnCard: colorizeName('initLearnCard'),
104
- seed: colorizeName('seed'),
105
- generateRandomSeed: colorizeName('generateRandomSeed'),
106
- types: colorizeName('types'),
107
- getTestCache: colorizeName('getTestCache'),
108
- getLearnCardBundlePassword: colorizeName('getLearnCardBundlePassword'),
109
- copy: colorizeName('copy'),
110
- exportLearnCardBundle: colorizeName('exportLearnCardBundle'),
111
- importLearnCardBundle: colorizeName('importLearnCardBundle'),
112
- createLearnCardBundle: colorizeName('createLearnCardBundle'),
113
- readLearnCardBundle: colorizeName('readLearnCardBundle'),
114
- restoreLearnCardFromBundle: colorizeName('restoreLearnCardFromBundle'),
115
- };
116
-
117
- const colorizeReplInput = (input: string): string => {
118
- return [...replGlobals]
119
- .sort((a, b) => b.length - a.length)
120
- .reduce((output, name) => {
121
- const colorizedName = g[name as keyof typeof g];
122
-
123
- if (!colorizedName) return output;
124
-
125
- return output.replace(new RegExp(`\\b${name}\\b`, 'g'), colorizedName);
126
- }, input);
127
- };
128
-
129
- const copyFunction = (text: string | object | number) => {
130
- if (typeof text === 'object') {
131
- text = JSON.stringify(text);
132
- }
133
- if (typeof text === 'number') {
134
- text = text.toString();
135
- }
136
- try {
137
- clipboard.writeSync(text);
138
- console.log('Copied to clipboard!');
139
- } catch (error) {
140
- console.error(
141
- 'Failed to copy to clipboard:',
142
- error instanceof Error ? error.message : 'Unknown error'
143
- );
144
- }
145
- };
146
-
147
- const getLearnCardBundlePassword = async (prompt = 'Bundle password: '): Promise<string> => {
148
- process.stdout.write(prompt);
149
-
150
- const mutedOutput = new Writable({
151
- write(_chunk, _encoding, callback) {
152
- callback();
153
- },
154
- });
155
- const rl = createInterface({ input: process.stdin, output: mutedOutput, terminal: true });
156
-
157
- try {
158
- return await rl.question('');
159
- } finally {
160
- rl.close();
161
- process.stdout.write('\n');
162
- }
163
- };
164
-
165
- const globalAssignmentPattern = /^(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([\s\S]*?);?\s*$/;
166
-
167
- const parseGlobalAssignment = (
168
- source: string
169
- ): { name: string; initializer: string } | undefined => {
170
- const match = source.match(globalAssignmentPattern);
171
-
172
- if (!match) return;
173
-
174
- const name = match[1];
175
- const initializer = match[2];
176
-
177
- if (!name || !initializer) return;
178
-
179
- return { name, initializer };
180
- };
181
-
182
- const getCurrentToken = (line: string): string => {
183
- return line.match(/(?:[A-Za-z_$][\w$]*\.)*[A-Za-z_$][\w$]*\.?$/)?.[0] ?? '';
184
- };
185
-
186
- const getCompletionBase = (token: string): string | undefined => {
187
- const lastDotIndex = token.lastIndexOf('.');
188
-
189
- if (lastDotIndex === -1) return;
190
-
191
- return token.slice(0, lastDotIndex);
192
- };
193
-
194
- const getCompletionPrefix = (token: string): string => {
195
- const lastDotIndex = token.lastIndexOf('.');
196
-
197
- if (lastDotIndex === -1) return token;
198
-
199
- return token.slice(lastDotIndex + 1);
200
- };
201
-
202
- const getOwnPropertyNames = (value: unknown): string[] => {
203
- if (value === null || value === undefined) return [];
204
-
205
- const names = new Set<string>();
206
-
207
- let current: unknown = Object(value);
208
-
209
- while (current && current !== Object.prototype) {
210
- for (const name of Object.getOwnPropertyNames(current)) names.add(name);
211
-
212
- current = Object.getPrototypeOf(current);
213
- }
214
-
215
- return [...names];
216
- };
217
-
218
- const completeBunReplInput = (line: string): ReplCompletion => {
219
- const token = getCurrentToken(line);
220
-
221
- if (!token) return [replCommands, ''];
222
-
223
- const base = getCompletionBase(token);
224
- const prefix = getCompletionPrefix(token);
225
-
226
- if (!base) {
227
- const completions = [...replCommands, ...replGlobals].filter(name =>
228
- name.startsWith(prefix)
229
- );
230
-
231
- return [completions.length ? completions : replGlobals, token];
232
- }
233
-
234
- try {
235
- const target = globalThis.eval(base) as unknown;
236
- const completions = getOwnPropertyNames(target)
237
- .filter(name => identifierPattern.test(name))
238
- .filter(name => name.startsWith(prefix))
239
- .map(name => `${base}.${name}`);
240
-
241
- return [completions, token];
242
- } catch {
243
- return [[], token];
244
- }
245
- };
246
-
247
- const evaluateBunReplInput = async (source: string): Promise<unknown> => {
248
- const trimmedSource = source.trim();
249
- const expressionSource = trimmedSource.replace(/;+\s*$/, '');
250
- const statements = trimmedSource
251
- .split(';')
252
- .map(statement => statement.trim())
253
- .filter(Boolean);
254
-
255
- try {
256
- return await globalThis.eval(`(async () => (${expressionSource}))()`);
257
- } catch {
258
- if (statements.length > 1) {
259
- const lastStatement = statements[statements.length - 1];
260
-
261
- if (lastStatement && !/^(?:const|let|var)\b/.test(lastStatement)) {
262
- const body = statements.slice(0, -1).join(';\n');
263
- const wrappedSource = `${body ? `${body};\n` : ''}return (${lastStatement});`;
264
-
265
- return await globalThis.eval(`(async () => { ${wrappedSource} })()`);
266
- }
267
- }
268
-
269
- const globalAssignment = parseGlobalAssignment(trimmedSource);
270
-
271
- if (globalAssignment) {
272
- const value = await globalThis.eval(
273
- `(async () => (${globalAssignment.initializer}))()`
274
- );
275
- (globalThis as Record<string, unknown>)[globalAssignment.name] = value;
276
- return value;
277
- }
278
-
279
- return await globalThis.eval(`(async () => { ${trimmedSource} })()`);
280
- }
281
- };
282
-
283
- const startReadlineRepl = async (colorize?: (input: string) => string): Promise<void> => {
284
- const rl = createInterface({
285
- input: process.stdin,
286
- output: process.stdout,
287
- prompt: colorize ? colorize('> ') : '> ',
288
- completer: completeBunReplInput,
289
- });
290
-
291
- rl.prompt();
292
-
293
- for await (const line of rl) {
294
- const source = line.trim();
295
-
296
- if (source === '.exit') break;
297
-
298
- if (!source) {
299
- rl.prompt();
300
-
301
- continue;
302
- }
303
-
304
- try {
305
- const result = await evaluateBunReplInput(source);
306
-
307
- process.stdout.write(
308
- `${inspect(result, { depth: 6, colors: process.stdout.isTTY })}\n`
309
- );
310
- } catch (error) {
311
- process.stdout.write(
312
- `${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`
313
- );
314
- }
315
-
316
- rl.prompt();
317
- }
318
-
319
- rl.close();
320
- };
321
-
322
- const startBunRepl = async (colorize: (input: string) => string): Promise<void> => {
323
- await startReadlineRepl(colorize);
324
- };
325
-
326
- const startCliRepl = async (colorize: (input: string) => string): Promise<void> => {
327
- if (!('Bun' in globalThis)) {
328
- const repl = await import('pretty-repl');
329
-
330
- repl.default.start();
331
-
332
- return;
333
- }
334
-
335
- await startBunRepl(colorize);
336
- };
337
-
338
- program
339
- .command('send [recipient]')
340
- .description(
341
- 'Send a "Quickstart Complete" badge to an email, phone number, profile ID, or DID (prompts if omitted). Creates .env and send.mjs in the current folder.'
342
- )
343
- .option('-y, --yes', 'accept defaults without prompting')
344
- .option('--name <displayName>', 'display name for your issuer profile')
345
- .option('--badge <name>', 'name of the badge to send (default: "Quickstart Complete")')
346
- .option('--description <text>', 'badge description')
347
- .option(
348
- '--profile-id <id>',
349
- 'public handle for your profile (default: derived from the display name)'
350
- )
351
- .option('--network <url>', 'network tRPC URL (default: production)')
352
- .addOption(
353
- new Option(
354
- '--as <profileId>',
355
- 'send as a profile you manage (from `org apply`), signed with its did:web'
356
- ).env('LEARNCARD_AS')
357
- )
358
- .option(
359
- '--template',
360
- 'send using a reusable template and hosted signing authority (default once setup-signing or org apply has run)'
361
- )
362
- .option('--no-template', 'sign with the local key even if a signing authority is registered')
363
- .option('--template-uri <uri>', 'send from a specific template (implies --template)')
364
- .option('--webhook-url <url>', 'receive ISSUANCE_DELIVERED / ISSUANCE_CLAIMED at this URL')
365
- .option('--suppress-delivery', 'skip the claim email; you deliver inbox.claimUrl yourself')
366
- .option(
367
- '--guardian-email <email>',
368
- "require a guardian's approval before the recipient can claim"
369
- )
370
- .option('--json', 'print a single JSON result on stdout')
371
- .action(
372
- async (
373
- recipient: string | undefined,
374
- opts: {
375
- yes?: boolean;
376
- name?: string;
377
- badge?: string;
378
- description?: string;
379
- profileId?: string;
380
- network?: string;
381
- template?: boolean;
382
- json?: boolean;
383
- }
384
- ) => {
385
- out.json = !!opts.json;
386
- out.result = {};
387
- const didkit = fs.readFile(
388
- require.resolve('@learncard/didkit-plugin/dist/didkit/didkit_wasm_bg.wasm')
389
- );
390
- try {
391
- await runSend(recipient, { ...opts, didkit });
392
- if (out.json) {
393
- process.stdout.write(
394
- JSON.stringify({ ok: true, command: 'send', ...out.result }) + '\n'
395
- );
396
- }
397
- process.exit(0);
398
- } catch (error) {
399
- const message = error instanceof Error ? error.message : String(error);
400
- const firstLine = message.split('\n')[0]!;
401
- if (out.json) {
402
- process.stdout.write(
403
- JSON.stringify({ ok: false, command: 'send', error: firstLine }) + '\n'
404
- );
405
- process.exit(1);
406
- }
407
- console.error(`\n${firstLine}`);
408
- // Input mistakes explain themselves; keep the docs link for network/auth failures.
409
- if (
410
- !/is not an email address|is a placeholder address|A recipient is required/.test(
411
- firstLine
412
- )
413
- )
414
- console.error(
415
- 'Troubleshooting: https://docs.learncard.com/start-here/your-first-integration#if-something-goes-wrong'
416
- );
417
- process.exit(1);
418
- }
419
- }
420
- );
421
-
422
- const commandOptions = (command: ReturnType<typeof program.command>) =>
423
- command
424
- .option('-y, --yes', 'accept defaults without prompting')
425
- .option('--profile-id <id>', 'public handle for your issuer profile')
426
- .option('--network <url>', 'network tRPC URL or staging (default: production)')
427
- .option('--json', 'print a single JSON result on stdout');
428
-
429
- /**
430
- * Shared runner for every subcommand except `send` (which has its own didkit/error
431
- * formatting). Sets up `out` for this run and, in `--json` mode, prints exactly one
432
- * envelope on stdout instead of the human-mode stderr message. Pass `wrap: false` for
433
- * commands (only `verify` today) that already print their own `--json` output.
434
- */
435
- const runCommand = async (
436
- command: string,
437
- options: { json?: boolean },
438
- action: (didkit: Promise<Buffer>) => Promise<void>,
439
- wrap: boolean = true
440
- ) => {
441
- out.json = !!options.json;
442
- out.result = {};
443
- try {
444
- await action(
445
- fs.readFile(require.resolve('@learncard/didkit-plugin/dist/didkit/didkit_wasm_bg.wasm'))
446
- );
447
- if (out.json && wrap) {
448
- process.stdout.write(JSON.stringify({ ok: true, command, ...out.result }) + '\n');
449
- }
450
- process.exit(process.exitCode || 0);
451
- } catch (error) {
452
- const message =
453
- error instanceof Error
454
- ? error.message.split('\n')[0]!
455
- : 'Command failed. Please try again.';
456
- if (out.json && wrap) {
457
- process.stdout.write(JSON.stringify({ ok: false, command, error: message }) + '\n');
458
- process.exit(1);
459
- }
460
- console.error(message);
461
- process.exit(1);
462
- }
463
- };
464
-
465
- program
466
- .command('demo')
467
- .description('Guided demonstrations using real LearnCard credentials.')
468
- .command('refresh')
469
- .description('Send a demo badge, publish an update, and refresh the recipient’s copy.')
470
- .option('--inbox', 'use Universal Inbox deferred issuance for an address with no account')
471
- .option('--ui', 'claim and refresh in the local LearnCard app (interactive only)')
472
- .option('--app-url <url>', 'local LearnCard app URL for --ui (default: http://localhost:3000)')
473
- .option(
474
- '--lca-url <url>',
475
- 'local LCA URL for --inbox terminal mode (default: http://localhost:5100/trpc)'
476
- )
477
- .option(
478
- '--email [address]',
479
- 'real-email mode: request a provisional claim email to your own address (requires --inbox --ui; prompts when omitted)'
480
- )
481
- .option('-y, --yes', 'run all steps without pausing')
482
- .option('--network <url>', 'network tRPC URL or staging', 'http://localhost:4000/trpc')
483
- .option('--json', 'print a single JSON result on stdout (no pauses)')
484
- .action(options =>
485
- runCommand('demo refresh', options, async didkit => {
486
- const { runDemoRefreshCommand } = await import('./demo-refresh-command');
487
- await runDemoRefreshCommand({ ...options, didkit });
488
- })
489
- );
490
-
491
- commandOptions(
492
- program.command('consent-contract').description("Connect a user's LearnCard to your platform.")
493
- )
494
- .option('--name <name>', 'contract name (default: issuer display name)')
495
- .option(
496
- '--redirect-url <url>',
497
- 'callback URL (default: http://localhost:3000/consent-callback)'
498
- )
499
- .option('--description <text>', 'what users see when asked to consent')
500
- .option(
501
- '--needs-guardian-consent',
502
- 'GameFlow: minors need a guardian to approve the connection'
503
- )
504
- .action(options =>
505
- runCommand('consent-contract', options, async didkit => {
506
- const { runConsentContract } = await import('./consent-contract');
507
- await runConsentContract({ ...options, didkit });
508
- })
509
- );
510
-
511
- commandOptions(program.command('embed').description('Put a Claim button on your site.'))
512
- .option('--name <name>', 'integration name (default: issuer display name)')
513
- .option(
514
- '--domains <origins>',
515
- 'comma-separated origins (default: http://localhost:3000,http://localhost:5173)'
516
- )
517
- .option('--rotate-key', 'rotate the integration publishable key')
518
- .action(options =>
519
- runCommand('embed', options, async didkit => {
520
- const { runEmbed } = await import('./embed');
521
- await runEmbed({ ...options, didkit });
522
- })
523
- );
524
-
525
- commandOptions(
526
- program
527
- .command('setup-signing')
528
- .description('Set up LearnCard to sign credentials for your project.')
529
- )
530
- .option('--name <name>', 'signing authority name (default: default-issuer)')
531
- .option('--endpoint <url>', 'register your own VC-API signing service instead (with --did)')
532
- .option('--did <did>', 'DID of your own signing service (with --endpoint)')
533
- .action(options =>
534
- runCommand('setup-signing', options, async didkit => {
535
- const { runSetupSigning } = await import('./setup-signing');
536
- await runSetupSigning({ ...options, didkit });
537
- })
538
- );
539
-
540
- commandOptions(
541
- program.command('token').description('Create a scoped API token and reusable send.sh.')
542
- )
543
- .option('--name <name>', 'auth grant name (default: cli-<date>)')
544
- .option('--scope <scope>', 'space-separated permissions (default: boosts:write)')
545
- .option('--revoke <grantId>', 'revoke an existing auth grant')
546
- .option('--expires <days>', 'token lifetime in days (default: no expiry)')
547
- .option('--list', 'list your auth grants')
548
- .action(options =>
549
- runCommand('token', options, async didkit => {
550
- const { runToken } = await import('./token');
551
- await runToken({ ...options, didkit });
552
- })
553
- );
554
-
555
- program
556
- .command('verify <file>')
557
- .description('Verify a credential or presentation JSON file; use - for stdin.')
558
- .option('--json', 'print the raw verification result')
559
- .action((file, options) =>
560
- runCommand(
561
- 'verify',
562
- options,
563
- async didkit => {
564
- const { runVerify } = await import('./verify');
565
- await runVerify(file, { ...options, didkit });
566
- },
567
- false
568
- )
569
- );
570
-
571
- commandOptions(
572
- program
573
- .command('revoke <credentialUri>')
574
- .description('Revoke or suspend an issued credential on the network.')
575
- )
576
- .option('--suspend', 'suspend instead of permanently revoking')
577
- .option('--template-uri <uri>', 'template URI if it cannot be read from the credential')
578
- .option(
579
- '--recipient <profileId>',
580
- 'recipient profile ID if it cannot be read from the credential'
581
- )
582
- .action((uri, options) =>
583
- runCommand('revoke', options, async didkit => {
584
- const { runRevoke } = await import('./revoke');
585
- await runRevoke(uri, { ...options, didkit });
586
- })
587
- );
588
-
589
- commandOptions(
590
- program
591
- .command('status [activityId]')
592
- .description('What happened to a credential you sent: created, delivered, claimed.')
593
- )
594
- .option('--limit <n>', 'how many recent sends to list (default: 20)')
595
- .option(
596
- '--event <type>',
597
- 'only list sends whose latest event is this: created|delivered|claimed|expired|failed'
598
- )
599
- .action((activityId, options) =>
600
- runCommand('status', options, async didkit => {
601
- const { runStatus } = await import('./status');
602
- await runStatus(activityId, { ...options, didkit });
603
- })
604
- );
605
-
606
- program
607
- .command('open [target]')
608
- .description(
609
- 'Open the LearnCard app signed in as this project. Targets: portal (default), wallet, template, contract, integration.'
610
- )
611
- .option('-y, --yes', 'accept defaults without prompting')
612
- .option('--network <url>', 'network tRPC URL or staging (default: production)')
613
- .option('--app-url <url>', 'LearnCard app URL for self-hosted or local networks')
614
- .option('--url-fragment', 'pass the seed in the URL fragment instead of the clipboard')
615
- .option('--no-browser', 'print the URL without opening a browser')
616
- .option('--json', 'print a single JSON result on stdout')
617
- .action((target, options) =>
618
- runCommand('open', options, async () => {
619
- const { runOpen, OPEN_TARGETS } = await import('./open');
620
- if (target && !(target in OPEN_TARGETS))
621
- throw new Error(
622
- `Unknown target "${target}". Use one of: ${Object.keys(OPEN_TARGETS).join(', ')}.`
623
- );
624
- await runOpen(target, options);
625
- })
626
- );
627
-
628
- commandOptions(
629
- program.command('webhook [email]').description('Know when your credential was claimed.')
630
- )
631
- .option('--to <email>', 'recipient email')
632
- .option('--url <publicUrl>', 'public HTTPS webhook URL')
633
- .option('--port <n>', 'receiver port (default: 8787)')
634
- .option('--name <name>', 'display name for your issuer profile')
635
- .option(
636
- '--timeout <seconds>',
637
- 'in --json mode, seconds to wait for webhook events (default: 60)'
638
- )
639
- .option('--wait-for-claim', 'in --json mode, also wait for ISSUANCE_CLAIMED before exiting')
640
- .action((email, options) =>
641
- runCommand('webhook', options, async didkit => {
642
- const { runWebhook } = await import('./webhook');
643
- await runWebhook(email, { ...options, didkit });
644
- })
645
- );
646
-
647
- registerOrgCommand(program, runCommand);
648
- registerDoctorCommand(program, runCommand);
649
- registerClrCommand(program, runCommand);
650
- registerInboxCommand(program, runCommand);
651
- registerRefreshCommand(program, runCommand);
652
- registerWhoamiCommand(program, runCommand);
653
- registerPromoteCommand(program, runCommand);
654
-
655
- const JOURNEY = [
656
- 'send',
657
- 'status',
658
- 'whoami',
659
- 'setup-signing',
660
- 'token',
661
- 'webhook',
662
- 'consent-contract',
663
- 'embed',
664
- 'verify',
665
- 'revoke',
666
- 'open',
667
- 'org',
668
- 'doctor',
669
- 'promote',
670
- 'clr',
671
- 'inbox',
672
- 'refresh',
673
- 'init',
674
- 'repl',
675
- ];
676
-
677
- program
678
- .name('learncard')
679
- .description(
680
- 'Issue, track, and verify credentials from the terminal. Every command shares the .env in the current folder.'
681
- )
682
- .showSuggestionAfterError()
683
- .configureHelp({
684
- sortSubcommands: false,
685
- visibleCommands: cmd =>
686
- [...cmd.commands].sort((a, b) => JOURNEY.indexOf(a.name()) - JOURNEY.indexOf(b.name())),
687
- })
688
- .addHelpText(
689
- 'before',
690
- '\nStart here: npx @learncard/cli send\nThen: npx @learncard/cli status\n'
691
- )
692
- .addHelpText(
693
- 'after',
694
- '\nEvery command: -y skips prompts, --json prints one machine-readable result, --network staging|<url> picks the network.\n' +
695
- 'Environment:\n' +
696
- ' LC_YES=1 Same as passing -y to every command.\n' +
697
- ' LCA_API_URL, NETWORK_URL Override service URLs for self-hosted networks.\n'
698
- );
699
-
700
- const runRepl = async (_seed: string = generateRandomSeed()) => {
701
- console.clear();
702
-
703
- const envSeed = process.env.LEARNCARD_CLI_SEED ?? process.env.SEED;
704
- const seedInput = envSeed ?? _seed;
705
- const seed = seedInput.padStart(64, '0');
706
-
707
- console.log(
708
- gradient(['cyan', 'green'])(figlet.textSync('Learn Card', { font: 'Big Money-ne' }))
709
- );
710
- console.log('Welcome to the Learn Card CLI!\n');
711
-
712
- console.log(`Your seed is ${seed}\n`);
713
-
714
- if (envSeed) {
715
- console.log('Using seed from LEARNCARD_CLI_SEED / SEED.\n');
716
- }
717
-
718
- console.log('Creating wallet...');
719
-
720
- cliGlobals.seed = seed;
721
- cliGlobals.generateRandomSeed = generateRandomSeed;
722
- cliGlobals.emptyLearnCard = emptyLearnCard;
723
- cliGlobals.learnCardFromSeed = learnCardFromSeed;
724
- cliGlobals.initLearnCard = initLearnCard;
725
-
726
- const didkit = fs.readFile(
727
- require.resolve('@learncard/didkit-plugin/dist/didkit/didkit_wasm_bg.wasm')
728
- );
729
-
730
- const _learnCard = await initLearnCard({
731
- seed,
732
- network: true,
733
- allowRemoteContexts: true,
734
- didkit,
735
- });
736
-
737
- const lcaApiLc = await _learnCard.addPlugin(
738
- await getLCAPlugin(_learnCard, 'https://api.learncard.app/trpc')
739
- );
740
-
741
- cliGlobals.learnCard = await lcaApiLc.addPlugin(getLerRsPlugin(lcaApiLc));
742
- // Add LinkedClaims plugin so endorse/verify/store/getEndorsements are available in the CLI
743
- cliGlobals.learnCard = await cliGlobals.learnCard.addPlugin(
744
- getLinkedClaimsPlugin(cliGlobals.learnCard)
745
- );
746
-
747
- // Add OpenBadge v2 wrapper plugin for backwards-compatible OBv2 -> VC wrapping
748
- cliGlobals.learnCard = await cliGlobals.learnCard.addPlugin(
749
- openBadgeV2Plugin(cliGlobals.learnCard)
750
- );
751
-
752
- // Add Render Method plugin for attaching W3C renderMethod to VCs
753
- cliGlobals.learnCard = await cliGlobals.learnCard.addPlugin(
754
- getRenderMethodPlugin(cliGlobals.learnCard)
755
- );
756
-
757
- cliGlobals.types = types;
758
- cliGlobals.getTestCache = getTestCache;
759
-
760
- cliGlobals.copy = copyFunction;
761
- cliGlobals.getLearnCardBundlePassword = getLearnCardBundlePassword;
762
- cliGlobals.exportLearnCardBundle = createExportLearnCardBundleHelper(
763
- writeLearnCardBundle,
764
- cliGlobals.learnCard
765
- );
766
-
767
- cliGlobals.restoreLearnCardFromBundle = createRestoreLearnCardFromBundleHelper(restoreBundle, {
768
- network: true,
769
- allowRemoteContexts: true,
770
- didkit,
771
- });
772
- cliGlobals.importLearnCardBundle = importLearnCardBundle;
773
- cliGlobals.createLearnCardBundle = createLearnCardBundle;
774
- cliGlobals.readLearnCardBundle = readLearnCardBundle;
775
-
776
- // delete 'Creating wallet...' message
777
- process.stdout.moveCursor?.(0, -1);
778
- process.stdout.clearLine?.(1);
779
-
780
- console.log('Wallet created!\n');
781
-
782
- console.log('┌───────────────────────────────────────────────────────────────┐');
783
- console.log('│ Variables Available │');
784
- console.log('├────────────────────────────┬──────────────────────────────────┤');
785
- console.log('│ Variable │ Description │');
786
- console.log('├────────────────────────────┼──────────────────────────────────┤');
787
- console.log(`│ ${g.learnCard} │ Learn Card Wallet │`);
788
- console.log(`│ ${g.initLearnCard} │ Wallet Instantiation Function │`);
789
- console.log(`│ ${g.seed} │ Seed used to generate wallet │`);
790
- console.log(`│ ${g.generateRandomSeed} │ Generates a random seed │`);
791
- console.log(`│ ${g.types} │ Helpful zod validators │`);
792
- console.log(`│ ${g.copy} │ Copy text to clipboard │`);
793
- console.log(`│ ${g.getLearnCardBundlePassword} │ Prompt for bundle password │`);
794
- console.log(`│ ${g.exportLearnCardBundle} │ Export wallet continuity ZIP │`);
795
- console.log(`│ ${g.importLearnCardBundle} │ Import continuity ZIP │`);
796
- console.log(`│ ${g.restoreLearnCardFromBundle} │ Restore original wallet from ZIP │`);
797
- console.log('└────────────────────────────┴──────────────────────────────────┘');
798
-
799
- console.log('');
800
-
801
- console.log('For help/documentation regarding your wallet, please read the documentation at\n');
802
-
803
- console.log('https://docs.learncard.com/sdks/learncard-core/construction\n');
804
-
805
- console.log("To get a feel for what's possible, try some of the following commands\n");
806
-
807
- console.log(
808
- '┌─────────────────────────┬───────────────────────────────────────────────────────────────────────────────────┐'
809
- );
810
- console.log(
811
- '│ Description │ Command │'
812
- );
813
- console.log(
814
- '├─────────────────────────┼───────────────────────────────────────────────────────────────────────────────────┤'
815
- );
816
- console.log(
817
- `│ View your did │ ${g.learnCard}.id.did(); │`
818
- );
819
- console.log(
820
- `│ Generate an unsigned VC │ ${g.learnCard}.invoke.getTestVc(); │`
821
- );
822
- console.log(
823
- `│ Issue a signed VC │ await ${g.learnCard}.invoke.issueCredential(uvc); │`
824
- );
825
- console.log(
826
- `│ Verify a signed VC │ await ${g.learnCard}.invoke.verifyCredential(vc); │`
827
- );
828
- console.log(
829
- `│ Issue a signed VP │ await ${g.learnCard}.invoke.issuePresentation(vc); │`
830
- );
831
- console.log(
832
- `│ Verify a signed VP │ await ${g.learnCard}.invoke.verifyPresentation(vp); │`
833
- );
834
- console.log(
835
- `│ Prompt bundle password │ const password = await ${g.getLearnCardBundlePassword}(); │`
836
- );
837
- console.log(
838
- `│ Export wallet ZIP │ await ${g.exportLearnCardBundle}(${g.learnCard}, { out: './export.zip', password }); │`
839
- );
840
- console.log(
841
- `│ Restore original wallet │ await ${g.restoreLearnCardFromBundle}('./export.zip', { password }); │`
842
- );
843
- console.log(
844
- '└─────────────────────────┴───────────────────────────────────────────────────────────────────────────────────┘'
845
- );
846
-
847
- console.log('');
848
-
849
- await startCliRepl(colorizeReplInput);
850
- };
851
-
852
- commandOptions(
853
- program
854
- .command('init')
855
- .description("Create this folder's issuer identity and profile without sending anything.")
856
- .option('--name <displayName>', 'display name for your issuer profile')
857
- ).action(options =>
858
- runCommand('init', options, async didkit => {
859
- const { runInit } = await import('./init');
860
- await runInit({ ...options, didkit });
861
- })
862
- );
863
-
864
- program
865
- .command('repl [seed]')
866
- .description('Interactive JavaScript console with a LearnCard preloaded (advanced).')
867
- .action(runRepl);
868
-
869
- program.version(packageJson.version);
870
-
871
- // Bare `learncard` with a TTY opens the console, as it always has; anything else is a command.
872
- if (process.argv.length <= 2 && process.stdin.isTTY) {
873
- runRepl();
874
- } else {
875
- program.parse(process.argv);
876
- }