@takosjp/yurucommu-core 3.4.1 → 3.4.4

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 (113) hide show
  1. package/migrations/0023_delivery_resolution_outbox.sql +33 -0
  2. package/migrations/0024_delivery_fanout_outbox.sql +35 -0
  3. package/migrations/0025_delivery_endpoint_terminal_retention.sql +6 -0
  4. package/migrations/0026_remote_actor_fetch_failures.sql +29 -0
  5. package/migrations/0027_remote_actor_tombstones.sql +12 -0
  6. package/migrations/0028_remote_actor_delivery_fence.sql +30 -0
  7. package/migrations/0029_delivery_endpoint_recipients.sql +47 -0
  8. package/package.json +2 -1
  9. package/packages/api/package.json +1 -1
  10. package/packages/api/src/lib/api/communities.ts +2 -0
  11. package/packages/api/src/lib/api/fetch.ts +69 -13
  12. package/packages/api/src/lib/api/normalize.ts +32 -10
  13. package/packages/api/src/lib/api/notifications.ts +4 -1
  14. package/packages/api/src/lib/rtc-client.ts +127 -29
  15. package/src/backend/federation-helpers.ts +1 -0
  16. package/src/backend/index.ts +35 -3
  17. package/src/backend/lib/account-migration.ts +340 -0
  18. package/src/backend/lib/activity-delete-cascade.ts +193 -0
  19. package/src/backend/lib/activitypub-actor-cache.ts +615 -48
  20. package/src/backend/lib/activitypub-actor-identity-sql.ts +179 -0
  21. package/src/backend/lib/activitypub-actor-identity.ts +39 -0
  22. package/src/backend/lib/activitypub-validators.ts +62 -4
  23. package/src/backend/lib/ap-ids.ts +36 -6
  24. package/src/backend/lib/ap-verify.ts +7 -17
  25. package/src/backend/lib/blocklist-purge.ts +676 -42
  26. package/src/backend/lib/blocklist.ts +278 -39
  27. package/src/backend/lib/community-visibility.ts +47 -33
  28. package/src/backend/lib/delivery/fanout-outbox.ts +336 -0
  29. package/src/backend/lib/delivery/planner.ts +16 -12
  30. package/src/backend/lib/delivery/queue-batching.ts +389 -145
  31. package/src/backend/lib/delivery/queue-delivery.ts +47 -25
  32. package/src/backend/lib/delivery/queue.ts +479 -73
  33. package/src/backend/lib/delivery/resolution-outbox.ts +456 -0
  34. package/src/backend/lib/delivery/types.ts +31 -7
  35. package/src/backend/lib/feed-exclude.ts +40 -28
  36. package/src/backend/lib/follow-edge-mutations.ts +217 -0
  37. package/src/backend/lib/notification-eligibility.ts +15 -9
  38. package/src/backend/lib/notification-push.ts +2 -2
  39. package/src/backend/lib/oidc-id-token.ts +66 -0
  40. package/src/backend/lib/personal-actor-moderation.ts +239 -0
  41. package/src/backend/lib/post-visibility.ts +79 -16
  42. package/src/backend/lib/remote-activity-id.ts +61 -0
  43. package/src/backend/lib/unread-counts.ts +3 -0
  44. package/src/backend/public.ts +16 -0
  45. package/src/backend/retention.ts +115 -0
  46. package/src/backend/routes/account-teardown.ts +422 -156
  47. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +104 -105
  48. package/src/backend/routes/activitypub/handlers/inbound-community-scope.ts +121 -0
  49. package/src/backend/routes/activitypub/handlers/inbound-object-identity.ts +54 -0
  50. package/src/backend/routes/activitypub/handlers/inbound-reply-target.ts +34 -0
  51. package/src/backend/routes/activitypub/handlers/inbound-story-projection.ts +438 -0
  52. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1121 -806
  53. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +118 -153
  54. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +185 -168
  55. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +220 -32
  56. package/src/backend/routes/activitypub/inbound-activity-identity.ts +16 -0
  57. package/src/backend/routes/activitypub/inbound-activity-reference.ts +116 -0
  58. package/src/backend/routes/activitypub/inbound-addressing.ts +87 -0
  59. package/src/backend/routes/activitypub/inbox-addressing.ts +22 -19
  60. package/src/backend/routes/activitypub/inbox-types.ts +14 -2
  61. package/src/backend/routes/activitypub/inbox.ts +81 -105
  62. package/src/backend/routes/activitypub/outbox.ts +0 -0
  63. package/src/backend/routes/activitypub.ts +6 -5
  64. package/src/backend/routes/actors-helpers.ts +34 -8
  65. package/src/backend/routes/actors.ts +333 -169
  66. package/src/backend/routes/auth-helpers.ts +57 -9
  67. package/src/backend/routes/auth.ts +10 -2
  68. package/src/backend/routes/communities/membership-invites.ts +4 -1
  69. package/src/backend/routes/communities/membership-members.ts +201 -73
  70. package/src/backend/routes/communities/membership-requests.ts +141 -76
  71. package/src/backend/routes/communities/membership-shared.ts +228 -21
  72. package/src/backend/routes/communities/messages.ts +9 -2
  73. package/src/backend/routes/communities/routes.ts +48 -13
  74. package/src/backend/routes/dm/contacts.ts +29 -41
  75. package/src/backend/routes/dm/conversations-helpers.ts +9 -1
  76. package/src/backend/routes/dm/messages.ts +36 -42
  77. package/src/backend/routes/dm/read-archive.ts +4 -2
  78. package/src/backend/routes/dm/requests.ts +62 -54
  79. package/src/backend/routes/follow-helpers.ts +200 -60
  80. package/src/backend/routes/follow.ts +146 -140
  81. package/src/backend/routes/media.ts +20 -94
  82. package/src/backend/routes/moderation.ts +72 -4
  83. package/src/backend/routes/notes.ts +3 -4
  84. package/src/backend/routes/notifications.ts +209 -108
  85. package/src/backend/routes/posts/delete-cascade.ts +253 -89
  86. package/src/backend/routes/posts/federation.ts +373 -0
  87. package/src/backend/routes/posts/interactions.ts +160 -184
  88. package/src/backend/routes/posts/like-mutation.ts +240 -0
  89. package/src/backend/routes/posts/post-helpers.ts +112 -162
  90. package/src/backend/routes/posts/queries.ts +150 -54
  91. package/src/backend/routes/posts/routes.ts +169 -329
  92. package/src/backend/routes/posts/transformers.ts +61 -6
  93. package/src/backend/routes/recommendations.ts +37 -44
  94. package/src/backend/routes/rtc/index.ts +26 -6
  95. package/src/backend/routes/search.ts +39 -55
  96. package/src/backend/routes/stories/interactions.ts +22 -6
  97. package/src/backend/routes/stories/query-helpers.ts +28 -41
  98. package/src/backend/routes/stories/routes.ts +130 -115
  99. package/src/backend/routes/takos-tools/dm.ts +17 -17
  100. package/src/backend/routes/takos-tools/posts.ts +189 -106
  101. package/src/backend/routes/takos-tools/search.ts +13 -4
  102. package/src/backend/routes/takos-tools/timeline.ts +35 -27
  103. package/src/backend/routes/takos-tools-response.ts +6 -2
  104. package/src/backend/routes/timeline.ts +5 -5
  105. package/src/backend/runtime/call-signaling-do.ts +35 -4
  106. package/src/backend/runtime/one-time-ticket.ts +116 -0
  107. package/src/backend/runtime/realtime-stream-do.ts +5 -61
  108. package/src/backend/runtime/signaling-hub.ts +36 -3
  109. package/src/backend/server.ts +95 -80
  110. package/src/db/d1-write.ts +67 -43
  111. package/src/db/schema/federation.ts +42 -1
  112. package/src/db/schema/messaging.ts +110 -2
  113. package/src/db/schema.ts +1 -1
