@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.
- package/CHANGELOG.md +25 -0
- package/command-root-catalogue.json +452 -0
- package/dist/cached-message-deletion-authority.d.ts +27 -0
- package/dist/cached-message-deletion-authority.js +288 -0
- package/dist/channel-audit-configuration.d.ts +11 -1
- package/dist/channel-audit-configuration.js +28 -6
- package/dist/channel-audit-control.js +42 -2
- package/dist/channel-audit-permissions.js +76 -3
- package/dist/cloud-command-policy-sync.d.ts +26 -0
- package/dist/cloud-command-policy-sync.js +198 -0
- package/dist/cloud-connector.d.ts +6 -1
- package/dist/cloud-connector.js +224 -53
- package/dist/cloud-control-dispatcher.js +18 -0
- package/dist/command-policy-runtime.d.ts +23 -0
- package/dist/command-policy-runtime.js +53 -0
- package/dist/command-root-catalogue.d.ts +5 -0
- package/dist/command-root-catalogue.js +16 -0
- package/dist/control-error-mapping.d.ts +8 -0
- package/dist/control-error-mapping.js +60 -0
- package/dist/control-errors.d.ts +3 -1
- package/dist/control-errors.js +4 -1
- package/dist/control-health-routes.d.ts +10 -0
- package/dist/control-health-routes.js +28 -0
- package/dist/control-server-schemas.d.ts +7 -0
- package/dist/control-server-schemas.js +9 -0
- package/dist/control-server.d.ts +27 -6
- package/dist/control-server.js +103 -122
- package/dist/creator-invite-key.d.ts +6 -0
- package/dist/creator-invite-key.js +16 -0
- package/dist/creator-invite-service.d.ts +3 -1
- package/dist/creator-invite-service.js +8 -7
- package/dist/creator-onboarding-platform.d.ts +1 -0
- package/dist/creator-onboarding-platform.js +5 -2
- package/dist/dashboard-feature-control.d.ts +2 -1
- package/dist/dashboard-feature-control.js +1 -0
- package/dist/dashboard-logging.d.ts +20 -0
- package/dist/dashboard-logging.js +39 -0
- package/dist/dashboard-module-projection.d.ts +1 -1
- package/dist/dashboard-module-projection.js +5 -2
- package/dist/dashboard-preview-import-routes.js +8 -0
- package/dist/database-bootstrap.d.ts +1 -1
- package/dist/database-bootstrap.js +4 -3
- package/dist/discord-channel-audit.js +1 -0
- package/dist/discord-user-identities.d.ts +14 -0
- package/dist/discord-user-identities.js +64 -0
- package/dist/emergency-deletion-dispatcher.d.ts +68 -0
- package/dist/emergency-deletion-dispatcher.js +502 -0
- package/dist/emergency-deletion-rest-transport.d.ts +24 -0
- package/dist/emergency-deletion-rest-transport.js +253 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +53 -10
- package/dist/guarded-discord-dispatcher.d.ts +29 -0
- package/dist/guarded-discord-dispatcher.js +204 -0
- package/dist/guild-command-discord-client.d.ts +2 -0
- package/dist/guild-command-discord-client.js +7 -0
- package/dist/guild-command-projection.d.ts +4 -1
- package/dist/guild-command-projection.js +25 -0
- package/dist/guild-command-read-service.d.ts +10 -1
- package/dist/guild-command-read-service.js +37 -0
- package/dist/guild-command-worker.d.ts +19 -4
- package/dist/guild-command-worker.js +356 -26
- package/dist/guild-message-protection-author-roles.d.ts +7 -0
- package/dist/guild-message-protection-author-roles.js +93 -0
- package/dist/guild-message-protection-deletion.d.ts +21 -0
- package/dist/guild-message-protection-deletion.js +138 -0
- package/dist/guild-message-protection-delivery.d.ts +41 -0
- package/dist/guild-message-protection-delivery.js +223 -0
- package/dist/guild-message-update-metadata.d.ts +32 -0
- package/dist/guild-message-update-metadata.js +255 -0
- package/dist/guilds.d.ts +2 -1
- package/dist/guilds.js +11 -1
- package/dist/hosted-server-moderation.d.ts +2 -2
- package/dist/index.js +68 -4
- package/dist/interactions.d.ts +3 -1
- package/dist/interactions.js +77 -12
- package/dist/invite-rollup-maintenance.d.ts +9 -0
- package/dist/invite-rollup-maintenance.js +12 -0
- package/dist/managed-resource-authorization.d.ts +15 -0
- package/dist/managed-resource-authorization.js +37 -0
- package/dist/managed-resource-create-defaults.d.ts +21 -0
- package/dist/managed-resource-create-defaults.js +83 -0
- package/dist/managed-resource-query.d.ts +6 -0
- package/dist/managed-resource-query.js +15 -0
- package/dist/member-role-ids.d.ts +17 -0
- package/dist/member-role-ids.js +66 -0
- package/dist/message-protection-admission.d.ts +60 -0
- package/dist/message-protection-admission.js +279 -0
- package/dist/message-protection-cold-data.d.ts +42 -0
- package/dist/message-protection-cold-data.js +180 -0
- package/dist/message-protection-coordinator.d.ts +73 -0
- package/dist/message-protection-coordinator.js +622 -0
- package/dist/message-protection-settings.d.ts +40 -0
- package/dist/message-protection-settings.js +275 -0
- package/dist/message-protection-summary.d.ts +8 -0
- package/dist/message-protection-summary.js +98 -0
- package/dist/module-availability-control.js +20 -12
- package/dist/module-logging.d.ts +3 -2
- package/dist/module-logging.js +6 -1
- package/dist/modules.js +56 -1
- package/dist/mysql-runtime-persistence.d.ts +1 -0
- package/dist/mysql-runtime-persistence.js +4 -1
- package/dist/nexus-command-recovery-control-routes.d.ts +5 -0
- package/dist/nexus-command-recovery-control-routes.js +48 -0
- package/dist/nexus-command-references.d.ts +6 -0
- package/dist/nexus-command-references.js +33 -0
- package/dist/scheduled-task-runner.js +23 -4
- package/dist/shard-command-startup.d.ts +13 -0
- package/dist/shard-command-startup.js +27 -0
- package/dist/shard-contracts.d.ts +84 -0
- package/dist/shard-contracts.js +33 -0
- package/dist/shard-core-readiness.d.ts +5 -0
- package/dist/shard-core-readiness.js +19 -0
- package/dist/shard-router.d.ts +3 -0
- package/dist/shard-router.js +10 -1
- package/dist/shard-supervisor.d.ts +1 -1
- package/dist/shard-supervisor.js +131 -43
- package/dist/shard-worker.d.ts +1 -0
- package/dist/shard-worker.js +37 -15
- package/documentation/README.md +1 -0
- package/documentation/guides/self-hosting.md +54 -0
- package/package.json +18 -17
|
@@ -12,6 +12,10 @@ export class GuildCommandWorker {
|
|
|
12
12
|
#installations;
|
|
13
13
|
#availability;
|
|
14
14
|
#modules;
|
|
15
|
+
#commandRestrictions;
|
|
16
|
+
#commandCatalogue;
|
|
17
|
+
#commandPolicyDelivery;
|
|
18
|
+
#catalogueReady = false;
|
|
15
19
|
#logger;
|
|
16
20
|
#workerId;
|
|
17
21
|
#fencingToken;
|
|
@@ -41,6 +45,9 @@ export class GuildCommandWorker {
|
|
|
41
45
|
this.#installations = repositories.installations;
|
|
42
46
|
this.#availability = repositories.availability;
|
|
43
47
|
this.#modules = input.modules;
|
|
48
|
+
this.#commandRestrictions = input.commandRestrictions;
|
|
49
|
+
this.#commandCatalogue = input.commandCatalogue;
|
|
50
|
+
this.#commandPolicyDelivery = input.commandPolicyDelivery;
|
|
44
51
|
this.#logger = input.logger;
|
|
45
52
|
this.#workerId = input.workerId ?? `guild-command:${randomUUID()}`;
|
|
46
53
|
this.#fencingToken = input.fencingToken;
|
|
@@ -62,6 +69,16 @@ export class GuildCommandWorker {
|
|
|
62
69
|
async runOnce() {
|
|
63
70
|
if (this.#stopping)
|
|
64
71
|
return;
|
|
72
|
+
await this.#commandRestrictions?.recheck();
|
|
73
|
+
try {
|
|
74
|
+
await this.#commandCatalogue?.recheck();
|
|
75
|
+
this.#catalogueReady = this.#commandCatalogue !== undefined;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
this.#catalogueReady = false;
|
|
79
|
+
this.#logger.warn({ event: "guild_commands.catalogue_unavailable" }, "Command catalogue recovery is unavailable");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
65
82
|
const capacity = Math.max(0, this.#concurrency - this.#active.size);
|
|
66
83
|
if (capacity === 0)
|
|
67
84
|
return;
|
|
@@ -154,12 +171,15 @@ export class GuildCommandWorker {
|
|
|
154
171
|
const heartbeat = setInterval(renew, Math.max(1_000, Math.floor(this.#leaseMs / 3)));
|
|
155
172
|
heartbeat.unref();
|
|
156
173
|
try {
|
|
157
|
-
const [enabled, availability, state] = await Promise.all([
|
|
174
|
+
const [enabled, availability, state, rootMappings] = await Promise.all([
|
|
158
175
|
this.#installations.listEnabledModules(request.guildId),
|
|
159
176
|
this.#availability.list(),
|
|
160
177
|
this.#repository.getState(request.applicationId, request.guildId),
|
|
178
|
+
this.#repository.listRootMappings(request.applicationId, request.guildId),
|
|
161
179
|
]);
|
|
162
|
-
if (!state ||
|
|
180
|
+
if (!state ||
|
|
181
|
+
state.desiredRevision !== request.requestedRevision ||
|
|
182
|
+
rootMappings.some(({ desiredRevision }) => desiredRevision > request.requestedRevision)) {
|
|
163
183
|
await this.#repository.completeDryRun({
|
|
164
184
|
request,
|
|
165
185
|
workerId: this.#workerId,
|
|
@@ -174,6 +194,35 @@ export class GuildCommandWorker {
|
|
|
174
194
|
});
|
|
175
195
|
return;
|
|
176
196
|
}
|
|
197
|
+
const currentPolicy = await this.#commandRestrictions?.getPolicy();
|
|
198
|
+
for (const mapping of rootMappings) {
|
|
199
|
+
if (mapping.canonicalRoot !== "nexus")
|
|
200
|
+
throw new GuildCommandLiveError("unsupported_root_mapping");
|
|
201
|
+
if (mapping.desiredRoot === "nexus")
|
|
202
|
+
continue;
|
|
203
|
+
const policy = currentPolicy ??
|
|
204
|
+
this.#modules.find(({ manifest }) => manifest.id === "helyx.nexus-core")?.nexusCommandNamePolicy;
|
|
205
|
+
const result = policy?.evaluate({
|
|
206
|
+
guildId: request.guildId,
|
|
207
|
+
proposedName: mapping.desiredRoot,
|
|
208
|
+
});
|
|
209
|
+
if (!policy?.version ||
|
|
210
|
+
!result ||
|
|
211
|
+
result.state === "unconfigured" ||
|
|
212
|
+
result.policyVersion !== policy.version)
|
|
213
|
+
throw new GuildCommandLiveError("command_name_policy_unavailable");
|
|
214
|
+
if (result.state === "blocked")
|
|
215
|
+
throw new GuildCommandLiveError("command_name_blocked");
|
|
216
|
+
}
|
|
217
|
+
let reservedCommandNames = [];
|
|
218
|
+
const catalogueReady = this.#catalogueReady;
|
|
219
|
+
try {
|
|
220
|
+
reservedCommandNames =
|
|
221
|
+
(await this.#commandCatalogue?.getReservedCommandNames()) ?? [];
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new GuildCommandLiveError("command_catalogue_unavailable");
|
|
225
|
+
}
|
|
177
226
|
const projection = projectGuildCommands({
|
|
178
227
|
modules: this.#modules,
|
|
179
228
|
enabledModuleIds: new Set(enabled.map(({ moduleId }) => moduleId)),
|
|
@@ -182,7 +231,16 @@ export class GuildCommandWorker {
|
|
|
182
231
|
.map(({ moduleId }) => moduleId)),
|
|
183
232
|
profile: state.visibilityProfile,
|
|
184
233
|
includeInternalCommands: request.guildId === this.#internalGuildId,
|
|
234
|
+
rootMappings,
|
|
235
|
+
reservedCommandNames,
|
|
185
236
|
});
|
|
237
|
+
await this.#validatePolicyDelivery(projection, currentPolicy?.version ??
|
|
238
|
+
this.#modules.find(({ manifest }) => manifest.id === "helyx.nexus-core")?.nexusCommandNamePolicy?.version ??
|
|
239
|
+
undefined);
|
|
240
|
+
if (!catalogueReady &&
|
|
241
|
+
rootMappings.some(({ desiredRoot }) => desiredRoot !== "nexus") &&
|
|
242
|
+
projection.definitions.some(({ ownerModuleId }) => ownerModuleId === "helyx.nexus-core"))
|
|
243
|
+
throw new GuildCommandLiveError("command_catalogue_unavailable");
|
|
186
244
|
if (this.#discord?.canWrite(request.guildId) &&
|
|
187
245
|
leaseAlive &&
|
|
188
246
|
!this.#controller.signal.aborted) {
|
|
@@ -216,7 +274,8 @@ export class GuildCommandWorker {
|
|
|
216
274
|
if (safeCode === "discord_observation_failed" ||
|
|
217
275
|
safeCode === "discord_rate_limited" ||
|
|
218
276
|
safeCode === "discord_write_ambiguous" ||
|
|
219
|
-
safeCode === "discord_write_mismatch"
|
|
277
|
+
safeCode === "discord_write_mismatch" ||
|
|
278
|
+
safeCode === "command_policy_unverified")
|
|
220
279
|
await this.#repository.defer({
|
|
221
280
|
request,
|
|
222
281
|
workerId: this.#workerId,
|
|
@@ -276,6 +335,20 @@ export class GuildCommandWorker {
|
|
|
276
335
|
leaseMs: this.#leaseMs,
|
|
277
336
|
verificationDelayMs: 1_000,
|
|
278
337
|
signal: this.#controller.signal,
|
|
338
|
+
validateWrite: async () => {
|
|
339
|
+
let reservedCommandNames = [];
|
|
340
|
+
if (this.#commandCatalogue) {
|
|
341
|
+
try {
|
|
342
|
+
reservedCommandNames =
|
|
343
|
+
await this.#commandCatalogue.getReservedCommandNames();
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
throw new GuildCommandLiveError("command_catalogue_unavailable");
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const policyVersion = await this.#validateCurrentNexusProjection(projection, reservedCommandNames, request.guildId);
|
|
350
|
+
await this.#validatePolicyDelivery(projection, policyVersion);
|
|
351
|
+
},
|
|
279
352
|
});
|
|
280
353
|
if (!result)
|
|
281
354
|
return;
|
|
@@ -291,6 +364,44 @@ export class GuildCommandWorker {
|
|
|
291
364
|
outcome: result.outcome,
|
|
292
365
|
}, "Guild commands reconciled");
|
|
293
366
|
}
|
|
367
|
+
async #validatePolicyDelivery(projection, policyVersion) {
|
|
368
|
+
if (!this.#commandPolicyDelivery ||
|
|
369
|
+
!projection.definitions.some(({ ownerModuleId, name }) => ownerModuleId === "helyx.nexus-core" && name !== "nexus"))
|
|
370
|
+
return;
|
|
371
|
+
try {
|
|
372
|
+
const gate = await this.#commandPolicyDelivery.readGate();
|
|
373
|
+
if (gate.ready &&
|
|
374
|
+
policyVersion === `command-restrictions:${String(gate.revision)}`)
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
/* A read failure cannot authorize an unverified custom root. */
|
|
379
|
+
}
|
|
380
|
+
throw new GuildCommandLiveError("command_policy_unverified");
|
|
381
|
+
}
|
|
382
|
+
async #validateCurrentNexusProjection(projection, reservedCommandNames, guildId) {
|
|
383
|
+
const names = projection.definitions
|
|
384
|
+
.filter(({ ownerModuleId, name }) => ownerModuleId === "helyx.nexus-core" && name !== "nexus")
|
|
385
|
+
.map(({ name }) => name);
|
|
386
|
+
if (!names.length)
|
|
387
|
+
return undefined;
|
|
388
|
+
const policy = (await this.#commandRestrictions?.getPolicy()) ??
|
|
389
|
+
this.#modules.find(({ manifest }) => manifest.id === "helyx.nexus-core")
|
|
390
|
+
?.nexusCommandNamePolicy;
|
|
391
|
+
for (const name of names) {
|
|
392
|
+
if (reservedCommandNames.includes(name))
|
|
393
|
+
throw new GuildCommandLiveError("command_name_reserved");
|
|
394
|
+
const result = policy?.evaluate({ guildId, proposedName: name });
|
|
395
|
+
if (!policy?.version ||
|
|
396
|
+
!result ||
|
|
397
|
+
result.state === "unconfigured" ||
|
|
398
|
+
result.policyVersion !== policy.version)
|
|
399
|
+
throw new GuildCommandLiveError("command_name_policy_unavailable");
|
|
400
|
+
if (result.state === "blocked")
|
|
401
|
+
throw new GuildCommandLiveError("command_name_blocked");
|
|
402
|
+
}
|
|
403
|
+
return policy?.version ?? undefined;
|
|
404
|
+
}
|
|
294
405
|
}
|
|
295
406
|
export async function reconcileLiveGuildCommands(input) {
|
|
296
407
|
let observed;
|
|
@@ -302,6 +413,68 @@ export async function reconcileLiveGuildCommands(input) {
|
|
|
302
413
|
? "discord_rate_limited"
|
|
303
414
|
: "discord_observation_failed");
|
|
304
415
|
}
|
|
416
|
+
// A name match is not an ownership match. Delete a tracked Nexus ID before
|
|
417
|
+
// the ordinary bulk writer can reuse it for a different module's root.
|
|
418
|
+
const handedOffRoots = new Set();
|
|
419
|
+
const retiredDuringRun = new Set();
|
|
420
|
+
if (diffGuildCommands(input.projection, observed).conflicts.length > 0)
|
|
421
|
+
throw new GuildCommandLiveError("discord_command_conflict");
|
|
422
|
+
const handoffs = (input.projection.rootMappings ?? []).filter((mapping) => mapping.observedRoot !== null &&
|
|
423
|
+
mapping.observedCommandId !== null &&
|
|
424
|
+
input.projection.definitions.some(({ name, type, ownerModuleId }) => type === 1 &&
|
|
425
|
+
name === mapping.observedRoot &&
|
|
426
|
+
ownerModuleId !== "helyx.nexus-core"));
|
|
427
|
+
for (const mapping of handoffs) {
|
|
428
|
+
if (mapping.applicationId !== input.request.applicationId ||
|
|
429
|
+
mapping.guildId !== input.request.guildId ||
|
|
430
|
+
mapping.canonicalRoot !== "nexus")
|
|
431
|
+
throw new GuildCommandLiveError("unsupported_root_mapping");
|
|
432
|
+
if (!(await currentLease()))
|
|
433
|
+
return null;
|
|
434
|
+
handedOffRoots.add(mapping.observedRoot);
|
|
435
|
+
retiredDuringRun.add(mapping.observedCommandId);
|
|
436
|
+
observed = await removeTrackedId(mapping.observedCommandId, mapping.observedRoot);
|
|
437
|
+
if (!(await currentLease()))
|
|
438
|
+
return null;
|
|
439
|
+
let retired;
|
|
440
|
+
try {
|
|
441
|
+
retired = await input.repository.retireRootObservation({
|
|
442
|
+
request: input.request,
|
|
443
|
+
workerId: input.workerId,
|
|
444
|
+
fencingToken: input.fencingToken,
|
|
445
|
+
canonicalRoot: mapping.canonicalRoot,
|
|
446
|
+
settingsRevision: mapping.settingsRevision,
|
|
447
|
+
desiredRevision: mapping.desiredRevision,
|
|
448
|
+
expectedObservedRoot: mapping.observedRoot,
|
|
449
|
+
expectedObservedCommandId: mapping.observedCommandId,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
throw new GuildCommandLiveError("discord_write_ambiguous");
|
|
454
|
+
}
|
|
455
|
+
if (!retired)
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
// A previous attempt may have committed retirement and then crashed before
|
|
459
|
+
// installing the new owner. The scoped ledger also protects that retry path.
|
|
460
|
+
for (const command of [...observed]) {
|
|
461
|
+
if (!command.id ||
|
|
462
|
+
!/^\d{17,20}$/u.test(command.id) ||
|
|
463
|
+
(command.type ?? 1) !== 1 ||
|
|
464
|
+
!input.projection.definitions.some(({ name, type, ownerModuleId }) => type === 1 &&
|
|
465
|
+
name === command.name &&
|
|
466
|
+
ownerModuleId !== "helyx.nexus-core"))
|
|
467
|
+
continue;
|
|
468
|
+
if (await input.repository.isRetiredRootCommandId(input.request.applicationId, input.request.guildId, command.id)) {
|
|
469
|
+
if (!(await currentLease()))
|
|
470
|
+
return null;
|
|
471
|
+
handedOffRoots.add(command.name);
|
|
472
|
+
retiredDuringRun.add(command.id);
|
|
473
|
+
observed = await removeTrackedId(command.id, command.name);
|
|
474
|
+
if (!(await currentLease()))
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
305
478
|
const diff = diffGuildCommands(input.projection, observed);
|
|
306
479
|
if (diff.conflicts.length > 0)
|
|
307
480
|
throw new GuildCommandLiveError("discord_command_conflict");
|
|
@@ -326,11 +499,19 @@ export async function reconcileLiveGuildCommands(input) {
|
|
|
326
499
|
throw error;
|
|
327
500
|
}
|
|
328
501
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
502
|
+
let current;
|
|
503
|
+
try {
|
|
504
|
+
current = await currentLease();
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
if (ledgerId)
|
|
508
|
+
await input.repository.settleCreation({
|
|
509
|
+
ledgerId,
|
|
510
|
+
state: "released",
|
|
511
|
+
observedReference: "write_not_started",
|
|
512
|
+
});
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
334
515
|
if (!current || input.signal?.aborted) {
|
|
335
516
|
if (ledgerId)
|
|
336
517
|
await input.repository.settleCreation({
|
|
@@ -385,26 +566,175 @@ export async function reconcileLiveGuildCommands(input) {
|
|
|
385
566
|
});
|
|
386
567
|
throw new GuildCommandLiveError("discord_write_mismatch");
|
|
387
568
|
}
|
|
569
|
+
observed = written;
|
|
388
570
|
}
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
571
|
+
const rootMappings = input.projection.rootMappings ?? [];
|
|
572
|
+
if (rootMappings.length > 0) {
|
|
573
|
+
// A successful replacement response alone does not activate a new route.
|
|
574
|
+
// Read the full collection back before recording its actual Discord ID.
|
|
575
|
+
try {
|
|
576
|
+
observed = await input.discord.observe(input.request.guildId);
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
if (ledgerId)
|
|
580
|
+
await input.repository.settleCreation({
|
|
581
|
+
ledgerId,
|
|
582
|
+
state: "committed",
|
|
583
|
+
observedReference: `discord:guild:${input.request.guildId}:root_readback_ambiguous`,
|
|
584
|
+
});
|
|
585
|
+
throw new GuildCommandLiveError("discord_observation_failed");
|
|
586
|
+
}
|
|
587
|
+
if (!hasNoChanges(diffGuildCommands(input.projection, observed))) {
|
|
588
|
+
if (ledgerId)
|
|
589
|
+
await input.repository.settleCreation({
|
|
590
|
+
ledgerId,
|
|
591
|
+
state: "committed",
|
|
592
|
+
observedReference: `discord:guild:${input.request.guildId}:root_readback_mismatch`,
|
|
593
|
+
});
|
|
594
|
+
throw new GuildCommandLiveError("discord_write_mismatch");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const rootObservations = [];
|
|
598
|
+
try {
|
|
599
|
+
await input.validateWrite?.();
|
|
600
|
+
for (const command of observed) {
|
|
601
|
+
if (handedOffRoots.has(command.name) &&
|
|
602
|
+
(!command.id ||
|
|
603
|
+
!/^\d{17,20}$/u.test(command.id) ||
|
|
604
|
+
(command.application_id !== undefined &&
|
|
605
|
+
command.application_id !== input.request.applicationId) ||
|
|
606
|
+
(command.guild_id !== undefined &&
|
|
607
|
+
command.guild_id !== input.request.guildId)))
|
|
608
|
+
throw new GuildCommandLiveError("discord_observation_failed");
|
|
609
|
+
if (command.id && retiredDuringRun.has(command.id))
|
|
610
|
+
throw new GuildCommandLiveError("discord_write_mismatch");
|
|
611
|
+
if (command.id &&
|
|
612
|
+
/^\d{17,20}$/u.test(command.id) &&
|
|
613
|
+
input.projection.definitions.some(({ name, type, ownerModuleId }) => type === 1 &&
|
|
614
|
+
name === command.name &&
|
|
615
|
+
ownerModuleId !== "helyx.nexus-core") &&
|
|
616
|
+
(await input.repository.isRetiredRootCommandId(input.request.applicationId, input.request.guildId, command.id)))
|
|
617
|
+
throw new GuildCommandLiveError("discord_write_mismatch");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch (error) {
|
|
621
|
+
if (ledgerId)
|
|
622
|
+
await retainCreatedCapacity();
|
|
623
|
+
throw error instanceof GuildCommandLiveError
|
|
624
|
+
? error
|
|
625
|
+
: new GuildCommandLiveError("discord_observation_failed");
|
|
626
|
+
}
|
|
627
|
+
for (const mapping of rootMappings) {
|
|
628
|
+
const projected = input.projection.definitions.find(({ name, type, ownerModuleId }) => name === mapping.desiredRoot &&
|
|
629
|
+
type === 1 &&
|
|
630
|
+
ownerModuleId === "helyx.nexus-core");
|
|
631
|
+
const actual = projected
|
|
632
|
+
? observed.find(({ name, type }) => name === mapping.desiredRoot && (type ?? 1) === 1)
|
|
633
|
+
: undefined;
|
|
634
|
+
if (projected && (!actual?.id || !/^\d{17,20}$/u.test(actual.id))) {
|
|
635
|
+
if (ledgerId)
|
|
636
|
+
await input.repository.settleCreation({
|
|
637
|
+
ledgerId,
|
|
638
|
+
state: "committed",
|
|
639
|
+
observedReference: `discord:guild:${input.request.guildId}:root_id_unverified`,
|
|
640
|
+
});
|
|
641
|
+
throw new GuildCommandLiveError("discord_observation_failed");
|
|
642
|
+
}
|
|
643
|
+
rootObservations.push({
|
|
644
|
+
canonicalRoot: mapping.canonicalRoot,
|
|
645
|
+
desiredRevision: mapping.desiredRevision,
|
|
646
|
+
settingsRevision: mapping.settingsRevision,
|
|
647
|
+
observedRoot: actual?.name ?? null,
|
|
648
|
+
observedCommandId: actual?.id ?? null,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
let outcome;
|
|
652
|
+
try {
|
|
653
|
+
outcome = await input.repository.completeLive({
|
|
654
|
+
request: input.request,
|
|
655
|
+
workerId: input.workerId,
|
|
656
|
+
fencingToken: input.fencingToken,
|
|
657
|
+
definitionHash: input.projection.definitionHash,
|
|
658
|
+
rootCount: input.projection.rootCount,
|
|
659
|
+
subcommandCount: input.projection.subcommandCount,
|
|
660
|
+
observedDefinitionHash: input.projection.definitionHash,
|
|
661
|
+
installedRootCount: input.projection.rootCount,
|
|
662
|
+
createdCount: diff.create.length,
|
|
663
|
+
updatedCount: diff.update.length,
|
|
664
|
+
removedCount: diff.remove.length,
|
|
665
|
+
createdCommands: diff.create.map(({ name }) => `/${name}`),
|
|
666
|
+
updatedCommands: diff.update.map(({ name }) => `/${name}`),
|
|
667
|
+
removedCommands: diff.remove.map(({ name }) => `/${name}`),
|
|
668
|
+
...(rootObservations.length > 0 ? { rootObservations } : {}),
|
|
669
|
+
...(ledgerId ? { ledgerId } : {}),
|
|
670
|
+
observedReference: `discord:guild:${input.request.guildId}:roots:${input.projection.rootCount}`,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
if (ledgerId)
|
|
675
|
+
await retainCreatedCapacity();
|
|
676
|
+
throw error;
|
|
677
|
+
}
|
|
678
|
+
if (ledgerId && outcome !== "completed")
|
|
679
|
+
await retainCreatedCapacity();
|
|
407
680
|
return { outcome, diff };
|
|
681
|
+
async function currentLease() {
|
|
682
|
+
if (input.signal?.aborted ||
|
|
683
|
+
!(await input.repository.isLeaseCurrent({
|
|
684
|
+
request: input.request,
|
|
685
|
+
workerId: input.workerId,
|
|
686
|
+
fencingToken: input.fencingToken,
|
|
687
|
+
})))
|
|
688
|
+
return false;
|
|
689
|
+
// Final freshness must follow every potentially delayed catalogue/lease read.
|
|
690
|
+
await input.validateWrite?.();
|
|
691
|
+
return !input.signal?.aborted;
|
|
692
|
+
}
|
|
693
|
+
async function removeTrackedId(commandId, expectedName) {
|
|
694
|
+
const target = observed.find(({ id }) => id === commandId);
|
|
695
|
+
if (observed.some(({ name, id }) => name === expectedName && !id) ||
|
|
696
|
+
(target &&
|
|
697
|
+
(target.name !== expectedName ||
|
|
698
|
+
(target.type ?? 1) !== 1 ||
|
|
699
|
+
(target.application_id !== undefined &&
|
|
700
|
+
target.application_id !== input.request.applicationId) ||
|
|
701
|
+
(target.guild_id !== undefined &&
|
|
702
|
+
target.guild_id !== input.request.guildId))))
|
|
703
|
+
throw new GuildCommandLiveError("discord_observation_failed");
|
|
704
|
+
if (!target)
|
|
705
|
+
return observed;
|
|
706
|
+
try {
|
|
707
|
+
await input.discord.remove(input.request.guildId, commandId);
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
/* A timeout or 404 is settled only by the independent readback. */
|
|
711
|
+
}
|
|
712
|
+
let readback;
|
|
713
|
+
try {
|
|
714
|
+
readback = await input.discord.observe(input.request.guildId);
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
throw new GuildCommandLiveError("discord_observation_failed");
|
|
718
|
+
}
|
|
719
|
+
if (readback.some(({ id }) => id === commandId) ||
|
|
720
|
+
readback.some(({ name, id }) => name === expectedName && !id))
|
|
721
|
+
throw new GuildCommandLiveError("discord_write_mismatch");
|
|
722
|
+
return readback;
|
|
723
|
+
}
|
|
724
|
+
async function retainCreatedCapacity() {
|
|
725
|
+
// Discord was already written or read back successfully. A failed/fenced
|
|
726
|
+
// completion must not release real creations when its reservation expires.
|
|
727
|
+
try {
|
|
728
|
+
await input.repository.settleCreation({
|
|
729
|
+
ledgerId: ledgerId,
|
|
730
|
+
state: "committed",
|
|
731
|
+
observedReference: `discord:guild:${input.request.guildId}:completion_unacknowledged`,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// Preserve the original completion error; durable retry still owns it.
|
|
736
|
+
}
|
|
737
|
+
}
|
|
408
738
|
}
|
|
409
739
|
class GuildCommandLiveError extends Error {
|
|
410
740
|
safeCode;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Message } from "discord.js";
|
|
2
|
+
/** Pinned cache compatibility: public member.roles.cache silently omits uncached assigned IDs. */
|
|
3
|
+
export declare function snapshotProtectionAuthorRoles(message: Message, webhook: boolean): Readonly<{
|
|
4
|
+
authorRoleIds: readonly string[];
|
|
5
|
+
authorRolesResolved: boolean;
|
|
6
|
+
}>;
|
|
7
|
+
//# sourceMappingURL=guild-message-protection-author-roles.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const maximumAssignedRoles = 250; // Private snapshot bound, not a Discord product limit.
|
|
2
|
+
const maximumPublishedRoles = 250;
|
|
3
|
+
const snowflake = /^\d{17,20}$/u;
|
|
4
|
+
const unknown = Object.freeze({
|
|
5
|
+
authorRoleIds: Object.freeze([]),
|
|
6
|
+
authorRolesResolved: false,
|
|
7
|
+
});
|
|
8
|
+
const webhookRoles = Object.freeze({
|
|
9
|
+
authorRoleIds: Object.freeze([]),
|
|
10
|
+
authorRolesResolved: true,
|
|
11
|
+
});
|
|
12
|
+
/** Pinned cache compatibility: public member.roles.cache silently omits uncached assigned IDs. */
|
|
13
|
+
export function snapshotProtectionAuthorRoles(message, webhook) {
|
|
14
|
+
if (webhook)
|
|
15
|
+
return webhookRoles;
|
|
16
|
+
try {
|
|
17
|
+
const guildId = message.guildId, authorId = message.author.id;
|
|
18
|
+
const client = message.client, guild = message.guild, member = message.member;
|
|
19
|
+
if (!guildId ||
|
|
20
|
+
!snowflake.test(guildId) ||
|
|
21
|
+
!snowflake.test(authorId) ||
|
|
22
|
+
!guild ||
|
|
23
|
+
!member ||
|
|
24
|
+
guild.id !== guildId ||
|
|
25
|
+
guild.client !== client ||
|
|
26
|
+
guild.available !== true ||
|
|
27
|
+
client.guilds.cache.get(guildId) !== guild ||
|
|
28
|
+
member.guild !== guild ||
|
|
29
|
+
member.client !== client ||
|
|
30
|
+
member.id !== authorId ||
|
|
31
|
+
member.user.id !== authorId ||
|
|
32
|
+
member.partial !== false ||
|
|
33
|
+
guild.members.cache.get(authorId) !== member)
|
|
34
|
+
return unknown;
|
|
35
|
+
const assigned = Reflect.get(member, "_roles");
|
|
36
|
+
if (!Array.isArray(assigned) || assigned.length > maximumAssignedRoles)
|
|
37
|
+
return unknown;
|
|
38
|
+
const length = assigned.length, snapshot = [], seen = new Set();
|
|
39
|
+
for (let index = 0; index < length; index++) {
|
|
40
|
+
const id = assigned[index];
|
|
41
|
+
if (typeof id !== "string" ||
|
|
42
|
+
!snowflake.test(id) ||
|
|
43
|
+
id === guildId ||
|
|
44
|
+
seen.has(id))
|
|
45
|
+
return unknown;
|
|
46
|
+
seen.add(id);
|
|
47
|
+
snapshot.push(id);
|
|
48
|
+
}
|
|
49
|
+
if (assigned.length !== length)
|
|
50
|
+
return unknown;
|
|
51
|
+
seen.add(guildId);
|
|
52
|
+
const roles = [];
|
|
53
|
+
// Role IDs suffice for exemption metadata. No permission bits/names or member fetch.
|
|
54
|
+
for (const id of seen) {
|
|
55
|
+
const role = guild.roles.cache.get(id);
|
|
56
|
+
if (!role ||
|
|
57
|
+
role.id !== id ||
|
|
58
|
+
role.guild !== guild ||
|
|
59
|
+
role.client !== client)
|
|
60
|
+
return unknown;
|
|
61
|
+
roles.push({ id, role });
|
|
62
|
+
}
|
|
63
|
+
for (const { id, role } of roles) {
|
|
64
|
+
if (guild.roles.cache.get(id) !== role ||
|
|
65
|
+
role.id !== id ||
|
|
66
|
+
role.guild !== guild ||
|
|
67
|
+
role.client !== client)
|
|
68
|
+
return unknown;
|
|
69
|
+
}
|
|
70
|
+
if (client.guilds.cache.get(guildId) !== guild ||
|
|
71
|
+
guild.available !== true ||
|
|
72
|
+
message.guildId !== guildId ||
|
|
73
|
+
message.author.id !== authorId ||
|
|
74
|
+
guild.members.cache.get(authorId) !== member ||
|
|
75
|
+
member.guild !== guild ||
|
|
76
|
+
member.client !== client ||
|
|
77
|
+
member.id !== authorId ||
|
|
78
|
+
member.partial !== false ||
|
|
79
|
+
Reflect.get(member, "_roles") !== assigned ||
|
|
80
|
+
assigned.length !== length ||
|
|
81
|
+
snapshot.some((id, index) => assigned[index] !== id))
|
|
82
|
+
return unknown;
|
|
83
|
+
const ids = [...seen].sort();
|
|
84
|
+
return Object.freeze({
|
|
85
|
+
authorRoleIds: Object.freeze(ids.slice(0, maximumPublishedRoles)),
|
|
86
|
+
authorRolesResolved: ids.length <= maximumPublishedRoles,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return unknown;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=guild-message-protection-author-roles.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { GuildMessageProtectionDeletionResult } from "@helyx/sdk";
|
|
2
|
+
import type { Client, GuildBasedChannel } from "discord.js";
|
|
3
|
+
import { type CachedMessageDeletionAuthority, type CachedMessageDeletionScope } from "./cached-message-deletion-authority.js";
|
|
4
|
+
import type { EmergencyDeletionDispatcher } from "./emergency-deletion-dispatcher.js";
|
|
5
|
+
import type { MessageProtectionEligibility } from "./guild-message-protection-delivery.js";
|
|
6
|
+
export type MessageProtectionAuthorityFactory = (source: Readonly<CachedMessageDeletionScope & {
|
|
7
|
+
channel: GuildBasedChannel;
|
|
8
|
+
}>, isStopped: () => boolean) => CachedMessageDeletionAuthority | undefined;
|
|
9
|
+
/** Separate lexical scope: queued grants never capture an execute/Message environment. */
|
|
10
|
+
export declare function createProtectionDeliveryLifetime(): Readonly<{
|
|
11
|
+
isStopped: () => boolean;
|
|
12
|
+
stop: () => void;
|
|
13
|
+
}>;
|
|
14
|
+
/** Explicit private composition only; never derive configuration from mutable client identity. */
|
|
15
|
+
export declare function createMessageProtectionAuthorityFactory(client: Client, expectedApplicationId: string | undefined, eligibility: MessageProtectionEligibility | undefined, deletion: EmergencyDeletionDispatcher | undefined): MessageProtectionAuthorityFactory | undefined;
|
|
16
|
+
/** Handler-local owner; only its content-free grant is handed to the queue. */
|
|
17
|
+
export declare function createProtectionDeletionOperation(input: CachedMessageDeletionScope, deletion: EmergencyDeletionDispatcher | undefined, strict: boolean, captured: CachedMessageDeletionAuthority | undefined, isActive: () => boolean): Readonly<{
|
|
18
|
+
releaseUnused: () => void;
|
|
19
|
+
invoke: () => Promise<GuildMessageProtectionDeletionResult>;
|
|
20
|
+
}>;
|
|
21
|
+
//# sourceMappingURL=guild-message-protection-deletion.d.ts.map
|