@takosjp/yurucommu-core 3.4.3 → 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 (112) 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 +16 -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/retention.ts +38 -1
  45. package/src/backend/routes/account-teardown.ts +422 -156
  46. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +104 -105
  47. package/src/backend/routes/activitypub/handlers/inbound-community-scope.ts +121 -0
  48. package/src/backend/routes/activitypub/handlers/inbound-object-identity.ts +54 -0
  49. package/src/backend/routes/activitypub/handlers/inbound-reply-target.ts +34 -0
  50. package/src/backend/routes/activitypub/handlers/inbound-story-projection.ts +438 -0
  51. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1121 -806
  52. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +118 -153
  53. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +185 -168
  54. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +220 -32
  55. package/src/backend/routes/activitypub/inbound-activity-identity.ts +16 -0
  56. package/src/backend/routes/activitypub/inbound-activity-reference.ts +116 -0
  57. package/src/backend/routes/activitypub/inbound-addressing.ts +87 -0
  58. package/src/backend/routes/activitypub/inbox-addressing.ts +22 -19
  59. package/src/backend/routes/activitypub/inbox-types.ts +14 -2
  60. package/src/backend/routes/activitypub/inbox.ts +81 -105
  61. package/src/backend/routes/activitypub/outbox.ts +0 -0
  62. package/src/backend/routes/activitypub.ts +6 -5
  63. package/src/backend/routes/actors-helpers.ts +34 -8
  64. package/src/backend/routes/actors.ts +328 -164
  65. package/src/backend/routes/auth-helpers.ts +57 -9
  66. package/src/backend/routes/auth.ts +10 -2
  67. package/src/backend/routes/communities/membership-invites.ts +4 -1
  68. package/src/backend/routes/communities/membership-members.ts +201 -73
  69. package/src/backend/routes/communities/membership-requests.ts +141 -76
  70. package/src/backend/routes/communities/membership-shared.ts +228 -21
  71. package/src/backend/routes/communities/messages.ts +9 -2
  72. package/src/backend/routes/communities/routes.ts +48 -13
  73. package/src/backend/routes/dm/contacts.ts +29 -41
  74. package/src/backend/routes/dm/conversations-helpers.ts +9 -1
  75. package/src/backend/routes/dm/messages.ts +36 -42
  76. package/src/backend/routes/dm/read-archive.ts +4 -2
  77. package/src/backend/routes/dm/requests.ts +62 -54
  78. package/src/backend/routes/follow-helpers.ts +200 -60
  79. package/src/backend/routes/follow.ts +146 -140
  80. package/src/backend/routes/media.ts +20 -94
  81. package/src/backend/routes/moderation.ts +72 -4
  82. package/src/backend/routes/notes.ts +3 -4
  83. package/src/backend/routes/notifications.ts +209 -108
  84. package/src/backend/routes/posts/delete-cascade.ts +253 -89
  85. package/src/backend/routes/posts/federation.ts +373 -0
  86. package/src/backend/routes/posts/interactions.ts +160 -184
  87. package/src/backend/routes/posts/like-mutation.ts +240 -0
  88. package/src/backend/routes/posts/post-helpers.ts +112 -162
  89. package/src/backend/routes/posts/queries.ts +150 -54
  90. package/src/backend/routes/posts/routes.ts +169 -329
  91. package/src/backend/routes/posts/transformers.ts +61 -6
  92. package/src/backend/routes/recommendations.ts +37 -44
  93. package/src/backend/routes/rtc/index.ts +26 -6
  94. package/src/backend/routes/search.ts +39 -55
  95. package/src/backend/routes/stories/interactions.ts +22 -6
  96. package/src/backend/routes/stories/query-helpers.ts +28 -41
  97. package/src/backend/routes/stories/routes.ts +125 -108
  98. package/src/backend/routes/takos-tools/dm.ts +17 -17
  99. package/src/backend/routes/takos-tools/posts.ts +189 -106
  100. package/src/backend/routes/takos-tools/search.ts +13 -4
  101. package/src/backend/routes/takos-tools/timeline.ts +35 -27
  102. package/src/backend/routes/takos-tools-response.ts +6 -2
  103. package/src/backend/routes/timeline.ts +5 -5
  104. package/src/backend/runtime/call-signaling-do.ts +35 -4
  105. package/src/backend/runtime/one-time-ticket.ts +116 -0
  106. package/src/backend/runtime/realtime-stream-do.ts +5 -61
  107. package/src/backend/runtime/signaling-hub.ts +36 -3
  108. package/src/backend/server.ts +95 -80
  109. package/src/db/d1-write.ts +67 -43
  110. package/src/db/schema/federation.ts +42 -1
  111. package/src/db/schema/messaging.ts +110 -2
  112. package/src/db/schema.ts +1 -1
