@vibes.diy/api-svc 2.4.8 → 2.4.11

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 (38) hide show
  1. package/cf-serve.js +16 -0
  2. package/cf-serve.js.map +1 -1
  3. package/create-handler.d.ts +10 -1
  4. package/create-handler.js +1 -0
  5. package/create-handler.js.map +1 -1
  6. package/index.d.ts +1 -0
  7. package/index.js +1 -0
  8. package/index.js.map +1 -1
  9. package/intern/prompt-assembly.d.ts +21 -0
  10. package/intern/prompt-assembly.js +194 -0
  11. package/intern/prompt-assembly.js.map +1 -0
  12. package/intern/prompt-asset-fetch.d.ts +6 -0
  13. package/intern/prompt-asset-fetch.js +18 -0
  14. package/intern/prompt-asset-fetch.js.map +1 -0
  15. package/intern/prompt-streaming.d.ts +48 -0
  16. package/intern/prompt-streaming.js +139 -0
  17. package/intern/prompt-streaming.js.map +1 -0
  18. package/package.json +11 -11
  19. package/public/access-function.d.ts +1 -0
  20. package/public/access-function.js +32 -0
  21. package/public/access-function.js.map +1 -1
  22. package/public/app-documents.js +316 -17
  23. package/public/app-documents.js.map +1 -1
  24. package/public/channel-read-filter.d.ts +9 -0
  25. package/public/channel-read-filter.js +22 -0
  26. package/public/channel-read-filter.js.map +1 -0
  27. package/public/ensure-app-slug-item.js +226 -1
  28. package/public/ensure-app-slug-item.js.map +1 -1
  29. package/public/grant-reduce.d.ts +25 -0
  30. package/public/grant-reduce.js +130 -0
  31. package/public/grant-reduce.js.map +1 -0
  32. package/public/prompt-chat-section.d.ts +11 -70
  33. package/public/prompt-chat-section.js +12 -343
  34. package/public/prompt-chat-section.js.map +1 -1
  35. package/public/report-top-vibes-by-members.js +9 -2
  36. package/public/report-top-vibes-by-members.js.map +1 -1
  37. package/types.d.ts +15 -1
  38. package/types.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/api-svc",
3
- "version": "2.4.8",
3
+ "version": "2.4.11",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "scripts": {
@@ -25,18 +25,18 @@
25
25
  "@libsql/client": "~0.17.3",
26
26
  "@neondatabase/serverless": "~1.1.0",
27
27
  "@noble/hashes": "~2.2.0",
28
- "@vibes.diy/api-impl": "2.4.8",
29
- "@vibes.diy/api-pkg": "2.4.8",
30
- "@vibes.diy/api-sql": "2.4.8",
31
- "@vibes.diy/api-types": "2.4.8",
32
- "@vibes.diy/call-ai-v2": "2.4.8",
33
- "@vibes.diy/prompts": "2.4.8",
34
- "@vibes.diy/use-vibes-base": "2.4.8",
35
- "@vibes.diy/vibe-db-explorer": "2.4.8",
36
- "@vibes.diy/vibe-types": "2.4.8",
28
+ "@vibes.diy/api-impl": "2.4.11",
29
+ "@vibes.diy/api-pkg": "2.4.11",
30
+ "@vibes.diy/api-sql": "2.4.11",
31
+ "@vibes.diy/api-types": "2.4.11",
32
+ "@vibes.diy/call-ai-v2": "2.4.11",
33
+ "@vibes.diy/prompts": "2.4.11",
34
+ "@vibes.diy/use-vibes-base": "2.4.11",
35
+ "@vibes.diy/vibe-db-explorer": "2.4.11",
36
+ "@vibes.diy/vibe-types": "2.4.11",
37
37
  "acorn": "~8.16.0",
38
38
  "arktype": "~2.2.0",
39
- "call-ai": "2.4.8",
39
+ "call-ai": "2.4.11",
40
40
  "cookie": "~1.1.1",
41
41
  "drizzle-orm": "~0.45.2",
42
42
  "jose": "~6.2.2",
@@ -2,6 +2,7 @@ import type { AccessDescriptor, AccessFunction, Helpers, UserContext } from "@vi
2
2
  export type { AccessDescriptor, AccessFunction, Helpers, UserContext };
3
3
  export declare function enforceAllowAnonymous(result: AccessDescriptor, user: UserContext | null): void;
4
4
  export declare function makeHelpers(user: UserContext | null): Helpers;
5
+ export declare function extractExportSource(fullSource: string, bindingDbName: string): string | undefined;
5
6
  export declare class ForbiddenError extends Error {
6
7
  readonly forbidden: string;
7
8
  constructor(reason: string);
@@ -17,6 +17,38 @@ export function makeHelpers(user) {
17
17
  },
18
18
  };
19
19
  }
