@vibes.diy/prompts 8.1.1 → 8.2.0

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
@@ -404,375 +404,9 @@ const { useLiveQuery, database } = useFireproof("favorites", {
404
404
 
405
405
  Notes: use it only where anonymous-first state makes sense (favorites, drafts, a scratch list). A returning signed-out visitor is automatically steered to sign in rather than starting a fresh throwaway local session. Migration keeps local data intact if it fails, so nothing is lost.
406
406
 
407
- ## Reading Resolved Grants (`access`)
407
+ ## Reading Resolved Grants and worked access examples
408
408
 
409
- `useFireproof()` returns an `access` property the viewer's resolved roles and channels for that database, computed server-side from the access function's `members` and `grant` declarations. Use `access.roles` (ReadonlySet), `access.channels` (ReadonlySet), `access.hasRole(name)`, and `access.hasChannel(name)`. Use these to reflect roles/channels in the UI; gate writes with `useVibe(dbName).can`.
410
-
411
- For databases without an access function export, `access` has empty roles and channels. No separate pending flag — grants arrive alongside the viewer identity, so `useViewer().isViewerPending` covers both.
412
-
413
- App.jsx
414
-
415
- ```jsx
416
- <<<<<<< SEARCH
417
- import { useFireproof } from "use-fireproof";
418
- =======
419
- import { useFireproof } from "use-fireproof";
420
- import { useVibe } from "use-vibes";
421
- >>>>>>> REPLACE
422
- ```
423
-
424
- App.jsx
425
-
426
- ```jsx
427
- <<<<<<< SEARCH
428
- const { useLiveQuery, database } = useFireproof("announcements", {
429
- acl: { write: ["members"], delete: ["editors"] },
430
- });
431
- =======
432
- const { database, useLiveQuery, access } = useFireproof("comments");
433
- const { can, me } = useVibe("comments");
434
- >>>>>>> REPLACE
435
- ```
436
-
437
- App.jsx
438
-
439
- ```jsx
440
- <<<<<<< SEARCH
441
- <h3>Recent Documents</h3>
442
- <ul>
443
- {docs.map((doc) => (
444
- <li key={doc._id}>
445
- {doc.text}
446
- <button onClick={() => database.put({ ...doc, favorite: !doc.favorite })}>
447
- {doc.favorite ? "★" : "☆"}
448
- </button>
449
- </li>
450
- ))}
451
- </ul>
452
- =======
453
- {/* gate writes with useVibe().can, not access.* — and gate the SAME db you write to */}
454
- {can.create({ type: "comment", authorHandle: me?.userHandle }).ok && <CommentForm database={database} />}
455
- {access.hasRole("moderator") && <ModToolsBadge />}
456
- {access.hasChannel("announcements") && <Announcements />}
457
- >>>>>>> REPLACE
458
- ```
459
-
460
- The AI agent writes the access function (so it knows the role names) and writes the UI (so it knows which roles gate which components). The `access` object is the bridge — it lets the UI reflect server-enforced permissions without duplicating the logic.
461
-
462
- The access function is the single source of truth for permissions. Gate write surfaces with `useVibe(dbName).can`, which runs this same access function. The `access` object (`access.hasRole(name)`, `access.hasChannel(name)`) reflects the viewer's resolved roles/channels for DISPLAY — role badges, showing/hiding read-only sections — not as the write gate.
463
-
464
- `access.hasChannel()` covers every grant path — public channels, restricted channels, role-expanded channels. The access function decides who gets access and how; the UI uses `access.hasChannel(name)` to reflect membership for display.
465
-
466
- ### Complete example: Team announcements with channels
467
-
468
- This example shows the full round-trip — access.js declares channels and grants; App.jsx reads them back via `access`. Key details:
469
-
470
- - **Owner bootstrap:** the vibe owner is auto-seeded into the reserved `owner` role, so gate management operations (channel setup, role grants, moderation) on `ctx.requireRole("owner")` — **never on a display flag** (the UI gates on `can.*`). No bootstrap problem — the seed means the owner can manage without a prior grant. Default content, though, should be author-owned (anyone signed-in creates and edits their own); reserve owner-gating for shared admin surfaces.
471
- - **Channel identity:** Channel docs use `_id: "ch:" + name` so names are unique. The `_id` is the channel identifier everywhere — in `channels`, `grant`, and `ctx.requireAccess()`.
472
- - **Channel grant:** A channel document grants the creator (`grant.users`), adds `grant.public` so all members can read, and `grant.roles` so posters can write.
473
- - **Write surfaces** are gated with `useVibe(dbName).can.create/edit/delete` — it runs this same access function, so the UI verdict matches the server. Render `.reason` when denied. (See use-vibe docs.)
474
- - **`ViewerTag`** takes `userHandle` to render another user (authors, rosters). The current viewer's own pill and sign-in button are system chrome in the Vibes Switch (the logo) — don't add one to the app's UI, except a guarded no-prop `{viewer && <ViewerTag />}` when you want inline avatar self-edit for any signed-in member (see use-viewer docs).
475
-
476
- access.js
477
-
478
- ```js
479
- export function announcements(doc, oldDoc, user, ctx) {
480
- if (!user) throw { forbidden: "sign in" };
481
-
482
- if (doc.type === "channel") {
483
- ctx.requireRole("owner");
484
- return {
485
- channels: [doc._id],
486
- grant: {
487
- users: { [user.userHandle]: [doc._id] },
488
- public: [doc._id],
489
- roles: { poster: [doc._id] },
490
- },
491
- };
492
- }
493
-
494
- if (doc.type === "roleGrant") {
495
- ctx.requireRole("owner");
496
- // A grant doc must ALSO route to a channel — a result with no `channels`
497
- // (only `members`/`grant`) is rejected as an "unreadable write". Route it to
498
- // an owner-readable admin channel (not a public one) so the grant persists
499
- // and the owner can read the roster back; the membership then applies.
500
- return {
501
- channels: ["admin:grants"],
502
- members: { [doc.role]: [doc.userHandle] },
503
- grant: { users: { [user.userHandle]: ["admin:grants"] } },
504
- };
505
- }
506
-
507
- if (doc.type === "post") {
508
- // Author fixed at create; ownership immutable. On update a non-author may
509
- // only append one legitimate ImgGen version — the platform predicate
510
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
511
- if (oldDoc === null) {
512
- if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
513
- } else if (doc.authorHandle !== oldDoc.authorHandle) {
514
- throw { forbidden: "cannot change author" };
515
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
516
- throw { forbidden: "not author" };
517
- }
518
- ctx.requireAccess(doc.channel);
519
- return { channels: [doc.channel] };
520
- }
521
-
522
- throw { forbidden: "unknown document type" };
523
- }
524
- ```
525
-
526
- App.jsx — `useVibe().can` gates every write surface (posts AND owner-only management); `access.hasChannel()` reflects display-only membership:
527
-
528
- ```jsx
529
- import React from "react";
530
- import { useFireproof } from "use-fireproof";
531
- import { useViewer, useVibe } from "use-vibes";
532
-
533
- export default function App() {
534
- const { viewer, isViewerPending, ViewerTag } = useViewer();
535
- const { database, useLiveQuery, access } = useFireproof("announcements");
536
- const { can } = useVibe("announcements");
537
-
538
- const { docs: posts } = useLiveQuery("type", { key: "post" });
539
- const [draft, setDraft] = React.useState("");
540
- const [channel, setChannel] = React.useState("general");
541
- // Build each candidate from the doc you'll actually write — the access function
542
- // checks authorHandle/channel (and owner-only for roleGrant), so a bare partial
543
- // would be denied and hide the control even from users who can act.
544
- const canPost = can.create({ type: "post", channel, authorHandle: viewer?.userHandle });
545
- // Owner-only management gates on can.* too — the access fn calls
546
- // ctx.requireRole("owner"), so this verdict is false for everyone but the owner.
547
- const canGrant = can.create({ type: "roleGrant", role: "poster", userHandle: "newUser" });
548
-
549
- if (isViewerPending) return null;
550
-
551
- async function submitPost() {
552
- if (!draft.trim() || !viewer) return;
553
- await database.put({
554
- type: "post",
555
- channel,
556
- body: draft.trim(),
557
- authorHandle: viewer.userHandle,
558
- createdAt: Date.now(),
559
- });
560
- setDraft("");
561
- }
562
-
563
- return (
564
- <div>
565
- {/* No current-user pill / sign-in button here — that's system chrome in the
566
- Vibes Switch (the panel the logo opens). ViewerTag below renders post authors. */}
567
-
568
- {/* gate the write surface on useVibe().can — it runs the access function */}
569
- {canPost.ok ? (
570
- <form
571
- onSubmit={(e) => {
572
- e.preventDefault();
573
- submitPost();
574
- }}
575
- >
576
- <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />
577
- <button type="submit">Post</button>
578
- </form>
579
- ) : (
580
- viewer && <p style={{ color: "var(--muted, #888)" }}>{canPost.reason}</p>
581
- )}
582
-
583
- {/* owner-only management — gated on can.* */}
584
- {canGrant.ok && (
585
- <button onClick={() => database.put({ type: "roleGrant", role: "poster", userHandle: "newUser" })}>
586
- Grant poster role
587
- </button>
588
- )}
589
-
590
- {posts.map((p) => (
591
- <div key={p._id}>
592
- <ViewerTag userHandle={p.authorHandle} />
593
- <p>{p.body}</p>
594
- {can.delete(p).ok && <button onClick={() => database.del(p._id)}>Delete</button>}
595
- </div>
596
- ))}
597
- </div>
598
- );
599
- }
600
- ```
601
-
602
- The pattern: `useVibe().can` gates every write surface — including owner-only management, which the access function enforces via `ctx.requireRole("owner")` so `can.create({ type: "roleGrant", … })` is false for non-owners. `access.hasChannel()` reflects display-only membership; the write gate is always `can.*`. The access function is the server-side authority — `useVibe().can` is how the UI reflects its decisions for writes.
603
-
604
- **Owner-management panels (appoint/revoke moderators, grant/revoke roles) gate on `can.*`.** Gate each mutating control — and the panel's visibility — on the `can.*` verdict from the doc you'll actually write: appoint on `can.create({ type: "modGrant", role: "moderator", userHandle }).ok`, revoke on `can.delete(grantDoc).ok` — and render `.reason` when denied. `can.*` runs the app's own `access.js` to produce the verdict, so the control's enabled state and message track what the access function decides and stay correct as the rule grows beyond owner-only (a delegated admin role, say).
605
-
606
- ```jsx
607
- // Gate visibility AND each write on the can.* verdict, and supply the denial reason
608
- const canAppoint = can.create({ type: "modGrant", role: "moderator", userHandle });
609
- const canRevoke = can.delete(grantDoc);
610
- {(canAppoint.ok || canRevoke.ok) && (
611
- <ModeratorPanel>
612
- {canAppoint.ok && (
613
- <button onClick={() => database.put({ type: "modGrant", role: "moderator", userHandle })}>Appoint</button>
614
- )}
615
- {canRevoke.ok ? (
616
- <button onClick={() => database.del(grantDoc._id)}>Revoke</button>
617
- ) : (
618
- <p>{canRevoke.reason}</p>
619
- )}
620
- </ModeratorPanel>
621
- )}
622
- ```
623
-
624
- ### Example: Channel board with open channels (any member posts)
625
-
626
- 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.
627
-
628
- access.js
629
-
630
- ```js
631
- export function chat(doc, oldDoc, user, ctx) {
632
- if (!user) throw { forbidden: "sign in" };
633
-
634
- if (doc.type === "channel") {
635
- ctx.requireRole("owner");
636
- // Open channel: public READ for everyone. No write-membership grant is
637
- // needed — any signed-in user may post (see the post rule below).
638
- return { channels: [doc._id], grant: { public: [doc._id] } };
639
- }
640
-
641
- if (doc.type === "post") {
642
- // Author fixed at create; ownership immutable. On update a non-author may
643
- // only append one legitimate ImgGen version — the platform predicate
644
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
645
- if (oldDoc === null) {
646
- if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
647
- } else if (doc.authorHandle !== oldDoc.authorHandle) {
648
- throw { forbidden: "cannot change author" };
649
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.authorHandle !== user.userHandle) {
650
- throw { forbidden: "not author" };
651
- }
652
- // Any signed-in author may post to this open channel. Do NOT call
653
- // ctx.requireAccess(doc.channel) here: the channel is grant.public
654
- // (read-only), which never satisfies requireAccess, so gating on it would
655
- // block every non-owner from posting. requireAccess is for members-only
656
- // channels whose writers were granted membership (see announcements above).
657
- return { channels: [doc.channel] };
658
- }
659
-
660
- throw { forbidden: "unknown document type" };
661
- }
662
- ```
663
-
664
- App.jsx — `access.hasChannel()` filters which channels are visible (display); `useVibe().can` gates the write surface:
665
-
666
- ```jsx
667
- <<<<<<< SEARCH
668
- const { database, useLiveQuery, access } = useFireproof("announcements");
669
- =======
670
- const { database, useLiveQuery, access } = useFireproof("chat");
671
- const { docs: channels } = useLiveQuery("type", { key: "channel" });
672
- // Build the candidate from the doc you'll write — the access fn checks
673
- // authorHandle, so a bare { type: "post" } would be denied and hide the form.
674
- const canPost = useVibe("chat").can.create({ type: "post", channel, authorHandle: viewer?.userHandle });
675
- >>>>>>> REPLACE
676
- ```
677
-
678
- ```jsx
679
- <<<<<<< SEARCH
680
- {viewer && access.hasChannel(channel) && (
681
- =======
682
- {/* filter to channels the viewer can see — use _id as channel identifier (display only) */}
683
- {channels.filter((ch) => access.hasChannel(ch._id)).map((ch) => (
684
- <button key={ch._id} onClick={() => setChannel(ch._id)}>{ch.name}</button>
685
- ))}
686
-
687
- {/* gate the write surface on useVibe().can */}
688
- {canPost.ok ? (
689
- >>>>>>> REPLACE
690
- ```
691
-
692
- Channel `_id` is the channel identifier everywhere. The access function uses `doc._id` for routing and grants. A deterministic `_id` like `"ch:" + name` enforces uniqueness — two users can't create duplicate channels.
693
-
694
- ### Example: Per-object sharing (collaborate on your own objects, no admin)
695
-
696
- 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.
697
-
698
- access.js
699
-
700
- ```js
701
- export default function (doc, oldDoc, user, ctx) {
702
- if (!user) throw { forbidden: "sign in" };
703
- const ch = (id) => `list:${id}`;
704
-
705
- if (doc.type === "list") {
706
- // Creator owns the list doc; route it to its own channel and grant self.
707
- const author = oldDoc ? oldDoc.author : doc.author;
708
- if (author !== user.userHandle) throw { forbidden: "not your list" };
709
- // author is write-once: an update must not re-author the list (which would
710
- // change who can edit it and hand control to someone never granted).
711
- if (oldDoc && doc.author !== oldDoc.author) throw { forbidden: "cannot change author" };
712
- return { channels: [ch(doc._id)], grant: { users: { [user.userHandle]: [ch(doc._id)] } } };
713
- }
714
-
715
- if (doc.type === "item") {
716
- // Any member of the list may add/edit items in it. listId is immutable —
717
- // without this, a member of list X could re-point an existing item from a
718
- // list they don't belong to into X (it would still pass requireAccess(X)).
719
- if (oldDoc && oldDoc.listId !== doc.listId) throw { forbidden: "cannot move item" };
720
- ctx.requireAccess(ch(doc.listId));
721
- return { channels: [ch(doc.listId)] };
722
- }
723
-
724
- if (doc.type === "share") {
725
- // Any current member invites a peer by handle — grants them the list channel.
726
- // Route the share doc to the list channel itself (every member already holds
727
- // it), so members see who was added without a second channel to grant.
728
- ctx.requireAccess(ch(doc.listId));
729
- return { channels: [ch(doc.listId)], grant: { users: { [doc.invitee]: [ch(doc.listId)] } } };
730
- }
731
-
732
- throw { forbidden: "unknown document type" };
733
- }
734
- ```
735
-
736
- 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.
737
-
738
- 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):
739
-
740
- ```jsx
741
- <<<<<<< SEARCH
742
- const { database, useLiveQuery, access } = useFireproof("notes");
743
- =======
744
- const { database, useLiveQuery, access } = useFireproof("lists");
745
- const { me, can } = useVibe("lists");
746
- const { docs: lists } = useLiveQuery("type", { key: "list" });
747
- const visible = lists.filter((l) => access.hasChannel(`list:${l._id}`));
748
- // Each member edits any item — items gate on requireAccess(list:<id>), not authorHandle.
749
- const canAddItem = (list) => can.create({ type: "item", listId: list._id, authorHandle: me?.userHandle }).ok;
750
- const canShare = (list) => can.create({ type: "share", listId: list._id, invitee: "x" }).ok;
751
- >>>>>>> REPLACE
752
- ```
753
-
754
- ```jsx
755
- <<<<<<< SEARCH
756
- {viewer && <button onClick={addNote}>+ note</button>}
757
- =======
758
- {/* create list — any signed-in visitor can start their own */}
759
- {can.create({ type: "list", author: me?.userHandle }).ok && (
760
- <button onClick={() => database.put({ type: "list", author: me.userHandle, name: "new list" })}>
761
- + new list
762
- </button>
763
- )}
764
-
765
- {visible.map((list) => (
766
- <section key={list._id}>
767
- <h3>{list.name}</h3>
768
- {/* add-item form is shown only when the access fn would accept the write */}
769
- {canAddItem(list) && <AddItemForm listId={list._id} />}
770
- {/* invite peer — only shown to members of this list */}
771
- {canShare(list) && <ShareForm listId={list._id} />}
772
- </section>
773
- ))}
774
- >>>>>>> REPLACE
775
- ```
409
+ `useFireproof()` returns an `access` property (resolved roles/channels for display) and the full worked round-trip examples team announcements, open channel boards, per-object sharing, workspace chat, anonymous surveys, public guestbooks live in the **access skill** (`access.js`), which is included whenever the app is permission-shaped (privacy, sharing, teams, members, roles, approval). Gate every write surface with `useVibe(dbName).can`; reflect membership for display with `access.hasRole(name)` / `access.hasChannel(name)`.
776
410
 
777
411
  ---
778
412
 
@@ -852,222 +486,6 @@ A channel is a _reusable_ unit of read access: grant a user into a channel once
852
486
  - **A document goes to a one-off set with no reusable group:** route to several channels at once — `return { channels: [\`user:${aHandle}\`, \`user:${bHandle}\`] }`. Mint a per-document channel (`channels: [doc._id]`) only when each document genuinely has its own disjoint audience.
853
487
  - **Refusing a write:** `throw { forbidden: "reason" }`. Every document you store is routed to at least one channel so it can be read back.
854
488
 
855
- ### Example: Workspace chat with channels
856
-
857
- access.js
858
-
859
- ```js
860
- export function chat(doc, oldDoc, user, ctx) {
861
- if (!user) throw { forbidden: "authentication required" };
862
-
863
- if (doc.type === "channel-meta") {
864
- if (doc.ownerHandle !== user.userHandle) throw { forbidden: "not owner" };
865
- if (oldDoc && oldDoc.ownerHandle !== user.userHandle) throw { forbidden: "not owner" };
866
- return {
867
- channels: [doc._id],
868
- grant: {
869
- users: Object.fromEntries([[doc.ownerHandle, [doc._id]], ...doc.memberHandles.map((h) => [h, [doc._id]])]),
870
- },
871
- };
872
- }
873
-
874
- if (doc.type === "message") {
875
- // Author fixed at create; ownership immutable. On update a non-author may
876
- // only append one legitimate ImgGen version — the platform predicate
877
- // ctx.isImgGenVersionAppend decides that (oldDoc is null on create).
878
- if (oldDoc === null) {
879
- if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
880
- } else if (doc.userHandle !== oldDoc.userHandle) {
881
- throw { forbidden: "cannot change author" };
882
- } else if (ctx.isImgGenVersionAppend(doc, oldDoc) === false && oldDoc.userHandle !== user.userHandle) {
883
- throw { forbidden: "not author" };
884
- }
885
- ctx.requireAccess(doc.channelId);
886
- return { channels: [doc.channelId] };
887
- }
888
-
889
- if (doc.type === "channel-invite") {
890
- if (doc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
891
- if (oldDoc && oldDoc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
892
- ctx.requireAccess(doc.channelId);
893
- return {
894
- channels: [doc.channelId],
895
- grant: { users: { [doc.inviteeHandle]: [doc.channelId] } },
896
- };
897
- }
898
-
899
- throw { forbidden: "unknown document type" };
900
- }
901
- ```
902
-
903
- 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.
904
-
905
- App.jsx — show only channels the viewer is in (`access.hasChannel`), gate the compose box and invite form on `useVibe().can`:
906
-
907
- ```jsx
908
- <<<<<<< SEARCH
909
- const { database, useLiveQuery, access } = useFireproof("notes");
910
- =======
911
- const { database, useLiveQuery, access } = useFireproof("chat");
912
- const { me, can } = useVibe("chat");
913
- const { docs: channels } = useLiveQuery("type", { key: "channel-meta" });
914
- // Filter to channels the viewer has been granted into — non-members never see them in the list.
915
- const myChannels = channels.filter((ch) => access.hasChannel(ch._id));
916
- const [channelId, setChannelId] = React.useState(null);
917
- const canPost = can.create({ type: "message", channelId, userHandle: me?.userHandle });
918
- const canInvite = can.create({ type: "channel-invite", channelId, senderHandle: me?.userHandle, inviteeHandle: "x" });
919
- >>>>>>> REPLACE
920
- ```
921
-
922
- ```jsx
923
- <<<<<<< SEARCH
924
- {viewer && <Compose onSend={send} />}
925
- =======
926
- {/* sidebar lists only the channels the viewer is in */}
927
- <nav>{myChannels.map((ch) => <button key={ch._id} onClick={() => setChannelId(ch._id)}>{ch.name}</button>)}</nav>
928
-
929
- {/* compose is shown only when the viewer is a member of the selected channel */}
930
- {channelId && canPost.ok ? (
931
- <Compose onSend={(text) => database.put({ type: "message", channelId, userHandle: me.userHandle, text })} />
932
- ) : (
933
- <p>{canPost.reason || "Pick a channel"}</p>
934
- )}
935
-
936
- {/* any member of this channel may invite a peer */}
937
- {channelId && canInvite.ok && <InviteForm channelId={channelId} senderHandle={me.userHandle} />}
938
- >>>>>>> REPLACE
939
- ```
940
-
941
- ### Example: Anonymous survey with role-gated results
942
-
943
- access.js
944
-
945
- ```js
946
- export function survey(doc, oldDoc, user, ctx) {
947
- if (doc.type === "survey-response") {
948
- if (oldDoc) throw { forbidden: "responses are write-once" };
949
- return { channels: ["inbound-responses"], allowAnonymous: true };
950
- }
951
-
952
- if (doc.type === "survey-config") {
953
- ctx.requireRole("owner");
954
- // Route this grant/config doc to an owner-readable admin channel — a
955
- // grant-only result (no `channels`) is rejected as an "unreadable write".
956
- return {
957
- channels: ["admin:grants"],
958
- grant: {
959
- users: { [user.userHandle]: ["admin:grants"] },
960
- roles: { "feedback-team": ["inbound-responses"] },
961
- },
962
- };
963
- }
964
-
965
- if (doc.type === "final-results") {
966
- ctx.requireRole("feedback-team");
967
- return { channels: [doc._id], grant: { public: [doc._id] } };
968
- }
969
-
970
- throw { forbidden: "unknown document type" };
971
- }
972
- ```
973
-
974
- 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.
975
-
976
- App.jsx — anonymous-friendly submit form, owner-only config panel, role-gated results view:
977
-
978
- ```jsx
979
- <<<<<<< SEARCH
980
- const { database } = useFireproof("notes");
981
- =======
982
- const { database, useLiveQuery, access } = useFireproof("survey");
983
- const { me, can } = useVibe("survey");
984
- // Submit form is shown for anyone — allowAnonymous makes this can.create return ok for null user.
985
- const canSubmit = can.create({ type: "survey-response", question: "q1", answer: "" });
986
- // Owner-only — survey-config rule calls ctx.requireRole("owner").
987
- const canConfigure = can.create({ type: "survey-config" });
988
- const { docs: results } = useLiveQuery("type", { key: "final-results" });
989
- >>>>>>> REPLACE
990
- ```
991
-
992
- ```jsx
993
- <<<<<<< SEARCH
994
- <input value={answer} onChange={(e) => setAnswer(e.target.value)} />
995
- =======
996
- {/* anyone (signed in OR anonymous) can submit — only stamp authorHandle when present */}
997
- {canSubmit.ok ? (
998
- <form onSubmit={(e) => {
999
- e.preventDefault();
1000
- database.put({ type: "survey-response", question: "q1", answer, ...(me && { authorHandle: me.userHandle }) });
1001
- }}>
1002
- <input value={answer} onChange={(e) => setAnswer(e.target.value)} />
1003
- <button type="submit">Submit</button>
1004
- </form>
1005
- ) : <p>{canSubmit.reason}</p>}
1006
-
1007
- {/* owner-only admin to wire up the feedback-team role */}
1008
- {canConfigure.ok && <ConfigPanel />}
1009
-
1010
- {/* results render only for users with the feedback-team role — access.hasChannel filters them in */}
1011
- {results.filter((r) => access.hasChannel(r._id)).map((r) => <ResultCard key={r._id} doc={r} />)}
1012
- >>>>>>> REPLACE
1013
- ```
1014
-
1015
- ### Example: Public guestbook / contact form (anonymous writes)
1016
-
1017
- 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.
1018
-
1019
- access.js
1020
-
1021
- ```js
1022
- export function guestbook(doc, oldDoc, user, ctx) {
1023
- if (doc.type === "entry") {
1024
- if (oldDoc) throw { forbidden: "entries are write-once" };
1025
- // No `if (!user) throw` — anyone may sign. allowAnonymous opts the write in.
1026
- return { channels: ["public"], grant: { public: ["public"] }, allowAnonymous: true };
1027
- }
1028
- throw { forbidden: "unknown document type" };
1029
- }
1030
- ```
1031
-
1032
- 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.
1033
-
1034
- App.jsx — gate the form on `can.create({ type: "entry" }).ok`, which is true even when nobody is signed in:
1035
-
1036
- ```jsx
1037
- <<<<<<< SEARCH
1038
- const { database } = useFireproof("notes");
1039
- =======
1040
- const { database, useLiveQuery } = useFireproof("guestbook");
1041
- const { me, can } = useVibe("guestbook");
1042
- const { docs: entries } = useLiveQuery("type", { key: "entry" });
1043
- // allowAnonymous: true on the access fn → this returns ok for an anon viewer.
1044
- // The form is visible from first load, no sign-in wall.
1045
- const canSign = can.create({ type: "entry", message: "" });
1046
- >>>>>>> REPLACE
1047
- ```
1048
-
1049
- ```jsx
1050
- <<<<<<< SEARCH
1051
- <SignInGate>
1052
- <input value={message} onChange={(e) => setMessage(e.target.value)} />
1053
- </SignInGate>
1054
- =======
1055
- {canSign.ok ? (
1056
- <form onSubmit={(e) => {
1057
- e.preventDefault();
1058
- // Stamp authorHandle only when signed in — anon entries simply omit it.
1059
- database.put({ type: "entry", message, createdAt: Date.now(), ...(me && { authorHandle: me.userHandle }) });
1060
- setMessage("");
1061
- }}>
1062
- <input value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Leave a note" />
1063
- <button type="submit">Sign</button>
1064
- </form>
1065
- ) : <p>{canSign.reason}</p>}
1066
-
1067
- {entries.map((e) => <li key={e._id}>{e.message} — {e.authorHandle || "anonymous"}</li>)}
1068
- >>>>>>> REPLACE
1069
- ```
1070
-
1071
489
  ### Multiple databases in one file
1072
490
 
1073
491
  Each named export gates its own database. A single `/access.js` can gate all databases the app uses:
package/llms/index.d.ts CHANGED
@@ -11,5 +11,6 @@ export { webxrConfig } from "./webxr.js";
11
11
  export { useViewerConfig } from "./use-viewer.js";
12
12
  export { useVibeConfig } from "./use-vibe.js";
13
13
  export { createVibeConfig } from "./create-vibe.js";
14
+ export { accessConfig } from "./access.js";
14
15
  export type { LlmConfig } from "./types.js";
15
- export declare const allConfigs: readonly [import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig];
16
+ export declare const allConfigs: readonly [import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig];
package/llms/index.js CHANGED
@@ -11,6 +11,7 @@ import { webxrConfig } from "./webxr.js";
11
11
  import { useViewerConfig } from "./use-viewer.js";
12
12
  import { useVibeConfig } from "./use-vibe.js";
13
13
  import { createVibeConfig } from "./create-vibe.js";
14
+ import { accessConfig } from "./access.js";
14
15
  export { backendConfig } from "./backend.js";
15
16
  export { calendarConfig } from "./calendar.js";
16
17
  export { callaiConfig } from "./callai.js";
@@ -24,6 +25,7 @@ export { webxrConfig } from "./webxr.js";
24
25
  export { useViewerConfig } from "./use-viewer.js";
25
26
  export { useVibeConfig } from "./use-vibe.js";
26
27
  export { createVibeConfig } from "./create-vibe.js";
28
+ export { accessConfig } from "./access.js";
27
29
  export const allConfigs = [
28
30
  callaiConfig,
29
31
  imageGenConfig,
@@ -38,5 +40,6 @@ export const allConfigs = [
38
40
  createVibeConfig,
39
41
  backendConfig,
40
42
  calendarConfig,
43
+ accessConfig,
41
44
  ];
42
45
  //# sourceMappingURL=index.js.map
package/llms/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../jsr/llms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIpD,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,QAAQ;IACR,aAAa;IACb,WAAW;IACX,eAAe;IACf,WAAW;IACX,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,cAAc;CACN,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../jsr/llms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,QAAQ;IACR,aAAa;IACb,WAAW;IACX,eAAe;IACf,WAAW;IACX,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,cAAc;IACd,YAAY;CACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "8.1.1",
3
+ "version": "8.2.0",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -24,9 +24,9 @@
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
26
  "@adviser/cement": "~0.5.34",
27
- "@vibes.diy/call-ai-v2": "^8.1.1",
28
- "@vibes.diy/identity": "^8.1.1",
29
- "@vibes.diy/use-vibes-types": "^8.1.1",
27
+ "@vibes.diy/call-ai-v2": "^8.2.0",
28
+ "@vibes.diy/identity": "^8.2.0",
29
+ "@vibes.diy/use-vibes-types": "^8.2.0",
30
30
  "arktype": "~2.2.3",
31
31
  "json-schema-faker": "~0.6.2"
32
32
  },