@@ -88,31 +88,35 @@ function requireBatchable(db: Database, context: string): BatchableDb {
88
88
  return candidate as BatchableDb;
89
89
  }
90
90
 
91
- /**
92
- * Bound parameters Drizzle emits for one row of `rows`.
93
- *
94
- * Drizzle builds ONE column list for the whole `values([...])` call (the union
95
- * of the keys present across the rows) and binds a parameter per column per
96
- * row, so the budget must be divided by that union — not by the keys of the
97
- * first row.
98
- */
99
- function paramsPerRow(
91
+ /** Reject row keys that do not belong to the target table. */
92
+ function assertKnownRowKeys(
100
93
  table: SQLiteTable,
101
94
  rows: readonly Record<string, unknown>[],
102
- ): number {
103
- const keys = new Set<string>();
104
- for (const row of rows) {
105
- for (const key of Object.keys(row)) keys.add(key);
106
- }
95
+ ): void {
107
96
  const known = new Set(Object.keys(getTableColumns(table)));
108
- for (const key of keys) {
109
- if (!known.has(key)) {
110
- throw new D1WriteShapeError(
111
- `insertMany: row key "${key}" is not a column of the target table`,
112
- );
97
+ for (const row of rows) {
98
+ for (const key of Object.keys(row)) {
99
+ if (!known.has(key)) {
100
+ throw new D1WriteShapeError(
101
+ `insertMany: row key "${key}" is not a column of the target table`,
102
+ );
103
+ }
113
104
  }
114
105
  }
115
- return keys.size;
106
+ }
107
+
108
+ /** Count the exact parameters Drizzle compiled into an insert statement. */
109
+ function boundParameterCount(statement: D1Statement): number {
110
+ const candidate = statement as unknown as {
111
+ toSQL(): { readonly params: readonly unknown[] };
112
+ };
113
+ if (typeof candidate.toSQL !== "function") {
114
+ throw new D1WriteShapeError(
115
+ "insertMany: the active insert builder exposes no toSQL(); the D1 " +
116
+ "parameter budget cannot be proven before execution",
117
+ );
118
+ }
119
+ return candidate.toSQL().params.length;
116
120
  }
117
121
 
118
122
  /**
@@ -132,35 +136,47 @@ export function insertMany<TTable extends SQLiteTable>(
132
136
  ): readonly D1Statement[] {
133
137
  if (rows.length === 0) return [];
134
138
 
135
- const perRow = paramsPerRow(
136
- table,
137
- rows as readonly Record<string, unknown>[],
138
- );
139
- if (perRow === 0) {
140
- throw new D1WriteShapeError("insertMany: rows bind no columns");
141
- }
142
- if (perRow > D1_SAFE_PARAM_BUDGET) {
143
- throw new D1WriteShapeError(
144
- `insertMany: a single row binds ${perRow} parameters, over the D1 ` +
145
- `budget of ${D1_SAFE_PARAM_BUDGET}; no chunking can make this fit`,
146
- );
147
- }
139
+ assertKnownRowKeys(table, rows as readonly Record<string, unknown>[]);
148
140
 
149
- const chunkSize = Math.floor(D1_SAFE_PARAM_BUDGET / perRow);
150
- const statements: D1Statement[] = [];
151
- for (let offset = 0; offset < rows.length; offset += chunkSize) {
152
- const chunk = rows.slice(offset, offset + chunkSize);
153
- // Drizzle's insert builder is generic over the table; the concrete row type
154
- // is already checked by the public signature, so the builder-local widening
155
- // here is the narrowest cast that keeps the call site typed.
141
+ const buildStatement = (chunk: readonly TTable["$inferInsert"][]) => {
156
142
  const builder = db
157
143
  .insert(table)
158
144
  .values(chunk as TTable["$inferInsert"][]) as unknown as D1Statement & {
159
145
  onConflictDoNothing(): D1Statement;
160
146
  };
161
- statements.push(
162
- opts?.conflict === "ignore" ? builder.onConflictDoNothing() : builder,
163
- );
147
+ return opts?.conflict === "ignore"
148
+ ? builder.onConflictDoNothing()
149
+ : builder;
150
+ };
151
+
152
+ const statements: D1Statement[] = [];
153
+ for (let offset = 0; offset < rows.length;) {
154
+ // Start at no more than the parameter budget in rows, then compile and
155
+ // shrink. Inspecting the actual query is essential: Drizzle fills omitted
156
+ // `$defaultFn`, primitive `.default(...)`, and on-update defaults at compile
157
+ // time, each of which can add a bound parameter that is absent from the
158
+ // caller's row keys. The former key-count heuristic under-budgeted those
159
+ // hidden values and emitted production-invalid 105/120-parameter inserts.
160
+ let chunkSize = Math.min(D1_SAFE_PARAM_BUDGET, rows.length - offset);
161
+ let statement: D1Statement;
162
+ while (true) {
163
+ statement = buildStatement(rows.slice(offset, offset + chunkSize));
164
+ const parameters = boundParameterCount(statement);
165
+ if (parameters <= D1_SAFE_PARAM_BUDGET) break;
166
+ if (chunkSize === 1) {
167
+ throw new D1WriteShapeError(
168
+ `insertMany: a single row compiles to ${parameters} parameters, ` +
169
+ `over the D1 budget of ${D1_SAFE_PARAM_BUDGET}; no chunking can ` +
170
+ `make this fit`,
171
+ );
172
+ }
173
+ const scaled = Math.floor(
174
+ (chunkSize * D1_SAFE_PARAM_BUDGET) / parameters,
175
+ );
176
+ chunkSize = Math.max(1, Math.min(chunkSize - 1, scaled));
177
+ }
178
+ statements.push(statement);
179
+ offset += chunkSize;
164
180
  }
165
181
 
166
182
  if (statements.length > D1_MAX_BATCH_STATEMENTS) {
@@ -215,6 +231,14 @@ export async function runBatch(
215
231
  db: Database,
216
232
  statements: readonly [D1Statement, ...D1Statement[]],
217
233
  ): Promise<void> {
234
+ if (statements.length > D1_MAX_BATCH_STATEMENTS) {
235
+ throw new D1BatchTooLargeError(
236
+ `runBatch: ${statements.length} statements exceed the atomic batch cap ` +
237
+ `of ${D1_MAX_BATCH_STATEMENTS}. Do not split a logical mutation across ` +
238
+ `batches; make the writes set-based or use the generational-replace ` +
239
+ `pattern in docs/quality/d1-write.md §3.`,
240
+ );
241
+ }
218
242
  await requireBatchable(db, "runBatch").batch(statements);
219
243
  }
220
244
 
@@ -1,4 +1,4 @@
1
- import { index, sqliteTable, text } from "drizzle-orm/sqlite-core";
1
+ import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
2
  import { nowIsoUtc } from "./date-utils.ts";
3
3
 
4
4
  /**
@@ -17,3 +17,44 @@ export const inboundActivityClaims = sqliteTable(
17
17
  },
18
18
  (t) => [index("inbound_activity_claims_lease_idx").on(t.leaseExpiresAt)],
19
19
  );
20
+
21
+ /**
22
+ * Durable negative-cache/backoff state for a remote actor document fetch.
23
+ *
24
+ * This is deliberately separate from actor_cache: a failed fetch is not an
25
+ * actor and must never leak into actor search, signature verification, or
26
+ * delivery planning as if it were a partially valid cached identity.
27
+ */
28
+ export const remoteActorFetchFailures = sqliteTable(
29
+ "remote_actor_fetch_failures",
30
+ {
31
+ actorApId: text("actor_ap_id").primaryKey(),
32
+ kind: text("kind").notNull(),
33
+ reason: text("reason").notNull(),
34
+ httpStatus: integer("http_status"),
35
+ failureCount: integer("failure_count").notNull().default(1),
36
+ retryAt: text("retry_at"),
37
+ processingToken: text("processing_token"),
38
+ leaseExpiresAt: text("lease_expires_at"),
39
+ createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
40
+ updatedAt: text("updated_at").notNull().$defaultFn(nowIsoUtc),
41
+ },
42
+ (t) => [
43
+ index("remote_actor_fetch_failures_retry_idx").on(t.retryAt),
44
+ index("remote_actor_fetch_failures_lease_idx").on(t.leaseExpiresAt),
45
+ index("remote_actor_fetch_failures_updated_idx").on(t.updatedAt),
46
+ ],
47
+ );
48
+
49
+ /**
50
+ * Durable lifecycle authority established by a verified self-Delete(Actor).
51
+ *
52
+ * This is deliberately separate from actor_cache and fetch-failure backoff:
53
+ * cache data is disposable, a remote fetch can recover, but a signed deletion
54
+ * must fence late/concurrent cache writers and all subsequent inbound traffic.
55
+ */
56
+ export const remoteActorTombstones = sqliteTable("remote_actor_tombstones", {
57
+ actorApId: text("actor_ap_id").primaryKey(),
58
+ deleteActivityApId: text("delete_activity_ap_id").notNull(),
59
+ deletedAt: text("deleted_at").notNull().$defaultFn(nowIsoUtc),
60
+ });
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Messaging/DM tables: activities, inbox, deliveryQueue, deliveryCircuit,
3
- * notificationArchived, dmTyping, dmReadStatus, dmArchivedConversations, mediaUploads
2
+ * Messaging/DM tables: activities, inbox, deliveryQueue,
3
+ * deliveryEndpointRecipients, deliveryCircuit, notificationArchived, dmTyping,
4
+ * dmReadStatus, dmArchivedConversations, mediaUploads
4
5
  */
5
6
 
6
7
  import {
@@ -9,6 +10,7 @@ import {
9
10
  primaryKey,
10
11
  sqliteTable,
11
12
  text,
13
+ uniqueIndex,
12
14
  } from "drizzle-orm/sqlite-core";
13
15
  import { nowIso, nowIsoUtc } from "./date-utils.ts";
14
16
 
@@ -61,12 +63,118 @@ export const deliveryQueue = sqliteTable(
61
63
  nextAttemptAt: text("next_attempt_at").notNull().$defaultFn(nowIso),
62
64
  deliveredAt: text("delivered_at"),
63
65
  error: text("error"),
66
+ recipientAttributionComplete: integer("recipient_attribution_complete")
67
+ .notNull()
68
+ .default(0),
64
69
  createdAt: text("created_at").notNull().$defaultFn(nowIso),
65
70
  },
66
71
  (t) => [
67
72
  index("delivery_queue_activity_idx").on(t.activityApId),
68
73
  index("delivery_queue_next_attempt_idx").on(t.nextAttemptAt),
69
74
  index("delivery_queue_status_next_idx").on(t.status, t.nextAttemptAt),
75
+ index("delivery_queue_terminal_retention_idx").on(t.status, t.createdAt),
76
+ ],
77
+ );
78
+
79
+ /**
80
+ * Recipient attribution for endpoint-aggregated jobs created after migration
81
+ * 0029. Legacy delivery_queue rows deliberately remain unattributed rather
82
+ * than pretending an incomplete reconstruction is authoritative.
83
+ */
84
+ export const deliveryEndpointRecipients = sqliteTable(
85
+ "delivery_endpoint_recipients",
86
+ {
87
+ deliveryJobId: text("delivery_job_id").notNull(),
88
+ recipientActorApId: text("recipient_actor_ap_id").notNull(),
89
+ createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
90
+ },
91
+ (t) => [
92
+ primaryKey({ columns: [t.deliveryJobId, t.recipientActorApId] }),
93
+ index("delivery_endpoint_recipients_actor_idx").on(
94
+ t.recipientActorApId,
95
+ t.deliveryJobId,
96
+ ),
97
+ ],
98
+ );
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // DELIVERY_RESOLUTIONS
102
+ // ---------------------------------------------------------------------------
103
+
104
+ /**
105
+ * Durable first-hop outbox for an outbound Activity whose recipient endpoint
106
+ * is not yet known. Queue messages are only wakeups; this row remains the
107
+ * retry authority until actor resolution creates a deliveryQueue endpoint job.
108
+ */
109
+ export const deliveryResolutions = sqliteTable(
110
+ "delivery_resolutions",
111
+ {
112
+ id: text("id").primaryKey(),
113
+ activityApId: text("activity_ap_id").notNull(),
114
+ recipientActorApId: text("recipient_actor_ap_id").notNull(),
115
+ status: text("status").notNull().default("pending"),
116
+ processingToken: text("processing_token"),
117
+ attempts: integer("attempts").notNull().default(0),
118
+ nextAttemptAt: text("next_attempt_at").notNull().$defaultFn(nowIsoUtc),
119
+ lastError: text("last_error"),
120
+ createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
121
+ updatedAt: text("updated_at")
122
+ .notNull()
123
+ .$defaultFn(nowIsoUtc)
124
+ .$onUpdateFn(nowIsoUtc),
125
+ resolvedAt: text("resolved_at"),
126
+ },
127
+ (t) => [
128
+ uniqueIndex("delivery_resolutions_activity_actor_idx").on(
129
+ t.activityApId,
130
+ t.recipientActorApId,
131
+ ),
132
+ index("delivery_resolutions_status_next_idx").on(t.status, t.nextAttemptAt),
133
+ index("delivery_resolutions_terminal_retention_idx").on(
134
+ t.status,
135
+ t.updatedAt,
136
+ ),
137
+ index("delivery_resolutions_activity_idx").on(t.activityApId),
138
+ ],
139
+ );
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // DELIVERY_FANOUTS
143
+ // ---------------------------------------------------------------------------
144
+
145
+ /**
146
+ * Durable first-hop outbox for follower/community fanout. Queue messages only
147
+ * wake the planner; the row remains authoritative until the final page has
148
+ * completed so a failed producer RPC or Bun process restart can replay it.
149
+ */
150
+ export const deliveryFanouts = sqliteTable(
151
+ "delivery_fanouts",
152
+ {
153
+ id: text("id").primaryKey(),
154
+ activityApId: text("activity_ap_id").notNull(),
155
+ kind: text("kind").notNull(),
156
+ targetApId: text("target_ap_id").notNull(),
157
+ announceActivityApId: text("announce_activity_ap_id"),
158
+ status: text("status").notNull().default("pending"),
159
+ publications: integer("publications").notNull().default(0),
160
+ lastError: text("last_error"),
161
+ createdAt: text("created_at").notNull().$defaultFn(nowIsoUtc),
162
+ updatedAt: text("updated_at")
163
+ .notNull()
164
+ .$defaultFn(nowIsoUtc)
165
+ .$onUpdateFn(nowIsoUtc),
166
+ completedAt: text("completed_at"),
167
+ },
168
+ (t) => [
169
+ uniqueIndex("delivery_fanouts_intent_idx").on(
170
+ t.activityApId,
171
+ t.kind,
172
+ t.targetApId,
173
+ t.announceActivityApId,
174
+ ),
175
+ index("delivery_fanouts_status_created_idx").on(t.status, t.createdAt),
176
+ index("delivery_fanouts_terminal_retention_idx").on(t.status, t.updatedAt),
177
+ index("delivery_fanouts_activity_idx").on(t.activityApId),
70
178
  ],
71
179
  );
72
180
 
package/src/db/schema.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * - schema/social.ts: follows, blocks, mutes
9
9
  * - schema/communities.ts: communities, communityMembers, communityJoinRequests, communityInvites
10
10
  * - schema/stories.ts: storyViews, storyVotes, storyShares
11
- * - schema/messaging.ts: activities, deliveryQueue, deliveryCircuit, inbox, notificationArchived, dm*, dmCommunityReadStatus, mediaUploads
11
+ * - schema/messaging.ts: activities, deliveryQueue, deliveryEndpointRecipients, deliveryResolutions, deliveryFanouts, deliveryCircuit, inbox, notificationArchived, dm*, dmCommunityReadStatus, mediaUploads
12
12
  * - schema/relations.ts: all Drizzle relation definitions
13
13
  */
14
14