@astram/guide 0.2.10 → 0.2.12

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 +51 -9
  2. package/package.json +1 -1
package/CAPABILITIES.md CHANGED
@@ -187,10 +187,23 @@ imports: [
187
187
  service is built without `IdentityFeatureModule.forRoot`) when no credential was made.
188
188
  Once your `EmailSender` works, `POST /auth/forgot-password` reaches the user. A user whose email
189
189
  is set later stays locked until the next `bootstrap:org` run gives them a credential.
190
- - **Role owner.** Every role records an owner company when it is created: a non-root admin's
191
- active company; none when root creates it in the admin UI (a shared, root-only role). It cannot
192
- be changed by update. Deleting a company clears the owner of its roles. This round records
193
- ownership only: which roles an admin can see or administer is unchanged.
190
+ - **Roles belong to one company** (G-013, ADR-0047). A role is assigned only in its own company
191
+ (postings and service accounts; 400 otherwise, root included), read and managed through the owner
192
+ wall (a parent company's admins manage their subsidiaries' roles; out of reach ⇒ 404), and its name
193
+ is unique within its company only (409 names the existing role). `GET|POST
194
+ /identity/companies/:companyUuid/roles`; `POST /identity/roles` creates the role in the caller's
195
+ active company (400 "Choose a company for the role." without one). Role routes need
196
+ `identity:company:read` (the `access-admin` bundle grants it). An ownerless role (seeded without
197
+ `r_owner_company_uuid`) is root-only and assignable nowhere.
198
+ - **Departments and branches belong to one company** (G-012). `POST
199
+ /identity/companies/:companyUuid/departments` and `/branches` (`{ name }`), `PUT` (rename) and
200
+ `DELETE` on `/identity/departments/:uuid` and `/identity/branches/:uuid`; the list routes are walled
201
+ and return `{ rows: [{ uuid, name, usedBy? }], total, page, limit }` (`?page&limit&search`). Names
202
+ are unique per company ignoring case and accents (409); delete is refused (409) while any posting
203
+ uses the row; at most 1000 of each per company. Access bundle `org-structure`.
204
+ - **The active membership is always `active`** (G-011): `PUT /identity/memberships/:uuid` refuses
205
+ activating a non-active membership or moving the active one off `active` (409), and a body that
206
+ sets `isActive: true` with another status (400).
194
207
  - **Security policies (mandatory 2FA, ADR-0045).** `GET|POST /identity/security-policies`,
195
208
  `GET|PUT|DELETE /identity/security-policies/:uuid` (permissions
196
209
  `identity:security_policy:{read,create,update,delete}`; no access bundle carries them). Any holder
