@astram/guide 0.2.13 → 0.2.15
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 +66 -1
- package/package.json +1 -1
package/CAPABILITIES.md
CHANGED
|
@@ -429,6 +429,59 @@ imports: [
|
|
|
429
429
|
- `actionsOf(reg)` (`@astram/guard`) returns a registration's vocabulary. `actionsFor(typeKey)`
|
|
430
430
|
is deprecated and returns only the CRUD verbs for every type.
|
|
431
431
|
|
|
432
|
+
## Separation of duties (ADR-0048; core 1.12, engine 0.14, guard 0.21, identity 0.18)
|
|
433
|
+
- **Declare it on the type** when the people who started or asked about a record must not
|
|
434
|
+
approve it:
|
|
435
|
+
```ts
|
|
436
|
+
@ObjectType({
|
|
437
|
+
typeKey: 'my_batch',
|
|
438
|
+
actions: ['approve'],
|
|
439
|
+
separationOfDuties: [{
|
|
440
|
+
actions: ['approve'],
|
|
441
|
+
actorFields: ['createdBy'],
|
|
442
|
+
related: [{ typeKey: 'my_review_request', via: 'batchUuid', actorFields: ['createdBy'] }],
|
|
443
|
+
}],
|
|
444
|
+
// ...
|
|
445
|
+
})
|
|
446
|
+
```
|
|
447
|
+
`approve` is then denied (403) to whoever created the batch and to whoever created any
|
|
448
|
+
`my_review_request` row whose `batchUuid` is this batch, whatever their grants, owner
|
|
449
|
+
rights, shares or cross-tenant grants. No one is exempt: service accounts, delegated
|
|
450
|
+
operations and root (on a control-plane type) are bound too. Check it with
|
|
451
|
+
`records.authorize('my_batch', id, 'approve')`; an `update`/`delete` bound by the rule is
|
|
452
|
+
checked by the write itself. `records.authorizeType(typeKey, action)` refuses an action the
|
|
453
|
+
rule binds (`RegistrationError`): a type-level check cannot apply it. List both types in
|
|
454
|
+
`extraObjects`.
|
|
455
|
+
- What counts: the record's own `actorFields` (`trusted: 'actorId'` fields, or `uuid` fields
|
|
456
|
+
declared `immutable: true`, so nobody named can clear them first) and every related row's
|
|
457
|
+
`actorFields` (`trusted: 'actorId'` fields only, e.g. `createdBy`), every related row whatever
|
|
458
|
+
its status or tenant. Related rows are found by a `uuid` field that holds this record's id
|
|
459
|
+
(`via`, "down" only): declare the action on the record that others point at, and create a
|
|
460
|
+
related row only after reaching its `via` record (`records.get` / `authorize`).
|
|
461
|
+
- The rule reads the related rows that exist now: a requester who deletes their request is no
|
|
462
|
+
longer named. Do not grant `delete` on the related type to the people it names; cancel
|
|
463
|
+
requests through a status field instead (a cancelled row still counts).
|
|
464
|
+
- It needs the authority `IdentityFeatureModule` builds (not the legacy `decisionAuthority`
|
|
465
|
+
option) and a single installed `@astram/engine`; otherwise every bound action is denied.
|
|
466
|
+
- What it never does: bind `read` or `create` (boot refuses both), filter lists, or write an audit
|
|
467
|
+
row for the deny. If the principals cannot be read (a related value that is not a string, more
|
|
468
|
+
than 1,000 distinct people on one related field) the action is denied. A value of
|
|
469
|
+
`shield.separationOfDuties` that the engine did not build is dropped before the decision.
|
|
470
|
+
- Inside `records.transaction(fn)` the check holds a shared lock on the related rows it read, so
|
|
471
|
+
a new review request cannot be added between the check and your write; commit promptly. This
|
|
472
|
+
needs the MariaDB default isolation (REPEATABLE READ) and an index on the `via` column. An
|
|
473
|
+
approver and a requester racing can deadlock (MariaDB error 1213): retry the transaction.
|
|
474
|
+
- A bad declaration (undeclared action, unknown field, an own actor field that is neither `uuid`
|
|
475
|
+
nor `trusted: 'actorId'`, an own `uuid` actor field not declared `immutable: true`, a related
|
|
476
|
+
actor field that is not `trusted: 'actorId'`, a `via` that is
|
|
477
|
+
not an untrusted `uuid` field, unregistered related type, a tenant-scoped type pointing at a
|
|
478
|
+
type with no tenant column) fails boot with `RegistrationError`.
|
|
479
|
+
- **Tenant policies on people fields**: a `trusted: 'actorId'` field (e.g. `createdBy`) is a
|
|
480
|
+
reference to `user`, and `@Field({ references: 'user' })` marks any other field as one. Record
|
|
481
|
+
policies may then use `is_owner` / `is_not_owner` on it ("is me" / "is not me" in the
|
|
482
|
+
scaffolded builder), e.g. deny `activate` where `createdBy` is me. A tenant admin can delete
|
|
483
|
+
such a policy: use the declared rule above for anything the app requires.
|
|
484
|
+
|
|
432
485
|
## File upload and object storage (`@astram/guard` + `@astram/identity`)
|
|
433
486
|
One file per request, kept in a private S3/MinIO bucket with SSE-S3, with a `stored_file` record
|
|
434
487
|
on the record pipe. You write no storage, MIME or authorization code.
|
|
@@ -534,7 +587,19 @@ upload(@Req() req: Record<string, unknown>) {
|
|
|
534
587
|
- **Set `POOL_MAX_ENGINE`** (e.g. 10). Unset, the engine pool uses `APP_DB_MAX_CONNECTIONS`, and
|
|
535
588
|
the single-layout budget `workers × (POOL_MAX_CORE + POOL_MAX_ENGINE + APP_AUTHZ_POOL_MAX) +
|
|
536
589
|
headroom` fails boot. `APP_DB_MAX_USER_CONNECTIONS` (the account's cap, if the host sets one) is
|
|
537
|
-
checked too.
|
|
590
|
+
checked too. The budget also counts 1 connection per worker for the audit sealer's own pool.
|
|
591
|
+
- **Set `POOL_WORKERS` and `APP_DB_MAX_CONNECTIONS` too** (your real worker count and what the
|
|
592
|
+
server gives you). Their defaults (8 workers, 250) describe a dedicated four-database server; on
|
|
593
|
+
them the single-layout budget is 8 × 27 + 40 = 256 > 250 and boot fails.
|
|
594
|
+
- **Idle connections and timeouts (G-026, both layouts):** the `POOL_MAX_*` values are caps. Idle,
|
|
595
|
+
every framework pool shrinks to `POOL_MIN_IDLE` (default 1) after `POOL_IDLE_TIMEOUT_S` (default
|
|
596
|
+
600), so an idle process holds about 4 connections. Optional `APP_DB_QUERY_TIMEOUT_MS`
|
|
597
|
+
(1000–3600000) aborts a longer statement (the framework's lock waits and the sealer are exempt);
|
|
598
|
+
optional `POOL_LEAK_DETECTION_MS` logs a connection held longer. Invalid values fail boot. Never
|
|
599
|
+
build or patch a pool to tune these; if a setting is missing, flag a gap.
|
|
600
|
+
- **Shutdown:** `main.ts` must call `app.enableShutdownHooks()` (the scaffold does; add it to an app
|
|
601
|
+
scaffolded before G-026). Then a SIGTERM stops the framework's jobs, finishes in-flight requests
|
|
602
|
+
and closes every pool. Set PM2 `kill_timeout` (e.g. 5000 ms) above the shutdown time.
|
|
538
603
|
- All framework tables keep their names in `<db>`; your tables live there too. **Reserved
|
|
539
604
|
table names:** your object types and migrations must not use a framework table name (`companies`,
|
|
540
605
|
`users`, `roles`, `departments`, `branches`, `company_membership`, `membership_posting`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/guide",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
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",
|