@astram/guide 0.2.4 → 0.2.6
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 +211 -7
- package/package.json +1 -1
package/CAPABILITIES.md
CHANGED
|
@@ -44,8 +44,19 @@ imports: [
|
|
|
44
44
|
`identityControlPlaneKeys?`, `config?`, and optional overrides `credentialResolver?`,
|
|
45
45
|
`profileResolver?`, `credentialWriter?`, `resetTokenStore?`, `recoveryCodeStore?`,
|
|
46
46
|
`profileWriter?`, `sessionSweeper?`, `auditSink?`, `mfaDeviceStore?`, `totpVerifier?`,
|
|
47
|
-
`sessionFactsProjector?`, `scopeSubtreeResolver?`, `membershipChecker
|
|
48
|
-
|
|
47
|
+
`sessionFactsProjector?`, `scopeSubtreeResolver?`, `membershipChecker?`, and
|
|
48
|
+
`storage?: ObjectStore | 'env'` (file storage, below). With `auth: true` every override has a
|
|
49
|
+
MariaDB/Redis default; leave them out unless you have a reason.
|
|
50
|
+
- **Secrets at boot** — `main.ts` calls `await resolveBootSecrets()` (`@astram/guard`) first,
|
|
51
|
+
then loads `AppModule` with `await import('./app.module')` (the scaffold already does). Every env
|
|
52
|
+
value written as `secret-ref:<path>` is replaced with the secret before any `forRoot` reads env.
|
|
53
|
+
Provider: `SECRETS_PROVIDER` = `none` (default) | `encrypted-file` (`SECRETS_FILE`,
|
|
54
|
+
`SECRETS_KEK_FILE` (a mounted 32-byte key file, never an env value), `SECRETS_DEK_FILE`); `vault`
|
|
55
|
+
is reserved and refused. A missing, unreadable or corrupt `SECRETS_FILE` aborts boot naming the
|
|
56
|
+
variable, even before any reference uses it. An unresolvable reference, or a reference with no provider, aborts
|
|
57
|
+
boot (env key and path only, never a value). If `main.ts` skips the call while a reference is in
|
|
58
|
+
env, `ShieldModule` refuses to boot (`assertNoUnresolvedSecretRefs`). Do not import `AppModule`
|
|
59
|
+
statically in `main.ts`.
|
|
49
60
|
- **`IdentityFeatureModule.forRoot({ rootIds?, extraObjects? })`** (`@astram/identity`). Pass the
|
|
50
61
|
SAME `rootIds` as `ShieldModule`. `extraObjects` (your own registered object types) are given
|
|
51
62
|
HERE ONLY: the module builds the record authorization from them and `ShieldModule` decides with
|
|
@@ -123,8 +134,8 @@ imports: [
|
|
|
123
134
|
(099/100) run only with `NODE_ENV=development` or `ASTRAM_SEED_DEV=1`.
|
|
124
135
|
|
|
125
136
|
## The record pipe (`@astram/engine`)
|
|
126
|
-
- **`RecordEngine`** (token `RECORD_ENGINE`) / `RecordsService` — create/get/update/delete/list
|
|
127
|
-
over registered object types through one pipe: permission gate (`decide()`) → server-trusted
|
|
137
|
+
- **`RecordEngine`** (token `RECORD_ENGINE`) / `RecordsService` — create/get/update/delete/list,
|
|
138
|
+
plus `aggregate` and `createMany`, over registered object types through one pipe: permission gate (`decide()`) → server-trusted
|
|
128
139
|
write context → validate → write → junction sync → audit (mandatory) → event.
|
|
129
140
|
- **Record decision inputs**: `get`, `update` and `delete` read the stored row first and pass its
|
|
130
141
|
fields and tenant to `decide()`, so record rules such as "deny update where status eq locked"
|
|
@@ -134,6 +145,44 @@ imports: [
|
|
|
134
145
|
(record rules, owner shares, cross-tenant grants, subtree), filtered in SQL; `get()` uses the
|
|
135
146
|
same scope. Membership/subtree-scoped lists ("companies I belong to") are provided.
|
|
136
147
|
- Stable paging: every `list()` orders by the primary key last.
|
|
148
|
+
- **`list()` filters** (`where`, field key → value, ANDed): a scalar is equality (`null` ⇒
|
|
149
|
+
`IS NULL`); or exactly one operator object per field: `eq`, `in` (≤ 1000 values; `[]` matches
|
|
150
|
+
nothing), `gt`/`gte`/`lt`/`lte`, `between: [lo, hi]` (range operators on `string`, `number`,
|
|
151
|
+
`uuid`, `timestamp` only), `isNull: true`, `isNotNull: true`. Every operand is validated and
|
|
152
|
+
coerced like a write (bad operand ⇒ `ValidationError`; uuids lower-cased, booleans bound as
|
|
153
|
+
1/0, timestamps as `Date`) and bound as a parameter. An unknown field or operator, two
|
|
154
|
+
operators in one object, or an operator not allowed for the field type (`eq`/`in` on `json`,
|
|
155
|
+
ranges on `boolean`/`json`) throws `RegistrationError`; nothing degrades to "no filter".
|
|
156
|
+
Only a scalar `null` means `IS NULL` (or use `isNull: true`): `{ eq: null }` and a `null`
|
|
157
|
+
inside `in` are rejected with `ValidationError`. A reversed `between` (`lo > hi`) is not
|
|
158
|
+
swapped and matches nothing. A `number` operand of ±Infinity is a `ValidationError`.
|
|
159
|
+
Filters run inside the same scoped query, so `list ≡ decide()` still holds.
|
|
160
|
+
- **Behaviour change (engine 0.7):** equality in `where` is now validated and coerced like a
|
|
161
|
+
write; `eq` on json fields is rejected; scalar `null` now means `IS NULL` (it matched nothing
|
|
162
|
+
before). A key present with the value `undefined` (`where: { x: undefined }`) now throws
|
|
163
|
+
`ValidationError`: when spreading optional filters into `where`, omit undefined keys. An invalid timestamp (an invalid `Date`, `NaN` or ±Infinity epoch) is now a
|
|
164
|
+
`ValidationError` on writes too, and so is ±Infinity on a `number` field (writes and filters).
|
|
165
|
+
- **`aggregate(typeKey, { where?, search?, groupBy?, measures, limit? })`**: `count` / `sum` /
|
|
166
|
+
`avg` / `min` / `max` over exactly the rows `list()` returns for the same `where`/`search`.
|
|
167
|
+
Gated as object-type `read` like `list()`; writes no audit row. `groupBy` ≤ 3 non-json keys;
|
|
168
|
+
1–10 measures, each `{ fn, field?, as }` with `as` matching `^[a-z_][a-z0-9_]{0,63}$`;
|
|
169
|
+
`sum`/`avg` on `number` only; `min`/`max` on `string`, `number`, `uuid`, `timestamp` (returns
|
|
170
|
+
the column's value, a `Date` for timestamps). Groups are ordered by the group keys; ungrouped ⇒
|
|
171
|
+
one group, grouped with no rows ⇒ `groups: []`. `limit` caps the number of groups (default 100,
|
|
172
|
+
max 1000).
|
|
173
|
+
- **Silent truncation:** a grouped aggregate returns at most `limit` groups and gives no signal
|
|
174
|
+
that more exist. If the number of groups can exceed `limit`, count the groups first or narrow
|
|
175
|
+
`where`.
|
|
176
|
+
- **DECIMAL precision:** `sum`/`avg` (and numeric `min`/`max`) come back as JS `number`. An
|
|
177
|
+
integer outside the safe range throws `EngineUnavailableError`, but a DECIMAL result that
|
|
178
|
+
does not fit a double is returned rounded. Do not use `aggregate` for exact money totals on
|
|
179
|
+
DECIMAL columns.
|
|
180
|
+
- **`createMany(typeKey, inputs)`**: 1–500 rows per call (otherwise `ValidationError` before any
|
|
181
|
+
write). Each row runs the full single `create()` pipe (gate, validate, create wall, audit) in
|
|
182
|
+
one transaction, in input order; ids come back in input order. The first failing row throws
|
|
183
|
+
and the whole batch rolls back, audit rows included. Each row gets its own audit event tagged
|
|
184
|
+
with the call's `batchId` and the row's `batchIndex` (ADR-0039). Inside `transaction(fn)` it
|
|
185
|
+
joins the outer transaction.
|
|
137
186
|
- `transaction(fn)` runs several pipe calls in one unit of work (rolls back on any failed write);
|
|
138
187
|
`lockForUpdate(typeKey, id)` serialises a check-then-write.
|
|
139
188
|
- `@Field({ immutable: true })` create-only fields; `trusted` fields are stamped server-side.
|
|
@@ -149,12 +198,167 @@ imports: [
|
|
|
149
198
|
(a Rule-4 STOP). Field types this version: `string`, `number`, `boolean`, `uuid`, `timestamp`,
|
|
150
199
|
`json`.
|
|
151
200
|
|
|
201
|
+
## Custom object actions (ADR-0038; engine 0.7, guard 0.14, identity 0.11)
|
|
202
|
+
- **Declare domain verbs on the type**: `@ObjectType({ ..., actions: ['activate', 'review'] })`.
|
|
203
|
+
The type's vocabulary is `read, create, update, delete` plus the declared actions, and each one
|
|
204
|
+
is a Gated permission key `identity:<typeKey>:<action>` on the type's `access.plane`.
|
|
205
|
+
`crudActions: false` drops the CRUD verbs (for authority-only types never written through the
|
|
206
|
+
engine, which then refuses create/update/delete on them; engine get/list on such a type without
|
|
207
|
+
a `read` action are denied by the gate, not refused pre-SQL like the writes); it needs a
|
|
208
|
+
non-empty `actions`.
|
|
209
|
+
- Action names: snake_case (`^[a-z][a-z0-9_]*$`), at most 64 characters, no duplicates; `*` and
|
|
210
|
+
`access` are reserved; a CRUD verb in `actions` is rejected unless `crudActions: false`. A bad
|
|
211
|
+
declaration fails boot with `RegistrationError` naming the type and action.
|
|
212
|
+
- **Gate a route on it**: `@Classified({ typeKey: '<type>', action: 'activate' })`. List the type in
|
|
213
|
+
`IdentityFeatureModule.forRoot({ extraObjects })` (the decision authority and the catalog only
|
|
214
|
+
know those types). An action the type does not declare still aborts boot (`verifyGatable`).
|
|
215
|
+
Without the grant the route is 403.
|
|
216
|
+
- **Record-level check in the handler**: `records.authorize(typeKey, id, 'activate')` (also on
|
|
217
|
+
`BoundRecords` and the `transaction(fn)` handle). It runs the type gate, reads the row by id in
|
|
218
|
+
the caller's tenant, and decides at record level (record rules and owner shares for that
|
|
219
|
+
action or `*`). Allow resolves `void`; deny, a missing row and another tenant's row all throw
|
|
220
|
+
the same `AuthorizationDeniedError` (403); an undeclared action throws `RegistrationError`
|
|
221
|
+
(500) before any query. It returns no row data, writes no audit row and is not a list filter.
|
|
222
|
+
- **A handler that writes needs two grants**: `authorize()` for the custom action, then the
|
|
223
|
+
write through the normal pipe (`records.update(...)`), which needs `update` and is audited.
|
|
224
|
+
There is no write under the custom action's key. With `activate` alone the update is 403. To
|
|
225
|
+
keep check and write consistent, run both inside `records.transaction(fn)`.
|
|
226
|
+
- **Two behaviours of `authorize()` to know**:
|
|
227
|
+
- On a type with `readScope`, a non-root caller is denied for a row outside its write scope
|
|
228
|
+
(the same wall `update` uses), even when the custom action reads like a non-write check.
|
|
229
|
+
- Inside `transaction(fn)` it reads the row `FOR UPDATE` (a row lock held until commit) even
|
|
230
|
+
though it writes nothing; outside a transaction it is a plain read.
|
|
231
|
+
- **Admin surface picks them up**: the permission catalog (`GET /identity/permission-catalog`),
|
|
232
|
+
role grants, the grant ceiling, `identity:<type>:*` wildcards and `@AccessBundle` all accept
|
|
233
|
+
the declared actions. A record share or a record policy for an action the type does not
|
|
234
|
+
declare is a 400 (`*` still allowed). A record policy on action `create` is also a 400
|
|
235
|
+
(`InvalidPolicyError`): record rules are evaluated per record and never apply at create, so
|
|
236
|
+
gate creation with the type's `create` grant; a `*` rule applies to the other actions. The
|
|
237
|
+
scaffolded access form shows each type's extra
|
|
238
|
+
actions as extra checkboxes.
|
|
239
|
+
- **Auth facts**: pass `buildIdentityAuthKeys(extraObjects)` (`@astram/identity`, returns
|
|
240
|
+
`{ actionKeys, controlPlaneKeys }`) to `ShieldModule.forRoot({ identityActionKeys,
|
|
241
|
+
identityControlPlaneKeys })` with the SAME `extraObjects` array, so `/identity/auth-facts`
|
|
242
|
+
includes your types' custom keys. The plain `identityAuthActionKeys` /
|
|
243
|
+
`identityControlPlaneKeys` cover identity's own types only.
|
|
244
|
+
- **Changing the action list later**:
|
|
245
|
+
- Removing a declared action orphans its catalog row: new grants and bundles on it fail, held
|
|
246
|
+
keys stop counting in auth facts, and a route classified on it fails boot. Re-adding the action
|
|
247
|
+
reactivates the row, and existing role grants on it take effect again. If that is not wanted,
|
|
248
|
+
revoke the grants before removing the action.
|
|
249
|
+
- Record shares and record rules with action `*` cover custom actions added later. Review
|
|
250
|
+
wildcard shares and rules when you add a sensitive action.
|
|
251
|
+
- A share whose action has since been removed can still be revoked: `unshare` skips the
|
|
252
|
+
declared-action check (revoking cannot grant). An unknown type is still a 400.
|
|
253
|
+
- `actionsOf(reg)` (`@astram/guard`) returns a registration's vocabulary. `actionsFor(typeKey)`
|
|
254
|
+
is deprecated and returns only the CRUD verbs for every type.
|
|
255
|
+
|
|
256
|
+
## File upload and object storage (`@astram/guard` + `@astram/identity`)
|
|
257
|
+
One file per request, kept in a private S3/MinIO bucket with SSE-S3, with a `stored_file` record
|
|
258
|
+
on the record pipe. You write no storage, MIME or authorization code.
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// app.module.ts: ShieldModule.forRoot({ ..., storage: 'env' }) + IdentityFeatureModule.forRoot(...)
|
|
262
|
+
import { Classified, FileIntake, FILE_INTAKE_REQUEST_KEY, type FileIntakeResult } from '@astram/guard';
|
|
263
|
+
import { FilesService } from '@astram/identity';
|
|
264
|
+
|
|
265
|
+
@Post('imports')
|
|
266
|
+
@Classified({ typeKey: 'stored_file', action: 'create' }) // or your own type/action
|
|
267
|
+
@UseInterceptors(FileIntake({ field: 'file', profile: 'xlsx' }))
|
|
268
|
+
upload(@Req() req: Record<string, unknown>) {
|
|
269
|
+
const intake = req[FILE_INTAKE_REQUEST_KEY] as FileIntakeResult;
|
|
270
|
+
return this.files.store({ ...intake.file, bytes: intake.bytes });
|
|
271
|
+
// 201 { uuid, sha256, sizeBytes, contentType, originalName }; never the storage key
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- **`FileIntake({ field, profile, maxBytes? })`** — runs after the global guard, so a denied
|
|
276
|
+
caller's body is never read. Exactly one file in the named field, no other fields. Checks size,
|
|
277
|
+
extension, declared type and magic bytes; the stored type comes from the profile, never from the
|
|
278
|
+
client; the name is sanitized for display only. Errors (HTTP status + fixed reason code, never
|
|
279
|
+
the name or bytes): 400 `missing_file`, `too_many_files` (also a file sent under another field
|
|
280
|
+
name: multer reports it as an unexpected file), `empty`, `bad_request` (malformed or truncated
|
|
281
|
+
multipart, raw control characters in part headers); 413 `too_large`; 415 `type_not_allowed`,
|
|
282
|
+
`content_mismatch`. Raw UTF-8 and RFC 5987 (`filename*=UTF-8''…`) file names decode to the
|
|
283
|
+
same name. Size: the route cap defaults to the profile's configured cap
|
|
284
|
+
(`OBJECT_STORAGE_MAX_BYTES` under `storage: 'env'`, else 10 MiB); `maxBytes` can only lower it.
|
|
285
|
+
Profiles: `xlsx` only (50 MiB hard ceiling for any profile, route or env value). Needs
|
|
286
|
+
`@nestjs/platform-express` (an optional peer; the scaffold has it).
|
|
287
|
+
- **`FilesService`** (provided by `IdentityFeatureModule`) — `store(intake)`, `open(uuid)`,
|
|
288
|
+
`remove(uuid)`. No actor argument: tenant, owner and audit actor come from the session. `store`
|
|
289
|
+
puts the object at `t/<tenant>/<uuid>`, then creates the record (gate, tenant stamp, audit); a
|
|
290
|
+
failed or denied create deletes the object. `remove` is a hard delete of record and object.
|
|
291
|
+
- **`GET /files/:uuid`** (framework route) — `stored_file:read` decided per record: the uploader
|
|
292
|
+
reads by owner baseline, anyone else needs a grant or a share; access is never inherited from
|
|
293
|
+
your own record that points at the file. Missing and foreign answer the same 403; non-uuid 400.
|
|
294
|
+
The bytes are re-hashed against the stored sha256. Headers: `Content-Disposition: attachment`
|
|
295
|
+
(RFC 5987 name), `X-Content-Type-Options: nosniff`, `Cache-Control: no-store`, and
|
|
296
|
+
`Content-Type` from the record when it is an allowlisted type, else `application/octet-stream`.
|
|
297
|
+
- **Status codes** — storage not configured (no `storage` option or `OBJECT_STORAGE_BUCKET`
|
|
298
|
+
unset) ⇒ 501 from `FilesService` and the route; store down, stored object missing or sha256
|
|
299
|
+
mismatch ⇒ 503 with a fixed message.
|
|
300
|
+
- **Audit** — the create is the pipe's `admin_action` row; each read, delete and refused access
|
|
301
|
+
writes a `file_access` event (no key, name or bytes). `file_access` is a new `AuditEventType`
|
|
302
|
+
member: add a case if you `switch` over the union.
|
|
303
|
+
- **Env** (`loadObjectStorageConfig`): `OBJECT_STORAGE_BUCKET` (unset ⇒ storage off),
|
|
304
|
+
`OBJECT_STORAGE_ENDPOINT` (`https:`; `http:` only in development/test or to a loopback host),
|
|
305
|
+
`OBJECT_STORAGE_REGION` (default `us-east-1`), `OBJECT_STORAGE_ACCESS_KEY_ID`,
|
|
306
|
+
`OBJECT_STORAGE_SECRET_ACCESS_KEY`, `OBJECT_STORAGE_SSE` (`AES256` only),
|
|
307
|
+
`OBJECT_STORAGE_MAX_BYTES` (plain decimal bytes, no leading zeros; the per-file cap for the
|
|
308
|
+
`xlsx` profile and every `FileIntake` route). Outside development/test the two credentials must be
|
|
309
|
+
`secret-ref:` values resolved at boot; a literal is refused. With `storage: 'env'` the bucket is
|
|
310
|
+
pinged at boot (probe put/get/delete under `_probe/`); failure aborts boot.
|
|
311
|
+
- **Your xlsx parser must cap entry count and total uncompressed size.** The framework checks only
|
|
312
|
+
the zip header and `[Content_Types].xml`, not every entry, so a zip bomb in another part passes
|
|
313
|
+
the upload check.
|
|
314
|
+
- **Lint wall** — the scaffold bans `multer`, `@aws-sdk/*`, `minio`, `fs` / `node:fs` (and
|
|
315
|
+
`/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`.
|
|
317
|
+
- Not provided: malware scanning, per-tenant encryption of file bytes (SSE-S3 only), streaming
|
|
318
|
+
downloads, dedup (each upload is its own record; `sha256` is returned so you can detect repeats).
|
|
319
|
+
|
|
320
|
+
## Database names and several environments on one server (`SHIELD_DB_NAMESPACE`, ADR-0037)
|
|
321
|
+
- The framework uses four databases, `<ns>_app`, `<ns>_identity`, `<ns>_authz` and `<ns>_audit`,
|
|
322
|
+
with its own accounts (`<ns>_core`, `<ns>_migrate`, `<ns>_audit_ops`, …). If you leave
|
|
323
|
+
`SHIELD_DB_NAMESPACE` unset, `<ns>` is `shield` (`shield_app`, …), which is unchanged from
|
|
324
|
+
earlier releases.
|
|
325
|
+
- **To run dev and beta on one MariaDB server**, give each environment its own namespace. It must
|
|
326
|
+
be lowercase letters and digits only (`beta`, `staging2`; no `_`). Then:
|
|
327
|
+
1. Provision it with the framework's `db-provision --namespace beta` (from the astram-framework
|
|
328
|
+
repo, runbook §6). Use distinct passwords per namespace.
|
|
329
|
+
2. Set `SHIELD_DB_NAMESPACE=beta` for both the server and `npm run migration:run`. The same
|
|
330
|
+
value must reach both.
|
|
331
|
+
3. Run migrations (the ledger is `beta_app.schema_migrations`), then start the server.
|
|
332
|
+
- Defaults follow the namespace: `APP_DB_USER` ⇒ `<ns>_core`, `APP_DB_NAME` ⇒ `<ns>_identity`,
|
|
333
|
+
`DDL_DB_USER` ⇒ `<ns>_migrate`. Explicit values win.
|
|
334
|
+
- **Rules your app must follow:**
|
|
335
|
+
- `APP_DB_NAME` must be one of the four framework databases, even with the namespace unset.
|
|
336
|
+
Any other value refuses to boot.
|
|
337
|
+
- An invalid namespace refuses to boot and to migrate.
|
|
338
|
+
- Use a separate Redis (instance or DB index) per environment: Redis keys are not namespaced.
|
|
339
|
+
- Your own object types keep a literal `schema` or none. Do not hard-code `shield_*` names in
|
|
340
|
+
app SQL or migrations: read them from the injected `FRAMEWORK_SCHEMAS` (`@astram/guard`).
|
|
341
|
+
- **Boot check:** outside development/test (an unset `NODE_ENV` counts as production), the server
|
|
342
|
+
refuses to start when framework migrations are missing from the ledger, no ledger is found, or
|
|
343
|
+
the ledger is unreachable. Run `migration:run` before starting. With the namespace unset, the
|
|
344
|
+
server looks for the ledger in `DDL_DB_NAME`, its own `APP_DB_NAME`, then `shield_app`, so
|
|
345
|
+
`migration:run` with the server's `.env` is found. If your migrate job uses a different
|
|
346
|
+
`APP_DB_NAME`, set the same `DDL_DB_NAME` for migrate and the server.
|
|
347
|
+
- The `<ns>_backup` and `<ns>_monitor` accounts keep server-wide grants and can see every
|
|
348
|
+
namespace on the server.
|
|
349
|
+
- One database per account (some managed hosts) is NOT supported yet (needs the four databases).
|
|
350
|
+
|
|
152
351
|
## Migrations (`@astram/migrate`)
|
|
153
352
|
- `migrate` / `migrate:revert` / `migrate:status` with the deploy-time DDL account (`DDL_DB_*`);
|
|
154
353
|
the running app never holds DDL. Framework migrations merge with yours (`APP_MIGRATION_DIR`).
|
|
155
354
|
- Numeric ordering, duplicate-id detection, a database lock so concurrent deploys never apply a
|
|
156
355
|
migration twice (`APP_MIGRATION_LOCK_TIMEOUT`, default 120 s), and drift detection (a changed
|
|
157
356
|
applied file is reported; `--strict` fails on it). Never edit a migration that already ran.
|
|
357
|
+
- Upgrade `@astram/migrate` together with `@astram/guard` and `@astram/identity`. The framework
|
|
358
|
+
migrations read the database names from the migrate context (`ctx.schemas`). An older migrate
|
|
359
|
+
works only while `SHIELD_DB_NAMESPACE` is unset, and stops with "upgrade @astram/migrate"
|
|
360
|
+
otherwise. On upgrade, `migrate:status --strict` reports the framework's rewritten migrations as
|
|
361
|
+
SUPERSEDED, not drift, and `migrate` updates their ledger rows without re-running them.
|
|
158
362
|
|
|
159
363
|
## Frontend (`@astram/frontend-core`, `@astram/sdk`, scaffolded `frontend/`)
|
|
160
364
|
- `@astram/frontend-core`: auth, account, identity, audit, record-policy, share, cross-tenant and
|
|
@@ -170,9 +374,8 @@ imports: [
|
|
|
170
374
|
|
|
171
375
|
## Available, but NOT wired to a data path (do not assume they protect data yet)
|
|
172
376
|
- **Envelope encryption / per-tenant DEKs** (`createEncryptionService`, `LocalKekCrypto`): the
|
|
173
|
-
engine exists, but no record field is encrypted and no production `DekStore` ships.
|
|
174
|
-
|
|
175
|
-
the guard's runtime config does not resolve `secret-ref:` values at boot.
|
|
377
|
+
engine exists, but no record field is encrypted and no production `DekStore` ships. Stored
|
|
378
|
+
files are encrypted by the bucket (SSE-S3), not per tenant.
|
|
176
379
|
- **Risk engine** (`createRiskEngine`): advisory scoring with no request-path caller.
|
|
177
380
|
- **Hash-chained audit** (`createAuditRecorder`, `verifyChain`): the shipped audit writer is
|
|
178
381
|
append-only but does not store the chain.
|
|
@@ -186,5 +389,6 @@ imports: [
|
|
|
186
389
|
- External identity providers (OIDC, SAML, LDAP, Entra ID, Cognito, Okta, Google Workspace) and
|
|
187
390
|
WebAuthn ceremonies.
|
|
188
391
|
- 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).
|
|
189
393
|
|
|
190
394
|
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.
|
|
3
|
+
"version": "0.2.6",
|
|
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",
|