@helyx/bot 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +13 -0
  3. package/dist/cloud-connector.d.ts +7 -0
  4. package/dist/cloud-connector.js +118 -1
  5. package/dist/cloud-control-dispatcher.js +84 -0
  6. package/dist/control-server.d.ts +187 -5
  7. package/dist/control-server.js +673 -6
  8. package/dist/developer-operations-module.js +8 -0
  9. package/dist/discord-owned-message-service.d.ts +39 -0
  10. package/dist/discord-owned-message-service.js +138 -0
  11. package/dist/events.d.ts +70 -1
  12. package/dist/events.js +557 -5
  13. package/dist/index.js +61 -13
  14. package/dist/interactions.d.ts +3 -2
  15. package/dist/interactions.js +66 -31
  16. package/dist/module-logging.d.ts +1 -0
  17. package/dist/module-logging.js +19 -1
  18. package/dist/modules.d.ts +9 -0
  19. package/dist/modules.js +45 -0
  20. package/dist/resource-authorization.d.ts +7 -0
  21. package/dist/resource-authorization.js +12 -0
  22. package/dist/scheduled-task-runner.d.ts +130 -0
  23. package/dist/scheduled-task-runner.js +417 -0
  24. package/dist/shard-cloud-proxy.d.ts +4 -0
  25. package/dist/shard-cloud-proxy.js +34 -0
  26. package/dist/shard-contracts.d.ts +574 -23
  27. package/dist/shard-contracts.js +346 -0
  28. package/dist/shard-coordinator.d.ts +5 -3
  29. package/dist/shard-coordinator.js +85 -2
  30. package/dist/shard-router.d.ts +10 -0
  31. package/dist/shard-router.js +58 -0
  32. package/dist/shard-supervisor.d.ts +2 -1
  33. package/dist/shard-supervisor.js +187 -5
  34. package/dist/shard-worker.d.ts +8 -2
  35. package/dist/shard-worker.js +97 -16
  36. package/dist/support-module.d.ts +9 -0
  37. package/dist/support-module.js +172 -0
  38. package/package.json +9 -9
