@vibes.diy/prompts 2.5.13 → 2.5.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/llms/fireproof.md +72 -18
- package/package.json +4 -4
- package/system-prompt-initial.md +9 -6
- package/system-prompt.md +11 -7
package/llms/fireproof.md
CHANGED
|
@@ -420,11 +420,22 @@ export function announcements(doc, oldDoc, user, ctx) {
|
|
|
420
420
|
|
|
421
421
|
if (doc.type === "roleGrant") {
|
|
422
422
|
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
423
|
-
|
|
423
|
+
// A grant doc must ALSO route to a channel — a result with no `channels`
|
|
424
|
+
// (only `members`/`grant`) is rejected as an "unreadable write". Route it to
|
|
425
|
+
// an owner-readable admin channel (not a public one) so the grant persists
|
|
426
|
+
// and the owner can read the roster back; the membership then applies.
|
|
427
|
+
return {
|
|
428
|
+
channels: ["admin:grants"],
|
|
429
|
+
members: { [doc.role]: [doc.userHandle] },
|
|
430
|
+
grant: { users: { [user.userHandle]: ["admin:grants"] } },
|
|
431
|
+
};
|
|
424
432
|
}
|
|
425
433
|
|
|
426
434
|
if (doc.type === "post") {
|
|
427
435
|
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
436
|
+
// On update, the original author must be preserved — never let a writer
|
|
437
|
+
// overwrite someone else's doc or reassign its author.
|
|
438
|
+
if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
428
439
|
ctx.requireAccess(doc.channel);
|
|
429
440
|
return { channels: [doc.channel] };
|
|
430
441
|
}
|
|
@@ -510,9 +521,9 @@ export default function App() {
|
|
|
510
521
|
|
|
511
522
|
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
523
|
|
|
513
|
-
### Example: Channel board with
|
|
524
|
+
### Example: Channel board with open channels (any member posts)
|
|
514
525
|
|
|
515
|
-
|
|
526
|
+
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
527
|
|
|
517
528
|
access.js
|
|
518
529
|
|
|
@@ -522,18 +533,19 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
522
533
|
|
|
523
534
|
if (doc.type === "channel") {
|
|
524
535
|
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
users: { [user.userHandle]: [doc._id] },
|
|
529
|
-
public: [doc._id],
|
|
530
|
-
},
|
|
531
|
-
};
|
|
536
|
+
// Open channel: public READ for everyone. No write-membership grant is
|
|
537
|
+
// needed — any signed-in user may post (see the post rule below).
|
|
538
|
+
return { channels: [doc._id], grant: { public: [doc._id] } };
|
|
532
539
|
}
|
|
533
540
|
|
|
534
541
|
if (doc.type === "post") {
|
|
535
542
|
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
536
|
-
|
|
543
|
+
if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
544
|
+
// Any signed-in author may post to this open channel. Do NOT call
|
|
545
|
+
// ctx.requireAccess(doc.channel) here: the channel is grant.public
|
|
546
|
+
// (read-only), which never satisfies requireAccess, so gating on it would
|
|
547
|
+
// block every non-owner from posting. requireAccess is for members-only
|
|
548
|
+
// channels whose writers were granted membership (see announcements above).
|
|
537
549
|
return { channels: [doc.channel] };
|
|
538
550
|
}
|
|
539
551
|
|
|
@@ -549,7 +561,9 @@ App.jsx — `access.hasChannel()` filters which channels are visible (display);
|
|
|
549
561
|
=======
|
|
550
562
|
const { database, useLiveQuery, access } = useFireproof("chat");
|
|
551
563
|
const { docs: channels } = useLiveQuery("type", { key: "channel" });
|
|
552
|
-
|
|
564
|
+
// Build the candidate from the doc you'll write — the access fn checks
|
|
565
|
+
// authorHandle, so a bare { type: "post" } would be denied and hide the form.
|
|
566
|
+
const canPost = useVibe("chat").can.create({ type: "post", channel, authorHandle: viewer?.userHandle });
|
|
553
567
|
>>>>>>> REPLACE
|
|
554
568
|
```
|
|
555
569
|
|
|
@@ -585,9 +599,18 @@ Access functions live in `/access.js`, a separate file in the vibe's filesystem
|
|
|
585
599
|
|
|
586
600
|
**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
601
|
|
|
602
|
+
**`requireAccess` checks _membership_, not public read — don't gate an open channel's writes on it.** `ctx.requireAccess(channelId)` passes only for a channel the user is a member of: granted directly through `grant.users[handle]`, or through a `grant.roles` role they hold. **`grant.public` does NOT satisfy `requireAccess`** — public is read-only ("anyone through the door can _read_"), it never confers write membership. So a channel that is only `grant.public` and gated on `ctx.requireAccess` can be written by **nobody** but the owner-in-admin-mode — every other write returns `not in channel`, silently hiding the form (`useVibe().can` faithfully reflects this). Choose by intent:
|
|
603
|
+
|
|
604
|
+
- **Open channel — any signed-in user may post** (public board, guestbook, comment wall): do **not** call `ctx.requireAccess`. Route the doc to the channel and check the author — `if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }; if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" }; return { channels: [doc.channelId] }`. `grant.public` on the channel doc gives everyone read; the write is open to any author.
|
|
605
|
+
- **Restricted channel — only members may post**: gate the write on `ctx.requireAccess(doc.channelId)` **and** grant writers membership explicitly — `grant.users` (direct) or `grant.roles` + a `members`/role-grant doc. `public` alongside is read-only and is fine for letting non-members read, but it is never what lets a member write.
|
|
606
|
+
|
|
588
607
|
### AccessDescriptor return type
|
|
589
608
|
|
|
590
|
-
All fields are optional, but a stored document must be routed to at least one channel (`channels`) to be readable — a result with no channels is refused at write time. To reject a write outright, `throw { forbidden: "reason" }`.
|
|
609
|
+
All fields are optional, but a stored document must be routed to at least one channel (`channels`) to be readable — a result with no channels is refused at write time ("unreadable write"). To reject a write outright, `throw { forbidden: "reason" }`.
|
|
610
|
+
|
|
611
|
+
**Grant/member/meta docs need a channel too.** A role grant, membership, or config singleton that returns only `members`/`grant` with **no `channels`** is refused exactly like any other channel-less write — so the owner can't even create it. Route these to an owner-readable **admin channel** (e.g. `channels: ["admin:grants"]` with `grant: { users: { [user.userHandle]: ["admin:grants"] } }`), not a public channel. The `members`/`grant` still take effect globally; the channel just makes the doc persist and lets the owner read the roster back.
|
|
612
|
+
|
|
613
|
+
**Preserve the author on updates.** For an author-owned doc, checking the new author field is not enough — also check `oldDoc` so a writer can't overwrite someone else's doc or reassign its author: `if (oldDoc && oldDoc.<authorField> !== user.userHandle) throw { forbidden: "not author" }`, where `<authorField>` is whatever your doc uses (`authorHandle`, `userHandle`, `senderHandle`, …). (Write-once docs can simply `if (oldDoc) throw`.)
|
|
591
614
|
|
|
592
615
|
```ts
|
|
593
616
|
type AccessDescriptor = {
|
|
@@ -647,12 +670,14 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
647
670
|
|
|
648
671
|
if (doc.type === "message") {
|
|
649
672
|
if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
673
|
+
if (oldDoc && oldDoc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
650
674
|
ctx.requireAccess(doc.channelId);
|
|
651
675
|
return { channels: [doc.channelId] };
|
|
652
676
|
}
|
|
653
677
|
|
|
654
678
|
if (doc.type === "channel-invite") {
|
|
655
679
|
if (doc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
|
|
680
|
+
if (oldDoc && oldDoc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
|
|
656
681
|
ctx.requireAccess(doc.channelId);
|
|
657
682
|
return {
|
|
658
683
|
channels: [doc.channelId],
|
|
@@ -679,11 +704,13 @@ export function survey(doc, oldDoc, user, ctx) {
|
|
|
679
704
|
|
|
680
705
|
if (doc.type === "survey-config") {
|
|
681
706
|
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
707
|
+
// Route this grant/config doc to an owner-readable admin channel — a
|
|
708
|
+
// grant-only result (no `channels`) is rejected as an "unreadable write".
|
|
682
709
|
return {
|
|
710
|
+
channels: ["admin:grants"],
|
|
683
711
|
grant: {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
},
|
|
712
|
+
users: { [user.userHandle]: ["admin:grants"] },
|
|
713
|
+
roles: { "feedback-team": ["inbound-responses"] },
|
|
687
714
|
},
|
|
688
715
|
};
|
|
689
716
|
}
|
|
@@ -699,6 +726,25 @@ export function survey(doc, oldDoc, user, ctx) {
|
|
|
699
726
|
|
|
700
727
|
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
728
|
|
|
729
|
+
### Example: Public guestbook / contact form (anonymous writes)
|
|
730
|
+
|
|
731
|
+
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.
|
|
732
|
+
|
|
733
|
+
access.js
|
|
734
|
+
|
|
735
|
+
```js
|
|
736
|
+
export function guestbook(doc, oldDoc, user, ctx) {
|
|
737
|
+
if (doc.type === "entry") {
|
|
738
|
+
if (oldDoc) throw { forbidden: "entries are write-once" };
|
|
739
|
+
// No `if (!user) throw` — anyone may sign. allowAnonymous opts the write in.
|
|
740
|
+
return { channels: ["public"], grant: { public: ["public"] }, allowAnonymous: true };
|
|
741
|
+
}
|
|
742
|
+
throw { forbidden: "unknown document type" };
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
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.
|
|
747
|
+
|
|
702
748
|
### Multiple databases in one file
|
|
703
749
|
|
|
704
750
|
Each named export gates its own database. A single `/access.js` can gate all databases the app uses:
|
|
@@ -775,16 +821,24 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
775
821
|
export function chat(doc, oldDoc, user, ctx) {
|
|
776
822
|
if (!user) throw { forbidden: "authentication required" };
|
|
777
823
|
|
|
824
|
+
// Grant/meta docs must also route to a channel — a channel-less result is
|
|
825
|
+
// rejected as "unreadable write". Route them to an owner-readable admin channel.
|
|
778
826
|
if (doc.type === "team-meta") {
|
|
779
827
|
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
780
828
|
return {
|
|
829
|
+
channels: ["admin:grants"],
|
|
781
830
|
members: { [doc.teamId]: doc.memberHandles },
|
|
782
|
-
grant: { roles: { [doc.teamId]: doc.channels } },
|
|
831
|
+
grant: { users: { [user.userHandle]: ["admin:grants"] }, roles: { [doc.teamId]: doc.channels } },
|
|
783
832
|
};
|
|
784
833
|
}
|
|
785
834
|
|
|
786
835
|
if (doc.type === "membership") {
|
|
787
|
-
|
|
836
|
+
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
837
|
+
return {
|
|
838
|
+
channels: ["admin:grants"],
|
|
839
|
+
members: { [doc.role]: [doc.userHandle] },
|
|
840
|
+
grant: { users: { [user.userHandle]: ["admin:grants"] } },
|
|
841
|
+
};
|
|
788
842
|
}
|
|
789
843
|
|
|
790
844
|
ctx.requireAccess(doc.channelId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"description": "",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"@fireproof/core-types-base": "~0.24.19",
|
|
31
31
|
"@fireproof/core-types-protocols-cloud": "~0.24.19",
|
|
32
32
|
"@fireproof/use-fireproof": "~0.24.19",
|
|
33
|
-
"@vibes.diy/call-ai-v2": "^2.5.
|
|
34
|
-
"@vibes.diy/identity": "^2.5.
|
|
35
|
-
"@vibes.diy/use-vibes-types": "^2.5.
|
|
33
|
+
"@vibes.diy/call-ai-v2": "^2.5.15",
|
|
34
|
+
"@vibes.diy/identity": "^2.5.15",
|
|
35
|
+
"@vibes.diy/use-vibes-types": "^2.5.15",
|
|
36
36
|
"arktype": "~2.2.1",
|
|
37
37
|
"json-schema-faker": "~0.6.2"
|
|
38
38
|
},
|
package/system-prompt-initial.md
CHANGED
|
@@ -51,29 +51,32 @@ 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
|
|
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
|
|
60
|
-
> //
|
|
61
|
-
> //
|
|
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.
|
|
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
|
-
>
|
|
71
|
+
> if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
71
72
|
> return { channels: [doc.channelId] };
|
|
72
73
|
> }
|
|
73
74
|
> throw { forbidden: "unknown document type" };
|
|
74
75
|
> }
|
|
75
76
|
> ```
|
|
76
77
|
|
|
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
|
+
|
|
77
80
|
**Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
|
|
78
81
|
|
|
79
82
|
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
|
@@ -275,6 +275,7 @@ When the app uses channel-based read isolation or per-document write validation,
|
|
|
275
275
|
> if (!user) throw { forbidden: "authentication required" };
|
|
276
276
|
> if (doc.type === "message") {
|
|
277
277
|
> if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
278
|
+
> if (oldDoc && oldDoc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
278
279
|
> ctx.requireAccess(doc.channelId);
|
|
279
280
|
> return { channels: [doc.channelId] };
|
|
280
281
|
> }
|
|
@@ -282,6 +283,8 @@ When the app uses channel-based read isolation or per-document write validation,
|
|
|
282
283
|
> }
|
|
283
284
|
> ```
|
|
284
285
|
|
|
286
|
+
`ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. A grant/member/role doc must also return `channels` (route it to an owner-readable admin channel like `["admin:grants"]`) — a channel-less result is rejected as "unreadable write". On updates, also check `oldDoc` (`if (oldDoc && oldDoc.<authorField> !== user.userHandle) throw`, where `<authorField>` is your doc's author field — `authorHandle`/`userHandle`/`senderHandle`) so a writer can't overwrite or re-author someone else's doc. See the fireproof access docs.
|
|
287
|
+
|
|
285
288
|
**Never put access function code inside an `App.jsx` block** — it will overwrite the React component. The filename line (`access.js` vs `App.jsx`) is how the system knows which file to write.
|
|
286
289
|
|
|
287
290
|
## Your starter scaffold
|
|
@@ -396,29 +399,30 @@ Example streamed output for a team board app:
|
|
|
396
399
|
> }
|
|
397
400
|
> ```
|
|
398
401
|
>
|
|
399
|
-
> Access function — owner manages channels
|
|
402
|
+
> Access function — owner manages channels; any signed-in member posts to these open channels.
|
|
400
403
|
>
|
|
401
404
|
> access.js
|
|
402
405
|
>
|
|
403
406
|
> ```js
|
|
404
|
-
> // Each channel doc grants public
|
|
405
|
-
> //
|
|
406
|
-
> //
|
|
407
|
+
> // Each channel doc grants public READ to that channel; only the owner creates channels.
|
|
408
|
+
> // OPEN board: any signed-in user posts. Do NOT gate posts on ctx.requireAccess —
|
|
409
|
+
> // grant.public is read-only and never satisfies it, so it would block every
|
|
410
|
+
> // non-owner. (Members-only channel instead? grant a role + requireAccess it.)
|
|
407
411
|
> export function crewBoard(doc, oldDoc, user, ctx) {
|
|
408
412
|
> if (!user) throw { forbidden: "sign in" };
|
|
409
413
|
>
|
|
410
414
|
> if (doc.type === "channel") {
|
|
411
415
|
> if (!user.isOwner) throw { forbidden: "owner only" };
|
|
412
|
-
> return { channels: [doc.
|
|
416
|
+
> return { channels: [doc._id], grant: { public: [doc._id] } };
|
|
413
417
|
> }
|
|
414
418
|
>
|
|
415
419
|
> if (doc.type === "post") {
|
|
416
420
|
> if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
417
|
-
>
|
|
421
|
+
> if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
418
422
|
> return { channels: [doc.channelId] };
|
|
419
423
|
> }
|
|
420
424
|
>
|
|
421
|
-
>
|
|
425
|
+
> throw { forbidden: "unknown document type" };
|
|
422
426
|
> }
|
|
423
427
|
> ```
|
|
424
428
|
>
|