@learncard/cli 3.5.0 → 3.6.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 (53) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +230 -2
  3. package/dist/index.js +4337 -986
  4. package/examples/branded.network.yaml +20 -0
  5. package/examples/delegated-service-account.network.yaml +23 -0
  6. package/examples/minimal.network.yaml +7 -0
  7. package/examples/self-hosted-signing.network.yaml +10 -0
  8. package/examples/service-account.network.yaml +13 -0
  9. package/examples/state-districts.network.yaml +36 -0
  10. package/package.json +21 -17
  11. package/src/auth-grant.test.ts +54 -0
  12. package/src/auth-grant.ts +34 -0
  13. package/src/clr/validate.test.ts +65 -0
  14. package/src/clr/validate.ts +242 -0
  15. package/src/clr.ts +119 -0
  16. package/src/demo-inbox-refresh.test.ts +737 -0
  17. package/src/demo-inbox-refresh.ts +804 -0
  18. package/src/demo-refresh-command.test.ts +57 -0
  19. package/src/demo-refresh-command.ts +22 -0
  20. package/src/demo-refresh-ui.test.ts +66 -0
  21. package/src/demo-refresh-ui.ts +65 -0
  22. package/src/demo-refresh.test.ts +140 -0
  23. package/src/demo-refresh.ts +309 -0
  24. package/src/doctor/checks.test.ts +448 -0
  25. package/src/doctor/checks.ts +497 -0
  26. package/src/doctor.test.ts +67 -0
  27. package/src/doctor.ts +118 -0
  28. package/src/inbox.test.ts +257 -0
  29. package/src/inbox.ts +221 -0
  30. package/src/index.tsx +70 -8
  31. package/src/init.ts +1 -1
  32. package/src/open.ts +1 -1
  33. package/src/org/apply.test.ts +1108 -0
  34. package/src/org/apply.ts +924 -0
  35. package/src/org/branding.test.ts +60 -0
  36. package/src/org/diff.ts +14 -0
  37. package/src/org/load.ts +50 -0
  38. package/src/org/schema.test.ts +256 -0
  39. package/src/org/schema.ts +216 -0
  40. package/src/org.ts +124 -0
  41. package/src/project.test.ts +26 -1
  42. package/src/project.ts +105 -10
  43. package/src/promote.test.ts +142 -0
  44. package/src/promote.ts +202 -0
  45. package/src/refresh.test.ts +86 -0
  46. package/src/refresh.ts +93 -0
  47. package/src/send.test.ts +278 -2
  48. package/src/send.ts +152 -24
  49. package/src/setup-signing.ts +1 -1
  50. package/src/status.ts +2 -4
  51. package/src/whoami.test.ts +67 -0
  52. package/src/whoami.ts +129 -0
  53. package/tsconfig.json +1 -1
