@vibes.diy/prompts 2.4.8 → 2.4.9

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.
Files changed (2) hide show
  1. package/llms/fireproof.md +239 -55
  2. package/package.json +3 -3
package/llms/fireproof.md CHANGED
@@ -4,11 +4,13 @@ Fireproof is a lightweight embedded document database with encrypted live sync,
4
4
 
5
5
  ## Key Features
6
6
 
7
- - **Apps run anywhere:** Bundle UI, data, and logic in one file.
7
+ - **Apps run anywhere:** Bundle UI, data, and logic together.
8
8
  - **Real-Time & Offline-First:** Automatic persistence and live queries, runs in the browser - no loading or error states.
9
9
  - **Unified API:** TypeScript works with Deno, Bun, Node.js, and the browser.
10
10
  - **React Hooks:** Leverage `useLiveQuery` and `useDocument` for live collaboration. Note: these are NOT top-level exports — they are returned by the `useFireproof()` hook. Always destructure from `const { useLiveQuery, useDocument, database } = useFireproof("db-name")`.
11
11
 
12
+ **File structure:** A vibe's source is one or more files. `/App.jsx` is the entry point (React component). `/access.js` is optional — include it when the app needs per-document write validation or channel-based read isolation. Both files are pushed together and the server discovers `/access.js` automatically.
13
+
12
14
  Fireproof enforces cryptographic causal consistency and ledger integrity using hash history, providing git-like versioning with lightweight blockchain-style verification. Data is stored and replicated as content-addressed encrypted blobs, making it safe and easy to sync via commodity object storage providers.
13
15
 
14
16
  ## Installation
@@ -271,71 +273,253 @@ Each capability (`read`, `write`, `delete`) is independent. Omitting one falls b
271
273
 
272
274
  ---
273
275
 
274
- ## Multi-Space Pattern: Owner Creates, Members Discover
276
+ ## Access Function (`/access.js`)
275
277
 
276
- `listDbNames()` is owner-only members can't enumerate databases by name. Instead, the owner writes registry documents into a shared database that all members can query to discover available spaces.
278
+ The `acl` option above is a coarse per-database gate. Access functions are a finer gate: functions the server runs on every write (including deletes) before storing the document. They validate writes, route documents to channels, and declare grants that control who can read what. Only create an `/access.js` file when the user asks for per-document routing, channel-based isolation, or document-level write validation.
277
279
 
278
- ```jsx
279
- import { useFireproof, useViewer } from "use-vibes";
280
- import { fireproof } from "use-fireproof";
281
- import { useState } from "react";
280
+ Access functions live in `/access.js`, a separate file in the vibe's filesystem alongside `/App.jsx`. Each **named export** maps to a database name — `export function chat(...)` gates `useFireproof("chat")`. An `export default` function acts as a catch-all: it gates any database that doesn't have its own named export. Named exports always take precedence over the default.
282
281
 
283
- export default function App() {
284
- const [activeSlug, setActiveSlug] = useState(null);
285
- return activeSlug ? (
286
- <SpaceView slug={activeSlug} onBack={() => setActiveSlug(null)} />
287
- ) : (
288
- <SpaceList onSelectSpace={setActiveSlug} />
289
- );
282
+ ```js
283
+ // /access.js each export name = the database it gates
284
+ export function chat(doc, oldDoc, user, ctx) {
285
+ if (!user) throw { forbidden: "authentication required" };
286
+ if (doc.type === "message") {
287
+ if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
288
+ ctx.requireAccess(doc.channelId);
289
+ return { channels: [doc.channelId] };
290
+ }
291
+ return {};
292
+ }
293
+ ```
294
+
295
+ ```js
296
+ // /App.jsx — no access option needed; the server matches by database name
297
+ const { useLiveQuery, database } = useFireproof("chat");
298
+ ```
299
+
300
+ ### Function signature
301
+
302
+ ```ts
303
+ (doc, oldDoc, user: UserContext | null, ctx: Helpers) => AccessDescriptor;
304
+ ```
305
+
306
+ - `doc` — the document being written
307
+ - `oldDoc` — the previous version (null for new documents)
308
+ - `user` — the authenticated user, or `null` for anonymous requests
309
+ - `ctx` — server-provided helpers for checking materialized state
310
+
311
+ **UserContext:**
312
+
313
+ ```ts
314
+ {
315
+ userHandle: string // stable unique id — use for all auth checks
316
+ displayName?: string // display only — never use for identity checks
290
317
  }
