@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
package/src/init.ts DELETED
@@ -1,26 +0,0 @@
1
- import {
2
- connect,
3
- ensureIdentity,
4
- ensureProfile,
5
- loadProject,
6
- type ProjectOptions,
7
- } from './project';
8
- import { out } from './out';
9
-
10
- /** Create the identity and profile every other command needs, without sending anything. */
11
- export const runInit = async (options: ProjectOptions): Promise<void> => {
12
- const project = await loadProject(process.cwd());
13
- const fresh = !project.env.SECURE_SEED;
14
- const identity = await ensureIdentity(project, options);
15
- const learnCard = await connect(project, options);
16
- await ensureProfile(learnCard, identity, project);
17
- out.set({
18
- profileId: identity.profileId,
19
- displayName: identity.displayName,
20
- did: learnCard.id.did(),
21
- created: fresh,
22
- envPath: project.envPath,
23
- });
24
- out.log(fresh ? 'Ready. Your identity is in .env (keep it out of git).' : 'Already set up.');
25
- out.log('Next: npx @learncard/cli send');
26
- };
package/src/open.test.ts DELETED
@@ -1,40 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { impliesNoBrowser, openPath, signInUrl } from './open';
3
- import { appUrlFor, PRODUCTION_NETWORK, STAGING_NETWORK } from './project';
4
-
5
- describe('open', () => {
6
- it('maps networks to their app', () => {
7
- expect(appUrlFor(PRODUCTION_NETWORK)).toBe('https://learncard.app');
8
- expect(appUrlFor(STAGING_NETWORK)).toBe('https://staging.learncard.ai');
9
- expect(appUrlFor('http://localhost:4000/trpc', 'http://localhost:3000/')).toBe(
10
- 'http://localhost:3000'
11
- );
12
- });
13
-
14
- it('needs the matching .env value for resource targets', () => {
15
- expect(openPath('portal', {})).toBe('/app-store/developer');
16
- expect(openPath('template', {})).toBeNull();
17
- expect(openPath('template', { TEMPLATE_URI: 'lc:network:x/trpc:boost:1' })).toBe(
18
- '/app-store/developer?template=lc%3Anetwork%3Ax%2Ftrpc%3Aboost%3A1'
19
- );
20
- });
21
-
22
- it('builds the sign-in URL with next as an app path, seed only in the fragment when asked', () => {
23
- const plain = signInUrl('https://learncard.app', '/wallet');
24
- expect(plain).toBe('https://learncard.app/developer/sign-in?next=%2Fwallet');
25
- const withSeed = new URL(signInUrl('https://learncard.app', '/wallet', 'ab'.repeat(32)));
26
- expect(withSeed.hash).toBe(`#seed=${'ab'.repeat(32)}`);
27
- expect(withSeed.search).not.toContain('ab'.repeat(32));
28
- });
29
-
30
- it('never launches a browser when headless, --json, or --no-browser', () => {
31
- // A real TTY with no flags: fine to launch.
32
- expect(impliesNoBrowser({}, true)).toBe(false);
33
- // --json always implies headless, even at a real TTY.
34
- expect(impliesNoBrowser({ json: true }, true)).toBe(true);
35
- // No TTY (piped/CI) implies headless regardless of flags.
36
- expect(impliesNoBrowser({}, false)).toBe(true);
37
- // Explicit --no-browser (commander's `browser: false`) always wins.
38
- expect(impliesNoBrowser({ browser: false }, true)).toBe(true);
39
- });
40
- });
package/src/open.ts DELETED
@@ -1,186 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import clipboard from 'clipboardy';
3
-
4
- import {
5
- appUrlFor,
6
- loadProject,
7
- PRODUCTION_NETWORK,
8
- resolveServices,
9
- STAGING_NETWORK,
10
- type ProjectOptions,
11
- } from './project';
12
- import { out } from './out';
13
-
14
- export type OpenTarget = 'portal' | 'wallet' | 'template' | 'contract' | 'integration';
15
-
16
- export const OPEN_TARGETS: Record<OpenTarget, string> = {
17
- portal: 'the Developer Portal',
18
- wallet: 'your wallet',
19
- template: 'the template you last sent from',
20
- contract: 'your consent contract',
21
- integration: "your Claim button's integration",
22
- };
23
-
24
- /** App path for a target, given the project's .env. Returns null when the .env lacks what the target needs. */
25
- export const openPath = (target: OpenTarget, env: Record<string, string>): string | null => {
26
- switch (target) {
27
- case 'portal':
28
- return '/app-store/developer';
29
- case 'wallet':
30
- return '/wallet';
31
- case 'template':
32
- return env.TEMPLATE_URI
33
- ? `/app-store/developer?template=${encodeURIComponent(env.TEMPLATE_URI)}`
34
- : null;
35
- case 'contract':
36
- return env.CONTRACT_URI
37
- ? `/app-store/developer?contract=${encodeURIComponent(env.CONTRACT_URI)}`
38
- : null;
39
- case 'integration':
40
- return env.INTEGRATION_ID
41
- ? `/app-store/developer/integrations/${encodeURIComponent(env.INTEGRATION_ID)}`
42
- : null;
43
- }
44
- };
45
-
46
- export const signInUrl = (appUrl: string, next: string, seedInFragment?: string): string => {
47
- const url = new URL('/developer/sign-in', appUrl);
48
- url.searchParams.set('next', next);
49
- if (seedInFragment) url.hash = `seed=${seedInFragment}`;
50
- return url.toString();
51
- };
52
-
53
- const CLIPBOARD_TTL_MS = 60_000;
54
-
55
- /** Only plain http(s) URLs may reach the OS browser launcher or seed a sign-in link. */
56
- const isHttpUrl = (value: string): boolean => {
57
- try {
58
- return ['http:', 'https:'].includes(new URL(value).protocol);
59
- } catch {
60
- return false;
61
- }
62
- };
63
-
64
- const launchBrowser = (url: string): void => {
65
- const [cmd, args] =
66
- process.platform === 'darwin'
67
- ? ['open', [url]]
68
- : process.platform === 'win32'
69
- ? // No shell involved: rundll32 receives the URL as a normal argv entry, so
70
- // cmd.exe metacharacters (&, |, ^, etc.) in the URL are never interpreted.
71
- ['rundll32', ['url.dll,FileProtocolHandler', url]]
72
- : ['xdg-open', [url]];
73
- spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
74
- };
75
-
76
- /**
77
- * Clears the clipboard after the TTL, or immediately on Ctrl-C, but only if it still holds the
78
- * seed we wrote (the user may have copied something else). Resolves once the clipboard is safe.
79
- */
80
- const waitForClipboardClear = (seed: string): Promise<void> =>
81
- new Promise(resolve => {
82
- let settled = false;
83
- const finish = async (): Promise<void> => {
84
- if (settled) return;
85
- settled = true;
86
- clearTimeout(timer);
87
- process.removeListener('SIGINT', onSigint);
88
- if ((await clipboard.read().catch(() => '')) === seed)
89
- await clipboard.write('').catch(() => {});
90
- out.log('Clipboard cleared.');
91
- resolve();
92
- };
93
- const onSigint = (): void => {
94
- void finish();
95
- };
96
- const timer = setTimeout(() => void finish(), CLIPBOARD_TTL_MS);
97
- process.once('SIGINT', onSigint);
98
- });
99
-
100
- export interface OpenOptions extends ProjectOptions {
101
- appUrl?: string;
102
- urlFragment?: boolean;
103
- /** commander's attribute name for `--no-browser`; false only when that flag is passed. */
104
- browser?: boolean;
105
- }
106
-
107
- export type SeedDelivery = 'clipboard' | 'fragment' | 'none';
108
-
109
- /** A script or `--json` run has nowhere to click "paste"; never launch a browser for it. */
110
- export const impliesNoBrowser = (
111
- options: { browser?: boolean; json?: boolean },
112
- isTTY: boolean
113
- ): boolean => options.browser === false || !isTTY || !!options.json;
114
-
115
- export const runOpen = async (
116
- target: OpenTarget = 'portal',
117
- options: OpenOptions
118
- ): Promise<void> => {
119
- const project = await loadProject(process.cwd());
120
- const seed = project.env.SECURE_SEED;
121
- if (!seed)
122
- throw new Error(
123
- 'No SECURE_SEED in .env. Run a command that creates one first, e.g. npx @learncard/cli send'
124
- );
125
-
126
- const path = openPath(target, project.env);
127
- if (!path)
128
- throw new Error(
129
- `Nothing saved for "${target}" yet. Run the command that creates it first (send --template, consent-contract, or embed).`
130
- );
131
-
132
- const { network } = resolveServices(project.env, options.network);
133
- if (options.appUrl && !isHttpUrl(options.appUrl))
134
- throw new Error('--app-url must be a valid http(s) URL.');
135
- if (!options.appUrl && network !== PRODUCTION_NETWORK && network !== STAGING_NETWORK)
136
- throw new Error(
137
- `${network} has no hosted LearnCard app. Pass --app-url <url> for the app connected to it.`
138
- );
139
- const appUrl = appUrlFor(network, options.appUrl);
140
- const noBrowser = impliesNoBrowser(options, !!process.stdout.isTTY);
141
-
142
- let url: string;
143
- let seedDelivery: SeedDelivery;
144
- let clipboardCleared: Promise<void> | undefined;
145
- if (options.urlFragment) {
146
- url = signInUrl(appUrl, path, seed);
147
- out.log(
148
- 'Warning: --url-fragment puts your seed in the browser URL (history, screenshots).'
149
- );
150
- seedDelivery = 'fragment';
151
- } else if (out.json) {
152
- // Non-interactive: nobody is there to paste, and blocking a script for the clipboard TTL
153
- // would be worse than not copying. Scripts that need the seed use --url-fragment.
154
- url = signInUrl(appUrl, path);
155
- out.log(
156
- 'Seed not copied in --json mode. Paste it from .env, or re-run with --url-fragment.'
157
- );
158
- seedDelivery = 'none';
159
- } else {
160
- url = signInUrl(appUrl, path);
161
- try {
162
- await clipboard.write(seed);
163
- out.log(`Copied your seed to the clipboard (clears in ${CLIPBOARD_TTL_MS / 1000}s).`);
164
- seedDelivery = 'clipboard';
165
- clipboardCleared = waitForClipboardClear(seed);
166
- } catch {
167
- out.log(
168
- 'Clipboard unavailable. Paste the seed from .env, or re-run with --url-fragment.'
169
- );
170
- seedDelivery = 'none';
171
- }
172
- }
173
-
174
- if (!options.urlFragment) out.log('→ Click "Paste from clipboard", then Sign in.');
175
- if (!noBrowser) launchBrowser(url);
176
- // Printed last so `$(cli open | tail -1)` captures this line in non-json mode.
177
- out.log(
178
- `Opening ${OPEN_TARGETS[target]}: ${options.urlFragment ? url.split('#')[0] + '#seed=…' : url}`
179
- );
180
-
181
- out.set({ url, target, next: path, seedDelivery });
182
-
183
- // Keep the process alive until the clipboard is actually cleared (or Ctrl-C clears it early);
184
- // index.tsx's runCommand exits right after this promise resolves.
185
- await clipboardCleared;
186
- };