@vibes.diy/prompts 2.4.8 → 2.4.10
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 +239 -55
- 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
|
|
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
|
-
##
|
|
276
|
+
## Access Function (`/access.js`)
|
|
275
277
|
|
|
276
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
)
|
|
288
|
-
|
|
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
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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.
|
|
3
|
+
"version": "2.4.10",
|
|
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.
|
|
34
|
-
"@vibes.diy/use-vibes-types": "^2.4.
|
|
33
|
+
"@vibes.diy/call-ai-v2": "^2.4.10",
|
|
34
|
+
"@vibes.diy/use-vibes-types": "^2.4.10",
|
|
35
35
|
"arktype": "~2.2.0",
|
|
36
36
|
"json-schema-faker": "~0.6.1"
|
|
37
37
|
},
|