@vibes.diy/api-svc 2.4.11 → 2.4.13
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/README.md +12 -12
- package/cf-serve.js +1 -0
- package/cf-serve.js.map +1 -1
- package/create-handler.d.ts +1 -0
- package/create-handler.js +0 -10
- package/create-handler.js.map +1 -1
- package/intern/process-access-bindings.d.ts +12 -0
- package/intern/process-access-bindings.js +225 -0
- package/intern/process-access-bindings.js.map +1 -0
- package/intern/prompt-streaming.js +5 -4
- package/intern/prompt-streaming.js.map +1 -1
- package/intern/render-vibe.js +2 -2
- package/intern/render-vibe.js.map +1 -1
- package/intern/write-apps.js +11 -2
- package/intern/write-apps.js.map +1 -1
- package/models.json +1 -0
- package/package.json +11 -11
- package/public/access-function.d.ts +6 -1
- package/public/access-function.js +44 -9
- package/public/access-function.js.map +1 -1
- package/public/access-helpers.d.ts +7 -4
- package/public/access-helpers.js +7 -7
- package/public/access-helpers.js.map +1 -1
- package/public/app-documents-dm-eventos.d.ts +4 -0
- package/public/app-documents-dm-eventos.js +121 -0
- package/public/app-documents-dm-eventos.js.map +1 -0
- package/public/app-documents-query-filter.d.ts +6 -0
- package/public/app-documents-query-filter.js +33 -0
- package/public/app-documents-query-filter.js.map +1 -0
- package/public/app-documents-read-eventos.d.ts +6 -0
- package/public/app-documents-read-eventos.js +359 -0
- package/public/app-documents-read-eventos.js.map +1 -0
- package/public/app-documents-shared.d.ts +11 -0
- package/public/app-documents-shared.js +17 -0
- package/public/app-documents-shared.js.map +1 -0
- package/public/app-documents-write-eventos.d.ts +4 -0
- package/public/app-documents-write-eventos.js +405 -0
- package/public/app-documents-write-eventos.js.map +1 -0
- package/public/app-documents.d.ts +4 -15
- package/public/app-documents.js +4 -898
- package/public/app-documents.js.map +1 -1
- package/public/asset-upload-grant.js +1 -1
- package/public/asset-upload-grant.js.map +1 -1
- package/public/db-acl-resolver.js +1 -1
- package/public/db-acl-resolver.js.map +1 -1
- package/public/ensure-app-slug-item.js +9 -225
- package/public/ensure-app-slug-item.js.map +1 -1
- package/public/files-asset.js +1 -1
- package/public/files-asset.js.map +1 -1
- package/public/grant-reduce.d.ts +2 -2
- package/public/grant-reduce.js +5 -5
- package/public/grant-reduce.js.map +1 -1
- package/public/list-members.js +2 -2
- package/public/list-members.js.map +1 -1
- package/public/list-models.d.ts +1 -0
- package/public/prompt-chat-section.js +203 -49
- package/public/prompt-chat-section.js.map +1 -1
- package/public/who-am-i.d.ts +7 -0
- package/public/who-am-i.js +79 -6
- package/public/who-am-i.js.map +1 -1
- package/svc-ws-send-provider.d.ts +1 -0
- package/svc-ws-send-provider.js +1 -0
- package/svc-ws-send-provider.js.map +1 -1
- package/types.d.ts +1 -8
- package/types.js.map +1 -1
- package/vibes-msg-evento.js +1 -2
- package/vibes-msg-evento.js.map +1 -1
- package/public/get-fp-cloud-token.d.ts +0 -3
- package/public/get-fp-cloud-token.js +0 -140
- package/public/get-fp-cloud-token.js.map +0 -1
package/public/app-documents.js
CHANGED
|
@@ -1,899 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { eq, and, sql, inArray, desc } from "drizzle-orm";
|
|
6
|
-
import { max } from "drizzle-orm/sql";
|
|
7
|
-
import { type } from "arktype";
|
|
8
|
-
import { checkDocAccess, canRead, isPublicReadable } from "./access-helpers.js";
|
|
9
|
-
import { enforceAllowAnonymous, ForbiddenError, extractExportSource } from "./access-function.js";
|
|
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";
|
|
13
|
-
import { mintFilesUrls, isFileMeta } from "./files-url-mint.js";
|
|
14
|
-
async function readAllowed(vctx, acl, access, appSlug, ownerHandle) {
|
|
15
|
-
if (acl?.read !== undefined)
|
|
16
|
-
return aclAllows(acl, "read", access);
|
|
17
|
-
if (canRead(access))
|
|
18
|
-
return true;
|
|
19
|
-
return isPublicReadable(vctx, appSlug, ownerHandle);
|
|
20
|
-
}
|
|
21
|
-
function clientWsSend(ctx) {
|
|
22
|
-
return ctx.send.provider;
|
|
23
|
-
}
|
|
24
|
-
async function validateFilesUploads(vctx, doc, ownerHandle, appSlug) {
|
|
25
|
-
const files = doc?._files;
|
|
26
|
-
if (!files || typeof files !== "object")
|
|
27
|
-
return { ok: true };
|
|
28
|
-
const uploadIds = [];
|
|
29
|
-
for (const entry of Object.values(files)) {
|
|
30
|
-
if (isFileMeta(entry))
|
|
31
|
-
uploadIds.push(entry.uploadId);
|
|
32
|
-
}
|
|
33
|
-
if (uploadIds.length === 0)
|
|
34
|
-
return { ok: true };
|
|
35
|
-
const t = vctx.sql.tables.assetUploads;
|
|
36
|
-
const rows = await vctx.sql.db
|
|
37
|
-
.select({ uploadId: t.uploadId, ownerHandle: t.ownerHandle, appSlug: t.appSlug })
|
|
38
|
-
.from(t)
|
|
39
|
-
.where(inArray(t.uploadId, uploadIds));
|
|
40
|
-
const found = new Map(rows.map((r) => [r.uploadId, r]));
|
|
41
|
-
for (const id of uploadIds) {
|
|
42
|
-
const row = found.get(id);
|
|
43
|
-
if (!row)
|
|
44
|
-
return { ok: false, reason: `unknown uploadId: ${id}` };
|
|
45
|
-
if (row.ownerHandle !== ownerHandle || row.appSlug !== appSlug) {
|
|
46
|
-
return { ok: false, reason: `uploadId ${id} not minted for this app` };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return { ok: true };
|
|
50
|
-
}
|
|
51
|
-
export const putDocEvento = {
|
|
52
|
-
hash: "put-doc",
|
|
53
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
54
|
-
const ret = reqPutDoc(msg.payload);
|
|
55
|
-
if (ret instanceof type.errors) {
|
|
56
|
-
return Result.Ok(Option.None());
|
|
57
|
-
}
|
|
58
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
59
|
-
}),
|
|
60
|
-
handle: optAuth(async (ctx) => {
|
|
61
|
-
const req = ctx.validated.payload;
|
|
62
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
63
|
-
const userId = req._auth?.verifiedAuth.claims.userId ?? null;
|
|
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
|
-
}
|
|
69
|
-
const rAccess = await checkDirectChannelAccess(vctx, req.ownerHandle, userId);
|
|
70
|
-
if (rAccess.isErr() || !rAccess.Ok()) {
|
|
71
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
72
|
-
return Result.Ok(EventoResult.Continue);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
else if (userId) {
|
|
76
|
-
const access = await checkDocAccess(vctx, userId, req.appSlug, req.ownerHandle);
|
|
77
|
-
const rAcl = await resolveDbAcl(vctx, req.ownerHandle, req.appSlug, req.dbName);
|
|
78
|
-
if (rAcl.isErr()) {
|
|
79
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
80
|
-
return Result.Ok(EventoResult.Continue);
|
|
81
|
-
}
|
|
82
|
-
const acl = rAcl.Ok();
|
|
83
|
-
if (!aclAllows(acl, "write", access)) {
|
|
84
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
85
|
-
return Result.Ok(EventoResult.Continue);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
const filesCheck = await validateFilesUploads(vctx, req.doc, req.ownerHandle, req.appSlug);
|
|
89
|
-
if (!filesCheck.ok) {
|
|
90
|
-
await ctx.send.send(ctx, {
|
|
91
|
-
type: "vibes.diy.res-error",
|
|
92
|
-
error: { message: `Invalid file reference: ${filesCheck.reason}` },
|
|
93
|
-
});
|
|
94
|
-
return Result.Ok(EventoResult.Continue);
|
|
95
|
-
}
|
|
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
|
-
}
|
|
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();
|
|
204
|
-
const dbName = req.dbName;
|
|
205
|
-
const t = vctx.sql.tables.appDocuments;
|
|
206
|
-
const maxSeqResult = await vctx.sql.db
|
|
207
|
-
.select({ maxSeq: max(t.seq) })
|
|
208
|
-
.from(t)
|
|
209
|
-
.where(and(eq(t.ownerHandle, req.ownerHandle), eq(t.appSlug, req.appSlug), eq(t.dbName, dbName), eq(t.docId, docId)))
|
|
210
|
-
.then((r) => r[0]);
|
|
211
|
-
const nextSeq = (maxSeqResult?.maxSeq ?? 0) + 1;
|
|
212
|
-
await vctx.sql.db.insert(t).values({
|
|
213
|
-
ownerHandle: req.ownerHandle,
|
|
214
|
-
appSlug: req.appSlug,
|
|
215
|
-
dbName,
|
|
216
|
-
docId,
|
|
217
|
-
seq: nextSeq,
|
|
218
|
-
userId: userId ?? "unknown",
|
|
219
|
-
data: req.doc,
|
|
220
|
-
deleted: 0,
|
|
221
|
-
created: now,
|
|
222
|
-
});
|
|
223
|
-
if (isDirectChannel(req.ownerHandle)) {
|
|
224
|
-
const participants = directChannelParticipants(req.ownerHandle);
|
|
225
|
-
if (participants) {
|
|
226
|
-
const t_idx = vctx.sql.tables.directChannelIndex;
|
|
227
|
-
await vctx.sql.db
|
|
228
|
-
.insert(t_idx)
|
|
229
|
-
.values([
|
|
230
|
-
{ handle: participants[0], channelHandle: req.ownerHandle },
|
|
231
|
-
{ handle: participants[1], channelHandle: req.ownerHandle },
|
|
232
|
-
])
|
|
233
|
-
.onConflictDoNothing();
|
|
234
|
-
const t_usb = vctx.sql.tables.handleBinding;
|
|
235
|
-
const senderRow = await vctx.sql.db
|
|
236
|
-
.select({ handle: t_usb.handle })
|
|
237
|
-
.from(t_usb)
|
|
238
|
-
.where(and(eq(t_usb.userId, userId ?? ""), inArray(t_usb.handle, participants)))
|
|
239
|
-
.then((r) => r[0]);
|
|
240
|
-
const senderUserSlug = senderRow?.handle ?? "";
|
|
241
|
-
const recipientUserSlug = participants.find((h) => h !== senderUserSlug) ?? participants[1];
|
|
242
|
-
await vctx.postQueue({
|
|
243
|
-
payload: {
|
|
244
|
-
type: "vibes.diy.evt-dm-received",
|
|
245
|
-
senderUserId: userId ?? "",
|
|
246
|
-
senderUserSlug,
|
|
247
|
-
recipientUserSlug,
|
|
248
|
-
channelUserSlug: req.ownerHandle,
|
|
249
|
-
docId,
|
|
250
|
-
created: now,
|
|
251
|
-
bodySnippet: typeof req.doc.body === "string"
|
|
252
|
-
? req.doc.body.slice(0, 100)
|
|
253
|
-
: undefined,
|
|
254
|
-
},
|
|
255
|
-
tid: "queue-event",
|
|
256
|
-
src: "putDoc",
|
|
257
|
-
dst: "vibes-service",
|
|
258
|
-
ttl: 1,
|
|
259
|
-
});
|
|
260
|
-
if (senderUserSlug) {
|
|
261
|
-
const t_reads = vctx.sql.tables.directChannelReads;
|
|
262
|
-
await vctx.sql.db
|
|
263
|
-
.insert(t_reads)
|
|
264
|
-
.values({ channelHandle: req.ownerHandle, handle: senderUserSlug, lastSeenSeq: nextSeq })
|
|
265
|
-
.onConflictDoUpdate({
|
|
266
|
-
target: [t_reads.channelHandle, t_reads.handle],
|
|
267
|
-
set: { lastSeenSeq: sql `MAX(${t_reads.lastSeenSeq}, ${nextSeq})` },
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (dbName === COMMENTS_DB_NAME && nextSeq === 1) {
|
|
273
|
-
await vctx.postQueue({
|
|
274
|
-
payload: {
|
|
275
|
-
type: "vibes.diy.evt-comment-posted",
|
|
276
|
-
userId: userId ?? "unknown",
|
|
277
|
-
ownerHandle: req.ownerHandle,
|
|
278
|
-
appSlug: req.appSlug,
|
|
279
|
-
docId,
|
|
280
|
-
created: now,
|
|
281
|
-
email: req._auth?.verifiedAuth.claims.params.email ?? "unknown",
|
|
282
|
-
},
|
|
283
|
-
tid: "queue-event",
|
|
284
|
-
src: "putDoc",
|
|
285
|
-
dst: "vibes-service",
|
|
286
|
-
ttl: 1,
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
if (vctx.notifyDocChanged) {
|
|
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
|
-
}
|
|
340
|
-
}
|
|
341
|
-
await ctx.send.send(ctx, {
|
|
342
|
-
type: "vibes.diy.res-put-doc",
|
|
343
|
-
status: "ok",
|
|
344
|
-
id: docId,
|
|
345
|
-
});
|
|
346
|
-
return Result.Ok(EventoResult.Continue);
|
|
347
|
-
}),
|
|
348
|
-
};
|
|
349
|
-
export const getDocEvento = {
|
|
350
|
-
hash: "get-doc",
|
|
351
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
352
|
-
const ret = reqGetDoc(msg.payload);
|
|
353
|
-
if (ret instanceof type.errors) {
|
|
354
|
-
return Result.Ok(Option.None());
|
|
355
|
-
}
|
|
356
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
357
|
-
}),
|
|
358
|
-
handle: optAuth(async (ctx) => {
|
|
359
|
-
const req = ctx.validated.payload;
|
|
360
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
361
|
-
const access = req._auth
|
|
362
|
-
? await checkDocAccess(vctx, req._auth.verifiedAuth.claims.userId, req.appSlug, req.ownerHandle)
|
|
363
|
-
: "none";
|
|
364
|
-
const rAcl = await resolveDbAcl(vctx, req.ownerHandle, req.appSlug, req.dbName);
|
|
365
|
-
if (rAcl.isErr() || !(await readAllowed(vctx, rAcl.Ok(), access, req.appSlug, req.ownerHandle))) {
|
|
366
|
-
await ctx.send.send(ctx, {
|
|
367
|
-
type: "vibes.diy.res-error",
|
|
368
|
-
error: { message: "Access denied" },
|
|
369
|
-
});
|
|
370
|
-
return Result.Ok(EventoResult.Continue);
|
|
371
|
-
}
|
|
372
|
-
const t = vctx.sql.tables.appDocuments;
|
|
373
|
-
const row = await vctx.sql.db
|
|
374
|
-
.select()
|
|
375
|
-
.from(t)
|
|
376
|
-
.where(and(eq(t.ownerHandle, req.ownerHandle), eq(t.appSlug, req.appSlug), eq(t.dbName, req.dbName), eq(t.docId, req.docId)))
|
|
377
|
-
.orderBy(sql `${t.seq} desc`)
|
|
378
|
-
.limit(1)
|
|
379
|
-
.then((r) => r[0]);
|
|
380
|
-
if (!row || row.deleted === 1) {
|
|
381
|
-
await ctx.send.send(ctx, {
|
|
382
|
-
type: "vibes.diy.res-get-doc",
|
|
383
|
-
status: "not-found",
|
|
384
|
-
id: req.docId,
|
|
385
|
-
});
|
|
386
|
-
return Result.Ok(EventoResult.Continue);
|
|
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
|
-
}
|
|
441
|
-
const doc = mintFilesUrls(row.data, {
|
|
442
|
-
ownerHandle: req.ownerHandle,
|
|
443
|
-
appSlug: req.appSlug,
|
|
444
|
-
dbName: req.dbName,
|
|
445
|
-
docId: row.docId,
|
|
446
|
-
svc: vctx.params.vibes.svc,
|
|
447
|
-
});
|
|
448
|
-
await ctx.send.send(ctx, {
|
|
449
|
-
type: "vibes.diy.res-get-doc",
|
|
450
|
-
status: "ok",
|
|
451
|
-
id: row.docId,
|
|
452
|
-
doc,
|
|
453
|
-
});
|
|
454
|
-
return Result.Ok(EventoResult.Continue);
|
|
455
|
-
}),
|
|
456
|
-
};
|
|
457
|
-
function isInInclusiveRange(value, lo, hi) {
|
|
458
|
-
if (typeof value === "string" && typeof lo === "string" && typeof hi === "string") {
|
|
459
|
-
return value >= lo && value <= hi;
|
|
460
|
-
}
|
|
461
|
-
if (typeof value === "number" && typeof lo === "number" && typeof hi === "number") {
|
|
462
|
-
return value >= lo && value <= hi;
|
|
463
|
-
}
|
|
464
|
-
if (typeof value === "bigint" && typeof lo === "bigint" && typeof hi === "bigint") {
|
|
465
|
-
return value >= lo && value <= hi;
|
|
466
|
-
}
|
|
467
|
-
if (typeof value === "boolean" && typeof lo === "boolean" && typeof hi === "boolean") {
|
|
468
|
-
return Number(value) >= Number(lo) && Number(value) <= Number(hi);
|
|
469
|
-
}
|
|
470
|
-
return false;
|
|
471
|
-
}
|
|
472
|
-
export function applyQueryFilter(docs, filter) {
|
|
473
|
-
if (!filter)
|
|
474
|
-
return docs;
|
|
475
|
-
const { field, key, keys, range } = filter;
|
|
476
|
-
if (key !== undefined) {
|
|
477
|
-
return docs.filter((doc) => doc[field] === key);
|
|
478
|
-
}
|
|
479
|
-
if (keys !== undefined) {
|
|
480
|
-
const keySet = new Set(keys);
|
|
481
|
-
return docs.filter((doc) => keySet.has(doc[field]));
|
|
482
|
-
}
|
|
483
|
-
if (range !== undefined) {
|
|
484
|
-
const [lo, hi] = range;
|
|
485
|
-
return docs.filter((doc) => isInInclusiveRange(doc[field], lo, hi));
|
|
486
|
-
}
|
|
487
|
-
return docs;
|
|
488
|
-
}
|
|
489
|
-
export const queryDocsEvento = {
|
|
490
|
-
hash: "query-docs",
|
|
491
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
492
|
-
const ret = reqQueryDocs(msg.payload);
|
|
493
|
-
if (ret instanceof type.errors) {
|
|
494
|
-
return Result.Ok(Option.None());
|
|
495
|
-
}
|
|
496
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
497
|
-
}),
|
|
498
|
-
handle: optAuth(async (ctx) => {
|
|
499
|
-
const req = ctx.validated.payload;
|
|
500
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
501
|
-
if (isDirectChannel(req.ownerHandle)) {
|
|
502
|
-
const userId = req._auth?.verifiedAuth.claims.userId;
|
|
503
|
-
const rAccess = userId ? await checkDirectChannelAccess(vctx, req.ownerHandle, userId) : Result.Ok(false);
|
|
504
|
-
if (rAccess.isErr() || !rAccess.Ok()) {
|
|
505
|
-
await ctx.send.send(ctx, {
|
|
506
|
-
type: "vibes.diy.res-error",
|
|
507
|
-
error: { message: "Access denied" },
|
|
508
|
-
});
|
|
509
|
-
return Result.Ok(EventoResult.Continue);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
else {
|
|
513
|
-
const access = req._auth
|
|
514
|
-
? await checkDocAccess(vctx, req._auth.verifiedAuth.claims.userId, req.appSlug, req.ownerHandle)
|
|
515
|
-
: "none";
|
|
516
|
-
const rAcl = await resolveDbAcl(vctx, req.ownerHandle, req.appSlug, req.dbName);
|
|
517
|
-
if (rAcl.isErr() || !(await readAllowed(vctx, rAcl.Ok(), access, req.appSlug, req.ownerHandle))) {
|
|
518
|
-
await ctx.send.send(ctx, {
|
|
519
|
-
type: "vibes.diy.res-error",
|
|
520
|
-
error: { message: "Access denied" },
|
|
521
|
-
});
|
|
522
|
-
return Result.Ok(EventoResult.Continue);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
const t = vctx.sql.tables.appDocuments;
|
|
526
|
-
const rows = await vctx.sql.db
|
|
527
|
-
.select()
|
|
528
|
-
.from(t)
|
|
529
|
-
.where(and(eq(t.ownerHandle, req.ownerHandle), eq(t.appSlug, req.appSlug), eq(t.dbName, req.dbName)))
|
|
530
|
-
.orderBy(sql `${t.docId}, ${t.seq}`);
|
|
531
|
-
const latest = new Map();
|
|
532
|
-
for (const row of rows) {
|
|
533
|
-
latest.set(row.docId, row);
|
|
534
|
-
}
|
|
535
|
-
const docs = [];
|
|
536
|
-
for (const row of latest.values()) {
|
|
537
|
-
if (row.deleted === 1)
|
|
538
|
-
continue;
|
|
539
|
-
const doc = mintFilesUrls(row.data, {
|
|
540
|
-
ownerHandle: req.ownerHandle,
|
|
541
|
-
appSlug: req.appSlug,
|
|
542
|
-
dbName: req.dbName,
|
|
543
|
-
docId: row.docId,
|
|
544
|
-
svc: vctx.params.vibes.svc,
|
|
545
|
-
});
|
|
546
|
-
docs.push({
|
|
547
|
-
_id: row.docId,
|
|
548
|
-
...doc,
|
|
549
|
-
});
|
|
550
|
-
}
|
|
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);
|
|
589
|
-
await ctx.send.send(ctx, {
|
|
590
|
-
type: "vibes.diy.res-query-docs",
|
|
591
|
-
status: "ok",
|
|
592
|
-
docs: filteredDocs,
|
|
593
|
-
});
|
|
594
|
-
return Result.Ok(EventoResult.Continue);
|
|
595
|
-
}),
|
|
596
|
-
};
|
|
597
|
-
export const deleteDocEvento = {
|
|
598
|
-
hash: "delete-doc",
|
|
599
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
600
|
-
const ret = reqDeleteDoc(msg.payload);
|
|
601
|
-
if (ret instanceof type.errors) {
|
|
602
|
-
return Result.Ok(Option.None());
|
|
603
|
-
}
|
|
604
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
605
|
-
}),
|
|
606
|
-
handle: checkAuth(async (ctx) => {
|
|
607
|
-
const req = ctx.validated.payload;
|
|
608
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
609
|
-
const userId = req._auth.verifiedAuth.claims.userId;
|
|
610
|
-
if (isDirectChannel(req.ownerHandle)) {
|
|
611
|
-
const rAccess = await checkDirectChannelAccess(vctx, req.ownerHandle, userId);
|
|
612
|
-
if (rAccess.isErr() || !rAccess.Ok()) {
|
|
613
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
614
|
-
return Result.Ok(EventoResult.Continue);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
else {
|
|
618
|
-
const access = await checkDocAccess(vctx, userId, req.appSlug, req.ownerHandle);
|
|
619
|
-
const rAcl = await resolveDbAcl(vctx, req.ownerHandle, req.appSlug, req.dbName);
|
|
620
|
-
if (rAcl.isErr() || !aclAllows(rAcl.Ok(), "delete", access)) {
|
|
621
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
622
|
-
return Result.Ok(EventoResult.Continue);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
const now = new Date().toISOString();
|
|
626
|
-
const t = vctx.sql.tables.appDocuments;
|
|
627
|
-
const dbName = req.dbName;
|
|
628
|
-
const maxSeqResult = await vctx.sql.db
|
|
629
|
-
.select({ maxSeq: max(t.seq) })
|
|
630
|
-
.from(t)
|
|
631
|
-
.where(and(eq(t.ownerHandle, req.ownerHandle), eq(t.appSlug, req.appSlug), eq(t.dbName, dbName), eq(t.docId, req.docId)))
|
|
632
|
-
.then((r) => r[0]);
|
|
633
|
-
const nextSeq = (maxSeqResult?.maxSeq ?? 0) + 1;
|
|
634
|
-
await vctx.sql.db.insert(t).values({
|
|
635
|
-
ownerHandle: req.ownerHandle,
|
|
636
|
-
appSlug: req.appSlug,
|
|
637
|
-
dbName,
|
|
638
|
-
docId: req.docId,
|
|
639
|
-
seq: nextSeq,
|
|
640
|
-
userId: req._auth.verifiedAuth.claims.userId,
|
|
641
|
-
data: {},
|
|
642
|
-
deleted: 1,
|
|
643
|
-
created: now,
|
|
644
|
-
});
|
|
645
|
-
if (vctx.notifyDocChanged) {
|
|
646
|
-
vctx
|
|
647
|
-
.notifyDocChanged({ ownerHandle: req.ownerHandle, appSlug: req.appSlug, dbName, docId: req.docId }, clientWsSend(ctx).connId)
|
|
648
|
-
.catch((e) => console.error("DocNotify error:", e));
|
|
649
|
-
}
|
|
650
|
-
await ctx.send.send(ctx, {
|
|
651
|
-
type: "vibes.diy.res-delete-doc",
|
|
652
|
-
status: "ok",
|
|
653
|
-
id: req.docId,
|
|
654
|
-
});
|
|
655
|
-
return Result.Ok(EventoResult.Continue);
|
|
656
|
-
}),
|
|
657
|
-
};
|
|
658
|
-
export const subscribeDocsEvento = {
|
|
659
|
-
hash: "subscribe-docs",
|
|
660
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
661
|
-
const ret = reqSubscribeDocs(msg.payload);
|
|
662
|
-
if (ret instanceof type.errors) {
|
|
663
|
-
return Result.Ok(Option.None());
|
|
664
|
-
}
|
|
665
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
666
|
-
}),
|
|
667
|
-
handle: optAuth(async (ctx) => {
|
|
668
|
-
const req = ctx.validated.payload;
|
|
669
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
670
|
-
if (isDirectChannel(req.ownerHandle)) {
|
|
671
|
-
const userId = req._auth?.verifiedAuth.claims.userId;
|
|
672
|
-
const rAccess = userId ? await checkDirectChannelAccess(vctx, req.ownerHandle, userId) : Result.Ok(false);
|
|
673
|
-
if (rAccess.isErr() || !rAccess.Ok()) {
|
|
674
|
-
await ctx.send.send(ctx, {
|
|
675
|
-
type: "vibes.diy.res-error",
|
|
676
|
-
error: { message: "Access denied" },
|
|
677
|
-
});
|
|
678
|
-
return Result.Ok(EventoResult.Continue);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
else {
|
|
682
|
-
const access = req._auth
|
|
683
|
-
? await checkDocAccess(vctx, req._auth.verifiedAuth.claims.userId, req.appSlug, req.ownerHandle)
|
|
684
|
-
: "none";
|
|
685
|
-
const rAcl = await resolveDbAcl(vctx, req.ownerHandle, req.appSlug, req.dbName);
|
|
686
|
-
if (rAcl.isErr() || !(await readAllowed(vctx, rAcl.Ok(), access, req.appSlug, req.ownerHandle))) {
|
|
687
|
-
await ctx.send.send(ctx, {
|
|
688
|
-
type: "vibes.diy.res-error",
|
|
689
|
-
error: { message: "Access denied" },
|
|
690
|
-
});
|
|
691
|
-
return Result.Ok(EventoResult.Continue);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
const wsSend = clientWsSend(ctx);
|
|
695
|
-
const subscriptionKey = `${req.ownerHandle}/${req.appSlug}/${req.dbName}`;
|
|
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
|
-
}
|
|
739
|
-
if (vctx.registerDocSubscription) {
|
|
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
|
-
}
|
|
748
|
-
}
|
|
749
|
-
await ctx.send.send(ctx, {
|
|
750
|
-
type: "vibes.diy.res-subscribe-docs",
|
|
751
|
-
status: "ok",
|
|
752
|
-
});
|
|
753
|
-
return Result.Ok(EventoResult.Continue);
|
|
754
|
-
}),
|
|
755
|
-
};
|
|
756
|
-
export const listDbNamesEvento = {
|
|
757
|
-
hash: "list-db-names",
|
|
758
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
759
|
-
const ret = reqListDbNames(msg.payload);
|
|
760
|
-
if (ret instanceof type.errors) {
|
|
761
|
-
return Result.Ok(Option.None());
|
|
762
|
-
}
|
|
763
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
764
|
-
}),
|
|
765
|
-
handle: checkAuth(async (ctx) => {
|
|
766
|
-
const req = ctx.validated.payload;
|
|
767
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
768
|
-
const userId = req._auth.verifiedAuth.claims.userId;
|
|
769
|
-
const access = await checkDocAccess(vctx, userId, req.appSlug, req.ownerHandle);
|
|
770
|
-
if (access !== "owner") {
|
|
771
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
772
|
-
return Result.Ok(EventoResult.Continue);
|
|
773
|
-
}
|
|
774
|
-
const t = vctx.sql.tables.appDocuments;
|
|
775
|
-
const rows = await vctx.sql.db
|
|
776
|
-
.selectDistinct({ dbName: t.dbName })
|
|
777
|
-
.from(t)
|
|
778
|
-
.where(and(eq(t.ownerHandle, req.ownerHandle), eq(t.appSlug, req.appSlug)));
|
|
779
|
-
await ctx.send.send(ctx, {
|
|
780
|
-
type: "vibes.diy.res-list-db-names",
|
|
781
|
-
status: "ok",
|
|
782
|
-
dbNames: rows.map((r) => r.dbName),
|
|
783
|
-
});
|
|
784
|
-
return Result.Ok(EventoResult.Continue);
|
|
785
|
-
}),
|
|
786
|
-
};
|
|
787
|
-
export const listDmThreadsEvento = {
|
|
788
|
-
hash: "list-dm-threads",
|
|
789
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
790
|
-
const ret = reqListDmThreads(msg.payload);
|
|
791
|
-
if (ret instanceof type.errors)
|
|
792
|
-
return Result.Ok(Option.None());
|
|
793
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
794
|
-
}),
|
|
795
|
-
handle: checkAuth(async (ctx) => {
|
|
796
|
-
const req = ctx.validated.payload;
|
|
797
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
798
|
-
const userId = req._auth.verifiedAuth.claims.userId;
|
|
799
|
-
const t_usb = vctx.sql.tables.handleBinding;
|
|
800
|
-
const mySlugRows = await vctx.sql.db.select({ handle: t_usb.handle }).from(t_usb).where(eq(t_usb.userId, userId));
|
|
801
|
-
const myUserSlugs = mySlugRows.map((r) => r.handle);
|
|
802
|
-
if (myUserSlugs.length === 0) {
|
|
803
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-list-dm-threads", status: "ok", items: [] });
|
|
804
|
-
return Result.Ok(EventoResult.Continue);
|
|
805
|
-
}
|
|
806
|
-
const t_idx = vctx.sql.tables.directChannelIndex;
|
|
807
|
-
const channelRows = await vctx.sql.db
|
|
808
|
-
.select({ channelUserSlug: t_idx.channelHandle, ownerHandle: t_idx.handle })
|
|
809
|
-
.from(t_idx)
|
|
810
|
-
.where(inArray(t_idx.handle, myUserSlugs));
|
|
811
|
-
const t_docs = vctx.sql.tables.appDocuments;
|
|
812
|
-
const t_reads = vctx.sql.tables.directChannelReads;
|
|
813
|
-
const limit = req.pager?.limit ?? 50;
|
|
814
|
-
const channelSlugs = channelRows.map((r) => r.channelUserSlug);
|
|
815
|
-
const subq = vctx.sql.db
|
|
816
|
-
.select({ ownerHandle: t_docs.ownerHandle, maxSeq: max(t_docs.seq).as("maxSeq") })
|
|
817
|
-
.from(t_docs)
|
|
818
|
-
.where(and(inArray(t_docs.ownerHandle, channelSlugs), eq(t_docs.appSlug, "dm"), eq(t_docs.dbName, "messages"), eq(t_docs.deleted, 0)))
|
|
819
|
-
.groupBy(t_docs.ownerHandle)
|
|
820
|
-
.as("latest");
|
|
821
|
-
const latestDocs = await vctx.sql.db
|
|
822
|
-
.select()
|
|
823
|
-
.from(t_docs)
|
|
824
|
-
.innerJoin(subq, and(eq(t_docs.ownerHandle, subq.ownerHandle), eq(t_docs.seq, subq.maxSeq)));
|
|
825
|
-
const latestDocByChannel = new Map(latestDocs.map((row) => [row.AppDocuments.ownerHandle, row.AppDocuments]));
|
|
826
|
-
const readRows = await vctx.sql.db
|
|
827
|
-
.select({ channelUserSlug: t_reads.channelHandle, lastSeenSeq: t_reads.lastSeenSeq })
|
|
828
|
-
.from(t_reads)
|
|
829
|
-
.where(and(inArray(t_reads.channelHandle, channelSlugs), inArray(t_reads.handle, myUserSlugs)));
|
|
830
|
-
const lastSeenByChannel = new Map(readRows.map((r) => [r.channelUserSlug, r.lastSeenSeq]));
|
|
831
|
-
const items = channelRows.map(({ channelUserSlug, ownerHandle: mySlug }) => {
|
|
832
|
-
const otherUserSlug = (directChannelParticipants(channelUserSlug) ?? []).find((h) => h !== mySlug) ?? "";
|
|
833
|
-
const latestDoc = latestDocByChannel.get(channelUserSlug);
|
|
834
|
-
const latestSeq = latestDoc?.seq ?? 0;
|
|
835
|
-
const lastSeen = lastSeenByChannel.get(channelUserSlug) ?? 0;
|
|
836
|
-
const unreadCount = Math.max(0, latestSeq - lastSeen);
|
|
837
|
-
return {
|
|
838
|
-
channelUserSlug,
|
|
839
|
-
otherUserSlug,
|
|
840
|
-
latestSeq,
|
|
841
|
-
unreadCount,
|
|
842
|
-
latestMessage: latestDoc
|
|
843
|
-
? {
|
|
844
|
-
body: String(latestDoc.data.body ?? ""),
|
|
845
|
-
createdAt: latestDoc.created,
|
|
846
|
-
authorUserSlug: String(latestDoc.data.authorUserSlug ?? ""),
|
|
847
|
-
}
|
|
848
|
-
: undefined,
|
|
849
|
-
};
|
|
850
|
-
});
|
|
851
|
-
const sorted = items
|
|
852
|
-
.sort((a, b) => ((b.latestMessage?.createdAt ?? "") > (a.latestMessage?.createdAt ?? "") ? 1 : -1))
|
|
853
|
-
.slice(0, limit);
|
|
854
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-list-dm-threads", status: "ok", items: sorted });
|
|
855
|
-
return Result.Ok(EventoResult.Continue);
|
|
856
|
-
}),
|
|
857
|
-
};
|
|
858
|
-
export const markDmReadEvento = {
|
|
859
|
-
hash: "mark-dm-read",
|
|
860
|
-
validate: unwrapMsgBase(async (msg) => {
|
|
861
|
-
const ret = reqMarkDmRead(msg.payload);
|
|
862
|
-
if (ret instanceof type.errors)
|
|
863
|
-
return Result.Ok(Option.None());
|
|
864
|
-
return Result.Ok(Option.Some({ ...msg, payload: ret }));
|
|
865
|
-
}),
|
|
866
|
-
handle: checkAuth(async (ctx) => {
|
|
867
|
-
const req = ctx.validated.payload;
|
|
868
|
-
const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
|
|
869
|
-
const userId = req._auth.verifiedAuth.claims.userId;
|
|
870
|
-
const rAccess = await checkDirectChannelAccess(vctx, req.channelUserSlug, userId);
|
|
871
|
-
if (rAccess.isErr() || !rAccess.Ok()) {
|
|
872
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
873
|
-
return Result.Ok(EventoResult.Continue);
|
|
874
|
-
}
|
|
875
|
-
const participants = directChannelParticipants(req.channelUserSlug) ?? ["", ""];
|
|
876
|
-
const t_usb = vctx.sql.tables.handleBinding;
|
|
877
|
-
const slugRow = await vctx.sql.db
|
|
878
|
-
.select({ handle: t_usb.handle })
|
|
879
|
-
.from(t_usb)
|
|
880
|
-
.where(and(eq(t_usb.userId, userId), inArray(t_usb.handle, participants)))
|
|
881
|
-
.then((r) => r[0]);
|
|
882
|
-
if (!slugRow) {
|
|
883
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
|
|
884
|
-
return Result.Ok(EventoResult.Continue);
|
|
885
|
-
}
|
|
886
|
-
const myUserSlug = slugRow.handle;
|
|
887
|
-
const t_reads = vctx.sql.tables.directChannelReads;
|
|
888
|
-
await vctx.sql.db
|
|
889
|
-
.insert(t_reads)
|
|
890
|
-
.values({ channelHandle: req.channelUserSlug, handle: myUserSlug, lastSeenSeq: req.lastSeenSeq })
|
|
891
|
-
.onConflictDoUpdate({
|
|
892
|
-
target: [t_reads.channelHandle, t_reads.handle],
|
|
893
|
-
set: { lastSeenSeq: sql `MAX(${t_reads.lastSeenSeq}, ${req.lastSeenSeq})` },
|
|
894
|
-
});
|
|
895
|
-
await ctx.send.send(ctx, { type: "vibes.diy.res-mark-dm-read", status: "ok" });
|
|
896
|
-
return Result.Ok(EventoResult.Continue);
|
|
897
|
-
}),
|
|
898
|
-
};
|
|
1
|
+
export { putDocEvento, deleteDocEvento } from "./app-documents-write-eventos.js";
|
|
2
|
+
export { getDocEvento, queryDocsEvento, subscribeDocsEvento, listDbNamesEvento } from "./app-documents-read-eventos.js";
|
|
3
|
+
export { listDmThreadsEvento, markDmReadEvento } from "./app-documents-dm-eventos.js";
|
|
4
|
+
export { applyQueryFilter } from "./app-documents-query-filter.js";
|
|
899
5
|
//# sourceMappingURL=app-documents.js.map
|