Skip to content

Commit 3085162

Browse files
committed
Update docs: two seams (authz + provenance), not three
README and ARCHITECTURE.md described Seam A/B/C (AdminAuthz, Identity, Provenance) and an ownerName field that no longer exist. Reflects the current shape: AdminAuthz and Provenance are the only host seams, and creatorKind needs no seam — it is a denormalized column set at write time.
1 parent e6dba84 commit 3085162

2 files changed

Lines changed: 46 additions & 19 deletions

File tree

‎ARCHITECTURE.md‎

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ Three options have no sensible default — `db`, `contentStore`,
4848
`resolvePrincipal` — and the rest degrade a *feature*, never safety, when
4949
omitted. The README's table of what a minimal host passes is the reference; what
5050
matters architecturally is that every default **fails closed**: no `adminAuthz`
51-
means nobody is an admin, no `identity` means no directory and no cross-tenant
52-
reads, no `provenance` means no decoration.
51+
means nobody is an admin and no tenant may read another's artifacts, no
52+
`provenance` means no decoration.
5353

5454
What the package does **not** require of a host: no auth middleware, no session
5555
library, no particular schema, no control-plane tables, no id scheme, no UI.
@@ -61,7 +61,7 @@ route surface.
6161

6262
## The ports
6363

64-
Five host seams. Four are types declared in `ports.ts`; `resolvePrincipal` is a
64+
Four host seams. Three are types declared in `ports.ts`; `resolvePrincipal` is a
6565
`mountArtifacts` option, typed at the mount boundary because it takes the host's
6666
request context as `unknown`. Every optional seam has a **named, exported**
6767
default.
@@ -70,18 +70,29 @@ default.
7070
| --- | --- | --- |
7171
| `resolvePrincipal` | Who the request runs as. Reads the host session; returns `null` when signed out. | none — required |
7272
| `ContentStore` | Where an artifact's file bytes live. | none — required |
73-
| `AdminAuthz` (Seam A) | Authorization. Only archive/unarchive consults it. | `denyAllAdminAuthz` |
74-
| `Identity` (Seam B) | Owner display names, the agent→human ownership resolution, creator-kind principal sets, and cross-tenant membership. | `anonymousIdentity` |
75-
| `Provenance` (Seam C) | A **display-only** decorator over serialized rows. | `noProvenance` |
76-
77-
Seam C's display-only status is a contract, not a convention: it may add fields
78-
to rows on their way out and must never affect *what* is returned or *who* may
79-
see it. That is why provenance is a port at all — joining a host's workflow
80-
tables to decorate a row would make this package depend on a schema it must not
81-
know, and the dep-guard fails the build on exactly that.
82-
83-
Seam B's `ownerIsMemberOfTenant` gates cross-tenant reads and must fail closed;
84-
the shipped `anonymousIdentity` does.
73+
| `AdminAuthz` (Seam A) | Authorization: who may administer (archive/unarchive) a row they do not exactly own, and who may cross a tenant boundary to read. | `denyAllAdminAuthz` |
74+
| `Provenance` (Seam B) | A **display-only** decorator over serialized rows. | `noProvenance` |
75+
76+
TWO seams, not three. There is no directory port: owner display names are
77+
gone from the serialized row entirely, and `?creatorKind=` is answered by a
78+
denormalized column (`artifact.creator_kind`) set once, from the caller's own
79+
scope, at write time — never resolved by a host lookup, so there is nothing
80+
here for a host to skip and silently return zero rows for.
81+
82+
Seam B's display-only status is a contract, not a convention: it may add
83+
fields to rows on their way out and must never affect *what* is returned or
84+
*who* may see it. That is why provenance is a port at all — joining a host's
85+
workflow tables to decorate a row would make this package depend on a schema
86+
it must not know, and the dep-guard fails the build on exactly that.
87+
88+
`AdminAuthz` answers two independent verdicts, both must fail closed:
89+
90+
- `canAdminister(scope, row)` — called only after the core has already
91+
granted the exact-owner match, so a host's implementation covers whatever
92+
else it wants to allow (a tenant admin, or the human member behind the agent
93+
that owns the row).
94+
- `canReadTenant(scope, targetTenantId)` — the gate on a cross-tenant
95+
`artifact_read`. The shipped `denyAllAdminAuthz` refuses both.
8596

8697
### ContentStore
8798

‎packages/artifact-core/README.md‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,34 @@ whole `createApp` wiring.
7474

7575
## The seams
7676

77-
Three options have no sensible default; the rest fail closed and degrade a *feature*,
77+
TWO seams — authorization and provenance. `?creatorKind=` needs no seam at all: it is a
78+
denormalized column set once, from the caller's own scope, at write time.
79+
80+
Two options have no sensible default; the rest fail closed and degrade a *feature*,
7881
never safety.
7982

8083
| Option | Required | Default and what omitting it costs |
8184
| --- | --- | --- |
8285
| `db` | **yes** | The drizzle handle the host already has |
8386
| `contentStore` | **yes** | `InlineContentStore` for a minimal host |
8487
| `resolvePrincipal` | **yes** | `(ctx: unknown) => { tenant, principal } \| null`. Identical to `@corbits/mailbox-core`'s, so a host mounting both passes one function to both. The resolved tenant is authoritative — no caller-supplied override. |
85-
| `adminAuthz` | no | `denyAllAdminAuthz` — only the owner (and the member behind a producing agent) can archive. Nothing becomes more permissive. |
86-
| `identity` | no | `anonymousIdentity` — `ownerName` is `null`, `?creatorKind=` matches nothing, cross-tenant reads refused. |
87-
| `provenance` | no | `noProvenance` — rows carry no decoration. Display-only by contract, so it can never change what is returned or who sees it. |
88+
| `adminAuthz` (Seam A) | no | `denyAllAdminAuthz` — nobody is an admin and no tenant may read another's artifacts. Nothing becomes more permissive. |
89+
| `provenance` (Seam B) | no | `noProvenance` — rows carry no decoration. Display-only by contract, so it can never change what is returned or who sees it. |
8890
| `uploadPolicy` | no | `ARTIFACT_UPLOAD_POLICY` — the standard document/image/spreadsheet allowlist. |
8991

92+
`AdminAuthz` answers two verdicts, both fail-closed:
93+
94+
```ts
95+
type AdminAuthz = {
96+
// Called only after the core has already granted the exact-owner match, so
97+
// a host implementation covers whatever else it wants to allow — a tenant
98+
// admin, or the human member behind the agent that owns the row.
99+
canAdminister(scope: ResolvedPrincipal, row: { ownerPrincipalId: string | null }): Promise<boolean>;
100+
// The gate on a cross-tenant artifact_read.
101+
canReadTenant(scope: ResolvedPrincipal, targetTenantId: string): Promise<boolean>;
102+
};
103+
```
104+
90105
## Routes
91106

92107
| Surface | Behavior |
@@ -150,6 +165,7 @@ try {
150165
createFileArtifact(tx, contentStore, {
151166
scope,
152167
ownerPrincipalId: scope.principal,
168+
creatorKind: "user",
153169
filename: file.name,
154170
mimeType: file.type,
155171
bytes,

0 commit comments

Comments
 (0)