@vibes.diy/api-svc 14.1.21 → 14.1.23

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 (64) hide show
  1. package/create-handler.d.ts +2 -1
  2. package/create-handler.js +1 -0
  3. package/create-handler.js.map +1 -1
  4. package/index.d.ts +2 -0
  5. package/index.js +2 -0
  6. package/index.js.map +1 -1
  7. package/intern/agent-belt.js +15 -4
  8. package/intern/agent-belt.js.map +1 -1
  9. package/intern/agent-build-tools.js +5 -2
  10. package/intern/agent-build-tools.js.map +1 -1
  11. package/intern/agent-debug-tools.d.ts +33 -0
  12. package/intern/agent-debug-tools.js +676 -0
  13. package/intern/agent-debug-tools.js.map +1 -0
  14. package/intern/agent-deferred-access.d.ts +3 -0
  15. package/intern/agent-deferred-access.js +45 -1
  16. package/intern/agent-deferred-access.js.map +1 -1
  17. package/intern/agent-file-tools.js +8 -2
  18. package/intern/agent-file-tools.js.map +1 -1
  19. package/intern/agent-instruction.d.ts +1 -1
  20. package/intern/agent-instruction.js +2 -2
  21. package/intern/agent-instruction.js.map +1 -1
  22. package/intern/agent-session-instruction.d.ts +2 -2
  23. package/intern/agent-session-instruction.js +15 -2
  24. package/intern/agent-session-instruction.js.map +1 -1
  25. package/intern/agent-session-runtime.d.ts +1 -0
  26. package/intern/agent-session-runtime.js +38 -6
  27. package/intern/agent-session-runtime.js.map +1 -1
  28. package/intern/attempt-backend-fetch.js +7 -1
  29. package/intern/attempt-backend-fetch.js.map +1 -1
  30. package/intern/backend-do-addr.d.ts +2 -0
  31. package/intern/backend-do-addr.js +2 -0
  32. package/intern/backend-do-addr.js.map +1 -1
  33. package/intern/backend-do-control-plane.d.ts +18 -0
  34. package/intern/backend-do-control-plane.js +90 -1
  35. package/intern/backend-do-control-plane.js.map +1 -1
  36. package/intern/mint-decision.d.ts +8 -0
  37. package/intern/mint-decision.js +15 -3
  38. package/intern/mint-decision.js.map +1 -1
  39. package/intern/resolve-backend-session-user.d.ts +8 -0
  40. package/intern/resolve-backend-session-user.js +10 -0
  41. package/intern/resolve-backend-session-user.js.map +1 -1
  42. package/package.json +11 -11
  43. package/public/app-documents-broadcast.d.ts +2 -0
  44. package/public/app-documents-broadcast.js +7 -0
  45. package/public/app-documents-broadcast.js.map +1 -1
  46. package/public/app-documents-read-eventos.d.ts +52 -1
  47. package/public/app-documents-read-eventos.js +250 -222
  48. package/public/app-documents-read-eventos.js.map +1 -1
  49. package/public/app-documents-write-eventos.d.ts +42 -0
  50. package/public/app-documents-write-eventos.js +334 -334
  51. package/public/app-documents-write-eventos.js.map +1 -1
  52. package/public/app-logs.d.ts +14 -2
  53. package/public/app-logs.js +60 -45
  54. package/public/app-logs.js.map +1 -1
  55. package/public/app-setting-items.d.ts +4 -0
  56. package/public/app-setting-items.js +6 -3
  57. package/public/app-setting-items.js.map +1 -1
  58. package/public/app-settings-sql-upsert.d.ts +4 -0
  59. package/public/app-settings-sql-upsert.js +1 -1
  60. package/public/app-settings-sql-upsert.js.map +1 -1
  61. package/public/prompt-chat-section.js +1 -0
  62. package/public/prompt-chat-section.js.map +1 -1
  63. package/types.d.ts +2 -1
  64. package/types.js.map +1 -1
@@ -10,6 +10,7 @@ import { resolveAccessBinding, resolveAccessFnSource } from "./access-binding-re
10
10
  import { extractContribution, newSeededReduce } from "./grant-reduce.js";
11
11
  import { resolveDeveloperHandles } from "./developer-grants.js";
12
12
  import { clientWsSend, connectionAdminMode } from "./app-documents-shared.js";
13
+ import { addressesDirectChannel } from "./dm-lane.js";
13
14
  import { normalizeChannels } from "./normalize-channels.js";
14
15
  import { audienceChannelsOf, hasInheritChannel, inheritChannelOf, INHERIT_CAPABILITY_CHANNEL, isReservedChannelId, stripForeignAudience, } from "./audience-channels.js";
15
16
  import { emitBackendOnChange } from "../intern/emit-backend-onchange.js";
@@ -21,7 +22,7 @@ import { orThrow, syncChannelIndex } from "./access-fn-channel-index.js";
21
22
  import { resolveWriteAclVerdict, resolveWriterContext } from "./write-front-door.js";
