@vibes.diy/prompts 2.5.19 → 2.5.26

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
@@ -614,7 +614,7 @@ Channel `_id` is the channel identifier everywhere. The access function uses `do
614
614
 
615
615
  ### Example: Per-object sharing (collaborate on your own objects, no admin)
616
616
 
617
- A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships.
617
+ Reach for this whenever the prompt says **invite, join, collaborate, share with, together, with my partner/team** — a shared shopping list you invite a partner to, a whiteboard people can join, a trip a group plans together. A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel, so **any member edits any item**; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships.
618
618
 
619
619
  access.js
620
620
 
@@ -654,7 +654,46 @@ export default function (doc, oldDoc, user, ctx) {
654
654
  }
655
655
  ```
656
656
 
657
- To invite someone who isn't a member yet without knowing their handle in advance, invert the flow with a request doc: a `request` type takes **no** `ctx.requireAccess` (any signed-in user may create one — their handle is `user.userHandle`, unforgeable) and routes to the list channel `ch(doc.listId)`, where current members read it (the requester can't read their own request back — they aren't a member yet — which is fine; they just wait to be granted). A member then writes the `share` above to approve. The UI gates each surface with `useVibe("lists").can` and reflects which lists the viewer can see with `access.hasChannel(...)`.
657
+ To invite someone who isn't a member yet without knowing their handle in advance, invert the flow with a request doc: a `request` type takes **no** `ctx.requireAccess` (any signed-in user may create one — their handle is `user.userHandle`, unforgeable) and routes to the list channel `ch(doc.listId)`, where current members read it (the requester can't read their own request back — they aren't a member yet — which is fine; they just wait to be granted). A member then writes the `share` above to approve.
658
+
659
+ App.jsx — `access.hasChannel()` shows only the lists the viewer belongs to; `useVibe("lists").can` gates each write surface (create list, add item, invite peer):
660
+
661
+ ```jsx
662
+ <<<<<<< SEARCH
663
+ const { database, useLiveQuery, access } = useFireproof("notes");
664
+ =======
665
+ const { database, useLiveQuery, access } = useFireproof("lists");
666
+ const { me, can } = useVibe("lists");
667
+ const { docs: lists } = useLiveQuery("type", { key: "list" });
668
+ const visible = lists.filter((l) => access.hasChannel(`list:${l._id}`));
669
+ // Each member edits any item — items gate on requireAccess(list:<id>), not authorHandle.
670
+ const canAddItem = (list) => can.create({ type: "item", listId: list._id, authorHandle: me?.userHandle }).ok;
671
+ const canShare = (list) => can.create({ type: "share", listId: list._id, invitee: "x" }).ok;
672
+ >>>>>>> REPLACE
673
+ ```
674
+
675
+ ```jsx
676
+ <<<<<<< SEARCH
677
+ {viewer && <button onClick={addNote}>+ note</button>}
678
+ =======
679
+ {/* create list — any signed-in visitor can start their own */}
680
+ {can.create({ type: "list", author: me?.userHandle }).ok && (
681
+ <button onClick={() => database.put({ type: "list", author: me.userHandle, name: "new list" })}>
682
+ + new list
683
+ </button>
684
+ )}
685
+
686
+ {visible.map((list) => (
687
+ <section key={list._id}>
688
+ <h3>{list.name}</h3>
689
+ {/* add-item form is shown only when the access fn would accept the write */}
690
+ {canAddItem(list) && <AddItemForm listId={list._id} />}
691
+ {/* invite peer — only shown to members of this list */}
692
+ {canShare(list) && <ShareForm listId={list._id} />}
693
+ </section>
694
+ ))}
695
+ >>>>>>> REPLACE
696
+ ```
658
697
 
659
698
  ---
660
699
 
