@helyx/module-account-gate 1.0.15 → 1.1.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 (69) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +55 -90
  3. package/dist/challenge-interactions.d.ts +12 -0
  4. package/dist/challenge-interactions.js +238 -0
  5. package/dist/commands.js +3 -2
  6. package/dist/contracts.d.ts +1 -1
  7. package/dist/contracts.js +1 -1
  8. package/dist/expiry-workflows.d.ts +32 -0
  9. package/dist/expiry-workflows.js +281 -0
  10. package/dist/gateway-interactions.d.ts +5 -0
  11. package/dist/gateway-interactions.js +124 -0
  12. package/dist/health.d.ts +10 -0
  13. package/dist/health.js +55 -0
  14. package/dist/index.d.ts +8 -19
  15. package/dist/index.js +35 -3157
  16. package/dist/member-lifecycle.d.ts +21 -0
  17. package/dist/member-lifecycle.js +305 -0
  18. package/dist/member-resource.js +14 -74
  19. package/dist/member-restart.d.ts +4 -0
  20. package/dist/member-restart.js +76 -0
  21. package/dist/member-revocation.d.ts +21 -0
  22. package/dist/member-revocation.js +127 -0
  23. package/dist/member-start.d.ts +5 -0
  24. package/dist/member-start.js +199 -0
  25. package/dist/member-summary.d.ts +47 -0
  26. package/dist/member-summary.js +200 -0
  27. package/dist/panel-maintenance.d.ts +26 -0
  28. package/dist/panel-maintenance.js +143 -0
  29. package/dist/panel-model.d.ts +47 -0
  30. package/dist/panel-model.js +128 -0
  31. package/dist/panel-mutations.d.ts +24 -0
  32. package/dist/panel-mutations.js +89 -0
  33. package/dist/panel-presentation.d.ts +8 -0
  34. package/dist/panel-presentation.js +67 -0
  35. package/dist/panel-resource.d.ts +2 -13
  36. package/dist/panel-resource.js +122 -310
  37. package/dist/panel-runtime.d.ts +8 -0
  38. package/dist/panel-runtime.js +34 -0
  39. package/dist/records.d.ts +42 -2
  40. package/dist/records.js +31 -0
  41. package/dist/review-decisions.d.ts +3 -0
  42. package/dist/review-decisions.js +274 -0
  43. package/dist/review-delivery.d.ts +34 -0
  44. package/dist/review-delivery.js +252 -0
  45. package/dist/review-presentation.d.ts +3 -0
  46. package/dist/review-presentation.js +28 -0
  47. package/dist/review-store.d.ts +4 -0
  48. package/dist/review-store.js +94 -0
  49. package/dist/role-recovery.d.ts +23 -0
  50. package/dist/role-recovery.js +176 -0
  51. package/dist/runtime-dependencies.d.ts +32 -0
  52. package/dist/runtime-dependencies.js +27 -0
  53. package/dist/runtime-support.d.ts +19 -0
  54. package/dist/runtime-support.js +74 -0
  55. package/dist/setup-presentation.d.ts +15 -0
  56. package/dist/setup-presentation.js +378 -0
  57. package/dist/strict-mode.d.ts +17 -0
  58. package/dist/strict-mode.js +154 -0
  59. package/dist/task-contributions.d.ts +4 -0
  60. package/dist/task-contributions.js +113 -0
  61. package/dist/task-scheduling.d.ts +12 -0
  62. package/dist/task-scheduling.js +79 -0
  63. package/dist/terminal-finalization.d.ts +12 -0
  64. package/dist/terminal-finalization.js +238 -0
  65. package/dist/workflow-fences.d.ts +6 -0
  66. package/dist/workflow-fences.js +73 -0
  67. package/manifest.json +1 -1
  68. package/migrations/0002_panel_mutation_claims.sql +34 -0
  69. package/package.json +5 -4
@@ -1,16 +1,14 @@
1
1
  import { DashboardActionValidationError, HELYX_SERVICE_NAMES, } from "@helyx/sdk";