@@ -19,6 +19,8 @@ import type {
19
19
  HubToClientFrame,
20
20
  IceServerConfig,
21
21
  } from "../types/call.ts";
22
+ import { apiPost } from "./api/fetch.ts";
23
+ import { getYurucommuApiTransport } from "./transport.ts";
22
24
 
23
25
  export type CallUiState =
24
26
  | "idle"
@@ -45,7 +47,7 @@ export interface CallClientEvents {
45
47
  }
46
48
 
47
49
  export interface CallClientOptions {
48
- /** Origin the app is served from (defaults to the page origin). */
50
+ /** Explicit server-origin override; otherwise the configured API transport owns it. */
49
51
  origin?: string;
50
52
  /** WebSocket reconnect backoff ceiling (ms). */
51
53
  maxBackoffMs?: number;
@@ -67,12 +69,15 @@ interface ActiveCall {
67
69
  }
68
70
 
69
71
  const CANDIDATE_FLUSH_MS = 200;
72
+ const MAX_PENDING_SOCKET_FRAMES = 64;
70
73
 
71
74
  export class CallClient {
72
75
  private ws: WebSocket | null = null;
73
76
  private wantConnected = false;
74
77
  private backoff = 500;
75
78
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
79
+ private openingSocket = false;
80
+ private readonly pendingFrames: ClientToHubFrame[] = [];
76
81
  private readonly listeners = new Map<keyof CallClientEvents, Set<Listener>>();
77
82
  private call: ActiveCall | null = null;
78
83
  private localStream: MediaStream | null = null;
@@ -114,16 +119,24 @@ export class CallClient {
114
119
  }
115
120
 
116
121
  // --- connection -----------------------------------------------------------
117
- private origin(): string {
118
- return (
119
- this.options.origin ??
120
- (typeof location !== "undefined" ? location.origin : "")
121
- );
122
+ private apiUrl(path: string): string {
123
+ const origin = this.options.origin;
124
+ return origin ? new URL(path, `${origin}/`).toString() : path;
122
125
  }
123
126
 
124
- private socketUrl(): string {
125
- const o = this.origin();
126
- return `${o.replace(/^http/, "ws")}/api/rtc/socket`;
127
+ private socketUrl(actorApId: string, ticket: string): string {
128
+ const transport = getYurucommuApiTransport();
129
+ const resolved = transport.resolveUrl(this.apiUrl("/api/rtc/socket"));
130
+ const base =
131
+ this.options.origin ??
132
+ (typeof location !== "undefined" ? location.origin : "http://localhost");
133
+ const url = new URL(resolved, base);
134
+ if (url.protocol === "http:") url.protocol = "ws:";
135
+ else if (url.protocol === "https:") url.protocol = "wss:";
136
+ else throw new Error("call socket URL must use HTTP(S)");
137
+ url.searchParams.set("actor", actorApId);
138
+ url.searchParams.set("ticket", ticket);
139
+ return url.toString();
127
140
  }
128
141
 
129
142
  connect(): void {
@@ -137,13 +150,46 @@ export class CallClient {
137
150
  this.reconnectTimer = null;
138
151
  this.ws?.close();
139
152
  this.ws = null;
153
+ this.pendingFrames.length = 0;
140
154
  }
141
155
 
142
156
  private openSocket(): void {
143
157
  if (this.ws && this.ws.readyState <= WebSocket.OPEN) return;
158
+ if (this.openingSocket) return;
159
+ this.openingSocket = true;
160
+ void this.openSocketWithTicket().finally(() => {
161
+ this.openingSocket = false;
162
+ });
163
+ }
164
+
165
+ private async openSocketWithTicket(): Promise<void> {
166
+ let actorApId: string;
167
+ let ticket: string;
168
+ try {
169
+ const response = await apiPost(this.apiUrl("/api/rtc/ticket"));
170
+ if (!response.ok) throw new Error(`ticket ${response.status}`);
171
+ const body = (await response.json()) as {
172
+ actor_ap_id?: unknown;
173
+ ticket?: unknown;
174
+ };
175
+ if (
176
+ typeof body.actor_ap_id !== "string" ||
177
+ typeof body.ticket !== "string"
178
+ ) {
179
+ throw new Error("bad ticket response");
180
+ }
181
+ actorApId = body.actor_ap_id;
182
+ ticket = body.ticket;
183
+ } catch (err) {
184
+ this.emit("error", "socket_ticket_failed", String(err));
185
+ this.scheduleReconnect();
186
+ return;
187
+ }
188
+ if (!this.wantConnected) return;
189
+
144
190
  let socket: WebSocket;
145
191
  try {
146
- socket = new WebSocket(this.socketUrl());
192
+ socket = new WebSocket(this.socketUrl(actorApId, ticket));
147
193
  } catch (err) {
148
194
  this.emit("error", "socket_open_failed", String(err));
149
195
  this.scheduleReconnect();
@@ -152,8 +198,22 @@ export class CallClient {
152
198
  this.ws = socket;
153
199
  socket.onopen = () => {
154
200
  this.backoff = 500;
155
- this.send({ t: "hello" });
156
- if (this.call) this.send({ t: "resume", callId: this.call.callId });
201
+ socket.send(JSON.stringify({ t: "hello" } satisfies ClientToHubFrame));
202
+ const pending = this.pendingFrames.splice(0);
203
+ const hasInitialInvite =
204
+ this.call !== null &&
205
+ pending.some(
206
+ (frame) => frame.t === "invite" && frame.callId === this.call?.callId,
207
+ );
208
+ if (this.call && !hasInitialInvite) {
209
+ socket.send(
210
+ JSON.stringify({
211
+ t: "resume",
212
+ callId: this.call.callId,
213
+ } satisfies ClientToHubFrame),
214
+ );
215
+ }
216
+ for (const frame of pending) socket.send(JSON.stringify(frame));
157
217
  };
158
218
  socket.onmessage = (ev) => {
159
219
  void this.onFrame(ev.data);
@@ -181,7 +241,14 @@ export class CallClient {
181
241
  private send(frame: ClientToHubFrame): void {
182
242
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
183
243
  this.ws.send(JSON.stringify(frame));
244
+ return;
184
245
  }
246
+ if (!this.wantConnected) return;
247
+ if (this.pendingFrames.length >= MAX_PENDING_SOCKET_FRAMES) {
248
+ this.emit("error", "socket_queue_full");
249
+ return;
250
+ }
251
+ this.pendingFrames.push(frame);
185
252
  }
186
253
 
187
254
  // --- public call control --------------------------------------------------
@@ -189,11 +256,9 @@ export class CallClient {
189
256
  /** Place an outgoing call. Fetches a callId + ICE, then rings the peer. */
190
257
  async startCall(peer: string, media: CallMediaKind): Promise<void> {
191
258
  if (this.call) throw new Error("already in a call");
192
- const res = await fetch(`${this.origin()}/api/rtc/calls`, {
193
- method: "POST",
194
- credentials: "include",
195
- headers: { "Content-Type": "application/json" },
196
- body: JSON.stringify({ to: peer, media }),
259
+ const res = await apiPost(this.apiUrl("/api/rtc/calls"), {
260
+ to: peer,
261
+ media,
197
262
  });
198
263
  if (!res.ok) {
199
264
  const code = res.status === 403 ? "blocked" : "start_failed";
@@ -204,18 +269,19 @@ export class CallClient {
204
269
  callId: string;
205
270
  iceServers: IceServerConfig[];
206
271
  };
207
- this.call = this.newCall(
272
+ const call = this.newCall(
208
273
  data.callId,
209
274
  peer,
210
275
  media,
211
276
  "caller",
212
277
  data.iceServers,
213
278
  );
279
+ this.call = call;
214
280
  this.setState("calling");
215
281
  this.connect();
216
282
  this.send({ t: "invite", callId: data.callId, to: peer, media });
217
- await this.setupMedia(this.call);
218
- await this.makeOffer(this.call);
283
+ if (!(await this.setupMedia(call))) return;
284
+ await this.makeOffer(call);
219
285
  }
220
286
 
221
287
  /** Accept the current incoming call. */
@@ -224,11 +290,14 @@ export class CallClient {
224
290
  if (!call || call.role !== "callee") return;
225
291
  this.setState("connecting");
226
292
  this.send({ t: "accept", callId: call.callId });
227
- await this.setupMedia(call);
293
+ if (!(await this.setupMedia(call))) return;
228
294
  // The offer was already applied on arrival; create + send the answer.
229
- if (call.pc && call.remoteDescriptionSet) {
230
- const answer = await call.pc.createAnswer();
231
- await call.pc.setLocalDescription(answer);
295
+ const pc = call.pc;
296
+ if (pc && call.remoteDescriptionSet && this.isActiveCall(call)) {
297
+ const answer = await pc.createAnswer();
298
+ if (!this.isActivePeer(call, pc)) return;
299
+ await pc.setLocalDescription(answer);
300
+ if (!this.isActivePeer(call, pc)) return;
232
301
  this.send({ t: "answer", callId: call.callId, sdp: answer.sdp ?? "" });
233
302
  }
234
303
  }
@@ -412,24 +481,45 @@ export class CallClient {
412
481
  }
413
482
 
414
483
  // --- media + peer connection ---------------------------------------------
415
- private async setupMedia(call: ActiveCall): Promise<void> {
484
+ private isActiveCall(call: ActiveCall): boolean {
485
+ return this.call === call;
486
+ }
487
+
488
+ private isActivePeer(call: ActiveCall, pc: RTCPeerConnection): boolean {
489
+ return this.isActiveCall(call) && call.pc === pc;
490
+ }
491
+
492
+ private async setupMedia(call: ActiveCall): Promise<boolean> {
493
+ if (!this.isActiveCall(call)) return false;
416
494
  if (!this.localStream) {
495
+ let stream: MediaStream;
417
496
  try {
418
- this.localStream = await navigator.mediaDevices.getUserMedia({
497
+ stream = await navigator.mediaDevices.getUserMedia({
419
498
  audio: call.media.audio,
420
499
  video: call.media.video,
421
500
  });
422
501
  } catch (err) {
502
+ if (!this.isActiveCall(call)) return false;
423
503
  this.emit("error", "media_denied", String(err));
424
504
  this.hangup("media_denied");
425
- return;
505
+ return false;
506
+ }
507
+ if (!this.isActiveCall(call)) {
508
+ // Permission prompts resolve asynchronously. If the user cancelled or
509
+ // another call replaced this one while the prompt was open, the newly
510
+ // acquired tracks have no owner and must be stopped immediately.
511
+ stream.getTracks().forEach((track) => track.stop());
512
+ return false;
426
513
  }
514
+ this.localStream = stream;
427
515
  this.emit("localstream", this.localStream);
428
516
  }
517
+ if (!this.isActiveCall(call)) return false;
429
518
  if (!call.pc) this.buildPeerConnection(call);
430
519
  for (const track of this.localStream.getTracks()) {
431
520
  call.pc?.addTrack(track, this.localStream);
432
521
  }
522
+ return this.isActiveCall(call);
433
523
  }
434
524
 
435
525
  private buildPeerConnection(call: ActiveCall): void {
@@ -441,13 +531,16 @@ export class CallClient {
441
531
  })),
442
532
  });
443
533
  pc.onicecandidate = (ev) => {
534
+ if (!this.isActivePeer(call, pc)) return;
444
535
  if (ev.candidate) this.bufferCandidate(call, ev.candidate.toJSON());
445
536
  else this.flushCandidates(call);
446
537
  };
447
538
  pc.ontrack = (ev) => {
539
+ if (!this.isActivePeer(call, pc)) return;
448
540
  this.emit("remotestream", ev.streams[0] ?? null);
449
541
  };
450
542
  pc.onconnectionstatechange = () => {
543
+ if (!this.isActivePeer(call, pc)) return;
451
544
  if (pc.connectionState === "connected") this.setState("connected");
452
545
  else if (
453
546
  pc.connectionState === "failed" ||
@@ -460,9 +553,14 @@ export class CallClient {
460
553
  }
461
554
 
462
555
  private async makeOffer(call: ActiveCall): Promise<void> {
556
+ if (!this.isActiveCall(call)) return;
463
557
  if (!call.pc) this.buildPeerConnection(call);
464
- const offer = await call.pc!.createOffer();
465
- await call.pc!.setLocalDescription(offer);
558
+ const pc = call.pc;
559
+ if (!pc) return;
560
+ const offer = await pc.createOffer();
561
+ if (!this.isActivePeer(call, pc)) return;
562
+ await pc.setLocalDescription(offer);
563
+ if (!this.isActivePeer(call, pc)) return;
466
564
  this.send({ t: "offer", callId: call.callId, sdp: offer.sdp ?? "" });
467
565
  }
468
566
 
@@ -11,6 +11,7 @@ export {
11
11
  activityApId,
12
12
  actorApId,
13
13
  communityApId,
14
+ formatPreferredUsername,
14
15
  formatUsername,
15
16
  generateId,
16
17
  getDomain,
@@ -47,7 +47,7 @@ import {
47
47
  import { logger } from "./lib/logger.ts";
48
48
 
49
49
  const log = logger.child({ component: "backend.index" });
50
- let lastNotificationPushRecoverySweep = 0;
50
+ let lastOutboxRecoverySweep = 0;
51
51
  import type { IQueueBatch } from "./runtime/queue.ts";
52
52
  import type {
53
53
  D1Database,
@@ -56,16 +56,21 @@ import type {
56
56
  MessageBatch,
57
57
  Queue,
58
58
  R2Bucket,
59
+ ScheduledController,
59
60
  } from "@cloudflare/workers-types";
60
61
  import type {
61
62
  DeliveryDlqMessageV1,
62
63
  DeliveryQueueMessageV1,
63
64
  } from "./lib/delivery/types.ts";
64
65
  import {
66
+ enqueuePendingDeliveryEndpointJobs,
65
67
  handleDeliveryDlqBatch,
66
68
  handleDeliveryQueueBatch,
67
69
  } from "./lib/delivery/queue.ts";
70
+ import { enqueuePendingDeliveryResolutionJobs } from "./lib/delivery/resolution-outbox.ts";
71
+ import { enqueuePendingDeliveryFanoutJobs } from "./lib/delivery/fanout-outbox.ts";
68
72
  import { enqueuePendingNotificationPushJobs } from "./lib/notification-push.ts";
73
+ import { runYurucommuRetention } from "./retention.ts";
69
74
 
70
75
  type YurucommuApp = Hono<{ Bindings: Env; Variables: Variables }>;
71
76
 
@@ -618,10 +623,20 @@ function applyGlobalMiddleware(
618
623
  const method = c.req.method.toUpperCase();
619
624
  const now = Date.now();
620
625
  const mutating = !["GET", "HEAD", "OPTIONS"].includes(method);
621
- const recoveryDue = now - lastNotificationPushRecoverySweep >= 60_000;
626
+ const recoveryDue = now - lastOutboxRecoverySweep >= 60_000;
622
627
  if (mutating || recoveryDue) {
623
- if (recoveryDue) lastNotificationPushRecoverySweep = now;
628
+ if (recoveryDue) lastOutboxRecoverySweep = now;
624
629
  const sweep = (async () => {
630
+ try {
631
+ await enqueuePendingDeliveryFanoutJobs(c.env);
632
+ await enqueuePendingDeliveryEndpointJobs(c.env);
633
+ await enqueuePendingDeliveryResolutionJobs(c.env);
634
+ } catch (error) {
635
+ log.error("Failed to enqueue durable federation outbox", {
636
+ event: "delivery.outbox.enqueue_failed",
637
+ error,
638
+ });
639
+ }
625
640
  try {
626
641
  await enqueuePendingNotificationPushJobs(c.env);
627
642
  } catch (error) {
@@ -994,6 +1009,12 @@ type WorkerBindings = EnvVars & {
994
1009
  REALTIME_STREAM?: DurableObjectNamespace;
995
1010
  };
996
1011
 
1012
+ function isMaterializedRuntimeEnv(
1013
+ bindings: WorkerBindings | Env,
1014
+ ): bindings is Env {
1015
+ return "DB_INSTANCE" in bindings && !!bindings.DB_INSTANCE;
1016
+ }
1017
+
997
1018
  export default {
998
1019
  async fetch(
999
1020
  request: Request,
@@ -1012,4 +1033,15 @@ export default {
1012
1033
  wrapCloudflareBindings(bindings),
1013
1034
  );
1014
1035
  },
1036
+
1037
+ async scheduled(
1038
+ _controller: ScheduledController,
1039
+ bindings: WorkerBindings | Env,
1040
+ _ctx: ExecutionContext,
1041
+ ): Promise<void> {
1042
+ const env = isMaterializedRuntimeEnv(bindings)
1043
+ ? bindings
1044
+ : wrapCloudflareBindings(bindings);
1045
+ await runYurucommuRetention(env);
1046
+ },
1015
1047
  };