@@ -764,6 +803,42 @@ export function chat(doc, oldDoc, user, ctx) {
764
803
 
765
804
  This single access function handles three document types: **channel-meta** — owner creates a channel and grants access to listed members, **message** — only the author can post, must already have channel access, **channel-invite** — any channel member can invite others; deleting the invite revokes the grant.
766
805
 
806
+ App.jsx — show only channels the viewer is in (`access.hasChannel`), gate the compose box and invite form on `useVibe().can`:
807
+
808
+ ```jsx
809
+ <<<<<<< SEARCH
810
+ const { database, useLiveQuery, access } = useFireproof("notes");
811
+ =======
812
+ const { database, useLiveQuery, access } = useFireproof("chat");
813
+ const { me, can } = useVibe("chat");
814
+ const { docs: channels } = useLiveQuery("type", { key: "channel-meta" });
815
+ // Filter to channels the viewer has been granted into — non-members never see them in the list.
816
+ const myChannels = channels.filter((ch) => access.hasChannel(ch._id));
817
+ const [channelId, setChannelId] = React.useState(null);
818
+ const canPost = can.create({ type: "message", channelId, userHandle: me?.userHandle });
819
+ const canInvite = can.create({ type: "channel-invite", channelId, senderHandle: me?.userHandle, inviteeHandle: "x" });
820
+ >>>>>>> REPLACE
821
+ ```
822
+
823
+ ```jsx
824
+ <<<<<<< SEARCH
825
+ {viewer && <Compose onSend={send} />}
826
+ =======
827
+ {/* sidebar lists only the channels the viewer is in */}
828
+ <nav>{myChannels.map((ch) => <button key={ch._id} onClick={() => setChannelId(ch._id)}>{ch.name}</button>)}</nav>
829
+
830
+ {/* compose is shown only when the viewer is a member of the selected channel */}
831
+ {channelId && canPost.ok ? (
832
+ <Compose onSend={(text) => database.put({ type: "message", channelId, userHandle: me.userHandle, text })} />
833
+ ) : (
834
+ <p>{canPost.reason || "Pick a channel"}</p>
835
+ )}
836
+
837
+ {/* any member of this channel may invite a peer */}
838
+ {channelId && canInvite.ok && <InviteForm channelId={channelId} senderHandle={me.userHandle} />}
839
+ >>>>>>> REPLACE
840
+ ```
841
+
767
842
  ### Example: Anonymous survey with role-gated results
768
843
 
769
844
  access.js
@@ -799,6 +874,45 @@ export function survey(doc, oldDoc, user, ctx) {
799
874
 
800
875
  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.
801
876
 
877
+ App.jsx — anonymous-friendly submit form, owner-only config panel, role-gated results view:
878
+
879
+ ```jsx
880
+ <<<<<<< SEARCH
881
+ const { database } = useFireproof("notes");
882
+ =======
883
+ const { database, useLiveQuery, access } = useFireproof("survey");
884
+ const { me, can } = useVibe("survey");
885
+ // Submit form is shown for anyone — allowAnonymous makes this can.create return ok for null user.
886
+ const canSubmit = can.create({ type: "survey-response", question: "q1", answer: "" });
887
+ // Owner-only — survey-config rule calls ctx.requireRole("owner").
888
+ const canConfigure = can.create({ type: "survey-config" });
889
+ const { docs: results } = useLiveQuery("type", { key: "final-results" });
890
+ >>>>>>> REPLACE
891
+ ```
892
+
893
+ ```jsx
894
+ <<<<<<< SEARCH
895
+ <input value={answer} onChange={(e) => setAnswer(e.target.value)} />
896
+ =======
897
+ {/* anyone (signed in OR anonymous) can submit — only stamp authorHandle when present */}
898
+ {canSubmit.ok ? (
899
+ <form onSubmit={(e) => {
900
+ e.preventDefault();
901
+ database.put({ type: "survey-response", question: "q1", answer, ...(me && { authorHandle: me.userHandle }) });
902
+ }}>
903
+ <input value={answer} onChange={(e) => setAnswer(e.target.value)} />
904
+ <button type="submit">Submit</button>
905
+ </form>
906
+ ) : <p>{canSubmit.reason}</p>}
907
+
908
+ {/* owner-only admin to wire up the feedback-team role */}
909
+ {canConfigure.ok && <ConfigPanel />}
910
+
911
+ {/* results render only for users with the feedback-team role — access.hasChannel filters them in */}
912
+ {results.filter((r) => access.hasChannel(r._id)).map((r) => <ResultCard key={r._id} doc={r} />)}
913
+ >>>>>>> REPLACE
914
+ ```
915
+
802
916
  ### Example: Public guestbook / contact form (anonymous writes)
803
917
 
804
918
  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.
@@ -818,6 +932,43 @@ export function guestbook(doc, oldDoc, user, ctx) {
818
932
 
819
933
  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.
820
934
 
935
+ App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true even when nobody is signed in:
936
+
937
+ ```jsx
938
+ <<<<<<< SEARCH
939
+ const { database } = useFireproof("notes");
940
+ =======
941
+ const { database, useLiveQuery } = useFireproof("guestbook");
942
+ const { me, can } = useVibe("guestbook");
943
+ const { docs: entries } = useLiveQuery("type", { key: "entry" });
944
+ // allowAnonymous: true on the access fn → this returns ok for an anon viewer.
945
+ // The form is visible from first load, no sign-in wall.
946
+ const canSign = can.create({ type: "entry", message: "" });
947
+ >>>>>>> REPLACE
948
+ ```
949
+
950
+ ```jsx
951
+ <<<<<<< SEARCH
952
+ <SignInGate>
953
+ <input value={message} onChange={(e) => setMessage(e.target.value)} />
954
+ </SignInGate>
955
+ =======
956
+ {canSign.ok ? (
957
+ <form onSubmit={(e) => {
958
+ e.preventDefault();
959
+ // Stamp authorHandle only when signed in — anon entries simply omit it.
960
+ database.put({ type: "entry", message, createdAt: Date.now(), ...(me && { authorHandle: me.userHandle }) });
961
+ setMessage("");
962
+ }}>
963
+ <input value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Leave a note" />
964
+ <button type="submit">Sign</button>
965
+ </form>
966
+ ) : <p>{canSign.reason}</p>}
967
+
968
+ {entries.map((e) => <li key={e._id}>{e.message} — {e.authorHandle || "anonymous"}</li>)}
969
+ >>>>>>> REPLACE
970
+ ```
971
+
821
972
  ### Multiple databases in one file
822
973
 
823
974
  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.19",
3
+ "version": "2.5.26",
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.19",
34
- "@vibes.diy/identity": "^2.5.19",
35
- "@vibes.diy/use-vibes-types": "^2.5.19",
33
+ "@vibes.diy/call-ai-v2": "^2.5.26",
34
+ "@vibes.diy/identity": "^2.5.26",
35
+ "@vibes.diy/use-vibes-types": "^2.5.26",
36
36
  "arktype": "~2.2.1",
37
37
  "json-schema-faker": "~0.6.2"
38
38
  },
@@ -77,6 +77,18 @@ Target ~40–60 lines. The shell should look like a real app with empty sections
77
77
 
78
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.
79
79
 
80
+ **Build the permission model around what a newcomer should be able to do.** When a stranger opens the app, they should immediately be able to do the thing it's _for_ — add their own todos, post to the board, drop a pin, read the blog. So the default is: every signed-in visitor is a first-class participant who creates their own objects and edits what they created (`doc.authorHandle === user.userHandle`, checking `oldDoc` on updates), from first load, with no one needing to let them in.
81
+
82
+ **Most apps are multiplayer even when they sound solo — match the shape to the model.** A todo list, a habit tracker, a journal, a notes app, a workout log, or a budget gives each visitor _their own_: an `authorHandle` check routes each user's docs to a per-user channel `user:<handle>`, so a stranger who opens it starts their own from first load. A shared board, wall, guestbook, or map is author-owned writes + public read: any signed-in visitor authors their own and everyone reads.
83
+
84
+ **"Invite", "join", "collaborate", "share with", "together", "with my partner/team" → per-object collaboration** — each shared thing is its own space its members reach directly. Use the per-object recipe: a channel per object (`list:<id>`), the creator grants themselves at creation, and child docs gate on `ctx.requireAccess("list:<id>")` so any member edits any child doc in it. A member-authored `share` doc grants a peer the same channel, and a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ author/creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable.
85
+
86
+ When the app is genuinely one person's to publish — a personal blog, an announcements feed — the newcomer's job is to _read_: gate writes with `ctx.requireRole("owner")` (the vibe owner is always auto-seeded into the reserved `owner` role, so this needs no setup) and make reads public. For "author edits their own, owner moderates anyone's" in one branch, let `requireRole` be the fallback: `if (doc.authorHandle !== user.userHandle) ctx.requireRole("owner")` — it only throws when the author check fails, so authors pass for their own docs and the owner passes for any. Gate the UI on `useVibe(dbName).can`, never on `user.isOwner`.
87
+
88
+ **Sharing objects: channels carry objects, roles carry types.** A channel is one shareable thing (`list:<id>`) — membership in it means "can reach this object." A role is a _kind_ of participant (`author`, `editor`), a small reusable vocabulary. To let people collaborate on _their own_ objects with no admin in the loop: the creator routes the object to its channel and grants themselves access; any member shares it by granting a peer into the same channel; child docs gate on `ctx.requireAccess("list:<id>")`. The reserved `owner` role is the only one auto-seeded — every _other_ role is populated by the app's own grant docs (a member writes a doc whose access-fn output adds another user to a role or channel). So to put someone in a role, write a grant; don't expect a role to be pre-filled.
89
+
90
+ **Public vs private is the owner's ACL envelope, not your code.** Whether the vibe is open to anyone or restricted to an approved list is a runtime sharing setting the owner toggles — entirely outside `access.js`. Keep `access.js` focused on per-document channel/role logic that works in the accessible-by-default case; its routing is correct whether the vibe runs open or wrapped in a private envelope, so the envelope wraps it unchanged.
91
+
80
92
  **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.
81
93
 
82
94
  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
@@ -349,6 +349,10 @@ Keep `useViewer` (for `ViewerTag`) on `App`'s first line, and add `useVibe(dbNam
349
349
 
350
350
  **Build the permission model around what a newcomer should be able to do.** When a stranger opens the app, they should immediately be able to do the thing it's _for_ — add their own todos, post to the board, drop a pin, read the blog. So the default is: every signed-in visitor is a first-class participant who creates their own objects and edits what they created (`doc.authorHandle === user.userHandle`, checking `oldDoc` on updates), from first load, with no one needing to let them in.
351
351
 
352
+ **Most apps are multiplayer even when they sound solo — match the shape to the model.** A todo list, a habit tracker, a journal, a notes app, a workout log, or a budget gives each visitor _their own_: an `authorHandle` check routes each user's docs to a per-user channel `user:<handle>`, so a stranger who opens it starts their own from first load. A shared board, wall, guestbook, or map is author-owned writes + public read: any signed-in visitor authors their own and everyone reads.
353
+
354
+ **"Invite", "join", "collaborate", "share with", "together", "with my partner/team" → per-object collaboration** — each shared thing is its own space its members reach directly. Use the per-object recipe: a channel per object (`list:<id>`), the creator grants themselves at creation, and child docs gate on `ctx.requireAccess("list:<id>")` so any member edits any child doc in it. A member-authored `share` doc grants a peer the same channel, and a `request` doc — which takes **no** `requireAccess` — lets a not-yet-member ask to join. Keep the _object's own_ author/creator field write-once (`if (oldDoc && doc.author !== oldDoc.author) throw`) and a child's object-id immutable.
355
+
352
356
  When the app is genuinely one person's to publish — a personal blog, an announcements feed — the newcomer's job is to _read_: gate writes with `ctx.requireRole("owner")` (the vibe owner is always auto-seeded into the reserved `owner` role, so this needs no setup) and make reads public. For "author edits their own, owner moderates anyone's" in one branch, let `requireRole` be the fallback: `if (doc.authorHandle !== user.userHandle) ctx.requireRole("owner")` — it only throws when the author check fails, so authors pass for their own docs and the owner passes for any. Gate the UI on `useVibe(dbName).can`, never on `user.isOwner`.
353
357
 
354
358
  **Sharing objects: channels carry objects, roles carry types.** A channel is one shareable thing (`list:<id>`) — membership in it means "can reach this object." A role is a _kind_ of participant (`author`, `editor`), a small reusable vocabulary. To let people collaborate on _their own_ objects with no admin in the loop: the creator routes the object to its channel and grants themselves access; any member shares it by granting a peer into the same channel; child docs gate on `ctx.requireAccess("list:<id>")`. The reserved `owner` role is the only one auto-seeded — every _other_ role is populated by the app's own grant docs (a member writes a doc whose access-fn output adds another user to a role or channel). So to put someone in a role, write a grant; don't expect a role to be pre-filled.