@helyx/bot 1.5.1 → 1.5.3

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 (121) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/command-root-catalogue.json +452 -0
  3. package/dist/cached-message-deletion-authority.d.ts +27 -0
  4. package/dist/cached-message-deletion-authority.js +288 -0
  5. package/dist/channel-audit-configuration.d.ts +11 -1
  6. package/dist/channel-audit-configuration.js +28 -6
  7. package/dist/channel-audit-control.js +42 -2
  8. package/dist/channel-audit-permissions.js +76 -3
  9. package/dist/cloud-command-policy-sync.d.ts +26 -0
  10. package/dist/cloud-command-policy-sync.js +198 -0
  11. package/dist/cloud-connector.d.ts +6 -1
  12. package/dist/cloud-connector.js +224 -53
  13. package/dist/cloud-control-dispatcher.js +18 -0
  14. package/dist/command-policy-runtime.d.ts +23 -0
  15. package/dist/command-policy-runtime.js +53 -0
  16. package/dist/command-root-catalogue.d.ts +5 -0
  17. package/dist/command-root-catalogue.js +16 -0
  18. package/dist/control-error-mapping.d.ts +8 -0
  19. package/dist/control-error-mapping.js +60 -0
  20. package/dist/control-errors.d.ts +3 -1
  21. package/dist/control-errors.js +4 -1
  22. package/dist/control-health-routes.d.ts +10 -0
  23. package/dist/control-health-routes.js +28 -0
  24. package/dist/control-server-schemas.d.ts +7 -0
  25. package/dist/control-server-schemas.js +9 -0
  26. package/dist/control-server.d.ts +27 -6
  27. package/dist/control-server.js +103 -122
  28. package/dist/creator-invite-key.d.ts +6 -0
  29. package/dist/creator-invite-key.js +16 -0
  30. package/dist/creator-invite-service.d.ts +3 -1
  31. package/dist/creator-invite-service.js +8 -7
  32. package/dist/creator-onboarding-platform.d.ts +1 -0
  33. package/dist/creator-onboarding-platform.js +5 -2
  34. package/dist/dashboard-feature-control.d.ts +2 -1
  35. package/dist/dashboard-feature-control.js +1 -0
  36. package/dist/dashboard-logging.d.ts +20 -0
  37. package/dist/dashboard-logging.js +39 -0
  38. package/dist/dashboard-module-projection.d.ts +1 -1
  39. package/dist/dashboard-module-projection.js +5 -2
  40. package/dist/dashboard-preview-import-routes.js +8 -0
  41. package/dist/database-bootstrap.d.ts +1 -1
  42. package/dist/database-bootstrap.js +4 -3
  43. package/dist/discord-channel-audit.js +1 -0
  44. package/dist/discord-user-identities.d.ts +14 -0
  45. package/dist/discord-user-identities.js +64 -0
  46. package/dist/emergency-deletion-dispatcher.d.ts +68 -0
  47. package/dist/emergency-deletion-dispatcher.js +502 -0
  48. package/dist/emergency-deletion-rest-transport.d.ts +24 -0
  49. package/dist/emergency-deletion-rest-transport.js +253 -0
  50. package/dist/events.d.ts +5 -0
  51. package/dist/events.js +53 -10
  52. package/dist/guarded-discord-dispatcher.d.ts +29 -0
  53. package/dist/guarded-discord-dispatcher.js +204 -0
  54. package/dist/guild-command-discord-client.d.ts +2 -0
  55. package/dist/guild-command-discord-client.js +7 -0
  56. package/dist/guild-command-projection.d.ts +4 -1
  57. package/dist/guild-command-projection.js +25 -0
  58. package/dist/guild-command-read-service.d.ts +10 -1
  59. package/dist/guild-command-read-service.js +37 -0
  60. package/dist/guild-command-worker.d.ts +19 -4
  61. package/dist/guild-command-worker.js +356 -26
  62. package/dist/guild-message-protection-author-roles.d.ts +7 -0
  63. package/dist/guild-message-protection-author-roles.js +93 -0
  64. package/dist/guild-message-protection-deletion.d.ts +21 -0
  65. package/dist/guild-message-protection-deletion.js +138 -0
  66. package/dist/guild-message-protection-delivery.d.ts +41 -0
  67. package/dist/guild-message-protection-delivery.js +223 -0
  68. package/dist/guild-message-update-metadata.d.ts +32 -0
  69. package/dist/guild-message-update-metadata.js +255 -0
  70. package/dist/guilds.d.ts +2 -1
  71. package/dist/guilds.js +11 -1
  72. package/dist/hosted-server-moderation.d.ts +2 -2
  73. package/dist/index.js +68 -4
  74. package/dist/interactions.d.ts +3 -1
  75. package/dist/interactions.js +77 -12
  76. package/dist/invite-rollup-maintenance.d.ts +9 -0
  77. package/dist/invite-rollup-maintenance.js +12 -0
  78. package/dist/managed-resource-authorization.d.ts +15 -0
  79. package/dist/managed-resource-authorization.js +37 -0
  80. package/dist/managed-resource-create-defaults.d.ts +21 -0
  81. package/dist/managed-resource-create-defaults.js +83 -0
  82. package/dist/managed-resource-query.d.ts +6 -0
  83. package/dist/managed-resource-query.js +15 -0
  84. package/dist/member-role-ids.d.ts +17 -0
  85. package/dist/member-role-ids.js +66 -0
  86. package/dist/message-protection-admission.d.ts +60 -0
  87. package/dist/message-protection-admission.js +279 -0
  88. package/dist/message-protection-cold-data.d.ts +42 -0
  89. package/dist/message-protection-cold-data.js +180 -0
  90. package/dist/message-protection-coordinator.d.ts +73 -0
  91. package/dist/message-protection-coordinator.js +622 -0
  92. package/dist/message-protection-settings.d.ts +40 -0
  93. package/dist/message-protection-settings.js +275 -0
  94. package/dist/message-protection-summary.d.ts +8 -0
  95. package/dist/message-protection-summary.js +98 -0
  96. package/dist/module-availability-control.js +20 -12
  97. package/dist/module-logging.d.ts +3 -2
  98. package/dist/module-logging.js +6 -1
  99. package/dist/modules.js +56 -1
  100. package/dist/mysql-runtime-persistence.d.ts +1 -0
  101. package/dist/mysql-runtime-persistence.js +4 -1
  102. package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
  103. package/dist/nexus-command-recovery-control-routes.js +48 -0
  104. package/dist/nexus-command-references.d.ts +6 -0
  105. package/dist/nexus-command-references.js +33 -0
  106. package/dist/scheduled-task-runner.js +23 -4
  107. package/dist/shard-command-startup.d.ts +13 -0
  108. package/dist/shard-command-startup.js +27 -0
  109. package/dist/shard-contracts.d.ts +84 -0
  110. package/dist/shard-contracts.js +33 -0
  111. package/dist/shard-core-readiness.d.ts +5 -0
  112. package/dist/shard-core-readiness.js +19 -0
  113. package/dist/shard-router.d.ts +3 -0
  114. package/dist/shard-router.js +10 -1
  115. package/dist/shard-supervisor.d.ts +1 -1
  116. package/dist/shard-supervisor.js +131 -43
  117. package/dist/shard-worker.d.ts +1 -0
  118. package/dist/shard-worker.js +37 -15
  119. package/documentation/README.md +1 -0
  120. package/documentation/guides/self-hosting.md +54 -0
  121. package/package.json +18 -17
