@vibes.diy/prompts 2.5.14 → 2.5.15

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/llms/fireproof.md CHANGED
@@ -420,11 +420,22 @@ export function announcements(doc, oldDoc, user, ctx) {
420
420
 
421
421
  if (doc.type === "roleGrant") {
422
422
  if (!user.isOwner) throw { forbidden: "owner only" };
423
- return { members: { [doc.role]: [doc.userHandle] } };
423
+ // A grant doc must ALSO route to a channel — a result with no `channels`
424
+ // (only `members`/`grant`) is rejected as an "unreadable write". Route it to
425
+ // an owner-readable admin channel (not a public one) so the grant persists
426
+ // and the owner can read the roster back; the membership then applies.
427
+ return {
428
+ channels: ["admin:grants"],
429
+ members: { [doc.role]: [doc.userHandle] },
430
+ grant: { users: { [user.userHandle]: ["admin:grants"] } },
431
+ };
424
432
  }
425
433
 
426
434
  if (doc.type === "post") {
427
435
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
436
+ // On update, the original author must be preserved — never let a writer
437
+ // overwrite someone else's doc or reassign its author.
438
+ if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
428
439
  ctx.requireAccess(doc.channel);
429
440
  return { channels: [doc.channel] };
430
441
  }
@@ -529,6 +540,7 @@ export function chat(doc, oldDoc, user, ctx) {
529
540
 
530
541
  if (doc.type === "post") {
531
542
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
543
+ if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
532
544
  // Any signed-in author may post to this open channel. Do NOT call
533
545
  // ctx.requireAccess(doc.channel) here: the channel is grant.public
534
546
  // (read-only), which never satisfies requireAccess, so gating on it would
@@ -589,12 +601,16 @@ Access functions live in `/access.js`, a separate file in the vibe's filesystem
589
601
 
590
602
  **`requireAccess` checks _membership_, not public read — don't gate an open channel's writes on it.** `ctx.requireAccess(channelId)` passes only for a channel the user is a member of: granted directly through `grant.users[handle]`, or through a `grant.roles` role they hold. **`grant.public` does NOT satisfy `requireAccess`** — public is read-only ("anyone through the door can _read_"), it never confers write membership. So a channel that is only `grant.public` and gated on `ctx.requireAccess` can be written by **nobody** but the owner-in-admin-mode — every other write returns `not in channel`, silently hiding the form (`useVibe().can` faithfully reflects this). Choose by intent:
591
603
 
592
- - **Open channel — any signed-in user may post** (public board, guestbook, comment wall): do **not** call `ctx.requireAccess`. Route the doc to the channel and check the author — `if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }; return { channels: [doc.channelId] }`. `grant.public` on the channel doc gives everyone read; the write is open to any author.
604
+ - **Open channel — any signed-in user may post** (public board, guestbook, comment wall): do **not** call `ctx.requireAccess`. Route the doc to the channel and check the author — `if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }; if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" }; return { channels: [doc.channelId] }`. `grant.public` on the channel doc gives everyone read; the write is open to any author.
593
605
  - **Restricted channel — only members may post**: gate the write on `ctx.requireAccess(doc.channelId)` **and** grant writers membership explicitly — `grant.users` (direct) or `grant.roles` + a `members`/role-grant doc. `public` alongside is read-only and is fine for letting non-members read, but it is never what lets a member write.
594
606
 
595
607
  ### AccessDescriptor return type
596
608
 
597
- All fields are optional, but a stored document must be routed to at least one channel (`channels`) to be readable — a result with no channels is refused at write time. To reject a write outright, `throw { forbidden: "reason" }`.
609
+ All fields are optional, but a stored document must be routed to at least one channel (`channels`) to be readable — a result with no channels is refused at write time ("unreadable write"). To reject a write outright, `throw { forbidden: "reason" }`.
610
+
611
+ **Grant/member/meta docs need a channel too.** A role grant, membership, or config singleton that returns only `members`/`grant` with **no `channels`** is refused exactly like any other channel-less write — so the owner can't even create it. Route these to an owner-readable **admin channel** (e.g. `channels: ["admin:grants"]` with `grant: { users: { [user.userHandle]: ["admin:grants"] } }`), not a public channel. The `members`/`grant` still take effect globally; the channel just makes the doc persist and lets the owner read the roster back.
612
+
613
+ **Preserve the author on updates.** For an author-owned doc, checking the new author field is not enough — also check `oldDoc` so a writer can't overwrite someone else's doc or reassign its author: `if (oldDoc && oldDoc.<authorField> !== user.userHandle) throw { forbidden: "not author" }`, where `<authorField>` is whatever your doc uses (`authorHandle`, `userHandle`, `senderHandle`, …). (Write-once docs can simply `if (oldDoc) throw`.)
598
614
 
599
615
  ```ts
600
616
  type AccessDescriptor = {
@@ -654,12 +670,14 @@ export function chat(doc, oldDoc, user, ctx) {
654
670
 
655
671
  if (doc.type === "message") {
656
672
  if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
673
+ if (oldDoc && oldDoc.userHandle !== user.userHandle) throw { forbidden: "not author" };
657
674
  ctx.requireAccess(doc.channelId);
658
675
  return { channels: [doc.channelId] };
659
676
  }
660
677
 
661
678
  if (doc.type === "channel-invite") {
662
679
  if (doc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
680
+ if (oldDoc && oldDoc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
663
681
  ctx.requireAccess(doc.channelId);
664
682
  return {
665
683
  channels: [doc.channelId],
@@ -686,11 +704,13 @@ export function survey(doc, oldDoc, user, ctx) {
686
704
 
687
705
  if (doc.type === "survey-config") {
688
706
  if (!user.isOwner) throw { forbidden: "owner only" };
707
+ // Route this grant/config doc to an owner-readable admin channel — a
708
+ // grant-only result (no `channels`) is rejected as an "unreadable write".
689
709
  return {
710
+ channels: ["admin:grants"],
690
711
  grant: {
691
- roles: {
692
- "feedback-team": ["inbound-responses"],
693
- },
712
+ users: { [user.userHandle]: ["admin:grants"] },
713
+ roles: { "feedback-team": ["inbound-responses"] },
694
714
  },
695
715
  };
696
716
  }
@@ -801,16 +821,24 @@ export function chat(doc, oldDoc, user, ctx) {
801
821
  export function chat(doc, oldDoc, user, ctx) {
802
822
  if (!user) throw { forbidden: "authentication required" };
803
823
 
824
+ // Grant/meta docs must also route to a channel — a channel-less result is
825
+ // rejected as "unreadable write". Route them to an owner-readable admin channel.
804
826
  if (doc.type === "team-meta") {
805
827
  if (!user.isOwner) throw { forbidden: "owner only" };
806
828
  return {
829
+ channels: ["admin:grants"],
807
830
  members: { [doc.teamId]: doc.memberHandles },
808
- grant: { roles: { [doc.teamId]: doc.channels } },
831
+ grant: { users: { [user.userHandle]: ["admin:grants"] }, roles: { [doc.teamId]: doc.channels } },
809
832
  };
810
833
  }
811
834
 
812
835
  if (doc.type === "membership") {
813
- return { members: { [doc.role]: [doc.userHandle] } };
836
+ if (!user.isOwner) throw { forbidden: "owner only" };
837
+ return {
838
+ channels: ["admin:grants"],
839
+ members: { [doc.role]: [doc.userHandle] },
840
+ grant: { users: { [user.userHandle]: ["admin:grants"] } },
841
+ };
814
842
  }
815
843
 
816
844
  ctx.requireAccess(doc.channelId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "2.5.14",
3
+ "version": "2.5.15",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -30,9 +30,9 @@
30
30
  "@fireproof/core-types-base": "~0.24.19",
31
31
  "@fireproof/core-types-protocols-cloud": "~0.24.19",
32
32
  "@fireproof/use-fireproof": "~0.24.19",
33
- "@vibes.diy/call-ai-v2": "^2.5.14",
34
- "@vibes.diy/identity": "^2.5.14",
35
- "@vibes.diy/use-vibes-types": "^2.5.14",
33
+ "@vibes.diy/call-ai-v2": "^2.5.15",
34
+ "@vibes.diy/identity": "^2.5.15",
35
+ "@vibes.diy/use-vibes-types": "^2.5.15",
36
36
  "arktype": "~2.2.1",
37
37
  "json-schema-faker": "~0.6.2"
38
38
  },
@@ -68,13 +68,14 @@ Target ~40–60 lines. The shell should look like a real app with empty sections
68
68
  > }
69
69
  > if (doc.type === "message") {
70
70
  > if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
71
+ > if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
71
72
  > return { channels: [doc.channelId] };
72
73
  > }
73
74
  > throw { forbidden: "unknown document type" };
74
75
  > }
75
76
  > ```
76
77
 
77
- `ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open channel anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`.
78
+ `ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open channel anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/member doc must also return `channels` (route it to an owner-readable admin channel) — a channel-less result is rejected. On updates, also check the old author field (`oldDoc.authorHandle`/`userHandle`/`senderHandle`) so a writer can't overwrite or re-author someone else's doc.
78
79
 
79
80
  **Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
80
81
 
package/system-prompt.md CHANGED
@@ -275,6 +275,7 @@ When the app uses channel-based read isolation or per-document write validation,
275
275
  > if (!user) throw { forbidden: "authentication required" };
276
276
  > if (doc.type === "message") {
277
277
  > if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
278
+ > if (oldDoc && oldDoc.userHandle !== user.userHandle) throw { forbidden: "not author" };
278
279
  > ctx.requireAccess(doc.channelId);
279
280
  > return { channels: [doc.channelId] };
280
281
  > }
@@ -282,7 +283,7 @@ When the app uses channel-based read isolation or per-document write validation,
282
283
  > }
283
284
  > ```
284
285
 
285
- `ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. See the fireproof access docs.
286
+ `ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/member/role doc must also return `channels` (route it to an owner-readable admin channel like `["admin:grants"]`) — a channel-less result is rejected as "unreadable write". On updates, also check `oldDoc` (`if (oldDoc && oldDoc.<authorField> !== user.userHandle) throw`, where `<authorField>` is your doc's author field — `authorHandle`/`userHandle`/`senderHandle`) so a writer can't overwrite or re-author someone else's doc. See the fireproof access docs.
286
287
 
287
288
  **Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
288
289
 
@@ -417,6 +418,7 @@ Example streamed output for a team board app:
417
418
  >
418
419
  > if (doc.type === "post") {
419
420
  > if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
421
+ > if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
420
422
  > return { channels: [doc.channelId] };
421
423
  > }
422
424
  >