@astram/guide 0.2.7 → 0.2.9

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 (2) hide show
  1. package/CAPABILITIES.md +93 -8
  2. package/package.json +1 -1
package/CAPABILITIES.md CHANGED
@@ -17,17 +17,17 @@ below is wired by the two `forRoot` calls in your `app.module.ts`; you write no
17
17
  ## Wiring (the ONE seam, plus the identity module)
18
18
 
19
19
  ```ts
20
- import { ShieldModule } from '@astram/guard';
20
+ import { ShieldModule, rootIdsFromEnv } from '@astram/guard';
21
21
  import {
22
22
  IdentityFeatureModule, identityAuthActionKeys, identityControlPlaneKeys,
23
23
  } from '@astram/identity';
24
24
 
25
- const rootIds = ['<platform-admin user uuid>']; // from config, never from a request
25
+ const rootIds = rootIdsFromEnv(process.env); // SHIELD_ROOT_IDS; never from a request
26
26
 
27
27
  imports: [
28
28
  ShieldModule.forRoot({
29
29
  auth: true,
30
- emailSender: myEmailSender, // or 'disabled' — REQUIRED outside dev/test
30
+ emailSender: 'env', // or your EmailSender, or 'disabled' — REQUIRED outside dev/test
31
31
  rootIds,
32
32
  identityActionKeys: identityAuthActionKeys,
33
33
  identityControlPlaneKeys,
@@ -40,7 +40,7 @@ imports: [
40
40
  - **`ShieldModule.forRoot(options?)`** (`@astram/guard`; `forRootAsync` for DI-computed options).
41
41
  Options: `auth?`, `recordAuthorization?` (or a pre-built `decisionAuthority?`; only for an app
42
42
  WITHOUT `IdentityFeatureModule`, never both),
43
- `emailSender?: EmailSender | 'disabled'`, `rootIds?`, `identityActionKeys?`,
43
+ `emailSender?: EmailSender | 'disabled' | 'env'`, `rootIds?`, `identityActionKeys?`,
44
44
  `identityControlPlaneKeys?`, `config?`, and optional overrides `credentialResolver?`,
45
45
  `profileResolver?`, `credentialWriter?`, `resetTokenStore?`, `recoveryCodeStore?`,
46
46
  `profileWriter?`, `sessionSweeper?`, `auditSink?`, `mfaDeviceStore?`, `totpVerifier?`,
@@ -62,6 +62,10 @@ imports: [
62
62
  HERE ONLY: the module builds the record authorization from them and `ShieldModule` decides with
63
63
  it. Passing `recordAuthorization`/`decisionAuthority` to `ShieldModule` as well fails the boot.
64
64
  Without `rootIds` it logs a boot warning.
65
+ - **`rootIdsFromEnv(env)`** (`@astram/guard`) reads `SHIELD_ROOT_IDS` (comma-separated user
66
+ uuids, lower-case canonical form). Any invalid or duplicate entry fails boot (the value is never
67
+ echoed); unset returns an empty list. `npm run bootstrap:org` prints the line to set. An app that
68
+ reads its own variable keeps working; root is never inferred from `u_is_platform_superuser`.
65
69
  - `ShieldModule.forRoot()` with no options and no identity module = enforcement only (no DB, no
66
70
  Redis, boots without `.env`); it binds a stub that denies every `@Classified` route.
67
71
 
@@ -102,9 +106,30 @@ imports: [
102
106
  drops their roles at once.
103
107
  - **Silent recovery**: after a Redis loss, the next refresh rebuilds the session from MariaDB
104
108
  (no forced re-login); a restricted (recovery-code) session rebuilds restricted.
105
- - **Email**: reset links go through your `EmailSender` (`send({ to, subject, html, text })`).
106
- Outside `NODE_ENV=development|test`, `auth: true` without `emailSender` fails boot;
107
- `'disabled'` sends nothing and `/auth/forgot-password` still answers 204.
109
+ - **Email** (G-010, ADR-0043): `emailSender: 'env'` picks the provider from configuration —
110
+ `EMAIL_PROVIDER` = `smtp` | `brevo` | `sendgrid` | `mailgun` | `disabled` (no default), plus
111
+ `EMAIL_FROM_ADDRESS`, `EMAIL_FROM_NAME` and the provider's variables (the MESSAGING block in the
112
+ template's `server/_env.example`; runbook §6). It needs the `@astram/messaging` package installed;
113
+ write no mail code. Outside `NODE_ENV=development|test` credentials must be `secret-ref:` values, so
114
+ a `SECRETS_PROVIDER` is required first; bad config fails boot naming the variable;
115
+ `MESSAGE_BOOT_PROBE=1` checks credentials at boot without sending. Your own `EmailSender`
116
+ (`send({ to, subject, html, text })`) still works. Either way every framework email (password
117
+ reset, invite, email-change confirmation, security notices) is bounded (`MESSAGE_SEND_TIMEOUT_MS`,
118
+ default 10 s, one retry on a temporary failure) and audited (`message.sent` / `message.failed`:
119
+ provider, purpose, masked recipient; never the link or body). Outside `NODE_ENV=development|test`,
120
+ `auth: true` without `emailSender` fails boot; `'disabled'` sends nothing and
121
+ `/auth/forgot-password` still answers 204. SMS and WhatsApp are ports only (`SmsProvider`,
122
+ `WhatsAppProvider` in `@astram/core`); no adapter yet, and `SMS_PROVIDER` / `WHATSAPP_PROVIDER`
123
+ accept only `disabled`.
124
+ - **Administrator invite** (G-010): `POST /identity/users/:uuid/invite` (permission
125
+ `identity:user:invite`; wildcard `identity:*` / `identity:user:*` holders have it after the upgrade)
126
+ emails a one-time set-password link to a user whose sign-in is still pending. It is tenant-walled and
127
+ dominance-checked like update; 409 for a user who already set a password or has no credential, 429
128
+ over 3 per user or `INVITE_RATE_PER_ADMIN` per admin per hour, 501 with mail disabled and no link
129
+ return. `IdentityFeatureModule.forRoot({ invite: { returnLinkToAdmin: true } })` also returns the
130
+ link once. User reads carry a top-level `signInStatus` (`pending` | `active` | `none`). Frontend:
131
+ `identity.inviteUser(token, uuid)` (`@astram/frontend-core`); the scaffolded Users page has the
132
+ Invite button, and the reset page shows "Set your password" for `purpose=invite` links.
108
133
  - **Service-account API keys** (`Authorization: Bearer ask_…`) authenticate agents and machine
109
134
  clients through the same `decide()` path; scope, status and roles are read live; issuing a key
110
135
  needs an explicit expiry (`expiresAtMs`) or `neverExpires: true`.
@@ -131,7 +156,63 @@ imports: [
131
156
  customer business data.
132
157
  - Migrations for `shield_identity`, `shield_authz`, `shield_app` identity tables ship in
133
158
  `@astram/identity` and `@astram/guard` and run through `@astram/migrate` automatically. Dev seeds
134
- (099/100) run only with `NODE_ENV=development` or `ASTRAM_SEED_DEV=1`.
159
+ (099/100) run only with `NODE_ENV=development` or `ASTRAM_SEED_DEV=1`. Anywhere else, create the
160
+ first users with `npm run bootstrap:org` (below).
161
+ - **API-created users can be reached by reset.** `POST /identity/users` with an email and a
162
+ company creates the user, an active membership and a **locked** credential in one transaction
163
+ (no password works until the user sets one). The response is `{ id, credential: 'locked' }`, or
164
+ `{ id, credential: 'none', reason }` (`no_email`, `no_company`; `no_provisioner` only when the
165
+ service is built without `IdentityFeatureModule.forRoot`) when no credential was made.
166
+ Once your `EmailSender` works, `POST /auth/forgot-password` reaches the user. A user whose email
167
+ is set later stays locked until the next `bootstrap:org` run gives them a credential.
168
+ - **Role owner.** Every role records an owner company when it is created: a non-root admin's
169
+ active company; none when root creates it in the admin UI (a shared, root-only role). It cannot
170
+ be changed by update. Deleting a company clears the owner of its roles. This round records
171
+ ownership only: which roles an admin can see or administer is unchanged.
172
+
173
+ ## Organisation bootstrap (`npm run bootstrap:org`, `@astram/identity` `runOrgBootstrap`, ADR-0042)
174
+ - **What it does.** Creates your organisation from one JSON file at deploy time: platform roots,
175
+ a company tree (tenants and `children`), departments and branches per company, roles with their
176
+ permission keys, and users with one active membership and their role postings. It runs against
177
+ your own `AppModule` (`NestFactory.createApplicationContext`), so keys of your `extraObjects` are
178
+ grantable. Every write goes through the framework's services and `decide()`, as the declared
179
+ operation `shield:org-bootstrap`; audit rows carry `operation`, `step` and `onBehalfOf` (the
180
+ operator). It needs auth on and `IdentityFeatureModule` wired; without the module it exits with
181
+ "wire IdentityFeatureModule in AppModule before running bootstrap:org".
182
+ - **File.** `{ "version": 1, "roots": [{ username, email, homeCompany }], "tenants": [company] }`;
183
+ `company` = `{ name, departments?, branches?, roles?: [{ name, permissions }], users?:
184
+ [{ username, email, company?, postings?: [{ role, department?, branch? }] }], children? }`.
185
+ Unknown fields, unknown permission keys (allowed: catalog keys plus `<feature>:*` and
186
+ `<feature>:<type>:*`), duplicate usernames or emails and references to undeclared roles,
187
+ departments or branches are refused before any write. No password, uuid or secret in the file.
188
+ A user with no postings is allowed (signs in, holds nothing).
189
+ - **Run.** `npm run bootstrap:org -- --file org.json` is a dry run (prints `create` / `unchanged` /
190
+ `drift` / `not-in-file` / `conflict`). Add `--apply` to write; it needs
191
+ `ASTRAM_BOOTSTRAP_OPERATOR`. Root passwords: `ASTRAM_BOOTSTRAP_ROOT_PASSWORD_<USERNAME>` (a
192
+ `secret-ref:` outside development/test, so a `SECRETS_PROVIDER` is needed) or `--interactive`.
193
+ Other flags: `--reissue-links`, `--link-ttl-hours <1..168>` (default 72), `--links-to-stdout`,
194
+ `--claim-unowned-roles`. Runs hold a database lock.
195
+ - **First sign-in.** Non-root users get a locked credential and a one-time set-password link,
196
+ printed once, to a terminal only (`--links-to-stdout` sends them to a pipe, where logs can keep
197
+ them). The operator delivers each link; the user redeems it at the reset page. Outside
198
+ development/test `RESET_LINK_BASE_URL` must be set and not localhost.
199
+ - **Roots.** The run prints each root's uuid and a `SHIELD_ROOT_IDS=…` line: set it and restart
200
+ (`rootIdsFromEnv`, above).
201
+ - **Re-runs and new tenants.** Additive and idempotent: a second `--apply` writes nothing. A file
202
+ covers only the tenants it declares, so onboard a new tenant with a new file. A role name owned by
203
+ another tenant is a `conflict` (roles are still global by name); an unowned role needs
204
+ `--claim-unowned-roles`.
205
+ - **What it does NOT do.** No deletes, renames, moves or revokes (differences are reported only);
206
+ it never activates an inactive membership or updates a changed email. No upload from the UI.
207
+ Department and branch admin routes are separate. With `emailSender` configured, `--email-links`
208
+ emails each non-root set-password link instead of printing it; root links are never emailed and
209
+ follow the terminal-only print rule (it cannot be combined with `--links-to-stdout`).
210
+ - **Existing apps** (scaffolded before this release) have no `bootstrap:org`. Upgrade
211
+ `@astram/identity` and `@astram/guard` (`npm update @astram/identity @astram/guard`), copy
212
+ `src/bootstrap-org.ts` and `src/bootstrap-org.args.ts` from a freshly scaffolded app (or from
213
+ `create-astram-app/template/server/src/` in the framework repo) into `server/src/`, and add
214
+ `"bootstrap:org": "node --env-file-if-exists=.env dist/bootstrap-org.js"` to the `scripts` of
215
+ `server/package.json`. `npm run build` compiles both files; no other change is needed.
135
216
 
136
217
  ## The record pipe (`@astram/engine`)
137
218
  - **`RecordEngine`** (token `RECORD_ENGINE`) / `RecordsService` — create/get/update/delete/list,
@@ -423,5 +504,9 @@ upload(@Req() req: Record<string, unknown>) {
423
504
  - External identity providers (OIDC, SAML, LDAP, Entra ID, Cognito, Okta, Google Workspace) and
424
505
  WebAuthn ceremonies.
425
506
  - Tenant- or role-mandated MFA (MFA is enforced for users who enrolled a device).
507
+ - SMS and WhatsApp delivery adapters, phone numbers on users, phone verification and SMS/WhatsApp
508
+ MFA (the ports exist in `@astram/core`; adapters come with a phone-number identity feature).
509
+ - A durable outbox or queue for guaranteed delivery (sends are best-effort with one retry; re-issue
510
+ an invite), bounce or webhook processing, and admin-editable message templates.
426
511
 
427
512
  If you need one of these, STOP and flag it in `.astram/guide/gaps.md` (Rule 4).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guide",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "license": "MIT",
5
5
  "description": "Astram framework — the AI/dev GUIDE layer (ADR-0025). Ships the framework's hard rules + capabilities manifest as installed, upgrade-refreshed docs. A consumer app's CLAUDE.md references FRAMEWORK_RULES.md from this package; `npm update` replaces it wholesale so new rules arrive cleanly, while the app's own .astram/guide/ layer is never touched. This is the framework-owned FLOOR: an app rule may tighten it, never weaken it.",
6
6
  "type": "commonjs",