@@ -0,0 +1,73 @@
1
+ import type { Client, Guild } from "discord.js";
2
+ import type { ModuleDefinition } from "@helyx/sdk";
3
+ import type { LocalModuleMutationObserver } from "@helyx/platform";
4
+ import { type MessageProtectionOwnerIdentity, type MessageProtectionOwnerPort } from "@helyx/database-contracts";
5
+ import type { MessageProtectionEligibility } from "./guild-message-protection-delivery.js";
6
+ import type { MessageProtectionColdResult, MessageProtectionColdScope } from "./message-protection-cold-data.js";
7
+ import type { MessageProtectionSettingsLoader } from "./message-protection-settings.js";
8
+ export interface MessageProtectionGuildObserver {
9
+ ready(): void;
10
+ beforeSync(guild: Guild): (() => void) | undefined;
11
+ invalidateGuild(guildId: string): void;
12
+ invalidateAll(): void;
13
+ }
14
+ /**
15
+ * Dormant standalone composition. No production construction, SQL on messages,
16
+ * module authority, timer, automatic reconnect/reclaim or sharded-mode grant.
17
+ */
18
+ export declare class MessageProtectionCoordinator {
19
+ #private;
20
+ private readonly options;
21
+ readonly eligibility: MessageProtectionEligibility;
22
+ readonly guildObserver: MessageProtectionGuildObserver;
23
+ readonly localModuleMutations: LocalModuleMutationObserver;
24
+ constructor(options: {
25
+ identity: MessageProtectionOwnerIdentity;
26
+ client: Client;
27
+ owners: MessageProtectionOwnerPort;
28
+ coldData: {
29
+ read(scope: MessageProtectionColdScope): Promise<MessageProtectionColdResult>;
30
+ };
31
+ settings: MessageProtectionSettingsLoader;
32
+ leaseMs: number;
33
+ maximumAgeMs: number;
34
+ capacity: number;
35
+ maximumConcurrentReads: number;
36
+ maximumConcurrentMutations: number;
37
+ modules: readonly ModuleDefinition[];
38
+ now?: () => number;
39
+ });
40
+ /** Call once at accepted ready identity; a failed boot never recaptures. */
41
+ start(): Promise<boolean>;
42
+ renew(): Promise<boolean>;
43
+ /** Gateway ready/resume does not restore guild epochs or old tokens. */
44
+ resume(): boolean;
45
+ disconnect(): void;
46
+ invalidateGuild(guildId: string): void;
47
+ private invalidateGuildAdmissions;
48
+ invalidateScope(scope: MessageProtectionColdScope): void;
49
+ get pendingReadCount(): number;
50
+ get activeLocalMutationCount(): number;
51
+ get localReloadJobCount(): number;
52
+ whenLocalReloadsSettled(): Promise<void>;
53
+ reload(scope: MessageProtectionColdScope): Promise<boolean>;
54
+ private reloadReserved;
55
+ /** Revocation is synchronous; durable release/drain follows it. */
56
+ stop(): Promise<void>;
57
+ private beforeSync;
58
+ private beginLocalMutation;
59
+ private enqueueLocalReload;
60
+ private startLocalDrain;
61
+ private drainLocalReloads;
62
+ private sourceGuild;
63
+ private guildCurrent;
64
+ private readyIdentity;
65
+ private ownerCurrent;
66
+ private acceptLease;
67
+ private time;
68
+ private revoke;
69
+ private pruneTokens;
70
+ private deleteToken;
71
+ private releaseQuietly;
72
+ }
73
+ //# sourceMappingURL=message-protection-coordinator.d.ts.map
@@ -0,0 +1,622 @@
1
+ import { createHash } from "node:crypto";
2
+ import { assertMessageProtectionOwnerIdentity, assertMessageProtectionOwnerLease, assertMessageProtectionOwnerLeaseMs, } from "@helyx/database-contracts";
3
+ import { MessageProtectionAdmissionPublisher } from "./message-protection-admission.js";
4
+ const unavailable = Object.freeze({ state: "unavailable" });
5
+ /**
6
+ * Dormant standalone composition. No production construction, SQL on messages,
7
+ * module authority, timer, automatic reconnect/reclaim or sharded-mode grant.
8
+ */
9
+ export class MessageProtectionCoordinator {
10
+ options;
11
+ eligibility;
12
+ guildObserver;
13
+ localModuleMutations;
14
+ #identity;
15
+ #publisher;
16
+ #guilds = new Map();
17
+ #tokens = new Map();
18
+ #scopeTokens = new Map();
19
+ #reloads = new Map();
20
+ #pendingReads = new Map();
21
+ #localHolds = new Map();
22
+ #localJobs = new Map();
23
+ #localModuleIds;
24
+ #activeLocalMutations = 0;
25
+ #localDrain;
26
+ #clock;
27
+ #lastNow = -Infinity;
28
+ #started = false;
29
+ #stopped = false;
30
+ #gatewayReady = false;
31
+ #gatewayEpoch = 0n;
32
+ #guildEpoch = 0n;
33
+ #lease;
34
+ #ownerDeadline = -Infinity;
35
+ #renewal;
36
+ #stop;
37
+ constructor(options) {
38
+ this.options = options;
39
+ assertMessageProtectionOwnerIdentity(options.identity);
40
+ assertMessageProtectionOwnerLeaseMs(options.leaseMs);
41
+ if (!Number.isSafeInteger(options.maximumConcurrentMutations) ||
42
+ options.maximumConcurrentMutations < 1 ||
43
+ options.maximumConcurrentMutations > 256)
44
+ throw new TypeError("Invalid protection mutation concurrency");
45
+ const moduleIds = options.modules
46
+ .filter((module) => Boolean(module.events?.guildMessageUpdateMetadata?.length) ||
47
+ module.events?.guildMessageCreate?.some((contribution) => contribution.protection))
48
+ .map((module) => module.manifest.id);
49
+ if (moduleIds.length > options.capacity ||
50
+ new Set(moduleIds).size !== moduleIds.length)
51
+ throw new TypeError("Invalid protection reload recipients");
52
+ this.#localModuleIds = Object.freeze(moduleIds);
53
+ if (!Number.isSafeInteger(options.maximumConcurrentReads) ||
54
+ options.maximumConcurrentReads < 1 ||
55
+ options.maximumConcurrentReads > Math.min(options.capacity, 256))
56
+ throw new TypeError("Invalid protection cold-read concurrency");
57
+ this.#identity = Object.freeze({ ...options.identity });
58
+ this.options = Object.freeze({ ...options, identity: this.#identity });
59
+ this.#clock = options.now ?? (() => performance.now());
60
+ this.#publisher = new MessageProtectionAdmissionPublisher({
61
+ owner: { ...this.#identity, mode: "standalone" },
62
+ capacity: options.capacity,
63
+ maxAgeMs: options.maximumAgeMs,
64
+ now: () => this.time(),
65
+ });
66
+ this.eligibility = Object.freeze({
67
+ read: (guildId, moduleId) => {
68
+ if (!this.ownerCurrent() ||
69
+ this.#localHolds.has(guildId) ||
70
+ !this.guildCurrent(guildId) ||
71
+ !this.options.settings.isCurrent({ moduleId }))
72
+ return unavailable;
73
+ const admission = this.#publisher.eligibility.read(guildId, moduleId);
74
+ if (admission.state !== "enabled")
75
+ return admission;
76
+ const key = scopeKey({ guildId, moduleId });
77
+ const previous = this.#scopeTokens.get(key);
78
+ if (previous && previous !== admission.generation)
79
+ this.#tokens.delete(previous);
80
+ if (!this.#tokens.has(admission.generation) &&
81
+ this.#tokens.size >= options.capacity)
82
+ this.pruneTokens();
83
+ if (!this.#tokens.has(admission.generation) &&
84
+ this.#tokens.size >= options.capacity)
85
+ return unavailable;
86
+ this.#tokens.set(admission.generation, {
87
+ token: admission.generation,
88
+ scope: { guildId, moduleId },
89
+ });
90
+ this.#scopeTokens.set(key, admission.generation);
91
+ return admission;
92
+ },
93
+ isCurrent: (token) => {
94
+ const entry = this.#tokens.get(token);
95
+ return Boolean(entry &&
96
+ !this.#localHolds.has(entry.scope.guildId) &&
97
+ this.ownerCurrent() &&
98
+ this.guildCurrent(entry.scope.guildId) &&
99
+ this.options.settings.isCurrent(entry.scope) &&
100
+ this.#publisher.eligibility.isCurrent(token));
101
+ },
102
+ });
103
+ this.guildObserver = Object.freeze({
104
+ ready: () => {
105
+ this.resume();
106
+ },
107
+ beforeSync: (guild) => this.beforeSync(guild),
108
+ invalidateGuild: (guildId) => this.invalidateGuild(guildId),
109
+ invalidateAll: () => this.disconnect(),
110
+ });
111
+ this.localModuleMutations = Object.freeze({
112
+ begin: (scope) => this.beginLocalMutation(scope.guildId),
113
+ });
114
+ }
115
+ /** Call once at accepted ready identity; a failed boot never recaptures. */
116
+ async start() {
117
+ if (this.#started || this.#stopped)
118
+ return false;
119
+ this.#started = true;
120
+ if (!this.readyIdentity()) {
121
+ this.revoke();
122
+ return false;
123
+ }
124
+ const gatewayEpoch = this.#gatewayEpoch;
125
+ try {
126
+ const capture = await this.options.owners.capture(this.#identity);
127
+ if (this.#stopped ||
128
+ gatewayEpoch !== this.#gatewayEpoch ||
129
+ !this.readyIdentity()) {
130
+ this.revoke();
131
+ return false;
132
+ }
133
+ const capturedAt = this.time();
134
+ if (!Number.isFinite(capturedAt))
135
+ return false;
136
+ const lease = await this.options.owners.claim({
137
+ capture,
138
+ leaseMs: this.options.leaseMs,
139
+ });
140
+ if (!lease) {
141
+ this.revoke();
142
+ return false;
143
+ }
144
+ if (this.#stopped ||
145
+ gatewayEpoch !== this.#gatewayEpoch ||
146
+ !this.readyIdentity() ||
147
+ !this.acceptLease(lease, capturedAt, capture)) {
148
+ this.revoke();
149
+ await this.releaseQuietly(lease);
150
+ return false;
151
+ }
152
+ this.#gatewayReady = true;
153
+ return true;
154
+ }
155
+ catch {
156
+ this.revoke();
157
+ return false;
158
+ }
159
+ }
160
+ renew() {
161
+ if (this.#renewal)
162
+ return this.#renewal;
163
+ if (!this.ownerCurrent() || !this.#lease)
164
+ return Promise.resolve(false);
165
+ const lease = this.#lease;
166
+ const capturedAt = this.time();
167
+ this.#renewal = (async () => {
168
+ try {
169
+ const renewed = await this.options.owners.renew({
170
+ lease,
171
+ leaseMs: this.options.leaseMs,
172
+ });
173
+ if (!renewed) {
174
+ this.revoke();
175
+ return false;
176
+ }
177
+ // A delayed response may not revive a lease whose prior local budget expired.
178
+ if (this.#stopped ||
179
+ !this.ownerCurrent() ||
180
+ !this.acceptLease(renewed, capturedAt, lease)) {
181
+ this.revoke();
182
+ await this.releaseQuietly(renewed);
183
+ return false;
184
+ }
185
+ return true;
186
+ }
187
+ catch {
188
+ this.revoke();
189
+ return false;
190
+ }
191
+ finally {
192
+ this.#renewal = undefined;
193
+ }
194
+ })();
195
+ return this.#renewal;
196
+ }
197
+ /** Gateway ready/resume does not restore guild epochs or old tokens. */
198
+ resume() {
199
+ if (!this.ownerCurrent() || !this.readyIdentity())
200
+ return false;
201
+ this.#gatewayReady = true;
202
+ return true;
203
+ }
204
+ disconnect() {
205
+ this.#gatewayEpoch += 1n;
206
+ this.#gatewayReady = false;
207
+ this.#publisher.invalidateAll();
208
+ this.#guilds.clear();
209
+ this.#tokens.clear();
210
+ this.#scopeTokens.clear();
211
+ this.#reloads.clear();
212
+ this.#localJobs.clear();
213
+ }
214
+ invalidateGuild(guildId) {
215
+ this.#guilds.delete(guildId);
216
+ this.#localJobs.delete(guildId);
217
+ this.invalidateGuildAdmissions(guildId);
218
+ }
219
+ invalidateGuildAdmissions(guildId) {
220
+ this.#publisher.invalidateGuild(guildId);
221
+ for (const [token, entry] of this.#tokens)
222
+ if (entry.scope.guildId === guildId)
223
+ this.deleteToken(token, entry);
224
+ for (const key of this.#reloads.keys())
225
+ if (key.startsWith(`${guildId}:`))
226
+ this.#reloads.delete(key);
227
+ }
228
+ invalidateScope(scope) {
229
+ this.#publisher.invalidateScope(scope);
230
+ this.#reloads.delete(scopeKey(scope));
231
+ const token = this.#scopeTokens.get(scopeKey(scope));
232
+ const entry = token ? this.#tokens.get(token) : undefined;
233
+ if (token && entry)
234
+ this.deleteToken(token, entry);
235
+ }
236
+ get pendingReadCount() {
237
+ return this.#pendingReads.size;
238
+ }
239
+ get activeLocalMutationCount() {
240
+ return this.#activeLocalMutations;
241
+ }
242
+ get localReloadJobCount() {
243
+ return this.#localJobs.size;
244
+ }
245
+ async whenLocalReloadsSettled() {
246
+ while (this.#localDrain)
247
+ await this.#localDrain;
248
+ }
249
+ reload(scope) {
250
+ const key = scopeKey(scope);
251
+ const pending = this.#pendingReads.get(key);
252
+ if (pending)
253
+ return pending;
254
+ if (this.#stopped ||
255
+ this.#localHolds.has(scope.guildId) ||
256
+ this.#pendingReads.size >= this.options.maximumConcurrentReads)
257
+ return Promise.resolve(false);
258
+ const stableScope = Object.freeze({
259
+ guildId: scope.guildId,
260
+ moduleId: scope.moduleId,
261
+ });
262
+ // Reserve before invoking any reader, including a synchronously reentrant port.
263
+ const operation = Promise.resolve()
264
+ .then(() => this.reloadReserved(stableScope))
265
+ .finally(() => {
266
+ if (this.#pendingReads.get(key) === operation)
267
+ this.#pendingReads.delete(key);
268
+ });
269
+ this.#pendingReads.set(key, operation);
270
+ return operation;
271
+ }
272
+ async reloadReserved(scope) {
273
+ if (!this.ownerCurrent() ||
274
+ this.#localHolds.has(scope.guildId) ||
275
+ !this.guildCurrent(scope.guildId))
276
+ return false;
277
+ const guild = this.#guilds.get(scope.guildId);
278
+ const ticket = this.#publisher.captureReload(scope);
279
+ if (!ticket)
280
+ return false;
281
+ const key = scopeKey(scope);
282
+ this.#reloads.set(key, ticket);
283
+ let data;
284
+ try {
285
+ data = await this.options.coldData.read(scope);
286
+ }
287
+ catch {
288
+ if (this.#reloads.get(key) === ticket)
289
+ this.invalidateScope(scope);
290
+ return false;
291
+ }
292
+ if (this.#reloads.get(key) !== ticket)
293
+ return false;
294
+ if (!this.ownerCurrent() ||
295
+ this.#localHolds.has(scope.guildId) ||
296
+ !this.guildCurrent(scope.guildId) ||
297
+ this.#guilds.get(scope.guildId) !== guild ||
298
+ data.kind !== "observed" ||
299
+ data.configuredApplicationId !== this.#identity.applicationId ||
300
+ data.scope.guildId !== scope.guildId ||
301
+ data.scope.moduleId !== scope.moduleId) {
302
+ this.invalidateScope(scope);
303
+ return false;
304
+ }
305
+ // This first slice admits standalone observations only; old shard tuples hold.
306
+ if (data.guild?.ownership.kind !== "unbound") {
307
+ this.invalidateScope(scope);
308
+ return false;
309
+ }
310
+ // This await retains the original ticket, capture deadline and pending-read slot.
311
+ let settings;
312
+ try {
313
+ settings = await this.options.settings.read(data);
314
+ }
315
+ catch {
316
+ if (this.#reloads.get(key) === ticket)
317
+ this.invalidateScope(scope);
318
+ return false;
319
+ }
320
+ if (this.#reloads.get(key) !== ticket)
321
+ return false;
322
+ if (!this.ownerCurrent() ||
323
+ this.#localHolds.has(scope.guildId) ||
324
+ !this.guildCurrent(scope.guildId) ||
325
+ this.#guilds.get(scope.guildId) !== guild ||
326
+ settings.kind === "unavailable" ||
327
+ !/^[a-f0-9]{64}$/u.test(settings.revisionFingerprint)) {
328
+ this.invalidateScope(scope);
329
+ return false;
330
+ }
331
+ const expiresAt = Math.min(this.#ownerDeadline, data.freshness.expiresAt, ticket.capturedAt + this.options.maximumAgeMs);
332
+ const now = this.time();
333
+ if (!Number.isFinite(now) ||
334
+ !Number.isFinite(expiresAt) ||
335
+ !Number.isFinite(data.freshness.capturedAt) ||
336
+ !Number.isFinite(data.freshness.completedAt) ||
337
+ expiresAt <= now ||
338
+ data.freshness.capturedAt < ticket.capturedAt ||
339
+ data.freshness.completedAt < data.freshness.capturedAt ||
340
+ data.freshness.completedAt > now ||
341
+ !/^[a-f0-9]{64}$/u.test(data.revisionFingerprint) ||
342
+ !this.options.settings.isCurrent(scope)) {
343
+ this.invalidateScope(scope);
344
+ return false;
345
+ }
346
+ const enabled = Boolean(data.guild &&
347
+ data.installation?.enabled &&
348
+ !(data.availability.present && data.availability.globallyDisabled) &&
349
+ data.tierAccess.granted);
350
+ const revisionFingerprint = createHash("sha256")
351
+ .update(JSON.stringify([
352
+ 1,
353
+ this.#identity,
354
+ this.#lease?.lineageId,
355
+ this.#lease?.ownershipId,
356
+ guild.epoch,
357
+ data.revisionFingerprint,
358
+ settings.revisionFingerprint,
359
+ ]))
360
+ .digest("hex");
361
+ this.#reloads.delete(key);
362
+ return this.#publisher.publish(ticket, {
363
+ state: enabled ? "enabled" : "disabled",
364
+ revisionFingerprint,
365
+ validForMs: expiresAt - ticket.capturedAt,
366
+ });
367
+ }
368
+ /** Revocation is synchronous; durable release/drain follows it. */
369
+ stop() {
370
+ if (this.#stop)
371
+ return this.#stop;
372
+ this.revoke();
373
+ this.#stop = (async () => {
374
+ await this.#renewal;
375
+ if (this.#lease)
376
+ await this.releaseQuietly(this.#lease);
377
+ })();
378
+ return this.#stop;
379
+ }
380
+ beforeSync(guild) {
381
+ if (!this.ownerCurrent() || !this.#gatewayReady || !this.sourceGuild(guild))
382
+ return undefined;
383
+ let entry = this.#guilds.get(guild.id);
384
+ if (!entry || entry.guild !== guild) {
385
+ if (!entry && this.#guilds.size >= this.options.capacity)
386
+ return undefined;
387
+ this.invalidateGuild(guild.id);
388
+ entry = { guild, epoch: String(++this.#guildEpoch), accepted: false };
389
+ this.#guilds.set(guild.id, entry);
390
+ }
391
+ const captured = entry;
392
+ let used = false;
393
+ return () => {
394
+ if (used)
395
+ return;
396
+ used = true;
397
+ if (this.ownerCurrent() &&
398
+ this.#gatewayReady &&
399
+ this.#guilds.get(guild.id) === captured &&
400
+ this.sourceGuild(guild))
401
+ captured.accepted = true;
402
+ };
403
+ }
404
+ beginLocalMutation(guildId) {
405
+ if (!/^\d{17,20}$/u.test(guildId))
406
+ throw new TypeError("Invalid protection mutation guild");
407
+ if (this.#stopped)
408
+ return Object.freeze({ settled() { } });
409
+ if (this.#activeLocalMutations >= this.options.maximumConcurrentMutations) {
410
+ this.revoke();
411
+ throw new Error("Protection local mutation capacity exceeded");
412
+ }
413
+ this.invalidateGuildAdmissions(guildId);
414
+ this.#localJobs.delete(guildId);
415
+ let hold = this.#localHolds.get(guildId);
416
+ if (!hold) {
417
+ hold = { count: 0 };
418
+ this.#localHolds.set(guildId, hold);
419
+ }
420
+ hold.count++;
421
+ this.#activeLocalMutations++;
422
+ const captured = hold;
423
+ let settled = false;
424
+ return Object.freeze({
425
+ settled: () => {
426
+ if (settled)
427
+ return;
428
+ settled = true;
429
+ this.#activeLocalMutations--;
430
+ captured.count--;
431
+ if (this.#localHolds.get(guildId) !== captured || captured.count > 0)
432
+ return;
433
+ this.#localHolds.delete(guildId);
434
+ if (this.#stopped)
435
+ return;
436
+ try {
437
+ this.enqueueLocalReload(guildId);
438
+ }
439
+ catch {
440
+ this.revoke();
441
+ }
442
+ },
443
+ });
444
+ }
445
+ enqueueLocalReload(guildId) {
446
+ if (!this.ownerCurrent() ||
447
+ this.#localHolds.has(guildId) ||
448
+ !this.guildCurrent(guildId) ||
449
+ this.#localModuleIds.length === 0)
450
+ return;
451
+ if (!this.#localJobs.has(guildId) &&
452
+ this.#localJobs.size >= this.options.capacity) {
453
+ this.revoke();
454
+ return;
455
+ }
456
+ this.#localJobs.set(guildId, {
457
+ guild: this.#guilds.get(guildId),
458
+ gatewayEpoch: this.#gatewayEpoch,
459
+ cursor: 0,
460
+ });
461
+ this.startLocalDrain();
462
+ }
463
+ startLocalDrain() {
464
+ if (this.#localDrain)
465
+ return;
466
+ this.#localDrain = Promise.resolve()
467
+ .then(() => this.drainLocalReloads())
468
+ .catch(() => {
469
+ this.revoke();
470
+ })
471
+ .finally(() => {
472
+ this.#localDrain = undefined;
473
+ if (!this.#stopped && this.#localJobs.size > 0)
474
+ this.startLocalDrain();
475
+ });
476
+ }
477
+ async drainLocalReloads() {
478
+ while (!this.#stopped && this.#localJobs.size > 0) {
479
+ const [guildId, job] = this.#localJobs.entries().next().value;
480
+ const current = () => !this.#stopped &&
481
+ this.#localJobs.get(guildId) === job &&
482
+ this.#guilds.get(guildId) === job.guild &&
483
+ this.#gatewayEpoch === job.gatewayEpoch &&
484
+ !this.#localHolds.has(guildId) &&
485
+ this.ownerCurrent() &&
486
+ this.guildCurrent(guildId);
487
+ if (!current()) {
488
+ if (this.#localJobs.get(guildId) === job)
489
+ this.#localJobs.delete(guildId);
490
+ continue;
491
+ }
492
+ const moduleId = this.#localModuleIds[job.cursor];
493
+ const scope = { guildId, moduleId };
494
+ // A read issued before mutation is still charged, but is not fresh reconciliation.
495
+ const previous = this.#pendingReads.get(scopeKey(scope));
496
+ if (previous)
497
+ await previous.catch(() => false);
498
+ while (current() &&
499
+ this.#pendingReads.size >= this.options.maximumConcurrentReads)
500
+ await Promise.race(this.#pendingReads.values()).catch(() => false);
501
+ if (!current())
502
+ continue;
503
+ await this.reload(scope).catch(() => false);
504
+ if (this.#localJobs.get(guildId) !== job)
505
+ continue;
506
+ job.cursor++;
507
+ this.#localJobs.delete(guildId);
508
+ // One fresh attempt per recipient; failure remains held, without retry spinning.
509
+ if (job.cursor < this.#localModuleIds.length &&
510
+ currentEpoch(this.#guilds.get(guildId), job.guild, this.#gatewayEpoch, job.gatewayEpoch))
511
+ this.#localJobs.set(guildId, job);
512
+ }
513
+ }
514
+ sourceGuild(guild) {
515
+ return Boolean(this.readyIdentity() &&
516
+ guild.client === this.options.client &&
517
+ guild.available &&
518
+ this.options.client.guilds.cache.get(guild.id) === guild);
519
+ }
520
+ guildCurrent(guildId) {
521
+ const entry = this.#guilds.get(guildId);
522
+ if (!this.#gatewayReady ||
523
+ !entry?.accepted ||
524
+ !this.sourceGuild(entry.guild)) {
525
+ if (entry)
526
+ this.invalidateGuild(guildId);
527
+ return false;
528
+ }
529
+ return true;
530
+ }
531
+ readyIdentity() {
532
+ const client = this.options.client;
533
+ return Boolean(client.isReady() &&
534
+ client.user?.id === this.#identity.applicationId &&
535
+ client.application?.id === this.#identity.applicationId);
536
+ }
537
+ ownerCurrent() {
538
+ if (this.#stopped || !this.#lease)
539
+ return false;
540
+ const now = this.time();
541
+ if (!Number.isFinite(now) || now >= this.#ownerDeadline) {
542
+ this.revoke();
543
+ return false;
544
+ }
545
+ return true;
546
+ }
547
+ acceptLease(lease, capturedAt, previous) {
548
+ assertMessageProtectionOwnerLease(lease);
549
+ if (lease.identity.applicationId !== this.#identity.applicationId ||
550
+ lease.identity.instanceId !== this.#identity.instanceId ||
551
+ lease.lineageId !== previous.lineageId ||
552
+ ("ownershipId" in previous &&
553
+ lease.ownershipId !== previous.ownershipId) ||
554
+ BigInt(lease.version) !== BigInt(previous.version) + 1n)
555
+ return false;
556
+ const budget = Date.parse(lease.expiresAt) - Date.parse(lease.databaseObservedAt) - 1;
557
+ const deadline = capturedAt + Math.min(budget, this.options.leaseMs - 1);
558
+ if (!Number.isFinite(deadline) || deadline <= this.time() || this.#stopped)
559
+ return false;
560
+ this.#lease = Object.freeze({ ...lease, identity: this.#identity });
561
+ this.#ownerDeadline = deadline;
562
+ return true;
563
+ }
564
+ time() {
565
+ if (this.#stopped)
566
+ return NaN;
567
+ let now;
568
+ try {
569
+ now = this.#clock();
570
+ }
571
+ catch {
572
+ this.revoke();
573
+ return NaN;
574
+ }
575
+ if (this.#stopped ||
576
+ !Number.isFinite(now) ||
577
+ now < 0 ||
578
+ now > Number.MAX_SAFE_INTEGER - 600_000 ||
579
+ now < this.#lastNow) {
580
+ this.revoke();
581
+ return NaN;
582
+ }
583
+ this.#lastNow = now;
584
+ return now;
585
+ }
586
+ revoke() {
587
+ this.#stopped = true;
588
+ this.#gatewayReady = false;
589
+ this.#publisher.stop();
590
+ this.#guilds.clear();
591
+ this.#tokens.clear();
592
+ this.#scopeTokens.clear();
593
+ this.#reloads.clear();
594
+ this.#localJobs.clear();
595
+ }
596
+ pruneTokens() {
597
+ for (const [token, entry] of this.#tokens)
598
+ if (!this.#publisher.eligibility.isCurrent(token) ||
599
+ !this.guildCurrent(entry.scope.guildId))
600
+ this.deleteToken(token, entry);
601
+ }
602
+ deleteToken(token, entry) {
603
+ this.#tokens.delete(token);
604
+ if (this.#scopeTokens.get(scopeKey(entry.scope)) === token)
605
+ this.#scopeTokens.delete(scopeKey(entry.scope));
606
+ }
607
+ async releaseQuietly(lease) {
608
+ try {
609
+ await this.options.owners.release(lease);
610
+ }
611
+ catch {
612
+ /* Local owner remains revoked; durable lease expires. */
613
+ }
614
+ }
615
+ }
616
+ function currentEpoch(current, expected, actualGateway, expectedGateway) {
617
+ return current === expected && actualGateway === expectedGateway;
618
+ }
619
+ function scopeKey(scope) {
620
+ return `${scope.guildId}:${scope.moduleId}`;
621
+ }
622
+ //# sourceMappingURL=message-protection-coordinator.js.map