@astram/guide 0.2.9 → 0.2.10

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/CAPABILITIES.md CHANGED
@@ -100,8 +100,9 @@ imports: [
100
100
  - **Lockout and rate limits**: per-account login and forgot-password budgets (keyed on the
101
101
  resolved account, so spelling variants share one counter); per-IP limits for login,
102
102
  forgot-password and reset-password, active only when Express `trust proxy` is set explicitly
103
- (`TRUST_PROXY` in the scaffold). Rate limiters fail open on a Redis error by design; the request
104
- path and the TOTP replay guard fail closed.
103
+ (`TRUST_PROXY` in the scaffold). On a Redis error the login, MFA and recovery-code budgets fail
104
+ closed (503), as do the request path and the TOTP replay guard; the forgot-password and
105
+ reset-password per-IP counters fail open (logged).
105
106
  - **Disabled accounts** cannot log in or refresh; disabling a user revokes their sessions and
106
107
  drops their roles at once.
107
108
  - **Silent recovery**: after a Redis loss, the next refresh rebuilds the session from MariaDB
@@ -130,6 +131,27 @@ imports: [
130
131
  link once. User reads carry a top-level `signInStatus` (`pending` | `active` | `none`). Frontend:
131
132
  `identity.inviteUser(token, uuid)` (`@astram/frontend-core`); the scaffolded Users page has the
132
133
  Invite button, and the reset page shows "Set your password" for `purpose=invite` links.
134
+ - **Sign-in options** (ADR-0045; guard 0.20, identity 0.17, frontend-core 0.8). Each is OFF until
135
+ you turn it on; an upgrade without new options changes nothing on the wire except the logout fix
136
+ below. Write none of this yourself (Rule 6).
137
+
138
+ | Option | Default | What it does | Risk / note |
139
+ |--------|---------|--------------|-------------|
140
+ | `ShieldModule.forRoot({ signInWith: ['email', 'username'] })` | `['email']` | One field takes the email or the user's login name (`sonja`). `'email'` must stay in the list (boot fails otherwise). | Login names are global: an admin who hits "That sign-in name is already in use." learns the name exists somewhere (no tenant is named). |
141
+ | `captcha: 'env'` | off | Cloudflare Turnstile or Google reCAPTCHA (v3 score, v2 checkbox) on login, forgot-password and reset / set-password, from `CAPTCHA_*` env. Adaptive by default: a challenge after 3 failures for the account, the typed name or the IP. | Platform-wide only. Outside development/test `CAPTCHA_SECRET` must be a `secret-ref:` and `CAPTCHA_EXPECTED_HOSTNAMES` is required. `CAPTCHA_FAIL_MODE=open` lets sign-in through when the provider is down (audited, limits still apply); keep `closed` unless you accept that. The frontend build needs `VITE_CAPTCHA_PROVIDER` = the server's `CAPTCHA_PROVIDER` (CSP); a mismatch shows a "misconfigured" notice. |
142
+ | `logoutRequiresProof: true` | `false` this release; `true` from the next guard major | `POST /auth/logout` ends a session only with proof: its refresh token, or a bearer bound to it. | With `false`, an unproven logout (an old `{ sessionId }`-only frontend) still ends the session and is counted (`auth.logout.unproven`); anyone holding a leaked session id can end that session. The scaffolded app sets `true`. A custom frontend must send `{ sessionId, refreshToken }` + bearer first (`@astram/frontend-core` `logout(session)` does). |
143
+ | Mandatory 2FA (`/identity/security-policies`, identity wired) | no policies | Admins require 2FA for everyone in a company subtree, holders of a role, or holders of a permission key (`identity:*` allowed); grace 0–14 days, per user from when they become subject. Root can set a platform floor. | A required user without 2FA can only reach the enrolment pages (403 `mfa_enrollment_required`) until they enrol; during grace `/auth/me` carries `mfaEnrollmentDeadline`. Root accounts are bound only by floor policies. Takes effect on the user's next request. |
144
+
145
+ - **What counts as 2FA for a policy:** a confirmed **TOTP** authenticator only, in this release.
146
+ Recovery codes never count. Email codes, passkeys and SMS are not available as factors yet.
147
+ - Removing the last device while a policy applies answers 409 `mfa_required_by_policy`.
148
+ - `GET /auth/login-options` (public) returns `{ signInWith, captcha: { provider, siteKey, mode } | null }`
149
+ for the login page; a 401 with `captcha: 'required'` asks the page to show the widget.
150
+ - Sign-in events are audited by default when `IdentityFeatureModule` is loaded (`auth.login.*`,
151
+ `auth.logout`, `auth.mfa.*`, `auth.captcha.*`, `auth.handle.changed`; failed logins only for
152
+ real accounts, at most one row per lockout window). Your own `auditSink` still wins.
153
+ - Restricted sessions (recovery-code login or mandatory 2FA) get 403 on `POST /auth/scope-switch`,
154
+ and `GET /identity/auth-facts` answers `{ facts: [], restricted: 'reenroll' | 'mfa_enrollment' }`.
133
155
  - **Service-account API keys** (`Authorization: Bearer ask_…`) authenticate agents and machine
134
156
  clients through the same `decide()` path; scope, status and roles are read live; issuing a key
135
157
  needs an explicit expiry (`expiresAtMs`) or `neverExpires: true`.
@@ -169,6 +191,26 @@ imports: [
169
191
  active company; none when root creates it in the admin UI (a shared, root-only role). It cannot
170
192
  be changed by update. Deleting a company clears the owner of its roles. This round records
171
193
  ownership only: which roles an admin can see or administer is unchanged.
194
+ - **Security policies (mandatory 2FA, ADR-0045).** `GET|POST /identity/security-policies`,
195
+ `GET|PUT|DELETE /identity/security-policies/:uuid` (permissions
196
+ `identity:security_policy:{read,create,update,delete}`; no access bundle carries them). Any holder
197
+ can create or tighten a policy for a company in their subtree; lowering or deleting a role or
198
+ permission policy needs the grant ceiling over what it covers, a company-wide policy needs all of
199
+ `identity:*` at that company or above; the platform floor is root-only. Every write is a
200
+ `permission_change` audit row and takes effect on the affected users' next request.
201
+ - **Login names (G-014).** `loginName` on `POST /identity/users` and `PUT /identity/users/:uuid`
202
+ (`null` clears) (and optional `loginName` per
203
+ user in `bootstrap:org` files): `[a-z0-9._-]`, 3–64 characters, stored lower-case, never `@`.
204
+ Needs dominance over the user; audited `auth.handle.changed`; existing sessions stay. Users cannot
205
+ set or change their own (no `/account/*` route does).
206
+ - **Admin sign-in controls.** `POST /identity/users/:uuid/{reset-mfa,unlock,end-sessions}`
207
+ (permissions `identity:user:{reset_mfa,unlock,end_sessions}`), body `{ code }` = the admin's OWN
208
+ current TOTP code; an admin without 2FA gets 409 `mfa_required`; needs dominance; not on yourself.
209
+ Reset-MFA deletes the user's devices and recovery codes and ends their sessions; unlock clears the
210
+ login, MFA, recovery and CAPTCHA counters; end-sessions revokes every session. All audited. User
211
+ reads carry `mfaStatus` (`enrolled` | `required` | `grace` | `not_required`), never device data.
212
+ Roles holding `identity:*` or `identity:user:*` gain these actions (and `security_policy`) on
213
+ upgrade; exact-key roles need a grant.
172
214
 
173
215
  ## Organisation bootstrap (`npm run bootstrap:org`, `@astram/identity` `runOrgBootstrap`, ADR-0042)
174
216
  - **What it does.** Creates your organisation from one JSON file at deploy time: platform roots,
@@ -486,6 +528,14 @@ upload(@Req() req: Record<string, unknown>) {
486
528
  - The scaffolded frontend (`create-astram-app --with-frontend` or `npx -p create-astram-app
487
529
  astram-add-frontend`) ships login, MFA, recovery, forgot/reset, account and admin pages, with a
488
530
  strict Content-Security-Policy.
531
+ - Sign-in options (frontend-core 0.8): `auth.loginOptions()`, `captchaToken` on `login` /
532
+ `requestReset` / `resetPassword` (a failure carries `captchaRequired`), `restrictionReasonOf(body)`,
533
+ `logout(session)` sends the proof, `createSecurityPolicyService(http)`, and
534
+ `identity.{resetUserMfa,unlockUser,endUserSessions}(token, uuid, code)`. The scaffolded frontend
535
+ has the login label from `signInWith`, the CAPTCHA widget (`VITE_CAPTCHA_PROVIDER`,
536
+ `create-astram-app --captcha <provider>`), routing to 2FA setup on `mfa_enrollment_required`, a
537
+ grace banner, the Security page (policies), and Users-page 2FA status, sign-in name and admin
538
+ actions.
489
539
 
490
540
  ## Available, but NOT wired to a data path (do not assume they protect data yet)
491
541
  - **Envelope encryption / per-tenant DEKs** (`createEncryptionService`, `LocalKekCrypto`): the
@@ -503,7 +553,10 @@ upload(@Req() req: Record<string, unknown>) {
503
553
  - Per-principal column masking on reads (row-level scoping is provided; column-level is not).
504
554
  - External identity providers (OIDC, SAML, LDAP, Entra ID, Cognito, Okta, Google Workspace) and
505
555
  WebAuthn ceremonies.
506
- - Tenant- or role-mandated MFA (MFA is enforced for users who enrolled a device).
556
+ - Second factors other than TOTP (email code, passkey/WebAuthn, SMS), passwordless email-code
557
+ sign-in, a per-tenant allowed-factor list, a user's own session list, security notification
558
+ emails, and a configurable password policy (mandatory 2FA by tenant, role or permission IS
559
+ provided, above).
507
560
  - SMS and WhatsApp delivery adapters, phone numbers on users, phone verification and SMS/WhatsApp
508
561
  MFA (the ports exist in `@astram/core`; adapters come with a phone-number identity feature).
509
562
  - A durable outbox or queue for guaranteed delivery (sends are best-effort with one retry; re-issue
@@ -58,6 +58,16 @@ infrastructure, and they may not re-export it). The lint wall is a backstop, not
58
58
  sandbox: code that slips past it is still a Rule 2 violation. If you find yourself
59
59
  wanting a raw query, see Rule 4.
60
60
 
61
+ **Shared reference data** (code lists, catalogues, units: the same rows for every
62
+ tenant) is an object type declared `access: { plane, global: true }`, with no tenant
63
+ field and no `readScope`. It is read-only through the engine for everyone; create and
64
+ change its rows only in an app migration (idempotent, select-before-insert on the
65
+ business key). Never fake shared data with a match-all `readScope`, and never put
66
+ tenant data in a global type. A record policy on a global type applies only to callers
67
+ whose active tenant is the policy's tenant (plus platform policies): it does not cascade
68
+ down the company hierarchy, so a parent's deny does not hide the row from its child
69
+ companies' users. To hide a row across a group, author the policy in each tenant.
70
+
61
71
  ## 3. Classify every route (deny-by-default)
62
72
 
63
73
  Every HTTP route is DENIED by the global enforcement guard unless you classify it:
@@ -117,6 +127,24 @@ contributes configuration through the seam (`rootIds` from config, an `emailSend
117
127
  own object types as `extraObjects`) and its data; access policy is authored by
118
128
  administrators as role grants and record policies, not in code. See `CAPABILITIES.md`.
119
129
 
130
+ Sign-in options are configuration too, never app code (`CAPABILITIES.md`, "Sign-in options"):
131
+ - **Username sign-in:** `signInWith: ['email', 'username']`. Do not look users up by name
132
+ yourself, and do not let users set their own login name; admins set it (`loginName` on the users
133
+ API) or `bootstrap:org` does.
134
+ - **CAPTCHA:** `captcha: 'env'` plus the `CAPTCHA_*` variables and the same provider in
135
+ `VITE_CAPTCHA_PROVIDER`. Do not add your own CAPTCHA check, widget script origin or bot filter on
136
+ `/auth/*`; do not set `CAPTCHA_FAIL_MODE=open` without a recorded decision.
137
+ - **Mandatory 2FA:** admins create policies through `/identity/security-policies` (the Security
138
+ page). Do not gate routes on "has 2FA" in app code or keep your own list of who must enrol. In
139
+ this release only a confirmed **TOTP** authenticator counts as 2FA for a policy; recovery codes
140
+ never do. Handle 403 `mfa_enrollment_required` like `reenrollment_required`: send the user to the
141
+ 2FA setup page.
142
+ - **Logout:** call `logout(session)` from `@astram/frontend-core` (it sends the refresh token and
143
+ bearer as proof) and set `logoutRequiresProof: true` once your frontend does. Never end a
144
+ session by id alone.
145
+ - **Unlock, reset 2FA, end sessions:** use the users API actions (`reset-mfa`, `unlock`,
146
+ `end-sessions`); never clear limiter keys or delete MFA rows yourself.
147
+
120
148
  Never reimplement session handling, token verification, password hashing, or an
121
149
  allow/deny check in app code. Never take a tenant, company, role or user id for an access
122
150
  decision from the request body — the framework takes them from the session. If the auth
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guide",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",
@@ -19,5 +19,10 @@
19
19
  "sideEffects": false,
20
20
  "publishConfig": {
21
21
  "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/ASTRAM-2-0/astram-framework.git",
26
+ "directory": "packages/guide"
22
27
  }
23
28
  }