@vibes.diy/prompts 12.1.0 → 12.1.2
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/backend.md +120 -22
- package/package.json +4 -4
package/llms/backend.md
CHANGED
|
@@ -81,6 +81,44 @@ than `forbidden` — keep scheduled writes small and well-guarded. `fetch` and
|
|
|
81
81
|
`onChange` never get admin mode: they're user-triggerable, and a handler any
|
|
82
82
|
visitor can invoke must not run owner-privileged.
|
|
83
83
|
|
|
84
|
+
### Taking on the authorization job: `config.fetch.unfilteredReads`
|
|
85
|
+
|
|
86
|
+
Because the `fetch` lane runs as the calling user, a `fetch` handler by default
|
|
87
|
+
reads nothing for a caller with no session, and cannot read an access-fn-bound
|
|
88
|
+
database without an opt-in. Some routes legitimately need to: a calendar feed at
|
|
89
|
+
`GET /_api/faves.ics?t=<token>` is fetched by a calendar app that carries no
|
|
90
|
+
login, and the docs it serves are private to one user. For those, `backend.js`
|
|
91
|
+
can opt in — per database, `fetch` lane only:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
export const config = {
|
|
95
|
+
fetch: {
|
|
96
|
+
unfilteredReads: {
|
|
97
|
+
dbs: ["faves"],
|
|
98
|
+
why: "serves per-user .ics calendar feeds authorized by an unguessable token",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This is an **obligation, not a convenience**. Inside that handler, for those
|
|
105
|
+
databases, you are taking on the authorization job the platform normally does:
|
|
106
|
+
both the anonymous refusal and the access-function scoping come off, so
|
|
107
|
+
`ctx.db.query`/`ctx.db.get` return **every** document in the named databases —
|
|
108
|
+
all users, all channels — to whoever called the route. The legitimate pattern is
|
|
109
|
+
a **capability**: an unguessable, per-user, revocable token in the URL or a
|
|
110
|
+
header, checked before you read, with the response scoped to exactly that
|
|
111
|
+
token's owner. "The URL is hard to guess" only holds if you mint the token
|
|
112
|
+
yourself with real entropy and never print it anywhere else.
|
|
113
|
+
|
|
114
|
+
Two things to design around. The opt-in is scoped to the **lane, not the
|
|
115
|
+
route**: `backend.js` exports one `fetch` handler that routes internally, so
|
|
116
|
+
every path inside it can read those databases — keep the check at the top and
|
|
117
|
+
name as few databases as the feature needs. And it is a **static declaration**:
|
|
118
|
+
`dbs` must be plain string literals and `why` a non-empty string literal saying
|
|
119
|
+
who is authorized and how, or the push is rejected. There is no call-site flag,
|
|
120
|
+
and no other lane can opt in.
|
|
121
|
+
|
|
84
122
|
## ctx — what a handler gets
|
|
85
123
|
|
|
86
124
|
```js
|
|
@@ -89,7 +127,8 @@ ctx.userInfo; // { userHandle } or null — who the handler is acting as
|
|
|
89
127
|
ctx.secrets; // Record<string,string> — the owner's per-vibe secrets; frozen, {} when none
|
|
90
128
|
await ctx.db.put(doc, { db: "notes", id: "optional-id" }); // resolves to the doc id AFTER commit
|
|
91
129
|
await ctx.db.delete(docId, { db: "notes" });
|
|
92
|
-
const docs = await ctx.db.query({ db: "notes" }); //
|
|
130
|
+
const docs = await ctx.db.query({ db: "notes", field: "type", key: "note", limit: 100 }); // a filtered page
|
|
131
|
+
const doc = await ctx.db.get("state:cursor", { db: "notes" }); // one doc by _id, or null
|
|
93
132
|
const text = await ctx.callAI("prompt", { model: "openrouter/auto", max_tokens: 500 }); // server-side AI call
|
|
94
133
|
const res = await ctx.fetch("https://api.example.com/x", { headers }); // outbound HTTP — use this, not bare fetch()
|
|
95
134
|
```
|
|
@@ -99,13 +138,57 @@ const res = await ctx.fetch("https://api.example.com/x", { headers }); // outbou
|
|
|
99
138
|
database that triggered the event is the default.
|
|
100
139
|
- Always `await` db calls; they resolve only after the write commits (or throw
|
|
101
140
|
when the access function denies it).
|
|
102
|
-
- `ctx.db.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
141
|
+
- `ctx.db.get(id, { db })` resolves the single document with that `_id`, or
|
|
142
|
+
`null` when it is missing or deleted. It is the right read whenever you can
|
|
143
|
+
name the document — a tick's state doc, a token lookup, a singleton. Never
|
|
144
|
+
scan a database looking for a doc whose id you already know.
|
|
145
|
+
- `ctx.db.query({ db, field, key, keys, range, prefix, limit, after })` reads a
|
|
146
|
+
**page** of the database's latest docs. Every filter is optional and they
|
|
147
|
+
combine: `field` names the document field to match on, then `key` (one exact
|
|
148
|
+
value), `keys` (any of several), `range` (`[from, to]`) or `prefix` (a string,
|
|
149
|
+
or an array prefix — charwise-encoded, same semantics the client's queries
|
|
150
|
+
use) narrows it. Three edges to get right: **no `field` means no filtering**
|
|
151
|
+
(you get a raw page of everything), a bare `field` with no selector still
|
|
152
|
+
drops docs that lack that field, and `prefix` takes the **raw** value — a
|
|
153
|
+
plain string or array, never anything pre-encoded (the encoding is internal).
|
|
154
|
+
`limit` only clamps the page **down**; the 2000-doc cap stays. Ask for what
|
|
155
|
+
the handler actually needs — reading the whole database and filtering in the
|
|
156
|
+
handler wastes the cap and goes blind as the app grows. Sorting still happens
|
|
157
|
+
in the handler.
|
|
158
|
+
- The result is a real `Array` of docs that **also** destructures — both
|
|
159
|
+
`const docs = await ctx.db.query(...)` and
|
|
160
|
+
`const { docs, next } = await ctx.db.query(...)` work.
|
|
161
|
+
- **Paginate on `next`, never on emptiness.** `next` is computed from the raw
|
|
162
|
+
page *before* filtering, and only when that page came back full — so a
|
|
163
|
+
narrowly filtered page can be **empty and still carry `next`**. Stopping at
|
|
164
|
+
the first empty page silently truncates the read:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
let after;
|
|
168
|
+
do {
|
|
169
|
+
const page = await ctx.db.query({ db: "faves", field: "type", key: "favorite", limit: 500, after });
|
|
170
|
+
for (const doc of page) {
|
|
171
|
+
// …
|
|
172
|
+
}
|
|
173
|
+
after = page.next;
|
|
174
|
+
} while (after);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`next` rides on the array itself, so read it **before** transforming: any
|
|
178
|
+
array operation (`.filter()`, `.map()`, spread) returns a plain array with no
|
|
179
|
+
`next`. On the last page the property is absent, not undefined — `"next" in
|
|
180
|
+
page` is `false`.
|
|
181
|
+
- Both reads are read-ACL-gated as the acting identity, through the same gate:
|
|
182
|
+
anonymous `fetch` callers are denied, and in `fetch`/`onChange` a database
|
|
183
|
+
bound to an access function cannot be read at all (keep those lanes' read
|
|
184
|
+
databases on plain ACLs). In `scheduled` (admin mode) reads are
|
|
185
|
+
**unfiltered** — access-fn-bound databases return every doc, across all users
|
|
186
|
+
and channels. Made for `scheduled` sweeps: read, decide, then `put`/`delete`.
|
|
187
|
+
- The one exception to that last sentence is the declared opt-in above,
|
|
188
|
+
`config.fetch.unfilteredReads` — a `fetch` handler that names a database there
|
|
189
|
+
reads it unfiltered, anonymous callers included, and owes the authorization
|
|
190
|
+
check itself. Nothing changes for writes, or for any lane or database you
|
|
191
|
+
didn't name.
|
|
109
192
|
- `ctx.secrets` carries the owner's per-vibe secrets, set in the vibe's settings
|
|
110
193
|
page or with `vibes-diy secrets set KEY`. Use it to verify inbound credentials —
|
|
111
194
|
a webhook's shared secret or token — or to authenticate outbound calls
|
|
@@ -177,8 +260,8 @@ const SNAPSHOT = [
|
|
|
177
260
|
];
|
|
178
261
|
|
|
179
262
|
async function ensureSnapshot(ctx) {
|
|
180
|
-
|
|
181
|
-
if (
|
|
263
|
+
// Point-read by id — the snapshot has a known `_id`, so there's nothing to scan for.
|
|
264
|
+
if (await ctx.db.get(SNAPSHOT[0]._id, { db: "nodeReleases" })) return;
|
|
182
265
|
for (const doc of SNAPSHOT) await ctx.db.put(doc, { db: "nodeReleases" });
|
|
183
266
|
}
|
|
184
267
|
|
|
@@ -190,18 +273,19 @@ function unchanged(existing, next) {
|
|
|
190
273
|
return Object.keys(next).every((k) => existing[k] === next[k]);
|
|
191
274
|
}
|
|
192
275
|
|
|
193
|
-
|
|
194
|
-
|
|
276
|
+
// The diff source is a POINT-READ per incoming id, not a query over the stored
|
|
277
|
+
// set: the work is bounded by what the feed sent (≤20 docs), never by how big
|
|
278
|
+
// the database has grown — so a db that outgrows a query page can never
|
|
279
|
+
// silently eject rows from the diff and turn this tick into a rewrite loop.
|
|
280
|
+
async function putIfChanged(ctx, next) {
|
|
281
|
+
const existing = await ctx.db.get(next._id, { db: "nodeReleases" });
|
|
282
|
+
if (unchanged(existing, next)) return;
|
|
195
283
|
await ctx.db.put(next, { db: "nodeReleases" });
|
|
196
284
|
}
|
|
197
285
|
|
|
198
286
|
export async function scheduled(event, ctx) {
|
|
199
287
|
await ensureSnapshot(ctx);
|
|
200
288
|
|
|
201
|
-
// Read current docs once so each write can diff against what's already stored.
|
|
202
|
-
const current = await ctx.db.query({ db: "nodeReleases" });
|
|
203
|
-
const byId = new Map(current.map((doc) => [doc._id, doc]));
|
|
204
|
-
|
|
205
289
|
// One request, no retry storm; ctx.fetch enforces 15s/10MB and egress caps.
|
|
206
290
|
const response = await ctx.fetch("https://api.github.com/repos/nodejs/node/releases?per_page=20", {
|
|
207
291
|
headers: { accept: "application/vnd.github+json" },
|
|
@@ -215,7 +299,7 @@ export async function scheduled(event, ctx) {
|
|
|
215
299
|
if (denied?.vibesEgressDenied === true) {
|
|
216
300
|
// A real state change — putIfChanged writes once on entering denial, then
|
|
217
301
|
// nothing while it persists. No per-tick timestamp, so no churn.
|
|
218
|
-
await putIfChanged(ctx,
|
|
302
|
+
await putIfChanged(ctx, {
|
|
219
303
|
_id: "refresh:status",
|
|
220
304
|
type: "refreshStatus",
|
|
221
305
|
state: "egress-denied",
|
|
@@ -233,7 +317,7 @@ export async function scheduled(event, ctx) {
|
|
|
233
317
|
for (const row of rows.slice(0, 20)) {
|
|
234
318
|
if (!row?.id || !row?.html_url || !row?.published_at) continue;
|
|
235
319
|
// Stable per-item `_id` + diff: a row whose content is identical is skipped.
|
|
236
|
-
await putIfChanged(ctx,
|
|
320
|
+
await putIfChanged(ctx, {
|
|
237
321
|
_id: `release:github-${row.id}`,
|
|
238
322
|
type: "release",
|
|
239
323
|
title: String(row.name || row.tag_name || "Node.js release"),
|
|
@@ -243,7 +327,7 @@ export async function scheduled(event, ctx) {
|
|
|
243
327
|
});
|
|
244
328
|
}
|
|
245
329
|
|
|
246
|
-
await putIfChanged(ctx,
|
|
330
|
+
await putIfChanged(ctx, {
|
|
247
331
|
_id: "refresh:status",
|
|
248
332
|
type: "refreshStatus",
|
|
249
333
|
state: "fresh",
|
|
@@ -294,15 +378,29 @@ down. Catch that exact 403 shape and show the saved snapshot with honest copy
|
|
|
294
378
|
render a broken screen, or silently label saved data as fresh.
|
|
295
379
|
|
|
296
380
|
**Zero-churn discipline for scheduled ticks.** A cron runs forever, so it must
|
|
297
|
-
not write forever.
|
|
298
|
-
|
|
299
|
-
|
|
381
|
+
not write forever. Give every item a stable `_id` and `put` **only** the rows
|
|
382
|
+
whose content actually changed — the `putIfChanged` diff above, which
|
|
383
|
+
point-reads each id it is about to write. Diff against **point-reads of the
|
|
384
|
+
incoming ids**, so the work is bounded by what arrived this tick, not by how
|
|
385
|
+
big the database has grown; reach for a keyed `ctx.db.query` only when the
|
|
386
|
+
stored set itself is the target (and then walk `next` to exhaustion before
|
|
387
|
+
diffing — a one-page diff source re-puts everything the page missed, forever). Never stamp a per-tick `checkedAt`/`updatedAt`/run
|
|
300
388
|
counter onto a doc: it makes every put look new, and each spurious put mints a
|
|
301
389
|
fresh revision and a sync-feed row that re-tickles every replica on every tick.
|
|
302
390
|
A failed or empty upstream fetch aborts with no writes, preserving the last-good
|
|
303
391
|
rows. Only a genuine content change — a new or updated row, a real state
|
|
304
392
|
transition — earns a write.
|
|
305
393
|
|
|
394
|
+
**A tick's own memory is a document it reads by id.** Whatever a scheduled
|
|
395
|
+
handler needs to know about what it already did — a cursor, a last-synced
|
|
396
|
+
marker, a per-run state — lives in one doc with a fixed `_id`, read with
|
|
397
|
+
`ctx.db.get(id, { db })`. Never re-derive that answer by scanning the database:
|
|
398
|
+
a sweep that diffs against its own whole-db read goes blind the moment the
|
|
399
|
+
database outgrows one page, and then rewrites everything on every tick, forever.
|
|
400
|
+
Keep a copy of the state in module scope as well, and read a **missing or failed**
|
|
401
|
+
state read as *"don't know — skip this tick"*, never as *"nothing has been done
|
|
402
|
+
yet"*: that second reading is exactly what restarts the endless rewrite.
|
|
403
|
+
|
|
306
404
|
## fetch — the app's HTTP endpoint
|
|
307
405
|
|
|
308
406
|
Runs for requests to the app's `/_api` route. The request path is rooted after
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "12.1.
|
|
3
|
+
"version": "12.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"description": "",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"license": "Apache-2.0",
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@adviser/cement": "~0.5.34",
|
|
27
|
-
"@vibes.diy/call-ai-v2": "^12.1.
|
|
28
|
-
"@vibes.diy/identity": "^12.1.
|
|
29
|
-
"@vibes.diy/use-vibes-types": "^12.1.
|
|
27
|
+
"@vibes.diy/call-ai-v2": "^12.1.2",
|
|
28
|
+
"@vibes.diy/identity": "^12.1.2",
|
|
29
|
+
"@vibes.diy/use-vibes-types": "^12.1.2",
|
|
30
30
|
"arktype": "~2.2.3",
|
|
31
31
|
"json-schema-faker": "~0.6.2"
|
|
32
32
|
},
|