From 6fb20b3a0ffc66be8ac7622ed22bbcdd1131618f Mon Sep 17 00:00:00 2001 From: Jen Garcia Date: Sun, 20 Sep 2026 07:32:44 -0400 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20adopt=20core=200.37=20=E2=80=94=20p?= =?UTF-8?q?ermissions=20as=20associations,=20and=20the=20no-bump=20tier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record permissions are associations now, and six operations no longer move a version. Both reach the command surface, so this is one change rather than two. **The ACL is per-element.** `Permission[]` is replaced by two association kinds — `permission`, whose bit is its label and whose grantee carries a required role, and `anyone` for world-read spelled affirmatively. `perm` calls `grantAccess()`/`revokeAccess()` instead of reading the whole list, editing an entry and writing it back: that read-modify-write dropped whatever a second admin granted between the read and the write, which is exactly the case a sharing command has to survive. The flags follow the model rather than the old shape. `--public` becomes `--anyone`, which carries read alone, so `--write` beside it is refused here rather than written and bounced by core. `--group` requires `--role`, because member and admin name two different sets of people and neither is a safe default. `perm add --read --write` grants read first and `perm rm` withdraws write first — the one ordering that satisfies core's rule that no write lands in a set whose grantee cannot read it; the rule itself is left to core's message rather than re-derived. `perm ls` is new: the ACL was otherwise only legible in `show --json`. `grant`'s target is core's required grantee union — `--default` becomes `--authenticated`, a DID-holder tier distinct from `--anyone`'s anonymous reach, and `--group` takes `--role`. `grant ls` accepts the same flags as a query, widened by the listing-only `--role any`, and `grant rm` reports a count now that `revoke()` returns what it withdrew. **Associating, sharing, moving and (un)listing bump nothing.** `tag`, `link`, `perm` and `attach` stop printing a version that did not move and compare the record before and after instead, so a repeat says `already tagged` rather than claiming a write. `commit` reports the version its own `mutate()` produced; the tag and attachment reconcile that follow cannot move it. Naming `contentPatch` alongside `parentId` is what keeps a move under `ifVersion`, which is now pinned from both sides. Also: `rm --hard` reports the files the purged record referenced, since destroying the record destroys the only rows naming them, and `attach add` records an `attachmentRecordId` so a filename resolves to the upload that reference came from. Co-Authored-By: Claude Opus 5 --- .changeset/adopt-core-0-37.md | 43 +++++ README.md | 11 +- docs/design.md | 131 ++++++++----- package.json | 10 +- pnpm-lock.yaml | 94 ++++----- pnpm-workspace.yaml | 16 +- scripts/smoke.mjs | 14 ++ src/cli.ts | 126 +++++++----- src/commands/access.ts | 320 +++++++++++++++++++------------ src/commands/associations.ts | 49 +++-- src/commands/attach.ts | 18 +- src/commands/edit.ts | 30 +-- src/commands/records.ts | 14 +- src/index.ts | 9 +- tests/access.test.ts | 180 +++++++++++++---- tests/associations.test.ts | 22 ++- tests/edit-commit.test.ts | 38 +++- tests/record-format.test.ts | 4 +- tests/server-integration.test.ts | 12 +- 19 files changed, 775 insertions(+), 366 deletions(-) create mode 100644 .changeset/adopt-core-0-37.md diff --git a/.changeset/adopt-core-0-37.md b/.changeset/adopt-core-0-37.md new file mode 100644 index 0000000..b50c47b --- /dev/null +++ b/.changeset/adopt-core-0-37.md @@ -0,0 +1,43 @@ +--- +'@haverstack/cli': minor +--- + +Adopt core 0.37: record permissions are associations, and six operations no longer bump a version + +**`Permission[]` is gone.** A record's ACL is two association kinds over the same table — +`{ kind: 'permission', label: 'read' | 'write', grantee }` and `{ kind: 'anyone', label: +'read' }` — with `grantAccess()` / `revokeAccess()` adding and withdrawing exactly one +element. `hstack perm` follows: it no longer reads the whole list, edits an entry and +writes the array back, a read-modify-write that silently discarded whatever a second +admin granted in between. + +The flags move with the model. `--public` becomes `--anyone`, which carries `read` alone +(`--write` beside it is refused rather than written and bounced). `--group` now requires +`--role member` or `--role admin`, because member and admin are two different elements and +neither is a safe guess. `perm rm` naming neither `--read` nor `--write` withdraws the +target's access entirely; naming one withdraws just that one. `perm add --read --write` +grants read first and `perm rm` withdraws write first, which is the one ordering that +satisfies core's rule that no write lands in a set whose grantee cannot read it. New: +`hstack perm ls ` prints the whole ACL. + +`hstack grant`'s target is core's required `GrantGrantee` union: `--default` becomes +`--authenticated` (any entity holding a DID — a tier below `--anyone`, which also reaches +anonymous requesters), and `--group` takes `--role`. `grant ls` accepts the same flags as +a query, widened by the listing-only `--role any`, and `grant rm` reports how many grants +it withdrew now that `revoke()` returns them. + +**`associate`, `dissociate`, `permissions`, `reparent`, `unlist` and `list` are no-bump**: +they leave `version` and `updatedAt` exactly where they stand. `tag`, `link`, `perm` and +`attach` therefore stop printing a version that did not move, and compare the record +before and after instead — a repeat now says `already tagged` rather than claiming a write +that did not happen. `hstack commit` reports the version its own `mutate()` produced, +since the tag and attachment reconcile that follow cannot move it. + +Also: `hstack rm --hard` reports the files the purged record referenced (core's +`delete()` returns them, because destroying the record destroys the only rows naming +them), and `hstack attach add` records an `attachmentRecordId` so a filename resolves to +the upload that reference came from. + +**Not backwards compatible.** The SQLite schema is create-if-missing with no migrations, +so a database written before core 0.37 keeps its old tables and fails on the first +permission write. Existing stacks must be recreated. diff --git a/README.md b/README.md index a79b12b..4aab6c8 100644 --- a/README.md +++ b/README.md @@ -53,12 +53,19 @@ hstack show # render one record hstack rm # soft-delete; hstack restore to undo hstack tag add