@astram/guide 0.2.6 → 0.2.8

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 +101 -6
  2. package/package.json +1 -1
package/CAPABILITIES.md CHANGED
@@ -17,12 +17,12 @@ 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({
@@ -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
 
@@ -131,7 +135,62 @@ imports: [
131
135
  customer business data.
132
136
  - Migrations for `shield_identity`, `shield_authz`, `shield_app` identity tables ship in
133
137
  `@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`.
138
+ (099/100) run only with `NODE_ENV=development` or `ASTRAM_SEED_DEV=1`. Anywhere else, create the
139
+ first users with `npm run bootstrap:org` (below).
140
+ - **API-created users can be reached by reset.** `POST /identity/users` with an email and a
141
+ company creates the user, an active membership and a **locked** credential in one transaction
142
+ (no password works until the user sets one). The response is `{ id, credential: 'locked' }`, or
143
+ `{ id, credential: 'none', reason }` (`no_email`, `no_company`; `no_provisioner` only when the
144
+ service is built without `IdentityFeatureModule.forRoot`) when no credential was made.
145
+ Once your `EmailSender` works, `POST /auth/forgot-password` reaches the user. A user whose email
146
+ is set later stays locked until the next `bootstrap:org` run gives them a credential.
147
+ - **Role owner.** Every role records an owner company when it is created: a non-root admin's
148
+ active company; none when root creates it in the admin UI (a shared, root-only role). It cannot
149
+ be changed by update. Deleting a company clears the owner of its roles. This round records
150
+ ownership only: which roles an admin can see or administer is unchanged.
151
+
152
+ ## Organisation bootstrap (`npm run bootstrap:org`, `@astram/identity` `runOrgBootstrap`, ADR-0042)
153
+ - **What it does.** Creates your organisation from one JSON file at deploy time: platform roots,
154
+ a company tree (tenants and `children`), departments and branches per company, roles with their
155
+ permission keys, and users with one active membership and their role postings. It runs against
156
+ your own `AppModule` (`NestFactory.createApplicationContext`), so keys of your `extraObjects` are
157
+ grantable. Every write goes through the framework's services and `decide()`, as the declared
158
+ operation `shield:org-bootstrap`; audit rows carry `operation`, `step` and `onBehalfOf` (the
159
+ operator). It needs auth on and `IdentityFeatureModule` wired; without the module it exits with
160
+ "wire IdentityFeatureModule in AppModule before running bootstrap:org".
161
+ - **File.** `{ "version": 1, "roots": [{ username, email, homeCompany }], "tenants": [company] }`;
162
+ `company` = `{ name, departments?, branches?, roles?: [{ name, permissions }], users?:
163
+ [{ username, email, company?, postings?: [{ role, department?, branch? }] }], children? }`.
164
+ Unknown fields, unknown permission keys (allowed: catalog keys plus `<feature>:*` and
165
+ `<feature>:<type>:*`), duplicate usernames or emails and references to undeclared roles,
166
+ departments or branches are refused before any write. No password, uuid or secret in the file.
167
+ A user with no postings is allowed (signs in, holds nothing).
168
+ - **Run.** `npm run bootstrap:org -- --file org.json` is a dry run (prints `create` / `unchanged` /
169
+ `drift` / `not-in-file` / `conflict`). Add `--apply` to write; it needs
170
+ `ASTRAM_BOOTSTRAP_OPERATOR`. Root passwords: `ASTRAM_BOOTSTRAP_ROOT_PASSWORD_<USERNAME>` (a
171
+ `secret-ref:` outside development/test, so a `SECRETS_PROVIDER` is needed) or `--interactive`.
172
+ Other flags: `--reissue-links`, `--link-ttl-hours <1..168>` (default 72), `--links-to-stdout`,
173
+ `--claim-unowned-roles`. Runs hold a database lock.
174
+ - **First sign-in.** Non-root users get a locked credential and a one-time set-password link,
175
+ printed once, to a terminal only (`--links-to-stdout` sends them to a pipe, where logs can keep
176
+ them). The operator delivers each link; the user redeems it at the reset page. Outside
177
+ development/test `RESET_LINK_BASE_URL` must be set and not localhost.
178
+ - **Roots.** The run prints each root's uuid and a `SHIELD_ROOT_IDS=…` line: set it and restart
179
+ (`rootIdsFromEnv`, above).
180
+ - **Re-runs and new tenants.** Additive and idempotent: a second `--apply` writes nothing. A file
181
+ covers only the tenants it declares, so onboard a new tenant with a new file. A role name owned by
182
+ another tenant is a `conflict` (roles are still global by name); an unowned role needs
183
+ `--claim-unowned-roles`.
184
+ - **What it does NOT do.** No deletes, renames, moves or revokes (differences are reported only);
185
+ it never activates an inactive membership or updates a changed email. No upload from the UI. No
186
+ administrator invite route or emailed link yet (G-010); department and branch admin routes are
187
+ separate too.
188
+ - **Existing apps** (scaffolded before this release) have no `bootstrap:org`. Upgrade
189
+ `@astram/identity` and `@astram/guard` (`npm update @astram/identity @astram/guard`), copy
190
+ `src/bootstrap-org.ts` and `src/bootstrap-org.args.ts` from a freshly scaffolded app (or from
191
+ `create-astram-app/template/server/src/` in the framework repo) into `server/src/`, and add
192
+ `"bootstrap:org": "node --env-file-if-exists=.env dist/bootstrap-org.js"` to the `scripts` of
193
+ `server/package.json`. `npm run build` compiles both files; no other change is needed.
135
194
 
136
195
  ## The record pipe (`@astram/engine`)
137
196
  - **`RecordEngine`** (token `RECORD_ENGINE`) / `RecordsService` — create/get/update/delete/list,
@@ -313,7 +372,7 @@ upload(@Req() req: Record<string, unknown>) {
313
372
  the upload check.
314
373
  - **Lint wall** — the scaffold bans `multer`, `@aws-sdk/*`, `minio`, `fs` / `node:fs` (and
315
374
  `/promises`) in `src/features/**`: use `FileIntake` and `FilesService`, never the bucket.
316
- - Do not grant `stored_file:create` to app roles; uploads go through `FilesService`.
375
+ - Roles that upload need `stored_file:create` (FilesService creates the record as the caller), but app code must create `stored_file` rows only through `FilesService.store()` — never `records.create('stored_file', …)` / `createMany`, which would let a caller set its own `storageKey` (downloads still refuse a key outside the caller's tenant, KI #137).
317
376
  - Not provided: malware scanning, per-tenant encryption of file bytes (SSE-S3 only), streaming
318
377
  downloads, dedup (each upload is its own record; `sha256` is returned so you can detect repeats).
319
378
 
@@ -346,7 +405,41 @@ upload(@Req() req: Record<string, unknown>) {
346
405
  `APP_DB_NAME`, set the same `DDL_DB_NAME` for migrate and the server.
347
406
  - The `<ns>_backup` and `<ns>_monitor` accounts keep server-wide grants and can see every
348
407
  namespace on the server.
349
- - One database per account (some managed hosts) is NOT supported yet (needs the four databases).
408
+ - One database per account (some managed hosts): see the single-database layout below.
409
+
410
+ ## Single-database layout (`SHIELD_DB_LAYOUT=single`, ADR-0041, opt-in, reduced isolation)
411
+ - For hosts that give one database per account. The four-database layout stays the default.
412
+ - **Set:** `SHIELD_DB_LAYOUT=single`, `SHIELD_DB_NAME=<db>` (`^[a-z0-9_]{1,64}$`),
413
+ `SHIELD_DB_LAYOUT_ACK_REDUCED_ISOLATION=1` (exactly `1`, every environment, production
414
+ included), `APP_DB_USER`. Leave `SHIELD_DB_NAMESPACE` unset; `APP_DB_NAME`/`DDL_DB_NAME` unset
415
+ or `<db>`. Same values for the server and `npm run migration:run`. Anything else refuses to boot
416
+ and to migrate.
417
+ - **Set `POOL_MAX_ENGINE`** (e.g. 10). Unset, the engine pool uses `APP_DB_MAX_CONNECTIONS`, and
418
+ the single-layout budget `workers × (POOL_MAX_CORE + POOL_MAX_ENGINE + APP_AUTHZ_POOL_MAX) +
419
+ headroom` fails boot. `APP_DB_MAX_USER_CONNECTIONS` (the account's cap, if the host sets one) is
420
+ checked too.
421
+ - All framework tables keep their names in `<db>`; your tables live there too. **Reserved
422
+ table names:** your object types and migrations must not use a framework table name (`companies`,
423
+ `users`, `roles`, `departments`, `branches`, `company_membership`, `membership_posting`,
424
+ `company_departments`, `company_branches`, `service_account`, `shared_resource`, `stored_file`,
425
+ `identity_credential`, `session`, `refresh_token`, `mfa_device`, `password_reset_token`,
426
+ `mfa_recovery_code`, `email_change_token`, `permission`, `role_permission`,
427
+ `authz_permission_version`, `access_bundle`, `access_bundle_permission`,
428
+ `record_permission_rule`, `record_share`, `scope_closure`, `service_account_key`,
429
+ `cross_tenant_grant`, `audit_log`, the ledger `schema_migrations`) or `prune_audit`. In single
430
+ mode a clash refuses to boot (object type) or to migrate (`CREATE TABLE` in your migrations);
431
+ in the four layout it only warns, so you see it early. The migration check reads whole files,
432
+ `down()` included: an app whose migrations ever created or renamed onto a reserved name cannot
433
+ migrate in single mode.
434
+ - **One account** (`DDL_DB_*` unset ⇒ `APP_DB_*`): migrations and the server share the account
435
+ (`ALL PRIVILEGES ON <db>.*`). You lose append-only audit (#6), no runtime DDL and schema
436
+ separation (#11), the DB-enforced retention floor, and the separate DDL account; `audit:prune`
437
+ refuses to run. Boot logs this list every time (the privilege probe runs with
438
+ `ShieldModule.forRoot({ auth: true })`).
439
+ - **Two accounts** (if the host allows a second user): the framework repo's
440
+ `db-provision --layout single` keeps `audit_log` append-only and the runtime free of DDL, and
441
+ installs `prune_audit`; re-run it after a migration adds a table. Schema separation is still lost.
442
+ - Choose the layout at install: switching an existing install is not supported. MariaDB only.
350
443
 
351
444
  ## Migrations (`@astram/migrate`)
352
445
  - `migrate` / `migrate:revert` / `migrate:status` with the deploy-time DDL account (`DDL_DB_*`);
@@ -389,6 +482,8 @@ upload(@Req() req: Record<string, unknown>) {
389
482
  - External identity providers (OIDC, SAML, LDAP, Entra ID, Cognito, Okta, Google Workspace) and
390
483
  WebAuthn ceremonies.
391
484
  - Tenant- or role-mandated MFA (MFA is enforced for users who enrolled a device).
392
- - Single-database hosting (all framework tables in one database per account).
485
+ - An administrator invite (admin-console route that emails a set-password link) and framework
486
+ email/SMS delivery adapters (framework item G-010). Today first sign-in links come from
487
+ `npm run bootstrap:org` only.
393
488
 
394
489
  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.6",
3
+ "version": "0.2.8",
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",