22
23
  import { resolveActingHandle } from "./resolve-active-handle.js";
23
24
  import { purgeRootHtmlCacheSameColo } from "../intern/swr-purge.js";
24
- import { notifyDocChangedFanOut, notifyViewerGrantsChangedFanOut, emitCommentPostedEvent } from "./app-documents-broadcast.js";
25
+ import { notifyDocChangedFanOut, notifyViewerGrantsChangedFanOut, emitCommentPostedEvent, } from "./app-documents-broadcast.js";
25
26
  import { notifyViewerGrantDeltaIfChanged } from "./app-documents-grant-delta.js";
26
27
  import { persistPutRevision } from "./app-documents-put-persist.js";
27
28
  import { validateFilesUploads } from "./validate-files-uploads.js";
@@ -78,6 +79,193 @@ export async function deriveAccessChannels(vctx, p, afbRow) {
78
79
  const sanitized = stripForeignAudience(invokeResult, p.userContext?.userHandle ?? null);
79
80
  return [...realChannels, ...audienceChannelsOf(sanitized), ...inheritChannelOf(sanitized)];
80
81
  }
82
+ export async function putDocForCaller(vctx, args) {
83
+ if (addressesDirectChannel(args.ownerHandle))
84
+ return { kind: "denied", message: "Access denied" };
85
+ const userId = args.userId;
86
+ const adminRequested = args.adminMode;
87
+ if (args.actingHandle !== undefined && userId !== null) {
88
+ const acting = await resolveActingHandle(vctx, userId, args.actingHandle);
89
+ if (!acting.ok)
90
+ return { kind: "denied", message: "Acting handle not bound" };
91
+ }
92
+ const aclVerdict = await resolveWriteAclVerdict(vctx, {
93
+ ownerHandle: args.ownerHandle,
94
+ appSlug: args.appSlug,
95
+ dbName: args.dbName,
96
+ userId,
97
+ adminRequested,
98
+ });
99
+ if (!aclVerdict.aclWriteAllowed)
100
+ return { kind: "denied", message: "Access denied" };
101
+ const docAccessOverride = aclVerdict.adminActive;
102
+ const filesCheck = await validateFilesUploads(vctx, args.doc, args.ownerHandle, args.appSlug);
103
+ if (!filesCheck.ok)
104
+ return { kind: "denied", message: `Invalid file reference: ${filesCheck.reason}` };
105
+ let accessResult;
106
+ let grantsReduceBefore;
107
+ const afbRow = await resolveAccessBinding(vctx, args.ownerHandle, args.appSlug, args.dbName);
108
+ if (!userId && !afbRow?.accessFnCid)
109
+ return { kind: "denied", message: "Access denied" };
110
+ const docId = args.docId ?? vctx.sthis.timeOrderedNextId().str;
111
+ let headRow;
112
+ if (args.docId) {
113
+ const tDocs = vctx.sql.tables.appDocuments;
114
+ headRow = await vctx.sql.db
115
+ .select({ data: tDocs.data, deleted: tDocs.deleted })
116
+ .from(tDocs)
117
+ .where(and(eq(tDocs.ownerHandle, args.ownerHandle), eq(tDocs.appSlug, args.appSlug), eq(tDocs.dbName, args.dbName), eq(tDocs.docId, args.docId)))
118
+ .orderBy(desc(tDocs.seq))
119
+ .limit(1)
120
+ .then((r) => r[0]);
121
+ }
122
+ if (afbRow?.accessFnCid && !vctx.invokeAccessFn)
123
+ return { kind: "denied", message: "Access function unavailable" };
124
+ if (afbRow?.accessFnCid && vctx.invokeAccessFn) {
125
+ const { userContext, unboundActingHandle } = await resolveWriterContext(vctx, {
126
+ userId,
127
+ ownerHandle: args.ownerHandle,
128
+ ...(args.actingHandle !== undefined ? { actingHandle: args.actingHandle } : {}),
129
+ });
130
+ if (unboundActingHandle)
131
+ return { kind: "denied", message: "Acting handle not bound" };
132
+ const oldDoc = headRow?.data ?? null;
133
+ const adminActive = docAccessOverride;
134
+ const gate = await runPutAccessGate(vctx, {
135
+ ownerHandle: args.ownerHandle,
136
+ appSlug: args.appSlug,
137
+ dbName: args.dbName,
138
+ fnCid: afbRow.accessFnCid,
139
+ accessFnAssetUri: afbRow.accessFnAssetUri,
140
+ accessFnDbName: afbRow.dbName,
141
+ docId,
142
+ doc: args.doc,
143
+ oldDoc,
144
+ oldDocDeleted: headRow?.deleted === 1,
145
+ userContext,
146
+ adminMode: adminActive,
147
+ });
148
+ if (gate.kind === "deny") {
149
+ return { kind: "denied", message: gate.message, ...(gate.code === undefined ? {} : { code: gate.code }) };
150
+ }
151
+ accessResult = gate.descriptor;
152
+ grantsReduceBefore = gate.grantsReduceBefore;
153
+ }
154
+ const now = new Date().toISOString();
155
+ const dbName = args.dbName;
156
+ const oldFeedChannels = afbRow?.accessFnCid
157
+ ? await loadDocChannels(vctx, { ownerHandle: args.ownerHandle, appSlug: args.appSlug, dbName, docId })
158
+ : [];
159
+ let persisted;
160
+ try {
161
+ persisted = await persistPutRevision(vctx, {
162
+ ownerHandle: args.ownerHandle,
163
+ appSlug: args.appSlug,
164
+ dbName,
165
+ docId,
166
+ doc: args.doc,
167
+ userId,
168
+ now,
169
+ accessResult,
170
+ accessFnCid: afbRow?.accessFnCid,
171
+ });
172
+ }
173
+ catch (err) {
174
+ if (err instanceof SeqConflictError) {
175
+ return {
176
+ kind: "denied",
177
+ code: "conflict",
178
+ message: `write conflict: head is at seq ${err.currentHeadSeq}, retry the write`,
179
+ };
180
+ }
181
+ throw err;
182
+ }
183
+ const { alloc, storeAccessOutput, grantUpsertErr, noopRefreshedStaleOutput } = persisted;
184
+ const notifyGrantDeltaIfChanged = () => {
185
+ if (!storeAccessOutput || !grantsReduceBefore)
186
+ return;
187
+ notifyViewerGrantDeltaIfChanged(vctx, {
188
+ ownerHandle: args.ownerHandle,
189
+ appSlug: args.appSlug,
190
+ docId,
191
+ descriptor: storeAccessOutput.descriptor,
192
+ outputHasGrants: storeAccessOutput.outputHasGrants,
193
+ grantsReduceBefore,
194
+ senderConnId: args.senderConnId ?? "",
195
+ });
196
+ };
197
+ if (!alloc.inserted) {
198
+ if (grantUpsertErr) {
199
+ console.warn("AccessFnOutputs stale-row refresh on no-op failed:", grantUpsertErr);
200
+ }
201
+ else if (noopRefreshedStaleOutput) {
202
+ notifyGrantDeltaIfChanged();
203
+ }
204
+ return { kind: "ok", docId };
205
+ }
206
+ const nextSeq = alloc.seq;
207
+ await bestEffortChannelChange({
208
+ db: vctx.sql.db,
209
+ flavour: vctx.sql.flavour,
210
+ ownerHandle: args.ownerHandle,
211
+ appSlug: args.appSlug,
212
+ dbName,
213
+ docId,
214
+ seq: nextSeq,
215
+ oldChannels: oldFeedChannels,
216
+ newChannels: channelSetOfDescriptor(accessResult),
217
+ alwaysPut: true,
218
+ created: now,
219
+ });
220
+ await emitBackendOnChange(vctx, {
221
+ ownerHandle: args.ownerHandle,
222
+ appSlug: args.appSlug,
223
+ dbName,
224
+ docId,
225
+ seq: nextSeq,
226
+ deleted: false,
227
+ doc: args.doc,
228
+ originDepth: 0,
229
+ writerUserId: userId,
230
+ });
231
+ await emitCommentPostedEvent(vctx, {
232
+ ownerHandle: args.ownerHandle,
233
+ appSlug: args.appSlug,
234
+ dbName,
235
+ docId,
236
+ userId,
237
+ email: args.email,
238
+ nextSeq,
239
+ now,
240
+ });
241
+ const putFanoutChannels = [
242
+ ...normalizeChannels(accessResult?.channels ?? []),
243
+ ...(accessResult === undefined ? [] : audienceChannelsOf(accessResult)),
244
+ ...(accessResult === undefined ? [] : inheritChannelOf(accessResult)),
245
+ ];
246
+ if (hasInheritChannel(putFanoutChannels))
247
+ putFanoutChannels.push(INHERIT_CAPABILITY_CHANNEL);
248
+ await (args.fanOutDocChanged ?? notifyDocChangedFanOut)(vctx, {
249
+ ownerHandle: args.ownerHandle,
250
+ appSlug: args.appSlug,
251
+ dbName,
252
+ docId,
253
+ channels: putFanoutChannels,
254
+ senderConnId: args.senderConnId ?? "",
255
+ });
256
+ if (storeAccessOutput) {
257
+ if (grantUpsertErr) {
258
+ console.error("AccessFnOutputs upsert failed:", grantUpsertErr);
259
+ if (storeAccessOutput.outputHasGrants === 1) {
260
+ return { kind: "denied", message: "grant storage failed — retry the write" };
261
+ }
262
+ }
263
+ else {
264
+ notifyGrantDeltaIfChanged();
265
+ }
266
+ }
267
+ return { kind: "ok", docId };
268
+ }
81
269
  export const putDocEvento = {
82
270
  hash: "put-doc",
83
271
  validate: unwrapMsgBase(async (msg) => {
@@ -90,236 +278,167 @@ export const putDocEvento = {
90
278
  handle: optAuth(async (ctx) => {
91
279
  const req = ctx.validated.payload;
92
280
  const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
93
- const userId = req._auth?.verifiedAuth.claims.userId ?? null;
94
- const adminRequested = connectionAdminMode(ctx) || req.adminMode === true;
95
- if (req.actingHandle !== undefined && userId !== null) {
96
- const acting = await resolveActingHandle(vctx, userId, req.actingHandle);
97
- if (!acting.ok) {
98
- await ctx.send.send(ctx, {
99
- type: "vibes.diy.res-error",
100
- error: { message: "Acting handle not bound" },
101
- });
102
- return Result.Ok(EventoResult.Continue);
103
- }
104
- }
105
- const aclVerdict = await resolveWriteAclVerdict(vctx, {
281
+ const answer = await putDocForCaller(vctx, {
282
+ userId: req._auth?.verifiedAuth.claims.userId ?? null,
106
283
  ownerHandle: req.ownerHandle,
107
284
  appSlug: req.appSlug,
108
285
  dbName: req.dbName,
109
- userId,
110
- adminRequested,
286
+ ...(req.docId === undefined ? {} : { docId: req.docId }),
287
+ doc: req.doc,
288
+ adminMode: connectionAdminMode(ctx) || req.adminMode === true,
289
+ ...(req.actingHandle === undefined ? {} : { actingHandle: req.actingHandle }),
290
+ ...(req._auth?.verifiedAuth.claims.params.email === undefined ? {} : { email: req._auth.verifiedAuth.claims.params.email }),
291
+ senderConnId: clientWsSend(ctx).connId,
111
292
  });
112
- if (!aclVerdict.aclWriteAllowed) {
113
- await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: "Access denied" } });
114
- return Result.Ok(EventoResult.Continue);
115
- }
116
- const docAccessOverride = aclVerdict.adminActive;
117
- const filesCheck = await validateFilesUploads(vctx, req.doc, req.ownerHandle, req.appSlug);
118
- if (!filesCheck.ok) {
119
- await ctx.send.send(ctx, {
120
- type: "vibes.diy.res-error",
121
- error: { message: `Invalid file reference: ${filesCheck.reason}` },
122
- });
123
- return Result.Ok(EventoResult.Continue);
124
- }
125
- let accessResult;
126
- let grantsReduceBefore;
127
- const afbRow = await resolveAccessBinding(vctx, req.ownerHandle, req.appSlug, req.dbName);
128
- if (!userId && !afbRow?.accessFnCid) {
293
+ if (answer.kind === "denied") {
129
294
  await ctx.send.send(ctx, {
130
295
  type: "vibes.diy.res-error",
131
- error: { message: "Access denied" },
296
+ error: { message: answer.message, ...(answer.code === undefined ? {} : { code: answer.code }) },
132
297
  });
133
298
  return Result.Ok(EventoResult.Continue);
134
299
  }
135
- const docId = req.docId ?? vctx.sthis.timeOrderedNextId().str;
136
- let headRow;
137
- if (req.docId) {
138
- const tDocs = vctx.sql.tables.appDocuments;
139
- headRow = await vctx.sql.db
140
- .select({ data: tDocs.data, deleted: tDocs.deleted })
141
- .from(tDocs)
142
- .where(and(eq(tDocs.ownerHandle, req.ownerHandle), eq(tDocs.appSlug, req.appSlug), eq(tDocs.dbName, req.dbName), eq(tDocs.docId, req.docId)))
143
- .orderBy(desc(tDocs.seq))
144
- .limit(1)
145
- .then((r) => r[0]);
146
- }
147
- if (afbRow?.accessFnCid && !vctx.invokeAccessFn) {
148
- await ctx.send.send(ctx, {
149
- type: "vibes.diy.res-error",
150
- error: { message: "Access function unavailable" },
151
- });
152
- return Result.Ok(EventoResult.Continue);
153
- }
154
- if (afbRow?.accessFnCid && vctx.invokeAccessFn) {
155
- const { userContext, unboundActingHandle } = await resolveWriterContext(vctx, {
156
- userId,
157
- ownerHandle: req.ownerHandle,
158
- ...(req.actingHandle !== undefined ? { actingHandle: req.actingHandle } : {}),
159
- });
160
- if (unboundActingHandle) {
161
- await ctx.send.send(ctx, {
162
- type: "vibes.diy.res-error",
163
- error: { message: "Acting handle not bound" },
164
- });
165
- return Result.Ok(EventoResult.Continue);
166
- }
167
- const oldDoc = headRow?.data ?? null;
168
- const adminActive = docAccessOverride;
169
- const gate = await runPutAccessGate(vctx, {
170
- ownerHandle: req.ownerHandle,
171
- appSlug: req.appSlug,
172
- dbName: req.dbName,
173
- fnCid: afbRow.accessFnCid,
174
- accessFnAssetUri: afbRow.accessFnAssetUri,
175
- accessFnDbName: afbRow.dbName,
176
- docId,
177
- doc: req.doc,
178
- oldDoc,
179
- oldDocDeleted: headRow?.deleted === 1,
180
- userContext,
181
- adminMode: adminActive,
182
- });
183
- if (gate.kind === "deny") {
184
- await ctx.send.send(ctx, {
185
- type: "vibes.diy.res-error",
186
- error: { message: gate.message, code: gate.code },
187
- });
188
- return Result.Ok(EventoResult.Continue);
300
+ await ctx.send.send(ctx, {
301
+ type: "vibes.diy.res-put-doc",
302
+ status: "ok",
303
+ id: answer.docId,
304
+ });
305
+ return Result.Ok(EventoResult.Continue);
306
+ }),
307
+ };
308
+ export async function deleteDocForCaller(vctx, args) {
309
+ if (addressesDirectChannel(args.ownerHandle))
310
+ return { kind: "denied", message: "Access denied" };
311
+ const userId = args.userId;
312
+ let pinnedWriter;
313
+ if (args.actingHandle !== undefined) {
314
+ const writer = await resolveWriterContext(vctx, {
315
+ userId,
316
+ ownerHandle: args.ownerHandle,
317
+ actingHandle: args.actingHandle,
318
+ });
319
+ if (writer.unboundActingHandle)
320
+ return { kind: "denied", message: "Acting handle not bound" };
321
+ pinnedWriter = writer.userContext;
322
+ }
323
+ const now = new Date().toISOString();
324
+ const t = vctx.sql.tables.appDocuments;
325
+ const dbName = args.dbName;
326
+ const afbRow = await resolveAccessBinding(vctx, args.ownerHandle, args.appSlug, dbName);
327
+ const effectiveFnCid = afbRow?.accessFnCid;
328
+ const delGate = await runDeleteAccessGate(vctx, {
329
+ ownerHandle: args.ownerHandle,
330
+ appSlug: args.appSlug,
331
+ dbName: args.dbName,
332
+ docId: args.docId,
333
+ userId,
334
+ adminMode: args.adminMode,
335
+ ...(pinnedWriter !== undefined ? { userContext: pinnedWriter } : {}),
336
+ lane: "interactive",
337
+ });
338
+ if (delGate.kind === "deny")
339
+ return { kind: "denied", message: delGate.message };
340
+ const tOutputs = vctx.sql.tables.accessFnOutputs;
341
+ let deletedDocHadGrants = false;
342
+ let channels = [];
343
+ const deleteOutputSidecar = async (_seq, exec) => {
344
+ const outRow = await exec
345
+ .select({ output: tOutputs.output, hasGrants: tOutputs.hasGrants, fnCid: tOutputs.fnCid })
346
+ .from(tOutputs)
347
+ .where(and(eq(tOutputs.ownerHandle, args.ownerHandle), eq(tOutputs.appSlug, args.appSlug), eq(tOutputs.dbName, dbName), eq(tOutputs.docId, args.docId)))
348
+ .limit(1)
349
+ .then((r) => r[0]);
350
+ const outputIsLive = effectiveFnCid !== undefined && outRow !== undefined;
351
+ if (outputIsLive) {
352
+ deletedDocHadGrants = outRow.hasGrants === 1;
353
+ if (outRow.output) {
354
+ try {
355
+ const parsed = JSON.parse(outRow.output);
356
+ channels = [...normalizeChannels(parsed.channels ?? []), ...audienceChannelsOf(parsed), ...inheritChannelOf(parsed)];
357
+ if (hasInheritChannel(channels))
358
+ channels.push(INHERIT_CAPABILITY_CHANNEL);
359
+ }
360
+ catch (e) {
361
+ console.error("deleteDoc stored-channels parse error:", e);
362
+ }
189
363
  }
190
- accessResult = gate.descriptor;
191
- grantsReduceBefore = gate.grantsReduceBefore;
192
364
  }
193
- const now = new Date().toISOString();
194
- const dbName = req.dbName;
195
- const oldFeedChannels = afbRow?.accessFnCid
196
- ? await loadDocChannels(vctx, { ownerHandle: req.ownerHandle, appSlug: req.appSlug, dbName, docId })
197
- : [];
198
- let persisted;
199
- try {
200
- persisted = await persistPutRevision(vctx, {
201
- ownerHandle: req.ownerHandle,
202
- appSlug: req.appSlug,
365
+ await exec
366
+ .delete(tOutputs)
367
+ .where(and(eq(tOutputs.ownerHandle, args.ownerHandle), eq(tOutputs.appSlug, args.appSlug), eq(tOutputs.dbName, dbName), eq(tOutputs.docId, args.docId)));
368
+ orThrow(await syncChannelIndex(exec, vctx.sql.tables, { ownerHandle: args.ownerHandle, appSlug: args.appSlug, dbName, docId: args.docId }, null), "frontend delete");
369
+ };
370
+ let delAlloc;
371
+ try {
372
+ delAlloc = await allocateAndInsertRevision({
373
+ db: vctx.sql.db,
374
+ flavour: vctx.sql.flavour,
375
+ table: t,
376
+ row: {
377
+ ownerHandle: args.ownerHandle,
378
+ appSlug: args.appSlug,
203
379
  dbName,
204
- docId,
205
- doc: req.doc,
380
+ docId: args.docId,
206
381
  userId,
207
- now,
208
- accessResult,
209
- accessFnCid: afbRow?.accessFnCid,
210
- });
211
- }
212
- catch (err) {
213
- if (err instanceof SeqConflictError) {
214
- await ctx.send.send(ctx, {
215
- type: "vibes.diy.res-error",
216
- error: { code: "conflict", message: `write conflict: head is at seq ${err.currentHeadSeq}, retry the write` },
217
- });
218
- return Result.Ok(EventoResult.Continue);
219
- }
220
- throw err;
221
- }
222
- const { alloc, storeAccessOutput, grantUpsertErr, noopRefreshedStaleOutput } = persisted;
223
- const notifyGrantDeltaIfChanged = () => {
224
- if (!storeAccessOutput || !grantsReduceBefore)
225
- return;
226
- notifyViewerGrantDeltaIfChanged(vctx, {
227
- ownerHandle: req.ownerHandle,
228
- appSlug: req.appSlug,
229
- docId,
230
- descriptor: storeAccessOutput.descriptor,
231
- outputHasGrants: storeAccessOutput.outputHasGrants,
232
- grantsReduceBefore,
233
- senderConnId: clientWsSend(ctx).connId,
234
- });
235
- };
236
- if (!alloc.inserted) {
237
- if (grantUpsertErr) {
238
- console.warn("AccessFnOutputs stale-row refresh on no-op failed:", grantUpsertErr);
239
- }
240
- else if (noopRefreshedStaleOutput) {
241
- notifyGrantDeltaIfChanged();
242
- }
243
- await ctx.send.send(ctx, {
244
- type: "vibes.diy.res-put-doc",
245
- status: "ok",
246
- id: docId,
247
- });
248
- return Result.Ok(EventoResult.Continue);
382
+ data: {},
383
+ deleted: 1,
384
+ created: now,
385
+ },
386
+ sidecar: deleteOutputSidecar,
387
+ });
388
+ }
389
+ catch (err) {
390
+ if (err instanceof SeqConflictError) {
391
+ return {
392
+ kind: "denied",
393
+ code: "conflict",
394
+ message: `write conflict: head is at seq ${err.currentHeadSeq}, retry the delete`,
395
+ };
249
396
  }
250
- const nextSeq = alloc.seq;
251
- await bestEffortChannelChange({
397
+ throw err;
398
+ }
399
+ if (delAlloc.inserted) {
400
+ await bestEffortAppendDelete({
252
401
  db: vctx.sql.db,
253
402
  flavour: vctx.sql.flavour,
254
- ownerHandle: req.ownerHandle,
255
- appSlug: req.appSlug,
403
+ ownerHandle: args.ownerHandle,
404
+ appSlug: args.appSlug,
256
405
  dbName,
257
- docId,
258
- seq: nextSeq,
259
- oldChannels: oldFeedChannels,
260
- newChannels: channelSetOfDescriptor(accessResult),
261
- alwaysPut: true,
406
+ docId: args.docId,
407
+ seq: delAlloc.seq,
262
408
  created: now,
263
409
  });
410
+ }
411
+ if (delAlloc.inserted) {
264
412
  await emitBackendOnChange(vctx, {
265
- ownerHandle: req.ownerHandle,
266
- appSlug: req.appSlug,
413
+ ownerHandle: args.ownerHandle,
414
+ appSlug: args.appSlug,
267
415
  dbName,
268
- docId,
269
- seq: nextSeq,
270
- deleted: false,
271
- doc: req.doc,
416
+ docId: args.docId,
417
+ seq: delAlloc.seq,
418
+ deleted: true,
419
+ doc: {},
272
420
  originDepth: 0,
273
421
  writerUserId: userId,
274
422
  });
275
- await emitCommentPostedEvent(vctx, {
276
- ownerHandle: req.ownerHandle,
277
- appSlug: req.appSlug,
278
- dbName,
279
- docId,
280
- userId,
281
- email: req._auth?.verifiedAuth.claims.params.email,
282
- nextSeq,
283
- now,
284
- });
285
- const putFanoutChannels = [
286
- ...normalizeChannels(accessResult?.channels ?? []),
287
- ...(accessResult === undefined ? [] : audienceChannelsOf(accessResult)),
288
- ...(accessResult === undefined ? [] : inheritChannelOf(accessResult)),
289
- ];
290
- if (hasInheritChannel(putFanoutChannels))
291
- putFanoutChannels.push(INHERIT_CAPABILITY_CHANNEL);
292
- notifyDocChangedFanOut(vctx, {
293
- ownerHandle: req.ownerHandle,
294
- appSlug: req.appSlug,
295
- dbName,
296
- docId,
297
- channels: putFanoutChannels,
298
- senderConnId: clientWsSend(ctx).connId,
299
- });
300
- if (storeAccessOutput) {
301
- if (grantUpsertErr) {
302
- console.error("AccessFnOutputs upsert failed:", grantUpsertErr);
303
- if (storeAccessOutput.outputHasGrants === 1) {
304
- await ctx.send.send(ctx, {
305
- type: "vibes.diy.res-error",
306
- error: { message: "grant storage failed — retry the write" },
307
- });
308
- return Result.Ok(EventoResult.Continue);
309
- }
310
- }
311
- else {
312
- notifyGrantDeltaIfChanged();
313
- }
314
- }
315
- await ctx.send.send(ctx, {
316
- type: "vibes.diy.res-put-doc",
317
- status: "ok",
318
- id: docId,
423
+ }
424
+ await (args.fanOutDocChanged ?? notifyDocChangedFanOut)(vctx, {
425
+ ownerHandle: args.ownerHandle,
426
+ appSlug: args.appSlug,
427
+ dbName,
428
+ docId: args.docId,
429
+ channels,
430
+ senderConnId: args.senderConnId ?? "",
431
+ });
432
+ if (deletedDocHadGrants) {
433
+ notifyViewerGrantsChangedFanOut(vctx, {
434
+ ownerHandle: args.ownerHandle,
435
+ appSlug: args.appSlug,
436
+ senderConnId: args.senderConnId ?? "",
319
437
  });
320
- return Result.Ok(EventoResult.Continue);
321
- }),
322
- };
438
+ }
439
+ purgeRootHtmlCacheSameColo(vctx, args.ownerHandle, args.appSlug);
440
+ return { kind: "ok", docId: args.docId };
441
+ }
323
442
  export const deleteDocEvento = {
324
443
  hash: "delete-doc",
325
444
  validate: unwrapMsgBase(async (msg) => {
@@ -332,146 +451,27 @@ export const deleteDocEvento = {
332
451
  handle: checkAuth(async (ctx) => {
333
452
  const req = ctx.validated.payload;
334
453
  const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
335
- const userId = req._auth.verifiedAuth.claims.userId;
336
- let pinnedWriter;
337
- if (req.actingHandle !== undefined) {
338
- const writer = await resolveWriterContext(vctx, {
339
- userId,
340
- ownerHandle: req.ownerHandle,
341
- actingHandle: req.actingHandle,
342
- });
343
- if (writer.unboundActingHandle) {
344
- await ctx.send.send(ctx, {
345
- type: "vibes.diy.res-error",
346
- error: { message: "Acting handle not bound" },
347
- });
348
- return Result.Ok(EventoResult.Continue);
349
- }
350
- pinnedWriter = writer.userContext;
351
- }
352
- const now = new Date().toISOString();
353
- const t = vctx.sql.tables.appDocuments;
354
- const dbName = req.dbName;
355
- const afbRow = await resolveAccessBinding(vctx, req.ownerHandle, req.appSlug, dbName);
356
- const effectiveFnCid = afbRow?.accessFnCid;
357
- const delGate = await runDeleteAccessGate(vctx, {
454
+ const answer = await deleteDocForCaller(vctx, {
455
+ userId: req._auth.verifiedAuth.claims.userId,
358
456
  ownerHandle: req.ownerHandle,
359
457
  appSlug: req.appSlug,
360
458
  dbName: req.dbName,
361
459
  docId: req.docId,
362
- userId,
363
460
  adminMode: connectionAdminMode(ctx) || req.adminMode === true,
364
- ...(pinnedWriter !== undefined ? { userContext: pinnedWriter } : {}),
365
- lane: "interactive",
366
- });
367
- if (delGate.kind === "deny") {
368
- await ctx.send.send(ctx, { type: "vibes.diy.res-error", error: { message: delGate.message } });
369
- return Result.Ok(EventoResult.Continue);
370
- }
371
- const tOutputs = vctx.sql.tables.accessFnOutputs;
372
- let deletedDocHadGrants = false;
373
- let channels = [];
374
- const deleteOutputSidecar = async (_seq, exec) => {
375
- const outRow = await exec
376
- .select({ output: tOutputs.output, hasGrants: tOutputs.hasGrants, fnCid: tOutputs.fnCid })
377
- .from(tOutputs)
378
- .where(and(eq(tOutputs.ownerHandle, req.ownerHandle), eq(tOutputs.appSlug, req.appSlug), eq(tOutputs.dbName, dbName), eq(tOutputs.docId, req.docId)))
379
- .limit(1)
380
- .then((r) => r[0]);
381
- const outputIsLive = effectiveFnCid !== undefined && outRow !== undefined;
382
- if (outputIsLive) {
383
- deletedDocHadGrants = outRow.hasGrants === 1;
384
- if (outRow.output) {
385
- try {
386
- const parsed = JSON.parse(outRow.output);
387
- channels = [...normalizeChannels(parsed.channels ?? []), ...audienceChannelsOf(parsed), ...inheritChannelOf(parsed)];
388
- if (hasInheritChannel(channels))
389
- channels.push(INHERIT_CAPABILITY_CHANNEL);
390
- }
391
- catch (e) {
392
- console.error("deleteDoc stored-channels parse error:", e);
393
- }
394
- }
395
- }
396
- await exec
397
- .delete(tOutputs)
398
- .where(and(eq(tOutputs.ownerHandle, req.ownerHandle), eq(tOutputs.appSlug, req.appSlug), eq(tOutputs.dbName, dbName), eq(tOutputs.docId, req.docId)));
399
- orThrow(await syncChannelIndex(exec, vctx.sql.tables, { ownerHandle: req.ownerHandle, appSlug: req.appSlug, dbName, docId: req.docId }, null), "frontend delete");
400
- };
401
- let delAlloc;
402
- try {
403
- delAlloc = await allocateAndInsertRevision({
404
- db: vctx.sql.db,
405
- flavour: vctx.sql.flavour,
406
- table: t,
407
- row: {
408
- ownerHandle: req.ownerHandle,
409
- appSlug: req.appSlug,
410
- dbName,
411
- docId: req.docId,
412
- userId: req._auth.verifiedAuth.claims.userId,
413
- data: {},
414
- deleted: 1,
415
- created: now,
416
- },
417
- sidecar: deleteOutputSidecar,
418
- });
419
- }
420
- catch (err) {
421
- if (err instanceof SeqConflictError) {
422
- await ctx.send.send(ctx, {
423
- type: "vibes.diy.res-error",
424
- error: { code: "conflict", message: `write conflict: head is at seq ${err.currentHeadSeq}, retry the delete` },
425
- });
426
- return Result.Ok(EventoResult.Continue);
427
- }
428
- throw err;
429
- }
430
- if (delAlloc.inserted) {
431
- await bestEffortAppendDelete({
432
- db: vctx.sql.db,
433
- flavour: vctx.sql.flavour,
434
- ownerHandle: req.ownerHandle,
435
- appSlug: req.appSlug,
436
- dbName,
437
- docId: req.docId,
438
- seq: delAlloc.seq,
439
- created: now,
440
- });
441
- }
442
- if (delAlloc.inserted) {
443
- await emitBackendOnChange(vctx, {
444
- ownerHandle: req.ownerHandle,
445
- appSlug: req.appSlug,
446
- dbName,
447
- docId: req.docId,
448
- seq: delAlloc.seq,
449
- deleted: true,
450
- doc: {},
451
- originDepth: 0,
452
- writerUserId: req._auth.verifiedAuth.claims.userId,
453
- });
454
- }
455
- notifyDocChangedFanOut(vctx, {
456
- ownerHandle: req.ownerHandle,
457
- appSlug: req.appSlug,
458
- dbName,
459
- docId: req.docId,
460
- channels,
461
+ ...(req.actingHandle === undefined ? {} : { actingHandle: req.actingHandle }),
461
462
  senderConnId: clientWsSend(ctx).connId,
462
463
  });
463
- if (deletedDocHadGrants) {
464
- notifyViewerGrantsChangedFanOut(vctx, {
465
- ownerHandle: req.ownerHandle,
466
- appSlug: req.appSlug,
467
- senderConnId: clientWsSend(ctx).connId,
464
+ if (answer.kind === "denied") {
465
+ await ctx.send.send(ctx, {
466
+ type: "vibes.diy.res-error",
467
+ error: { message: answer.message, ...(answer.code === undefined ? {} : { code: answer.code }) },
468
468
  });
469
+ return Result.Ok(EventoResult.Continue);
469
470
  }
470
- purgeRootHtmlCacheSameColo(vctx, req.ownerHandle, req.appSlug);
471
471
  await ctx.send.send(ctx, {
472
472
  type: "vibes.diy.res-delete-doc",
473
473
  status: "ok",
474
- id: req.docId,
474
+ id: answer.docId,
475
475
  });
476
476
  return Result.Ok(EventoResult.Continue);
477
477
  }),