@helyx/bot 1.5.2 → 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 +16 -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/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 +124 -43
- package/dist/shard-worker.js +25 -15
- package/documentation/README.md +1 -0
- package/documentation/guides/self-hosting.md +54 -0
- package/package.json +18 -17
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { BotControlValidationError } from "./control-errors.js";
|
|
2
|
+
export function validateManagedResourceQuery(module, resourceType, input) {
|
|
3
|
+
const query = module.manifest.dashboard?.resources?.find(({ id }) => id === resourceType)?.query;
|
|
4
|
+
if (input.search !== undefined && !query?.search)
|
|
5
|
+
throw new BotControlValidationError("Managed resource search is not declared by this module");
|
|
6
|
+
for (const [key, value] of Object.entries(input.filters ?? {})) {
|
|
7
|
+
const filter = query?.filters?.find((candidate) => candidate.key === key);
|
|
8
|
+
if (!filter)
|
|
9
|
+
throw new BotControlValidationError("Managed resource filter is not declared by this module");
|
|
10
|
+
if (filter.type === "select" &&
|
|
11
|
+
!filter.options.some((option) => option.value === value))
|
|
12
|
+
throw new BotControlValidationError("Managed resource filter value is not declared by this module");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=managed-resource-query.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Client, Guild, GuildMember } from "discord.js";
|
|
2
|
+
/** Private actor-construction failure; never turn an unknown member into no roles. */
|
|
3
|
+
export declare class MemberRoleSnapshotError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Pinned Discord.js compatibility: member.roles.cache omits assigned IDs whose
|
|
8
|
+
* Role objects are unavailable. Authorization must retain those IDs for denies.
|
|
9
|
+
*/
|
|
10
|
+
export declare function snapshotMemberRoleIds({ member, client, guild, guildId, actorUserId, }: {
|
|
11
|
+
member: GuildMember;
|
|
12
|
+
client: Client;
|
|
13
|
+
guild: Guild | null;
|
|
14
|
+
guildId: string | null;
|
|
15
|
+
actorUserId: string;
|
|
16
|
+
}): readonly string[];
|
|
17
|
+
//# sourceMappingURL=member-role-ids.d.ts.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Client, Guild, GuildMember } from "discord.js";
|
|
2
|
+
const maximumAssignedRoles = 250;
|
|
3
|
+
const snowflake = /^\d{17,20}$/u;
|
|
4
|
+
/** Private actor-construction failure; never turn an unknown member into no roles. */
|
|
5
|
+
export class MemberRoleSnapshotError extends Error {
|
|
6
|
+
constructor() {
|
|
7
|
+
super("Discord member roles could not be verified.");
|
|
8
|
+
this.name = "MemberRoleSnapshotError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Pinned Discord.js compatibility: member.roles.cache omits assigned IDs whose
|
|
13
|
+
* Role objects are unavailable. Authorization must retain those IDs for denies.
|
|
14
|
+
*/
|
|
15
|
+
export function snapshotMemberRoleIds({ member, client, guild, guildId, actorUserId, }) {
|
|
16
|
+
try {
|
|
17
|
+
if (!(client instanceof Client) ||
|
|
18
|
+
!(guild instanceof Guild) ||
|
|
19
|
+
!(member instanceof GuildMember) ||
|
|
20
|
+
typeof guildId !== "string" ||
|
|
21
|
+
!snowflake.test(guildId) ||
|
|
22
|
+
!snowflake.test(actorUserId))
|
|
23
|
+
throw new MemberRoleSnapshotError();
|
|
24
|
+
const user = member.user;
|
|
25
|
+
const currentIdentity = () => guild.id === guildId &&
|
|
26
|
+
guild.client === client &&
|
|
27
|
+
member.guild === guild &&
|
|
28
|
+
member.client === client &&
|
|
29
|
+
member.id === actorUserId &&
|
|
30
|
+
member.user === user &&
|
|
31
|
+
user.id === actorUserId &&
|
|
32
|
+
user.client === client &&
|
|
33
|
+
member.partial === false;
|
|
34
|
+
if (!currentIdentity())
|
|
35
|
+
throw new MemberRoleSnapshotError();
|
|
36
|
+
const assigned = Reflect.get(member, "_roles");
|
|
37
|
+
if (!Array.isArray(assigned) || assigned.length > maximumAssignedRoles)
|
|
38
|
+
throw new MemberRoleSnapshotError();
|
|
39
|
+
const length = assigned.length;
|
|
40
|
+
const snapshot = [];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (let index = 0; index < length; index++) {
|
|
43
|
+
const id = assigned[index];
|
|
44
|
+
if (typeof id !== "string" ||
|
|
45
|
+
!snowflake.test(id) ||
|
|
46
|
+
id === guildId ||
|
|
47
|
+
seen.has(id))
|
|
48
|
+
throw new MemberRoleSnapshotError();
|
|
49
|
+
seen.add(id);
|
|
50
|
+
snapshot.push(id);
|
|
51
|
+
}
|
|
52
|
+
if (!currentIdentity() ||
|
|
53
|
+
Reflect.get(member, "_roles") !== assigned ||
|
|
54
|
+
assigned.length !== length ||
|
|
55
|
+
snapshot.some((id, index) => assigned[index] !== id) ||
|
|
56
|
+
!currentIdentity() ||
|
|
57
|
+
Reflect.get(member, "_roles") !== assigned ||
|
|
58
|
+
assigned.length !== length)
|
|
59
|
+
throw new MemberRoleSnapshotError();
|
|
60
|
+
return Object.freeze([guildId, ...snapshot].sort());
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new MemberRoleSnapshotError();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=member-role-ids.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { MessageProtectionEligibility } from "./guild-message-protection-delivery.js";
|
|
2
|
+
export type ProtectionAdmissionOwner = Readonly<{
|
|
3
|
+
applicationId: string;
|
|
4
|
+
instanceId: string;
|
|
5
|
+
} & ({
|
|
6
|
+
mode: "standalone";
|
|
7
|
+
} | {
|
|
8
|
+
mode: "sharded";
|
|
9
|
+
scope: "local" | "coordinated";
|
|
10
|
+
generation: string;
|
|
11
|
+
deploymentOrder: string;
|
|
12
|
+
supervisorId: string;
|
|
13
|
+
shardId: number;
|
|
14
|
+
shardCount: number;
|
|
15
|
+
fencingToken: string;
|
|
16
|
+
})>;
|
|
17
|
+
export interface ProtectionAdmissionScope {
|
|
18
|
+
readonly guildId: string;
|
|
19
|
+
readonly moduleId: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ProtectionReloadTicket {
|
|
22
|
+
readonly owner: ProtectionAdmissionOwner;
|
|
23
|
+
readonly scope: ProtectionAdmissionScope;
|
|
24
|
+
readonly ownerEpoch: string;
|
|
25
|
+
readonly scopeEpoch: string;
|
|
26
|
+
readonly sequence: string;
|
|
27
|
+
readonly capturedAt: number;
|
|
28
|
+
}
|
|
29
|
+
/** Trusted cold-loader digest, not a claimed existing persistence revision. */
|
|
30
|
+
export interface ProtectionAdmissionSnapshot {
|
|
31
|
+
readonly state: "enabled" | "disabled";
|
|
32
|
+
readonly revisionFingerprint: string;
|
|
33
|
+
/** Lease measured from ticket capture, never from delayed completion. */
|
|
34
|
+
readonly validForMs: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Private, pure runtime publisher. No loader, service registry, timer or I/O.
|
|
38
|
+
* Production composition intentionally does not instantiate or expose it yet.
|
|
39
|
+
*/
|
|
40
|
+
export declare class MessageProtectionAdmissionPublisher {
|
|
41
|
+
#private;
|
|
42
|
+
readonly owner: ProtectionAdmissionOwner;
|
|
43
|
+
readonly eligibility: MessageProtectionEligibility;
|
|
44
|
+
constructor(options: {
|
|
45
|
+
owner: ProtectionAdmissionOwner;
|
|
46
|
+
capacity: number;
|
|
47
|
+
maxAgeMs: number;
|
|
48
|
+
now?: () => number;
|
|
49
|
+
});
|
|
50
|
+
get scopeCount(): number;
|
|
51
|
+
/** One latest reference-bound ticket per scope; replacement rejects old I/O. */
|
|
52
|
+
captureReload(scope: ProtectionAdmissionScope): ProtectionReloadTicket | null;
|
|
53
|
+
publish(ticket: ProtectionReloadTicket, snapshot: ProtectionAdmissionSnapshot): boolean;
|
|
54
|
+
invalidateScope(scope: ProtectionAdmissionScope): void;
|
|
55
|
+
invalidateGuild(guildId: string): void;
|
|
56
|
+
invalidateModule(moduleId: string): void;
|
|
57
|
+
invalidateAll(): void;
|
|
58
|
+
stop(): void;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=message-protection-admission.d.ts.map
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
|
|
3
|
+
const SNOWFLAKE = /^\d{17,20}$/u;
|
|
4
|
+
const MODULE_ID = /^[a-z][a-z0-9-]{0,62}\.[a-z][a-z0-9-]{0,62}$/u;
|
|
5
|
+
const unavailable = Object.freeze({ state: "unavailable" });
|
|
6
|
+
const disabled = Object.freeze({ state: "disabled" });
|
|
7
|
+
/**
|
|
8
|
+
* Private, pure runtime publisher. No loader, service registry, timer or I/O.
|
|
9
|
+
* Production composition intentionally does not instantiate or expose it yet.
|
|
10
|
+
*/
|
|
11
|
+
export class MessageProtectionAdmissionPublisher {
|
|
12
|
+
owner;
|
|
13
|
+
eligibility;
|
|
14
|
+
#entries = new Map();
|
|
15
|
+
#tokens = new Map();
|
|
16
|
+
#clock;
|
|
17
|
+
#capacity;
|
|
18
|
+
#maxAgeMs;
|
|
19
|
+
#ownerEpoch = 0n;
|
|
20
|
+
#sequence = 0n;
|
|
21
|
+
#scopeEpoch = 0n;
|
|
22
|
+
#lastNow = -Infinity;
|
|
23
|
+
#stopped = false;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
assertOwner(options.owner);
|
|
26
|
+
if (!Number.isSafeInteger(options.capacity) ||
|
|
27
|
+
options.capacity < 1 ||
|
|
28
|
+
options.capacity > 100_000)
|
|
29
|
+
throw new TypeError("Protection admission capacity must be 1..100000");
|
|
30
|
+
if (!Number.isSafeInteger(options.maxAgeMs) ||
|
|
31
|
+
options.maxAgeMs < 1 ||
|
|
32
|
+
options.maxAgeMs > 600_000)
|
|
33
|
+
throw new TypeError("Protection admission maximum age must be 1..600000ms");
|
|
34
|
+
this.owner = Object.freeze({ ...options.owner });
|
|
35
|
+
this.#capacity = options.capacity;
|
|
36
|
+
this.#maxAgeMs = options.maxAgeMs;
|
|
37
|
+
this.#clock = options.now ?? (() => performance.now());
|
|
38
|
+
this.eligibility = Object.freeze({
|
|
39
|
+
read: (guildId, moduleId) => this.#read(guildId, moduleId),
|
|
40
|
+
isCurrent: (token) => this.#isCurrent(token),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
get scopeCount() {
|
|
44
|
+
return this.#entries.size;
|
|
45
|
+
}
|
|
46
|
+
/** One latest reference-bound ticket per scope; replacement rejects old I/O. */
|
|
47
|
+
captureReload(scope) {
|
|
48
|
+
assertScope(scope);
|
|
49
|
+
const now = this.#now();
|
|
50
|
+
if (now === null)
|
|
51
|
+
return null;
|
|
52
|
+
const key = scopeKey(scope);
|
|
53
|
+
let entry = this.#entries.get(key);
|
|
54
|
+
if (!entry) {
|
|
55
|
+
this.#prune(now);
|
|
56
|
+
if (this.#entries.size >= this.#capacity)
|
|
57
|
+
return null;
|
|
58
|
+
entry = {
|
|
59
|
+
scope: Object.freeze({
|
|
60
|
+
guildId: scope.guildId,
|
|
61
|
+
moduleId: scope.moduleId,
|
|
62
|
+
}),
|
|
63
|
+
epoch: String(++this.#scopeEpoch),
|
|
64
|
+
};
|
|
65
|
+
this.#entries.set(key, entry);
|
|
66
|
+
}
|
|
67
|
+
const ticket = Object.freeze({
|
|
68
|
+
owner: this.owner,
|
|
69
|
+
scope: entry.scope,
|
|
70
|
+
ownerEpoch: String(this.#ownerEpoch),
|
|
71
|
+
scopeEpoch: entry.epoch,
|
|
72
|
+
sequence: String(++this.#sequence),
|
|
73
|
+
capturedAt: now,
|
|
74
|
+
});
|
|
75
|
+
entry.ticket = ticket;
|
|
76
|
+
return ticket;
|
|
77
|
+
}
|
|
78
|
+
publish(ticket, snapshot) {
|
|
79
|
+
const now = this.#now();
|
|
80
|
+
if (now === null || !ticket || !validScope(ticket.scope))
|
|
81
|
+
return false;
|
|
82
|
+
const key = scopeKey(ticket.scope), entry = this.#entries.get(key);
|
|
83
|
+
if (!entry ||
|
|
84
|
+
entry.ticket !== ticket ||
|
|
85
|
+
ticket.owner !== this.owner ||
|
|
86
|
+
ticket.ownerEpoch !== String(this.#ownerEpoch) ||
|
|
87
|
+
ticket.scopeEpoch !== entry.epoch)
|
|
88
|
+
return false;
|
|
89
|
+
delete entry.ticket; // A receipt cannot be replayed to extend freshness.
|
|
90
|
+
const normalized = normalizeSnapshot(snapshot, this.#maxAgeMs);
|
|
91
|
+
if (!normalized || ticket.capturedAt + normalized.validForMs <= now) {
|
|
92
|
+
this.#remove(key, entry);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const same = entry.snapshot?.state === normalized.state &&
|
|
96
|
+
entry.snapshot.revisionFingerprint === normalized.revisionFingerprint &&
|
|
97
|
+
entry.expiresAt !== undefined &&
|
|
98
|
+
now < entry.expiresAt;
|
|
99
|
+
if (!same || normalized.state !== "enabled")
|
|
100
|
+
this.#revoke(entry);
|
|
101
|
+
entry.snapshot = normalized;
|
|
102
|
+
entry.expiresAt = ticket.capturedAt + normalized.validForMs;
|
|
103
|
+
if (normalized.state === "enabled" && !entry.token) {
|
|
104
|
+
entry.token = randomUUID();
|
|
105
|
+
this.#tokens.set(entry.token, entry);
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
invalidateScope(scope) {
|
|
110
|
+
assertScope(scope);
|
|
111
|
+
const key = scopeKey(scope), entry = this.#entries.get(key);
|
|
112
|
+
if (entry)
|
|
113
|
+
this.#remove(key, entry);
|
|
114
|
+
}
|
|
115
|
+
invalidateGuild(guildId) {
|
|
116
|
+
if (!SNOWFLAKE.test(guildId))
|
|
117
|
+
throw new TypeError("Invalid protection guild scope");
|
|
118
|
+
for (const [key, entry] of this.#entries)
|
|
119
|
+
if (entry.scope.guildId === guildId)
|
|
120
|
+
this.#remove(key, entry);
|
|
121
|
+
}
|
|
122
|
+
invalidateModule(moduleId) {
|
|
123
|
+
if (!MODULE_ID.test(moduleId))
|
|
124
|
+
throw new TypeError("Invalid protection module scope");
|
|
125
|
+
for (const [key, entry] of this.#entries)
|
|
126
|
+
if (entry.scope.moduleId === moduleId)
|
|
127
|
+
this.#remove(key, entry);
|
|
128
|
+
}
|
|
129
|
+
invalidateAll() {
|
|
130
|
+
++this.#ownerEpoch;
|
|
131
|
+
this.#entries.clear();
|
|
132
|
+
this.#tokens.clear();
|
|
133
|
+
}
|
|
134
|
+
stop() {
|
|
135
|
+
this.#stopped = true;
|
|
136
|
+
this.invalidateAll();
|
|
137
|
+
}
|
|
138
|
+
#read(guildId, moduleId) {
|
|
139
|
+
const now = this.#now();
|
|
140
|
+
if (now === null || !validScope({ guildId, moduleId }))
|
|
141
|
+
return unavailable;
|
|
142
|
+
const entry = this.#entries.get(scopeKey({ guildId, moduleId }));
|
|
143
|
+
if (!entry || !this.#live(entry, now))
|
|
144
|
+
return unavailable;
|
|
145
|
+
if (entry.snapshot?.state === "disabled")
|
|
146
|
+
return disabled;
|
|
147
|
+
return entry.token
|
|
148
|
+
? Object.freeze({ state: "enabled", generation: entry.token })
|
|
149
|
+
: unavailable;
|
|
150
|
+
}
|
|
151
|
+
#isCurrent(token) {
|
|
152
|
+
const now = this.#now();
|
|
153
|
+
if (now === null || typeof token !== "string")
|
|
154
|
+
return false;
|
|
155
|
+
const entry = this.#tokens.get(token);
|
|
156
|
+
return Boolean(entry &&
|
|
157
|
+
this.#live(entry, now) &&
|
|
158
|
+
entry.token === token &&
|
|
159
|
+
entry.snapshot?.state === "enabled");
|
|
160
|
+
}
|
|
161
|
+
#live(entry, now) {
|
|
162
|
+
if (entry.expiresAt === undefined || now >= entry.expiresAt) {
|
|
163
|
+
this.#revoke(entry);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
#revoke(entry) {
|
|
169
|
+
if (entry.token)
|
|
170
|
+
this.#tokens.delete(entry.token);
|
|
171
|
+
delete entry.token;
|
|
172
|
+
}
|
|
173
|
+
#remove(key, entry) {
|
|
174
|
+
this.#revoke(entry);
|
|
175
|
+
this.#entries.delete(key);
|
|
176
|
+
}
|
|
177
|
+
#prune(now) {
|
|
178
|
+
for (const [key, entry] of this.#entries) {
|
|
179
|
+
const pending = entry.ticket && now < entry.ticket.capturedAt + this.#maxAgeMs;
|
|
180
|
+
if (!this.#live(entry, now) && !pending)
|
|
181
|
+
this.#remove(key, entry);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
#now() {
|
|
185
|
+
if (this.#stopped)
|
|
186
|
+
return null;
|
|
187
|
+
let now;
|
|
188
|
+
try {
|
|
189
|
+
now = this.#clock();
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
this.stop();
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (!Number.isFinite(now) ||
|
|
196
|
+
now < 0 ||
|
|
197
|
+
now < this.#lastNow ||
|
|
198
|
+
now > Number.MAX_SAFE_INTEGER - this.#maxAgeMs) {
|
|
199
|
+
this.stop();
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
this.#lastNow = now;
|
|
203
|
+
return now;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function validScope(scope) {
|
|
207
|
+
return Boolean(scope &&
|
|
208
|
+
typeof scope.guildId === "string" &&
|
|
209
|
+
SNOWFLAKE.test(scope.guildId) &&
|
|
210
|
+
typeof scope.moduleId === "string" &&
|
|
211
|
+
MODULE_ID.test(scope.moduleId));
|
|
212
|
+
}
|
|
213
|
+
function assertScope(scope) {
|
|
214
|
+
if (!validScope(scope))
|
|
215
|
+
throw new TypeError("Invalid protection admission scope");
|
|
216
|
+
}
|
|
217
|
+
function scopeKey(scope) {
|
|
218
|
+
return `${scope.guildId}:${scope.moduleId}`;
|
|
219
|
+
}
|
|
220
|
+
function normalizeSnapshot(snapshot, maxAgeMs) {
|
|
221
|
+
try {
|
|
222
|
+
if (!snapshot ||
|
|
223
|
+
!plain(snapshot) ||
|
|
224
|
+
Object.keys(snapshot).length !== 3 ||
|
|
225
|
+
Object.keys(snapshot).some((key) => !["state", "revisionFingerprint", "validForMs"].includes(key)))
|
|
226
|
+
return null;
|
|
227
|
+
const { state, revisionFingerprint, validForMs } = snapshot;
|
|
228
|
+
if ((state !== "enabled" && state !== "disabled") ||
|
|
229
|
+
typeof revisionFingerprint !== "string" ||
|
|
230
|
+
!/^[a-f0-9]{64}$/u.test(revisionFingerprint) ||
|
|
231
|
+
!Number.isSafeInteger(validForMs) ||
|
|
232
|
+
validForMs < 1 ||
|
|
233
|
+
validForMs > maxAgeMs)
|
|
234
|
+
return null;
|
|
235
|
+
return Object.freeze({ state, revisionFingerprint, validForMs });
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function plain(value) {
|
|
242
|
+
return (!Array.isArray(value) &&
|
|
243
|
+
(Object.getPrototypeOf(value) === Object.prototype ||
|
|
244
|
+
Object.getPrototypeOf(value) === null));
|
|
245
|
+
}
|
|
246
|
+
function assertOwner(owner) {
|
|
247
|
+
if (!owner ||
|
|
248
|
+
!plain(owner) ||
|
|
249
|
+
typeof owner.applicationId !== "string" ||
|
|
250
|
+
!SNOWFLAKE.test(owner.applicationId) ||
|
|
251
|
+
typeof owner.instanceId !== "string" ||
|
|
252
|
+
!UUID.test(owner.instanceId))
|
|
253
|
+
throw new TypeError("Invalid protection admission owner");
|
|
254
|
+
if (owner.mode === "standalone" && Object.keys(owner).length === 3)
|
|
255
|
+
return;
|
|
256
|
+
if (owner.mode === "sharded" &&
|
|
257
|
+
Object.keys(owner).length === 10 &&
|
|
258
|
+
["local", "coordinated"].includes(owner.scope) &&
|
|
259
|
+
typeof owner.generation === "string" &&
|
|
260
|
+
/^[a-zA-Z0-9._:-]{1,128}$/u.test(owner.generation) &&
|
|
261
|
+
positiveDecimal(owner.deploymentOrder) &&
|
|
262
|
+
typeof owner.supervisorId === "string" &&
|
|
263
|
+
UUID.test(owner.supervisorId) &&
|
|
264
|
+
Number.isSafeInteger(owner.shardCount) &&
|
|
265
|
+
owner.shardCount >= 1 &&
|
|
266
|
+
owner.shardCount <= 4096 &&
|
|
267
|
+
Number.isSafeInteger(owner.shardId) &&
|
|
268
|
+
owner.shardId >= 0 &&
|
|
269
|
+
owner.shardId < owner.shardCount &&
|
|
270
|
+
positiveDecimal(owner.fencingToken))
|
|
271
|
+
return;
|
|
272
|
+
throw new TypeError("Invalid protection admission owner");
|
|
273
|
+
}
|
|
274
|
+
function positiveDecimal(value) {
|
|
275
|
+
return (typeof value === "string" &&
|
|
276
|
+
/^[1-9]\d{0,18}$/u.test(value) &&
|
|
277
|
+
BigInt(value) <= 9223372036854775807n);
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=message-protection-admission.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type MessageProtectionSnapshotData, type MessageProtectionSnapshotReader } from "@helyx/database-contracts";
|
|
2
|
+
import type { ModuleDefinition } from "@helyx/sdk";
|
|
3
|
+
export interface MessageProtectionColdScope {
|
|
4
|
+
readonly guildId: string;
|
|
5
|
+
readonly moduleId: string;
|
|
6
|
+
}
|
|
7
|
+
export type MessageProtectionColdResult = Readonly<{
|
|
8
|
+
kind: "unavailable";
|
|
9
|
+
reason: "read-failed" | "malformed" | "stale" | "unsupported-module";
|
|
10
|
+
}> | Readonly<{
|
|
11
|
+
kind: "observed";
|
|
12
|
+
scope: Readonly<MessageProtectionColdScope>;
|
|
13
|
+
configuredApplicationId: string;
|
|
14
|
+
databaseObservedAt: string;
|
|
15
|
+
guild: MessageProtectionSnapshotData["guild"];
|
|
16
|
+
installation: MessageProtectionSnapshotData["installation"];
|
|
17
|
+
configuration: MessageProtectionSnapshotData["configuration"];
|
|
18
|
+
availability: MessageProtectionSnapshotData["availability"];
|
|
19
|
+
tierAccess: Readonly<{
|
|
20
|
+
required: boolean;
|
|
21
|
+
reviewed: boolean;
|
|
22
|
+
granted: boolean;
|
|
23
|
+
activeUntil: string | null;
|
|
24
|
+
}>;
|
|
25
|
+
revisionFingerprint: string;
|
|
26
|
+
freshness: Readonly<{
|
|
27
|
+
capturedAt: number;
|
|
28
|
+
completedAt: number;
|
|
29
|
+
expiresAt: number;
|
|
30
|
+
}>;
|
|
31
|
+
}>;
|
|
32
|
+
/** No owner grant, enabled state, publisher call, timer or production registration. */
|
|
33
|
+
export declare function createMessageProtectionColdDataReader(options: {
|
|
34
|
+
applicationId: string;
|
|
35
|
+
modules: readonly ModuleDefinition[];
|
|
36
|
+
data: MessageProtectionSnapshotReader;
|
|
37
|
+
maximumAgeMs: number;
|
|
38
|
+
now?: () => number;
|
|
39
|
+
}): {
|
|
40
|
+
read(scope: MessageProtectionColdScope): Promise<MessageProtectionColdResult>;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=message-protection-cold-data.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { MessageProtectionSnapshotDataError, assertMessageProtectionSnapshotQuery, copyMessageProtectionSnapshotData, } from "@helyx/database-contracts";
|
|
3
|
+
import { getModuleTier, HELYX_PREMIUM_ENTITLEMENT_CAPABILITY, isReviewedPremiumModuleId, } from "@helyx/module-manifest";
|
|
4
|
+
/** No owner grant, enabled state, publisher call, timer or production registration. */
|
|
5
|
+
export function createMessageProtectionColdDataReader(options) {
|
|
6
|
+
if (typeof options.applicationId !== "string" ||
|
|
7
|
+
!/^\d{17,20}$/u.test(options.applicationId) ||
|
|
8
|
+
!Number.isSafeInteger(options.maximumAgeMs) ||
|
|
9
|
+
options.maximumAgeMs < 1 ||
|
|
10
|
+
options.maximumAgeMs > 600_000)
|
|
11
|
+
throw new TypeError("Invalid protection cold-reader context");
|
|
12
|
+
const applicationId = options.applicationId;
|
|
13
|
+
const maximumAgeMs = options.maximumAgeMs;
|
|
14
|
+
const dataReader = options.data;
|
|
15
|
+
const clock = options.now ?? (() => performance.now());
|
|
16
|
+
let lastNow = -Infinity;
|
|
17
|
+
let clockFailed = false;
|
|
18
|
+
const readClock = () => {
|
|
19
|
+
if (clockFailed)
|
|
20
|
+
return NaN;
|
|
21
|
+
let value;
|
|
22
|
+
try {
|
|
23
|
+
value = clock();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
clockFailed = true;
|
|
27
|
+
return NaN;
|
|
28
|
+
}
|
|
29
|
+
if (!validClock(value, maximumAgeMs) || value < lastNow) {
|
|
30
|
+
clockFailed = true;
|
|
31
|
+
return NaN;
|
|
32
|
+
}
|
|
33
|
+
lastNow = value;
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
if (new Set(options.modules.map(({ manifest }) => manifest.id)).size !==
|
|
37
|
+
options.modules.length)
|
|
38
|
+
throw new TypeError("Duplicate protection cold-reader module");
|
|
39
|
+
const policies = new Map(options.modules.flatMap((module) => {
|
|
40
|
+
const protection = (module.events?.guildMessageCreate ?? []).flatMap(({ protection: value }) => value
|
|
41
|
+
? [
|
|
42
|
+
{
|
|
43
|
+
version: value.version,
|
|
44
|
+
otherBots: value.otherBots === true,
|
|
45
|
+
webhooks: value.webhooks === true,
|
|
46
|
+
summary: value.summary === true,
|
|
47
|
+
deleteMessage: value.deleteMessage === true,
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
: []);
|
|
51
|
+
const updateMetadata = (module.events?.guildMessageUpdateMetadata ?? []).map(({ metadata }) => ({
|
|
52
|
+
version: metadata.version,
|
|
53
|
+
otherBots: metadata.otherBots === true,
|
|
54
|
+
webhooks: metadata.webhooks === true,
|
|
55
|
+
}));
|
|
56
|
+
return protection.length || updateMetadata.length
|
|
57
|
+
? [
|
|
58
|
+
[
|
|
59
|
+
module.manifest.id,
|
|
60
|
+
Object.freeze({
|
|
61
|
+
moduleId: module.manifest.id,
|
|
62
|
+
version: module.manifest.version,
|
|
63
|
+
schemaVersion: module.configuration?.schemaVersion ?? 1,
|
|
64
|
+
premium: getModuleTier(module.manifest) === "premium",
|
|
65
|
+
reviewed: isReviewedPremiumModuleId(module.manifest.id),
|
|
66
|
+
protection: Object.freeze(protection.map((item) => Object.freeze(item))),
|
|
67
|
+
updateMetadata: Object.freeze(updateMetadata.map((item) => Object.freeze(item))),
|
|
68
|
+
}),
|
|
69
|
+
],
|
|
70
|
+
]
|
|
71
|
+
: [];
|
|
72
|
+
}));
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
async read(scope) {
|
|
75
|
+
const policy = policies.get(scope.moduleId);
|
|
76
|
+
if (!policy)
|
|
77
|
+
return unavailable("unsupported-module");
|
|
78
|
+
const query = {
|
|
79
|
+
guildId: scope.guildId,
|
|
80
|
+
moduleId: scope.moduleId,
|
|
81
|
+
capability: HELYX_PREMIUM_ENTITLEMENT_CAPABILITY,
|
|
82
|
+
};
|
|
83
|
+
try {
|
|
84
|
+
assertMessageProtectionSnapshotQuery(query);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return unavailable("malformed");
|
|
88
|
+
}
|
|
89
|
+
let capturedAt;
|
|
90
|
+
try {
|
|
91
|
+
capturedAt = readClock();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return unavailable("stale");
|
|
95
|
+
}
|
|
96
|
+
if (!validClock(capturedAt, maximumAgeMs))
|
|
97
|
+
return unavailable("stale");
|
|
98
|
+
let data;
|
|
99
|
+
try {
|
|
100
|
+
data = await dataReader.read(query);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return unavailable(error instanceof MessageProtectionSnapshotDataError
|
|
104
|
+
? "malformed"
|
|
105
|
+
: "read-failed");
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
data = copyMessageProtectionSnapshotData(query, data);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return unavailable("malformed");
|
|
112
|
+
}
|
|
113
|
+
let completedAt;
|
|
114
|
+
try {
|
|
115
|
+
completedAt = readClock();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return unavailable("stale");
|
|
119
|
+
}
|
|
120
|
+
if (!validClock(completedAt, maximumAgeMs) || completedAt < capturedAt)
|
|
121
|
+
return unavailable("stale");
|
|
122
|
+
const granted = !policy.premium || (policy.reviewed && data.entitlement.granted);
|
|
123
|
+
const activeUntil = policy.premium && policy.reviewed && granted
|
|
124
|
+
? data.entitlement.activeUntil
|
|
125
|
+
: null;
|
|
126
|
+
let budget = maximumAgeMs;
|
|
127
|
+
if (activeUntil !== null)
|
|
128
|
+
budget = Math.min(budget, Math.max(0, Math.floor(Date.parse(activeUntil) - Date.parse(data.observedAt)) -
|
|
129
|
+
1));
|
|
130
|
+
const expiresAt = capturedAt + budget;
|
|
131
|
+
if (!Number.isFinite(expiresAt) || completedAt >= expiresAt)
|
|
132
|
+
return unavailable("stale");
|
|
133
|
+
const stableScope = Object.freeze({
|
|
134
|
+
guildId: query.guildId,
|
|
135
|
+
moduleId: query.moduleId,
|
|
136
|
+
});
|
|
137
|
+
const tierAccess = Object.freeze({
|
|
138
|
+
required: policy.premium,
|
|
139
|
+
reviewed: policy.reviewed,
|
|
140
|
+
granted,
|
|
141
|
+
activeUntil,
|
|
142
|
+
});
|
|
143
|
+
const vector = [
|
|
144
|
+
1,
|
|
145
|
+
applicationId,
|
|
146
|
+
policy,
|
|
147
|
+
stableScope,
|
|
148
|
+
data.guild,
|
|
149
|
+
data.installation,
|
|
150
|
+
data.configuration,
|
|
151
|
+
data.availability,
|
|
152
|
+
tierAccess,
|
|
153
|
+
];
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
kind: "observed",
|
|
156
|
+
scope: stableScope,
|
|
157
|
+
configuredApplicationId: applicationId,
|
|
158
|
+
databaseObservedAt: data.observedAt,
|
|
159
|
+
guild: data.guild,
|
|
160
|
+
installation: data.installation,
|
|
161
|
+
configuration: data.configuration,
|
|
162
|
+
availability: data.availability,
|
|
163
|
+
tierAccess,
|
|
164
|
+
revisionFingerprint: createHash("sha256")
|
|
165
|
+
.update(JSON.stringify(vector))
|
|
166
|
+
.digest("hex"),
|
|
167
|
+
freshness: Object.freeze({ capturedAt, completedAt, expiresAt }),
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function validClock(value, maxAgeMs) {
|
|
173
|
+
return (Number.isFinite(value) &&
|
|
174
|
+
value >= 0 &&
|
|
175
|
+
value <= Number.MAX_SAFE_INTEGER - maxAgeMs);
|
|
176
|
+
}
|
|
177
|
+
function unavailable(reason) {
|
|
178
|
+
return Object.freeze({ kind: "unavailable", reason });
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=message-protection-cold-data.js.map
|