318
+ ```
319
+
320
+ **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.
321
+
322
+ - `ctx.requireAccess(channelId)` — throws if user is not in the channel
323
+ - `ctx.requireRole(roleName)` — throws if user is not in the role
324
+
325
+ ### AccessDescriptor return type
326
+
327
+ All fields are optional. `{}` is a valid return. `throw { forbidden: "reason" }` rejects the write.
328
+
329
+ ```ts
330
+ type AccessDescriptor = {
331
+ channels?: string[]; // route this doc to channels
332
+ members?: Record<roleName, userHandle[]>; // role membership (reduced by union)
333
+ grant?: {
334
+ users?: Record<userHandle, string[]>; // direct user → channel grants (reduced by union)
335
+ roles?: Record<roleName, string[]>; // role → channel grants (reduced by union)
336
+ public?: string[]; // public read — no auth required
337
+ };
338
+ expiry?: string | number | null; // ISO date or unix seconds
339
+ allowAnonymous?: boolean; // opt-in for null-user writes
340
+ };
341
+ ```
342
+
343
+ ### Key concepts
344
+
345
+ **Channels** route documents. A document with `channels: ["general"]` is only visible to users who have been granted access to `"general"`. Channels are the unit of read isolation.
346
+
347
+ **Grants are additive.** The effective access state is the union of every current document's `AccessDescriptor` output. There is no "remove grant" operation — deleting a document drops its contribution from the union automatically. This makes revocation trivial: delete the document that granted access, and the grant disappears on next sync.
348
+
349
+ **Grant resolution order:** The server resolves per-user channel access in two passes — first expand `grant.roles` through `members`, then union with `grant.users` direct grants.
291
350
 
292
- function SpaceList({ onSelectSpace }) {
293
- const viewer = useViewer();
294
- // No explicit ACL on the registry — app-level defaults let all members read
295
- const { database: registry, useLiveQuery } = useFireproof("spaces-registry");
296
- const { docs: spaces } = useLiveQuery("type", { key: "space", descending: true });
351
+ **`allowAnonymous` prevents a footgun.** If `user` is `null` and the function returns without throwing, the runtime checks `allowAnonymous`. If absent or `false`, the write is rejected. This prevents a function that never inspects `user` from silently opening anonymous writes. When `user` is not null, `allowAnonymous` has no effect. `grant.public` grants public _read_; anonymous _write_ requires `allowAnonymous: true` separately.
297
352
 
298
- async function createSpace(name, slug, acl) {
299
- await registry.put({ type: "space", name, slug, acl, userSlug: viewer?.userSlug, createdAt: Date.now() });
300
- // Declares the ACL server-side — owner-only; silently ignored for non-owners
301
- fireproof(slug).applyAcl(acl);
353
+ **Access functions are server-enforced policy code.** Checks should be deterministic over `(doc, oldDoc, user, ctx)` and deny with `throw { forbidden: "reason" }` when violated.
354
+
355
+ ### Example: Workspace chat with channels
356
+
357
+ ```js
358
+ // /access.js
359
+ export function chat(doc, oldDoc, user, ctx) {
360
+ if (!user) throw { forbidden: "authentication required" };
361
+
362
+ if (doc.type === "channel-meta") {
363
+ if (doc.ownerHandle !== user.userHandle) throw { forbidden: "not owner" };
364
+ if (oldDoc && oldDoc.ownerHandle !== user.userHandle) throw { forbidden: "not owner" };
365
+ return {
366
+ channels: [doc._id],
367
+ grant: {
368
+ users: Object.fromEntries([[doc.ownerHandle, [doc._id]], ...doc.memberHandles.map((h) => [h, [doc._id]])]),
369
+ },
370
+ };
302
371
  }
303
372
 
304
- return (
305
- <div>
306
- <ul>
307
- {spaces.map((s) => (
308
- <li key={s._id}>
309
- <button onClick={() => onSelectSpace(s.slug)}>{s.name}</button>
310
- </li>
311
- ))}
312
- </ul>
313
- <button onClick={() => createSpace("Announcements", "space-announcements", { write: ["editors"], delete: ["editors"] })}>
314
- + Create Space
315
- </button>
316
- </div>
317
- );
373
+ if (doc.type === "message") {
374
+ if (doc.userHandle !== user.userHandle) throw { forbidden: "not author" };
375
+ ctx.requireAccess(doc.channelId);
376
+ return { channels: [doc.channelId] };
377
+ }
378
+
379
+ if (doc.type === "channel-invite") {
380
+ if (doc.senderHandle !== user.userHandle) throw { forbidden: "not sender" };
381
+ ctx.requireAccess(doc.channelId);
382
+ return {
383
+ channels: [doc.channelId],
384
+ grant: { users: { [doc.inviteeHandle]: [doc.channelId] } },
385
+ };
386
+ }
387
+
388
+ return {};
318
389
  }
