@learncard/cli 3.6.8 → 4.0.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +67 -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
@@ -1,62 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import fs from 'node:fs/promises';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
- import { authorityName, setupSigning } from './setup-signing';
6
- import { loadProject } from './project';
7
-
8
- describe('signing authority setup', () => {
9
- it('defaults to default-issuer and reuses the project selection', () => {
10
- const project = { env: {}, existing: '', envPath: '/unused/.env' };
11
- expect(authorityName(project)).toBe('default-issuer');
12
- expect(authorityName({ ...project, env: { SIGNING_AUTHORITY_NAME: 'existing' } })).toBe(
13
- 'existing'
14
- );
15
- expect(authorityName(project, 'custom')).toBe('custom');
16
- });
17
- it('resumes after failed registration and leaves an existing primary untouched', async () => {
18
- const cwd = await fs.mkdtemp(path.join(os.tmpdir(), 'lc-signing-'));
19
- const log = vi.spyOn(console, 'log').mockImplementation(() => {});
20
- const authority = {
21
- name: 'default-issuer',
22
- endpoint: 'https://issuer.example/api',
23
- did: 'did:key:issuer',
24
- ownerDid: 'did:key:owner',
25
- };
26
- const invoke = {
27
- getRegisteredSigningAuthorities: vi.fn().mockResolvedValue([]),
28
- getSigningAuthorities: vi.fn().mockResolvedValueOnce([]).mockResolvedValue([authority]),
29
- createSigningAuthority: vi.fn().mockResolvedValue(authority),
30
- registerSigningAuthority: vi
31
- .fn()
32
- .mockRejectedValueOnce(new Error('Connection lost'))
33
- .mockResolvedValue(true),
34
- setPrimaryRegisteredSigningAuthority: vi.fn().mockResolvedValue(true),
35
- };
36
- try {
37
- const project = await loadProject(cwd);
38
- await expect(setupSigning(project, { invoke })).rejects.toThrow('Connection lost');
39
- await setupSigning(project, { invoke });
40
- expect(invoke.createSigningAuthority).toHaveBeenCalledTimes(1);
41
- expect(project.env.SIGNING_AUTHORITY_ENDPOINT).toBe(authority.endpoint);
42
- invoke.getRegisteredSigningAuthorities.mockResolvedValue([
43
- {
44
- signingAuthority: { endpoint: authority.endpoint },
45
- relationship: { name: authority.name, did: authority.did, isPrimary: true },
46
- },
47
- ]);
48
- await setupSigning(project, { invoke });
49
- expect(invoke.createSigningAuthority).toHaveBeenCalledTimes(1);
50
- expect(invoke.setPrimaryRegisteredSigningAuthority).toHaveBeenCalledTimes(1);
51
- expect(log).toHaveBeenCalledWith(
52
- 'Signing authority "default-issuer" is already your primary.'
53
- );
54
- const demoProject = { env: {}, existing: '', envPath: '/unused/.env' };
55
- await setupSigning(demoProject, { invoke }, undefined, { persist: false });
56
- expect(demoProject.env).toEqual({});
57
- } finally {
58
- log.mockRestore();
59
- await fs.rm(cwd, { recursive: true, force: true });
60
- }
61
- });
62
- });
@@ -1,185 +0,0 @@
1
- import type { LCALearnCard } from '@learncard/lca-api-plugin';
2
- import {
3
- connect,
4
- ensureIdentity,
5
- ensureProfile,
6
- loadProject,
7
- saveProject,
8
- type Project,
9
- type ProjectOptions,
10
- type NetworkCard,
11
- } from './project';
12
- import { out } from './out';
13
-
14
- export const authorityName = (project: Project, name?: string): string =>
15
- name ?? project.env.SIGNING_AUTHORITY_NAME ?? 'default-issuer';
16
-
17
- export interface SigningAuthorityResult {
18
- name: string;
19
- endpoint: string;
20
- did: string;
21
- alreadyConfigured: boolean;
22
- }
23
-
24
- type SigningCard = {
25
- invoke: Pick<
26
- LCALearnCard['invoke'],
27
- | 'getRegisteredSigningAuthorities'
28
- | 'getSigningAuthorities'
29
- | 'setPrimaryRegisteredSigningAuthority'
30
- | 'createSigningAuthority'
31
- | 'registerSigningAuthority'
32
- >;
33
- };
34
-
35
- /** Create and register a hosted signer once; also repair a changed primary selection. */
36
- export const setupSigning = async (
37
- project: Project,
38
- learnCard: SigningCard,
39
- requestedName?: string,
40
- options: { persist?: boolean } = {}
41
- ): Promise<SigningAuthorityResult> => {
42
- const name = authorityName(project, requestedName);
43
- const registered = (await learnCard.invoke.getRegisteredSigningAuthorities()).find(
44
- authority =>
45
- authority.relationship.name === name &&
46
- (requestedName ||
47
- !project.env.SIGNING_AUTHORITY_ENDPOINT ||
48
- authority.signingAuthority.endpoint === project.env.SIGNING_AUTHORITY_ENDPOINT)
49
- );
50
- if (registered) {
51
- const endpoint = registered.signingAuthority.endpoint;
52
- if (!registered.relationship.isPrimary) {
53
- if (!(await learnCard.invoke.setPrimaryRegisteredSigningAuthority(endpoint, name))) {
54
- throw new Error(
55
- 'Could not select the primary signing authority. Please try again.'
56
- );
57
- }
58
- }
59
- if (options.persist !== false) {
60
- await saveProject(project, {
61
- SIGNING_AUTHORITY_NAME: name,
62
- SIGNING_AUTHORITY_ENDPOINT: endpoint,
63
- });
64
- }
65
- out.log(`Signing authority "${name}" is already your primary.`);
66
- return { name, endpoint, did: registered.relationship.did, alreadyConfigured: true };
67
- }
68
- const hosted = await learnCard.invoke.getSigningAuthorities();
69
- if (!hosted) throw new Error('Could not look up hosted signing authorities. Please try again.');
70
- const existing = hosted.find(authority => authority.name === name);
71
- const authority = existing || (await learnCard.invoke.createSigningAuthority(name));
72
- if (!authority || !authority.endpoint || !authority.did)
73
- throw new Error('Could not create signing authority. Please try again.');
74
- const registeredSuccessfully = await learnCard.invoke.registerSigningAuthority(
75
- authority.endpoint,
76
- authority.name,
77
- authority.did
78
- );
79
- if (!registeredSuccessfully)
80
- throw new Error('Could not register signing authority. Please try again.');
81
- if (
82
- !(await learnCard.invoke.setPrimaryRegisteredSigningAuthority(
83
- authority.endpoint,
84
- authority.name
85
- ))
86
- ) {
87
- throw new Error('Could not select the primary signing authority. Please try again.');
88
- }
89
- if (options.persist !== false) {
90
- await saveProject(project, {
91
- SIGNING_AUTHORITY_NAME: authority.name,
92
- SIGNING_AUTHORITY_ENDPOINT: authority.endpoint,
93
- });
94
- }
95
- out.log(`LearnCard will now sign credentials for ${project.env.PROFILE_ID}.`);
96
- out.log('What this did:');
97
- out.log(
98
- existing
99
- ? ` const authority = (await learnCard.invoke.getSigningAuthorities()).find(authority => authority.name === ${JSON.stringify(name)});`
100
- : ` const authority = await learnCard.invoke.createSigningAuthority(${JSON.stringify(name)});`
101
- );
102
- out.log(
103
- ' await learnCard.invoke.registerSigningAuthority(authority.endpoint, authority.name, authority.did);'
104
- );
105
- out.log(
106
- ' await learnCard.invoke.setPrimaryRegisteredSigningAuthority(authority.endpoint, authority.name);'
107
- );
108
- return {
109
- name: authority.name,
110
- endpoint: authority.endpoint,
111
- did: authority.did,
112
- alreadyConfigured: false,
113
- };
114
- };
115
-
116
- type SetupSigningOptions = ProjectOptions & { endpoint?: string; did?: string };
117
-
118
- /** Register a signing service you run yourself instead of a LearnCard-hosted one. */
119
- const registerOwnAuthority = async (
120
- project: Project,
121
- learnCard: {
122
- invoke: Pick<
123
- NetworkCard['invoke'],
124
- 'registerSigningAuthority' | 'setPrimaryRegisteredSigningAuthority'
125
- >;
126
- },
127
- name: string,
128
- endpoint: string,
129
- did: string
130
- ) => {
131
- if (!/^https:\/\//.test(endpoint)) throw new Error('--endpoint must be an https:// URL.');
132
- if (!/^did:/.test(did)) throw new Error('--did must be a DID (did:web:..., did:key:...).');
133
- if (!(await learnCard.invoke.registerSigningAuthority(endpoint, name, did)))
134
- throw new Error('Could not register the signing authority.');
135
- if (!(await learnCard.invoke.setPrimaryRegisteredSigningAuthority(endpoint, name)))
136
- throw new Error('Could not set the signing authority as primary.');
137
- await saveProject(project, {
138
- SIGNING_AUTHORITY_NAME: name,
139
- SIGNING_AUTHORITY_ENDPOINT: endpoint,
140
- });
141
- out.log(`Your service at ${endpoint} will now sign credentials for this profile.`);
142
- out.log('What this did:');
143
- out.log(
144
- ` await learnCard.invoke.registerSigningAuthority(${JSON.stringify(endpoint)}, ${JSON.stringify(name)}, ${JSON.stringify(did)});`
145
- );
146
- out.log(
147
- ` await learnCard.invoke.setPrimaryRegisteredSigningAuthority(${JSON.stringify(endpoint)}, ${JSON.stringify(name)});`
148
- );
149
- return { name, endpoint, did, alreadyConfigured: false };
150
- };
151
-
152
- export const runSetupSigning = async (options: SetupSigningOptions): Promise<void> => {
153
- const project = await loadProject(process.cwd());
154
- const identity = await ensureIdentity(project, { ...options, name: undefined });
155
- if ((options.endpoint && !options.did) || (!options.endpoint && options.did))
156
- throw new Error('Pass both --endpoint and --did to register your own signing service.');
157
- let authority;
158
- if (options.endpoint) {
159
- const learnCard = await connect(project, options);
160
- await ensureProfile(learnCard, identity, project);
161
- authority = await registerOwnAuthority(
162
- project,
163
- learnCard,
164
- options.name ?? 'my-issuer',
165
- options.endpoint,
166
- options.did!
167
- );
168
- } else {
169
- const learnCard = await connect(project, { ...options, lca: true });
170
- await ensureProfile(learnCard, identity, project);
171
- authority = await setupSigning(project, learnCard, options.name);
172
- }
173
- out.set({
174
- profileId: identity.profileId,
175
- signingAuthority: {
176
- name: authority.name,
177
- endpoint: authority.endpoint,
178
- did: authority.did,
179
- },
180
- alreadyConfigured: authority.alreadyConfigured,
181
- });
182
- out.log(
183
- 'Send from a template: npx @learncard/cli send --template\nSee it in the app: npx @learncard/cli open'
184
- );
185
- };
@@ -1,16 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { out } from './out';
4
-
5
- /** Preserve files a developer may already have customized. Returns whether this run wrote it. */
6
- export const writeSnippet = async (filename: string, source: string): Promise<boolean> => {
7
- try {
8
- await fs.writeFile(path.join(process.cwd(), filename), source, { flag: 'wx' });
9
- out.log(`Wrote ./${filename}`);
10
- return true;
11
- } catch (error) {
12
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
13
- out.log(`Kept existing ./${filename}`);
14
- return false;
15
- }
16
- };
@@ -1,33 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { formatEvent, summarize } from './status';
3
-
4
- const created = {
5
- activityId: 'a1',
6
- eventType: 'CREATED',
7
- timestamp: '2026-09-11T18:12:27.116Z',
8
- recipientType: 'email',
9
- recipientIdentifier: 'jane@example.com',
10
- boost: { name: 'Quickstart Complete' },
11
- };
12
-
13
- describe('status', () => {
14
- it('reports the latest event as the state and keeps the chain newest-first', () => {
15
- const claimed = {
16
- ...created,
17
- eventType: 'CLAIMED',
18
- timestamp: '2026-09-11T18:20:00.000Z',
19
- credentialUri: 'lc:network:x/trpc:credential:1',
20
- };
21
- const s = summarize([created, claimed]);
22
- expect(s.state).toBe('CLAIMED');
23
- expect(s.credentialUri).toBe('lc:network:x/trpc:credential:1');
24
- expect(s.events.map(e => e.type)).toEqual(['CLAIMED', 'CREATED']);
25
- });
26
-
27
- it('surfaces the failure reason on FAILED events', () => {
28
- const failed = { ...created, eventType: 'FAILED', metadata: { error: 'bad email' } };
29
- expect(formatEvent(failed)).toContain('FAILED');
30
- expect(formatEvent(failed)).toContain('bad email');
31
- expect(summarize([failed]).events[0]).toMatchObject({ type: 'FAILED', error: 'bad email' });
32
- });
33
- });
package/src/status.ts DELETED
@@ -1,94 +0,0 @@
1
- import { connect, ensureIdentity, loadProject, type ProjectOptions } from './project';
2
- import { out } from './out';
3
-
4
- type ActivityEvent = {
5
- activityId: string;
6
- eventType: string;
7
- timestamp: string;
8
- recipientType: string;
9
- recipientIdentifier: string;
10
- credentialUri?: string;
11
- status?: string;
12
- boost?: { name?: string } | null;
13
- metadata?: Record<string, unknown>;
14
- };
15
-
16
- const LATEST_FIRST = (a: ActivityEvent, b: ActivityEvent) => b.timestamp.localeCompare(a.timestamp);
17
-
18
- /** One human line per event: time, what happened, to whom. */
19
- export const formatEvent = (event: ActivityEvent): string => {
20
- const when = event.timestamp.replace('T', ' ').slice(0, 19);
21
- const failure = event.eventType === 'FAILED' ? ` ${String(event.metadata?.error ?? '')}` : '';
22
- return `${when} ${event.eventType.padEnd(9)} ${event.recipientIdentifier}${failure}`;
23
- };
24
-
25
- /** Collapse a chain into what a developer wants to know: the current state. */
26
- export const summarize = (chain: ActivityEvent[]) => {
27
- const ordered = [...chain].sort(LATEST_FIRST);
28
- const latest = ordered[0];
29
- const claimed = chain.find(e => e.eventType === 'CLAIMED');
30
- return {
31
- state: latest?.eventType ?? 'UNKNOWN',
32
- recipient: latest?.recipientIdentifier,
33
- template: latest?.boost?.name,
34
- credentialUri: claimed?.credentialUri ?? latest?.credentialUri,
35
- credentialStatus: latest?.status,
36
- events: ordered.map(e => ({
37
- type: e.eventType,
38
- at: e.timestamp,
39
- ...(e.eventType === 'FAILED' && e.metadata?.error ? { error: e.metadata.error } : {}),
40
- })),
41
- };
42
- };
43
-
44
- type StatusOptions = ProjectOptions & { limit?: string; event?: string };
45
-
46
- export const runStatus = async (activityId: string | undefined, options: StatusOptions) => {
47
- const project = await loadProject(process.cwd());
48
- if (!project.env.SECURE_SEED)
49
- throw new Error('No SECURE_SEED in .env. Send something first: npx @learncard/cli send');
50
- await ensureIdentity(project, options);
51
- const learnCard = await connect(project, options);
52
-
53
- if (activityId) {
54
- const chain = (await learnCard.invoke.getActivityChain({ activityId })) as ActivityEvent[];
55
- if (!chain.length) throw new Error(`No activity found for ${activityId}.`);
56
- const summary = summarize(chain);
57
- out.log(`${summary.state} ${summary.template ?? ''} → ${summary.recipient}`);
58
- for (const e of [...chain].sort(LATEST_FIRST)) out.log(` ${formatEvent(e)}`);
59
- if (summary.credentialUri) out.log(`Credential: ${summary.credentialUri}`);
60
- if (summary.state !== 'CLAIMED')
61
- out.log('Not claimed yet. Re-run to check again, or use `webhook` to be told.');
62
- out.set({ activityId, ...summary });
63
- return;
64
- }
65
-
66
- const limit = Number(options.limit ?? 20);
67
- const eventType = options.event?.toUpperCase();
68
- const page = await learnCard.invoke.getMyActivities({
69
- limit,
70
- ...(eventType ? { eventType: eventType as never } : {}),
71
- });
72
- const records = page.records as ActivityEvent[];
73
- if (!records.length) {
74
- out.log('No sends yet. Try: npx @learncard/cli send');
75
- out.set({ activities: [] });
76
- return;
77
- }
78
- out.log('activityId state recipient');
79
- for (const r of records)
80
- out.log(`${r.activityId} ${r.eventType.padEnd(9)} ${r.recipientIdentifier}`);
81
- if (page.hasMore)
82
- out.log(`…more. Raise --limit or filter with --event claimed|delivered|created.`);
83
- out.log('Details for one: npx @learncard/cli status <activityId>');
84
- out.set({
85
- activities: records.map(r => ({
86
- activityId: r.activityId,
87
- state: r.eventType,
88
- recipient: r.recipientIdentifier,
89
- template: r.boost?.name,
90
- at: r.timestamp,
91
- })),
92
- hasMore: page.hasMore,
93
- });
94
- };
package/src/token.test.ts DELETED
@@ -1,28 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { validateScope, SCOPE_RESOURCES } from './token';
3
- import { SEND_SH } from './generated/snippets';
4
- import { withEnvTokenLoader } from './project';
5
-
6
- describe('token scopes', () => {
7
- it('accepts real resource names and wildcards', () => {
8
- for (const resource of SCOPE_RESOURCES)
9
- expect(validateScope(`${resource}:write`)).toBe(`${resource}:write`);
10
- expect(validateScope(' boosts:write inbox:read ')).toBe('boosts:write inbox:read');
11
- expect(validateScope('*:*')).toBe('*:*');
12
- expect(validateScope('')).toBe('');
13
- });
14
- it('rejects unknown resources, stale aliases, and malformed actions', () => {
15
- expect(() => validateScope('unicorns:write')).toThrow('Unknown scope resource');
16
- expect(() => validateScope('profile:write')).toThrow('profiles');
17
- expect(() => validateScope('boosts:admin')).toThrow('read, write, delete');
18
- expect(() => validateScope('boosts:write:extra')).toThrow();
19
- });
20
- it('writes send.sh that reads API_TOKEN from .env without executing it', () => {
21
- const written = withEnvTokenLoader(SEND_SH);
22
- expect(written).toContain("sed -n 's/^API_TOKEN=//p' .env");
23
- expect(written).not.toMatch(/tr -d/);
24
- expect(written).toContain('Bearer $TOKEN');
25
- expect(written).not.toContain('. .env');
26
- expect(written).not.toContain('source .env');
27
- });
28
- });
package/src/token.ts DELETED
@@ -1,139 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import {
4
- connect,
5
- ensureIdentity,
6
- ensureProfile,
7
- loadProject,
8
- saveProject,
9
- type ProjectOptions,
10
- localizeSnippet,
11
- resolveServices,
12
- withEnvTokenLoader,
13
- } from './project';
14
- import { out } from './out';
15
-
16
- // Route-derived resources documented in auth-grants-and-api-tokens.md (not the stale singular aliases).
17
- export const SCOPE_RESOURCES = [
18
- 'boosts',
19
- 'inbox',
20
- 'credentials',
21
- 'presentations',
22
- 'profiles',
23
- 'profileManagers',
24
- 'connections',
25
- 'contracts',
26
- 'contracts-data',
27
- 'signingAuthorities',
28
- 'authGrants',
29
- 'didMetadata',
30
- 'claimHooks',
31
- 'skills',
32
- 'app-store',
33
- 'integrations',
34
- 'contact-methods',
35
- 'activity',
36
- 'storage',
37
- ] as const;
38
-
39
- export const validateScope = (scope: string): string => {
40
- for (const part of scope.trim().split(/\s+/).filter(Boolean)) {
41
- const [resource, action, extra] = part.split(':');
42
- if (
43
- extra !== undefined ||
44
- !resource ||
45
- (resource !== '*' && !SCOPE_RESOURCES.some(value => value === resource))
46
- ) {
47
- throw new Error(
48
- `Unknown scope resource in "${part}". Choose: ${SCOPE_RESOURCES.join(', ')}, or *.`
49
- );
50
- }
51
- if (!action || !['read', 'write', 'delete', '*'].includes(action)) {
52
- throw new Error(`Invalid action in "${part}". Use read, write, delete, or *.`);
53
- }
54
- }
55
- return scope.trim().split(/\s+/).filter(Boolean).join(' ');
56
- };
57
-
58
- export const runToken = async (
59
- options: ProjectOptions & { scope?: string; revoke?: string; expires?: string; list?: boolean }
60
- ): Promise<void> => {
61
- const scope = validateScope(options.scope ?? 'boosts:write');
62
- const project = await loadProject(process.cwd());
63
- const identity = await ensureIdentity(project, { ...options, name: undefined });
64
- const learnCard = await connect(project, options);
65
- await ensureProfile(learnCard, identity, project);
66
- if (options.list) {
67
- const grants = (await learnCard.invoke.getAuthGrants()) ?? [];
68
- for (const g of grants)
69
- out.log(
70
- `${g.id} ${(g.status ?? '').padEnd(8)} ${(g.scope ?? '').padEnd(22)} ${g.name ?? ''}${
71
- g.expiresAt ? ` expires ${g.expiresAt.slice(0, 10)}` : ''
72
- }`
73
- );
74
- if (!grants.length) out.log('No auth grants yet.');
75
- out.set({
76
- grants: grants.map(g => ({
77
- id: g.id,
78
- name: g.name,
79
- scope: g.scope,
80
- status: g.status,
81
- expiresAt: g.expiresAt,
82
- })),
83
- });
84
- return;
85
- }
86
- if (options.revoke) {
87
- if (!(await learnCard.invoke.revokeAuthGrant(options.revoke)))
88
- throw new Error('Could not revoke auth grant.');
89
- out.log(`Revoked auth grant ${options.revoke}.`);
90
- out.log('Create a replacement: npx @learncard/cli token');
91
- out.set({ grantId: options.revoke });
92
- return;
93
- }
94
- const id = await learnCard.invoke.addAuthGrant({
95
- name: options.name ?? `cli-${new Date().toISOString().slice(0, 10)}`,
96
- scope,
97
- ...(options.expires
98
- ? {
99
- expiresAt: new Date(
100
- Date.now() + Number(options.expires) * 86_400_000
101
- ).toISOString(),
102
- }
103
- : {}),
104
- });
105
- const token = await learnCard.invoke.getAPITokenForAuthGrant(id);
106
- // Tighten existing files before writing the bearer credential.
107
- await fs.chmod(project.envPath, 0o600);
108
- await saveProject(project, { API_TOKEN: token, API_TOKEN_SCOPE: scope });
109
- out.log(`Created auth grant ${id}.`);
110
- out.log(
111
- "Warning: this token won't be shown again by this command. It is saved in .env; keep it private."
112
- );
113
- out.log(token);
114
- const file = path.join(process.cwd(), 'send.sh');
115
- let wroteSendSh = false;
116
- // Static import would make org schema validation require this absent-on-clean-checkout asset.
117
- const { SEND_SH } = await import('./generated/snippets');
118
- try {
119
- await fs.writeFile(
120
- file,
121
- withEnvTokenLoader(
122
- localizeSnippet(SEND_SH, resolveServices(project.env, options.network))
123
- ),
124
- {
125
- flag: 'wx',
126
- mode: 0o700,
127
- }
128
- );
129
- out.log('Wrote ./send.sh');
130
- wroteSendSh = true;
131
- } catch (error) {
132
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
133
- out.log('Kept existing ./send.sh');
134
- }
135
- out.log(
136
- 'Next: save your send payload as request.json, then run sh ./send.sh (example: https://docs.learncard.com/start-here/your-first-integration).'
137
- );
138
- out.set({ grantId: id, scope, token, files: wroteSendSh ? ['./send.sh'] : [] });
139
- };
package/src/types.d.ts DELETED
@@ -1,17 +0,0 @@
1
- declare module 'gradient-string' {
2
- const gradient: (...args: any[]) => any;
3
- export default gradient;
4
- }
5
-
6
- declare module '@learncard/holder-continuity' {
7
- export type ExportLearnCardBundleOptions = any;
8
- export type LearnCardBundleWallet = any;
9
- export type RestoreLearnCardFromBundleOptions = any;
10
- export type RestoreLearnCardInit = any;
11
-
12
- export const createLearnCardBundle: any;
13
- export const exportLearnCardBundle: any;
14
- export const importLearnCardBundle: any;
15
- export const readLearnCardBundle: any;
16
- export const restoreLearnCardFromBundle: any;
17
- }
@@ -1,35 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { formatVerification, isPresentation, verificationFailed } from './verify';
3
-
4
- describe('verification output', () => {
5
- it('formats raw checks and errors', () => {
6
- const result = {
7
- checks: ['expiration'],
8
- errors: ['signature error: Bad signature'],
9
- warnings: ['Review issuer'],
10
- };
11
- expect(formatVerification(result)).toEqual([
12
- '✓ expiration',
13
- '! Review issuer',
14
- '✗ proof: Bad signature',
15
- ]);
16
- expect(verificationFailed(result)).toBe(true);
17
- });
18
- it('keeps warnings distinct from failures in prettified output', () => {
19
- const result = [
20
- { status: 'Success' as const, check: 'proof', message: 'Valid' },
21
- { status: 'Error' as const, check: 'issuer', message: 'Not checked' },
22
- ];
23
- expect(formatVerification(result)).toEqual(['✓ proof', '! issuer: Not checked']);
24
- expect(verificationFailed(result)).toBe(false);
25
- expect(verificationFailed([{ status: 'Failed', check: 'proof', details: 'Invalid' }])).toBe(
26
- true
27
- );
28
- });
29
- it('detects presentations by string or array type', () => {
30
- expect(isPresentation({ type: ['VerifiablePresentation'] })).toBe(true);
31
- expect(isPresentation({ type: 'VerifiablePresentation' })).toBe(true);
32
- expect(isPresentation({ type: ['VerifiableCredential'] })).toBe(false);
33
- expect(isPresentation(null)).toBe(false);
34
- });
35
- });