@helyx/bot 0.4.32 → 0.4.33

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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.33
4
+
5
+ ### Patch Changes
6
+
7
+ - Add versioned OAuth consumer cleanup contracts, durable and resumable
8
+ revocation state, least-privilege provider scopes, and Creator Connections
9
+ cleanup dispatch. Promote the hosted-only Creator Connections module to live
10
+ availability with clear OAuth activation validation. Ignore shard responses
11
+ that finish after shutdown has already detached their worker.
12
+
3
13
  ## 0.4.32
4
14
 
5
15
  ### Patch Changes
@@ -1,5 +1,5 @@
1
1
  import { type DatabasePool, type GuildShardOwnership } from "@helyx/database";
2
- import { type DashboardActionResult, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
2
+ import { type DashboardActionResult, type GuildAccessPolicy, type ManagedResourceCreationIntent, type ManagedResourceDetail, type ManagedResourcePage, type ManagedResourceRelationPage, type ManagedResourceValue, type MessageResponse, type ModuleDefinition, type OAuthConsumerCleanupResult, type PermissionActor, type PrivacyCapturePage, type PrivacyErasePage, type PrivacyProviderExecutor, type PublicAchievementProjectionPage, type ServiceAccess } from "@helyx/sdk";
3
3
  import { type ActionReceipt, type SignedAssessmentEnvelope } from "@helyx/verification-contracts";
4
4
  import { type Client } from "discord.js";
5
5
  import { type FastifyInstance } from "fastify";
@@ -143,6 +143,17 @@ export interface DashboardGuildAccessState extends GuildAccessPolicy {
143
143
  updatedAt: string | null;
144
144
  }
145
145
  export interface BotControlHandler {
146
+ cleanupOAuthConsumer(input: {
147
+ consumerBindingId: string;
148
+ moduleId: string;
149
+ provider: "youtube" | "twitch";
150
+ guildId: string;
151
+ resourceType: string;
152
+ resourceId: string;
153
+ subjectUserId: string;
154
+ contractVersion: number;
155
+ correlationId: string;
156
+ }): Promise<OAuthConsumerCleanupResult>;
146
157
  sendSupportNotification(input: SupportNotificationInput): Promise<void>;
147
158
  listGuildPresence(input: {
148
159
  guildIds: readonly string[];
@@ -339,6 +350,17 @@ export declare class BotControlService implements BotControlHandler {
339
350
  guildId: string;
340
351
  channelId: string;
341
352
  } | undefined, presenceOwnership?: GuildShardOwnership | undefined, presenceShardCount?: number | undefined);
353
+ cleanupOAuthConsumer(input: {
354
+ consumerBindingId: string;
355
+ moduleId: string;
356
+ provider: "youtube" | "twitch";
357
+ guildId: string;
358
+ resourceType: string;
359
+ resourceId: string;
360
+ subjectUserId: string;
361
+ contractVersion: number;
362
+ correlationId: string;
363
+ }): Promise<OAuthConsumerCleanupResult>;
342
364
  listGuildPresence(input: {
343
365
  guildIds: readonly string[];
344
366
  correlationId: string;
@@ -286,6 +286,26 @@ export class BotControlService {
286
286
  })
287
287
  : []);
288
288
  }
289
+ async cleanupOAuthConsumer(input) {
290
+ const module = this.#modules.get(input.moduleId);
291
+ if (!module)
292
+ throw new BotControlNotFoundError("OAuth consumer module is unavailable");
293
+ const consumer = module.oauthConsumers?.find((candidate) => candidate.provider === input.provider &&
294
+ candidate.resourceType === input.resourceType &&
295
+ candidate.version === input.contractVersion);
296
+ if (!consumer)
297
+ throw new BotControlNotFoundError("OAuth consumer cleanup contract is unavailable");
298
+ return consumer.cleanup({
299
+ consumerBindingId: input.consumerBindingId,
300
+ provider: input.provider,
301
+ guildId: input.guildId,
302
+ resourceType: input.resourceType,
303
+ resourceId: input.resourceId,
304
+ subjectUserId: input.subjectUserId,
305
+ correlationId: input.correlationId,
306
+ services: this.#services,
307
+ });
308
+ }
289
309
  async listGuildPresence(input) {
290
310
  const guildIds = [...new Set(input.guildIds)];
291
311
  if (guildIds.length > 200)
@@ -1213,6 +1233,23 @@ export async function createBotControlServer(input) {
1213
1233
  server.get("/health", (_request, reply) => readiness(reply));
1214
1234
  server.get("/v1/health", (_request, reply) => readiness(reply));
1215
1235
  server.post("/v1/verification/results", async (request) => input.service.acceptVerificationResult(signedAssessmentEnvelopeSchema.parse(request.body)));
1236
+ server.post("/v1/oauth/consumers/cleanup", async (request) => {
1237
+ const body = z
1238
+ .object({
1239
+ consumerBindingId: z.uuid(),
1240
+ moduleId,
1241
+ provider: z.enum(["youtube", "twitch"]),
1242
+ guildId: snowflake,
1243
+ resourceType: z.string().regex(/^[a-z][a-z0-9_-]*$/u),
1244
+ resourceId,
1245
+ subjectUserId: snowflake,
1246
+ contractVersion: z.number().int().min(1).max(32_767),
1247
+ correlationId: z.string().trim().min(1).max(200),
1248
+ })
1249
+ .strict()
1250
+ .parse(request.body);
1251
+ return input.service.cleanupOAuthConsumer(body);
1252
+ });
1216
1253
  server.get("/v1/shards/health", async (request, reply) => {
1217
1254
  reply.header("Cache-Control", "no-store");
1218
1255
  if (!input.shardHealth)
@@ -155,9 +155,13 @@ export function createOAuthModule() {
155
155
  componentsV2: {
156
156
  text: [
157
157
  {
158
- content: result.providerRevocation === "completed"
159
- ? `${providerName(result.provider)} access has been removed from Helyx. Local credentials were deleted.`
160
- : `${providerName(result.provider)} access has been removed from Helyx and local credentials were deleted. Helyx could not confirm provider-side revocation, so you may also remove Helyx from your ${providerName(result.provider)} connected-app settings.`,
158
+ content: result.outcome === "processing"
159
+ ? `${providerName(result.provider)} access removal is already in progress. Helyx will finish it automatically.`
160
+ : result.outcome === "cleanup_pending"
161
+ ? `${providerName(result.provider)} credentials were deleted. Helyx is still removing related roles and connection records.`
162
+ : result.providerRevocation === "completed"
163
+ ? `${providerName(result.provider)} access has been removed from Helyx. Local credentials were deleted.`
164
+ : `${providerName(result.provider)} access has been removed from Helyx and local credentials were deleted. Helyx could not confirm provider-side revocation, so you may also remove Helyx from your ${providerName(result.provider)} connected-app settings.`,
161
165
  },
162
166
  ],
163
167
  },
@@ -30,7 +30,7 @@ export class HttpOAuthService {
30
30
  "content-type": "application/json",
31
31
  },
32
32
  body: JSON.stringify(body),
33
- signal: AbortSignal.timeout(6_000),
33
+ signal: AbortSignal.timeout(15_000),
34
34
  });
35
35
  if (!response.ok)
36
36
  throw new Error(`oauth_service_http_${response.status}`);
@@ -359,6 +359,20 @@ export declare const shardControlOperationSchema: z.ZodDiscriminatedUnion<[z.Zod
359
359
  expectedRevision: z.ZodNumber;
360
360
  value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
361
361
  actorAuthenticatedAt: z.ZodOptional<z.ZodISODateTime>;
362
+ }, z.core.$strict>, z.ZodObject<{
363
+ kind: z.ZodLiteral<"oauth.consumer.cleanup">;
364
+ consumerBindingId: z.ZodUUID;
365
+ moduleId: z.ZodString;
366
+ provider: z.ZodEnum<{
367
+ youtube: "youtube";
368
+ twitch: "twitch";
369
+ }>;
370
+ guildId: z.ZodString;
371
+ resourceType: z.ZodString;
372
+ resourceId: z.ZodString;
373
+ subjectUserId: z.ZodString;
374
+ contractVersion: z.ZodNumber;
375
+ correlationId: z.ZodString;
362
376
  }, z.core.$strict>, z.ZodObject<{
363
377
  kind: z.ZodLiteral<"support.notification.send">;
364
378
  guildId: z.ZodString;
@@ -698,6 +712,20 @@ export declare const shardControlRequestSchema: z.ZodObject<{
698
712
  expectedRevision: z.ZodNumber;
699
713
  value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
700
714
  actorAuthenticatedAt: z.ZodOptional<z.ZodISODateTime>;
715
+ }, z.core.$strict>, z.ZodObject<{
716
+ kind: z.ZodLiteral<"oauth.consumer.cleanup">;
717
+ consumerBindingId: z.ZodUUID;
718
+ moduleId: z.ZodString;
719
+ provider: z.ZodEnum<{
720
+ youtube: "youtube";
721
+ twitch: "twitch";
722
+ }>;
723
+ guildId: z.ZodString;
724
+ resourceType: z.ZodString;
725
+ resourceId: z.ZodString;
726
+ subjectUserId: z.ZodString;
727
+ contractVersion: z.ZodNumber;
728
+ correlationId: z.ZodString;
701
729
  }, z.core.$strict>, z.ZodObject<{
702
730
  kind: z.ZodLiteral<"support.notification.send">;
703
731
  guildId: z.ZodString;
@@ -1542,6 +1570,20 @@ export declare const supervisorMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
1542
1570
  expectedRevision: z.ZodNumber;
1543
1571
  value: z.ZodRecord<z.ZodString, z.ZodCustom<ManagedResourceValue, ManagedResourceValue>>;
1544
1572
  actorAuthenticatedAt: z.ZodOptional<z.ZodISODateTime>;
1573
+ }, z.core.$strict>, z.ZodObject<{
1574
+ kind: z.ZodLiteral<"oauth.consumer.cleanup">;
1575
+ consumerBindingId: z.ZodUUID;
1576
+ moduleId: z.ZodString;
1577
+ provider: z.ZodEnum<{
1578
+ youtube: "youtube";
1579
+ twitch: "twitch";
1580
+ }>;
1581
+ guildId: z.ZodString;
1582
+ resourceType: z.ZodString;
1583
+ resourceId: z.ZodString;
1584
+ subjectUserId: z.ZodString;
1585
+ contractVersion: z.ZodNumber;
1586
+ correlationId: z.ZodString;
1545
1587
  }, z.core.$strict>, z.ZodObject<{
1546
1588
  kind: z.ZodLiteral<"support.notification.send">;
1547
1589
  guildId: z.ZodString;
@@ -425,6 +425,20 @@ export const shardControlOperationSchema = z.discriminatedUnion("kind", [
425
425
  ...mutationContext.shape,
426
426
  })
427
427
  .strict(),
428
+ z
429
+ .object({
430
+ kind: z.literal("oauth.consumer.cleanup"),
431
+ consumerBindingId: z.uuid(),
432
+ moduleId,
433
+ provider: z.enum(["youtube", "twitch"]),
434
+ guildId: snowflake,
435
+ resourceType,
436
+ resourceId,
437
+ subjectUserId: snowflake,
438
+ contractVersion: z.number().int().min(1).max(32_767),
439
+ correlationId: z.string().trim().min(1).max(200),
440
+ })
441
+ .strict(),
428
442
  z
429
443
  .object({
430
444
  kind: z.literal("support.notification.send"),
@@ -1111,6 +1125,18 @@ export async function executeShardControlOperation(handler, operation) {
1111
1125
  ? { actorAuthenticatedAt: operation.actorAuthenticatedAt }
1112
1126
  : {}),
1113
1127
  });
1128
+ case "oauth.consumer.cleanup":
1129
+ return handler.cleanupOAuthConsumer({
1130
+ consumerBindingId: operation.consumerBindingId,
1131
+ moduleId: operation.moduleId,
1132
+ provider: operation.provider,
1133
+ guildId: operation.guildId,
1134
+ resourceType: operation.resourceType,
1135
+ resourceId: operation.resourceId,
1136
+ subjectUserId: operation.subjectUserId,
1137
+ contractVersion: operation.contractVersion,
1138
+ correlationId: operation.correlationId,
1139
+ });
1114
1140
  case "support.notification.send": {
1115
1141
  return handler.sendSupportNotification({
1116
1142
  notificationId: operation.notificationId,
@@ -15,6 +15,7 @@ export declare class ShardControlRouter implements BotControlHandler {
15
15
  coordinator?: ShardCoordinator;
16
16
  notificationGuildId?: string;
17
17
  });
18
+ cleanupOAuthConsumer(input: Parameters<BotControlHandler["cleanupOAuthConsumer"]>[0]): ReturnType<BotControlHandler["cleanupOAuthConsumer"]>;
18
19
  getGuildState(guildId: string): ReturnType<BotControlHandler["getGuildState"]>;
19
20
  canAccessDashboard(input: Parameters<BotControlHandler["canAccessDashboard"]>[0]): ReturnType<BotControlHandler["canAccessDashboard"]>;
20
21
  getGuildAccessPolicy(input: Parameters<BotControlHandler["getGuildAccessPolicy"]>[0]): ReturnType<BotControlHandler["getGuildAccessPolicy"]>;
@@ -12,6 +12,9 @@ export class ShardControlRouter {
12
12
  constructor(input) {
13
13
  this.input = input;
14
14
  }
15
+ cleanupOAuthConsumer(input) {
16
+ return this.#route({ kind: "oauth.consumer.cleanup", ...input });
17
+ }
15
18
  getGuildState(guildId) {
16
19
  return this.#route({ kind: "guild.state.get", guildId });
17
20
  }
@@ -651,7 +651,22 @@ export async function startShardSupervisor(environment, processEnvironment, work
651
651
  }
652
652
  }
653
653
  assertShardMessageSize(response);
654
- await workers.get(shardId)?.send(response);
654
+ await sendWorkerResponse(shardId, response);
655
+ }
656
+ async function sendWorkerResponse(shardId, response) {
657
+ if (shuttingDown)
658
+ return;
659
+ const worker = workers.get(shardId);
660
+ if (!worker?.process)
661
+ return;
662
+ try {
663
+ await worker.send(response);
664
+ }
665
+ catch (error) {
666
+ if (shuttingDown || !worker.process)
667
+ return;
668
+ throw error;
669
+ }
655
670
  }
656
671
  async function handleWorkerMessage(shardId, raw) {
657
672
  if (serializedBytes(raw) > SHARD_CONTROL_MAX_BYTES)
@@ -709,7 +724,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
709
724
  const response = leaderLease
710
725
  ? await executeLeaderCloud(message.data)
711
726
  : await routeCloudToLeader(message.data);
712
- await workers.get(shardId)?.send(response);
727
+ await sendWorkerResponse(shardId, response);
713
728
  return;
714
729
  }
715
730
  if (message.data.type === "helyx.operations.health.request") {
@@ -720,7 +735,7 @@ export async function startShardSupervisor(environment, processEnvironment, work
720
735
  health: boundedOperationalHealth(await buildHealth()),
721
736
  };
722
737
  assertShardMessageSize(response);
723
- await workers.get(shardId)?.send(response);
738
+ await sendWorkerResponse(shardId, response);
724
739
  }
725
740
  }
726
741
  function dispatchLocal(shardId, request) {
@@ -1392,9 +1407,14 @@ export function buildShardWorkerEnvironment(source, identity, restartCount) {
1392
1407
  "HELYX_SUPPORT_STAFF_CHANNEL_ID",
1393
1408
  "HELYX_MODULE_PACKAGES",
1394
1409
  "HELYX_MESSAGE_CONTENT_ENABLED",
1410
+ "HELYX_GUILD_MEMBERS_ENABLED",
1411
+ "HELYX_GUILD_RECONCILIATION_INTERVAL_SECONDS",
1395
1412
  "HELYX_MODULE_CATALOGUE_PATH",
1396
1413
  "HELYX_CLOUD_CONNECT_ENABLED",
1414
+ "HELYX_CLOUD_API_URL",
1397
1415
  "HELYX_OWNER_USER_IDS",
1416
+ "HELYX_OAUTH_ENABLED",
1417
+ "HELYX_OAUTH_INTERNAL_SECRET",
1398
1418
  "HELYX_VERIFICATION_ENABLED",
1399
1419
  "HELYX_VERIFICATION_INSTALLATION_ID",
1400
1420
  "HELYX_VERIFICATION_URL",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helyx/bot",
3
- "version": "0.4.32",
3
+ "version": "0.4.33",
4
4
  "description": "Production Discord runtime for the modular Helyx platform.",
5
5
  "keywords": [
6
6
  "helyx",