2
- import { ACCOUNT_GATE_COMPONENTS, ACCOUNT_GATE_IDENTITY } from "./contracts.js";
2
+ import { ACCOUNT_GATE_IDENTITY } from "./contracts.js";
3
3
  import { parseAccountGateConfiguration } from "./configuration.js";
4
4
  import { managedResourceSummary } from "./managed-resource-summary.js";
5
- import { hashPasswordEntryPhrase } from "./password-entry.js";
5
+ import { queuePanelMessageCleanup } from "./panel-maintenance.js";
6
+ import { DEFAULT_PANEL_MESSAGES, panelAccessType, panelDetail as detail, panelLimitError, panelMessage, panelMessageFromValue, panelNumber as numberValue, panelPasswordFromRow, panelString as stringValue, savedPanelMessage, } from "./panel-model.js";
7
+ import { claimPanelMutation, finalizePanelMutation, } from "./panel-mutations.js";
8
+ import { configuredPanelAccessType as configuredAccessType, inspectedPanelDetail as inspectedDetail, panelPasswordForCreate, panelPasswordForUpdate, } from "./panel-presentation.js";
6
9
  import { accountGateRecords } from "./records.js";
7
- const VIOLET = 0x7c3aed;
10
+ export { getGatePanelRuntime, hasPasswordGatePanel } from "./panel-runtime.js";
8
11
  const MAX_GATE_PANELS = 10;
