@astram/guide 0.2.26 → 0.2.27

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