@vibes.diy/api-svc 5.0.1 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/cf-serve.d.ts +4 -1
  2. package/cf-serve.js +23 -0
  3. package/cf-serve.js.map +1 -1
  4. package/create-handler.d.ts +9 -1
  5. package/create-handler.js +5 -0
  6. package/create-handler.js.map +1 -1
  7. package/evento-handler-manifest.js +25 -0
  8. package/evento-handler-manifest.js.map +1 -1
  9. package/index.d.ts +8 -0
  10. package/index.js +8 -0
  11. package/index.js.map +1 -1
  12. package/intern/backend-db-callback.js +56 -0
  13. package/intern/backend-db-callback.js.map +1 -1
  14. package/intern/backend-egress.d.ts +1 -1
  15. package/intern/backend-egress.js +74 -22
  16. package/intern/backend-egress.js.map +1 -1
  17. package/intern/backend-policy-version.d.ts +1 -1
  18. package/intern/backend-policy-version.js +1 -1
  19. package/intern/delete-access-gate.d.ts +4 -0
  20. package/intern/delete-access-gate.js +46 -5
  21. package/intern/delete-access-gate.js.map +1 -1
  22. package/intern/egress-platform-list.d.ts +1 -0
  23. package/intern/egress-platform-list.js +4 -0
  24. package/intern/egress-platform-list.js.map +1 -1
  25. package/intern/emit-notification.d.ts +2 -1
  26. package/intern/emit-notification.js +1 -0
  27. package/intern/emit-notification.js.map +1 -1
  28. package/intern/ensure-app-metadata.js +4 -0
  29. package/intern/ensure-app-metadata.js.map +1 -1
  30. package/intern/ensure-chat-id.js +13 -2
  31. package/intern/ensure-chat-id.js.map +1 -1
  32. package/intern/ensure-slug-binding.js +20 -2
  33. package/intern/ensure-slug-binding.js.map +1 -1
  34. package/intern/notify-social-edge.d.ts +9 -0
  35. package/intern/notify-social-edge.js +45 -0
  36. package/intern/notify-social-edge.js.map +1 -0
  37. package/intern/pre-allocate.d.ts +1 -0
  38. package/intern/pre-allocate.js +1 -0
  39. package/intern/pre-allocate.js.map +1 -1
  40. package/intern/process-access-bindings.js +12 -2
  41. package/intern/process-access-bindings.js.map +1 -1
  42. package/intern/put-access-gate.js +25 -3
  43. package/intern/put-access-gate.js.map +1 -1
  44. package/intern/read-anonymous-seed-docs.js +1 -1
  45. package/intern/read-anonymous-seed-docs.js.map +1 -1
  46. package/intern/recovery.d.ts +7 -1
  47. package/intern/recovery.js +5 -5
  48. package/intern/recovery.js.map +1 -1
  49. package/intern/render-vibe.js +7 -3
  50. package/intern/render-vibe.js.map +1 -1
  51. package/intern/slot-assembler.d.ts +2 -0
  52. package/intern/slot-assembler.js +9 -3
  53. package/intern/slot-assembler.js.map +1 -1
  54. package/intern/write-apps.js +9 -2
  55. package/intern/write-apps.js.map +1 -1
  56. package/package.json +16 -15
  57. package/public/access-function.js +4 -1
  58. package/public/access-function.js.map +1 -1
  59. package/public/access-helpers.js +5 -0
  60. package/public/access-helpers.js.map +1 -1
  61. package/public/app-documents-broadcast.d.ts +8 -0
  62. package/public/app-documents-broadcast.js +15 -0
  63. package/public/app-documents-broadcast.js.map +1 -1
  64. package/public/app-documents-grant-delta.js +3 -2
  65. package/public/app-documents-grant-delta.js.map +1 -1
  66. package/public/app-documents-read-eventos.js +72 -13
  67. package/public/app-documents-read-eventos.js.map +1 -1
  68. package/public/app-documents-write-eventos.js +28 -11
  69. package/public/app-documents-write-eventos.js.map +1 -1
  70. package/public/app-settings-sql-upsert.js +4 -1
  71. package/public/app-settings-sql-upsert.js.map +1 -1
  72. package/public/audience-arms-store.d.ts +21 -0
  73. package/public/audience-arms-store.js +110 -0
  74. package/public/audience-arms-store.js.map +1 -0
  75. package/public/audience-channels.d.ts +14 -0
  76. package/public/audience-channels.js +71 -0
  77. package/public/audience-channels.js.map +1 -0
  78. package/public/audience-derived-trigger.d.ts +15 -0
  79. package/public/audience-derived-trigger.js +45 -0
  80. package/public/audience-derived-trigger.js.map +1 -0
  81. package/public/audience-key-patch.d.ts +16 -0
  82. package/public/audience-key-patch.js +38 -0
  83. package/public/audience-key-patch.js.map +1 -0
  84. package/public/channel-read-filter.js +4 -2
  85. package/public/channel-read-filter.js.map +1 -1
  86. package/public/curated-new-vibes.d.ts +2 -0
  87. package/public/curated-new-vibes.js +1 -1
  88. package/public/curated-new-vibes.js.map +1 -1
  89. package/public/developer-grants-flow.d.ts +5 -0
  90. package/public/developer-grants-flow.js +218 -0
  91. package/public/developer-grants-flow.js.map +1 -0
  92. package/public/developer-grants.d.ts +12 -0
  93. package/public/developer-grants.js +40 -0
  94. package/public/developer-grants.js.map +1 -0
  95. package/public/ensure-app-settings.js +69 -3
  96. package/public/ensure-app-settings.js.map +1 -1
  97. package/public/grant-reduce.d.ts +4 -2
  98. package/public/grant-reduce.js +11 -3
  99. package/public/grant-reduce.js.map +1 -1
  100. package/public/home-leaderboard.d.ts +2 -0
  101. package/public/home-leaderboard.js +1 -1
  102. package/public/home-leaderboard.js.map +1 -1
  103. package/public/invite-unsubscribe.d.ts +3 -0
  104. package/public/invite-unsubscribe.js +31 -0
  105. package/public/invite-unsubscribe.js.map +1 -0
  106. package/public/related-vibes.d.ts +3 -0
  107. package/public/related-vibes.js +134 -0
  108. package/public/related-vibes.js.map +1 -0
  109. package/public/serv-entry-point.js +2 -2
  110. package/public/serv-entry-point.js.map +1 -1
  111. package/public/social-flow.d.ts +21 -0
  112. package/public/social-flow.js +765 -0
  113. package/public/social-flow.js.map +1 -0
  114. package/public/social-invite-flow.d.ts +3 -0
  115. package/public/social-invite-flow.js +73 -0
  116. package/public/social-invite-flow.js.map +1 -0
  117. package/public/social-invite-signup-match.d.ts +6 -0
  118. package/public/social-invite-signup-match.js +38 -0
  119. package/public/social-invite-signup-match.js.map +1 -0
  120. package/public/social-invite-store.d.ts +70 -0
  121. package/public/social-invite-store.js +172 -0
  122. package/public/social-invite-store.js.map +1 -0
  123. package/public/social-store.d.ts +109 -0
  124. package/public/social-store.js +469 -0
  125. package/public/social-store.js.map +1 -0
  126. package/public/who-am-i.js +5 -3
  127. package/public/who-am-i.js.map +1 -1
  128. package/svc-ws-send-provider.d.ts +2 -0
  129. package/svc-ws-send-provider.js +2 -0
  130. package/svc-ws-send-provider.js.map +1 -1
  131. package/types.d.ts +20 -1
  132. package/types.js.map +1 -1
  133. package/usage-report/README.md +1 -0
  134. package/usage-report/vibe-usage.py +171 -0
