@vibes.diy/prompts 2.5.18 → 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 +153 -2
- package/package.json +4 -4
- package/system-prompt-initial.md +12 -0
- package/system-prompt.md +4 -0
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
|
|
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.
|
|
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.
|
|
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.
|
|
34
|
-
"@vibes.diy/identity": "^2.5.
|
|
35
|
-
"@vibes.diy/use-vibes-types": "^2.5.
|
|
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
|
},
|
package/system-prompt-initial.md
CHANGED
|
@@ -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.
|