@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.
- package/CHANGELOG.md +67 -0
- package/README.md +36 -12
- package/dist/index.js +6673 -2038
- package/package.json +27 -15
- package/project.json +0 -42
- package/rollup.config.js +0 -34
- package/scripts/embed-snippets.ts +0 -47
- package/src/auth-grant.test.ts +0 -54
- package/src/auth-grant.ts +0 -34
- package/src/clr/validate.test.ts +0 -65
- package/src/clr/validate.ts +0 -242
- package/src/clr.ts +0 -119
- package/src/consent-contract.ts +0 -116
- package/src/demo-inbox-refresh.test.ts +0 -737
- package/src/demo-inbox-refresh.ts +0 -804
- package/src/demo-refresh-command.test.ts +0 -57
- package/src/demo-refresh-command.ts +0 -22
- package/src/demo-refresh-ui.test.ts +0 -66
- package/src/demo-refresh-ui.ts +0 -65
- package/src/demo-refresh.test.ts +0 -140
- package/src/demo-refresh.ts +0 -309
- package/src/doctor/checks.test.ts +0 -448
- package/src/doctor/checks.ts +0 -497
- package/src/doctor.test.ts +0 -67
- package/src/doctor.ts +0 -118
- package/src/embed.ts +0 -102
- package/src/inbox.test.ts +0 -257
- package/src/inbox.ts +0 -221
- package/src/index.tsx +0 -876
- package/src/init.ts +0 -26
- package/src/open.test.ts +0 -40
- package/src/open.ts +0 -186
- package/src/org/apply.test.ts +0 -1108
- package/src/org/apply.ts +0 -924
- package/src/org/branding.test.ts +0 -60
- package/src/org/diff.ts +0 -14
- package/src/org/load.ts +0 -50
- package/src/org/schema.test.ts +0 -256
- package/src/org/schema.ts +0 -216
- package/src/org.ts +0 -124
- package/src/out.test.ts +0 -40
- package/src/out.ts +0 -16
- package/src/phase-two.test.ts +0 -154
- package/src/project.test.ts +0 -232
- package/src/project.ts +0 -489
- package/src/promote.test.ts +0 -142
- package/src/promote.ts +0 -202
- package/src/random.ts +0 -3
- package/src/refresh.test.ts +0 -86
- package/src/refresh.ts +0 -93
- package/src/replHelpers.test.ts +0 -80
- package/src/replHelpers.ts +0 -45
- package/src/revoke.test.ts +0 -29
- package/src/revoke.ts +0 -65
- package/src/send-template.test.ts +0 -20
- package/src/send.test.ts +0 -321
- package/src/send.ts +0 -373
- package/src/setup-signing.test.ts +0 -62
- package/src/setup-signing.ts +0 -185
- package/src/snippet-files.ts +0 -16
- package/src/status.test.ts +0 -33
- package/src/status.ts +0 -94
- package/src/token.test.ts +0 -28
- package/src/token.ts +0 -139
- package/src/types.d.ts +0 -17
- package/src/verify.test.ts +0 -35
- package/src/verify.ts +0 -66
- package/src/webhook.ts +0 -247
- package/src/whoami.test.ts +0 -67
- package/src/whoami.ts +0 -129
- package/tsconfig.json +0 -38
- 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
|
-
});
|
package/src/setup-signing.ts
DELETED
|
@@ -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
|
-
};
|
package/src/snippet-files.ts
DELETED
|
@@ -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
|
-
};
|
package/src/status.test.ts
DELETED
|
@@ -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
|
-
}
|
package/src/verify.test.ts
DELETED
|
@@ -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
|
-
});
|