@astram/guide 0.2.26 → 0.2.28
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 +67 -6
- package/package.json +1 -1
package/CAPABILITIES.md
CHANGED
|
@@ -82,7 +82,14 @@ imports: [
|
|
|
82
82
|
- **Live roles** — a role posting change, a grant change or a user disable bumps the permission
|
|
83
83
|
version; the next request reloads facts and roles. No re-login needed.
|
|
84
84
|
- **Engine errors → HTTP** — `AuthorizationDeniedError` → 403, `ValidationError` → 400,
|
|
85
|
-
`EngineUnavailableError` → 503
|
|
85
|
+
`EngineUnavailableError` → 503, `UniqueConflictError` → 409 `unique_conflict` for your
|
|
86
|
+
controllers too (global filter; id-free, value-free messages).
|
|
87
|
+
- **Duplicate keys** — a create / update that hits a UNIQUE index throws `UniqueConflictError`
|
|
88
|
+
(`typeKey`, `constraint` = the index name, `rowIndex` for `createMany`). Match it with
|
|
89
|
+
`isUniqueConflict(err, '<constraint>')`, never by message text. Inside `transaction(fn)` the
|
|
90
|
+
transaction is then rollback-only (a further write is a 503): re-read in a fresh transaction for
|
|
91
|
+
the idempotent answer. On a tenant type, lead every UNIQUE index with the tenant column, or a
|
|
92
|
+
409 reveals that another tenant holds the value.
|
|
86
93
|
|
|
87
94
|
## Authentication and sessions (`@astram/guard`, `auth: true`)
|
|
88
95
|
- `POST /auth/login` `{ identityHint, password }` → `{ session, tokens }`, or an MFA challenge.
|
|
@@ -748,6 +755,51 @@ upload(@Req() req: Record<string, unknown>) {
|
|
|
748
755
|
- Not provided: malware scanning, per-tenant encryption of file bytes (SSE-S3 only), streaming
|
|
749
756
|
downloads, dedup (each upload is its own record; `sha256` is returned so you can detect repeats).
|
|
750
757
|
|
|
758
|
+
### Sharing an attached file with a role (`SHIELD_FILE_SHARE`, guard 0.33, identity 0.32; ADR-0040 addendum)
|
|
759
|
+
Use this when users of a role in the same company must read a file attached to one of your records
|
|
760
|
+
("the stewards of a source read its revision file"). Never write `record_share` yourself and never
|
|
761
|
+
grant a tenant-wide `stored_file` rule for this.
|
|
762
|
+
1. **Declare the attachment field** on your parent type: `@Field({ type: 'uuid', references:
|
|
763
|
+
'stored_file' })`. The type must be tenant-walled (a `trusted: 'tenantId'` field), data plane,
|
|
764
|
+
not global, not `operationReads`, with a uuid primary key, and the field must not be `trusted`.
|
|
765
|
+
A type that declares such a field but breaks a rule fails boot naming the type and field; boot
|
|
766
|
+
logs one line `file-share parents: <type> (<fields>)`.
|
|
767
|
+
2. **Inject the port** in an app service (not an agent capability: that fails agent boot,
|
|
768
|
+
`inject_framework`):
|
|
769
|
+
```ts
|
|
770
|
+
import { Inject } from '@nestjs/common';
|
|
771
|
+
import { SHIELD_FILE_SHARE, FileShareError, type FileSharePort } from '@astram/guard';
|
|
772
|
+
constructor(@Inject(SHIELD_FILE_SHARE) private readonly fileShare: FileSharePort) {}
|
|
773
|
+
const r = await this.fileShare.shareWithRole(fileId, roleId, { typeKey: 'source', id: sourceId });
|
|
774
|
+
// r = { created, effective, missing }
|
|
775
|
+
```
|
|
776
|
+
It is request-scoped: actor and tenant come from the session, no argument names them. The caller
|
|
777
|
+
must be able to `update` the parent and read the file itself; the parent must reference the file
|
|
778
|
+
and the role must belong to the same company. Each call runs in one transaction (it joins yours;
|
|
779
|
+
a refusal then rolls yours back).
|
|
780
|
+
3. **The role still needs `identity:stored_file:read`.** The port never grants it. Without it the
|
|
781
|
+
share is written and `shareWithRole` returns `{ effective: false, missing:
|
|
782
|
+
['identity:stored_file:read'] }`; it works with no further call once the key is granted.
|
|
783
|
+
4. **Revoke in the same transaction as the detach, deactivate or delete.** Call
|
|
784
|
+
`unshare(fileId, roleId, parent)` when you detach a file (it works after the detach) and
|
|
785
|
+
`revokeForParent(parent)` before you deactivate or delete the parent. A share outlives its reason
|
|
786
|
+
if you skip this. After a parent is already deleted, `revokeForParent` still works for a caller
|
|
787
|
+
holding the parent type's `delete` key. `reconcileFile(fileId)` removes the file's port rows
|
|
788
|
+
whose parent is gone, no longer references it or is no longer eligible (any user who can read
|
|
789
|
+
the file); `listForParent(parent)` returns `{ fileId, roleId, createdAtMs }[]`. Those ids are
|
|
790
|
+
for linking only: resolve them to labels (role name, file display name) before showing them,
|
|
791
|
+
and never show a raw id in the UI.
|
|
792
|
+
- **Errors** (`FileShareError.code`, fixed messages, `httpStatus`): `file_share_invalid` 400
|
|
793
|
+
(malformed id, ineligible parent type), `file_share_not_found` 404 (any missing, hidden, foreign
|
|
794
|
+
or unattached id; root or tenantless caller), `file_share_denied` 403 (caller reads but may not
|
|
795
|
+
update the parent), `file_share_unavailable` 503 (database or audit outage; rolled back).
|
|
796
|
+
- **Audit and owner view.** Each change writes `share.created` / `share.removed` with operation
|
|
797
|
+
`shield:file-share` (`FILE_SHARE_OPERATION` from `@astram/identity`), step, parent type and id,
|
|
798
|
+
never the file name. The file owner (or a user `decide()` allows to `update` the file) sees these
|
|
799
|
+
rows in `GET /identity/record-shares` as `via: <parent type label>`, `removable: false`; deleting
|
|
800
|
+
one there answers 403 `This share is managed by a linked record.`
|
|
801
|
+
- Run `astram-migrate` before deploying (guard migration 126 on `record_share`); no env change.
|
|
802
|
+
|
|
751
803
|
## Database names and several environments on one server (`SHIELD_DB_NAMESPACE`, ADR-0037)
|
|
752
804
|
- The framework uses four databases, `<ns>_app`, `<ns>_identity`, `<ns>_authz` and `<ns>_audit`,
|
|
753
805
|
with its own accounts (`<ns>_core`, `<ns>_migrate`, `<ns>_audit_ops`, …). If you leave
|
|
@@ -972,11 +1024,20 @@ upload(@Req() req: Record<string, unknown>) {
|
|
|
972
1024
|
**propose**, and a person applies.
|
|
973
1025
|
- Declare what a change means with `defineProposalKind({ id, version, label, payloadSchema,
|
|
974
1026
|
targetTypes: [oneAppType], keys: { propose, review, apply }, plan, validate?, preview?,
|
|
975
|
-
distinctApprover?, maxItems?, expiryMs? })` (`@astram/identity`)
|
|
976
|
-
`IdentityFeatureModule.forRoot({ proposalKinds })`. `plan` turns a payload into
|
|
977
|
-
`update` items on that one type; `plan` and `validate` get a read-only `ctx.records`.
|
|
978
|
-
apply handler: the framework writes the approved items through the record pipe as the
|
|
979
|
-
in one transaction, with read-back. The three keys are custom actions on an app type
|
|
1027
|
+
distinctApprover?, maxItems?, expiryMs?, screen?, screenCodes?, submit? })` (`@astram/identity`)
|
|
1028
|
+
and pass it to `IdentityFeatureModule.forRoot({ proposalKinds })`. `plan` turns a payload into
|
|
1029
|
+
`create` / `update` items on that one type; `plan` and `validate` get a read-only `ctx.records`.
|
|
1030
|
+
There is no apply handler: the framework writes the approved items through the record pipe as the
|
|
1031
|
+
approver, in one transaction, with read-back. The three keys are custom actions on an app type
|
|
1032
|
+
(ADR-0038).
|
|
1033
|
+
- To check a draft before anything is stored (for example personal data in a label), give the kind
|
|
1034
|
+
`screen({ payload, title, rationale, metadata }, ctx)` and the codes it may refuse with in
|
|
1035
|
+
`screenCodes`. It runs on every submit path (the HTTP route, your own code through
|
|
1036
|
+
`SHIELD_PROPOSAL_PORT`, the assistant). Return `{ refuse: '<code>' }` to refuse: the caller gets
|
|
1037
|
+
422 `screened` with that code, and no proposal, audit row or notice is written. A throw or an
|
|
1038
|
+
undeclared code refuses too (`screen_failed`). Do not screen in `validate`: its refusals are stored.
|
|
1039
|
+
`submit: 'app-only'` stops the generic `POST /identity/proposals` route for that kind, so only your
|
|
1040
|
+
own routes (through `SHIELD_PROPOSAL_PORT`) and the assistant can submit it.
|
|
980
1041
|
- A capability with `effect: 'propose'`, `proposes: [kindIds]` and `requires` including
|
|
981
1042
|
`identity:agent:propose` returns a draft `{ kind, payload, title, rationale?, metadata? }`; the
|
|
982
1043
|
framework submits it as the user. The model gets a handle and counts; the browser gets
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astram/guide",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.28",
|
|
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",
|