20
+ export function extractExportSource(fullSource, bindingDbName) {
21
+ let pattern;
22
+ if (bindingDbName === "*") {
23
+ pattern = /export\s+default\s+(?:function\s*(?:\w+\s*)?\([^)]*\)\s*\{|\([^)]*\)\s*=>\s*\{|\w+\s*=>\s*\{)/;
24
+ }
25
+ else {
26
+ pattern = new RegExp(`export\\s+function\\s+${bindingDbName}\\s*\\([^)]*\\)\\s*\\{`);
27
+ }
28
+ const match = fullSource.match(pattern);
29
+ if (!match)
30
+ return undefined;
31
+ const start = match.index;
32
+ if (start === undefined)
33
+ return undefined;
34
+ let depth = 0;
35
+ let end = start;
36
+ for (let i = start; i < fullSource.length; i++) {
37
+ if (fullSource[i] === "{")
38
+ depth++;
39
+ if (fullSource[i] === "}") {
40
+ depth--;
41
+ if (depth === 0) {
42
+ end = i + 1;
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ let extracted = fullSource.slice(start, end).replace(/^export\s+/, "");
48
+ if (bindingDbName === "*")
49
+ extracted = extracted.replace(/^default\s+/, "");
50
+ return extracted;
51
+ }
20
52
  export class ForbiddenError extends Error {
21
53
  forbidden;
22
54
  constructor(reason) {
@@ -1 +1 @@
1
- {"version":3,"file":"access-function.js","sourceRoot":"","sources":["../../jsr/public/access-function.ts"],"names":[],"mappings":"AA4BA,MAAM,UAAU,qBAAqB,CAAC,MAAwB,EAAE,IAAwB;IACtF,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAC5C,MAAM,IAAI,cAAc,CAAC,yBAAyB,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAQD,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,OAAO;QACL,aAAa,CAAC,SAAiB;YAC7B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,MAAM,IAAI,cAAc,CAAC,mBAAmB,SAAS,EAAE,CAAC,CAAC;YAC3D,CAAC;QAGH,CAAC;QACD,WAAW,CAAC,QAAgB;YAC1B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,MAAM,IAAI,cAAc,CAAC,gBAAgB,QAAQ,EAAE,CAAC,CAAC;YACvD,CAAC;QAGH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,SAAS,CAAS;IAE3B,YAAY,MAAc;QACxB,KAAK,CAAC,MAAM,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;IAC1B,CAAC;CACF"}
1
+ {"version":3,"file":"access-function.js","sourceRoot":"","sources":["../../jsr/public/access-function.ts"],"names":[],"mappings":"AA4BA,MAAM,UAAU,qBAAqB,CAAC,MAAwB,EAAE,IAAwB;IACtF,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;QAC5C,MAAM,IAAI,cAAc,CAAC,yBAAyB,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAQD,MAAM,UAAU,WAAW,CAAC,IAAwB;IAClD,OAAO;QACL,aAAa,CAAC,SAAiB;YAC7B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,MAAM,IAAI,cAAc,CAAC,mBAAmB,SAAS,EAAE,CAAC,CAAC;YAC3D,CAAC;QAGH,CAAC;QACD,WAAW,CAAC,QAAgB;YAC1B,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,MAAM,IAAI,cAAc,CAAC,gBAAgB,QAAQ,EAAE,CAAC,CAAC;YACvD,CAAC;QAGH,CAAC;KACF,CAAC;AACJ,CAAC;AAQD,MAAM,UAAU,mBAAmB,CAAC,UAAkB,EAAE,aAAqB;IAC3E,IAAI,OAAe,CAAC;IACpB,IAAI,aAAa,KAAK,GAAG,EAAE,CAAC;QAC1B,OAAO,GAAG,+FAA+F,CAAC;IAC5G,CAAC;SAAM,CAAC;QACN,OAAO,GAAG,IAAI,MAAM,CAAC,yBAAyB,aAAa,wBAAwB,CAAC,CAAC;IACvF,CAAC;IACD,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAC1B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC1C,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,KAAK,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC/C,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG;YAAE,KAAK,EAAE,CAAC;QACnC,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;YAC1B,KAAK,EAAE,CAAC;YACR,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;gBAChB,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;gBACZ,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IACD,IAAI,SAAS,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;IACvE,IAAI,aAAa,KAAK,GAAG;QAAE,SAAS,GAAG,SAAS,CAAC,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAC5E,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IAC9B,SAAS,CAAS;IAE3B,YAAY,MAAc;QACxB,KAAK,CAAC,MAAM,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;IAC1B,CAAC;CACF"}
@@ -1,12 +1,15 @@
1
- import { Result, Option, EventoResult } from "@adviser/cement";
1
+ import { Result, Option, EventoResult, exception2Result } from "@adviser/cement";
2
2
  import { reqPutDoc, reqGetDoc, reqQueryDocs, reqDeleteDoc, reqSubscribeDocs, reqListDbNames, reqListDmThreads, reqMarkDmRead, COMMENTS_DB_NAME, isDirectChannel, directChannelParticipants, } from "@vibes.diy/api-types";
3
3
  import { unwrapMsgBase } from "../unwrap-msg-base.js";
4
4
  import { checkAuth, optAuth } from "../check-auth.js";
5
- import { eq, and, sql, inArray } from "drizzle-orm";
5
+ import { eq, and, sql, inArray, desc } from "drizzle-orm";
6
6
  import { max } from "drizzle-orm/sql";
7
7
  import { type } from "arktype";
8
8
  import { checkDocAccess, canRead, isPublicReadable } from "./access-helpers.js";
9
+ import { enforceAllowAnonymous, ForbiddenError, extractExportSource } from "./access-function.js";
9
10
  import { aclAllows, resolveDbAcl, checkDirectChannelAccess } from "./db-acl-resolver.js";
11
+ import { GrantReduce, extractContribution } from "./grant-reduce.js";
12
+ import { filterDocsByChannel } from "./channel-read-filter.js";
10
13
  import { mintFilesUrls, isFileMeta } from "./files-url-mint.js";
11
14
  async function readAllowed(vctx, acl, access, appSlug, ownerHandle) {
12
15
  if (acl?.read !== undefined)
@@ -54,18 +57,22 @@ export const putDocEvento = {
54
57
  }
55
58
  return Result.Ok(Option.Some({ ...msg, payload: ret }));
56
59
  }),
57
- handle: checkAuth(async (ctx) => {
60
+ handle: optAuth(async (ctx) => {
58
61
  const req = ctx.validated.payload;
59
62
  const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
60
- const userId = req._auth.verifiedAuth.claims.userId;
63
+ const userId = req._auth?.verifiedAuth.claims.userId ?? null;
61
64
  if (isDirectChannel(req.ownerHandle)) {
65
+ if (!userId) {
66
+ await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
67
+ return Result.Ok(EventoResult.Continue);
68
+ }
62
69
  const rAccess = await checkDirectChannelAccess(vctx, req.ownerHandle, userId);
63
70
  if (rAccess.isErr() || !rAccess.Ok()) {
64
71
  await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
65
72
  return Result.Ok(EventoResult.Continue);
66
73
  }
67
74
  }
68
- else {
75
+ else if (userId) {
69
76
  const access = await checkDocAccess(vctx, userId, req.appSlug, req.ownerHandle);
70
77
  const rAcl = await resolveDbAcl(vctx, req.ownerHandle, req.appSlug, req.dbName);
71
78
  if (rAcl.isErr()) {
@@ -86,8 +93,114 @@ export const putDocEvento = {
86
93
  });
87
94
  return Result.Ok(EventoResult.Continue);
88
95
  }
89
- const now = new Date().toISOString();
96
+ let accessResult;
97
+ const tAfb = vctx.sql.tables.accessFunctionBindings;
98
+ const afbRow = await vctx.sql.db
99
+ .select({ accessFnCid: tAfb.accessFnCid, accessFnAssetUri: tAfb.accessFnAssetUri, dbName: tAfb.dbName })
100
+ .from(tAfb)
101
+ .where(and(eq(tAfb.userSlug, req.ownerHandle), eq(tAfb.appSlug, req.appSlug), inArray(tAfb.dbName, [req.dbName, "*"])))
102
+ .orderBy(sql `CASE WHEN ${tAfb.dbName} = ${req.dbName} THEN 0 ELSE 1 END`)
103
+ .limit(1)
104
+ .then((r) => r[0]);
105
+ if (!userId && !afbRow?.accessFnCid) {
106
+ await ctx.send.send(ctx, {
107
+ type: "vibes.diy.res-error",
108
+ error: { message: "Access denied" },
109
+ });
110
+ return Result.Ok(EventoResult.Continue);
111
+ }
90
112
  const docId = req.docId ?? vctx.sthis.timeOrderedNextId().str;
113
+ if (afbRow?.accessFnCid && vctx.invokeAccessFn) {
114
+ const fnCid = afbRow.accessFnCid;
115
+ const t_usb = vctx.sql.tables.handleBinding;
116
+ const writerRow = userId
117
+ ? await vctx.sql.db
118
+ .select({ handle: t_usb.handle })
119
+ .from(t_usb)
120
+ .where(eq(t_usb.userId, userId))
121
+ .limit(1)
122
+ .then((r) => r[0])
123
+ : undefined;
124
+ const userContext = writerRow?.handle ? { userHandle: writerRow.handle } : null;
125
+ let oldDoc = null;
126
+ if (req.docId) {
127
+ const tDocs = vctx.sql.tables.appDocuments;
128
+ const existing = await vctx.sql.db
129
+ .select({ data: tDocs.data })
130
+ .from(tDocs)
131
+ .where(and(eq(tDocs.ownerHandle, req.ownerHandle), eq(tDocs.appSlug, req.appSlug), eq(tDocs.dbName, req.dbName), eq(tDocs.docId, req.docId)))
132
+ .orderBy(desc(tDocs.seq))
133
+ .limit(1)
134
+ .then((r) => r[0]);
135
+ oldDoc = existing?.data ?? null;
136
+ }
137
+ let accessFnSource;
138
+ if (afbRow.accessFnAssetUri) {
139
+ const rFetch = await vctx.storage.fetch(afbRow.accessFnAssetUri);
140
+ if (rFetch.type === "fetch.ok") {
141
+ const reader = rFetch.data.getReader();
142
+ const chunks = [];
143
+ for (;;) {
144
+ const { done, value } = await reader.read();
145
+ if (done)
146
+ break;
147
+ if (value)
148
+ chunks.push(value);
149
+ }
150
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
151
+ const merged = new Uint8Array(totalLength);
152
+ let offset = 0;
153
+ for (const chunk of chunks) {
154
+ merged.set(chunk, offset);
155
+ offset += chunk.length;
156
+ }
157
+ const rawSource = new TextDecoder().decode(merged);
158
+ accessFnSource = extractExportSource(rawSource, afbRow.dbName) ?? rawSource;
159
+ }
160
+ }
161
+ const tOutputs = vctx.sql.tables.accessFnOutputs;
162
+ const storedOutputs = await vctx.sql.db
163
+ .select({ docId: tOutputs.docId, output: tOutputs.output })
164
+ .from(tOutputs)
165
+ .where(and(eq(tOutputs.userSlug, req.ownerHandle), eq(tOutputs.appSlug, req.appSlug), eq(tOutputs.dbName, req.dbName), eq(tOutputs.fnCid, fnCid), eq(tOutputs.hasGrants, 1)));
166
+ const reduce = new GrantReduce();
167
+ for (const row of storedOutputs) {
168
+ reduce.addDoc(row.docId, extractContribution(JSON.parse(row.output)));
169
+ }
170
+ const grantState = {
171
+ members: Object.fromEntries(Array.from(reduce.effectiveMembers).map(([k, v]) => [k, Array.from(v)])),
172
+ roleGrants: Object.fromEntries(Array.from(reduce.roleGrants).map(([k, v]) => [k, Array.from(v)])),
173
+ userGrants: Object.fromEntries(Array.from(reduce.userGrants).map(([k, v]) => [k, Array.from(v)])),
174
+ };
175
+ const invokeResult = await vctx.invokeAccessFn({
176
+ cid: fnCid,
177
+ doc: { ...req.doc, _id: docId },
178
+ oldDoc,
179
+ user: userContext,
180
+ source: accessFnSource,
181
+ grantState,
182
+ });
183
+ if ("forbidden" in invokeResult) {
184
+ await ctx.send.send(ctx, {
185
+ type: "vibes.diy.res-error",
186
+ error: { message: invokeResult.forbidden },
187
+ });
188
+ return Result.Ok(EventoResult.Continue);
189
+ }
190
+ try {
191
+ enforceAllowAnonymous(invokeResult, userContext);
192
+ }
193
+ catch (err) {
194
+ const reason = err instanceof ForbiddenError ? err.forbidden : String(err);
195
+ await ctx.send.send(ctx, {
196
+ type: "vibes.diy.res-error",
197
+ error: { message: reason },
198
+ });
199
+ return Result.Ok(EventoResult.Continue);
200
+ }
201
+ accessResult = invokeResult;
202
+ }
203
+ const now = new Date().toISOString();
91
204
  const dbName = req.dbName;
92
205
  const t = vctx.sql.tables.appDocuments;
93
206
  const maxSeqResult = await vctx.sql.db
@@ -102,7 +215,7 @@ export const putDocEvento = {
102
215
  dbName,
103
216
  docId,
104
217
  seq: nextSeq,
105
- userId,
218
+ userId: userId ?? "unknown",
106
219
  data: req.doc,
107
220
  deleted: 0,
108
221
  created: now,
@@ -122,14 +235,14 @@ export const putDocEvento = {
122
235
  const senderRow = await vctx.sql.db
123
236
  .select({ handle: t_usb.handle })
124
237
  .from(t_usb)
125
- .where(and(eq(t_usb.userId, userId), inArray(t_usb.handle, participants)))
238
+ .where(and(eq(t_usb.userId, userId ?? ""), inArray(t_usb.handle, participants)))
126
239
  .then((r) => r[0]);
127
240
  const senderUserSlug = senderRow?.handle ?? "";
128
241
  const recipientUserSlug = participants.find((h) => h !== senderUserSlug) ?? participants[1];
129
242
  await vctx.postQueue({
130
243
  payload: {
131
244
  type: "vibes.diy.evt-dm-received",
132
- senderUserId: userId,
245
+ senderUserId: userId ?? "",
133
246
  senderUserSlug,
134
247
  recipientUserSlug,
135
248
  channelUserSlug: req.ownerHandle,
@@ -160,12 +273,12 @@ export const putDocEvento = {
160
273
  await vctx.postQueue({
161
274
  payload: {
162
275
  type: "vibes.diy.evt-comment-posted",
163
- userId,
276
+ userId: userId ?? "unknown",
164
277
  ownerHandle: req.ownerHandle,
165
278
  appSlug: req.appSlug,
166
279
  docId,
167
280
  created: now,
168
- email: req._auth.verifiedAuth.claims.params.email,
281
+ email: req._auth?.verifiedAuth.claims.params.email ?? "unknown",
169
282
  },
170
283
  tid: "queue-event",
171
284
  src: "putDoc",
@@ -174,9 +287,56 @@ export const putDocEvento = {
174
287
  });
175
288
  }
176
289
  if (vctx.notifyDocChanged) {
177
- vctx
178
- .notifyDocChanged({ ownerHandle: req.ownerHandle, appSlug: req.appSlug, dbName, docId }, clientWsSend(ctx).connId)
179
- .catch((e) => console.error("DocNotify error:", e));
290
+ if (accessResult?.channels?.length) {
291
+ for (const channel of accessResult.channels) {
292
+ vctx
293
+ .notifyDocChanged({ ownerHandle: req.ownerHandle, appSlug: req.appSlug, dbName: channel, docId }, clientWsSend(ctx).connId)
294
+ .catch((e) => console.error("DocNotify channel error:", e));
295
+ }
296
+ }
297
+ else {
298
+ vctx
299
+ .notifyDocChanged({ ownerHandle: req.ownerHandle, appSlug: req.appSlug, dbName, docId }, clientWsSend(ctx).connId)
300
+ .catch((e) => console.error("DocNotify error:", e));
301
+ }
302
+ }
303
+ if (accessResult && !("forbidden" in accessResult) && afbRow?.accessFnCid) {
304
+ const tOutputs = vctx.sql.tables.accessFnOutputs;
305
+ const outputHasGrants = (accessResult.members && Object.keys(accessResult.members).length > 0) ||
306
+ (accessResult.grant?.users && Object.keys(accessResult.grant.users).length > 0) ||
307
+ (accessResult.grant?.roles && Object.keys(accessResult.grant.roles).length > 0) ||
308
+ (accessResult.grant?.public && accessResult.grant.public.length > 0)
309
+ ? 1
310
+ : 0;
311
+ const rUpsert = await exception2Result(() => vctx.sql.db
312
+ .insert(tOutputs)
313
+ .values({
314
+ userSlug: req.ownerHandle,
315
+ appSlug: req.appSlug,
316
+ dbName: req.dbName,
317
+ docId,
318
+ fnCid: afbRow.accessFnCid,
319
+ output: JSON.stringify(accessResult),
320
+ hasGrants: outputHasGrants,
321
+ })
322
+ .onConflictDoUpdate({
323
+ target: [tOutputs.userSlug, tOutputs.appSlug, tOutputs.dbName, tOutputs.docId],
324
+ set: {
325
+ fnCid: afbRow.accessFnCid,
326
+ output: JSON.stringify(accessResult),
327
+ hasGrants: outputHasGrants,
328
+ },
329
+ }));
330
+ if (rUpsert.isErr()) {
331
+ console.error("AccessFnOutputs upsert failed:", rUpsert.Err());
332
+ if (outputHasGrants === 1) {
333
+ await ctx.send.send(ctx, {
334
+ type: "vibes.diy.res-error",
335
+ error: { message: "grant storage failed — retry the write" },
336
+ });
337
+ return Result.Ok(EventoResult.Continue);
338
+ }
339
+ }
180
340
  }
181
341
  await ctx.send.send(ctx, {
182
342
  type: "vibes.diy.res-put-doc",
@@ -225,6 +385,59 @@ export const getDocEvento = {
225
385
  });
226
386
  return Result.Ok(EventoResult.Continue);
227
387
  }
388
+ const tAfbG = vctx.sql.tables.accessFunctionBindings;
389
+ const afbRowG = await vctx.sql.db
390
+ .select({ accessFnCid: tAfbG.accessFnCid })
391
+ .from(tAfbG)
392
+ .where(and(eq(tAfbG.userSlug, req.ownerHandle), eq(tAfbG.appSlug, req.appSlug), inArray(tAfbG.dbName, [req.dbName, "*"])))
393
+ .orderBy(sql `CASE WHEN ${tAfbG.dbName} = ${req.dbName} THEN 0 ELSE 1 END`)
394
+ .limit(1)
395
+ .then((r) => r[0]);
396
+ if (afbRowG?.accessFnCid) {
397
+ const tOutputsG = vctx.sql.tables.accessFnOutputs;
398
+ const docOutput = await vctx.sql.db
399
+ .select({ output: tOutputsG.output })
400
+ .from(tOutputsG)
401
+ .where(and(eq(tOutputsG.userSlug, req.ownerHandle), eq(tOutputsG.appSlug, req.appSlug), eq(tOutputsG.dbName, req.dbName), eq(tOutputsG.docId, req.docId), eq(tOutputsG.fnCid, afbRowG.accessFnCid)))
402
+ .limit(1)
403
+ .then((r) => r[0]);
404
+ const parsed = docOutput ? JSON.parse(docOutput.output) : undefined;
405
+ const docChannels = parsed?.channels;
406
+ if (docChannels === undefined || docChannels.length === 0) {
407
+ await ctx.send.send(ctx, {
408
+ type: "vibes.diy.res-get-doc",
409
+ status: "not-found",
410
+ id: req.docId,
411
+ });
412
+ return Result.Ok(EventoResult.Continue);
413
+ }
414
+ const grantOutputs = await vctx.sql.db
415
+ .select({ docId: tOutputsG.docId, output: tOutputsG.output })
416
+ .from(tOutputsG)
417
+ .where(and(eq(tOutputsG.userSlug, req.ownerHandle), eq(tOutputsG.appSlug, req.appSlug), eq(tOutputsG.dbName, req.dbName), eq(tOutputsG.fnCid, afbRowG.accessFnCid), eq(tOutputsG.hasGrants, 1)));
418
+ const reduce = new GrantReduce();
419
+ for (const r of grantOutputs) {
420
+ reduce.addDoc(r.docId, extractContribution(JSON.parse(r.output)));
421
+ }
422
+ const userHandle = req._auth
423
+ ? await vctx.sql.db
424
+ .select({ handle: vctx.sql.tables.handleBinding.handle })
425
+ .from(vctx.sql.tables.handleBinding)
426
+ .where(eq(vctx.sql.tables.handleBinding.userId, req._auth.verifiedAuth.claims.userId))
427
+ .limit(1)
428
+ .then((r) => r[0]?.handle ?? null)
429
+ : null;
430
+ const effectiveChannels = userHandle !== null ? reduce.resolveEffectiveChannels(userHandle) : new Set();
431
+ const hasAccess = docChannels.some((ch) => effectiveChannels.has(ch) || reduce.publicChannels.has(ch));
432
+ if (!hasAccess) {
433
+ await ctx.send.send(ctx, {
434
+ type: "vibes.diy.res-get-doc",
435
+ status: "not-found",
436
+ id: req.docId,
437
+ });
438
+ return Result.Ok(EventoResult.Continue);
439
+ }
440
+ }
228
441
  const doc = mintFilesUrls(row.data, {
229
442
  ownerHandle: req.ownerHandle,
230
443
  appSlug: req.appSlug,
@@ -335,7 +548,44 @@ export const queryDocsEvento = {
335
548
  ...doc,
336
549
  });
337
550
  }
338
- const filteredDocs = applyQueryFilter(docs, req.filter);
551
+ const tAfbQ = vctx.sql.tables.accessFunctionBindings;
552
+ const afbRowQ = await vctx.sql.db
553
+ .select({ accessFnCid: tAfbQ.accessFnCid })
554
+ .from(tAfbQ)
555
+ .where(and(eq(tAfbQ.userSlug, req.ownerHandle), eq(tAfbQ.appSlug, req.appSlug), inArray(tAfbQ.dbName, [req.dbName, "*"])))
556
+ .orderBy(sql `CASE WHEN ${tAfbQ.dbName} = ${req.dbName} THEN 0 ELSE 1 END`)
557
+ .limit(1)
558
+ .then((r) => r[0]);
559
+ let channelFilteredDocs = docs;
560
+ if (afbRowQ?.accessFnCid) {
561
+ const tOutputsQ = vctx.sql.tables.accessFnOutputs;
562
+ const allOutputs = await vctx.sql.db
563
+ .select({ docId: tOutputsQ.docId, output: tOutputsQ.output })
564
+ .from(tOutputsQ)
565
+ .where(and(eq(tOutputsQ.userSlug, req.ownerHandle), eq(tOutputsQ.appSlug, req.appSlug), eq(tOutputsQ.dbName, req.dbName), eq(tOutputsQ.fnCid, afbRowQ.accessFnCid)));
566
+ const grantOutputs = allOutputs.filter((r) => {
567
+ const parsed = JSON.parse(r.output);
568
+ return ((parsed.members !== undefined && Object.keys(parsed.members).length > 0) ||
569
+ (parsed.grant?.users !== undefined && Object.keys(parsed.grant.users).length > 0) ||
570
+ (parsed.grant?.roles !== undefined && Object.keys(parsed.grant.roles).length > 0) ||
571
+ (parsed.grant?.public !== undefined && parsed.grant.public.length > 0));
572
+ });
573
+ const reduce = new GrantReduce();
574
+ for (const row of grantOutputs) {
575
+ reduce.addDoc(row.docId, extractContribution(JSON.parse(row.output)));
576
+ }
577
+ const userHandle = req._auth
578
+ ? await vctx.sql.db
579
+ .select({ handle: vctx.sql.tables.handleBinding.handle })
580
+ .from(vctx.sql.tables.handleBinding)
581
+ .where(eq(vctx.sql.tables.handleBinding.userId, req._auth.verifiedAuth.claims.userId))
582
+ .limit(1)
583
+ .then((r) => r[0]?.handle ?? null)
584
+ : null;
585
+ const effectiveChannels = userHandle !== null ? reduce.resolveEffectiveChannels(userHandle) : new Set();
586
+ channelFilteredDocs = filterDocsByChannel(docs, allOutputs, userHandle, effectiveChannels, reduce.publicChannels);
587
+ }
588
+ const filteredDocs = applyQueryFilter(channelFilteredDocs, req.filter);
339
589
  await ctx.send.send(ctx, {
340
590
  type: "vibes.diy.res-query-docs",
341
591
  status: "ok",
@@ -443,9 +693,58 @@ export const subscribeDocsEvento = {
443
693
  }
444
694
  const wsSend = clientWsSend(ctx);
445
695
  const subscriptionKey = `${req.ownerHandle}/${req.appSlug}/${req.dbName}`;
446
- wsSend.subscribedDocKeys.add(subscriptionKey);
696
+ const tAfbS = vctx.sql.tables.accessFunctionBindings;
697
+ const afbRowS = await vctx.sql.db
698
+ .select({ accessFnCid: tAfbS.accessFnCid })
699
+ .from(tAfbS)
700
+ .where(and(eq(tAfbS.userSlug, req.ownerHandle), eq(tAfbS.appSlug, req.appSlug), inArray(tAfbS.dbName, [req.dbName, "*"])))
701
+ .orderBy(sql `CASE WHEN ${tAfbS.dbName} = ${req.dbName} THEN 0 ELSE 1 END`)
702
+ .limit(1)
703
+ .then((r) => r[0]);
704
+ const channelKeys = [];
705
+ if (afbRowS?.accessFnCid) {
706
+ const tOutputsS = vctx.sql.tables.accessFnOutputs;
707
+ const grantOutputs = await vctx.sql.db
708
+ .select({ docId: tOutputsS.docId, output: tOutputsS.output })
709
+ .from(tOutputsS)
710
+ .where(and(eq(tOutputsS.userSlug, req.ownerHandle), eq(tOutputsS.appSlug, req.appSlug), eq(tOutputsS.dbName, req.dbName), eq(tOutputsS.fnCid, afbRowS.accessFnCid), eq(tOutputsS.hasGrants, 1)));
711
+ const reduce = new GrantReduce();
712
+ for (const row of grantOutputs) {
713
+ reduce.addDoc(row.docId, extractContribution(JSON.parse(row.output)));
714
+ }
715
+ const userHandle = req._auth
716
+ ? await vctx.sql.db
717
+ .select({ handle: vctx.sql.tables.handleBinding.handle })
718
+ .from(vctx.sql.tables.handleBinding)
719
+ .where(eq(vctx.sql.tables.handleBinding.userId, req._auth.verifiedAuth.claims.userId))
720
+ .limit(1)
721
+ .then((r) => r[0]?.handle ?? null)
722
+ : null;
723
+ const effectiveChannels = userHandle !== null ? reduce.resolveEffectiveChannels(userHandle) : new Set();
724
+ for (const ch of effectiveChannels) {
725
+ channelKeys.push(`${req.ownerHandle}/${req.appSlug}/${ch}`);
726
+ }
727
+ for (const ch of reduce.publicChannels) {
728
+ channelKeys.push(`${req.ownerHandle}/${req.appSlug}/${ch}`);
729
+ }
730
+ }
731
+ if (channelKeys.length > 0) {
732
+ for (const key of channelKeys) {
733
+ wsSend.subscribedDocKeys.add(key);
734
+ }
735
+ }
736
+ else {
737
+ wsSend.subscribedDocKeys.add(subscriptionKey);
738
+ }
447
739
  if (vctx.registerDocSubscription) {
448
- vctx.registerDocSubscription(subscriptionKey).catch((e) => console.error("DocNotify error:", e));
740
+ if (channelKeys.length > 0) {
741
+ for (const key of channelKeys) {
742
+ vctx.registerDocSubscription(key).catch((e) => console.error("DocNotify error:", e));
743
+ }
744
+ }
745
+ else {
746
+ vctx.registerDocSubscription(subscriptionKey).catch((e) => console.error("DocNotify error:", e));
747
+ }
449
748
  }
450
749
  await ctx.send.send(ctx, {
451
750
  type: "vibes.diy.res-subscribe-docs",