@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
package/src/verify.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import { initLearnCard } from '@learncard/init';
|
|
3
|
-
import { type VC, type VP, type VerificationCheck, type VerificationItem } from '@learncard/types';
|
|
4
|
-
|
|
5
|
-
export type VerifyResult = VerificationCheck | VerificationItem[];
|
|
6
|
-
|
|
7
|
-
export const isPresentation = (value: unknown): boolean => {
|
|
8
|
-
if (!value || typeof value !== 'object' || !('type' in value)) return false;
|
|
9
|
-
const types = Array.isArray(value.type) ? value.type : [value.type];
|
|
10
|
-
return types.includes('VerifiablePresentation');
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const checkName = (value: string): string => (value === 'signature' ? 'proof' : value);
|
|
14
|
-
|
|
15
|
-
export const verificationFailed = (result: VerifyResult): boolean =>
|
|
16
|
-
Array.isArray(result)
|
|
17
|
-
? result.some(item => item.status === 'Failed')
|
|
18
|
-
: result.errors.length > 0;
|
|
19
|
-
|
|
20
|
-
export const formatVerification = (result: VerifyResult): string[] => {
|
|
21
|
-
if (Array.isArray(result))
|
|
22
|
-
return result.map(item => {
|
|
23
|
-
const check = checkName(item.check);
|
|
24
|
-
if (item.status === 'Success') return `✓ ${check}`;
|
|
25
|
-
return `${item.status === 'Failed' ? '✗' : '!'} ${check}: ${item.details || item.message || 'Check failed'}`;
|
|
26
|
-
});
|
|
27
|
-
return [
|
|
28
|
-
...result.checks.map(check => `✓ ${checkName(check)}`),
|
|
29
|
-
...result.warnings.map(warning => `! ${warning}`),
|
|
30
|
-
...result.errors.map(error => {
|
|
31
|
-
const match = error.match(/^(.+?) error:\s*(.*)$/);
|
|
32
|
-
return match ? `✗ ${checkName(match[1]!)}: ${match[2]}` : `✗ ${error}`;
|
|
33
|
-
}),
|
|
34
|
-
];
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const runVerify = async (
|
|
38
|
-
file: string,
|
|
39
|
-
options: { json?: boolean; didkit?: Promise<Buffer> }
|
|
40
|
-
): Promise<void> => {
|
|
41
|
-
let text: string;
|
|
42
|
-
if (file === '-') {
|
|
43
|
-
const chunks: Buffer[] = [];
|
|
44
|
-
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
|
|
45
|
-
text = Buffer.concat(chunks).toString('utf8');
|
|
46
|
-
} else text = await fs.readFile(file, 'utf8');
|
|
47
|
-
const input: unknown = JSON.parse(text);
|
|
48
|
-
if (!input || typeof input !== 'object' || !('type' in input))
|
|
49
|
-
throw new Error('Expected a credential or presentation JSON object with a type.');
|
|
50
|
-
const presentation = isPresentation(input);
|
|
51
|
-
const types = Array.isArray(input.type) ? input.type : [input.type];
|
|
52
|
-
if (!presentation && !types.includes('VerifiableCredential'))
|
|
53
|
-
throw new Error('Expected VerifiableCredential or VerifiablePresentation in type.');
|
|
54
|
-
const learnCard = await initLearnCard({ ...(options.didkit && { didkit: options.didkit }) });
|
|
55
|
-
const result = presentation
|
|
56
|
-
? await learnCard.invoke.verifyPresentation(input as VP)
|
|
57
|
-
: options.json
|
|
58
|
-
? await learnCard.invoke.verifyCredential(input as VC)
|
|
59
|
-
: await learnCard.invoke.verifyCredential(input as VC, {}, true);
|
|
60
|
-
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
61
|
-
else {
|
|
62
|
-
for (const line of formatVerification(result)) console.log(line);
|
|
63
|
-
console.log('Next: review warnings and issuer trust before accepting this credential.');
|
|
64
|
-
}
|
|
65
|
-
if (verificationFailed(result)) process.exitCode = 1;
|
|
66
|
-
};
|
package/src/webhook.ts
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import type { Server } from 'node:http';
|
|
2
|
-
import { initLearnCard } from '@learncard/init';
|
|
3
|
-
import {
|
|
4
|
-
connect,
|
|
5
|
-
createPrompts,
|
|
6
|
-
ensureIdentity,
|
|
7
|
-
ensureProfile,
|
|
8
|
-
KEYS,
|
|
9
|
-
loadProject,
|
|
10
|
-
localizeSnippet,
|
|
11
|
-
resolveServices,
|
|
12
|
-
saveProject,
|
|
13
|
-
type ProjectOptions,
|
|
14
|
-
PRODUCTION_NETWORK,
|
|
15
|
-
} from './project';
|
|
16
|
-
import { DEFAULT_BADGE, templateCredential } from './send';
|
|
17
|
-
import { WEBHOOK_MJS } from './generated/snippets';
|
|
18
|
-
import { writeSnippet } from './snippet-files';
|
|
19
|
-
import { setupSigning } from './setup-signing';
|
|
20
|
-
import { out } from './out';
|
|
21
|
-
|
|
22
|
-
interface WebhookModule {
|
|
23
|
-
extractBearer: (header: unknown) => string | undefined;
|
|
24
|
-
webhookDedupeKey: (payload: unknown) => string | undefined;
|
|
25
|
-
createWebhookReceiver: (
|
|
26
|
-
verifier: {
|
|
27
|
-
invoke: {
|
|
28
|
-
verifyPresentation: (
|
|
29
|
-
token: string,
|
|
30
|
-
options: { proofFormat: 'jwt' }
|
|
31
|
-
) => Promise<{ errors: unknown[] }>;
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
expectedDid?: string
|
|
35
|
-
) => Server;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const webhookConfig = (
|
|
39
|
-
env: Record<string, string>,
|
|
40
|
-
options: { port?: string },
|
|
41
|
-
runtime: Record<string, string | undefined> = process.env
|
|
42
|
-
): { port: number; expectedDid: string | undefined } => {
|
|
43
|
-
const port = Number(options.port ?? runtime.PORT ?? env.PORT ?? 8787);
|
|
44
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
45
|
-
throw new Error('Port must be 1–65535.');
|
|
46
|
-
return { port, expectedDid: runtime.EXPECTED_NETWORK_DID ?? env.EXPECTED_NETWORK_DID };
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export const closeWebhookReceiver = (server: Server): Promise<void> =>
|
|
50
|
-
new Promise((resolve, reject) => {
|
|
51
|
-
// Verification may be waiting on a remote DID. Do not let it block Ctrl+C indefinitely.
|
|
52
|
-
const timer = setTimeout(() => server.closeAllConnections(), 1000);
|
|
53
|
-
timer.unref();
|
|
54
|
-
server.close(error => {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
if (error) reject(error);
|
|
57
|
-
else resolve();
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
/** Execute the embedded, canonical receiver, never a possibly edited file from the cwd. */
|
|
62
|
-
export const loadWebhookModule = async (): Promise<WebhookModule> => {
|
|
63
|
-
return import(`data:text/javascript;base64,${Buffer.from(WEBHOOK_MJS).toString('base64')}`);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export interface WebhookEvent {
|
|
67
|
-
type: string;
|
|
68
|
-
status?: string;
|
|
69
|
-
issuanceId?: string;
|
|
70
|
-
recipient?: string;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** The embedded receiver logs one space-separated line per event: type, status, issuanceId, recipient DID. */
|
|
74
|
-
export const parseWebhookLogLine = (line: string): WebhookEvent | undefined => {
|
|
75
|
-
const [type, status, issuanceId, recipient] = line.trim().split(/\s+/);
|
|
76
|
-
if (!type) return undefined;
|
|
77
|
-
return {
|
|
78
|
-
type,
|
|
79
|
-
...(status && { status }),
|
|
80
|
-
...(issuanceId && { issuanceId }),
|
|
81
|
-
...(recipient && { recipient }),
|
|
82
|
-
};
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* `--json` mode does not stream: intercept the receiver's own console.log lines (never
|
|
87
|
-
* forwarding them to real stdout) and resolve with everything seen once ISSUANCE_DELIVERED
|
|
88
|
-
* (and ISSUANCE_CLAIMED, if requested) has arrived, or once the timeout elapses.
|
|
89
|
-
*/
|
|
90
|
-
export const collectWebhookEvents = (
|
|
91
|
-
timeoutMs: number,
|
|
92
|
-
waitForClaim: boolean,
|
|
93
|
-
registerStop?: (stop: () => void) => void
|
|
94
|
-
): Promise<WebhookEvent[]> =>
|
|
95
|
-
new Promise(resolve => {
|
|
96
|
-
const events: WebhookEvent[] = [];
|
|
97
|
-
const originalLog = console.log;
|
|
98
|
-
const finish = (): void => {
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
console.log = originalLog;
|
|
101
|
-
resolve(events);
|
|
102
|
-
};
|
|
103
|
-
const timer = setTimeout(finish, Math.max(0, timeoutMs));
|
|
104
|
-
registerStop?.(finish);
|
|
105
|
-
console.log = (...args: unknown[]): void => {
|
|
106
|
-
const [first] = args;
|
|
107
|
-
const event = typeof first === 'string' ? parseWebhookLogLine(first) : undefined;
|
|
108
|
-
if (!event) return;
|
|
109
|
-
events.push(event);
|
|
110
|
-
const delivered = events.some(e => e.type === 'ISSUANCE_DELIVERED');
|
|
111
|
-
const claimed = events.some(e => e.type === 'ISSUANCE_CLAIMED');
|
|
112
|
-
if (delivered && (!waitForClaim || claimed)) finish();
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
export const runWebhook = async (
|
|
117
|
-
email: string | undefined,
|
|
118
|
-
options: ProjectOptions & {
|
|
119
|
-
to?: string;
|
|
120
|
-
url?: string;
|
|
121
|
-
port?: string;
|
|
122
|
-
timeout?: string;
|
|
123
|
-
waitForClaim?: boolean;
|
|
124
|
-
}
|
|
125
|
-
): Promise<void> => {
|
|
126
|
-
const project = await loadProject(process.cwd());
|
|
127
|
-
const { port, expectedDid } = webhookConfig(project.env, options);
|
|
128
|
-
const production = resolveServices(project.env, options.network).network === PRODUCTION_NETWORK;
|
|
129
|
-
if (options.url && production && new URL(options.url).protocol !== 'https:') {
|
|
130
|
-
throw new Error('--url must be a public HTTPS URL when using the production network.');
|
|
131
|
-
}
|
|
132
|
-
const identity = await ensureIdentity(project, options);
|
|
133
|
-
const learnCard = await connect(project, { ...options, lca: true });
|
|
134
|
-
await ensureProfile(learnCard, identity, project);
|
|
135
|
-
const wroteWebhookMjs = await writeSnippet(
|
|
136
|
-
'webhook.mjs',
|
|
137
|
-
localizeSnippet(WEBHOOK_MJS, resolveServices(project.env, options.network))
|
|
138
|
-
);
|
|
139
|
-
const verifier = await initLearnCard({ ...(options.didkit && { didkit: options.didkit }) });
|
|
140
|
-
const { createWebhookReceiver } = await loadWebhookModule();
|
|
141
|
-
const server = createWebhookReceiver(verifier, expectedDid);
|
|
142
|
-
server.requestTimeout = 5000;
|
|
143
|
-
await new Promise<void>((resolve, reject) => {
|
|
144
|
-
server.once('error', reject);
|
|
145
|
-
server.listen(port, () => {
|
|
146
|
-
server.removeListener('error', reject);
|
|
147
|
-
resolve();
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
out.log(`Listening on http://localhost:${port}`);
|
|
151
|
-
try {
|
|
152
|
-
if (!options.url) {
|
|
153
|
-
out.log(
|
|
154
|
-
`Next: expose port ${port} with ngrok/cloudflared and re-run with --url <publicUrl>. No credential sent.`
|
|
155
|
-
);
|
|
156
|
-
out.set({ port, files: wroteWebhookMjs ? ['./webhook.mjs'] : [] });
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (!expectedDid) {
|
|
160
|
-
out.log(
|
|
161
|
-
'Demo: signatures are verified, but any DID is accepted. Set EXPECTED_NETWORK_DID to your trusted network DID before production.'
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
const prompts = createPrompts(options.yes);
|
|
165
|
-
let recipient: string;
|
|
166
|
-
try {
|
|
167
|
-
recipient =
|
|
168
|
-
email || options.to || (await prompts.ask('Recipient email (--to <email>)', ''));
|
|
169
|
-
} finally {
|
|
170
|
-
prompts.close();
|
|
171
|
-
}
|
|
172
|
-
if (!recipient) throw new Error('Provide an email argument or --to <email>.');
|
|
173
|
-
// Template claims need a hosted signer; the demo does not persist signer configuration.
|
|
174
|
-
await setupSigning(project, learnCard, undefined, { persist: false });
|
|
175
|
-
if (!project.env[KEYS.TEMPLATE_URI]) {
|
|
176
|
-
const uri = await learnCard.invoke.createBoost(templateCredential(learnCard.id.did()), {
|
|
177
|
-
name: DEFAULT_BADGE.name,
|
|
178
|
-
category: 'Achievement',
|
|
179
|
-
status: 'LIVE',
|
|
180
|
-
});
|
|
181
|
-
await saveProject(project, { [KEYS.TEMPLATE_URI]: uri });
|
|
182
|
-
}
|
|
183
|
-
const templateUri = project.env[KEYS.TEMPLATE_URI]!;
|
|
184
|
-
// Start collecting before send() so an ISSUANCE_DELIVERED that arrives mid-call isn't missed.
|
|
185
|
-
const timeoutMs = (Number(options.timeout) > 0 ? Number(options.timeout) : 60) * 1000;
|
|
186
|
-
let stopCollectingEvents: (() => void) | undefined;
|
|
187
|
-
const eventsPromise = out.json
|
|
188
|
-
? collectWebhookEvents(timeoutMs, !!options.waitForClaim, stop => {
|
|
189
|
-
stopCollectingEvents = stop;
|
|
190
|
-
})
|
|
191
|
-
: undefined;
|
|
192
|
-
const onServerError = (): void => {};
|
|
193
|
-
if (eventsPromise) server.on('error', onServerError);
|
|
194
|
-
const result = await learnCard.invoke
|
|
195
|
-
.send({
|
|
196
|
-
type: 'boost',
|
|
197
|
-
recipient,
|
|
198
|
-
templateUri,
|
|
199
|
-
options: { webhookUrl: options.url },
|
|
200
|
-
})
|
|
201
|
-
.catch(error => {
|
|
202
|
-
if (eventsPromise) {
|
|
203
|
-
server.removeListener('error', onServerError);
|
|
204
|
-
stopCollectingEvents?.();
|
|
205
|
-
}
|
|
206
|
-
throw error;
|
|
207
|
-
});
|
|
208
|
-
out.log(`Sent. Watch for ISSUANCE_DELIVERED ${result.inbox?.status ?? ''}.`);
|
|
209
|
-
out.log(
|
|
210
|
-
result.inbox?.status === 'PENDING'
|
|
211
|
-
? `Next: click the claim link in your email to see ISSUANCE_CLAIMED. Ctrl+C to stop.`
|
|
212
|
-
: 'Next: this recipient already has LearnCard; no claim event is expected. Ctrl+C to stop.'
|
|
213
|
-
);
|
|
214
|
-
if (eventsPromise) {
|
|
215
|
-
const events = await eventsPromise;
|
|
216
|
-
server.removeListener('error', onServerError);
|
|
217
|
-
out.set({
|
|
218
|
-
port,
|
|
219
|
-
url: options.url,
|
|
220
|
-
templateUri,
|
|
221
|
-
issuanceId: result.inbox?.issuanceId,
|
|
222
|
-
events,
|
|
223
|
-
});
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
await new Promise<void>((resolve, reject) => {
|
|
227
|
-
const stop = (): void => {
|
|
228
|
-
cleanup();
|
|
229
|
-
resolve();
|
|
230
|
-
};
|
|
231
|
-
const fail = (error: Error): void => {
|
|
232
|
-
cleanup();
|
|
233
|
-
reject(error);
|
|
234
|
-
};
|
|
235
|
-
const cleanup = (): void => {
|
|
236
|
-
process.removeListener('SIGINT', stop);
|
|
237
|
-
process.removeListener('SIGTERM', stop);
|
|
238
|
-
server.removeListener('error', fail);
|
|
239
|
-
};
|
|
240
|
-
process.once('SIGINT', stop);
|
|
241
|
-
process.once('SIGTERM', stop);
|
|
242
|
-
server.once('error', fail);
|
|
243
|
-
});
|
|
244
|
-
} finally {
|
|
245
|
-
await closeWebhookReceiver(server);
|
|
246
|
-
}
|
|
247
|
-
};
|
package/src/whoami.test.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { Command } from 'commander';
|
|
3
|
-
|
|
4
|
-
import { registerWhoamiCommand, summarizeServiceAccounts } from './whoami';
|
|
5
|
-
import type { AuthGrantWithActAs } from './auth-grant';
|
|
6
|
-
|
|
7
|
-
describe('whoami command', () => {
|
|
8
|
-
it('registers with --network and --json only', () => {
|
|
9
|
-
const program = new Command();
|
|
10
|
-
registerWhoamiCommand(program, async () => {});
|
|
11
|
-
const cmd = program.commands.find(c => c.name() === 'whoami');
|
|
12
|
-
expect(cmd).toBeDefined();
|
|
13
|
-
expect(
|
|
14
|
-
program
|
|
15
|
-
.createHelp()
|
|
16
|
-
.visibleOptions(cmd!)
|
|
17
|
-
.map(o => o.long)
|
|
18
|
-
.filter(l => l !== '--help')
|
|
19
|
-
.sort()
|
|
20
|
-
).toEqual(['--json', '--network']);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('summarizeServiceAccounts', () => {
|
|
25
|
-
it('shapes active grants as { name, scope, actAs } for --json, dropping revoked ones', () => {
|
|
26
|
-
const grants: AuthGrantWithActAs[] = [
|
|
27
|
-
{
|
|
28
|
-
id: 'g1',
|
|
29
|
-
name: 'ea-clr-issuer',
|
|
30
|
-
status: 'active',
|
|
31
|
-
scope: 'inbox:write',
|
|
32
|
-
actAs: 'sc-greenville,sc-north',
|
|
33
|
-
},
|
|
34
|
-
{ id: 'g2', name: 'star-issuer', status: 'active', scope: 'inbox:write', actAs: '*' },
|
|
35
|
-
{ id: 'g3', name: 'no-delegation', status: 'active', scope: 'inbox:write' },
|
|
36
|
-
{ id: 'g4', name: 'old-issuer', status: 'revoked', scope: 'inbox:write', actAs: '*' },
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
expect(summarizeServiceAccounts(grants)).toEqual([
|
|
40
|
-
{ name: 'ea-clr-issuer', scope: 'inbox:write', actAs: 'sc-greenville,sc-north' },
|
|
41
|
-
{ name: 'star-issuer', scope: 'inbox:write', actAs: '*' },
|
|
42
|
-
{ name: 'no-delegation', scope: 'inbox:write', actAs: undefined },
|
|
43
|
-
]);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('returns an empty array when there are no grants', () => {
|
|
47
|
-
expect(summarizeServiceAccounts([])).toEqual([]);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('LEARNCARD_AS', () => {
|
|
52
|
-
it('is the env fallback for --as on inbox list', async () => {
|
|
53
|
-
const { registerInboxCommand } = await import('./inbox');
|
|
54
|
-
const program = new Command().exitOverride();
|
|
55
|
-
let seen: Record<string, unknown> | undefined;
|
|
56
|
-
registerInboxCommand(program, async (_cmd, options) => {
|
|
57
|
-
seen = options as Record<string, unknown>;
|
|
58
|
-
});
|
|
59
|
-
process.env.LEARNCARD_AS = 'cs-exampleville';
|
|
60
|
-
try {
|
|
61
|
-
await program.parseAsync(['node', 'learncard', 'inbox', 'list']);
|
|
62
|
-
} finally {
|
|
63
|
-
delete process.env.LEARNCARD_AS;
|
|
64
|
-
}
|
|
65
|
-
expect(seen?.as).toBe('cs-exampleville');
|
|
66
|
-
});
|
|
67
|
-
});
|
package/src/whoami.ts
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import type { Command } from 'commander';
|
|
2
|
-
import type { AuthGrantType } from '@learncard/types';
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
connect,
|
|
6
|
-
connectAsDidWeb,
|
|
7
|
-
loadProject,
|
|
8
|
-
resolveServices,
|
|
9
|
-
type ProjectOptions,
|
|
10
|
-
} from './project';
|
|
11
|
-
import { out } from './out';
|
|
12
|
-
import type { RunCommand } from './doctor';
|
|
13
|
-
import { describeActAs, getGrantActAs } from './auth-grant';
|
|
14
|
-
|
|
15
|
-
export interface ManagedSummary {
|
|
16
|
-
profileId: string;
|
|
17
|
-
displayName: string;
|
|
18
|
-
did: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ServiceAccountSummary {
|
|
22
|
-
name: string;
|
|
23
|
-
scope: string;
|
|
24
|
-
actAs?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Only active grants are relevant here; revoked ones are noise for this summary. */
|
|
28
|
-
export const summarizeServiceAccounts = (
|
|
29
|
-
grants: Array<Partial<AuthGrantType>>
|
|
30
|
-
): ServiceAccountSummary[] =>
|
|
31
|
-
grants
|
|
32
|
-
.filter(grant => grant.status === 'active')
|
|
33
|
-
.map(grant => ({
|
|
34
|
-
name: grant.name ?? '',
|
|
35
|
-
scope: grant.scope ?? '',
|
|
36
|
-
actAs: getGrantActAs(grant),
|
|
37
|
-
}));
|
|
38
|
-
|
|
39
|
-
export const runWhoami = async (options: ProjectOptions): Promise<void> => {
|
|
40
|
-
const project = await loadProject(process.cwd());
|
|
41
|
-
if (!project.env.SECURE_SEED) {
|
|
42
|
-
throw new Error(
|
|
43
|
-
'No identity in this folder. Run `npx @learncard/cli send you@example.com` or `org apply` first.'
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
const services = resolveServices(project.env, options.network);
|
|
47
|
-
const learnCard = await connect(project, options);
|
|
48
|
-
const profile = await learnCard.invoke.getProfile();
|
|
49
|
-
|
|
50
|
-
out.log(
|
|
51
|
-
profile
|
|
52
|
-
? `You are "${profile.displayName}" (${profile.profileId}) on ${services.network}`
|
|
53
|
-
: `Seed present but no profile on ${services.network} yet (PROFILE_ID=${project.env.PROFILE_ID ?? '?'}).`
|
|
54
|
-
);
|
|
55
|
-
out.log(` did: ${learnCard.id.did()}`);
|
|
56
|
-
if (project.env.SIGNING_AUTHORITY_NAME)
|
|
57
|
-
out.log(` signing authority: ${project.env.SIGNING_AUTHORITY_NAME}`);
|
|
58
|
-
|
|
59
|
-
const managerDid = project.env.ORG_PROFILE_MANAGER_DID;
|
|
60
|
-
const managed: ManagedSummary[] = [];
|
|
61
|
-
if (managerDid) {
|
|
62
|
-
const manager = await connectAsDidWeb(project, options, managerDid);
|
|
63
|
-
let cursor: string | undefined;
|
|
64
|
-
do {
|
|
65
|
-
const page = await manager.invoke.getManagedProfiles({ limit: 100, cursor });
|
|
66
|
-
for (const record of page.records) {
|
|
67
|
-
managed.push({
|
|
68
|
-
profileId: record.profileId,
|
|
69
|
-
displayName: record.displayName,
|
|
70
|
-
did: record.did,
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
cursor = page.hasMore ? (page.cursor ?? undefined) : undefined;
|
|
74
|
-
} while (cursor);
|
|
75
|
-
|
|
76
|
-
out.log(` manager: ${managerDid}`);
|
|
77
|
-
if (managed.length) {
|
|
78
|
-
out.log('You can act as (--as <profileId> or LEARNCARD_AS=<profileId>):');
|
|
79
|
-
for (const entry of managed)
|
|
80
|
-
out.log(` ${entry.profileId.padEnd(24)} ${entry.displayName}`);
|
|
81
|
-
} else {
|
|
82
|
-
out.log(
|
|
83
|
-
' no managed profiles yet — add them under profileManager.managed and run `org apply`.'
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const grants = profile ? ((await learnCard.invoke.getAuthGrants()) ?? []) : [];
|
|
89
|
-
const serviceAccounts = summarizeServiceAccounts(grants);
|
|
90
|
-
if (serviceAccounts.length) {
|
|
91
|
-
out.log('Service accounts:');
|
|
92
|
-
const nameWidth = Math.max(...serviceAccounts.map(entry => entry.name.length));
|
|
93
|
-
const scopeWidth = Math.max(...serviceAccounts.map(entry => entry.scope.length));
|
|
94
|
-
for (const entry of serviceAccounts)
|
|
95
|
-
out.log(
|
|
96
|
-
` ${entry.name.padEnd(nameWidth)} ${entry.scope.padEnd(scopeWidth)} ${describeActAs(entry.actAs)}`
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (process.env.LEARNCARD_AS)
|
|
101
|
-
out.log(
|
|
102
|
-
`LEARNCARD_AS is set: commands that support --as will act as "${process.env.LEARNCARD_AS}".`
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
out.set({
|
|
106
|
-
network: services.network,
|
|
107
|
-
profileId: profile?.profileId ?? project.env.PROFILE_ID,
|
|
108
|
-
displayName: profile?.displayName,
|
|
109
|
-
did: learnCard.id.did(),
|
|
110
|
-
signingAuthority: project.env.SIGNING_AUTHORITY_NAME,
|
|
111
|
-
managerDid,
|
|
112
|
-
managed,
|
|
113
|
-
serviceAccounts,
|
|
114
|
-
actingAs: process.env.LEARNCARD_AS,
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
export const registerWhoamiCommand = (program: Command, run: RunCommand): void => {
|
|
119
|
-
program
|
|
120
|
-
.command('whoami')
|
|
121
|
-
.description('Show the identity in this folder and the managed profiles --as can target.')
|
|
122
|
-
.option('--network <url>', 'network tRPC URL or staging (default: production)')
|
|
123
|
-
.option('--json', 'print a single JSON result on stdout')
|
|
124
|
-
.action(options =>
|
|
125
|
-
run('whoami', options, async didkit => {
|
|
126
|
-
await runWhoami({ ...options, didkit });
|
|
127
|
-
})
|
|
128
|
-
);
|
|
129
|
-
};
|
package/tsconfig.json
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.library.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"lib": ["es2022", "DOM"],
|
|
5
|
-
"module": "esnext",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"resolveJsonModule": true,
|
|
8
|
-
"removeComments": true,
|
|
9
|
-
"preserveConstEnums": true,
|
|
10
|
-
"strict": true,
|
|
11
|
-
"alwaysStrict": true,
|
|
12
|
-
"strictNullChecks": true,
|
|
13
|
-
"noUncheckedIndexedAccess": true,
|
|
14
|
-
"jsx": "react",
|
|
15
|
-
|
|
16
|
-
"noImplicitAny": true,
|
|
17
|
-
"noImplicitReturns": true,
|
|
18
|
-
"noImplicitThis": true,
|
|
19
|
-
"noUnusedLocals": true,
|
|
20
|
-
"noUnusedParameters": true,
|
|
21
|
-
"allowUnreachableCode": false,
|
|
22
|
-
"noFallthroughCasesInSwitch": true,
|
|
23
|
-
|
|
24
|
-
"target": "es2017",
|
|
25
|
-
"outDir": "dist",
|
|
26
|
-
"declaration": true,
|
|
27
|
-
"declarationMap": true,
|
|
28
|
-
"sourceMap": true,
|
|
29
|
-
|
|
30
|
-
"esModuleInterop": true,
|
|
31
|
-
"allowSyntheticDefaultImports": true,
|
|
32
|
-
"allowJs": false,
|
|
33
|
-
"skipLibCheck": true,
|
|
34
|
-
"forceConsistentCasingInFileNames": true
|
|
35
|
-
},
|
|
36
|
-
"include": ["src/**/*", "scripts/**/*.ts"],
|
|
37
|
-
"exclude": ["./dist/**/*", "./node_modules/**/*"]
|
|
38
|
-
}
|