@@ -233,7 +246,8 @@ imports: [
233
246
  `ASTRAM_BOOTSTRAP_OPERATOR`. Root passwords: `ASTRAM_BOOTSTRAP_ROOT_PASSWORD_<USERNAME>` (a
234
247
  `secret-ref:` outside development/test, so a `SECRETS_PROVIDER` is needed) or `--interactive`.
235
248
  Other flags: `--reissue-links`, `--link-ttl-hours <1..168>` (default 72), `--links-to-stdout`,
236
- `--claim-unowned-roles`. Runs hold a database lock.
249
+ `--claim-unowned-roles` (a no-op notice since roles belong to one company). Runs hold a database
250
+ lock.
237
251
  - **First sign-in.** Non-root users get a locked credential and a one-time set-password link,
238
252
  printed once, to a terminal only (`--links-to-stdout` sends them to a pipe, where logs can keep
239
253
  them). The operator delivers each link; the user redeems it at the reset page. Outside
@@ -241,9 +255,11 @@ imports: [
241
255
  - **Roots.** The run prints each root's uuid and a `SHIELD_ROOT_IDS=…` line: set it and restart
242
256
  (`rootIdsFromEnv`, above).
243
257
  - **Re-runs and new tenants.** Additive and idempotent: a second `--apply` writes nothing. A file
244
- covers only the tenants it declares, so onboard a new tenant with a new file. A role name owned by
245
- another tenant is a `conflict` (roles are still global by name); an unowned role needs
246
- `--claim-unowned-roles`.
258
+ covers only the tenants it declares, so onboard a new tenant with a new file. Roles, departments
259
+ and branches are created in the company that declares them, and the same name may exist in
260
+ another company. A posting must use a role declared on its own company (a role declared only on a
261
+ parent is a file error). After renaming a department or branch in the app, rename it in the file
262
+ too, or a re-run creates the old name again.
247
263
  - **What it does NOT do.** No deletes, renames, moves or revokes (differences are reported only);
248
264
  it never activates an inactive membership or updates a changed email. No upload from the UI.
249
265
  Department and branch admin routes are separate. With `emailSender` configured, `--email-links`
@@ -308,7 +324,11 @@ imports: [
308
324
  joins the outer transaction.
309
325
  - `transaction(fn)` runs several pipe calls in one unit of work (rolls back on any failed write);
310
326
  `lockForUpdate(typeKey, id)` serialises a check-then-write.
311
- - `@Field({ immutable: true })` create-only fields; `trusted` fields are stamped server-side.
327
+ - `@Field({ immutable: true })` create-only fields; `@Field({ nullable: true })` fields that
328
+ create, update and `createMany` may set to NULL (`json` included: SQL NULL, not `"null"`;
329
+ the audit row lists the cleared keys, never values; the column must be NULL-able in your
330
+ migration; refused on trusted, primary-key, `readScope` and `required` fields). Without
331
+ `nullable`, `null` is a validation error. `trusted` fields are stamped server-side.
312
332
  `readScope` declares the subtree wall; `create: 'unscoped'` opts a type out of the create wall.
313
333
  - **Physical wall** — the raw DB pool is sealed in the engine. The scaffold's
314
334
  `server/eslint.config.mjs` fails lint on driver imports, raw pools, `.query()` and the
@@ -321,6 +341,28 @@ imports: [
321
341
  (a Rule-4 STOP). Field types this version: `string`, `number`, `boolean`, `uuid`, `timestamp`,
322
342
  `json`.
323
343
 
344
+ ## Shared reference data: global types (ADR-0046; engine 0.11, guard 0.20)
345
+ - **Declare** `@ObjectType({ ..., access: { plane, global: true } })` for rows every tenant reads the
346
+ same (code lists, catalogues, units). No tenant field, no `readScope`. Registration refuses
347
+ `global` with `tenantScoped`, `owned`, `system`, a `readScope`, a `trusted: 'tenantId'` field,
348
+ `crudActions: true` or any action other than `read`, naming the type.
349
+ - **Reads** (list, get, `aggregate()`, search) carry no tenant clause, but the type's `read` grant
350
+ and deny policies still apply: a caller without `read` sees nothing. Root follows the plane:
351
+ `plane: 'data'` keeps root denied, `plane: 'control'` lets root read.
352
+ - **Writes** through the engine (create, update, delete, `createMany`) are refused for everyone,
353
+ root included, before any SQL. Seed and change rows only in an app migration, idempotent and
354
+ select-before-insert on the business key.
355
+ - **Policies do not cascade.** A record policy on a global type applies only to callers whose
356
+ active tenant is the policy's tenant, plus platform policies; a parent's deny does not reach its
357
+ child companies' users. To hide a row across a group, author the policy in each tenant or as a
358
+ platform policy.
359
+ - **No sharing, no cross-tenant reach**: a global type is never `owned`, so never a share target.
360
+ - **Tenant-less data types are now refused writes.** A `plane: 'data'` type with no tenant field,
361
+ no `readScope` and not global refuses non-root create, update, delete and `createMany`
362
+ (`AuthorizationDeniedError('tenantless_data_write_denied')`). Boot logs the global types once and
363
+ warns once per such type: make it global, give it a tenant field, or add a `readScope`.
364
+ - Never fake shared data with a match-all `readScope`, and never put tenant data in a global type.
365
+
324
366
  ## Custom object actions (ADR-0038; engine 0.7, guard 0.14, identity 0.11)
325
367
  - **Declare domain verbs on the type**: `@ObjectType({ ..., actions: ['activate', 'review'] })`.
326
368
  The type's vocabulary is `read, create, update, delete` plus the declared actions, and each one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astram/guide",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
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",