@vibes.diy/prompts 2.5.14 → 2.5.16
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 +65 -8
- package/package.json +4 -4
- package/system-prompt-initial.md +2 -1
- package/system-prompt.md +3 -1
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,6 +521,35 @@ 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
|
|
|
524
|
+
**Owner-management panels (appoint/revoke moderators, grant/revoke roles) gate on `can.*`, not `isOwner`.** It's tempting to wrap an admin panel in `{isOwner && <ModeratorPanel />}` and have its buttons call `database.put`/`database.del` directly. Don't let `isOwner` be what decides a write — it's the same display hint as everywhere else. Gate each mutating control on the 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 it grows beyond owner-only (a delegated admin role, say). Gate the panel's *visibility* on those same verdicts too, not `isOwner` alone — otherwise a delegated admin the access function now allows never reaches the controls or their reason.
|
|
525
|
+
|
|
526
|
+
```jsx
|
|
527
|
+
// Don't: isOwner decides visibility AND the writes are ungated — a non-owner the
|
|
528
|
+
// access function would allow never reaches the controls, and isOwner drifts from access.js
|
|
529
|
+
{isOwner && (
|
|
530
|
+
<ModeratorPanel>
|
|
531
|
+
<button onClick={() => database.put({ type: "modGrant", role: "moderator", userHandle })}>Appoint</button>
|
|
532
|
+
<button onClick={() => database.del(grantDoc._id)}>Revoke</button>
|
|
533
|
+
</ModeratorPanel>
|
|
534
|
+
)}
|
|
535
|
+
|
|
536
|
+
// Do: can.* decides visibility AND gates each write, and supplies the denial reason
|
|
537
|
+
const canAppoint = can.create({ type: "modGrant", role: "moderator", userHandle });
|
|
538
|
+
const canRevoke = can.delete(grantDoc);
|
|
539
|
+
{(canAppoint.ok || canRevoke.ok) && (
|
|
540
|
+
<ModeratorPanel>
|
|
541
|
+
{canAppoint.ok && (
|
|
542
|
+
<button onClick={() => database.put({ type: "modGrant", role: "moderator", userHandle })}>Appoint</button>
|
|
543
|
+
)}
|
|
544
|
+
{canRevoke.ok ? (
|
|
545
|
+
<button onClick={() => database.del(grantDoc._id)}>Revoke</button>
|
|
546
|
+
) : (
|
|
547
|
+
<p>{canRevoke.reason}</p>
|
|
548
|
+
)}
|
|
549
|
+
</ModeratorPanel>
|
|
550
|
+
)}
|
|
551
|
+
```
|
|
552
|
+
|
|
513
553
|
### Example: Channel board with open channels (any member posts)
|
|
514
554
|
|
|
515
555
|
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.
|
|
@@ -529,6 +569,7 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
529
569
|
|
|
530
570
|
if (doc.type === "post") {
|
|
531
571
|
if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
572
|
+
if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
532
573
|
// Any signed-in author may post to this open channel. Do NOT call
|
|
533
574
|
// ctx.requireAccess(doc.channel) here: the channel is grant.public
|
|
534
575
|
// (read-only), which never satisfies requireAccess, so gating on it would
|
|
@@ -589,12 +630,16 @@ Access functions live in `/access.js`, a separate file in the vibe's filesystem
|
|
|
589
630
|
|
|
590
631
|
**`requireAccess` checks _membership_, not public read — don't gate an open channel's writes on it.** `ctx.requireAccess(channelId)` passes only for a channel the user is a member of: granted directly through `grant.users[handle]`, or through a `grant.roles` role they hold. **`grant.public` does NOT satisfy `requireAccess`** — public is read-only ("anyone through the door can _read_"), it never confers write membership. So a channel that is only `grant.public` and gated on `ctx.requireAccess` can be written by **nobody** but the owner-in-admin-mode — every other write returns `not in channel`, silently hiding the form (`useVibe().can` faithfully reflects this). Choose by intent:
|
|
591
632
|
|
|
592
|
-
- **Open channel — any signed-in user may post** (public board, guestbook, comment wall): do **not** call `ctx.requireAccess`. Route the doc to the channel and check the author — `if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" }; return { channels: [doc.channelId] }`. `grant.public` on the channel doc gives everyone read; the write is open to any author.
|
|
633
|
+
- **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.
|
|
593
634
|
- **Restricted channel — only members may post**: gate the write on `ctx.requireAccess(doc.channelId)` **and** grant writers membership explicitly — `grant.users` (direct) or `grant.roles` + a `members`/role-grant doc. `public` alongside is read-only and is fine for letting non-members read, but it is never what lets a member write.
|
|
594
635
|
|
|
595
636
|
### AccessDescriptor return type
|
|
596
637
|
|
|
597
|
-
All fields are optional, but a stored document must be routed to at least one channel (`channels`) to be readable — a result with no channels is refused at write time. To reject a write outright, `throw { forbidden: "reason" }`.
|
|
638
|
+
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" }`.
|
|
639
|
+
|
|
640
|
+
**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.
|
|
641
|
+
|
|
642
|
+
**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`.)
|
|
598
643
|
|
|
599
644
|
```ts
|
|
600
645
|
type AccessDescriptor = {
|
|
@@ -654,12 +699,14 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
654
699
|
|
|
655
700
|
if (doc.type === "message") {
|
|
656
701
|
if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
702
|
+
if (oldDoc && oldDoc.userHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
657
703
|
ctx.requireAccess(doc.channelId);
|
|
658
704
|
return { channels: [doc.channelId] };
|
|
659
705
|
}
|
|
660
706
|
|
|
661
707
|
if (doc.type === "channel-invite") {
|
|
662
708
|
if (doc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
|
|
709
|
+
if (oldDoc && oldDoc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
|
|
663
710
|
ctx.requireAccess(doc.channelId);
|
|
664
711
|
return {
|
|
665
712
|
channels: [doc.channelId],
|
|
@@ -686,11 +733,13 @@ export function survey(doc, oldDoc, user, ctx) {
|
|
|
686
733
|
|
|
687
734
|
if (doc.type === "survey-config") {
|
|
688
735
|
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
736
|
+
// Route this grant/config doc to an owner-readable admin channel — a
|
|
737
|
+
// grant-only result (no `channels`) is rejected as an "unreadable write".
|
|
689
738
|
return {
|
|
739
|
+
channels: ["admin:grants"],
|
|
690
740
|
grant: {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
},
|
|
741
|
+
users: { [user.userHandle]: ["admin:grants"] },
|
|
742
|
+
roles: { "feedback-team": ["inbound-responses"] },
|
|
694
743
|
},
|
|
695
744
|
};
|
|
696
745
|
}
|
|
@@ -801,16 +850,24 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
801
850
|
export function chat(doc, oldDoc, user, ctx) {
|
|
802
851
|
if (!user) throw { forbidden: "authentication required" };
|
|
803
852
|
|
|
853
|
+
// Grant/meta docs must also route to a channel — a channel-less result is
|
|
854
|
+
// rejected as "unreadable write". Route them to an owner-readable admin channel.
|
|
804
855
|
if (doc.type === "team-meta") {
|
|
805
856
|
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
806
857
|
return {
|
|
858
|
+
channels: ["admin:grants"],
|
|
807
859
|
members: { [doc.teamId]: doc.memberHandles },
|
|
808
|
-
grant: { roles: { [doc.teamId]: doc.channels } },
|
|
860
|
+
grant: { users: { [user.userHandle]: ["admin:grants"] }, roles: { [doc.teamId]: doc.channels } },
|
|
809
861
|
};
|
|
810
862
|
}
|
|
811
863
|
|
|
812
864
|
if (doc.type === "membership") {
|
|
813
|
-
|
|
865
|
+
if (!user.isOwner) throw { forbidden: "owner only" };
|
|
866
|
+
return {
|
|
867
|
+
channels: ["admin:grants"],
|
|
868
|
+
members: { [doc.role]: [doc.userHandle] },
|
|
869
|
+
grant: { users: { [user.userHandle]: ["admin:grants"] } },
|
|
870
|
+
};
|
|
814
871
|
}
|
|
815
872
|
|
|
816
873
|
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.16",
|
|
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.16",
|
|
34
|
+
"@vibes.diy/identity": "^2.5.16",
|
|
35
|
+
"@vibes.diy/use-vibes-types": "^2.5.16",
|
|
36
36
|
"arktype": "~2.2.1",
|
|
37
37
|
"json-schema-faker": "~0.6.2"
|
|
38
38
|
},
|
package/system-prompt-initial.md
CHANGED
|
@@ -68,13 +68,14 @@ Target ~40–60 lines. The shell should look like a real app with empty sections
|
|
|
68
68
|
> }
|
|
69
69
|
> if (doc.type === "message") {
|
|
70
70
|
> if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
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
|
|
|
77
|
-
`ctx.requireAccess(channel)` gates on **membership** (a `grant.users`/`grant.roles` grant), not `grant.public` (read-only) — so an open channel anyone signed-in may post to must not gate writes on it; check the author and route the doc. For writes needing no sign-in ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`.
|
|
78
|
+
`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.
|
|
78
79
|
|
|
79
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.
|
|
80
81
|
|
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,7 +283,7 @@ When the app uses channel-based read isolation or per-document write validation,
|
|
|
282
283
|
> }
|
|
283
284
|
> ```
|
|
284
285
|
|
|
285
|
-
`ctx.requireAccess(channel)` gates on channel **membership** (a `grant.users`/`grant.roles` grant), NOT on `grant.public`, which is read-only — so a channel anyone signed-in should post to must **not** gate writes on `requireAccess` (it would block every non-owner); just check the author and route the doc. Reserve `requireAccess` for members-only channels whose writers you granted membership. For writes that need no sign-in at all ("anyone can sign/submit"), return `allowAnonymous: true` instead of throwing on `!user`. See the fireproof access docs.
|
|
286
|
+
`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.
|
|
286
287
|
|
|
287
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.
|
|
288
289
|
|
|
@@ -417,6 +418,7 @@ Example streamed output for a team board app:
|
|
|
417
418
|
>
|
|
418
419
|
> if (doc.type === "post") {
|
|
419
420
|
> if (doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
421
|
+
> if (oldDoc && oldDoc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
|
|
420
422
|
> return { channels: [doc.channelId] };
|
|
421
423
|
> }
|
|
422
424
|
>
|