@vibes.diy/prompts 2.5.17 → 2.5.19
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 +56 -12
- package/llms/use-vibe.md +1 -1
- package/package.json +4 -4
- package/system-prompt-initial.md +1 -1
- package/system-prompt.md +12 -2
package/llms/fireproof.md
CHANGED
|
@@ -394,7 +394,7 @@ The access function is the single source of truth for permissions. Gate write su
|
|
|
394
394
|
|
|
395
395
|
This example shows the full round-trip — access.js declares channels and grants; App.jsx reads them back via `access`. Key details:
|
|
396
396
|
|
|
397
|
-
- **Owner bootstrap:** `
|
|
397
|
+
- **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 `user.isOwner`** (`isOwner` is being retired from the access-fn `user`). 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.
|
|
398
398
|
- **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()`.
|
|
399
399
|
- **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.
|
|
400
400
|
- **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.)
|
|
@@ -407,7 +407,7 @@ export function announcements(doc, oldDoc, user, ctx) {
|
|
|
407
407
|
if (!user) throw { forbidden: "sign in" };
|
|
408
408
|
|
|
409
409
|
if (doc.type === "channel") {
|
|
410
|
-
|
|
410
|
+
ctx.requireRole("owner");
|
|
411
411
|
return {
|
|
412
412
|
channels: [doc._id],
|
|
413
413
|
grant: {
|
|
@@ -419,7 +419,7 @@ export function announcements(doc, oldDoc, user, ctx) {
|
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
if (doc.type === "roleGrant") {
|
|
422
|
-
|
|
422
|
+
ctx.requireRole("owner");
|
|
423
423
|
// A grant doc must ALSO route to a channel — a result with no `channels`
|
|
424
424
|
// (only `members`/`grant`) is rejected as an "unreadable write". Route it to
|
|
425
425
|
// an owner-readable admin channel (not a public one) so the grant persists
|
|
@@ -463,8 +463,8 @@ export default function App() {
|
|
|
463
463
|
// checks authorHandle/channel (and owner-only for roleGrant), so a bare partial
|
|
464
464
|
// would be denied and hide the control even from users who can act.
|
|
465
465
|
const canPost = can.create({ type: "post", channel, authorHandle: viewer?.userHandle });
|
|
466
|
-
// Owner-only management gates on can.* too — the access fn
|
|
467
|
-
//
|
|
466
|
+
// Owner-only management gates on can.* too — the access fn calls
|
|
467
|
+
// ctx.requireRole("owner"), so this verdict is false for everyone but the owner.
|
|
468
468
|
const canGrant = can.create({ type: "roleGrant", role: "poster", userHandle: "newUser" });
|
|
469
469
|
|
|
470
470
|
if (isViewerPending) return null;
|
|
@@ -519,7 +519,7 @@ export default function App() {
|
|
|
519
519
|
}
|
|
520
520
|
```
|
|
521
521
|
|
|
522
|
-
The pattern: `useVibe().can` gates every write surface — including owner-only management, which the access function enforces via `
|
|
522
|
+
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, 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.
|
|
523
523
|
|
|
524
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
525
|
|
|
@@ -561,7 +561,7 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
561
561
|
if (!user) throw { forbidden: "sign in" };
|
|
562
562
|
|
|
563
563
|
if (doc.type === "channel") {
|
|
564
|
-
|
|
564
|
+
ctx.requireRole("owner");
|
|
565
565
|
// Open channel: public READ for everyone. No write-membership grant is
|
|
566
566
|
// needed — any signed-in user may post (see the post rule below).
|
|
567
567
|
return { channels: [doc._id], grant: { public: [doc._id] } };
|
|
@@ -612,6 +612,50 @@ App.jsx — `access.hasChannel()` filters which channels are visible (display);
|
|
|
612
612
|
|
|
613
613
|
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.
|
|
614
614
|
|
|
615
|
+
### Example: Per-object sharing (collaborate on your own objects, no admin)
|
|
616
|
+
|
|
617
|
+
A list app where every signed-in user makes their own lists, sees only their own, and can invite anyone to collaborate on a specific list — peer to peer, with no app admin in the loop. The pattern: **a channel per object** (`list:<id>`); the creator grants themselves that channel at creation; child docs (items) gate on `ctx.requireAccess` of the list's channel; any current member shares the list by granting another user the same channel. Membership is direct `grant.users`, so each viewer's access scales with their own memberships.
|
|
618
|
+
|
|
619
|
+
access.js
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
export default function (doc, oldDoc, user, ctx) {
|
|
623
|
+
if (!user) throw { forbidden: "sign in" };
|
|
624
|
+
const ch = (id) => `list:${id}`;
|
|
625
|
+
|
|
626
|
+
if (doc.type === "list") {
|
|
627
|
+
// Creator owns the list doc; route it to its own channel and grant self.
|
|
628
|
+
const author = oldDoc ? oldDoc.author : doc.author;
|
|
629
|
+
if (author !== user.userHandle) throw { forbidden: "not your list" };
|
|
630
|
+
// author is write-once: an update must not re-author the list (which would
|
|
631
|
+
// change who can edit it and hand control to someone never granted).
|
|
632
|
+
if (oldDoc && doc.author !== oldDoc.author) throw { forbidden: "cannot change author" };
|
|
633
|
+
return { channels: [ch(doc._id)], grant: { users: { [user.userHandle]: [ch(doc._id)] } } };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (doc.type === "item") {
|
|
637
|
+
// Any member of the list may add/edit items in it. listId is immutable —
|
|
638
|
+
// without this, a member of list X could re-point an existing item from a
|
|
639
|
+
// list they don't belong to into X (it would still pass requireAccess(X)).
|
|
640
|
+
if (oldDoc && oldDoc.listId !== doc.listId) throw { forbidden: "cannot move item" };
|
|
641
|
+
ctx.requireAccess(ch(doc.listId));
|
|
642
|
+
return { channels: [ch(doc.listId)] };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (doc.type === "share") {
|
|
646
|
+
// Any current member invites a peer by handle — grants them the list channel.
|
|
647
|
+
// Route the share doc to the list channel itself (every member already holds
|
|
648
|
+
// it), so members see who was added without a second channel to grant.
|
|
649
|
+
ctx.requireAccess(ch(doc.listId));
|
|
650
|
+
return { channels: [ch(doc.listId)], grant: { users: { [doc.invitee]: [ch(doc.listId)] } } };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
throw { forbidden: "unknown document type" };
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
To invite someone who isn't a member yet without knowing their handle in advance, invert the flow with a request doc: a `request` type takes **no** `ctx.requireAccess` (any signed-in user may create one — their handle is `user.userHandle`, unforgeable) and routes to the list channel `ch(doc.listId)`, where current members read it (the requester can't read their own request back — they aren't a member yet — which is fine; they just wait to be granted). A member then writes the `share` above to approve. The UI gates each surface with `useVibe("lists").can` and reflects which lists the viewer can see with `access.hasChannel(...)`.
|
|
658
|
+
|
|
615
659
|
---
|
|
616
660
|
|
|
617
661
|
## Access Function (`/access.js`)
|
|
@@ -732,7 +776,7 @@ export function survey(doc, oldDoc, user, ctx) {
|
|
|
732
776
|
}
|
|
733
777
|
|
|
734
778
|
if (doc.type === "survey-config") {
|
|
735
|
-
|
|
779
|
+
ctx.requireRole("owner");
|
|
736
780
|
// Route this grant/config doc to an owner-readable admin channel — a
|
|
737
781
|
// grant-only result (no `channels`) is rejected as an "unreadable write".
|
|
738
782
|
return {
|
|
@@ -853,7 +897,7 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
853
897
|
// Grant/meta docs must also route to a channel — a channel-less result is
|
|
854
898
|
// rejected as "unreadable write". Route them to an owner-readable admin channel.
|
|
855
899
|
if (doc.type === "team-meta") {
|
|
856
|
-
|
|
900
|
+
ctx.requireRole("owner");
|
|
857
901
|
return {
|
|
858
902
|
channels: ["admin:grants"],
|
|
859
903
|
members: { [doc.teamId]: doc.memberHandles },
|
|
@@ -862,7 +906,7 @@ export function chat(doc, oldDoc, user, ctx) {
|
|
|
862
906
|
}
|
|
863
907
|
|
|
864
908
|
if (doc.type === "membership") {
|
|
865
|
-
|
|
909
|
+
ctx.requireRole("owner");
|
|
866
910
|
return {
|
|
867
911
|
channels: ["admin:grants"],
|
|
868
912
|
members: { [doc.role]: [doc.userHandle] },
|
|
@@ -887,11 +931,11 @@ access.js
|
|
|
887
931
|
```js
|
|
888
932
|
<<<<<<< SEARCH
|
|
889
933
|
if (doc.type === "team-meta") {
|
|
890
|
-
|
|
934
|
+
ctx.requireRole("owner");
|
|
891
935
|
return {
|
|
892
936
|
=======
|
|
893
937
|
if (doc.type === "team-meta") {
|
|
894
|
-
|
|
938
|
+
ctx.requireRole("owner");
|
|
895
939
|
// Immutable-after-create fields
|
|
896
940
|
if (oldDoc && doc.createdBy !== oldDoc.createdBy) {
|
|
897
941
|
throw { forbidden: "createdBy is immutable" };
|
package/llms/use-vibe.md
CHANGED
|
@@ -39,7 +39,7 @@ function PromptBar({ database }) {
|
|
|
39
39
|
|
|
40
40
|
## Owner-only and role-gated surfaces
|
|
41
41
|
|
|
42
|
-
Don't gate management UI on `isOwner` directly. Encode the rule in `access.js` (e.g. `
|
|
42
|
+
Don't gate management UI on `isOwner` directly. Encode the rule in `access.js` (e.g. `ctx.requireRole("owner")` — the owner is auto-seeded into the reserved `owner` role) and gate the UI on `can.*` for that database — the verdict reflects the same rule. Per-row edit/delete affordances: `{can.edit(doc).ok && <EditButton doc={doc} />}`. By default every signed-in visitor is a first-class participant who creates and edits their own data; reserve `requireRole("owner")` for genuinely owner-published apps and per-object channels for peer-to-peer sharing. Public-vs-private and the allowed-user list are the owner's runtime sharing settings, not `access.js`.
|
|
43
43
|
|
|
44
44
|
## The server is still the authority
|
|
45
45
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.19",
|
|
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.19",
|
|
34
|
+
"@vibes.diy/identity": "^2.5.19",
|
|
35
|
+
"@vibes.diy/use-vibes-types": "^2.5.19",
|
|
36
36
|
"arktype": "~2.2.1",
|
|
37
37
|
"json-schema-faker": "~0.6.2"
|
|
38
38
|
},
|
package/system-prompt-initial.md
CHANGED
|
@@ -63,7 +63,7 @@ Target ~40–60 lines. The shell should look like a real app with empty sections
|
|
|
63
63
|
> export function chat(doc, oldDoc, user, ctx) {
|
|
64
64
|
> if (!user) throw { forbidden: "sign in" };
|
|
65
65
|
> if (doc.type === "channel") {
|
|
66
|
-
>
|
|
66
|
+
> ctx.requireRole("owner"); // owner is auto-seeded into the reserved `owner` role
|
|
67
67
|
> return { channels: [doc._id], grant: { public: [doc._id] } };
|
|
68
68
|
> }
|
|
69
69
|
> if (doc.type === "message") {
|
package/system-prompt.md
CHANGED
|
@@ -347,6 +347,14 @@ Keep `useViewer` (for `ViewerTag`) on `App`'s first line, and add `useVibe(dbNam
|
|
|
347
347
|
|
|
348
348
|
**If the app needs an `access.js`, emit it right after the scaffold — before any feature edits.** Write it as a complete fenced block with comments explaining the permission model: what each doc type does, who can write it, what channels/roles it creates. This commits to the permission design early so every subsequent App.jsx edit can gate its write surfaces on `useVibe(dbName).can` — the same rules the access function enforces. If later feature edits introduce new doc types, emit a follow-up `access.js` block with the additions.
|
|
349
349
|
|
|
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
|
+
|
|
352
|
+
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
|
+
|
|
354
|
+
**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.
|
|
355
|
+
|
|
356
|
+
**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.
|
|
357
|
+
|
|
350
358
|
Example streamed output for a team board app:
|
|
351
359
|
|
|
352
360
|
> **Crew Board** — team channel board with live posts, pinned announcements, and owner-managed channels.
|
|
@@ -404,7 +412,9 @@ Example streamed output for a team board app:
|
|
|
404
412
|
> access.js
|
|
405
413
|
>
|
|
406
414
|
> ```js
|
|
407
|
-
> // Each channel doc grants public READ to that channel; only the owner creates
|
|
415
|
+
> // Each channel doc grants public READ to that channel; only the owner creates
|
|
416
|
+
> // channels. The vibe owner is auto-seeded into the reserved `owner` role, so
|
|
417
|
+
> // gate owner-only docs on ctx.requireRole("owner") — never on user.isOwner.
|
|
408
418
|
> // OPEN board: any signed-in user posts. Do NOT gate posts on ctx.requireAccess —
|
|
409
419
|
> // grant.public is read-only and never satisfies it, so it would block every
|
|
410
420
|
> // non-owner. (Members-only channel instead? grant a role + requireAccess it.)
|
|
@@ -412,7 +422,7 @@ Example streamed output for a team board app:
|
|
|
412
422
|
> if (!user) throw { forbidden: "sign in" };
|
|
413
423
|
>
|
|
414
424
|
> if (doc.type === "channel") {
|
|
415
|
-
>
|
|
425
|
+
> ctx.requireRole("owner");
|
|
416
426
|
> return { channels: [doc._id], grant: { public: [doc._id] } };
|
|
417
427
|
> }
|
|
418
428
|
>
|