@@ -0,0 +1,130 @@
1
+ import { type CoalescedTaskRecord, type ScheduledTaskRecord, type ScheduledTaskDefinition, type SchedulerHealth } from "@helyx/database";
2
+ import { type Logger, type ModuleDefinition, type ModuleScheduledTaskExecution, type ScheduledTaskExecutionResult, type ServiceAccess } from "@helyx/sdk";
3
+ import type { Pool } from "pg";
4
+ export interface ScheduledTaskRoute {
5
+ (task: ModuleScheduledTaskExecution, signal: AbortSignal): Promise<ScheduledTaskExecutionResult>;
6
+ }
7
+ interface ScheduledTaskRepositoryContract {
8
+ claimDue(input: {
9
+ workerId: string;
10
+ leaseMs: number;
11
+ limit?: number;
12
+ eligible?: readonly ScheduledTaskDefinition[];
13
+ }): Promise<ScheduledTaskRecord[]>;
14
+ claimCoalesced(input: {
15
+ workerId: string;
16
+ leaseMs: number;
17
+ limit?: number;
18
+ eligible?: readonly ScheduledTaskDefinition[];
19
+ }): Promise<CoalescedTaskRecord[]>;
20
+ heartbeat(input: {
21
+ taskId: string;
22
+ workerId: string;
23
+ leaseGeneration: number;
24
+ leaseMs: number;
25
+ }): Promise<boolean>;
26
+ heartbeatCoalesced(input: {
27
+ laneId: string;
28
+ workerId: string;
29
+ leaseGeneration: number;
30
+ claimedGeneration: number;
31
+ leaseMs: number;
32
+ }): Promise<boolean>;
33
+ complete(input: {
34
+ taskId: string;
35
+ workerId: string;
36
+ leaseGeneration: number;
37
+ }): Promise<boolean>;
38
+ completeCoalesced(input: {
39
+ laneId: string;
40
+ workerId: string;
41
+ leaseGeneration: number;
42
+ claimedGeneration: number;
43
+ }): Promise<boolean>;
44
+ defer(input: {
45
+ taskId: string;
46
+ workerId: string;
47
+ leaseGeneration: number;
48
+ delayMs: number;
49
+ safeCode: string;
50
+ }): Promise<boolean>;
51
+ deferCoalesced(input: {
52
+ laneId: string;
53
+ workerId: string;
54
+ leaseGeneration: number;
55
+ claimedGeneration: number;
56
+ delayMs: number;
57
+ safeCode: string;
58
+ }): Promise<boolean>;
59
+ retry(input: {
60
+ taskId: string;
61
+ workerId: string;
62
+ leaseGeneration: number;
63
+ delayMs: number;
64
+ safeCode: string;
65
+ }): Promise<boolean>;
66
+ retryCoalesced(input: {
67
+ laneId: string;
68
+ workerId: string;
69
+ leaseGeneration: number;
70
+ claimedGeneration: number;
71
+ delayMs: number;
72
+ safeCode: string;
73
+ }): Promise<boolean>;
74
+ markRequiresReview(input: {
75
+ taskId: string;
76
+ workerId: string;
77
+ leaseGeneration: number;
78
+ safeCode: string;
79
+ }): Promise<boolean>;
80
+ markCoalescedRequiresReview(input: {
81
+ laneId: string;
82
+ workerId: string;
83
+ leaseGeneration: number;
84
+ claimedGeneration: number;
85
+ safeCode: string;
86
+ }): Promise<boolean>;
87
+ getHealth(sampleLimit?: number): Promise<SchedulerHealth>;
88
+ }
89
+ export declare class ScheduledTaskRunner {
90
+ #private;
91
+ constructor(input: {
92
+ repository: ScheduledTaskRepositoryContract;
93
+ route: ScheduledTaskRoute;
94
+ logger: Logger;
95
+ workerId?: string;
96
+ pollIntervalMs?: number;
97
+ leaseMs?: number;
98
+ claimLimit?: number;
99
+ stopTimeoutMs?: number;
100
+ eligibleTasks?: readonly (ScheduledTaskDefinition & {
101
+ delivery: "one-shot" | "coalesced";
102
+ })[];
103
+ });
104
+ start(): void;
105
+ stop(): Promise<void>;
106
+ runOnce(): Promise<void>;
107
+ getHealth(): Promise<SchedulerHealth>;
108
+ }
109
+ export declare function createScheduledTaskRunner(input: {
110
+ database: Pool;
111
+ modules: readonly ModuleDefinition[];
112
+ route: ScheduledTaskRoute;
113
+ logger: Logger;
114
+ workerId?: string;
115
+ }): ScheduledTaskRunner;
116
+ export declare function schedulerOperationalHealth(health: SchedulerHealth): {
117
+ status: "available";
118
+ oneShot: ReturnType<typeof mapSchedulerWorkHealth>;
119
+ coalesced: ReturnType<typeof mapSchedulerWorkHealth>;
120
+ };
121
+ export declare function createLocalScheduledTaskRoute(input: {
122
+ modules: readonly ModuleDefinition[];
123
+ services: ServiceAccess;
124
+ logger: Logger;
125
+ }): ScheduledTaskRoute;
126
+ declare function mapSchedulerWorkHealth(health: SchedulerHealth["oneShot"]): Omit<SchedulerHealth["oneShot"], "oldestReadyAt"> & {
127
+ oldestReadyAt: string | null;
128
+ };
129
+ export {};
130
+ //# sourceMappingURL=scheduled-task-runner.d.ts.map
@@ -0,0 +1,417 @@
1
+ import { CORE_RECORD_RETENTION_TASK, ScheduledTaskRepository, } from "@helyx/database";
2
+ import { getModuleScheduledTaskContribution, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
3
+ import { randomUUID } from "node:crypto";
4
+ const DEFAULT_POLL_INTERVAL_MS = 1_000;
5
+ const DEFAULT_LEASE_MS = 45_000;
6
+ const DEFAULT_CLAIM_LIMIT = 8;
7
+ const DEFAULT_STOP_TIMEOUT_MS = 5_000;
8
+ const MAX_RESULT_DELAY_MS = 24 * 60 * 60 * 1_000;
9
+ const SAFE_CODE = /^[a-z0-9][a-z0-9._-]{0,99}$/u;
10
+ export class ScheduledTaskRunner {
11
+ #repository;
12
+ #route;
13
+ #logger;
14
+ #workerId;
15
+ #pollIntervalMs;
16
+ #leaseMs;
17
+ #claimLimit;
18
+ #stopTimeoutMs;
19
+ #oneShotEligible;
20
+ #coalescedEligible;
21
+ #catalogueFiltered;
22
+ #timer;
23
+ #active;
24
+ #controller = new AbortController();
25
+ #stopping = false;
26
+ constructor(input) {
27
+ this.#repository = input.repository;
28
+ this.#route = input.route;
29
+ this.#logger = input.logger;
30
+ this.#workerId = input.workerId ?? `scheduled-runner:${randomUUID()}`;
31
+ this.#pollIntervalMs = boundedInteger(input.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, 100, 60_000, "scheduler poll interval");
32
+ this.#leaseMs = boundedInteger(input.leaseMs ?? DEFAULT_LEASE_MS, 5_000, 5 * 60_000, "scheduler lease");
33
+ this.#claimLimit = boundedInteger(input.claimLimit ?? DEFAULT_CLAIM_LIMIT, 1, 25, "scheduler claim limit");
34
+ this.#stopTimeoutMs = boundedInteger(input.stopTimeoutMs ?? DEFAULT_STOP_TIMEOUT_MS, 100, 30_000, "scheduler stop timeout");
35
+ this.#catalogueFiltered = input.eligibleTasks !== undefined;
36
+ this.#oneShotEligible = input.eligibleTasks
37
+ ?.filter(({ delivery }) => delivery === "one-shot")
38
+ .map(withoutDelivery);
39
+ this.#coalescedEligible = input.eligibleTasks
40
+ ?.filter(({ delivery }) => delivery === "coalesced")
41
+ .map(withoutDelivery);
42
+ }
43
+ start() {
44
+ if (this.#timer || this.#stopping)
45
+ throw new Error("Scheduled task runner is already started or stopped");
46
+ this.#runSoon();
47
+ this.#timer = setInterval(() => this.#runSoon(), this.#pollIntervalMs);
48
+ this.#timer.unref();
49
+ }
50
+ async stop() {
51
+ if (!this.#stopping) {
52
+ this.#stopping = true;
53
+ if (this.#timer)
54
+ clearInterval(this.#timer);
55
+ this.#timer = undefined;
56
+ this.#controller.abort();
57
+ }
58
+ const active = this.#active;
59
+ if (!active)
60
+ return;
61
+ await Promise.race([active, wait(this.#stopTimeoutMs)]);
62
+ }
63
+ async runOnce() {
64
+ if (this.#stopping)
65
+ return;
66
+ if (this.#catalogueFiltered &&
67
+ this.#oneShotEligible?.length === 0 &&
68
+ this.#coalescedEligible?.length === 0)
69
+ return;
70
+ const [oneShot, coalesced] = await Promise.all([
71
+ this.#catalogueFiltered && this.#oneShotEligible?.length === 0
72
+ ? Promise.resolve([])
73
+ : this.#repository.claimDue({
74
+ workerId: this.#workerId,
75
+ leaseMs: this.#leaseMs,
76
+ limit: this.#claimLimit,
77
+ ...(this.#oneShotEligible === undefined
78
+ ? {}
79
+ : { eligible: this.#oneShotEligible }),
80
+ }),
81
+ this.#catalogueFiltered && this.#coalescedEligible?.length === 0
82
+ ? Promise.resolve([])
83
+ : this.#repository.claimCoalesced({
84
+ workerId: this.#workerId,
85
+ leaseMs: this.#leaseMs,
86
+ limit: this.#claimLimit,
87
+ ...(this.#coalescedEligible === undefined
88
+ ? {}
89
+ : { eligible: this.#coalescedEligible }),
90
+ }),
91
+ ]);
92
+ const tasks = [
93
+ ...oneShot.map((record) => ({
94
+ delivery: "one-shot",
95
+ record,
96
+ })),
97
+ ...coalesced.map((record) => ({
98
+ delivery: "coalesced",
99
+ record,
100
+ })),
101
+ ];
102
+ await Promise.all(tasks.map((task) => this.#execute(task).catch((error) => {
103
+ this.#logger.error({
104
+ event: "scheduler.settlement_failed",
105
+ delivery: task.delivery,
106
+ taskId: task.record.id,
107
+ moduleId: task.record.moduleId,
108
+ errorName: error instanceof Error ? error.name : "Error",
109
+ }, "Scheduled task could not be settled");
110
+ })));
111
+ }
112
+ getHealth() {
113
+ return this.#repository.getHealth();
114
+ }
115
+ #runSoon() {
116
+ if (this.#active || this.#stopping)
117
+ return;
118
+ this.#active = this.runOnce()
119
+ .catch((error) => {
120
+ this.#logger.error({
121
+ event: "scheduler.claim_failed",
122
+ errorName: error instanceof Error ? error.name : "Error",
123
+ }, "Scheduled task claim failed");
124
+ })
125
+ .finally(() => {
126
+ this.#active = undefined;
127
+ });
128
+ }
129
+ async #execute(task) {
130
+ const runnerSignal = this.#controller.signal;
131
+ if (runnerSignal.aborted)
132
+ return;
133
+ const controller = new AbortController();
134
+ let heartbeat;
135
+ let heartbeatRunning = false;
136
+ const abort = () => {
137
+ if (heartbeat)
138
+ clearInterval(heartbeat);
139
+ heartbeat = undefined;
140
+ controller.abort();
141
+ };
142
+ runnerSignal.addEventListener("abort", abort, { once: true });
143
+ const renewLease = () => {
144
+ if (heartbeatRunning || controller.signal.aborted)
145
+ return;
146
+ heartbeatRunning = true;
147
+ void this.#heartbeat(task)
148
+ .then((alive) => {
149
+ if (!alive)
150
+ abort();
151
+ })
152
+ .catch((error) => {
153
+ this.#logger.error({
154
+ event: "scheduler.heartbeat_failed",
155
+ delivery: task.delivery,
156
+ taskId: task.record.id,
157
+ moduleId: task.record.moduleId,
158
+ errorName: error instanceof Error ? error.name : "Error",
159
+ }, "Scheduled task lease heartbeat failed");
160
+ abort();
161
+ })
162
+ .finally(() => {
163
+ heartbeatRunning = false;
164
+ });
165
+ };
166
+ heartbeat = setInterval(renewLease, Math.max(1_000, Math.floor(this.#leaseMs / 3)));
167
+ heartbeat.unref();
168
+ let result;
169
+ try {
170
+ result = validateExecutionResult(await this.#route(toExecution(task), controller.signal));
171
+ }
172
+ catch {
173
+ result = controller.signal.aborted
174
+ ? deferred("runner_stopped", 1_000)
175
+ : {
176
+ outcome: "retry",
177
+ delayMs: retryDelay(task.record.attemptCount),
178
+ safeCode: "handler_failed",
179
+ };
180
+ }
181
+ finally {
182
+ if (heartbeat)
183
+ clearInterval(heartbeat);
184
+ runnerSignal.removeEventListener("abort", abort);
185
+ }
186
+ if (runnerSignal.aborted || controller.signal.aborted)
187
+ return;
188
+ await this.#settle(task, result);
189
+ }
190
+ #heartbeat(task) {
191
+ if (task.delivery === "one-shot") {
192
+ return this.#repository.heartbeat({
193
+ taskId: task.record.id,
194
+ workerId: this.#workerId,
195
+ leaseGeneration: task.record.leaseGeneration,
196
+ leaseMs: this.#leaseMs,
197
+ });
198
+ }
199
+ return this.#repository.heartbeatCoalesced({
200
+ laneId: task.record.id,
201
+ workerId: this.#workerId,
202
+ leaseGeneration: task.record.leaseGeneration,
203
+ claimedGeneration: task.record.claimedGeneration,
204
+ leaseMs: this.#leaseMs,
205
+ });
206
+ }
207
+ #settle(task, result) {
208
+ const common = {
209
+ workerId: this.#workerId,
210
+ leaseGeneration: task.record.leaseGeneration,
211
+ };
212
+ if (task.delivery === "one-shot") {
213
+ const target = { ...common, taskId: task.record.id };
214
+ switch (result.outcome) {
215
+ case "completed":
216
+ return this.#repository.complete(target);
217
+ case "deferred":
218
+ return this.#repository.defer({
219
+ ...target,
220
+ delayMs: result.delayMs,
221
+ safeCode: result.safeCode,
222
+ });
223
+ case "retry":
224
+ return this.#repository.retry({
225
+ ...target,
226
+ delayMs: result.delayMs,
227
+ safeCode: result.safeCode,
228
+ });
229
+ case "requires_review":
230
+ return this.#repository.markRequiresReview({
231
+ ...target,
232
+ safeCode: result.safeCode,
233
+ });
234
+ }
235
+ }
236
+ const target = {
237
+ ...common,
238
+ laneId: task.record.id,
239
+ claimedGeneration: task.record.claimedGeneration,
240
+ };
241
+ switch (result.outcome) {
242
+ case "completed":
243
+ return this.#repository.completeCoalesced(target);
244
+ case "deferred":
245
+ return this.#repository.deferCoalesced({
246
+ ...target,
247
+ delayMs: result.delayMs,
248
+ safeCode: result.safeCode,
249
+ });
250
+ case "retry":
251
+ return this.#repository.retryCoalesced({
252
+ ...target,
253
+ delayMs: result.delayMs,
254
+ safeCode: result.safeCode,
255
+ });
256
+ case "requires_review":
257
+ return this.#repository.markCoalescedRequiresReview({
258
+ ...target,
259
+ safeCode: result.safeCode,
260
+ });
261
+ }
262
+ }
263
+ }
264
+ export function createScheduledTaskRunner(input) {
265
+ return new ScheduledTaskRunner({
266
+ repository: new ScheduledTaskRepository(input.database),
267
+ route: input.route,
268
+ logger: input.logger,
269
+ eligibleTasks: [
270
+ {
271
+ ...CORE_RECORD_RETENTION_TASK,
272
+ delivery: "one-shot",
273
+ runWhenDisabled: true,
274
+ },
275
+ ...input.modules.flatMap((module) => (module.scheduledTasks ?? []).map((contribution) => ({
276
+ moduleId: module.manifest.id,
277
+ taskKind: contribution.kind,
278
+ taskVersion: contribution.version,
279
+ delivery: contribution.delivery,
280
+ }))),
281
+ ],
282
+ ...(input.workerId === undefined ? {} : { workerId: input.workerId }),
283
+ });
284
+ }
285
+ export function schedulerOperationalHealth(health) {
286
+ return {
287
+ status: "available",
288
+ oneShot: mapSchedulerWorkHealth(health.oneShot),
289
+ coalesced: mapSchedulerWorkHealth(health.coalesced),
290
+ };
291
+ }
292
+ export function createLocalScheduledTaskRoute(input) {
293
+ const modules = new Map(input.modules.map((module) => [module.manifest.id, module]));
294
+ return async (task, signal) => {
295
+ if (task.moduleId === CORE_RECORD_RETENTION_TASK.moduleId &&
296
+ task.taskKind === CORE_RECORD_RETENTION_TASK.taskKind &&
297
+ task.taskVersion === CORE_RECORD_RETENTION_TASK.taskVersion &&
298
+ task.delivery === "one-shot") {
299
+ const retentionTaskId = task.payload.retentionTaskId;
300
+ if (Object.keys(task.payload).length !== 1 ||
301
+ typeof retentionTaskId !== "string" ||
302
+ !/^[0-9a-f]{8}-[0-9a-f-]{27}$/iu.test(retentionTaskId))
303
+ return { outcome: "requires_review", safeCode: "payload_invalid" };
304
+ const result = await input.services
305
+ .get(HELYX_SERVICE_NAMES.recordRetention)
306
+ .execute(retentionTaskId);
307
+ return result.completed
308
+ ? { outcome: "completed" }
309
+ : {
310
+ outcome: "deferred",
311
+ delayMs: 100,
312
+ safeCode: "retention_batch_remaining",
313
+ };
314
+ }
315
+ const module = modules.get(task.moduleId);
316
+ if (!module)
317
+ return deferred("module_unavailable", DEFAULT_POLL_INTERVAL_MS * 30);
318
+ if (task.guildId) {
319
+ const enabled = await input.services
320
+ .get(HELYX_SERVICE_NAMES.installations)
321
+ .isModuleEnabled(task.guildId, task.moduleId);
322
+ if (!enabled)
323
+ return deferred("module_disabled", DEFAULT_POLL_INTERVAL_MS * 30);
324
+ }
325
+ const contribution = getModuleScheduledTaskContribution(module, task.taskKind);
326
+ if (!contribution ||
327
+ contribution.delivery !== task.delivery ||
328
+ contribution.version !== task.taskVersion) {
329
+ return { outcome: "requires_review", safeCode: "handler_mismatch" };
330
+ }
331
+ let payload;
332
+ try {
333
+ payload = contribution.validatePayload(task.payload);
334
+ }
335
+ catch {
336
+ return { outcome: "requires_review", safeCode: "payload_invalid" };
337
+ }
338
+ if (!samePayload(payload, task.payload))
339
+ return { outcome: "requires_review", safeCode: "payload_mismatch" };
340
+ return contribution.execute({
341
+ logger: input.logger.child({
342
+ moduleId: task.moduleId,
343
+ taskKind: task.taskKind,
344
+ taskId: task.taskId,
345
+ }),
346
+ services: input.services,
347
+ signal,
348
+ }, task);
349
+ };
350
+ }
351
+ function toExecution(task) {
352
+ const common = {
353
+ taskId: task.record.id,
354
+ moduleId: task.record.moduleId,
355
+ taskKind: task.record.taskKind,
356
+ taskVersion: task.record.taskVersion,
357
+ guildId: task.record.guildId,
358
+ payload: task.record.payload,
359
+ };
360
+ return task.delivery === "one-shot"
361
+ ? { delivery: "one-shot", ...common }
362
+ : {
363
+ delivery: "coalesced",
364
+ ...common,
365
+ claimedGeneration: task.record.claimedGeneration,
366
+ };
367
+ }
368
+ function validateExecutionResult(result) {
369
+ if (result.outcome === "completed")
370
+ return result;
371
+ if (!SAFE_CODE.test(result.safeCode))
372
+ throw new Error("Scheduled task returned an invalid safe code");
373
+ if (result.outcome === "requires_review")
374
+ return result;
375
+ boundedInteger(result.delayMs, 100, MAX_RESULT_DELAY_MS, "task delay");
376
+ return result;
377
+ }
378
+ function deferred(safeCode, delayMs) {
379
+ return { outcome: "deferred", delayMs, safeCode };
380
+ }
381
+ function retryDelay(attemptCount) {
382
+ return Math.min(5 * 60_000, 1_000 * 2 ** Math.min(attemptCount, 8));
383
+ }
384
+ function withoutDelivery(input) {
385
+ return {
386
+ moduleId: input.moduleId,
387
+ taskKind: input.taskKind,
388
+ taskVersion: input.taskVersion,
389
+ ...(input.runWhenDisabled === undefined
390
+ ? {}
391
+ : { runWhenDisabled: input.runWhenDisabled }),
392
+ };
393
+ }
394
+ function boundedInteger(value, minimum, maximum, label) {
395
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum)
396
+ throw new Error(`${label} must be between ${minimum} and ${maximum}`);
397
+ return value;
398
+ }
399
+ function samePayload(left, right) {
400
+ const leftEntries = Object.entries(left);
401
+ const rightKeys = Object.keys(right);
402
+ return (leftEntries.length === rightKeys.length &&
403
+ leftEntries.every(([key, value]) => right[key] === value));
404
+ }
405
+ function wait(milliseconds) {
406
+ return new Promise((resolve) => {
407
+ const timer = setTimeout(resolve, milliseconds);
408
+ timer.unref();
409
+ });
410
+ }
411
+ function mapSchedulerWorkHealth(health) {
412
+ return {
413
+ ...health,
414
+ oldestReadyAt: health.oldestReadyAt?.toISOString() ?? null,
415
+ };
416
+ }
417
+ //# sourceMappingURL=scheduled-task-runner.js.map
@@ -1,4 +1,5 @@
1
1
  import type { CloudConnectorConsentRecord } from "@helyx/database";
