create-astram-app 0.5.0 → 0.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.
- package/README.md +211 -38
- package/dist/add-frontend.js.map +1 -1
- package/dist/index.js +14 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/template/README.md +51 -20
- package/template/frontend/README.md +21 -11
- package/template/frontend/index.html +16 -0
- package/template/frontend/package.json +1 -1
- package/template/frontend/src/App.jsx +82 -20
- package/template/frontend/src/auth/AuthContext.jsx +139 -21
- package/template/frontend/src/auth/routeGate.js +31 -0
- package/template/frontend/src/auth/routeGate.test.mjs +59 -0
- package/template/frontend/src/auth/sessionFlow.js +27 -0
- package/template/frontend/src/auth/sessionFlow.test.mjs +63 -0
- package/template/frontend/src/components/EntityList.jsx +1 -7
- package/template/frontend/src/components/PageHeader.jsx +5 -4
- package/template/frontend/src/components/ShareDialog.jsx +3 -5
- package/template/frontend/src/components/access/CollapsibleGroup.jsx +4 -1
- package/template/frontend/src/components/ui/alert-dialog.tsx +27 -50
- package/template/frontend/src/components/ui/alert.tsx +17 -23
- package/template/frontend/src/components/ui/avatar.tsx +8 -20
- package/template/frontend/src/components/ui/badge.tsx +17 -26
- package/template/frontend/src/components/ui/button.tsx +22 -24
- package/template/frontend/src/components/ui/card.tsx +19 -36
- package/template/frontend/src/components/ui/checkbox.tsx +7 -10
- package/template/frontend/src/components/ui/date-field.tsx +26 -27
- package/template/frontend/src/components/ui/dialog.tsx +24 -40
- package/template/frontend/src/components/ui/dropdown-menu.tsx +25 -54
- package/template/frontend/src/components/ui/input-otp.tsx +1 -4
- package/template/frontend/src/components/ui/label.tsx +10 -12
- package/template/frontend/src/components/ui/searchable-select.tsx +1 -3
- package/template/frontend/src/components/ui/select.tsx +31 -43
- package/template/frontend/src/components/ui/separator.tsx +6 -6
- package/template/frontend/src/components/ui/sheet.tsx +29 -38
- package/template/frontend/src/components/ui/skeleton.tsx +3 -3
- package/template/frontend/src/components/ui/sonner.tsx +7 -7
- package/template/frontend/src/components/ui/table.tsx +56 -70
- package/template/frontend/src/components/ui/tooltip.tsx +7 -11
- package/template/frontend/src/config/branding.js +1 -4
- package/template/frontend/src/hooks/useMobile.tsx +4 -6
- package/template/frontend/src/index.css +5 -2
- package/template/frontend/src/pages/Dashboard.jsx +13 -7
- package/template/frontend/src/pages/ForgotPassword.jsx +9 -2
- package/template/frontend/src/pages/Login.jsx +85 -70
- package/template/frontend/src/pages/ResetPassword.jsx +9 -2
- package/template/frontend/src/pages/access/AccessForm.jsx +49 -15
- package/template/frontend/src/pages/access/AccessPage.jsx +10 -6
- package/template/frontend/src/pages/access/RecordPoliciesPage.jsx +9 -2
- package/template/frontend/src/pages/account/AccountLayout.jsx +11 -6
- package/template/frontend/src/pages/account/ChangePasswordPage.jsx +2 -2
- package/template/frontend/src/pages/account/ReenrollPage.jsx +3 -5
- package/template/frontend/src/pages/account/TwoFactorPage.jsx +3 -8
- package/template/frontend/src/pages/companies/CompaniesPage.jsx +7 -9
- package/template/frontend/src/pages/cross-tenant/GrantForm.jsx +41 -8
- package/template/frontend/src/pages/cross-tenant/GrantsPage.jsx +30 -7
- package/template/frontend/src/pages/cross-tenant/InboxPage.jsx +12 -3
- package/template/frontend/src/pages/roles/RolesPage.jsx +5 -9
- package/template/frontend/src/pages/service-accounts/ServiceAccountsPage.jsx +77 -18
- package/template/frontend/src/pages/service-accounts/accessModel.js +22 -0
- package/template/frontend/src/pages/service-accounts/accessModel.test.mjs +28 -0
- package/template/frontend/src/pages/users/MembershipCard.jsx +3 -1
- package/template/frontend/src/pages/users/PostingRow.jsx +8 -14
- package/template/frontend/src/pages/users/UsersPage.jsx +7 -9
- package/template/frontend/src/platform/httpClient.js +5 -0
- package/template/frontend/src/platform/sessionStore.js +55 -13
- package/template/frontend/src/platform/sessionStore.test.mjs +89 -0
- package/template/frontend/src/platform/shieldTransport.js +6 -0
- package/template/frontend/src/platform/shieldTransport.test.mjs +84 -5
- package/template/frontend/src/shell/AppShell.jsx +10 -12
- package/template/frontend/src/shell/Sidebar.jsx +15 -3
- package/template/frontend/src/shell/nav.js +1 -3
- package/template/frontend/vite.config.js +19 -1
- package/template/server/_env.example +22 -1
- package/template/server/eslint.config.mjs +194 -0
- package/template/server/package.json +11 -11
- package/template/server/scripts/dev.mjs +17 -6
- package/template/server/scripts/sync-astram-guide.mjs +45 -19
- package/template/server/src/app.module.ts +11 -5
- package/template/server/src/health/demo.controller.ts +16 -12
- package/template/server/src/main.ts +23 -1
- package/template/server/eslint.config.js +0 -95
package/README.md
CHANGED
|
@@ -1,61 +1,234 @@
|
|
|
1
1
|
# create-astram-app
|
|
2
2
|
|
|
3
|
-
The Astram app scaffolder (ADR-0023 §3).
|
|
4
|
-
on the published, immutable framework
|
|
5
|
-
|
|
3
|
+
The Astram app scaffolder (ADR-0023 §3). It emits a **new consumer app**: a NestJS API that
|
|
4
|
+
depends on the published, immutable framework packages and enforces **deny-by-default** from the
|
|
5
|
+
first boot, plus (optionally) a complete identity frontend. You write features; the framework
|
|
6
|
+
owns identity, authentication, sessions, authorization and audit.
|
|
6
7
|
|
|
7
8
|
## Use it
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
|
-
npx create-astram-app my-app
|
|
11
|
-
|
|
11
|
+
npx create-astram-app my-app # server/ only
|
|
12
|
+
npx create-astram-app my-app --with-frontend # server/ + frontend/ (identity UI)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`--with-frontend` may come before or after the name. The app name must be a single directory
|
|
16
|
+
name (letters, numbers, `.`, `_`, `-`; no leading dot).
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cd my-app/server
|
|
12
20
|
npm install
|
|
13
|
-
npm run dev #
|
|
21
|
+
npm run dev # API on http://localhost:3000; GET /health → 200, GET /demo/public → 200
|
|
22
|
+
npm test # proves deny-by-default (an unclassified route is denied)
|
|
23
|
+
|
|
24
|
+
cd ../frontend # only with --with-frontend
|
|
25
|
+
npm install
|
|
26
|
+
npm run dev # identity UI on http://localhost:5173 (proxies /api → :3000)
|
|
14
27
|
```
|
|
15
28
|
|
|
16
|
-
|
|
29
|
+
### Add the frontend to an existing app
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd my-app # the app root (the folder that holds server/)
|
|
33
|
+
npx -p create-astram-app astram-add-frontend
|
|
34
|
+
```
|
|
17
35
|
|
|
18
|
-
|
|
19
|
-
|
|
36
|
+
`astram-add-frontend` is a second bin in the `create-astram-app` package, so `npx` needs
|
|
37
|
+
`-p create-astram-app` unless the package is already installed (then plain
|
|
38
|
+
`npx astram-add-frontend` works). It copies the frontend template into `./frontend/`, substitutes
|
|
39
|
+
the app name from `./package.json` (or the folder name), and **never overwrites an existing
|
|
40
|
+
file**. Re-running it later only adds files the newer template has and your copy lacks.
|
|
41
|
+
|
|
42
|
+
## What it emits
|
|
20
43
|
|
|
21
44
|
```
|
|
22
45
|
my-app/
|
|
23
|
-
├──
|
|
24
|
-
│
|
|
25
|
-
│
|
|
26
|
-
├──
|
|
27
|
-
├──
|
|
28
|
-
|
|
29
|
-
│ ├──
|
|
30
|
-
│
|
|
31
|
-
│ ├──
|
|
32
|
-
│
|
|
33
|
-
|
|
34
|
-
├── .
|
|
35
|
-
|
|
36
|
-
├──
|
|
37
|
-
└──
|
|
46
|
+
├── server/ the API (NestJS) — always emitted
|
|
47
|
+
│ ├── package.json @astram/core, guard, engine, identity, migrate, guide, sdk as
|
|
48
|
+
│ │ published semver ranges (never workspace:*), NestJS runtime
|
|
49
|
+
│ ├── src/main.ts bootstrap; reads TRUST_PROXY → Express `trust proxy`
|
|
50
|
+
│ ├── src/app.module.ts ShieldModule.forRoot() — the ONE seam (auth/identity wiring
|
|
51
|
+
│ │ documented in its header comment)
|
|
52
|
+
│ ├── src/health/ @Public health probes + /demo/public (the unclassified demo
|
|
53
|
+
│ │ route is commented out → 404; see demo.controller.ts)
|
|
54
|
+
│ ├── src/features/ EMPTY — your editable space
|
|
55
|
+
│ ├── test/ enforcement e2e: 200 health; a test-only unclassified route is
|
|
56
|
+
│ │ denied 403 and fails the verifyGatable boot check
|
|
57
|
+
│ ├── eslint.config.mjs the feature wall: no raw DB access anywhere in src/** except
|
|
58
|
+
│ │ app.module.ts / main.ts
|
|
59
|
+
│ ├── scripts/ dev runner (sets NODE_ENV=development), guide sync
|
|
60
|
+
│ └── .env.example config references only (boots with no .env)
|
|
61
|
+
├── frontend/ only with --with-frontend (see below)
|
|
62
|
+
├── .astram/guide/ DEVELOPER-OWNED guide layer (ADR-0025) — never touched on upgrade
|
|
63
|
+
├── CLAUDE.md, .gitignore
|
|
64
|
+
└── README.md how to run + how to add features the framework way
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`server/` scripts: `dev`, `build`, `start`, `test`, `lint`, `migration:run`,
|
|
68
|
+
`migration:status`, `migration:revert`, `guide:sync`.
|
|
69
|
+
|
|
70
|
+
## The frontend (`--with-frontend` / `astram-add-frontend`)
|
|
71
|
+
|
|
72
|
+
A Vite + React + shadcn/ui app whose views you own and edit; the auth and API logic comes from
|
|
73
|
+
`@astram/frontend-core` and `@astram/sdk` and updates with `npm update`. See
|
|
74
|
+
`frontend/FRONTEND.md` in the generated app for the update and re-skin model.
|
|
75
|
+
|
|
76
|
+
- **Sign-in flows:** login, MFA challenge (TOTP or a recovery code), forced MFA re-enrolment
|
|
77
|
+
after a recovery-code login, forgot password, reset password.
|
|
78
|
+
- **Account:** profile, change password, two-factor devices and recovery codes.
|
|
79
|
+
- **Administration** (shown only to users whose roles grant it): companies, users (memberships
|
|
80
|
+
and role postings), roles, access (role grants and access bundles), record policies (guided rule
|
|
81
|
+
builder), service accounts (roles, scope, API keys with an explicit expiry), cross-tenant grants
|
|
82
|
+
(offer, inbox, revoke), audit log; owner shares from record screens.
|
|
83
|
+
- **Sessions:** a 401 refreshes the access token once and retries (single-flight, shared across
|
|
84
|
+
tabs with Web Locks); a refresh outage keeps you signed in; a rejected refresh signs you out and
|
|
85
|
+
remembers the page; sign-out in one tab reaches the others.
|
|
86
|
+
- **Content-Security-Policy:** `index.html` ships a strict CSP (`script-src 'self'`, no inline
|
|
87
|
+
script or eval). A dev-only Vite plugin relaxes it for hot reload. If your API is not served
|
|
88
|
+
same-origin under `/api`, add its origin to `connect-src`.
|
|
89
|
+
|
|
90
|
+
The admin pages need the server's identity wiring (below); without it they 404.
|
|
91
|
+
|
|
92
|
+
## Turning on auth and the identity admin
|
|
93
|
+
|
|
94
|
+
`server/src/app.module.ts` starts enforcement-only (`ShieldModule.forRoot()`: no database, no
|
|
95
|
+
Redis). Its header comment shows the full wiring. In short:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const rootIds: string[] = [/* platform-admin user uuids, read from your config */];
|
|
99
|
+
const authzPool = createIdentityAuthzPool(); // from @astram/identity
|
|
100
|
+
|
|
101
|
+
imports: [
|
|
102
|
+
ShieldModule.forRoot({
|
|
103
|
+
auth: true,
|
|
104
|
+
emailSender: myEmailSender, // or 'disabled'
|
|
105
|
+
rootIds,
|
|
106
|
+
identityActionKeys: identityAuthActionKeys,
|
|
107
|
+
identityControlPlaneKeys,
|
|
108
|
+
recordAuthorization: buildIdentityRecordAuthorization(authzPool, [/* YourType */]),
|
|
109
|
+
}),
|
|
110
|
+
IdentityFeatureModule.forRoot({ rootIds, extraObjects: [/* YourType */] }),
|
|
111
|
+
],
|
|
38
112
|
```
|
|
39
113
|
|
|
114
|
+
## Production requirements
|
|
115
|
+
|
|
116
|
+
With `auth: true`, check every item before the first production deploy.
|
|
117
|
+
|
|
118
|
+
- **`emailSender`.** Outside `NODE_ENV=development|test`, `auth: true` fails boot unless you pass
|
|
119
|
+
an `EmailSender` (`send({ to, subject, html, text })`, e.g. SMTP or SendGrid) or
|
|
120
|
+
`emailSender: 'disabled'` (no reset emails; `POST /auth/forgot-password` still answers 204).
|
|
121
|
+
The dev logger, which prints reset links, is never used in production.
|
|
122
|
+
- **Explicit `NODE_ENV`.** Set `NODE_ENV=production`. `npm run dev` sets `development` for you;
|
|
123
|
+
`npm start` sets nothing. An unset `NODE_ENV` counts as production for the email rule, and the
|
|
124
|
+
framework's dev seed migrations run only under `development` (or `ASTRAM_SEED_DEV=1`).
|
|
125
|
+
- **`TRUST_PROXY`.** The real number of reverse-proxy hops in front of the app (`0` = none,
|
|
126
|
+
`1` = behind the Astram Traefik). Unset turns the per-IP limits for login, forgot-password and
|
|
127
|
+
reset-password off, with a boot warning. Too high lets clients spoof their IP.
|
|
128
|
+
- **`REFRESH_HMAC_KEY`.** A long random secret, supplied through your secrets mechanism, never
|
|
129
|
+
committed. Boot fails without it.
|
|
130
|
+
- **Redis.** Required with auth: sessions, cached permission facts, the permission version, MFA
|
|
131
|
+
challenges and rate limits live there, and the request path reads only Redis. If Redis is down,
|
|
132
|
+
requests fail closed (401) and refresh answers 503, within `REDIS_COMMAND_TIMEOUT_MS` (default
|
|
133
|
+
2000). The app's Redis user must be allowed `EVAL` (the shipped `infra/redis` ACL allows it;
|
|
134
|
+
check your production ACL).
|
|
135
|
+
- **`rootIds`.** Pass the same platform-administrator user uuids to `ShieldModule.forRoot` and
|
|
136
|
+
`IdentityFeatureModule.forRoot`, from config. Without them nobody logs in as root and the
|
|
137
|
+
identity module logs a boot warning. The generated `app.module.ts` does not pass them yet; add
|
|
138
|
+
them when you turn auth on.
|
|
139
|
+
- **Migrations with the DDL account.** Run `npm run migration:run` with `DDL_DB_USER` /
|
|
140
|
+
`DDL_DB_PASSWORD` (a DDL-capable deploy account; host, port and database come from `APP_DB_*`).
|
|
141
|
+
The running app's `APP_DB_*` account must not hold DDL. The framework's guard and identity
|
|
142
|
+
migrations run first, then yours (`APP_MIGRATION_DIR`). Upgrading to guard 0.8 needs identity
|
|
143
|
+
migration `1710000000019-AddSessionMustReenroll`: apply migrations **before** starting the new
|
|
144
|
+
version. Concurrent deploys are safe (migrations hold a database lock;
|
|
145
|
+
`APP_MIGRATION_LOCK_TIMEOUT`, default 120 s).
|
|
146
|
+
- **`--strict`.** `migration:status` and `migration:run` report migrations whose file changed
|
|
147
|
+
after they ran (drift). Add `--strict` in CI against a fresh database to fail on drift. Do not
|
|
148
|
+
edit an applied migration. Out-of-order pending migrations are only a warning, because
|
|
149
|
+
framework upgrades ship migrations older than your own.
|
|
150
|
+
- **Existing Astram infra database volumes.** Init SQL runs only on an empty volume. On a volume
|
|
151
|
+
created before 2026-10-08, apply
|
|
152
|
+
`infra/mariadb/upgrade/2026-10-08-shield-core-audit-append-only.sql` once as root (it makes the
|
|
153
|
+
runtime `shield_core` account append-only on `shield_audit`). See `infra/README.md` in the
|
|
154
|
+
astram-framework repo.
|
|
155
|
+
- **API clients other than the generated frontend** must refresh tokens one at a time (one
|
|
156
|
+
in-flight `/auth/refresh` per session). Concurrent refreshes of the same token can trip reuse
|
|
157
|
+
detection, which revokes the session (the user must sign in again).
|
|
158
|
+
|
|
159
|
+
## Upgrading from 0.5.x
|
|
160
|
+
|
|
161
|
+
The generated app owns its files, and the scaffolder never overwrites them, so an upgrade has
|
|
162
|
+
two parts.
|
|
163
|
+
|
|
164
|
+
**1. Bump the framework ranges** in `server/package.json` (`@astram/core` ^1.4.0,
|
|
165
|
+
`@astram/guard` ^0.8.0, `@astram/engine` ^0.4.0, `@astram/identity` ^0.5.0,
|
|
166
|
+
`@astram/migrate` ^0.3.0, `@astram/guide` ^0.2.0; `@nestjs/*` ^11.2.7) and
|
|
167
|
+
`frontend/package.json` (`@astram/frontend-core` ^0.5.0), then `npm install` in both. Run the
|
|
168
|
+
migrations before starting the new server.
|
|
169
|
+
|
|
170
|
+
**2. Copy these files from a fresh scaffold** (`npx create-astram-app@latest tmp-app
|
|
171
|
+
--with-frontend`), and merge by hand where you changed them:
|
|
172
|
+
|
|
173
|
+
| File | Why |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `server/src/main.ts` | reads `TRUST_PROXY` |
|
|
176
|
+
| `server/scripts/dev.mjs` | `npm run dev` sets `NODE_ENV=development` |
|
|
177
|
+
| `server/eslint.config.mjs` (delete the old `eslint.config.js`), `lint` script in `server/package.json` | lint no longer crashes; wider feature wall over `src/**/*.{ts,mts,cts}` |
|
|
178
|
+
| `server/scripts/sync-astram-guide.mjs` | guide sync only in an app root, `ASTRAM_GUIDE_SYNC=0`, never fails install |
|
|
179
|
+
| `server/.env.example` | `TRUST_PROXY`, `NODE_ENV`, `REDIS_COMMAND_TIMEOUT_MS`, `LOGIN_IP_*` |
|
|
180
|
+
| `frontend/src/auth/AuthContext.jsx`, `sessionFlow.js`, `routeGate.js` | token refresh; admin nav after a fresh login; gated deep links |
|
|
181
|
+
| `frontend/src/App.jsx` | `RequireAuth` waits for auth facts |
|
|
182
|
+
| `frontend/src/platform/sessionStore.js` (plus doc-only `httpClient.js`, `shieldTransport.js`) | cross-tab session sync |
|
|
183
|
+
| `frontend/index.html`, `frontend/vite.config.js` | Content-Security-Policy and its dev relaxation |
|
|
184
|
+
| `frontend/src/pages/service-accounts/ServiceAccountsPage.jsx`, `accessModel.js` | asks for an API-key expiry (the server now requires one) |
|
|
185
|
+
|
|
186
|
+
**Breaking and behaviour changes** (details in each package's CHANGELOG):
|
|
187
|
+
|
|
188
|
+
- **guard 0.8 (breaking):** `auth: true` outside development/test fails boot without
|
|
189
|
+
`emailSender` (or `'disabled'`).
|
|
190
|
+
- **Login:** users with a confirmed MFA device must complete `/auth/mfa` on every password login;
|
|
191
|
+
5 wrong TOTP codes lock MFA for 15 minutes; a TOTP code works once. Lockout counters are per
|
|
192
|
+
account now (they reset once on upgrade). Per-IP limits need `TRUST_PROXY`. Disabled accounts
|
|
193
|
+
cannot sign in, and disabling a user ends their sessions.
|
|
194
|
+
- **Sessions:** a refresh-minted access token lives at most the access TTL; the previous token
|
|
195
|
+
stops working 30 s after a refresh; a replayed refresh token kills the session's live access
|
|
196
|
+
tokens; roles follow posting changes on the next request without re-login; after a Redis loss
|
|
197
|
+
the next refresh rebuilds the session instead of forcing a login. A Redis outage now fails fast
|
|
198
|
+
(about 2 s) instead of hanging; rate limiters stop limiting during the outage.
|
|
199
|
+
- **Identity API:** errors map to 403/404/409/400/503 instead of 500. `PUT /identity/companies/:uuid`
|
|
200
|
+
changes only `name`/`status`; moves use `PUT /identity/companies/:uuid/parent`. User updates
|
|
201
|
+
accept only `username`/`email`/`status`, and changing another user's email or status needs
|
|
202
|
+
dominance over that user. Role grants and postings are limited to roles you hold and that are
|
|
203
|
+
used only inside your subtree. Non-root admins cannot create top-level companies or attach a
|
|
204
|
+
user from outside their subtree. Grant changes are audited. Issuing a service-account key needs
|
|
205
|
+
`expiresAtMs` or `neverExpires: true`. API keys use the account's current scope and roles.
|
|
206
|
+
- **Engine:** non-root creates of subtree-scoped types are walled (403 outside your subtree);
|
|
207
|
+
update/delete/get of an unreachable id is a 403 instead of a silent no-op; record rules with
|
|
208
|
+
field conditions now apply to single-record reads and writes; unsorted lists come back in
|
|
209
|
+
primary-key order. Custom `RecordEngine` implementations must add `transaction` and
|
|
210
|
+
`lockForUpdate`.
|
|
211
|
+
- **Migrate:** migrations are ordered numerically (mixed-width prefixes may reorder; check
|
|
212
|
+
`migration:status`), runs take a database lock, and the tracking table gains a `checksum` column
|
|
213
|
+
(added automatically).
|
|
214
|
+
- **Core:** registering the same `(typeKey, actionKey)` twice across modules now fails at boot.
|
|
215
|
+
|
|
40
216
|
## Guarantees
|
|
41
217
|
|
|
42
|
-
- **Immutable-core consumption** — the emitted `package.json`
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
- **Never clobbers** — scaffolding into an existing directory
|
|
46
|
-
do
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
with no further wiring. (A turnkey login/session boilerplate arrives in a later
|
|
51
|
-
framework release; today the app gives correct-by-default enforcement.)
|
|
52
|
-
- **Honest** — the CLI never prints "not implemented"; it scaffolds a real app and
|
|
53
|
-
tells you how to run it and add a feature.
|
|
218
|
+
- **Immutable-core consumption** — the emitted `package.json` files use published semver ranges,
|
|
219
|
+
never `workspace:*` or a source symlink. The framework upgrades via `npm update`; your `src/`
|
|
220
|
+
is never touched.
|
|
221
|
+
- **Never clobbers** — scaffolding into an existing directory (or re-running
|
|
222
|
+
`astram-add-frontend`) writes only files that do not exist yet. Your `docs/`, `.astram/guide/`,
|
|
223
|
+
README and edited files are left as they are and reported as skipped.
|
|
224
|
+
- **Runs immediately** — `npm install && npm run dev` boots a framework-enforced API with no
|
|
225
|
+
`.env`. Real login and the identity admin need the database, Redis and the wiring above.
|
|
54
226
|
|
|
55
227
|
## Commands (for developing this package)
|
|
56
228
|
|
|
57
229
|
```bash
|
|
58
|
-
pnpm --filter create-astram-app build # tsc → dist/index.js
|
|
59
|
-
pnpm --filter create-astram-app test # vitest — CLI
|
|
230
|
+
pnpm --filter create-astram-app build # tsc → dist/index.js, dist/add-frontend.js
|
|
231
|
+
pnpm --filter create-astram-app test # vitest — CLI, never-clobber, emitted shape, lint wall
|
|
60
232
|
pnpm --filter create-astram-app lint
|
|
233
|
+
pnpm template:verify # (repo root) scaffold + build/lint/test against packed tarballs
|
|
61
234
|
```
|
package/dist/add-frontend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add-frontend.js","sourceRoot":"","sources":["../src/add-frontend.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAmB,MAAM,YAAY,CAAC;AAExE;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,CAAC;IAC1C,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAuB,CAAC;YAC5E,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC/D,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YACzB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,yEAAyE;YACzE,wEAAwE;QAC1E,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;AACvB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;
|
|
1
|
+
{"version":3,"file":"add-frontend.js","sourceRoot":"","sources":["../src/add-frontend.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -11,17 +11,18 @@
|
|
|
11
11
|
* builds features in the emitted `src/features/`; the framework lives read-only
|
|
12
12
|
* in `node_modules` and upgrades through `npm update`.
|
|
13
13
|
*
|
|
14
|
-
* What the emitted app IS
|
|
14
|
+
* What the emitted app IS:
|
|
15
15
|
* A running, framework-ENFORCED NestJS app. `ShieldModule.forRoot()` is the ONE
|
|
16
16
|
* registration seam: it wires the global deny-by-default guard (ADR-0002), so
|
|
17
17
|
* every route is denied unless explicitly classified (`@Public()`/`@Classified()`).
|
|
18
18
|
* Health probes + a demo route prove enforcement from the consumer's side.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Login, sessions, MFA and the identity admin come from the framework: turn them on
|
|
20
|
+
* with `ShieldModule.forRoot({ auth: true, … })` + `IdentityFeatureModule.forRoot()` (see
|
|
21
|
+
* the comment block in server/src/app.module.ts). `--with-frontend` adds the web UI.
|
|
21
22
|
*
|
|
22
23
|
* Reference skeleton: `examples/example-backend/` in the astram-framework repo.
|
|
23
24
|
*/
|
|
24
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from 'node:fs';
|
|
25
26
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
26
27
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
28
|
/** Token substituted with the app name at copy time (in package.json name, README title, …). */
|
|
@@ -137,7 +138,8 @@ export function copyTemplate(srcDir, destDir, appName, report, relBase = '') {
|
|
|
137
138
|
}
|
|
138
139
|
/** Text extensions that get token substitution + utf8 handling. */
|
|
139
140
|
function isTextFile(fileName) {
|
|
140
|
-
return /\.(json|ts|js|mjs|cjs|md|txt|example|yml|yaml|gitignore)$/i.test(fileName) ||
|
|
141
|
+
return (/\.(json|ts|js|mjs|cjs|md|txt|example|yml|yaml|gitignore)$/i.test(fileName) ||
|
|
142
|
+
fileName.startsWith('_'));
|
|
141
143
|
}
|
|
142
144
|
/**
|
|
143
145
|
* Names of the template's top-level SUBTREES that are copied separately (not
|
|
@@ -248,17 +250,20 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
248
250
|
console.log(' identity UI). You can still add any UI framework yourself later.');
|
|
249
251
|
}
|
|
250
252
|
console.log(' • The framework installed in server/ as immutable dependencies (@astram/core,');
|
|
251
|
-
console.log(' @astram/guard, @astram/engine, @astram/
|
|
253
|
+
console.log(' @astram/guard, @astram/engine, @astram/identity, @astram/migrate, @astram/sdk,');
|
|
254
|
+
console.log(' @astram/guide) — you never edit it; upgrade via');
|
|
252
255
|
console.log(' `npm update`.');
|
|
253
256
|
console.log(' • server/src/features/ — your editable API space. Add a feature by writing a');
|
|
254
257
|
console.log(' Nest module/controller there and registering it in server/src/app.module.ts.');
|
|
255
258
|
console.log(' • .astram/guide/ — your own, upgrade-safe project-rules space (ADR-0025).');
|
|
256
|
-
console.log('\n
|
|
257
|
-
console.log('
|
|
259
|
+
console.log('\n Login, sessions, MFA and the identity admin are built in: enable them in');
|
|
260
|
+
console.log(' server/src/app.module.ts (see its comment block). Before production, read the');
|
|
261
|
+
console.log(' README "Production requirements" (emailSender, NODE_ENV, TRUST_PROXY, migrations).');
|
|
258
262
|
console.log('\nNext steps:');
|
|
259
263
|
console.log(` cd ${appName}/server`);
|
|
260
264
|
console.log(' npm install');
|
|
261
|
-
console.log(' npm run dev # boots the API; GET /health → 200,
|
|
265
|
+
console.log(' npm run dev # boots the API; GET /health → 200, GET /demo/public → 200');
|
|
266
|
+
console.log(' npm test # proves deny-by-default: an unclassified route is denied');
|
|
262
267
|
if (withFrontend) {
|
|
263
268
|
console.log('');
|
|
264
269
|
console.log(` cd ${appName}/frontend`);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-astram-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "Scaffolder for a new Astram app — emits a running, framework-enforced
|
|
5
|
+
"description": "Scaffolder for a new Astram app — emits a running, framework-enforced NestJS server (deny-by-default, built-in auth/identity to switch on) and, with --with-frontend, a React admin/auth UI, over the immutable @astram/* framework packages.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"create-astram-app": "dist/index.js",
|
package/template/README.md
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
# __APP_NAME__
|
|
2
2
|
|
|
3
|
-
An **Astram consumer app**, scaffolded with `create-astram-app`. A
|
|
4
|
-
`
|
|
5
|
-
immutable framework packages
|
|
3
|
+
An **Astram consumer app**, scaffolded with `create-astram-app`. A `server/` (the API) and,
|
|
4
|
+
if you scaffolded with `--with-frontend` or ran `astram-add-frontend`, a `frontend/` (the
|
|
5
|
+
identity UI). The API depends on the published, immutable framework packages
|
|
6
|
+
([`@astram/core`](https://www.npmjs.com/package/@astram/core),
|
|
6
7
|
[`@astram/guard`](https://www.npmjs.com/package/@astram/guard),
|
|
7
8
|
[`@astram/engine`](https://www.npmjs.com/package/@astram/engine),
|
|
9
|
+
[`@astram/identity`](https://www.npmjs.com/package/@astram/identity),
|
|
10
|
+
[`@astram/migrate`](https://www.npmjs.com/package/@astram/migrate),
|
|
11
|
+
[`@astram/guide`](https://www.npmjs.com/package/@astram/guide),
|
|
8
12
|
[`@astram/sdk`](https://www.npmjs.com/package/@astram/sdk)) and enforces
|
|
9
13
|
**deny-by-default** from the very first boot. You build features; the framework lives
|
|
10
|
-
read-only in `
|
|
14
|
+
read-only in `node_modules` and upgrades via `npm update` — you never edit it.
|
|
11
15
|
|
|
12
16
|
## Layout
|
|
13
17
|
|
|
14
18
|
```
|
|
15
19
|
__APP_NAME__/
|
|
16
20
|
server/ the API (NestJS + the Astram framework enforcement)
|
|
17
|
-
frontend/
|
|
21
|
+
frontend/ the identity UI (only with --with-frontend / astram-add-frontend)
|
|
18
22
|
.astram/ your project-owned, upgrade-safe AI/dev guide (ADR-0025)
|
|
19
23
|
docs/ your own docs (the scaffolder never clobbers this)
|
|
20
24
|
```
|
|
@@ -24,7 +28,7 @@ __APP_NAME__/
|
|
|
24
28
|
```bash
|
|
25
29
|
cd server
|
|
26
30
|
npm install
|
|
27
|
-
npm run dev # boots the API
|
|
31
|
+
npm run dev # boots the API on http://localhost:3000 (sets NODE_ENV=development)
|
|
28
32
|
```
|
|
29
33
|
|
|
30
34
|
Then:
|
|
@@ -32,15 +36,19 @@ Then:
|
|
|
32
36
|
```bash
|
|
33
37
|
curl -i localhost:3000/health # 200 — @Public health probe
|
|
34
38
|
curl -i localhost:3000/demo/public # 200 — explicit @Public() opt-out
|
|
35
|
-
curl -i localhost:3000/demo/unclassified # 403 — DENIED (no classification)
|
|
36
39
|
```
|
|
37
40
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
The scaffold ships no unclassified route (the demo one in
|
|
42
|
+
`src/health/demo.controller.ts` is commented out, so `/demo/unclassified` is a 404).
|
|
43
|
+
The point is: **every route is denied unless you explicitly classify it.** An
|
|
44
|
+
unclassified route gets 403 from the global guard, and once `IdentityFeatureModule`
|
|
45
|
+
is wired the app refuses to boot (`verifyGatable`). Forgetting to declare a route
|
|
46
|
+
locks the door instead of silently exposing it. `npm test`
|
|
47
|
+
(`test/enforcement.e2e.spec.ts`) proves both with a test-only controller.
|
|
41
48
|
|
|
42
49
|
Other `server/` scripts: `npm run build` (→ `dist/`), `npm start` (run the build),
|
|
43
|
-
`npm test`, `npm run lint
|
|
50
|
+
`npm test`, `npm run lint`, `npm run migration:run` / `migration:status` /
|
|
51
|
+
`migration:revert`.
|
|
44
52
|
|
|
45
53
|
## How the API wires the framework
|
|
46
54
|
|
|
@@ -58,10 +66,27 @@ export class AppModule {}
|
|
|
58
66
|
```
|
|
59
67
|
|
|
60
68
|
`ShieldModule.forRoot()` binds the global deny-by-default guard (ADR-0002), the
|
|
61
|
-
boot-pool assertion (ADR-0018), and the Core decision-authority token.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
69
|
+
boot-pool assertion (ADR-0018), and the Core decision-authority token. With no options it
|
|
70
|
+
needs no database or Redis. To turn on login, sessions, MFA and the identity admin
|
|
71
|
+
(`/auth/*`, `/account/*`, `/identity/*`), follow the commented wiring in the header of
|
|
72
|
+
`server/src/app.module.ts`: `ShieldModule.forRoot({ auth: true, emailSender, rootIds,
|
|
73
|
+
identityActionKeys, identityControlPlaneKeys, recordAuthorization })` plus
|
|
74
|
+
`IdentityFeatureModule.forRoot({ rootIds, extraObjects })`. The framework provides the
|
|
75
|
+
credential store, MFA, session handling and the decision authority; you write none of them.
|
|
76
|
+
|
|
77
|
+
### Before you deploy with auth on
|
|
78
|
+
|
|
79
|
+
- Pass an `emailSender` (or `'disabled'`): outside `NODE_ENV=development|test` boot fails
|
|
80
|
+
without one. Set `NODE_ENV=production` explicitly; `npm start` does not set it.
|
|
81
|
+
- Set `TRUST_PROXY` to your real proxy hop count, or the per-IP login/reset limits stay off.
|
|
82
|
+
- Set `REFRESH_HMAC_KEY` (a long random secret, never committed) and `REDIS_URL`. Redis is
|
|
83
|
+
required: the request path reads only Redis and fails closed when it is down.
|
|
84
|
+
- Pass the same `rootIds` (platform-admin user uuids, from config) to both `forRoot` calls.
|
|
85
|
+
- Run `npm run migration:run` with the DDL account (`DDL_DB_USER` / `DDL_DB_PASSWORD`) before
|
|
86
|
+
starting a new version. Add `--strict` in CI to fail on edited migrations.
|
|
87
|
+
|
|
88
|
+
The full list, and how to upgrade an app scaffolded with an earlier version, is in the
|
|
89
|
+
[`create-astram-app` README](https://www.npmjs.com/package/create-astram-app).
|
|
65
90
|
|
|
66
91
|
## Add a feature (the framework way)
|
|
67
92
|
|
|
@@ -69,18 +94,24 @@ credentialResolver, decisionAuthority })`.
|
|
|
69
94
|
2. Register it in `server/src/app.module.ts` (add the module to `imports`, or its
|
|
70
95
|
controller to `controllers`).
|
|
71
96
|
3. **Classify every route:** `@Public()` for open routes, `@Classified({ typeKey,
|
|
72
|
-
action })` for guarded ones. Anything unclassified is denied (403
|
|
97
|
+
action })` for guarded ones. Anything unclassified is denied by design (403; with
|
|
98
|
+
`IdentityFeatureModule` wired, the app refuses to boot).
|
|
73
99
|
4. **Data access goes through the framework's RecordEngine** — never a raw DB pool.
|
|
74
|
-
The `server/eslint.config.
|
|
100
|
+
The `server/eslint.config.mjs` wall fails lint on off-road DB access anywhere in
|
|
101
|
+
`src/**` except `app.module.ts` and `main.ts`.
|
|
75
102
|
5. Add a test (see `server/test/` for the pattern).
|
|
76
103
|
|
|
77
104
|
`server/src/features/` starts empty — it's your space.
|
|
78
105
|
|
|
79
106
|
## The frontend
|
|
80
107
|
|
|
81
|
-
`
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
With `--with-frontend` (or `npx -p create-astram-app astram-add-frontend` from this
|
|
109
|
+
folder), `frontend/` holds a Vite/React identity UI: login with MFA and recovery codes,
|
|
110
|
+
forgot/reset password, account settings, and admin pages for companies, users, roles,
|
|
111
|
+
access, record policies, service accounts, cross-tenant grants and audit. It refreshes
|
|
112
|
+
expired access tokens on its own and ships a strict Content-Security-Policy. The views are
|
|
113
|
+
yours to edit; the logic updates through `@astram/frontend-core`. See
|
|
114
|
+
`frontend/FRONTEND.md`.
|
|
84
115
|
|
|
85
116
|
## The `.astram/guide/` directory
|
|
86
117
|
|
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
# frontend
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The Astram **identity UI** for this app: a Vite + React + shadcn/ui app scaffolded by
|
|
4
|
+
`create-astram-app --with-frontend` or `astram-add-frontend`. The files here are yours to
|
|
5
|
+
edit; the auth and API logic comes from `@astram/frontend-core` and `@astram/sdk` and
|
|
6
|
+
updates with `npm update`. See [`FRONTEND.md`](./FRONTEND.md) for the update, no-clobber
|
|
7
|
+
and re-skin model.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run dev # http://localhost:5173; proxies /api → http://localhost:3000 (API_URL to override)
|
|
12
|
+
```
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
- `POST /auth/refresh` `{ refreshToken }` → new tokens
|
|
12
|
-
- `POST /auth/logout` `{ sessionId }` → 204
|
|
13
|
-
- Protected routes: send `Authorization: Bearer <accessToken>`. A `@Classified` route
|
|
14
|
-
is enforced by the framework — no token → 401, denied by policy → 403.
|
|
14
|
+
Start the API (`../server`) first, with auth turned on, or there is nothing to sign in to.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
What it includes:
|
|
17
|
+
|
|
18
|
+
- Login, MFA challenge (TOTP or a recovery code), forced re-enrolment after a recovery-code
|
|
19
|
+
login, forgot password, reset password.
|
|
20
|
+
- Account: profile, change password, two-factor devices and recovery codes.
|
|
21
|
+
- Admin pages, shown only when your roles allow them: companies, users (memberships and
|
|
22
|
+
role postings), roles, access, record policies, service accounts, cross-tenant grants,
|
|
23
|
+
audit.
|
|
24
|
+
- Token refresh: a 401 refreshes once and retries (single-flight, shared across tabs).
|
|
25
|
+
- A strict Content-Security-Policy in `index.html`. If the API is not served same-origin
|
|
26
|
+
under `/api`, add its origin to `connect-src`.
|
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
<html lang="en" suppressHydrationWarning>
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
+
<!--
|
|
6
|
+
Content-Security-Policy (D11). The session tokens live in localStorage, so no script
|
|
7
|
+
may run unless it is served from this origin: no inline scripts, no eval, no third-party
|
|
8
|
+
script hosts. Allowed beyond 'self':
|
|
9
|
+
style-src 'unsafe-inline' the UI kit (sonner, Radix scroll lock) injects <style> tags
|
|
10
|
+
fonts.googleapis/gstatic the Inter/JetBrains Mono import in src/index.css
|
|
11
|
+
img-src data: blob: the MFA QR code is a data: URL
|
|
12
|
+
`npm run dev` relaxes script-src/connect-src for Vite's React-refresh preamble and HMR
|
|
13
|
+
websocket (see `devCsp` in vite.config.js); the built app gets this policy unchanged.
|
|
14
|
+
Add an API origin to connect-src if the frontend calls one other than same-origin /api.
|
|
15
|
+
frame-ancestors cannot be set in a <meta>; send it (or X-Frame-Options) as a header.
|
|
16
|
+
-->
|
|
17
|
+
<meta
|
|
18
|
+
http-equiv="Content-Security-Policy"
|
|
19
|
+
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: blob:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'"
|
|
20
|
+
/>
|
|
5
21
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
6
22
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
23
|
<title>__APP_NAME__</title>
|