@helyx/bot 0.4.40 → 0.4.42
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 +12 -0
- package/dist/control-server.d.ts +1 -0
- package/dist/control-server.js +13 -17
- package/dist/modules.js +18 -0
- package/dist/privacy-provider-registry.d.ts +8 -0
- package/dist/privacy-provider-registry.js +20 -0
- package/dist/shard-supervisor.d.ts +6 -1
- package/dist/shard-supervisor.js +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.42
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Include in-progress giveaway draws linked to a Discord user in privacy access and erasure processing, and reject modules that omit Discord identity record fields from their privacy declarations.
|
|
8
|
+
|
|
9
|
+
## 0.4.41
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Keep coordinated rolling deployments ready through remote shard ownership and atomically escalate terminal privacy-provider failures to staff review.
|
|
14
|
+
|
|
3
15
|
## 0.4.40
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/dist/control-server.d.ts
CHANGED
|
@@ -559,6 +559,7 @@ export declare function createBotControlServer(input: {
|
|
|
559
559
|
level: string;
|
|
560
560
|
};
|
|
561
561
|
shardHealth?: () => Promise<ShardHealthSnapshot>;
|
|
562
|
+
readiness?: () => Promise<boolean>;
|
|
562
563
|
guildCommands?: Parameters<typeof registerGuildCommandControlRoutes>[1];
|
|
563
564
|
}): Promise<FastifyInstance>;
|
|
564
565
|
export declare function mapBotControlError(error: unknown): {
|
package/dist/control-server.js
CHANGED
|
@@ -15,6 +15,7 @@ import { assertVersion, dashboardActionPermissionId, declaredManagedResourcePerm
|
|
|
15
15
|
import { dashboardCommands, } from "./dashboard-commands.js";
|
|
16
16
|
import { availabilityCatalogueDefinitions, availabilityCatalogue, dashboardModuleStatus, disabledModuleSummary, moduleAvailabilityService, moduleAvailabilityStates, } from "./module-availability-control.js";
|
|
17
17
|
import { registerGuildCommandControlRoutes } from "./guild-command-control-routes.js";
|
|
18
|
+
import { createPrivacyProviderRegistry, describePrivacyProviders, } from "./privacy-provider-registry.js";
|
|
18
19
|
const snowflake = z.string().regex(/^\d{17,20}$/u);
|
|
19
20
|
const moduleId = z.string().regex(/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)+$/u);
|
|
20
21
|
const MAX_DISCORD_SETTING_SELECTIONS = 25;
|
|
@@ -284,14 +285,7 @@ export class BotControlService {
|
|
|
284
285
|
this.#installations = services.get(HELYX_SERVICE_NAMES.installations);
|
|
285
286
|
this.#permissions = services.get(HELYX_SERVICE_NAMES.permissions);
|
|
286
287
|
this.#services = services;
|
|
287
|
-
this.#privacyProviders =
|
|
288
|
-
? modules
|
|
289
|
-
.filter((module) => module.privacy !== undefined)
|
|
290
|
-
.map((module) => {
|
|
291
|
-
const provider = module.privacy.create(privacyExecutor, this.#services);
|
|
292
|
-
return [provider.id, provider];
|
|
293
|
-
})
|
|
294
|
-
: []);
|
|
288
|
+
this.#privacyProviders = createPrivacyProviderRegistry(modules, privacyExecutor, this.#services);
|
|
295
289
|
}
|
|
296
290
|
async cleanupOAuthConsumer(input) {
|
|
297
291
|
const module = this.#modules.get(input.moduleId);
|
|
@@ -391,13 +385,7 @@ export class BotControlService {
|
|
|
391
385
|
.then((receipt) => actionReceiptSchema.parse(receipt));
|
|
392
386
|
}
|
|
393
387
|
listPrivacyProviders() {
|
|
394
|
-
return Promise.resolve(
|
|
395
|
-
.map((provider) => ({
|
|
396
|
-
id: provider.id,
|
|
397
|
-
version: provider.version,
|
|
398
|
-
containsPersonalData: provider.describe().containsPersonalData,
|
|
399
|
-
}))
|
|
400
|
-
.sort((left, right) => left.id.localeCompare(right.id)));
|
|
388
|
+
return Promise.resolve(describePrivacyProviders(this.#privacyProviders));
|
|
401
389
|
}
|
|
402
390
|
capturePrivacy(input) {
|
|
403
391
|
const provider = this.#privacyProvider(input.providerId, input.providerVersion);
|
|
@@ -1209,10 +1197,18 @@ export async function createBotControlServer(input) {
|
|
|
1209
1197
|
return reply.code(401).send({ message: "Control authentication failed" });
|
|
1210
1198
|
});
|
|
1211
1199
|
async function readiness(reply) {
|
|
1212
|
-
|
|
1200
|
+
const deploymentReadiness = input.readiness;
|
|
1201
|
+
const shardHealth = input.shardHealth;
|
|
1202
|
+
if (!deploymentReadiness && !shardHealth)
|
|
1213
1203
|
return { status: "ok" };
|
|
1214
1204
|
try {
|
|
1215
|
-
|
|
1205
|
+
if (deploymentReadiness && (await deploymentReadiness()))
|
|
1206
|
+
return { status: "ok" };
|
|
1207
|
+
if (deploymentReadiness)
|
|
1208
|
+
return reply.code(503).send({ status: "unavailable" });
|
|
1209
|
+
if (!shardHealth)
|
|
1210
|
+
return reply.code(503).send({ status: "unavailable" });
|
|
1211
|
+
const health = await shardHealth();
|
|
1216
1212
|
if (health.status === "healthy")
|
|
1217
1213
|
return { status: "ok" };
|
|
1218
1214
|
}
|
package/dist/modules.js
CHANGED
|
@@ -204,6 +204,19 @@ function validatePrivacyParity(definition) {
|
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
+
const subjectFieldsByCollection = new Map(definition.manifest.privacy.collections.map((collection) => [
|
|
208
|
+
collection.name,
|
|
209
|
+
new Set(collection.subjectFields.map(({ field }) => field)),
|
|
210
|
+
]));
|
|
211
|
+
for (const [collectionName, collection] of Object.entries(definition.records ?? {})) {
|
|
212
|
+
const declaredSubjectFields = subjectFieldsByCollection.get(collectionName);
|
|
213
|
+
for (const field of Object.keys(collection.fields)) {
|
|
214
|
+
if (isDiscordIdentityRecordField(field) &&
|
|
215
|
+
!declaredSubjectFields?.has(field)) {
|
|
216
|
+
throw new Error(`Module record field containing a Discord identity is not declared for privacy access: ${collectionName}.${field}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
207
220
|
const unavailable = () => {
|
|
208
221
|
throw new Error("Privacy provider executor is unavailable during loading");
|
|
209
222
|
};
|
|
@@ -220,6 +233,11 @@ function validatePrivacyParity(definition) {
|
|
|
220
233
|
throw new Error("Module manifest privacy declaration does not match executable provider");
|
|
221
234
|
}
|
|
222
235
|
}
|
|
236
|
+
function isDiscordIdentityRecordField(field) {
|
|
237
|
+
return (/(?:^|_)(?:user_id|member_id|subject_id)$/u.test(field) ||
|
|
238
|
+
field === "created_by" ||
|
|
239
|
+
field === "updated_by");
|
|
240
|
+
}
|
|
223
241
|
function assertSameContributionIds(declared, executable, label) {
|
|
224
242
|
if (new Set(executable).size !== executable.length) {
|
|
225
243
|
throw new Error(`Module has duplicate executable ${label}`);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ModuleDefinition, PrivacyProvider, PrivacyProviderExecutor, ServiceAccess } from "@helyx/sdk";
|
|
2
|
+
export declare function createPrivacyProviderRegistry(modules: readonly ModuleDefinition[], executor: PrivacyProviderExecutor | undefined, services: ServiceAccess): ReadonlyMap<string, PrivacyProvider>;
|
|
3
|
+
export declare function describePrivacyProviders(providers: ReadonlyMap<string, PrivacyProvider>): readonly {
|
|
4
|
+
id: string;
|
|
5
|
+
version: number;
|
|
6
|
+
containsPersonalData: boolean;
|
|
7
|
+
}[];
|
|
8
|
+
//# sourceMappingURL=privacy-provider-registry.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function createPrivacyProviderRegistry(modules, executor, services) {
|
|
2
|
+
if (!executor)
|
|
3
|
+
return new Map();
|
|
4
|
+
return new Map(modules.flatMap((module) => {
|
|
5
|
+
if (!module.privacy)
|
|
6
|
+
return [];
|
|
7
|
+
const provider = module.privacy.create(executor, services);
|
|
8
|
+
return [[provider.id, provider]];
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
export function describePrivacyProviders(providers) {
|
|
12
|
+
return [...providers.values()]
|
|
13
|
+
.map((provider) => ({
|
|
14
|
+
id: provider.id,
|
|
15
|
+
version: provider.version,
|
|
16
|
+
containsPersonalData: provider.describe().containsPersonalData,
|
|
17
|
+
}))
|
|
18
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=privacy-provider-registry.js.map
|
|
@@ -2,7 +2,7 @@ import type { BotEnvironment } from "@helyx/config";
|
|
|
2
2
|
import { type DatabasePool } from "@helyx/database";
|
|
3
3
|
import type { ModuleDefinition, NumericWriteBehindCommittedBatch, ScheduledTaskExecutionResult } from "@helyx/sdk";
|
|
4
4
|
import { buildApplicationCommandDefinitions } from "./interactions.js";
|
|
5
|
-
import { type ShardCloudRequest, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
5
|
+
import { type ShardCloudRequest, type ShardHealthSnapshot, type ShardIdentity, type ShardProcessHealth } from "./shard-contracts.js";
|
|
6
6
|
import { type ShardCoordinator, type ShardGenerationDescriptor, type ShardLeaderLease, type ShardLease } from "./shard-coordinator.js";
|
|
7
7
|
import type { BotHandle } from "./index.js";
|
|
8
8
|
export declare const STEADY_STATE_LEASE_LOSS_BOUND_MS: number;
|
|
@@ -20,6 +20,11 @@ export declare function startShardSupervisor(environment: BotEnvironment, proces
|
|
|
20
20
|
export declare function splitNumericCommittedBatch(batch: NumericWriteBehindCommittedBatch, index: number): NumericWriteBehindCommittedBatch;
|
|
21
21
|
export declare function buildShardWorkerEnvironment(source: NodeJS.ProcessEnv, identity: ShardIdentity, restartCount: number): NodeJS.ProcessEnv;
|
|
22
22
|
export declare function shardRuntimeCompositionHash(modules: readonly ModuleDefinition[], internalGuildId?: string, globalCommandMode?: Parameters<typeof buildApplicationCommandDefinitions>[2]): string;
|
|
23
|
+
export declare function coordinatedFollowerReady(input: {
|
|
24
|
+
health: ShardHealthSnapshot;
|
|
25
|
+
activeLeader: ShardLeaderLease | undefined;
|
|
26
|
+
supervisorId: string;
|
|
27
|
+
}): boolean;
|
|
23
28
|
export declare function boundedRespawnDelay(attempt: number): number;
|
|
24
29
|
export interface SteadyStateOwnershipRenewal {
|
|
25
30
|
generationRenewed: boolean;
|
package/dist/shard-supervisor.js
CHANGED
|
@@ -420,6 +420,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
420
420
|
secret: environment.HELYX_BOT_CONTROL_SECRET,
|
|
421
421
|
logger: { level: environment.LOG_LEVEL },
|
|
422
422
|
shardHealth: buildHealth,
|
|
423
|
+
readiness: buildDeploymentReadiness,
|
|
423
424
|
guildCommands: new GuildCommandReadService(database, environment.DISCORD_CLIENT_ID, () => guildCommandWorker?.getHealth(), environment.DISCORD_TOKEN, environment.HELYX_GUILD_COMMAND_WRITES_ENABLED, environment.HELYX_GUILD_COMMAND_WRITE_GUILD_IDS, environment.HELYX_GUILD_COMMAND_WRITE_SCOPE === "all", commandModules),
|
|
424
425
|
});
|
|
425
426
|
await controlServer.listen({
|
|
@@ -1343,6 +1344,19 @@ export async function startShardSupervisor(environment, processEnvironment, work
|
|
|
1343
1344
|
shards,
|
|
1344
1345
|
});
|
|
1345
1346
|
}
|
|
1347
|
+
async function buildDeploymentReadiness() {
|
|
1348
|
+
const health = await buildHealth();
|
|
1349
|
+
if (health.status === "healthy")
|
|
1350
|
+
return true;
|
|
1351
|
+
if (!coordinator)
|
|
1352
|
+
return false;
|
|
1353
|
+
const activeLeader = await withTimeout(coordinator.getLeaderLease(), LEASE_VALIDATION_TIMEOUT_MS).catch(() => undefined);
|
|
1354
|
+
return coordinatedFollowerReady({
|
|
1355
|
+
health,
|
|
1356
|
+
activeLeader,
|
|
1357
|
+
supervisorId,
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1346
1360
|
async function shutdown(reason = "programmatic") {
|
|
1347
1361
|
if (shuttingDown)
|
|
1348
1362
|
return;
|
|
@@ -1690,6 +1704,16 @@ function boundedOperationalHealth(health) {
|
|
|
1690
1704
|
shards: health.shards.slice(0, 25),
|
|
1691
1705
|
});
|
|
1692
1706
|
}
|
|
1707
|
+
export function coordinatedFollowerReady(input) {
|
|
1708
|
+
return (input.health.scope === "coordinated" &&
|
|
1709
|
+
input.health.status === "degraded" &&
|
|
1710
|
+
input.health.readyShardCount === input.health.configuredShardCount &&
|
|
1711
|
+
input.health.coordination.redis === "healthy" &&
|
|
1712
|
+
input.health.coordination.database === "healthy" &&
|
|
1713
|
+
input.health.scheduler?.status === "unavailable" &&
|
|
1714
|
+
input.activeLeader?.generation === input.health.generation &&
|
|
1715
|
+
input.activeLeader.supervisorId !== input.supervisorId);
|
|
1716
|
+
}
|
|
1693
1717
|
function wait(milliseconds) {
|
|
1694
1718
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1695
1719
|
}
|