@vibes.diy/prompts 2.5.13 → 2.5.14

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
@@ -510,9 +510,9 @@ export default function App() {
510
510
 
511
511
  The pattern: `useVibe().can` gates every write surface — including owner-only management, which the access function enforces via `if (!user.isOwner) throw` so `can.create({ type: "roleGrant", … })` is false for non-owners. `access.hasChannel()` reflects display-only membership, and `isOwner` is a display hint only, never the write gate. The access function is the server-side authority — `useVibe().can` is how the UI reflects its decisions for writes.
512
512
 
513
- ### Example: Channel board with restricted channels
513
+ ### Example: Channel board with open channels (any member posts)
514
514
 
515
- Some channels are open to all members, others are restricted. The access function decides — the UI uses `access.hasChannel()` to filter which channels to display, and `useVibe().can` to gate writes.
515
+ Channels everyone can read, and any signed-in user can post to. The channel doc is `grant.public` (read for everyone) and the post rule checks only the author **no `ctx.requireAccess`**, because public is read-only and would block every non-owner. (For a members-only board where the owner appoints who can post, grant a poster role and `requireAccess` it, as in the announcements example above.) The UI uses `access.hasChannel()` to filter which channels to display, and `useVibe().can` to gate writes.
516
516
 
517
517
  access.js
518
518
 
@@ -522,18 +522,18 @@ export function chat(doc, oldDoc, user, ctx) {
522
522
 
523
523
  if (doc.type === "channel") {
524
524
  if (!user.isOwner) throw { forbidden: "owner only" };
525
- return {
526
- channels: [doc._id],
527
- grant: {
528
- users: { [user.userHandle]: [doc._id] },
529
- public: [doc._id],
530
- },
531
- };
525
+ // Open channel: public READ for everyone. No write-membership grant is
526
+ // needed — any signed-in user may post (see the post rule below).
527
+ return { channels: [doc._id], grant: { public: [doc._id] } };
532
528
  }
533
529
 
534
530
  if (doc.type === "post") {
535
531
  if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
536
- ctx.requireAccess(doc.channel);
532
+ // Any signed-in author may post to this open channel. Do NOT call
533
+ // ctx.requireAccess(doc.channel) here: the channel is grant.public
534
+ // (read-only), which never satisfies requireAccess, so gating on it would
535
+ // block every non-owner from posting. requireAccess is for members-only
536
+ // channels whose writers were granted membership (see announcements above).
537
537
  return { channels: [doc.channel] };
538
538
  }
539
539
 
@@ -549,7 +549,9 @@ App.jsx — `access.hasChannel()` filters which channels are visible (display);
549
549
  =======
550
550
  const { database, useLiveQuery, access } = useFireproof("chat");
551
551
  const { docs: channels } = useLiveQuery("type", { key: "channel" });
552
- const canPost = useVibe("chat").can.create({ type: "post" });
552
+ // Build the candidate from the doc you'll write — the access fn checks
553
+ // authorHandle, so a bare { type: "post" } would be denied and hide the form.
554
+ const canPost = useVibe("chat").can.create({ type: "post", channel, authorHandle: viewer?.userHandle });
553
555
  >>>>>>> REPLACE
554
556
  ```
555
557
 
@@ -585,6 +587,11 @@ Access functions live in `/access.js`, a separate file in the vibe's filesystem
585
587
 
586
588
  **Helpers (`ctx`):** Opaque closures over the materialized grant state. They throw or pass — you cannot enumerate channels, list members, or iterate grants. Both helpers also throw when `user` is null: `ctx.requireAccess(channelId)` throws if user is not in the channel, `ctx.requireRole(roleName)` throws if user is not in the role.
587
589
 
590
+ **`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
+
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.
593
+ - **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
+
588
595
  ### AccessDescriptor return type
589
596
 
590
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" }`.
@@ -699,6 +706,25 @@ export function survey(doc, oldDoc, user, ctx) {
699
706
 
700
707
  Key patterns: `allowAnonymous: true` on survey-response lets unauthenticated visitors submit, `grant.public` on final-results makes them readable by any member without a specific channel grant, and the **singleton grant doc** pattern (survey-config) wires role-to-channel access in one place.
701
708
 
709
+ ### Example: Public guestbook / contact form (anonymous writes)
710
+
711
+ When the prompt says **anyone can sign / submit without logging in** (a guestbook, a contact form, an RSVP), do **not** throw on `!user` — return `allowAnonymous: true` so the write is accepted for anonymous visitors. `useVibe().can.create(...)` then returns `ok` for an anonymous viewer, and the form shows instead of a sign-in wall. Stamp `authorHandle` only when there is a user.
712
+
713
+ access.js
714
+
715
+ ```js
716
+ export function guestbook(doc, oldDoc, user, ctx) {
717
+ if (doc.type === "entry") {
718
+ if (oldDoc) throw { forbidden: "entries are write-once" };
719
+ // No `if (!user) throw` — anyone may sign. allowAnonymous opts the write in.
720
+ return { channels: ["public"], grant: { public: ["public"] }, allowAnonymous: true };
721
+ }
722
+ throw { forbidden: "unknown document type" };
723
+ }
724
+ ```
725
+
726
+ In `App.jsx`, gate the form on `useVibe("guestbook").can.create({ type: "entry" }).ok` (true for anon here) and stamp `authorHandle: me?.userHandle` only when signed in. Without `allowAnonymous: true` the runtime rejects the null-user write even though the function didn't throw — so the guestbook would silently require login, the exact miss to avoid.
727
+
702
728
  ### Multiple databases in one file
703
729
 
704
730
  Each named export gates its own database. A single `/access.js` can gate all databases the app uses:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "2.5.13",
3
+ "version": "2.5.14",
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.13",
34
- "@vibes.diy/identity": "^2.5.13",
35
- "@vibes.diy/use-vibes-types": "^2.5.13",
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",
36
36
  "arktype": "~2.2.1",
37
37
  "json-schema-faker": "~0.6.2"
38
38
  },
@@ -51,29 +51,31 @@ Target ~40–60 lines. The shell should look like a real app with empty sections
51
51
 
52
52
  **Step 3 — Feature edits.** Wire each feature with SEARCH/REPLACE edits. Each edit gets exactly one prose line (≤25 words) before it. Wire hooks, data, handlers, and `useFireproof` with `access` in these edits. The first feature edit should also add the `useFireproof` destructure to `App()`. Keep edits focused — one feature per edit, fully working after it lands.
53
53
 
54
- > Access function — owner manages channels, authenticated users post to channels they have access to.
54
+ > Access function — owner manages channels; any signed-in member posts to these open channels.
55
55
  >
56
56
  > access.js
57
57
  >
58
58
  > ```js
59
- > // Each channel doc grants public read access to that channel.
60
- > // Posts require channel accessthe server enforces this via ctx.requireAccess.
61
- > // Only the owner can create channels.
59
+ > // Each channel doc grants public READ; only the owner creates channels.
60
+ > // OPEN board: any signed-in user posts do NOT gate posts on ctx.requireAccess
61
+ > // (grant.public is read-only and never satisfies it, so it would block everyone
62
+ > // but the owner). Members-only channel instead? grant a role + requireAccess it.
62
63
  > export function chat(doc, oldDoc, user, ctx) {
63
64
  > if (!user) throw { forbidden: "sign in" };
64
65
  > if (doc.type === "channel") {
65
66
  > if (!user.isOwner) throw { forbidden: "owner only" };
66
- > return { channels: [doc.name], grant: { public: [doc.name] } };
67
+ > return { channels: [doc._id], grant: { public: [doc._id] } };
67
68
  > }
68
69
  > if (doc.type === "message") {
69
70
  > if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
70
- > ctx.requireAccess(doc.channelId);
71
71
  > return { channels: [doc.channelId] };
72
72
  > }
73
73
  > throw { forbidden: "unknown document type" };
74
74
  > }
75
75
  > ```
76
76
 
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
+
77
79
  **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.
78
80
 
79
81
  After the final edit (and `access.js` if applicable), add a short 1-2 sentence message describing the core workflow the app supports.
package/system-prompt.md CHANGED
@@ -282,6 +282,8 @@ When the app uses channel-based read isolation or per-document write validation,
282
282
  > }
283
283
  > ```
284
284
 
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
+
285
287
  **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.
286
288
 
287
289
  ## Your starter scaffold
@@ -396,29 +398,29 @@ Example streamed output for a team board app:
396
398
  > }
397
399
  > ```
398
400
  >
399
- > Access function — owner manages channels, members post to channels they have access to.
401
+ > Access function — owner manages channels; any signed-in member posts to these open channels.
400
402
  >
401
403
  > access.js
402
404
  >
403
405
  > ```js
404
- > // Each channel doc grants public read access to that channel.
405
- > // Posts require channel access the server enforces this via ctx.requireAccess.
406
- > // Only the owner can create channels or grant roles.
406
+ > // Each channel doc grants public READ to that channel; only the owner creates channels.
407
+ > // OPEN board: any signed-in user posts. Do NOT gate posts on ctx.requireAccess
408
+ > // grant.public is read-only and never satisfies it, so it would block every
409
+ > // non-owner. (Members-only channel instead? grant a role + requireAccess it.)
407
410
  > export function crewBoard(doc, oldDoc, user, ctx) {
408
411
  > if (!user) throw { forbidden: "sign in" };
409
412
  >
410
413
  > if (doc.type === "channel") {
411
414
  > if (!user.isOwner) throw { forbidden: "owner only" };
412
- > return { channels: [doc.name], grant: { public: [doc.name] } };
415
+ > return { channels: [doc._id], grant: { public: [doc._id] } };
413
416
  > }
414
417
  >
415
418
  > if (doc.type === "post") {
416
419
  > if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
417
- > ctx.requireAccess(doc.channelId);
418
420
  > return { channels: [doc.channelId] };
419
421
  > }
420
422
  >
421
- > return {};
423
+ > throw { forbidden: "unknown document type" };
422
424
  > }
423
425
  > ```
424
426
  >