9
- const DEFAULT_PANEL_MESSAGES = {
10
- challenges: "Complete a short human-interaction check to access the server. Press the button below when you are ready.",
11
- password: "Enter the password for this gate in the Password Entry channel. Never enter your Discord password or authentication code.",
12
- emoji: "React with the configured emoji on the Emoji Entry message to continue.",
13
- };
14
12
  export const gatePanelResource = {
15
13
  id: "gate-panel",
16
14
  availableWhileDisabled: true,
@@ -57,14 +55,50 @@ export const gatePanelResource = {
57
55
  if (!row)
58
56
  return { deleted: false };
59
57
  assertRevision(row, input.expectedRevision);
60
- await context.deleteChannelMessage(stringValue(row.channel_id), stringValue(row.message_id));
61
- const result = await records(context).deleteMany({
58
+ const store = records(context);
59
+ const claimed = await claimPanelMutation(store, row, {
60
+ ...auditContext(context),
61
+ action: "delete",
62
+ messageBody: savedPanelMessage(row),
63
+ accessType: panelAccessType(row),
64
+ password: panelPasswordFromRow(row),
65
+ });
66
+ if (!claimed)
67
+ throw stalePanel();
68
+ const channelId = stringValue(claimed.channel_id);
69
+ const messageId = stringValue(claimed.message_id);
70
+ const removed = await context
71
+ .deleteChannelMessage(channelId, messageId)
72
+ .then(() => true)
73
+ .catch(() => false);
74
+ if (!removed)
75
+ await queuePanelMessageCleanup({
76
+ services: context.services,
77
+ guildId: context.guildId,
78
+ panelId: input.resourceId,
79
+ channelId,
80
+ messageId,
81
+ });
82
+ const result = await store.deleteOne({
62
83
  moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
63
84
  collection: "gate_panels",
64
85
  where: { guild_id: context.guildId, panel_id: input.resourceId },
65
- limit: 1,
86
+ expected: { field: "revision", value: numberValue(claimed.revision) },
87
+ audit: {
88
+ guildId: context.guildId,
89
+ actorUserId: context.actor.userId,
90
+ action: "account_gate.panel_deleted",
91
+ source: "dashboard",
92
+ correlationId: context.correlationId,
93
+ idempotencyKey: `account-gate-panel-deleted:${input.resourceId}:${numberValue(claimed.revision)}`,
94
+ targetType: "account_gate_panel",
95
+ targetId: input.resourceId,
96
+ metadata: { channelId, messageId },
97
+ },
66
98
  });
67
- return { deleted: result.deleted === 1 };
99
+ if (!result.deleted)
100
+ throw stalePanel();
101
+ return { deleted: true };
68
102
  },
69
103
  async executeAction(context, input) {
70
104
  if (input.actionId !== "repair" && input.actionId !== "repost") {
@@ -94,20 +128,31 @@ async function writePanelMessage(context, current, messageBody, accessType, pass
94
128
  !inspection.channelMatches) {
95
129
  return replacePanel(context, current, action, messageBody, accessType, password);
96
130
  }
131
+ const store = records(context);
132
+ const claimed = await claimPanelMutation(store, current, {
133
+ ...auditContext(context),
134
+ action,
135
+ messageBody,
136
+ accessType,
137
+ password,
138
+ });
139
+ if (!claimed)
140
+ throw stalePanel();
97
141
  try {
98
- await context.editChannelMessage(stringValue(current.channel_id), stringValue(current.message_id), panelMessage(messageBody, accessType, stringValue(current.panel_id)));
142
+ await context.editChannelMessage(stringValue(claimed.channel_id), stringValue(claimed.message_id), panelMessage(messageBody, accessType, stringValue(claimed.panel_id)));
99
143
  }
100
144
  catch {
101
- return replacePanel(context, current, action, messageBody, accessType, password);
145
+ throw new DashboardActionValidationError("The Gate panel settings were saved, but the Discord message needs repair.");
102
146
  }
103
- return persistPanelRevision(context, current, {
104
- channelId: stringValue(current.channel_id),
105
- messageId: stringValue(current.message_id),
106
- messageBody,
107
- accessType,
108
- password,
147
+ const finalized = await finalizePanelMutation(store, claimed, {
148
+ ...auditContext(context),
109
149
  action,
150
+ channelId: stringValue(claimed.channel_id),
151
+ messageId: stringValue(claimed.message_id),
110
152
  });
153
+ if (!finalized)
154
+ throw new DashboardActionValidationError("The Gate panel message was updated, but its final state needs repair.");
155
+ return detail(finalized, "posted");
111
156
  }
112
157
  async function replacePanel(context, current, action, messageBody, accessType, password) {
113
158
  const configured = await context.services
@@ -117,85 +162,56 @@ async function replacePanel(context, current, action, messageBody, accessType, p
117
162
  throw new DashboardActionValidationError("Save Access Control settings before posting the panel.");
118
163
  }
119
164
  const configuration = parseAccountGateConfiguration(configured.value);
120
- const sent = await context.sendToChannel(configuration.gateChannelId, panelMessage(messageBody, accessType, stringValue(current.panel_id)));
165
+ const store = records(context);
166
+ const claimed = await claimPanelMutation(store, current, {
167
+ ...auditContext(context),
168
+ action,
169
+ messageBody,
170
+ accessType,
171
+ password,
172
+ });
173
+ if (!claimed)
174
+ throw stalePanel();
175
+ const sent = await context.sendToChannel(configuration.gateChannelId, panelMessage(messageBody, accessType, stringValue(claimed.panel_id)));
121
176
  try {
122
- const detail = await persistPanelRevision(context, current, {
177
+ const finalized = await finalizePanelMutation(store, claimed, {
178
+ ...auditContext(context),
179
+ action,
123
180
  channelId: configuration.gateChannelId,
124
181
  messageId: sent.messageId,
125
- messageBody,
126
- accessType,
127
- password,
128
- action,
129
182
  });
183
+ if (!finalized)
184
+ throw stalePanel();
130
185
  try {
131
186
  await context.deleteChannelMessage(stringValue(current.channel_id), stringValue(current.message_id));
132
187
  }
133
188
  catch {
134
- // The replacement is authoritative even when the old message is gone.
189
+ await queuePanelMessageCleanup({
190
+ services: context.services,
191
+ guildId: context.guildId,
192
+ panelId: stringValue(current.panel_id),
193
+ channelId: stringValue(current.channel_id),
194
+ messageId: stringValue(current.message_id),
195
+ });
135
196
  }
136
- return detail;
197
+ return detail(finalized, "posted");
137
198
  }
138
199
  catch (error) {
139
- await context
200
+ const removed = await context
140
201
  .deleteChannelMessage(configuration.gateChannelId, sent.messageId)
141
- .catch(() => undefined);
202
+ .then(() => true)
203
+ .catch(() => false);
204
+ if (!removed)
205
+ await queuePanelMessageCleanup({
206
+ services: context.services,
207
+ guildId: context.guildId,
208
+ panelId: stringValue(current.panel_id),
209
+ channelId: configuration.gateChannelId,
210
+ messageId: sent.messageId,
211
+ });
142
212
  throw error;
143
213
  }
144
214
  }
145
- async function persistPanelRevision(context, current, input) {
146
- const revision = numberValue(current.revision) + 1;
147
- const updatedAt = new Date();
148
- const result = await records(context).updateOne({
149
- moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
150
- collection: "gate_panels",
151
- where: {
152
- guild_id: context.guildId,
153
- panel_id: stringValue(current.panel_id),
154
- },
155
- expected: { field: "revision", value: numberValue(current.revision) },
156
- values: {
157
- channel_id: input.channelId,
158
- message_id: input.messageId,
159
- message_body: input.messageBody,
160
- access_type: input.accessType,
161
- ...(input.password
162
- ? {
163
- password_hash: input.password.encodedHash,
164
- password_ignore_case: input.password.ignoreCase,
165
- password_trim_whitespace: input.password.trimWhitespace,
166
- }
167
- : {}),
168
- revision,
169
- updated_at: updatedAt,
170
- },
171
- audit: {
172
- guildId: context.guildId,
173
- actorUserId: context.actor.userId,
174
- action: `account_gate.panel_${input.action}`,
175
- source: "dashboard",
176
- correlationId: context.correlationId,
177
- idempotencyKey: context.correlationId,
178
- targetType: "account_gate_panel",
179
- targetId: stringValue(current.panel_id),
180
- metadata: { revision },
181
- },
182
- });
183
- if (!result.updated) {
184
- throw new DashboardActionValidationError("The Gate panel changed in another session. Reload and try again.");
185
- }
186
- return detail({
187
- ...current,
188
- channel_id: input.channelId,
189
- message_id: input.messageId,
190
- message_body: input.messageBody,
191
- access_type: input.accessType,
192
- password_hash: input.password?.encodedHash ?? null,
193
- password_ignore_case: input.password?.ignoreCase ?? true,
194
- password_trim_whitespace: input.password?.trimWhitespace ?? true,
195
- revision,
196
- updated_at: updatedAt,
197
- }, "posted");
198
- }
199
215
  async function currentPanel(context, resourceId) {
200
216
  const current = await currentPanelOrNull(context, resourceId);
201
217
  if (!current) {
@@ -212,9 +228,8 @@ async function currentPanelOrNull(context, resourceId) {
212
228
  });
213
229
  }
214
230
  function assertRevision(current, expectedRevision) {
215
- if (numberValue(current.revision) !== expectedRevision) {
216
- throw new DashboardActionValidationError("The Gate panel changed in another session. Reload and try again.");
217
- }
231
+ if (numberValue(current.revision) !== expectedRevision)
232
+ throw stalePanel();
218
233
  }
219
234
  async function post(context, value = {}) {
220
235
  const configured = await context.services
@@ -255,6 +270,7 @@ async function post(context, value = {}) {
255
270
  password_trim_whitespace: password.trimWhitespace,
256
271
  }
257
272
  : {}),
273
+ status: "posted",
258
274
  revision: 1,
259
275
  created_at: now,
260
276
  updated_at: now,
@@ -267,19 +283,11 @@ async function post(context, value = {}) {
267
283
  values: row,
268
284
  });
269
285
  if (!created.created) {
270
- await context
271
- .deleteChannelMessage(configuration.gateChannelId, sent.messageId)
272
- .catch(() => undefined);
273
- const concurrent = await currentPanelOrNull(context, context.correlationId);
274
- if (concurrent)
275
- return inspectedDetail(context, concurrent);
276
286
  throw new DashboardActionValidationError("That gate-panel request was already handled. Reload the resources list.");
277
287
  }
278
288
  }
279
289
  catch (error) {
280
- await context
281
- .deleteChannelMessage(configuration.gateChannelId, sent.messageId)
282
- .catch(() => undefined);
290
+ await cleanupSentMessage(context, panelId, configuration.gateChannelId, sent.messageId);
283
291
  const concurrent = await currentPanelOrNull(context, context.correlationId);
284
292
  if (concurrent)
285
293
  return inspectedDetail(context, concurrent);
@@ -300,228 +308,32 @@ async function post(context, value = {}) {
300
308
  message_body: messageBody,
301
309
  }, "posted");
302
310
  }
303
- function panelMessage(messageBody, accessType, panelId) {
304
- if (accessType === "emoji") {
305
- return {
306
- allowedUserMentionIds: [],
307
- componentsV2: {
308
- text: [
309
- {
310
- content: messageBody,
311
- markdown: false,
312
- },
313
- ],
314
- },
315
- };
316
- }
317
- return {
318
- allowedUserMentionIds: [],
319
- componentsV2: {
320
- ...(accessType === "challenges" ? { accentColor: VIOLET } : {}),
321
- text: [
322
- ...(accessType === "challenges"
323
- ? [{ content: "## Complete Access Control" }]
324
- : []),
325
- {
326
- content: messageBody,
327
- markdown: false,
328
- },
329
- ],
330
- },
331
- buttons: [
332
- {
333
- type: "button",
334
- customId: `${ACCOUNT_GATE_COMPONENTS.startPrefix}${panelId}`,
335
- label: "Start check",
336
- style: "primary",
337
- },
338
- ],
339
- };
340
- }
341
- async function configuredAccessType(context) {
342
- const configured = await context.services
343
- .get(HELYX_SERVICE_NAMES.configuration)
344
- .get(context.guildId, ACCOUNT_GATE_IDENTITY.moduleId);
345
- if (!configured)
346
- return "challenges";
347
- return parseAccountGateConfiguration(configured.value).accessType;
348
- }
349
- async function inspectedDetail(context, row) {
350
- const inspected = await context.services
351
- .get(HELYX_SERVICE_NAMES.discordResources)
352
- .inspectMessage({
311
+ async function cleanupSentMessage(context, panelId, channelId, messageId) {
312
+ const removed = await context
313
+ .deleteChannelMessage(channelId, messageId)
314
+ .then(() => true)
315
+ .catch(() => false);
316
+ if (removed)
317
+ return;
318
+ await queuePanelMessageCleanup({
319
+ services: context.services,
353
320
  guildId: context.guildId,
354
- channelId: stringValue(row.channel_id),
355
- messageId: stringValue(row.message_id),
356
- });
357
- return detail(row, inspected.exists && inspected.guildMatches && inspected.channelMatches
358
- ? "posted"
359
- : "missing");
360
- }
361
- function detail(row, status) {
362
- const updatedAt = row.updated_at instanceof Date ? row.updated_at : new Date(0);
363
- const accessType = panelAccessType(row);
364
- const panelTitles = {
365
- challenges: "Challenge gate panel",
366
- password: "Password Entry instructions",
367
- emoji: "Emoji Entry instructions",
368
- };
369
- return {
370
- id: stringValue(row.panel_id),
371
- revision: numberValue(row.revision),
372
- status,
373
- title: panelTitles[accessType],
374
- description: status === "posted"
375
- ? "Published and currently available."
376
- : "The saved panel message is missing. Repost it to repair the panel.",
377
- attributes: {
378
- channelId: stringValue(row.channel_id),
379
- messageId: stringValue(row.message_id),
380
- accessType,
381
- passwordConfigured: accessType === "password" && panelPasswordFromRow(row) !== null,
382
- },
383
- value: {
384
- channelId: stringValue(row.channel_id),
385
- messageId: stringValue(row.message_id),
386
- accessType,
387
- messageBody: savedPanelMessage(row),
388
- replacementPhrase: "",
389
- },
390
- updatedAt: updatedAt.toISOString(),
391
- };
392
- }
393
- function panelAccessType(row) {
394
- const value = row.access_type;
395
- if (value === "password" || value === "emoji")
396
- return value;
397
- return "challenges";
398
- }
399
- function panelMessageFromValue(value, fallback) {
400
- const candidate = value.messageBody;
401
- if (candidate === undefined)
402
- return fallback;
403
- if (typeof candidate !== "string") {
404
- throw new DashboardActionValidationError("Enter a valid Access Control panel message.");
405
- }
406
- const normalized = candidate.trim();
407
- if (normalized.length < 1 || normalized.length > 1000) {
408
- throw new DashboardActionValidationError("The Access Control panel message must be between 1 and 1000 characters.");
409
- }
410
- return normalized;
411
- }
412
- function savedPanelMessage(row) {
413
- const message = row.message_body;
414
- return typeof message === "string" && message.length > 0
415
- ? message
416
- : DEFAULT_PANEL_MESSAGES[panelAccessType(row)];
417
- }
418
- export async function getGatePanelRuntime(store, guildId, panelId) {
419
- const row = await store.findOne({
420
- moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
421
- collection: "gate_panels",
422
- select: accountGateRecords.gate_panels.readableFields,
423
- where: { guild_id: guildId, panel_id: panelId },
424
- });
425
- if (!row)
426
- return null;
427
- return {
428
- accessType: panelAccessType(row),
429
- password: panelPasswordFromRow(row),
430
- };
431
- }
432
- export async function hasPasswordGatePanel(store, guildId) {
433
- const page = await store.findMany({
434
- moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
435
- collection: "gate_panels",
436
- select: accountGateRecords.gate_panels.readableFields,
437
- where: { guild_id: guildId, access_type: "password" },
438
- orderBy: [
439
- { field: "guild_id", direction: "asc" },
440
- { field: "created_at", direction: "asc" },
441
- { field: "panel_id", direction: "asc" },
442
- ],
443
- limit: MAX_GATE_PANELS,
321
+ panelId,
322
+ channelId,
323
+ messageId,
444
324
  });
445
- return page.records.some((row) => panelPasswordFromRow(row) !== null);
446
- }
447
- async function panelPasswordForCreate(configuration, accessType, value) {
448
- if (accessType !== "password")
449
- return null;
450
- const phrase = replacementPhrase(value, true);
451
- return {
452
- encodedHash: await hashPasswordEntryPhrase(phrase, {
453
- ignoreCase: configuration.passwordIgnoreCase,
454
- trimWhitespace: configuration.passwordTrimWhitespace,
455
- }),
456
- ignoreCase: configuration.passwordIgnoreCase,
457
- trimWhitespace: configuration.passwordTrimWhitespace,
458
- };
459
- }
460
- async function panelPasswordForUpdate(context, current, value) {
461
- if (panelAccessType(current) !== "password")
462
- return null;
463
- const existing = panelPasswordFromRow(current);
464
- const phrase = replacementPhrase(value, false);
465
- if (!phrase) {
466
- if (existing)
467
- return existing;
468
- throw new DashboardActionValidationError("Enter a password containing 4 to 200 characters before publishing this panel.");
469
- }
470
- const configured = await context.services
471
- .get(HELYX_SERVICE_NAMES.configuration)
472
- .get(context.guildId, ACCOUNT_GATE_IDENTITY.moduleId);
473
- if (!configured) {
474
- throw new DashboardActionValidationError("Save Access Control settings before changing this password.");
475
- }
476
- const configuration = parseAccountGateConfiguration(configured.value);
477
- return {
478
- encodedHash: await hashPasswordEntryPhrase(phrase, {
479
- ignoreCase: configuration.passwordIgnoreCase,
480
- trimWhitespace: configuration.passwordTrimWhitespace,
481
- }),
482
- ignoreCase: configuration.passwordIgnoreCase,
483
- trimWhitespace: configuration.passwordTrimWhitespace,
484
- };
485
325
  }
486
- function panelPasswordFromRow(row) {
487
- if (typeof row.password_hash !== "string" || row.password_hash.length < 1) {
488
- return null;
489
- }
326
+ function auditContext(context) {
490
327
  return {
491
- encodedHash: row.password_hash,
492
- ignoreCase: typeof row.password_ignore_case === "boolean"
493
- ? row.password_ignore_case
494
- : true,
495
- trimWhitespace: typeof row.password_trim_whitespace === "boolean"
496
- ? row.password_trim_whitespace
497
- : true,
328
+ guildId: context.guildId,
329
+ actorUserId: context.actor.userId,
330
+ correlationId: context.correlationId,
498
331
  };
499
332
  }
500
- function replacementPhrase(value, required) {
501
- const candidate = value.replacementPhrase;
502
- if ((candidate === undefined || candidate === "") && !required)
503
- return "";
504
- if (typeof candidate !== "string" ||
505
- candidate.length < 4 ||
506
- candidate.length > 200) {
507
- throw new DashboardActionValidationError("Enter a password containing 4 to 200 characters.");
508
- }
509
- return candidate;
510
- }
511
- function panelLimitError() {
512
- return new DashboardActionValidationError("You have reached your limit of 10 gate panels. Delete an unused gate before publishing another.");
333
+ function stalePanel() {
334
+ return new DashboardActionValidationError("The Gate panel changed in another session. Reload and try again.");
513
335
  }
514
336
  function records(context) {
515
337
  return context.services.get(HELYX_SERVICE_NAMES.records);
516
338
  }
517
- function stringValue(value) {
518
- if (typeof value !== "string")
519
- throw new Error("Access Control panel record is invalid");
520
- return value;
521
- }
522
- function numberValue(value) {
523
- if (typeof value !== "number" || !Number.isSafeInteger(value))
524
- throw new Error("Access Control panel record is invalid");
525
- return value;
526
- }
527
339
  //# sourceMappingURL=panel-resource.js.map
@@ -0,0 +1,8 @@
1
+ import type { ModuleRecordService } from "@helyx/sdk";
2
+ import type { GatePanelAccessType, PanelPassword } from "./panel-model.js";
3
+ export declare function getGatePanelRuntime(store: ModuleRecordService, guildId: string, panelId: string): Promise<{
4
+ accessType: GatePanelAccessType;
5
+ password: PanelPassword | null;
6
+ } | null>;
7
+ export declare function hasPasswordGatePanel(store: ModuleRecordService, guildId: string): Promise<boolean>;
8
+ //# sourceMappingURL=panel-runtime.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { ACCOUNT_GATE_IDENTITY } from "./contracts.js";
2
+ import { panelAccessType, panelPasswordFromRow } from "./panel-model.js";
3
+ import { accountGateRecords } from "./records.js";
4
+ const MAX_GATE_PANELS = 10;
5
+ export async function getGatePanelRuntime(store, guildId, panelId) {
6
+ const row = await store.findOne({
7
+ moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
8
+ collection: "gate_panels",
9
+ select: accountGateRecords.gate_panels.readableFields,
10
+ where: { guild_id: guildId, panel_id: panelId },
11
+ });
12
+ if (!row)
13
+ return null;
14
+ return {
15
+ accessType: panelAccessType(row),
16
+ password: panelPasswordFromRow(row),
17
+ };
18
+ }
19
+ export async function hasPasswordGatePanel(store, guildId) {
20
+ const page = await store.findMany({
21
+ moduleId: ACCOUNT_GATE_IDENTITY.moduleId,
22
+ collection: "gate_panels",
23
+ select: accountGateRecords.gate_panels.readableFields,
24
+ where: { guild_id: guildId, access_type: "password" },
25
+ orderBy: [
26
+ { field: "guild_id", direction: "asc" },
27
+ { field: "created_at", direction: "asc" },
28
+ { field: "panel_id", direction: "asc" },
29
+ ],
30
+ limit: MAX_GATE_PANELS,
31
+ });
32
+ return page.records.some((row) => panelPasswordFromRow(row) !== null);
33
+ }
34
+ //# sourceMappingURL=panel-runtime.js.map
package/dist/records.d.ts CHANGED
@@ -32,6 +32,9 @@ export declare const accountGateRecords: {
32
32
  readonly type: "boolean";
33
33
  readonly optional: true;
34
34
  };
35
+ readonly status: {
36
+ readonly type: "string";
37
+ };
35
38
  readonly revision: {
36
39
  readonly type: "number";
37
40
  };
@@ -43,10 +46,47 @@ export declare const accountGateRecords: {
43
46
  };
44
47
  };
45
48
  readonly uniqueKeys: readonly [readonly ["panel_id"], readonly ["guild_id", "panel_id"]];
46
- readonly readableFields: readonly ["panel_id", "guild_id", "channel_id", "message_id", "message_body", "access_type", "password_hash", "password_ignore_case", "password_trim_whitespace", "revision", "created_at", "updated_at"];
47
- readonly writableFields: readonly ["channel_id", "message_id", "message_body", "access_type", "password_hash", "password_ignore_case", "password_trim_whitespace", "revision", "updated_at"];
49
+ readonly readableFields: readonly ["panel_id", "guild_id", "channel_id", "message_id", "message_body", "access_type", "password_hash", "password_ignore_case", "password_trim_whitespace", "status", "revision", "created_at", "updated_at"];
50
+ readonly writableFields: readonly ["channel_id", "message_id", "message_body", "access_type", "password_hash", "password_ignore_case", "password_trim_whitespace", "status", "revision", "updated_at"];
48
51
  readonly orderKeys: readonly [readonly ["guild_id", "created_at", "panel_id"]];
49
52
  };
53
+ readonly panel_message_cleanup: {
54
+ readonly table: "account_gate_panel_message_cleanup";
55
+ readonly fields: {
56
+ readonly cleanup_id: {
57
+ readonly type: "string";
58
+ };
59
+ readonly guild_id: {
60
+ readonly type: "string";
61
+ };
62
+ readonly panel_id: {
63
+ readonly type: "string";
64
+ };
65
+ readonly channel_id: {
66
+ readonly type: "string";
67
+ };
68
+ readonly message_id: {
69
+ readonly type: "string";
70
+ };
71
+ readonly attempts: {
72
+ readonly type: "number";
73
+ };
74
+ readonly next_attempt_at: {
75
+ readonly type: "date";
76
+ };
77
+ readonly completed_at: {
78
+ readonly type: "date";
79
+ readonly optional: true;
80
+ };
81
+ readonly created_at: {
82
+ readonly type: "date";
83
+ };
84
+ };
85
+ readonly uniqueKeys: readonly [readonly ["cleanup_id"], readonly ["guild_id", "channel_id", "message_id"]];
86
+ readonly readableFields: readonly ["cleanup_id", "guild_id", "panel_id", "channel_id", "message_id", "attempts", "next_attempt_at", "completed_at", "created_at"];
87
+ readonly writableFields: readonly ["attempts", "next_attempt_at", "completed_at"];
88
+ readonly orderKeys: readonly [readonly ["next_attempt_at", "cleanup_id"]];
89
+ };
50
90
  readonly questions: {
51
91
  readonly table: "account_gate_questions";
52
92
  readonly fields: {