390
+ ```
319
391
 
320
- function SpaceView({ slug, onBack }) {
321
- const { useLiveQuery, useDocument } = useFireproof(slug);
322
- const { doc, merge, submit } = useDocument({ text: "", type: "message" });
323
- const { docs } = useLiveQuery("_id", { descending: true, limit: 50 });
392
+ This single access function handles three document types:
324
393
 
325
- return (
326
- <div>
327
- <button onClick={onBack}>← Back</button>
328
- <ul>
329
- {docs.map((d) => (
330
- <li key={d._id}>{d.text}</li>
331
- ))}
332
- </ul>
333
- <form onSubmit={submit}>
334
- <input value={doc.text} onChange={(e) => merge({ text: e.target.value })} placeholder="Message…" />
335
- <button type="submit">Send</button>
336
- </form>
337
- </div>
338
- );
394
+ - **channel-meta** — owner creates a channel and grants access to listed members
395
+ - **message** — only the author can post, must already have channel access
396
+ - **channel-invite** — any channel member can invite others; deleting the invite revokes the grant
397
+
398
+ ### Example: Anonymous survey with role-gated results
399
+
400
+ ```js
401
+ // /access.js
402
+ export function survey(doc, oldDoc, user, ctx) {
403
+ if (doc.type === "survey-response") {
404
+ if (oldDoc) throw { forbidden: "responses are write-once" };
405
+ return { channels: ["inbound-responses"], allowAnonymous: true };
406
+ }
407
+
408
+ if (doc.type === "survey-config") {
409
+ ctx.requireRole("survey-admin");
410
+ return {
411
+ grant: {
412
+ roles: {
413
+ "survey-admin": ["inbound-responses"],
414
+ "feedback-team": ["inbound-responses"],
415
+ },
416
+ },
417
+ };
418
+ }
419
+
420
+ if (doc.type === "final-results") {
421
+ ctx.requireRole("feedback-team");
422
+ return { channels: [doc._id], grant: { public: [doc._id] } };
423
+ }
424
+
425
+ if (!user) throw { forbidden: "authentication required" };
426
+ return {};
427
+ }
428
+ ```
429
+
430
+ Key patterns:
431
+
432
+ - `allowAnonymous: true` on survey-response lets unauthenticated visitors submit
433
+ - Requiring `doc._id` to be falsy prevents clients from choosing or overwriting response IDs
434
+ - `grant.public` on final-results makes them readable without authentication
435
+ - The **singleton grant doc** pattern (survey-config) wires role-to-channel access in one place
436
+
437
+ ### Multiple databases in one file
438
+
439
+ Each named export gates its own database. A single `/access.js` can gate all databases the app uses:
440
+
441
+ ```js
442
+ // /access.js
443
+ export function chat(doc, oldDoc, user, ctx) {
444
+ if (!user) throw { forbidden: "authentication required" };
445
+ ctx.requireAccess(doc.channelId);
446
+ return { channels: [doc.channelId] };
447
+ }
448
+
449
+ export function notes(doc, oldDoc, user, ctx) {
450
+ if (!user) throw { forbidden: "authentication required" };
451
+ return {};
452
+ }
453
+ ```
454
+
455
+ Databases without a matching named export fall through to `export default` if one exists. If there is no default export either, the database uses the default app-level permissions (no access function).
456
+
457
+ ### Catch-all with `export default`
458
+
459
+ Use `export default` to gate every database without writing a named export for each one. Named exports still take precedence for databases that need custom logic:
460
+
461
+ ```js
462
+ // /access.js
463
+ export function chat(doc, oldDoc, user, ctx) {
464
+ if (!user) throw { forbidden: "authentication required" };
465
+ ctx.requireAccess(doc.channelId);
466
+ return { channels: [doc.channelId] };
467
+ }
468
+
469
+ // Everything else: require authentication, no channel routing
470
+ export default function (doc, oldDoc, user, ctx) {
471
+ if (!user) throw { forbidden: "authentication required" };
472
+ return {};
473
+ }
474
+ ```
475
+
476
+ This is especially useful when an app has many databases or uses hyphenated names (`error-log`, `user-prefs`) that can't be JavaScript identifiers.
477
+
478
+ ### Roles via `members` reduce
479
+
480
+ Roles are not a fixed registry. They are materialized from document contributions:
481
+
482
+ ```js
483
+ // A team-meta doc contributes members to a role
484
+ if (doc.type === "team-meta") {
485
+ ctx.requireRole("admin");
486
+ return {
487
+ members: { [doc.teamId]: doc.memberHandles },
488
+ grant: { roles: { [doc.teamId]: doc.channels } },
489
+ };
490
+ }
491
+
492
+ // A per-employee membership doc contributes one handle
493
+ if (doc.type === "membership") {
494
+ return { members: { [doc.role]: [doc.userHandle] } };
495
+ }
496
+ ```
497
+
498
+ Both patterns produce identical reduced state. Deleting a membership doc removes the user from the role automatically.
499
+
500
+ ### Common `oldDoc` patterns
501
+
502
+ Use `oldDoc` (the previous version of the document) to enforce invariants across updates:
503
+
504
+ ```js
505
+ // New document (create)
506
+ if (oldDoc === null) {
507
+ // create-only logic
508
+ }
509
+
510
+ // Immutable-after-create fields
511
+ if (oldDoc && doc.createdBy !== oldDoc.createdBy) {
512
+ throw { forbidden: "createdBy is immutable" };
513
+ }
514
+
515
+ // Prevent unauthorized ownership transfer
516
+ if (oldDoc && oldDoc.ownerHandle !== user.userHandle) {
517
+ throw { forbidden: "not owner" };
518
+ }
519
+
520
+ // Monotonic version — can only increase
521
+ if (oldDoc && doc.version <= oldDoc.version) {
522
+ throw { forbidden: "version must increase" };
339
523
  }
340
524
  ```
341
525
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "2.4.8",
3
+ "version": "2.4.9",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -30,8 +30,8 @@
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.4.8",
34
- "@vibes.diy/use-vibes-types": "^2.4.8",
33
+ "@vibes.diy/call-ai-v2": "^2.4.9",
34
+ "@vibes.diy/use-vibes-types": "^2.4.9",
35
35
  "arktype": "~2.2.0",
36
36
  "json-schema-faker": "~0.6.1"
37
37
  },