package/src/send.ts CHANGED
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { randomUUID } from 'node:crypto';
4
4
  import {
5
5
  connect,
6
+ connectAsManaged,
6
7
  createPrompts,
7
8
  ensureIdentity,
8
9
  ensureProfile,
@@ -54,6 +55,7 @@ export const templateCredential = (issuerDid: string, badge: Badge = DEFAULT_BAD
54
55
  };
55
56
 
56
57
  type SendOptions = ProjectOptions & {
58
+ as?: string;
57
59
  badge?: string;
58
60
  description?: string;
59
61
  template?: boolean;
@@ -120,20 +122,113 @@ export const personalizeSendFromTemplateMjs = (deliveryOptions?: SendDeliveryOpt
120
122
  deliveryOptions
121
123
  );
122
124
 
125
+ /** Bind a generated script to its managed issuer, never the parent seed identity. */
126
+ export const withManagedIssuer = (content: string, managedDid: string): string =>
127
+ content
128
+ .replace(
129
+ 'const learnCard = await initLearnCard(',
130
+ `if (process.env.MANAGED_DID !== ${JSON.stringify(managedDid)}) {
131
+ throw new Error('MANAGED_DID is missing or changed. Re-run the CLI with --as to send as the intended profile.');
132
+ }
133
+ const learnCard = await initLearnCard(`
134
+ )
135
+ .replace(
136
+ 'seed: process.env.SECURE_SEED,',
137
+ 'seed: process.env.SECURE_SEED, didWeb: process.env.MANAGED_DID,'
138
+ )
139
+ .replace(
140
+ /if \(!\(await learnCard\.invoke\.getProfile\(\)\)\) \{[\s\S]*?\n\}/,
141
+ `if (!(await learnCard.invoke.getProfile())) {
142
+ throw new Error('The managed issuer profile could not be found.');
143
+ }`
144
+ );
145
+
123
146
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
124
147
  const PHONE = /^\+?\d{10,15}$/;
148
+ const DID = /^did:[a-z0-9]+:.+$/;
149
+ const PROFILE_ID = /^[a-z0-9-]{3,40}$/;
150
+
151
+ export type RecipientKind = 'email' | 'phone' | 'did' | 'profileId';
152
+
153
+ export const classifyRecipient = (value: string): RecipientKind => {
154
+ if (EMAIL.test(value)) return 'email';
155
+ if (PHONE.test(value)) return 'phone';
156
+ if (DID.test(value)) return 'did';
157
+ if (PROFILE_ID.test(value)) return 'profileId';
158
+ throw new Error(
159
+ `"${value}" is not an email, phone number, profile ID, or DID. Example: npx @learncard/cli send you@yourdomain.com`
160
+ );
161
+ };
125
162
 
126
- export const runSend = async (recipientEmail: string, options: SendOptions): Promise<void> => {
127
- if (!EMAIL.test(recipientEmail) && !PHONE.test(recipientEmail))
163
+ /** Domains reserved for documentation (RFC 2606 / RFC 6761). Mail to them goes nowhere. */
164
+ const PLACEHOLDER_DOMAIN = /(^|\.)example\.(com|net|org)$|(^|\.)(example|test|invalid|localhost)$/i;
165
+
166
+ export const RECIPIENT_PROMPT =
167
+ 'Where should we send your first badge? (email, phone number, profile ID, or DID)';
168
+
169
+ export const isPlaceholderRecipient = (recipient: string): boolean => {
170
+ const at = recipient.lastIndexOf('@');
171
+ return at !== -1 && PLACEHOLDER_DOMAIN.test(recipient.slice(at + 1));
172
+ };
173
+
174
+ /**
175
+ * Returns a human-readable reason `recipient` cannot receive a badge, or `undefined` if it can.
176
+ * Placeholder addresses like you@example.com pass a naive email check but are undeliverable,
177
+ * so they are called out specifically — docs readers paste them verbatim.
178
+ */
179
+ export const invalidRecipientReason = (recipient: string): string | undefined => {
180
+ if (!recipient) return 'Enter an email, phone number, profile ID, or DID.';
181
+ if (EMAIL.test(recipient) && isPlaceholderRecipient(recipient))
182
+ return `"${recipient}" is a placeholder address — nobody will receive the badge. Use a real email you can open.`;
183
+ if (
184
+ !EMAIL.test(recipient) &&
185
+ !PHONE.test(recipient) &&
186
+ !DID.test(recipient) &&
187
+ !PROFILE_ID.test(recipient)
188
+ )
189
+ return `"${recipient}" is not an email, phone number, profile ID, or DID.`;
190
+ return undefined;
191
+ };
192
+
193
+ type Prompts = ReturnType<typeof createPrompts>;
194
+
195
+ /**
196
+ * Settle on a deliverable recipient. When a human is at the terminal, bad or missing input
197
+ * re-prompts instead of failing; non-interactive runs throw a clear error.
198
+ */
199
+ export const resolveRecipient = async (
200
+ recipient: string | undefined,
201
+ prompts: Prompts
202
+ ): Promise<string> => {
203
+ let candidate = recipient?.trim() ?? '';
204
+ if (!candidate && !prompts.interactive)
128
205
  throw new Error(
129
- `"${recipientEmail}" is not an email address or phone number. Example: npx @learncard/cli send you@example.com`
206
+ 'A recipient is required when running non-interactively. Example: npx @learncard/cli send you@yourdomain.com --yes'
130
207
  );
208
+ if (!candidate) candidate = (await prompts.ask(RECIPIENT_PROMPT, '')).trim();
209
+ let reason = invalidRecipientReason(candidate);
210
+ while (reason) {
211
+ if (!prompts.interactive)
212
+ throw new Error(`${reason} Example: npx @learncard/cli send you@yourdomain.com`);
213
+ out.log(reason);
214
+ candidate = (await prompts.ask(RECIPIENT_PROMPT, '')).trim();
215
+ reason = invalidRecipientReason(candidate);
216
+ }
217
+ return candidate;
218
+ };
219
+
220
+ export const runSend = async (
221
+ recipient: string | undefined,
222
+ options: SendOptions
223
+ ): Promise<void> => {
131
224
  const cwd = process.cwd();
132
225
  const project = await loadProject(cwd);
133
226
  const prompts = createPrompts(options.yes);
227
+ let resolvedRecipient: string;
134
228
  let displayName: string | undefined;
135
229
  let badge: Badge;
136
230
  try {
231
+ resolvedRecipient = await resolveRecipient(recipient, prompts);
137
232
  const needsName = !project.env.PROFILE_ID && !options.profileId && !options.name;
138
233
  displayName = needsName
139
234
  ? await prompts.ask('Display name for your issuer profile', 'My Organization')
@@ -145,12 +240,34 @@ export const runSend = async (recipientEmail: string, options: SendOptions): Pro
145
240
  } finally {
146
241
  prompts.close();
147
242
  }
243
+ const recipientKind = classifyRecipient(resolvedRecipient);
148
244
  const identity = await ensureIdentity(project, { ...options, name: displayName, yes: true });
149
- const useTemplate = options.template || !!options.templateUri;
150
- const learnCard = useTemplate
151
- ? await connect(project, { ...options, lca: true })
152
- : await connect(project, options);
153
- await ensureProfile(learnCard, identity, project);
245
+ const asManaged = options.as ? await connectAsManaged(project, options, options.as) : undefined;
246
+ const hostedSigning = !!project.env.SIGNING_AUTHORITY_NAME;
247
+ const explicitTemplate = options.templateUri ? true : options.template;
248
+ const useTemplate = !asManaged && (explicitTemplate ?? hostedSigning);
249
+ if (useTemplate && explicitTemplate === undefined) {
250
+ out.log(
251
+ `Signing through the registered signing authority "${project.env.SIGNING_AUTHORITY_NAME}" (pass --no-template to sign with the local key instead).`
252
+ );
253
+ }
254
+ if (asManaged && explicitTemplate) {
255
+ out.log("--as signs with the managed profile's key; --template is ignored.");
256
+ }
257
+ const learnCard = asManaged
258
+ ? asManaged
259
+ : useTemplate
260
+ ? await connect(project, { ...options, lca: true })
261
+ : await connect(project, options);
262
+ if (asManaged) {
263
+ const managed = await learnCard.invoke.getProfile();
264
+ if (!managed) throw new Error(`Could not sign in as managed profile "${options.as}".`);
265
+ out.log(
266
+ `Acting as "${managed.displayName}" (${managed.profileId}) — a profile you manage.`
267
+ );
268
+ } else {
269
+ await ensureProfile(learnCard, identity, project);
270
+ }
154
271
 
155
272
  const effectiveSendOptions = sendOptions(options);
156
273
  let result;
@@ -184,7 +301,7 @@ export const runSend = async (recipientEmail: string, options: SendOptions): Pro
184
301
  }
185
302
  result = await learnCard.invoke.send({
186
303
  type: 'boost',
187
- recipient: recipientEmail,
304
+ recipient: resolvedRecipient,
188
305
  templateUri: options.templateUri ?? project.env.TEMPLATE_URI!,
189
306
  ...effectiveSendOptions,
190
307
  });
@@ -194,7 +311,7 @@ export const runSend = async (recipientEmail: string, options: SendOptions): Pro
194
311
  );
195
312
  result = await learnCard.invoke.send({
196
313
  type: 'boost',
197
- recipient: recipientEmail,
314
+ recipient: resolvedRecipient,
198
315
  signedCredential: credential,
199
316
  ...effectiveSendOptions,
200
317
  });
@@ -202,11 +319,15 @@ export const runSend = async (recipientEmail: string, options: SendOptions): Pro
202
319
  out.log('');
203
320
  if (result.inbox?.status === 'PENDING') {
204
321
  out.log(
205
- `Sent. ${recipientEmail} will get a claim email. You can also share this link directly:\n${result.inbox.claimUrl}`
322
+ `Sent. ${resolvedRecipient} will get a claim ${recipientKind === 'phone' ? 'text' : 'email'}. You can also share this link directly:\n${result.inbox.claimUrl}`
323
+ );
324
+ } else if (recipientKind === 'email' || recipientKind === 'phone') {
325
+ out.log(
326
+ `Delivered. ${resolvedRecipient} already uses LearnCard — the credential is in their wallet.`
206
327
  );
207
328
  } else {
208
329
  out.log(
209
- `Delivered. ${recipientEmail} already uses LearnCard — the credential is in their wallet.`
330
+ `Delivered directly to ${resolvedRecipient} — it is waiting in their LearnCard wallet.`
210
331
  );
211
332
  }
212
333
  out.log(`Reusable template for this badge: ${result.uri}`);
@@ -214,26 +335,33 @@ export const runSend = async (recipientEmail: string, options: SendOptions): Pro
214
335
  const sendPath = path.join(cwd, filename);
215
336
  let wroteSendFile = false;
216
337
  if (!(await fs.stat(sendPath).catch(() => null))) {
217
- await fs.writeFile(
218
- sendPath,
219
- localizeSnippet(
220
- useTemplate
221
- ? personalizeSendFromTemplateMjs(effectiveSendOptions.options)
222
- : personalizeSendMjs(identity.displayName, badge, effectiveSendOptions.options),
223
- resolveServices(project.env, options.network)
224
- )
225
- );
338
+ let content = useTemplate
339
+ ? personalizeSendFromTemplateMjs(effectiveSendOptions.options)
340
+ : personalizeSendMjs(identity.displayName, badge, effectiveSendOptions.options);
341
+ content = localizeSnippet(content, resolveServices(project.env, options.network));
342
+ if (asManaged) {
343
+ const managedDid = learnCard.id.did();
344
+ await saveProject(project, { MANAGED_DID: managedDid });
345
+ content = withManagedIssuer(content, managedDid);
346
+ }
347
+ await fs.writeFile(sendPath, content);
226
348
  wroteSendFile = true;
227
349
  out.log(
228
- `\nThe code that just ran is in ./${filename} — run it yourself:\n npm install @learncard/init\n node --env-file=.env ${filename} ${recipientEmail}`
350
+ `\nThe code that just ran is in ./${filename} — run it yourself:\n npm install @learncard/init\n node --env-file=.env ${filename} ${resolvedRecipient}`
351
+ );
352
+ } else if (asManaged) {
353
+ out.log(
354
+ `Existing ./${filename} was not changed and may use a different issuer. Repeat this send with the CLI --as ${options.as} instead.`
229
355
  );
230
356
  }
231
357
  out.log(`Check whether it was claimed: npx @learncard/cli status ${result.activityId}`);
232
358
  out.log(`See it in the app: npx @learncard/cli open${options.template ? ' template' : ''}`);
233
359
  out.set({
234
- profileId: identity.profileId,
360
+ profileId: options.as ?? identity.profileId,
361
+ ...(options.as && { onBehalfOf: identity.profileId }),
235
362
  did: learnCard.id.did(),
236
- recipient: recipientEmail,
363
+ recipient: resolvedRecipient,
364
+ recipientKind,
237
365
  status: result.inbox?.status === 'PENDING' ? 'PENDING' : 'ISSUED',
238
366
  ...(result.inbox?.claimUrl && { claimUrl: result.inbox.claimUrl }),
239
367
  templateUri: result.uri,
@@ -180,6 +180,6 @@ export const runSetupSigning = async (options: SetupSigningOptions): Promise<voi
180
180
  alreadyConfigured: authority.alreadyConfigured,
181
181
  });
182
182
  out.log(
183
- 'Send from a template: npx @learncard/cli send you@example.com --template\nSee it in the app: npx @learncard/cli open'
183
+ 'Send from a template: npx @learncard/cli send --template\nSee it in the app: npx @learncard/cli open'
184
184
  );
185
185
  };
package/src/status.ts CHANGED
@@ -46,9 +46,7 @@ type StatusOptions = ProjectOptions & { limit?: string; event?: string };
46
46
  export const runStatus = async (activityId: string | undefined, options: StatusOptions) => {
47
47
  const project = await loadProject(process.cwd());
48
48
  if (!project.env.SECURE_SEED)
49
- throw new Error(
50
- 'No SECURE_SEED in .env. Send something first: npx @learncard/cli send you@example.com'
51
- );
49
+ throw new Error('No SECURE_SEED in .env. Send something first: npx @learncard/cli send');
52
50
  await ensureIdentity(project, options);
53
51
  const learnCard = await connect(project, options);
54
52
 
@@ -73,7 +71,7 @@ export const runStatus = async (activityId: string | undefined, options: StatusO
73
71
  });
74
72
  const records = page.records as ActivityEvent[];
75
73
  if (!records.length) {
76
- out.log('No sends yet. Try: npx @learncard/cli send you@example.com');
74
+ out.log('No sends yet. Try: npx @learncard/cli send');
77
75
  out.set({ activities: [] });
78
76
  return;
79
77
  }
@@ -0,0 +1,67 @@
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 ADDED
@@ -0,0 +1,129 @@
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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "extends": "../../tsconfig.library.json",
3
3
  "compilerOptions": {
4
- "lib": ["es2017"],
4
+ "lib": ["es2022"],
5
5
  "module": "esnext",
6
6
  "moduleResolution": "bundler",
7
7
  "resolveJsonModule": true,