2
+ import type { SupportBugCreatePayload, SupportBugCreateResponsePayload } from "@helyx/control-protocol";
2
3
  import type { CloudConnector, CloudConnectorStatus } from "./cloud-connector.js";
3
4
  import { type ShardCloudResponse } from "./shard-contracts.js";
4
5
  export declare class WorkerCloudConnectorProxy implements CloudConnector {
@@ -15,6 +16,9 @@ export declare class WorkerCloudConnectorProxy implements CloudConnector {
15
16
  enrol(token: string, source: "startup" | "discord" | "file"): Promise<CloudConnectorStatus>;
16
17
  disconnect(): Promise<void>;
17
18
  rotate(): Promise<CloudConnectorStatus>;
19
+ submitBug(input: SupportBugCreatePayload): Promise<Extract<SupportBugCreateResponsePayload, {
20
+ ok: true;
21
+ }>["result"]>;
18
22
  stop(): Promise<void>;
19
23
  accept(response: ShardCloudResponse): void;
20
24
  }
@@ -28,6 +28,19 @@ export class WorkerCloudConnectorProxy {
28
28
  this.#status = parseStatus(await this.#request({ kind: "cloud.rotate" }));
29
29
  return this.#status;
30
30
  }
31
+ async submitBug(input) {
32
+ return parseSupportResult(await this.#request({
33
+ kind: "support.bug.create",
34
+ reporterDiscordUserId: input.reporterDiscordUserId,
35
+ guildId: input.guildId,
36
+ moduleOrArea: input.moduleOrArea,
37
+ summary: input.summary,
38
+ whatHappened: input.whatHappened,
39
+ ...(input.extraDetail ? { extraDetail: input.extraDetail } : {}),
40
+ interactionId: input.interactionId,
41
+ correlationId: input.correlationId,
42
+ }));
43
+ }
31
44
  stop() {
32
45
  for (const pending of this.#pending.values()) {
33
46
  clearTimeout(pending.timer);
@@ -91,4 +104,25 @@ function parseConsent(value) {
91
104
  const consent = value;
92
105
  return { ...consent, updatedAt: new Date(consent.updatedAt) };
93
106
  }
107
+ function parseSupportResult(value) {
108
+ const claimUrl = typeof value === "object" && value !== null && "claimUrl" in value
109
+ ? value.claimUrl
110
+ : undefined;
111
+ if (typeof value !== "object" ||
112
+ value === null ||
113
+ !("reference" in value) ||
114
+ typeof value.reference !== "string" ||
115
+ !("created" in value) ||
116
+ typeof value.created !== "boolean" ||
117
+ (claimUrl !== undefined &&
118
+ (typeof claimUrl !== "string" ||
119
+ claimUrl.length > 2_048 ||
120
+ !claimUrl.startsWith("https://"))))
121
+ throw new Error("Cloud supervisor returned an invalid support result");
122
+ return {
123
+ reference: value.reference,
124
+ created: value.created,
125
+ ...(typeof claimUrl === "string" ? { claimUrl } : {}),
126
+ };
127
+ }
94
128
  //# sourceMappingURL=shard-cloud-proxy.js.map