@@ -0,0 +1,765 @@
1
+ import { Result, Option, EventoResult, exception2Result } from "@adviser/cement";
2
+ import { isReqSocialFollow, isReqSocialUnfollow, isReqSocialApprove, isReqSocialRemoveFollower, isReqSocialBlock, isReqSocialUnblock, isReqSocialList, isReqSocialRelationship, isReqSocialSetPrivacy, isReqSocialRedeem, isReqSubscribeSocial, isReqSocialImportFriendEdges, isReqAudienceArmState, isReqSetAudienceArm, isReqSetAudiencePolicy, isReqListAudienceArms, } from "@vibes.diy/api-types";
3
+ import { and, eq } from "drizzle-orm/sql/expressions";
4
+ import { unwrapMsgBase } from "../unwrap-msg-base.js";
5
+ import { checkAuth } from "../check-auth.js";
6
+ import { patchAudienceKeys, subscribedDbPrefixes } from "./audience-key-patch.js";
7
+ import { audienceSubjectFromChannel } from "./audience-channels.js";
8
+ import { isArmed as isAudienceArmed, getArmState, setArm, listArmsForSubject } from "./audience-arms-store.js";
9
+ import { getAppPrivacySensitive } from "./audience-derived-trigger.js";
10
+ import { resolveActiveHandle } from "./resolve-active-handle.js";
11
+ import { ensureUserSlug, persistDefaultUserSlug } from "../intern/ensure-slug-binding.js";
12
+ import { notifySocialEdge } from "../intern/notify-social-edge.js";
13
+ import { followHandle, unfollowHandle, removeFollower, approveFollower, blockHandle, unblockHandle, getRelationship, listSocial, isHandlePrivate, setHandlePrivacy, getAudiencePolicy, setAudiencePolicy, SocialBlockedError, SelfFollowError, importFriendEdges, establishMutualActiveEdges, } from "./social-store.js";
14
+ import { markInviteRedeemed, selectPendingInviteByToken } from "./social-invite-store.js";
15
+ import { adminUserIdSet } from "./ensure-app-settings.js";
16
+ function clientWsSend(ctx) {
17
+ return ctx.send.provider;
18
+ }
19
+ export function socialSubscriptionKey(handle) {
20
+ return `social/${handle}`;
21
+ }
22
+ async function sendSocialEdgeEvent(vctx, value, senderConnId) {
23
+ const evt = {
24
+ ...value,
25
+ type: "vibes.diy.evt-social-edge",
26
+ };
27
+ await vctx.postQueue({
28
+ payload: evt,
29
+ tid: "queue-event",
30
+ src: "social-flow",
31
+ dst: "vibes-service",
32
+ ttl: 1,
33
+ });
34
+ if (vctx.notifySocialEdgeChanged) {
35
+ vctx.notifySocialEdgeChanged(evt, senderConnId ?? "").catch((e) => console.error("social-edge fan-out error:", e));
36
+ }
37
+ }
38
+ async function selectEdge(vctx, pair) {
39
+ const t = vctx.sql.tables.socialEdges;
40
+ const row = await vctx.sql.db
41
+ .select()
42
+ .from(t)
43
+ .where(and(eq(t.followerHandle, pair.follower), eq(t.followeeHandle, pair.followee)))
44
+ .limit(1)
45
+ .then((r) => r[0]);
46
+ if (row === undefined)
47
+ return undefined;
48
+ return {
49
+ followerHandle: row.followerHandle,
50
+ followeeHandle: row.followeeHandle,
51
+ state: row.state,
52
+ created: row.created,
53
+ updated: row.updated,
54
+ };
55
+ }
56
+ const NO_HANDLE_ERROR = {
57
+ type: "vibes.diy.res-error",
58
+ error: { message: "no handle bound to this user", code: "social-no-handle" },
59
+ };
60
+ async function patchLocalAudienceKeys(vctx, conn, patch) {
61
+ let armLookup;
62
+ if (patch.op === "add") {
63
+ const cache = new Map();
64
+ for (const prefix of subscribedDbPrefixes(conn)) {
65
+ const [ownerHandle, appSlug] = prefix.split("/");
66
+ for (const ch of patch.channels) {
67
+ const subject = audienceSubjectFromChannel(ch);
68
+ if (subject === undefined)
69
+ continue;
70
+ const cacheKey = `${ownerHandle}/${appSlug}/${subject}`;
71
+ if (cache.has(cacheKey))
72
+ continue;
73
+ cache.set(cacheKey, await isAudienceArmed(vctx, { subjectHandle: subject, ownerHandle, appSlug }));
74
+ }
75
+ }
76
+ armLookup = (ownerHandle, appSlug, subjectHandle) => cache.get(`${ownerHandle}/${appSlug}/${subjectHandle}`) ?? false;
77
+ }
78
+ patchAudienceKeys(conn, patch, {
79
+ register: (key) => vctx.registerDocSubscription?.(key) ?? Promise.resolve(),
80
+ deregister: (key) => vctx.deregisterDocSubscription?.(key) ?? Promise.resolve(),
81
+ isArmed: armLookup,
82
+ });
83
+ }
84
+ async function reconcileAudienceKeys(vctx, edge, op) {
85
+ const follower = edge.followerHandle;
86
+ const followee = edge.followeeHandle;
87
+ let mutual = op === "remove";
88
+ if (op === "add") {
89
+ const rReverse = await exception2Result(() => selectEdge(vctx, { follower: followee, followee: follower }));
90
+ if (rReverse.isErr()) {
91
+ console.error("audience reconciliation reverse-edge lookup failed (mutuals skipped):", rReverse.Err());
92
+ }
93
+ else {
94
+ mutual = rReverse.Ok()?.state === "active";
95
+ }
96
+ }
97
+ const followerChannels = [`~aud:followers:${followee}`];
98
+ if (mutual)
99
+ followerChannels.push(`~aud:mutuals:${followee}`);
100
+ const followeeChannels = mutual ? [`~aud:mutuals:${follower}`] : [];
101
+ if (op === "add" && vctx.registerAudienceReader !== undefined) {
102
+ const patchTargets = new Set();
103
+ for (const conn of vctx.connections) {
104
+ if (conn.subscribedDocHandle === follower)
105
+ patchTargets.add(follower);
106
+ if (conn.subscribedDocHandle === followee && followeeChannels.length > 0)
107
+ patchTargets.add(followee);
108
+ }
109
+ for (const handle of patchTargets) {
110
+ const rReg = await exception2Result(() => vctx.registerAudienceReader?.(handle) ?? Promise.resolve());
111
+ if (rReg.isErr())
112
+ console.error("aud-reader registration failed (local add leg):", rReg.Err());
113
+ }
114
+ }
115
+ for (const conn of vctx.connections) {
116
+ if (conn.subscribedDocHandle === follower) {
117
+ await patchLocalAudienceKeys(vctx, conn, { op, channels: followerChannels });
118
+ }
119
+ if (conn.subscribedDocHandle === followee && followeeChannels.length > 0) {
120
+ await patchLocalAudienceKeys(vctx, conn, { op, channels: followeeChannels });
121
+ }
122
+ }
123
+ const fan = vctx.patchAudienceKeysRemote;
124
+ if (fan !== undefined && op === "remove") {
125
+ const rFollower = await exception2Result(() => fan(follower, { op, channels: followerChannels }));
126
+ if (rFollower.isErr())
127
+ console.error("cross-shard audience-key fan failed (follower):", rFollower.Err());
128
+ if (followeeChannels.length > 0) {
129
+ const rFollowee = await exception2Result(() => fan(followee, { op, channels: followeeChannels }));
130
+ if (rFollowee.isErr())
131
+ console.error("cross-shard audience-key fan failed (followee):", rFollowee.Err());
132
+ }
133
+ }
134
+ }
135
+ export const socialFollowEvento = {
136
+ hash: "social-follow",
137
+ validate: unwrapMsgBase(async (msg) => {
138
+ if (isReqSocialFollow(msg.payload)) {
139
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
140
+ }
141
+ return Result.Ok(Option.None());
142
+ }),
143
+ handle: checkAuth(async (ctx) => {
144
+ const req = ctx.validated.payload;
145
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
146
+ const userId = req._auth.verifiedAuth.claims.userId;
147
+ const actingHandle = await resolveActiveHandle(vctx, userId);
148
+ if (actingHandle === undefined) {
149
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
150
+ return Result.Ok(EventoResult.Continue);
151
+ }
152
+ const pair = { follower: actingHandle, followee: req.targetHandle };
153
+ const rFollow = await followHandle(vctx, pair);
154
+ if (rFollow.isErr()) {
155
+ const err = rFollow.Err();
156
+ if (err instanceof SocialBlockedError || err instanceof SelfFollowError) {
157
+ await ctx.send.send(ctx, {
158
+ type: "vibes.diy.res-error",
159
+ error: { message: err.message, code: "social-follow-refused" },
160
+ });
161
+ return Result.Ok(EventoResult.Continue);
162
+ }
163
+ return Result.Err(rFollow);
164
+ }
165
+ const edge = await selectEdge(vctx, pair);
166
+ if (edge !== undefined) {
167
+ await sendSocialEdgeEvent(vctx, { op: "upsert", edge }, clientWsSend(ctx).connId);
168
+ if (edge.state === "active") {
169
+ await reconcileAudienceKeys(vctx, edge, "add");
170
+ }
171
+ if (rFollow.Ok().created === true) {
172
+ const rNotify = await exception2Result(() => notifySocialEdge(vctx, {
173
+ kind: edge.state === "active" ? "follower-added" : "follow-request",
174
+ followerHandle: pair.follower,
175
+ followeeHandle: pair.followee,
176
+ }));
177
+ if (rNotify.isErr())
178
+ console.error("social notification emit failed (follow):", rNotify.Err());
179
+ }
180
+ }
181
+ await ctx.send.send(ctx, {
182
+ type: "vibes.diy.res-social-follow",
183
+ targetHandle: req.targetHandle,
184
+ state: rFollow.Ok().state,
185
+ });
186
+ return Result.Ok(EventoResult.Continue);
187
+ }),
188
+ };
189
+ export const socialRedeemEvento = {
190
+ hash: "social-redeem",
191
+ validate: unwrapMsgBase(async (msg) => {
192
+ if (isReqSocialRedeem(msg.payload)) {
193
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
194
+ }
195
+ return Result.Ok(Option.None());
196
+ }),
197
+ handle: checkAuth(async (ctx) => {
198
+ const req = ctx.validated.payload;
199
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
200
+ const userId = req._auth.verifiedAuth.claims.userId;
201
+ let redeemerHandle = await resolveActiveHandle(vctx, userId);
202
+ if (redeemerHandle === undefined) {
203
+ const claims = req._auth.verifiedAuth.claims;
204
+ const rEnsured = await ensureUserSlug(vctx, claims, { userId });
205
+ if (rEnsured.isOk()) {
206
+ await persistDefaultUserSlug(vctx, userId, rEnsured.Ok().ownerHandle);
207
+ redeemerHandle = await resolveActiveHandle(vctx, userId);
208
+ }
209
+ if (redeemerHandle === undefined) {
210
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
211
+ return Result.Ok(EventoResult.Continue);
212
+ }
213
+ }
214
+ const nowIso = new Date().toISOString();
215
+ const invite = await selectPendingInviteByToken(vctx.sql, req.token);
216
+ const sendStatus = async (status, inviterHandle) => {
217
+ await ctx.send.send(ctx, {
218
+ type: "vibes.diy.res-social-redeem",
219
+ status,
220
+ ...(inviterHandle ? { inviterHandle } : {}),
221
+ });
222
+ return Result.Ok(EventoResult.Continue);
223
+ };
224
+ if (invite === undefined)
225
+ return sendStatus("invalid");
226
+ if (invite.inviterHandle === redeemerHandle)
227
+ return sendStatus("invalid", invite.inviterHandle);
228
+ const completeMutual = async () => {
229
+ const rEdges = await establishMutualActiveEdges(vctx, invite.inviterHandle, redeemerHandle);
230
+ if (rEdges.isErr())
231
+ console.error("redeem edge establishment failed:", rEdges.Err());
232
+ const changed = rEdges.isOk() ? rEdges.Ok().activeEdges : [];
233
+ const fwd = await selectEdge(vctx, { follower: invite.inviterHandle, followee: redeemerHandle });
234
+ const rev = await selectEdge(vctx, { follower: redeemerHandle, followee: invite.inviterHandle });
235
+ if (fwd?.state !== "active" || rev?.state !== "active") {
236
+ console.error("[social-invite] redeem formed no mutual edge (blocked pair):", invite.inviterHandle, redeemerHandle);
237
+ return sendStatus("invalid", invite.inviterHandle);
238
+ }
239
+ for (const pair of changed) {
240
+ const edge = await selectEdge(vctx, pair);
241
+ if (edge === undefined)
242
+ continue;
243
+ await sendSocialEdgeEvent(vctx, { op: "upsert", edge }, clientWsSend(ctx).connId);
244
+ const rRecon = await exception2Result(() => reconcileAudienceKeys(vctx, edge, "add"));
245
+ if (rRecon.isErr())
246
+ console.error("redeem audience reconcile failed:", rRecon.Err());
247
+ }
248
+ if (changed.length > 0) {
249
+ const rNotify = await exception2Result(() => notifySocialEdge(vctx, {
250
+ kind: "follow-approved",
251
+ followerHandle: invite.inviterHandle,
252
+ followeeHandle: redeemerHandle,
253
+ }));
254
+ if (rNotify.isErr())
255
+ console.error("social notification emit failed (redeem):", rNotify.Err());
256
+ }
257
+ return sendStatus("redeemed", invite.inviterHandle);
258
+ };
259
+ if (invite.state === "redeemed") {
260
+ if (invite.redeemedByUserId === userId)
261
+ return completeMutual();
262
+ return sendStatus("already-redeemed", invite.inviterHandle);
263
+ }
264
+ if (invite.state !== "pending" || invite.expires <= nowIso)
265
+ return sendStatus("expired", invite.inviterHandle);
266
+ const rRedeem = await markInviteRedeemed(vctx.sql, { token: req.token, redeemedByUserId: userId, nowIso });
267
+ if (rRedeem.redeemed === true)
268
+ return completeMutual();
269
+ const after = await selectPendingInviteByToken(vctx.sql, req.token);
270
+ if (after === undefined)
271
+ return sendStatus("invalid");
272
+ if (after.state === "redeemed" && after.redeemedByUserId === userId)
273
+ return completeMutual();
274
+ if (after.state === "redeemed")
275
+ return sendStatus("already-redeemed", invite.inviterHandle);
276
+ if (after.state === "expired" || after.expires <= nowIso)
277
+ return sendStatus("expired", invite.inviterHandle);
278
+ return sendStatus("already-redeemed", invite.inviterHandle);
279
+ }),
280
+ };
281
+ export const socialUnfollowEvento = {
282
+ hash: "social-unfollow",
283
+ validate: unwrapMsgBase(async (msg) => {
284
+ if (isReqSocialUnfollow(msg.payload)) {
285
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
286
+ }
287
+ return Result.Ok(Option.None());
288
+ }),
289
+ handle: checkAuth(async (ctx) => {
290
+ const req = ctx.validated.payload;
291
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
292
+ const userId = req._auth.verifiedAuth.claims.userId;
293
+ const actingHandle = await resolveActiveHandle(vctx, userId);
294
+ if (actingHandle === undefined) {
295
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
296
+ return Result.Ok(EventoResult.Continue);
297
+ }
298
+ const pair = { follower: actingHandle, followee: req.targetHandle };
299
+ const edge = await selectEdge(vctx, pair);
300
+ const rUnfollow = await unfollowHandle(vctx, pair);
301
+ if (rUnfollow.isErr())
302
+ return Result.Err(rUnfollow);
303
+ if (edge !== undefined) {
304
+ await sendSocialEdgeEvent(vctx, { op: "delete", edge }, clientWsSend(ctx).connId);
305
+ await reconcileAudienceKeys(vctx, edge, "remove");
306
+ }
307
+ await ctx.send.send(ctx, {
308
+ type: "vibes.diy.res-social-unfollow",
309
+ targetHandle: req.targetHandle,
310
+ });
311
+ return Result.Ok(EventoResult.Continue);
312
+ }),
313
+ };
314
+ export const socialApproveEvento = {
315
+ hash: "social-approve",
316
+ validate: unwrapMsgBase(async (msg) => {
317
+ if (isReqSocialApprove(msg.payload)) {
318
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
319
+ }
320
+ return Result.Ok(Option.None());
321
+ }),
322
+ handle: checkAuth(async (ctx) => {
323
+ const req = ctx.validated.payload;
324
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
325
+ const userId = req._auth.verifiedAuth.claims.userId;
326
+ const actingHandle = await resolveActiveHandle(vctx, userId);
327
+ if (actingHandle === undefined) {
328
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
329
+ return Result.Ok(EventoResult.Continue);
330
+ }
331
+ const pair = { follower: req.followerHandle, followee: actingHandle };
332
+ const pending = await selectEdge(vctx, pair);
333
+ if (pending === undefined || pending.state !== "requested") {
334
+ await ctx.send.send(ctx, {
335
+ type: "vibes.diy.res-error",
336
+ error: {
337
+ message: `no pending follow request from ${req.followerHandle} to ${actingHandle}`,
338
+ code: "social-approve-not-found",
339
+ },
340
+ });
341
+ return Result.Ok(EventoResult.Continue);
342
+ }
343
+ const rApprove = await approveFollower(vctx, pair);
344
+ if (rApprove.isErr())
345
+ return Result.Err(rApprove);
346
+ const edge = await selectEdge(vctx, pair);
347
+ if (edge === undefined || edge.state !== "active") {
348
+ await ctx.send.send(ctx, {
349
+ type: "vibes.diy.res-error",
350
+ error: {
351
+ message: `approve refused: ${req.followerHandle} -> ${actingHandle}`,
352
+ code: "social-approve-refused",
353
+ },
354
+ });
355
+ return Result.Ok(EventoResult.Continue);
356
+ }
357
+ await sendSocialEdgeEvent(vctx, { op: "upsert", edge }, clientWsSend(ctx).connId);
358
+ await reconcileAudienceKeys(vctx, edge, "add");
359
+ const rNotify = await exception2Result(() => notifySocialEdge(vctx, {
360
+ kind: "follow-approved",
361
+ followerHandle: req.followerHandle,
362
+ followeeHandle: actingHandle,
363
+ }));
364
+ if (rNotify.isErr())
365
+ console.error("social notification emit failed (approve):", rNotify.Err());
366
+ await ctx.send.send(ctx, {
367
+ type: "vibes.diy.res-social-approve",
368
+ followerHandle: req.followerHandle,
369
+ });
370
+ return Result.Ok(EventoResult.Continue);
371
+ }),
372
+ };
373
+ export const socialRemoveFollowerEvento = {
374
+ hash: "social-remove-follower",
375
+ validate: unwrapMsgBase(async (msg) => {
376
+ if (isReqSocialRemoveFollower(msg.payload)) {
377
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
378
+ }
379
+ return Result.Ok(Option.None());
380
+ }),
381
+ handle: checkAuth(async (ctx) => {
382
+ const req = ctx.validated.payload;
383
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
384
+ const userId = req._auth.verifiedAuth.claims.userId;
385
+ const actingHandle = await resolveActiveHandle(vctx, userId);
386
+ if (actingHandle === undefined) {
387
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
388
+ return Result.Ok(EventoResult.Continue);
389
+ }
390
+ const pair = { follower: req.followerHandle, followee: actingHandle };
391
+ const edge = await selectEdge(vctx, pair);
392
+ const rRemove = await removeFollower(vctx, pair);
393
+ if (rRemove.isErr())
394
+ return Result.Err(rRemove);
395
+ if (edge !== undefined) {
396
+ await sendSocialEdgeEvent(vctx, { op: "delete", edge }, clientWsSend(ctx).connId);
397
+ await reconcileAudienceKeys(vctx, edge, "remove");
398
+ }
399
+ await ctx.send.send(ctx, {
400
+ type: "vibes.diy.res-social-remove-follower",
401
+ followerHandle: req.followerHandle,
402
+ });
403
+ return Result.Ok(EventoResult.Continue);
404
+ }),
405
+ };
406
+ export const socialBlockEvento = {
407
+ hash: "social-block",
408
+ validate: unwrapMsgBase(async (msg) => {
409
+ if (isReqSocialBlock(msg.payload)) {
410
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
411
+ }
412
+ return Result.Ok(Option.None());
413
+ }),
414
+ handle: checkAuth(async (ctx) => {
415
+ const req = ctx.validated.payload;
416
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
417
+ const userId = req._auth.verifiedAuth.claims.userId;
418
+ const actingHandle = await resolveActiveHandle(vctx, userId);
419
+ if (actingHandle === undefined) {
420
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
421
+ return Result.Ok(EventoResult.Continue);
422
+ }
423
+ const outgoing = await selectEdge(vctx, { follower: actingHandle, followee: req.targetHandle });
424
+ const incoming = await selectEdge(vctx, { follower: req.targetHandle, followee: actingHandle });
425
+ const rBlock = await blockHandle(vctx, { blocker: actingHandle, blocked: req.targetHandle });
426
+ if (rBlock.isErr())
427
+ return Result.Err(rBlock);
428
+ const senderConnId = clientWsSend(ctx).connId;
429
+ for (const edge of [outgoing, incoming]) {
430
+ if (edge !== undefined) {
431
+ await sendSocialEdgeEvent(vctx, { op: "delete", edge }, senderConnId);
432
+ await reconcileAudienceKeys(vctx, edge, "remove");
433
+ }
434
+ }
435
+ await ctx.send.send(ctx, {
436
+ type: "vibes.diy.res-social-block",
437
+ targetHandle: req.targetHandle,
438
+ });
439
+ return Result.Ok(EventoResult.Continue);
440
+ }),
441
+ };
442
+ export const socialUnblockEvento = {
443
+ hash: "social-unblock",
444
+ validate: unwrapMsgBase(async (msg) => {
445
+ if (isReqSocialUnblock(msg.payload)) {
446
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
447
+ }
448
+ return Result.Ok(Option.None());
449
+ }),
450
+ handle: checkAuth(async (ctx) => {
451
+ const req = ctx.validated.payload;
452
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
453
+ const userId = req._auth.verifiedAuth.claims.userId;
454
+ const actingHandle = await resolveActiveHandle(vctx, userId);
455
+ if (actingHandle === undefined) {
456
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
457
+ return Result.Ok(EventoResult.Continue);
458
+ }
459
+ const rUnblock = await unblockHandle(vctx, { blocker: actingHandle, blocked: req.targetHandle });
460
+ if (rUnblock.isErr())
461
+ return Result.Err(rUnblock);
462
+ await ctx.send.send(ctx, {
463
+ type: "vibes.diy.res-social-unblock",
464
+ targetHandle: req.targetHandle,
465
+ });
466
+ return Result.Ok(EventoResult.Continue);
467
+ }),
468
+ };
469
+ export const socialListEvento = {
470
+ hash: "social-list",
471
+ validate: unwrapMsgBase(async (msg) => {
472
+ if (isReqSocialList(msg.payload)) {
473
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
474
+ }
475
+ return Result.Ok(Option.None());
476
+ }),
477
+ handle: checkAuth(async (ctx) => {
478
+ const req = ctx.validated.payload;
479
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
480
+ const userId = req._auth.verifiedAuth.claims.userId;
481
+ const actingHandle = await resolveActiveHandle(vctx, userId);
482
+ if (actingHandle === undefined) {
483
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
484
+ return Result.Ok(EventoResult.Continue);
485
+ }
486
+ const lists = await listSocial(vctx, actingHandle);
487
+ const priv = await isHandlePrivate(vctx, actingHandle);
488
+ await ctx.send.send(ctx, {
489
+ type: "vibes.diy.res-social-list",
490
+ handle: actingHandle,
491
+ private: priv,
492
+ following: lists.following.map((e) => ({
493
+ followerHandle: actingHandle,
494
+ followeeHandle: e.handle,
495
+ state: e.state,
496
+ created: e.created,
497
+ updated: e.updated,
498
+ })),
499
+ followers: lists.followers.map((e) => ({
500
+ followerHandle: e.handle,
501
+ followeeHandle: actingHandle,
502
+ state: e.state,
503
+ created: e.created,
504
+ updated: e.updated,
505
+ })),
506
+ requests: lists.requests.map((e) => ({
507
+ followerHandle: e.handle,
508
+ followeeHandle: actingHandle,
509
+ state: e.state,
510
+ created: e.created,
511
+ updated: e.updated,
512
+ })),
513
+ blocked: lists.blocked,
514
+ });
515
+ return Result.Ok(EventoResult.Continue);
516
+ }),
517
+ };
518
+ export const socialRelationshipEvento = {
519
+ hash: "social-relationship",
520
+ validate: unwrapMsgBase(async (msg) => {
521
+ if (isReqSocialRelationship(msg.payload)) {
522
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
523
+ }
524
+ return Result.Ok(Option.None());
525
+ }),
526
+ handle: checkAuth(async (ctx) => {
527
+ const req = ctx.validated.payload;
528
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
529
+ const userId = req._auth.verifiedAuth.claims.userId;
530
+ const actingHandle = await resolveActiveHandle(vctx, userId);
531
+ if (actingHandle === undefined) {
532
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
533
+ return Result.Ok(EventoResult.Continue);
534
+ }
535
+ const rel = await getRelationship(vctx, { self: actingHandle, other: req.targetHandle });
536
+ await ctx.send.send(ctx, {
537
+ type: "vibes.diy.res-social-relationship",
538
+ targetHandle: req.targetHandle,
539
+ following: rel.following,
540
+ followedBy: rel.followedBy,
541
+ blocked: rel.blocked,
542
+ });
543
+ return Result.Ok(EventoResult.Continue);
544
+ }),
545
+ };
546
+ export const socialSetPrivacyEvento = {
547
+ hash: "social-set-privacy",
548
+ validate: unwrapMsgBase(async (msg) => {
549
+ if (isReqSocialSetPrivacy(msg.payload)) {
550
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
551
+ }
552
+ return Result.Ok(Option.None());
553
+ }),
554
+ handle: checkAuth(async (ctx) => {
555
+ const req = ctx.validated.payload;
556
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
557
+ const userId = req._auth.verifiedAuth.claims.userId;
558
+ const actingHandle = await resolveActiveHandle(vctx, userId);
559
+ if (actingHandle === undefined) {
560
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
561
+ return Result.Ok(EventoResult.Continue);
562
+ }
563
+ const rSet = await setHandlePrivacy(vctx, { handle: actingHandle, private: req.private, userId });
564
+ if (rSet.isErr())
565
+ return Result.Err(rSet);
566
+ const senderConnId = clientWsSend(ctx).connId;
567
+ for (const edge of rSet.Ok().activated) {
568
+ await sendSocialEdgeEvent(vctx, { op: "upsert", edge }, senderConnId);
569
+ await reconcileAudienceKeys(vctx, edge, "add");
570
+ const rNotify = await exception2Result(() => notifySocialEdge(vctx, {
571
+ kind: "follow-approved",
572
+ followerHandle: edge.followerHandle,
573
+ followeeHandle: edge.followeeHandle,
574
+ }));
575
+ if (rNotify.isErr())
576
+ console.error("social notification emit failed (privacy-flip activation):", rNotify.Err());
577
+ }
578
+ await ctx.send.send(ctx, {
579
+ type: "vibes.diy.res-social-set-privacy",
580
+ private: req.private,
581
+ });
582
+ return Result.Ok(EventoResult.Continue);
583
+ }),
584
+ };
585
+ export const subscribeSocialEvento = {
586
+ hash: "subscribe-social",
587
+ validate: unwrapMsgBase(async (msg) => {
588
+ if (isReqSubscribeSocial(msg.payload)) {
589
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
590
+ }
591
+ return Result.Ok(Option.None());
592
+ }),
593
+ handle: checkAuth(async (ctx) => {
594
+ const req = ctx.validated.payload;
595
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
596
+ const userId = req._auth.verifiedAuth.claims.userId;
597
+ const actingHandle = await resolveActiveHandle(vctx, userId);
598
+ if (actingHandle === undefined) {
599
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
600
+ return Result.Ok(EventoResult.Continue);
601
+ }
602
+ const wsSend = clientWsSend(ctx);
603
+ const subscriptionKey = socialSubscriptionKey(actingHandle);
604
+ for (const key of [...wsSend.subscribedSocialKeys]) {
605
+ if (key === subscriptionKey)
606
+ continue;
607
+ wsSend.subscribedSocialKeys.delete(key);
608
+ vctx.deregisterSocialSubscription?.(key).catch((e) => console.error("social-key deregistration error:", e));
609
+ }
610
+ wsSend.subscribedSocialKeys.add(subscriptionKey);
611
+ if (vctx.registerSocialSubscription) {
612
+ vctx
613
+ .registerSocialSubscription(subscriptionKey)
614
+ .catch((e) => console.error("social subscription registration error:", e));
615
+ }
616
+ await ctx.send.send(ctx, {
617
+ type: "vibes.diy.res-subscribe-social",
618
+ status: "ok",
619
+ });
620
+ return Result.Ok(EventoResult.Continue);
621
+ }),
622
+ };
623
+ export async function socialImportFriendEdges(vctx, req) {
624
+ const callerUserId = req._auth.verifiedAuth.claims.userId;
625
+ if (adminUserIdSet(vctx).has(callerUserId) === false) {
626
+ return Result.Err("social-import-friend-edges: admin only");
627
+ }
628
+ const dryRun = req.dryRun === true;
629
+ const counts = await importFriendEdges(vctx, {
630
+ ownerHandle: req.ownerHandle,
631
+ appSlug: req.appSlug,
632
+ dbName: req.dbName,
633
+ dryRun,
634
+ });
635
+ return Result.Ok({ type: "vibes.diy.res-social-import-friend-edges", dryRun, counts });
636
+ }
637
+ export const socialImportFriendEdgesEvento = {
638
+ hash: "social-import-friend-edges",
639
+ validate: unwrapMsgBase(async (msg) => {
640
+ if (isReqSocialImportFriendEdges(msg.payload)) {
641
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
642
+ }
643
+ return Result.Ok(Option.None());
644
+ }),
645
+ handle: checkAuth(async (ctx) => {
646
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
647
+ const r = await socialImportFriendEdges(vctx, ctx.validated.payload);
648
+ if (r.isErr())
649
+ return Result.Err(r);
650
+ await ctx.send.send(ctx, r.Ok());
651
+ return Result.Ok(EventoResult.Continue);
652
+ }),
653
+ };
654
+ export const audienceArmStateEvento = {
655
+ hash: "audience-arm-state",
656
+ validate: unwrapMsgBase(async (msg) => {
657
+ if (isReqAudienceArmState(msg.payload)) {
658
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
659
+ }
660
+ return Result.Ok(Option.None());
661
+ }),
662
+ handle: checkAuth(async (ctx) => {
663
+ const req = ctx.validated.payload;
664
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
665
+ const userId = req._auth.verifiedAuth.claims.userId;
666
+ const actingHandle = await resolveActiveHandle(vctx, userId);
667
+ if (actingHandle === undefined) {
668
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
669
+ return Result.Ok(EventoResult.Continue);
670
+ }
671
+ const [state, policy, sensitive] = await Promise.all([
672
+ getArmState(vctx, { subjectHandle: actingHandle, ownerHandle: req.ownerHandle, appSlug: req.appSlug }),
673
+ getAudiencePolicy(vctx, actingHandle),
674
+ getAppPrivacySensitive(vctx, { ownerHandle: req.ownerHandle, appSlug: req.appSlug }),
675
+ ]);
676
+ await ctx.send.send(ctx, {
677
+ type: "vibes.diy.res-audience-arm-state",
678
+ ownerHandle: req.ownerHandle,
679
+ appSlug: req.appSlug,
680
+ state,
681
+ policy,
682
+ sensitive,
683
+ });
684
+ return Result.Ok(EventoResult.Continue);
685
+ }),
686
+ };
687
+ export const setAudienceArmEvento = {
688
+ hash: "set-audience-arm",
689
+ validate: unwrapMsgBase(async (msg) => {
690
+ if (isReqSetAudienceArm(msg.payload)) {
691
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
692
+ }
693
+ return Result.Ok(Option.None());
694
+ }),
695
+ handle: checkAuth(async (ctx) => {
696
+ const req = ctx.validated.payload;
697
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
698
+ const userId = req._auth.verifiedAuth.claims.userId;
699
+ const actingHandle = await resolveActiveHandle(vctx, userId);
700
+ if (actingHandle === undefined) {
701
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
702
+ return Result.Ok(EventoResult.Continue);
703
+ }
704
+ await setArm(vctx, { subjectHandle: actingHandle, ownerHandle: req.ownerHandle, appSlug: req.appSlug, state: req.state });
705
+ await ctx.send.send(ctx, {
706
+ type: "vibes.diy.res-set-audience-arm",
707
+ ownerHandle: req.ownerHandle,
708
+ appSlug: req.appSlug,
709
+ state: req.state,
710
+ });
711
+ return Result.Ok(EventoResult.Continue);
712
+ }),
713
+ };
714
+ export const setAudiencePolicyEvento = {
715
+ hash: "set-audience-policy",
716
+ validate: unwrapMsgBase(async (msg) => {
717
+ if (isReqSetAudiencePolicy(msg.payload)) {
718
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
719
+ }
720
+ return Result.Ok(Option.None());
721
+ }),
722
+ handle: checkAuth(async (ctx) => {
723
+ const req = ctx.validated.payload;
724
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
725
+ const userId = req._auth.verifiedAuth.claims.userId;
726
+ const actingHandle = await resolveActiveHandle(vctx, userId);
727
+ if (actingHandle === undefined) {
728
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
729
+ return Result.Ok(EventoResult.Continue);
730
+ }
731
+ await setAudiencePolicy(vctx, { handle: actingHandle, policy: req.policy, userId });
732
+ await ctx.send.send(ctx, {
733
+ type: "vibes.diy.res-set-audience-policy",
734
+ policy: req.policy,
735
+ });
736
+ return Result.Ok(EventoResult.Continue);
737
+ }),
738
+ };
739
+ export const listAudienceArmsEvento = {
740
+ hash: "list-audience-arms",
741
+ validate: unwrapMsgBase(async (msg) => {
742
+ if (isReqListAudienceArms(msg.payload)) {
743
+ return Result.Ok(Option.Some({ ...msg, payload: msg.payload }));
744
+ }
745
+ return Result.Ok(Option.None());
746
+ }),
747
+ handle: checkAuth(async (ctx) => {
748
+ const req = ctx.validated.payload;
749
+ const vctx = ctx.ctx.getOrThrow("vibesApiCtx");
750
+ const userId = req._auth.verifiedAuth.claims.userId;
751
+ const actingHandle = await resolveActiveHandle(vctx, userId);
752
+ if (actingHandle === undefined) {
753
+ await ctx.send.send(ctx, NO_HANDLE_ERROR);
754
+ return Result.Ok(EventoResult.Continue);
755
+ }
756
+ const [policy, arms] = await Promise.all([getAudiencePolicy(vctx, actingHandle), listArmsForSubject(vctx, actingHandle)]);
757
+ await ctx.send.send(ctx, {
758
+ type: "vibes.diy.res-list-audience-arms",
759
+ policy,
760
+ arms,
761
+ });
762
+ return Result.Ok(EventoResult.Continue);
763
+ }),
764
+ };
765
+ //# sourceMappingURL=social-flow.js.map