@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/CHANGELOG.md CHANGED
@@ -1,5 +1,66 @@
1
1
  # @learncard/cli
2
2
 
3
+ ## 3.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#1598](https://github.com/learningeconomy/LearnCard/pull/1598) [`2991bd32b03e26736239dd8e586e2f720d9bcd45`](https://github.com/learningeconomy/LearnCard/commit/2991bd32b03e26736239dd8e586e2f720d9bcd45) Thanks [@Custard7](https://github.com/Custard7)! - Act as a managed profile. A request may carry `X-LearnCard-Act-As: <profileId>`; the network swaps the acting profile when the authenticated profile manages the target, keeps the token's scope unchanged, and records `onBehalfOf` on the resulting activity. API tokens must opt in via a new `actAs` field on the auth grant (`'*'` or a list of profile IDs; absent = no delegation).
8
+
9
+ - `@learncard/types`: `ACT_AS_HEADER`, `AuthGrant.actAs`.
10
+ - `@learncard/network-plugin` / `@learncard/init`: `actAs` option on `initLearnCard` and the network plugin; `learnCard.invoke.actAs(profileId)` returns a scoped instance.
11
+ - `@learncard/cli`: `serviceAccounts[].actAs` in the org spec is set on the grant at creation; like scope and expiry, it is compared on re-apply and any drift errors with a revoke hint (dry-run reports `drifted`). `actAs` lists are compared as sets, so reordering profile IDs is not drift. With a `learncard-hosted` signer, `org apply` also registers a hosted signing authority on each managed profile so tokens acting as it can sign. Shown by `doctor` and `whoami`; new `examples/delegated-service-account.network.yaml`.
12
+
13
+ - [#1590](https://github.com/learningeconomy/LearnCard/pull/1590) [`59d2c92fa58ce1b864e0bf4e2aa60c85d3f8a9a4`](https://github.com/learningeconomy/LearnCard/commit/59d2c92fa58ce1b864e0bf4e2aa60c85d3f8a9a4) Thanks [@Custard7](https://github.com/Custard7)! - CLI: add issuer-org tooling for partner integrations — `org apply` (declarative, idempotent org bootstrap), `doctor` (read-only preflight), `clr validate` (CLR 2.0 transcript lint), `inbox list`, `refresh history`, and `promote` (staging → production).
14
+
15
+ Credential library: the `clr/provisional-transcript` fixture now marks itself `partial: true` with a `validUntil`, and its in-progress result points at a `Status`-typed ResultDescription (was `RawScore`). `buildFinalTranscriptVariant` strips those provisional markers.
16
+
17
+ LCA API plugin: skip the encryption-key probe when the profile does not exist yet (it could only 401) and log initialization warnings as one line instead of a full stack dump.
18
+
19
+ - [#1586](https://github.com/learningeconomy/LearnCard/pull/1586) [`dac4695428e42ecc99b05c1774fce937c021c657`](https://github.com/learningeconomy/LearnCard/commit/dac4695428e42ecc99b05c1774fce937c021c657) Thanks [@Custard7](https://github.com/Custard7)! - `send` now prompts for the recipient when omitted (`npx @learncard/cli send`) and rejects placeholder addresses such as `you@example.com` instead of silently sending to an undeliverable domain. Non-interactive runs (`--yes`, no TTY) still require the recipient as an argument.
20
+
21
+ - [#1585](https://github.com/learningeconomy/LearnCard/pull/1585) [`0c1bf9a8a33e6392d5fd279479d9ab4fb0449b5e`](https://github.com/learningeconomy/LearnCard/commit/0c1bf9a8a33e6392d5fd279479d9ab4fb0449b5e) Thanks [@goblincore](https://github.com/goblincore)! - Add `learncard demo refresh`, a guided demonstration of sending a refreshable badge through `sendBoost`, publishing a new version from the returned receipt, and verifying the recipient's refreshed copy. Uses fresh demo accounts, defaults to the local network, and supports `--network staging`, `--yes`, and `--json`.
22
+
23
+ Add interactive `--ui` mode for the local LearnCard app: print a demo recipient sign-in link, deliver claim and update notifications, and wait for the app to save each version. Supports a custom local `--app-url` and validates that the app and CLI use matching local services.
24
+
25
+ Add `learncard demo refresh --inbox` for the deferred Universal Inbox path: issue a refreshable certificate to a random `@example.com` address with email delivery suppressed, publish a final version before any holder exists (`notification: "not-applicable"`), then claim with a real DIDAuth presentation and refresh to an honors version. `--inbox --ui` prints a claim link for the local app, waits for the human to claim, publishes an update to the bound holder, and verifies the app replaced (not duplicated) the same entry. Terminal mode supports `--yes`/`--json`, is local-only, and takes `--lca-url` (default `http://localhost:5100/trpc`) for the local signing service. The existing direct demo is unchanged.
26
+
27
+ Add opt-in `--inbox --ui --email [address]`: prompt for an address, send a provisional certificate through normal email delivery, let the recipient sign in and claim, then publish a visible final-results update. This mode never reads recipient keys or personal credential storage. Keep the pre-claim update in the separate disposable-account walkthrough.
28
+
29
+ Stop the email walkthrough before publishing if the claim has no recipient account, and only advertise queued notifications when publication confirms them.
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies [[`2991bd32b03e26736239dd8e586e2f720d9bcd45`](https://github.com/learningeconomy/LearnCard/commit/2991bd32b03e26736239dd8e586e2f720d9bcd45), [`59d2c92fa58ce1b864e0bf4e2aa60c85d3f8a9a4`](https://github.com/learningeconomy/LearnCard/commit/59d2c92fa58ce1b864e0bf4e2aa60c85d3f8a9a4), [`0c1bf9a8a33e6392d5fd279479d9ab4fb0449b5e`](https://github.com/learningeconomy/LearnCard/commit/0c1bf9a8a33e6392d5fd279479d9ab4fb0449b5e), [`3aa4f5553ef92bc8a36a4bba50eeeba280d57441`](https://github.com/learningeconomy/LearnCard/commit/3aa4f5553ef92bc8a36a4bba50eeeba280d57441), [`3174c09b97fde6f7b5251390245038cf6b7ea9ba`](https://github.com/learningeconomy/LearnCard/commit/3174c09b97fde6f7b5251390245038cf6b7ea9ba), [`928e587378b3674766cf58a8bbe1cbd4d66d3a9f`](https://github.com/learningeconomy/LearnCard/commit/928e587378b3674766cf58a8bbe1cbd4d66d3a9f), [`928e587378b3674766cf58a8bbe1cbd4d66d3a9f`](https://github.com/learningeconomy/LearnCard/commit/928e587378b3674766cf58a8bbe1cbd4d66d3a9f)]:
34
+ - @learncard/types@5.21.0
35
+ - @learncard/init@2.5.0
36
+ - @learncard/lca-api-plugin@2.0.6
37
+ - @learncard/linked-claims-plugin@0.2.36
38
+ - @learncard/holder-continuity@0.2.19
39
+ - @learncard/core@9.4.36
40
+ - @learncard/network-brain-client@2.5.57
41
+ - @learncard/didkit-plugin@1.10.1
42
+ - @learncard/learn-cloud-plugin@2.3.42
43
+ - @learncard/ler-rs-plugin@0.1.27
44
+ - @learncard/open-badge-v2-plugin@1.1.37
45
+ - @learncard/render-method-plugin@8.0.0
46
+
47
+ ## 3.5.1
48
+
49
+ ### Patch Changes
50
+
51
+ - Updated dependencies [[`20b3844ddb7e649c9964308214ec4c395e9fc8db`](https://github.com/learningeconomy/LearnCard/commit/20b3844ddb7e649c9964308214ec4c395e9fc8db), [`b4f94f5a5ffbd52bad6cd26dc3dd627df8d5e6fb`](https://github.com/learningeconomy/LearnCard/commit/b4f94f5a5ffbd52bad6cd26dc3dd627df8d5e6fb), [`19bb79b1355dd9de7f71554fdb608f38b78ed6bb`](https://github.com/learningeconomy/LearnCard/commit/19bb79b1355dd9de7f71554fdb608f38b78ed6bb)]:
52
+ - @learncard/didkit-plugin@1.10.0
53
+ - @learncard/types@5.20.0
54
+ - @learncard/init@2.4.16
55
+ - @learncard/lca-api-plugin@2.0.5
56
+ - @learncard/learn-cloud-plugin@2.3.41
57
+ - @learncard/holder-continuity@0.2.18
58
+ - @learncard/core@9.4.35
59
+ - @learncard/ler-rs-plugin@0.1.26
60
+ - @learncard/linked-claims-plugin@0.2.35
61
+ - @learncard/open-badge-v2-plugin@1.1.36
62
+ - @learncard/render-method-plugin@7.0.0
63
+
3
64
  ## 3.5.0
4
65
 
5
66
  ### Minor Changes
package/README.md CHANGED
@@ -21,10 +21,187 @@ https://docs.learncard.com
21
21
  ### Send your first credential
22
22
 
23
23
  ```bash
24
- npx @learncard/cli send you@example.com
24
+ npx @learncard/cli send
25
25
  ```
26
26
 
27
- Generates a seed into `.env`, creates your issuer profile, sends a "Quickstart Complete" badge to the address, and writes the code it ran to `./send.mjs`. Pass `--yes` to accept defaults; `--name "Acme"`, `--badge "Welcome to Acme"`, `--description`, and `--profile-id` set them explicitly. Walkthrough: https://docs.learncard.com/start-here/your-first-integration
27
+ Prompts for a recipient if omitted (or pass it: `send you@yourdomain.com`), generates a seed into `.env`, creates your issuer profile, sends a "Quickstart Complete" badge, and writes the code it ran to `./send.mjs`. The recipient can be an email or phone number (delivered through the Universal Inbox with a claim link) or an existing profile ID or DID (delivered straight into their wallet). Placeholder addresses like `you@example.com` are rejected — nobody would receive the badge. Pass `--yes` to accept defaults (the recipient must then be given as an argument); `--name "Acme"`, `--badge "Welcome to Acme"`, `--description`, and `--profile-id` set them explicitly. Walkthrough: https://docs.learncard.com/start-here/your-first-integration
28
+
29
+ ### Demonstrate credential refresh
30
+
31
+ Run a guided, real-network demonstration without writing code:
32
+
33
+ ```bash
34
+ learncard demo refresh
35
+ ```
36
+
37
+ Press Enter between each step: send a **Provisional Course Certificate**, publish a
38
+ **Final Course Certificate**, then refresh the recipient's copy. The CLI shows the
39
+ before and after and verifies that the credential keeps its identity. It uses the
40
+ existing `sendBoost` flow with `enableRefresh: true` and publishes using the receipt
41
+ returned by that send. No second badge is sent.
42
+
43
+ The default network is `http://localhost:4000/trpc`. A developer must first start
44
+ the local services with managed refresh enabled. To present against staging once
45
+ LC-2198's service and SDK changes are deployed, use `learncard demo refresh --network staging`.
46
+ This command requires a CLI build containing this feature; it is not available in
47
+ older published versions. From this branch's repository root, run:
48
+
49
+ ```bash
50
+ bun --cwd packages/learn-card-cli start demo refresh
51
+ ```
52
+
53
+ By default, each run creates two demo accounts and a badge on the selected network. Keys stay
54
+ in memory, and those demo records remain on the network after the command exits.
55
+ The demo does not use your `.env` identity or send email. The refreshed copy is
56
+ shown in the CLI session; it is not saved to the LearnCard app.
57
+
58
+ Use `--yes` to run without pauses, or `--json` for a machine-readable result.
59
+
60
+ #### Show the certificate and notifications in the local app
61
+
62
+ Start the normal local app stack first (from `apps/learn-card-app`, run
63
+ `bun run lc dev learncard local full`).
64
+ Once the app is available at `http://localhost:3000`, run this in a second terminal
65
+ from the repository root:
66
+
67
+ ```bash
68
+ bun --cwd packages/learn-card-cli start demo refresh --ui
69
+ ```
70
+
71
+ 1. Follow the printed sign-in link to enter a fresh **Refresh Demo Learner** account.
72
+ If already signed in, switch to the demo account when prompted.
73
+ 2. Press Enter in the terminal to send the certificate. In the app, open **Alerts →
74
+ Claim → Accept**, skip the optional connection prompt, then view
75
+ **Provisional Course Certificate** in **Passport → Achievements**.
76
+ Open it to see **Provisional Results** and **Final grade: Pending** on the full certificate.
77
+ 3. Press Enter in the terminal to publish the final certificate. The command checks that
78
+ the app has saved the original first.
79
+ 4. Reload the app if needed and open **Alerts**. Select the notification that the school
80
+ updated a credential. The app retrieves and verifies **Final Course Certificate**,
81
+ replacing the existing copy. Open it to see **Final Results** and **Final grade: A**.
82
+ 5. Press Enter in the terminal to confirm. The CLI checks that the updated credential
83
+ was actually saved by the app and verifies it.
84
+
85
+ Keep the terminal open throughout. The sign-in link grants access to this disposable demo
86
+ account; keep it private and use it only for test data. The CLI does not write account keys
87
+ to files, but the recipient remains signed into the browser after the demo. Rerunning
88
+ creates fresh accounts. UI mode requires an interactive terminal and cannot be combined
89
+ with `--yes`, `--json`, or `LC_YES=1`.
90
+
91
+ For a different local app port, pass `--app-url http://localhost:3001`. The CLI reads
92
+ the app's served `tenant-config.json` to use the same LearnCloud and notification services.
93
+ Both the app and all these services must use loopback URLs, and `--network` must match
94
+ the app's Brain service. Local refresh trust is automatic in the development app.
95
+ If using the E2E service stack instead of the normal app stack, set the generated app
96
+ config's `apis.lcaApi` to `http://localhost:5200/trpc` and `apis.notificationsEndpoint`
97
+ to `http://localhost:5200/api/notifications/send` before starting the demo. Do not run
98
+ database-resetting E2E tests during a demonstration.
99
+
100
+ #### Receive the claim at your own email (opt-in real email)
101
+
102
+ Use `--email` to run the Universal Inbox lifecycle against an address you own. The CLI asks
103
+ the locally configured delivery service to email a **provisional** claim link, you claim it
104
+ in the app, then the school publishes a single visible **final** update.
105
+
106
+ ```bash
107
+ bun --cwd packages/learn-card-cli start demo refresh --inbox --ui --email you@example.com
108
+ ```
109
+
110
+ `--email` without a value prompts for the address. It requires `--inbox`, `--ui`, and an
111
+ interactive terminal, and the address is validated before any account is created. The
112
+ walkthrough is:
113
+
114
+ 1. **Issue provisional results.** The CLI asks the delivery service to email a claim link
115
+ for a **Provisional Course Certificate** to your address. It reports only that delivery
116
+ was requested; it never claims the email was sent or received.
117
+ 2. **Claim in the app.** Open the mailbox link, sign in or create an account **using that
118
+ same address**, and claim the provisional certificate. Keep the app open.
119
+ 3. **Publish final results.** Return to the terminal and press Enter. The CLI reads the
120
+ issuer's inbox-credential record for the bound holder DID, then publishes
121
+ **Final Certificate / Final grade: A** as the single visible update (version 2). That
122
+ publication requests an in-app notification and a update email with the issuer and certificate name prompting you to
123
+ log in and view notifications.
124
+ 4. **View the update.** Open the app notifications to see the final certificate. The CLI
125
+ never creates, signs in as, or reads the recipient wallet, so it does not verify the
126
+ wallet contents and cannot confirm that any email or notification was delivered.
127
+
128
+ Real email mode is local-only like the rest of the inbox demo. It never suppresses
129
+ delivery, and it never publishes a hidden pre-claim version: version 2 is the only update.
130
+
131
+ > **Operator setup.** `--email` only requests delivery. The operator must enable and
132
+ > configure the local email adapter/delivery service first; otherwise no email is sent. In
133
+ > the local test mode the adapter logs the message instead of sending it, so open the link
134
+ > on the same computer that runs the app. The CLI cannot verify that an address is
135
+ > deliverable, and it masks the address in `--json` output.
136
+
137
+ #### Advanced: refresh for a recipient who has no account yet (disposable preclaim path)
138
+
139
+ This path exercises the deferred Universal Inbox mechanics — including publishing an update
140
+ _before_ any holder exists — with a fake `@example.com` address and suppressed delivery. It
141
+ is not the recommended mail walkthrough; use `--email` above for that.
142
+
143
+ The direct demo sends to a profile that already exists. Add `--inbox` to exercise the
144
+ deferred Universal Inbox path: the school issues a refreshable certificate to a random
145
+ `@example.com` address with no LearnCard account, publishes a new version **before**
146
+ anyone claims, and only then is a real holder bound and sent an update. Email delivery
147
+ is suppressed, so the CLI shows the claim link instead of mailing it.
148
+
149
+ ```bash
150
+ bun --cwd packages/learn-card-cli start demo refresh --inbox
151
+ ```
152
+
153
+ Press Enter through four stages:
154
+
155
+ 1. **Issue provisional results.** Queues a **Provisional Course Certificate** for the
156
+ demo address. Nothing is emailed and there is no recipient to notify.
157
+ 2. **Publish final results before claim.** The school publishes **Final Results /
158
+ Final grade: A**. Because no holder exists yet, the publication reports
159
+ `notification: "not-applicable"`; nothing is announced.
160
+ 3. **Claim.** The CLI claims with a fresh local wallet using a real DIDAuth
161
+ presentation, exactly like the app. It receives the newest version (final results),
162
+ verifies the proof, and confirms the same credential identity.
163
+ 4. **Publish honors results.** Now that the claim has bound the holder DID, the school
164
+ publishes **Honors Results / Final grade: A+**. The holder refreshes and verifies
165
+ that the same credential now shows the honors version.
166
+
167
+ `--yes` runs every stage without pausing; `--json` prints one machine-readable result
168
+ with no seeds, tokens, or claim links. Terminal mode is local-only (it rejects a
169
+ non-loopback `--network`) and needs the local LCA signing service. `--lca-url` defaults
170
+ to `http://localhost:5100/trpc`; pass `--lca-url http://localhost:5200/trpc` when
171
+ presenting against the E2E stack on port 5200.
172
+
173
+ For the guided app experience, start the normal local app stack as above and run:
174
+
175
+ ```bash
176
+ bun --cwd packages/learn-card-cli start demo refresh --inbox --ui
177
+ ```
178
+
179
+ The CLI reads Brain, LearnCloud, LCA, and notification services from the app's
180
+ `tenant-config.json` and requires them all to be loopback, just like the direct demo.
181
+
182
+ 1. Press Enter to start, then follow the terminal prompts to issue provisional results
183
+ and publish **Final Results / Final grade: A** before the recipient account exists.
184
+ 2. Press Enter to create the demo recipient. Open the printed **sign-in link** and choose
185
+ **Switch account** if prompted. It takes you directly to the claim screen; the
186
+ separate claim link is also printed. Keep both links private.
187
+ 3. Click **Claim my credential → Accept**. The first certificate already shows final
188
+ results, not provisional results. Find it in **Passport → Achievements**.
189
+ This first delivery uses a claim link because there was no account to notify at issue time.
190
+ 4. Return to the terminal and press Enter to confirm the certificate is saved, then
191
+ Enter again to publish the honors update. Early confirmation keeps the demo waiting.
192
+ 5. Open **Alerts → “Inbox Demo School updated one of your credentials” → View Credential**.
193
+ Reload if the notification is not visible yet. The certificate now shows
194
+ **Honors Results / Final grade: A+**.
195
+ 6. Press Enter in the terminal to finish. The CLI verifies that the app replaced the
196
+ same entry (not a duplicate) and that the saved certificate has a valid signature.
197
+
198
+ In `--ui` mode the CLI never claims, accepts, refreshes, or saves on the app's behalf;
199
+ a human performs every app action. Rerunning creates fresh accounts. This mode requires
200
+ an interactive terminal and cannot be combined with `--yes`, `--json`, or `LC_YES=1`.
201
+
202
+ The unknown email is simulated with suppressed delivery, but the claim link, signing
203
+ authority, publication, claim binding, and refresh are all real. The direct
204
+ `demo refresh` (without `--inbox`) is unchanged.
28
205
 
29
206
  ### Interactive REPL
30
207
 
@@ -72,6 +249,57 @@ await exportLearnCardBundle({ out: './learncard-export.zip', password });
72
249
 
73
250
  See `@learncard/holder-continuity` `BUNDLE_SPEC.md` for the ZIP layout and manifest hashing rules.
74
251
 
252
+ ## Bootstrap an organization
253
+
254
+ ```bash
255
+ npx @learncard/cli org apply ./org.yaml
256
+ ```
257
+
258
+ Reconciles a declarative YAML/JSON spec (issuer profile, signing authority, districts/managed profiles, service-account tokens) against the network. With a `learncard-hosted` signer, every managed profile also gets its own hosted signing authority registered, so API tokens acting as a district can sign through it. Idempotent — re-running with the same file makes no changes. Use `--dry-run` to preview, and `--secrets-out ./secrets.env` to save any newly created service-account tokens (required the first time a `serviceAccounts` entry is created; each is written as `NAME=token` with the account name upper-cased and hyphens replaced by underscores, e.g. `EA_CLR_ISSUER=…`). `branding` (on the issuer and on each managed profile) sets the profile image, hero image, bios, website, type, and wallet display colours; it is diffed field-by-field, so re-running only sends what changed and `display` colours are merged, never wiped. Images must be https URLs for now — host them and paste the link. Webhooks are not registered on the network — LearnCard calls `configuration.webhookUrl` per issuance — so the first `webhooks[].url` is saved as `WEBHOOK_URL` in `.env` for `doctor` and your issuer code to default to. Start from one of `examples/*.network.yaml` — `minimal`, `service-account`, `branded`, `self-hosted-signing`, `state-districts`, or `delegated-service-account` — each shows one idea.
259
+
260
+ ```yaml
261
+ issuer:
262
+ profileId: scde
263
+ displayName: South Carolina Department of Education
264
+ branding:
265
+ image: https://cdn.example.org/scde-logo.png
266
+ shortBio: State education agency.
267
+ websiteLink: https://ed.sc.gov
268
+ display: { backgroundColor: '#18224E', accentColor: '#2E7D32' }
269
+ signingAuthority: { type: learncard-hosted, name: scde-clr }
270
+ profileManager:
271
+ displayName: SC Districts
272
+ managed:
273
+ - { profileId: sc-greenville, displayName: Greenville County Schools }
274
+ serviceAccounts:
275
+ - name: ea-clr-issuer
276
+ scopes: [inbox:write, inbox:read, credentials:write, credentials:read]
277
+ ```
278
+
279
+ A service account's `actAs` controls whether its token can act as a managed profile: `'*'` allows any profile under `profileManager.managed`, or list specific profile IDs (e.g. `actAs: [sc-greenville, sc-north]`) to limit it to those. Omit `actAs` and the token cannot act as anyone — delegation is deny-by-default, so add it explicitly wherever a backend needs to issue on a district's behalf. See `examples/delegated-service-account.network.yaml`.
280
+
281
+ Once the org exists, `send` picks up its pieces automatically: with a signing authority registered it signs through that authority by default (`--no-template` opts back out to the local key), and `--as <managedProfileId>` sends as one of the managed profiles — signed with that profile's own did:web, no separate seed or folder needed. `inbox list --as <managedProfileId>` shows what that profile has sent. `LEARNCARD_AS=<profileId>` does the same for a whole shell session without persisting anything, and `whoami` shows the folder's identity plus every profile `--as` can target (plus any service accounts and what they may act as). A folder is one identity; passing a different `--profile-id` is an error that points you at `--as`. Backends using an API token act as a managed profile by sending the `X-LearnCard-Act-As` header (SDK: `initLearnCard({ apiKey, actAs })`) — allowed only if the token's `actAs` permits it.
282
+
283
+ Spec objects are strict: an unknown or misspelled key fails validation rather than being dropped. Display names on the issuer, profile manager, and managed profiles are updated in place when they differ; a self-hosted signer whose registered DID differs from the spec is reported as drift. Service-account scopes, expiry, and `actAs` are all compared with the spec. If any of them drifts, apply errors with a revoke command: revoke the grant and re-run to create a replacement (dry-run reports `drifted`). With `--secrets-out`, a matching grant whose normalized key is missing from the secrets file gets a re-issued token. Tokens are written only to that file, never printed or stored elsewhere by the CLI.
284
+
285
+ New `send --as` scripts save `MANAGED_DID` in `.env` and use it with `didWeb` on the selected network. They reject missing or changed managed identities. Existing scripts are left untouched, with a warning to use the CLI if their issuer differs.
286
+
287
+ ## Preflight with doctor
288
+
289
+ ```bash
290
+ npx @learncard/cli doctor
291
+ ```
292
+
293
+ Checks this project's issuer setup against the network — identity, network reachability, API token scopes, signing authority (with a real test-sign + verify), did:web resolution, an optional `--webhook-url` POST ping, and whether managed credential refresh is enabled — printing one line per check with a remediation command for anything that fails. It calls network services but does not deliver credentials or provision resources. Trusted Registry verification is a manual check reported as skipped. Pass `--strict` to also exit non-zero on warnings, or `--scopes "..."` to check different permissions than the default Universal Inbox set.
294
+
295
+ ## Promote staging → production
296
+
297
+ ```bash
298
+ npx @learncard/cli promote --from staging --to production --org ./org.yaml
299
+ ```
300
+
301
+ Re-applies your org spec in its own `.learncard/production` folder (each network needs its own `.env`), writes fresh service-account tokens to `<target>/secrets.env` (or `--secrets-out`), and runs `doctor` — then prints the per-network checklist (profile, tokens, signing authority, templates, ConsentFlow contracts, credentials) that doesn't carry over; only your seed's `did:key` does. Add `--dry-run` to preview or `--skip-doctor` to skip the preflight.
302
+
75
303
  ## Contributing
76
304
 
77
305
  Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.