@moxxy/plugin-channel-discord 0.27.0
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/LICENSE +21 -0
- package/dist/allow-list.d.ts +20 -0
- package/dist/allow-list.d.ts.map +1 -0
- package/dist/allow-list.js +12 -0
- package/dist/allow-list.js.map +1 -0
- package/dist/approval.d.ts +29 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +65 -0
- package/dist/approval.js.map +1 -0
- package/dist/channel/allow-list-store.d.ts +22 -0
- package/dist/channel/allow-list-store.d.ts.map +1 -0
- package/dist/channel/allow-list-store.js +45 -0
- package/dist/channel/allow-list-store.js.map +1 -0
- package/dist/channel/approval-prompt.d.ts +16 -0
- package/dist/channel/approval-prompt.d.ts.map +1 -0
- package/dist/channel/approval-prompt.js +27 -0
- package/dist/channel/approval-prompt.js.map +1 -0
- package/dist/channel/components.d.ts +27 -0
- package/dist/channel/components.d.ts.map +1 -0
- package/dist/channel/components.js +34 -0
- package/dist/channel/components.js.map +1 -0
- package/dist/channel/discord-like.d.ts +31 -0
- package/dist/channel/discord-like.d.ts.map +1 -0
- package/dist/channel/discord-like.js +9 -0
- package/dist/channel/discord-like.js.map +1 -0
- package/dist/channel/interaction-handler.d.ts +67 -0
- package/dist/channel/interaction-handler.d.ts.map +1 -0
- package/dist/channel/interaction-handler.js +155 -0
- package/dist/channel/interaction-handler.js.map +1 -0
- package/dist/channel/message-handler.d.ts +60 -0
- package/dist/channel/message-handler.d.ts.map +1 -0
- package/dist/channel/message-handler.js +135 -0
- package/dist/channel/message-handler.js.map +1 -0
- package/dist/channel/pairing-handler.d.ts +58 -0
- package/dist/channel/pairing-handler.d.ts.map +1 -0
- package/dist/channel/pairing-handler.js +109 -0
- package/dist/channel/pairing-handler.js.map +1 -0
- package/dist/channel/permission-prompt.d.ts +19 -0
- package/dist/channel/permission-prompt.d.ts.map +1 -0
- package/dist/channel/permission-prompt.js +38 -0
- package/dist/channel/permission-prompt.js.map +1 -0
- package/dist/channel/slash-handler.d.ts +45 -0
- package/dist/channel/slash-handler.d.ts.map +1 -0
- package/dist/channel/slash-handler.js +107 -0
- package/dist/channel/slash-handler.js.map +1 -0
- package/dist/channel/turn-runner.d.ts +49 -0
- package/dist/channel/turn-runner.d.ts.map +1 -0
- package/dist/channel/turn-runner.js +116 -0
- package/dist/channel/turn-runner.js.map +1 -0
- package/dist/channel/typing-indicator.d.ts +12 -0
- package/dist/channel/typing-indicator.d.ts.map +1 -0
- package/dist/channel/typing-indicator.js +28 -0
- package/dist/channel/typing-indicator.js.map +1 -0
- package/dist/channel/voice-handler.d.ts +41 -0
- package/dist/channel/voice-handler.d.ts.map +1 -0
- package/dist/channel/voice-handler.js +105 -0
- package/dist/channel/voice-handler.js.map +1 -0
- package/dist/channel.d.ts +107 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +464 -0
- package/dist/channel.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +179 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +64 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +85 -0
- package/dist/keys.js.map +1 -0
- package/dist/pair-flow.d.ts +16 -0
- package/dist/pair-flow.d.ts.map +1 -0
- package/dist/pair-flow.js +109 -0
- package/dist/pair-flow.js.map +1 -0
- package/dist/pairing.d.ts +83 -0
- package/dist/pairing.d.ts.map +1 -0
- package/dist/pairing.js +98 -0
- package/dist/pairing.js.map +1 -0
- package/dist/permission.d.ts +30 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +54 -0
- package/dist/permission.js.map +1 -0
- package/dist/render.d.ts +34 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +204 -0
- package/dist/render.js.map +1 -0
- package/dist/schema.d.ts +123 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +70 -0
- package/dist/schema.js.map +1 -0
- package/dist/setup-wizard.d.ts +13 -0
- package/dist/setup-wizard.d.ts.map +1 -0
- package/dist/setup-wizard.js +133 -0
- package/dist/setup-wizard.js.map +1 -0
- package/package.json +89 -0
- package/src/allow-list.test.ts +64 -0
- package/src/allow-list.ts +30 -0
- package/src/approval.ts +76 -0
- package/src/channel/allow-list-store.ts +52 -0
- package/src/channel/approval-prompt.ts +43 -0
- package/src/channel/components.ts +49 -0
- package/src/channel/discord-like.ts +34 -0
- package/src/channel/interaction-handler.test.ts +212 -0
- package/src/channel/interaction-handler.ts +237 -0
- package/src/channel/message-handler.test.ts +295 -0
- package/src/channel/message-handler.ts +195 -0
- package/src/channel/pairing-handler.ts +147 -0
- package/src/channel/permission-prompt.ts +54 -0
- package/src/channel/slash-handler.ts +136 -0
- package/src/channel/turn-runner.test.ts +177 -0
- package/src/channel/turn-runner.ts +139 -0
- package/src/channel/typing-indicator.ts +31 -0
- package/src/channel/voice-handler.test.ts +177 -0
- package/src/channel/voice-handler.ts +150 -0
- package/src/channel/voice-reply.test.ts +142 -0
- package/src/channel.ts +582 -0
- package/src/index.ts +247 -0
- package/src/keys.ts +100 -0
- package/src/pair-flow.ts +122 -0
- package/src/pairing.test.ts +99 -0
- package/src/pairing.ts +178 -0
- package/src/permission.ts +70 -0
- package/src/render.test.ts +82 -0
- package/src/render.ts +228 -0
- package/src/schema.ts +94 -0
- package/src/setup-wizard.ts +162 -0
- package/src/subcommands.test.ts +182 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { defineChannel, definePlugin } from '@moxxy/sdk';
|
|
2
|
+
import { DiscordChannel } from './channel.js';
|
|
3
|
+
import { DISCORD_ALLOWED_CHANNELS_KEY, DISCORD_AUTHORIZED_USER_KEY, DISCORD_TOKEN_ENV, DISCORD_TOKEN_KEY, parseAllowedChannels, parseAuthorizedUser, resolveBotToken, } from './keys.js';
|
|
4
|
+
import { runDiscordWizard } from './setup-wizard.js';
|
|
5
|
+
import { runPairFlow } from './pair-flow.js';
|
|
6
|
+
export { DiscordChannel, } from './channel.js';
|
|
7
|
+
export { DiscordPermissionResolver } from './permission.js';
|
|
8
|
+
export { DiscordApprovalResolver } from './approval.js';
|
|
9
|
+
export { armPairing, clearDiscordPairing, confirmPendingCode, createDiscordPairingState, isUserAuthorized, mintCodeForPeer, pairingPhase, } from './pairing.js';
|
|
10
|
+
export { gateInbound } from './allow-list.js';
|
|
11
|
+
export { DiscordTurnRenderer, splitForDiscord, DISCORD_MESSAGE_LIMIT } from './render.js';
|
|
12
|
+
export { extractInboundMessage, inboundMessageSchema, MAX_AUDIO_BYTES, MAX_CONTENT_CHARS, } from './schema.js';
|
|
13
|
+
export { DISCORD_TOKEN_KEY, DISCORD_AUTHORIZED_USER_KEY, DISCORD_ALLOWED_CHANNELS_KEY, DISCORD_TOKEN_ENV, DISCORD_TOKEN_RE, parseAuthorizedUser, parseAllowedChannels, serializeAllowedChannels, resolveBotToken, } from './keys.js';
|
|
14
|
+
/**
|
|
15
|
+
* Build the Discord channel plugin with a host-injected vault (mirroring the
|
|
16
|
+
* Telegram plugin's vault injection).
|
|
17
|
+
*/
|
|
18
|
+
export function buildDiscordPlugin(opts) {
|
|
19
|
+
return makeDiscordPlugin(() => opts.vault);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Discovery-loadable default export: resolves the vault from the inter-plugin
|
|
23
|
+
* service registry in `onInit` (the vault plugin publishes `'vault'`). Requires
|
|
24
|
+
* `@moxxy/plugin-vault` to load first (declared in `package.json`
|
|
25
|
+
* `moxxy.requirements`). The channel + subcommands read the vault via
|
|
26
|
+
* `getVault()`, so resolution is deferred to call time — after `onInit` wired it.
|
|
27
|
+
*/
|
|
28
|
+
export const discordPlugin = (() => {
|
|
29
|
+
let resolved = null;
|
|
30
|
+
const getVault = () => {
|
|
31
|
+
if (!resolved) {
|
|
32
|
+
throw new Error('@moxxy/plugin-channel-discord: the "vault" service is unavailable — @moxxy/plugin-vault must load first');
|
|
33
|
+
}
|
|
34
|
+
return resolved;
|
|
35
|
+
};
|
|
36
|
+
const hooks = {
|
|
37
|
+
onInit: (ctx) => {
|
|
38
|
+
resolved = ctx.services.require('vault');
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
return makeDiscordPlugin(getVault, hooks);
|
|
42
|
+
})();
|
|
43
|
+
// Discovery entry: `createPluginLoader` requires a default Plugin export.
|
|
44
|
+
export default discordPlugin;
|
|
45
|
+
function makeDiscordPlugin(getVault, hooks) {
|
|
46
|
+
return definePlugin({
|
|
47
|
+
name: '@moxxy/plugin-channel-discord',
|
|
48
|
+
version: '0.0.0',
|
|
49
|
+
...(hooks ? { hooks } : {}),
|
|
50
|
+
channels: [
|
|
51
|
+
defineChannel({
|
|
52
|
+
name: 'discord',
|
|
53
|
+
description: 'Discord bot channel via discord.js (gateway). DM code pairing; streamed replies via message edits; button-based permission prompts.',
|
|
54
|
+
// Like Slack/Telegram, run on a dedicated, isolated runner (separate
|
|
55
|
+
// socket + sticky session) so the bot keeps its own persistent history
|
|
56
|
+
// apart from the user's desktop/TUI work. The gateway is an outbound
|
|
57
|
+
// WebSocket — no tunnel needed.
|
|
58
|
+
dedicatedRunner: true,
|
|
59
|
+
sessionSource: 'discord',
|
|
60
|
+
// Self-described config so a control surface (TUI `/channels`, `moxxy
|
|
61
|
+
// channels start`) can configure + run Discord without a hardcoded table.
|
|
62
|
+
config: {
|
|
63
|
+
fields: [
|
|
64
|
+
{
|
|
65
|
+
name: 'botToken',
|
|
66
|
+
label: 'Bot token',
|
|
67
|
+
vaultKey: DISCORD_TOKEN_KEY,
|
|
68
|
+
required: true,
|
|
69
|
+
secret: true,
|
|
70
|
+
placeholder: 'MTIz…abc.def…',
|
|
71
|
+
help: 'discord.com/developers → your app → Bot → Reset Token. Enable the MESSAGE CONTENT privileged intent on the same page.',
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
hasRequestUrl: false,
|
|
75
|
+
runHint: 'Invite the bot to a server via the link shown, then DM it — it replies with a one-time code; finish with `moxxy discord pair` in a terminal.',
|
|
76
|
+
connect: {
|
|
77
|
+
kind: 'url',
|
|
78
|
+
title: 'Invite the bot',
|
|
79
|
+
hint: 'Open the link to invite the bot to your server, then DM it to receive a pairing code and finish with `moxxy discord pair`.',
|
|
80
|
+
openable: true,
|
|
81
|
+
openLabel: 'Open Discord authorization',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
create: (deps) => new DiscordChannel({
|
|
85
|
+
vault: getVault(),
|
|
86
|
+
token: deps.options?.['token'] ?? undefined,
|
|
87
|
+
logger: deps.logger,
|
|
88
|
+
...(typeof deps.options?.['editFrameMs'] === 'number'
|
|
89
|
+
? { editFrameMs: deps.options['editFrameMs'] }
|
|
90
|
+
: {}),
|
|
91
|
+
}),
|
|
92
|
+
isAvailable: async () => {
|
|
93
|
+
// Env-first: a fully env-configured bot is available even in a probe
|
|
94
|
+
// context (e.g. the `moxxy channels` listing) where onInit has not
|
|
95
|
+
// yet wired the vault service, so `getVault()` would throw.
|
|
96
|
+
if (process.env[DISCORD_TOKEN_ENV]?.trim())
|
|
97
|
+
return { ok: true };
|
|
98
|
+
try {
|
|
99
|
+
if ((await resolveBotToken(getVault())) != null)
|
|
100
|
+
return { ok: true };
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
reason: "No bot token. Set MOXXY_DISCORD_TOKEN, or store one in the vault as '" +
|
|
104
|
+
DISCORD_TOKEN_KEY +
|
|
105
|
+
"' via `moxxy discord setup`.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
reason: 'Set MOXXY_DISCORD_TOKEN to skip the vault, or unlock the vault first.',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
interactiveCommand: 'setup',
|
|
116
|
+
subcommands: {
|
|
117
|
+
setup: {
|
|
118
|
+
description: 'Interactive setup: store a bot token (with privileged-intent guidance), pair an account, then start the bot. Shown by default for `moxxy discord` on a TTY.',
|
|
119
|
+
run: async (ctx) => {
|
|
120
|
+
// The wizard drives token entry + pairing through clack prompts,
|
|
121
|
+
// so it needs an interactive terminal. In a headless invocation
|
|
122
|
+
// we just start the bot directly.
|
|
123
|
+
if (process.stdin.isTTY !== true) {
|
|
124
|
+
return ctx.startChannel();
|
|
125
|
+
}
|
|
126
|
+
return runDiscordWizard(ctx);
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
pair: {
|
|
130
|
+
description: 'DM code pairing: start the bot with a pairing window armed, DM it from your account, then paste the code it replies with.',
|
|
131
|
+
run: async (ctx) => {
|
|
132
|
+
// Pairing needs the operator to paste a code, so it needs an
|
|
133
|
+
// interactive terminal. In a headless invocation we bail with a
|
|
134
|
+
// clear message instead of starting a bot nobody can pair.
|
|
135
|
+
if (process.stdin.isTTY !== true) {
|
|
136
|
+
process.stderr.write('Pairing needs a TTY (you paste the code the bot DMs back). Run `moxxy channels discord pair` on a workstation.\n');
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
return runPairFlow(ctx);
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
unpair: {
|
|
143
|
+
description: 'Forget the currently authorized Discord account.',
|
|
144
|
+
run: async (ctx) => {
|
|
145
|
+
const vault = ctx.deps.vault;
|
|
146
|
+
if (!vault) {
|
|
147
|
+
process.stderr.write('vault unavailable\n');
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
const removed = await vault.delete(DISCORD_AUTHORIZED_USER_KEY);
|
|
151
|
+
process.stdout.write(removed ? 'unpaired\n' : 'no pairing was active\n');
|
|
152
|
+
return 0;
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
status: {
|
|
156
|
+
description: 'Report whether a Discord token + an authorized account + allow-listed channels are configured.',
|
|
157
|
+
run: async (ctx) => {
|
|
158
|
+
const vault = ctx.deps.vault;
|
|
159
|
+
if (!vault) {
|
|
160
|
+
process.stderr.write('vault unavailable\n');
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
const hasToken = !!process.env[DISCORD_TOKEN_ENV]?.trim() || (await vault.has(DISCORD_TOKEN_KEY));
|
|
164
|
+
const authorized = await vault.get(DISCORD_AUTHORIZED_USER_KEY);
|
|
165
|
+
const allowed = parseAllowedChannels(await vault.get(DISCORD_ALLOWED_CHANNELS_KEY));
|
|
166
|
+
process.stdout.write(JSON.stringify({
|
|
167
|
+
tokenConfigured: hasToken,
|
|
168
|
+
authorizedUserId: parseAuthorizedUser(authorized),
|
|
169
|
+
allowedChannelIds: allowed,
|
|
170
|
+
}, null, 2) + '\n');
|
|
171
|
+
return 0;
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault keys + env overrides + validators shared by the channel, its
|
|
3
|
+
* subcommands, and the interactive setup wizard. Kept in their own module so
|
|
4
|
+
* the wizard / pair-flow helpers can import them without pulling in the
|
|
5
|
+
* plugin's full index. The names here are the single source of truth for what
|
|
6
|
+
* the channel reads at boot (mirrored by the desktop channel catalog).
|
|
7
|
+
*/
|
|
8
|
+
import { z } from '@moxxy/sdk';
|
|
9
|
+
/** Vault key for the Discord bot token. */
|
|
10
|
+
export declare const DISCORD_TOKEN_KEY = "discord_bot_token";
|
|
11
|
+
/** Vault key for the paired (authorized) Discord user id — the sole principal
|
|
12
|
+
* allowed to drive the session. */
|
|
13
|
+
export declare const DISCORD_AUTHORIZED_USER_KEY = "discord_authorized_user_id";
|
|
14
|
+
/** Vault key for the guild-channel allow-list (JSON array of channel ids the
|
|
15
|
+
* paired user may drive the bot from, beyond their own DM). */
|
|
16
|
+
export declare const DISCORD_ALLOWED_CHANNELS_KEY = "discord_allowed_channel_ids";
|
|
17
|
+
/**
|
|
18
|
+
* Vault key for the voice-replies preference. Single-paired-account model, so a
|
|
19
|
+
* present-flag (`'1'` = on, absent = off) toggled from a DM / allow-listed
|
|
20
|
+
* channel via `/voice`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const DISCORD_VOICE_REPLIES_KEY = "discord_voice_replies";
|
|
23
|
+
/** Env override for the bot token (beats the vault, matching every other channel). */
|
|
24
|
+
export declare const DISCORD_TOKEN_ENV = "MOXXY_DISCORD_TOKEN";
|
|
25
|
+
/**
|
|
26
|
+
* Shape-only validation of a Discord bot token: three dot-separated url-safe
|
|
27
|
+
* base64 segments (id.timestamp.hmac). Connectivity is proven by the gateway
|
|
28
|
+
* login, not here.
|
|
29
|
+
*/
|
|
30
|
+
export declare const DISCORD_TOKEN_RE: RegExp;
|
|
31
|
+
/** A Discord snowflake id — numeric string, 5..25 digits. */
|
|
32
|
+
export declare const snowflakeSchema: z.ZodString;
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the bot token: env override first, then the vault (the shared
|
|
35
|
+
* env→vault resolution in @moxxy/channel-kit). Returns null when neither is
|
|
36
|
+
* set. Trimmed; never returns an empty string.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveBotToken(vault: {
|
|
39
|
+
get(name: string): Promise<string | null>;
|
|
40
|
+
}): Promise<string | null>;
|
|
41
|
+
/**
|
|
42
|
+
* Parse the stored authorized-user id. Returns null for a missing OR corrupt
|
|
43
|
+
* value (anything that isn't a snowflake) so a bad vault write reads as
|
|
44
|
+
* "unpaired" instead of silently authorizing garbage.
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseAuthorizedUser(raw: string | null | undefined): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* Parse the stored guild-channel allow-list (a JSON array of snowflakes).
|
|
49
|
+
* Corrupt/missing values read as the empty list — DENY every guild channel —
|
|
50
|
+
* rather than throwing or letting junk ids through.
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseAllowedChannels(raw: string | null | undefined): ReadonlyArray<string>;
|
|
53
|
+
/** Serialize the allow-list for the vault (deduplicated, stable order). */
|
|
54
|
+
export declare function serializeAllowedChannels(ids: Iterable<string>): string;
|
|
55
|
+
/** Read the persisted voice-replies preference (`'1'` → on). */
|
|
56
|
+
export declare function loadVoiceReplies(vault: {
|
|
57
|
+
get(name: string): Promise<string | null>;
|
|
58
|
+
}): Promise<boolean>;
|
|
59
|
+
/** Persist the voice-replies preference: store `'1'` when on, remove it when off. */
|
|
60
|
+
export declare function saveVoiceReplies(vault: {
|
|
61
|
+
set(name: string, value: string): Promise<void>;
|
|
62
|
+
delete(name: string): Promise<boolean>;
|
|
63
|
+
}, on: boolean): Promise<void>;
|
|
64
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAG/B,2CAA2C;AAC3C,eAAO,MAAM,iBAAiB,sBAAsB,CAAC;AACrD;oCACoC;AACpC,eAAO,MAAM,2BAA2B,+BAA+B,CAAC;AACxE;gEACgE;AAChE,eAAO,MAAM,4BAA4B,gCAAgC,CAAC;AAC1E;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,0BAA0B,CAAC;AAEjE,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAEvD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAgE,CAAC;AAE9F,6DAA6D;AAC7D,eAAO,MAAM,eAAe,aAAoE,CAAC;AAEjG;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE;IAC3C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3C,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEzB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIjF;AAID;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,aAAa,CAAC,MAAM,CAAC,CAQ1F;AAED,2EAA2E;AAC3E,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAEtE;AAED,gEAAgE;AAChE,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC5C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAC3C,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnB;AAED,qFAAqF;AACrF,wBAAsB,gBAAgB,CACpC,KAAK,EAAE;IACL,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACxC,EACD,EAAE,EAAE,OAAO,GACV,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vault keys + env overrides + validators shared by the channel, its
|
|
3
|
+
* subcommands, and the interactive setup wizard. Kept in their own module so
|
|
4
|
+
* the wizard / pair-flow helpers can import them without pulling in the
|
|
5
|
+
* plugin's full index. The names here are the single source of truth for what
|
|
6
|
+
* the channel reads at boot (mirrored by the desktop channel catalog).
|
|
7
|
+
*/
|
|
8
|
+
import { z } from '@moxxy/sdk';
|
|
9
|
+
import { resolveSecret } from '@moxxy/channel-kit';
|
|
10
|
+
/** Vault key for the Discord bot token. */
|
|
11
|
+
export const DISCORD_TOKEN_KEY = 'discord_bot_token';
|
|
12
|
+
/** Vault key for the paired (authorized) Discord user id — the sole principal
|
|
13
|
+
* allowed to drive the session. */
|
|
14
|
+
export const DISCORD_AUTHORIZED_USER_KEY = 'discord_authorized_user_id';
|
|
15
|
+
/** Vault key for the guild-channel allow-list (JSON array of channel ids the
|
|
16
|
+
* paired user may drive the bot from, beyond their own DM). */
|
|
17
|
+
export const DISCORD_ALLOWED_CHANNELS_KEY = 'discord_allowed_channel_ids';
|
|
18
|
+
/**
|
|
19
|
+
* Vault key for the voice-replies preference. Single-paired-account model, so a
|
|
20
|
+
* present-flag (`'1'` = on, absent = off) toggled from a DM / allow-listed
|
|
21
|
+
* channel via `/voice`.
|
|
22
|
+
*/
|
|
23
|
+
export const DISCORD_VOICE_REPLIES_KEY = 'discord_voice_replies';
|
|
24
|
+
/** Env override for the bot token (beats the vault, matching every other channel). */
|
|
25
|
+
export const DISCORD_TOKEN_ENV = 'MOXXY_DISCORD_TOKEN';
|
|
26
|
+
/**
|
|
27
|
+
* Shape-only validation of a Discord bot token: three dot-separated url-safe
|
|
28
|
+
* base64 segments (id.timestamp.hmac). Connectivity is proven by the gateway
|
|
29
|
+
* login, not here.
|
|
30
|
+
*/
|
|
31
|
+
export const DISCORD_TOKEN_RE = /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{20,}$/;
|
|
32
|
+
/** A Discord snowflake id — numeric string, 5..25 digits. */
|
|
33
|
+
export const snowflakeSchema = z.string().regex(/^\d{5,25}$/, 'expected a Discord snowflake id');
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the bot token: env override first, then the vault (the shared
|
|
36
|
+
* env→vault resolution in @moxxy/channel-kit). Returns null when neither is
|
|
37
|
+
* set. Trimmed; never returns an empty string.
|
|
38
|
+
*/
|
|
39
|
+
export async function resolveBotToken(vault) {
|
|
40
|
+
return resolveSecret(vault, { envVar: DISCORD_TOKEN_ENV, vaultKey: DISCORD_TOKEN_KEY });
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse the stored authorized-user id. Returns null for a missing OR corrupt
|
|
44
|
+
* value (anything that isn't a snowflake) so a bad vault write reads as
|
|
45
|
+
* "unpaired" instead of silently authorizing garbage.
|
|
46
|
+
*/
|
|
47
|
+
export function parseAuthorizedUser(raw) {
|
|
48
|
+
if (!raw)
|
|
49
|
+
return null;
|
|
50
|
+
const trimmed = raw.trim();
|
|
51
|
+
return snowflakeSchema.safeParse(trimmed).success ? trimmed : null;
|
|
52
|
+
}
|
|
53
|
+
const allowedChannelsSchema = z.array(snowflakeSchema);
|
|
54
|
+
/**
|
|
55
|
+
* Parse the stored guild-channel allow-list (a JSON array of snowflakes).
|
|
56
|
+
* Corrupt/missing values read as the empty list — DENY every guild channel —
|
|
57
|
+
* rather than throwing or letting junk ids through.
|
|
58
|
+
*/
|
|
59
|
+
export function parseAllowedChannels(raw) {
|
|
60
|
+
if (!raw)
|
|
61
|
+
return [];
|
|
62
|
+
try {
|
|
63
|
+
const parsed = allowedChannelsSchema.safeParse(JSON.parse(raw));
|
|
64
|
+
return parsed.success ? parsed.data : [];
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Serialize the allow-list for the vault (deduplicated, stable order). */
|
|
71
|
+
export function serializeAllowedChannels(ids) {
|
|
72
|
+
return JSON.stringify([...new Set(ids)]);
|
|
73
|
+
}
|
|
74
|
+
/** Read the persisted voice-replies preference (`'1'` → on). */
|
|
75
|
+
export async function loadVoiceReplies(vault) {
|
|
76
|
+
return (await vault.get(DISCORD_VOICE_REPLIES_KEY)) === '1';
|
|
77
|
+
}
|
|
78
|
+
/** Persist the voice-replies preference: store `'1'` when on, remove it when off. */
|
|
79
|
+
export async function saveVoiceReplies(vault, on) {
|
|
80
|
+
if (on)
|
|
81
|
+
await vault.set(DISCORD_VOICE_REPLIES_KEY, '1');
|
|
82
|
+
else
|
|
83
|
+
await vault.delete(DISCORD_VOICE_REPLIES_KEY);
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=keys.js.map
|
package/dist/keys.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,YAAY,CAAC;AAC/B,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,2CAA2C;AAC3C,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AACrD;oCACoC;AACpC,MAAM,CAAC,MAAM,2BAA2B,GAAG,4BAA4B,CAAC;AACxE;gEACgE;AAChE,MAAM,CAAC,MAAM,4BAA4B,GAAG,6BAA6B,CAAC;AAC1E;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,uBAAuB,CAAC;AAEjE,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,qBAAqB,CAAC;AAEvD;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,6DAA6D,CAAC;AAE9F,6DAA6D;AAC7D,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,YAAY,EAAE,iCAAiC,CAAC,CAAC;AAEjG;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,KAErC;IACC,OAAO,aAAa,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,iBAAiB,EAAE,CAAC,CAAC;AAC1F,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAA8B;IAChE,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,OAAO,eAAe,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACrE,CAAC;AAED,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;AAEvD;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAA8B;IACjE,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,qBAAqB,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAChE,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,wBAAwB,CAAC,GAAqB;IAC5D,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED,gEAAgE;AAChE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,KAEtC;IACC,OAAO,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC,KAAK,GAAG,CAAC;AAC9D,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,KAGC,EACD,EAAW;IAEX,IAAI,EAAE;QAAE,MAAM,KAAK,CAAC,GAAG,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;;QACnD,MAAM,KAAK,CAAC,MAAM,CAAC,yBAAyB,CAAC,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ChannelSubcommandContext } from '@moxxy/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Drive the DM code pairing flow end-to-end in a terminal.
|
|
4
|
+
*
|
|
5
|
+
* Steps:
|
|
6
|
+
* 1. Build a DiscordChannel from the subcommand ctx and wire the session's
|
|
7
|
+
* permission resolver.
|
|
8
|
+
* 2. Start the bot with the pairing window armed (`pair: true`).
|
|
9
|
+
* 3. Tell the user to DM the bot (from the account that should own it) —
|
|
10
|
+
* the bot replies to that DM with a one-time code.
|
|
11
|
+
* 4. Prompt for the code here; on match that account is authorized and
|
|
12
|
+
* persisted.
|
|
13
|
+
* 5. Keep the bot running until Ctrl+C (mirrors the Telegram pair flow).
|
|
14
|
+
*/
|
|
15
|
+
export declare function runPairFlow(ctx: ChannelSubcommandContext): Promise<number>;
|
|
16
|
+
//# sourceMappingURL=pair-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pair-flow.d.ts","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAWnF;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgGhF"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { isCancel, log, outro, spinner, text } from '@clack/prompts';
|
|
2
|
+
import { exitAfterPairRequested } from '@moxxy/sdk';
|
|
3
|
+
import { DiscordChannel } from './channel.js';
|
|
4
|
+
// Tiny zero-dep ANSI dim helper, so this flow stays inside the plugin.
|
|
5
|
+
const ANSI = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
6
|
+
const dim = (s) => (ANSI ? `\x1b[2m${s}\x1b[22m` : s);
|
|
7
|
+
/** How many wrong codes the operator may paste before we give up. */
|
|
8
|
+
const MAX_CODE_ATTEMPTS = 5;
|
|
9
|
+
/**
|
|
10
|
+
* Drive the DM code pairing flow end-to-end in a terminal.
|
|
11
|
+
*
|
|
12
|
+
* Steps:
|
|
13
|
+
* 1. Build a DiscordChannel from the subcommand ctx and wire the session's
|
|
14
|
+
* permission resolver.
|
|
15
|
+
* 2. Start the bot with the pairing window armed (`pair: true`).
|
|
16
|
+
* 3. Tell the user to DM the bot (from the account that should own it) —
|
|
17
|
+
* the bot replies to that DM with a one-time code.
|
|
18
|
+
* 4. Prompt for the code here; on match that account is authorized and
|
|
19
|
+
* persisted.
|
|
20
|
+
* 5. Keep the bot running until Ctrl+C (mirrors the Telegram pair flow).
|
|
21
|
+
*/
|
|
22
|
+
export async function runPairFlow(ctx) {
|
|
23
|
+
const session = ctx.session;
|
|
24
|
+
const channel = new DiscordChannel({
|
|
25
|
+
vault: ctx.deps.vault,
|
|
26
|
+
token: ctx.deps.options?.['token'] ?? undefined,
|
|
27
|
+
logger: ctx.deps.logger,
|
|
28
|
+
});
|
|
29
|
+
session.setPermissionResolver(channel.permissionResolver);
|
|
30
|
+
outro(dim('starting the bot with a pairing window armed...'));
|
|
31
|
+
const handle = await channel.start({ session, pair: true });
|
|
32
|
+
const stopBot = async () => {
|
|
33
|
+
try {
|
|
34
|
+
await handle.stop('wizard');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* ignore */
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
// Already paired (no window armed): nothing to confirm. Tell the user how
|
|
41
|
+
// to re-pair rather than prompting for a code that can never arrive.
|
|
42
|
+
if (channel.connected) {
|
|
43
|
+
log.info('This bot is already paired. Run `moxxy channels discord unpair` first to pair a different account.');
|
|
44
|
+
await stopBot();
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
if (channel.requestUrl) {
|
|
48
|
+
log.info(`Invite the bot to a server (needed once so you can DM it):\n ${channel.requestUrl}`);
|
|
49
|
+
}
|
|
50
|
+
log.info('Now DM the bot from YOUR Discord account (any message). It replies with a one-time code — paste that code below.');
|
|
51
|
+
// Graceful Ctrl-C while waiting (or once running): stop the bot and exit.
|
|
52
|
+
let stopping = false;
|
|
53
|
+
const shutdown = async () => {
|
|
54
|
+
if (stopping)
|
|
55
|
+
return;
|
|
56
|
+
stopping = true;
|
|
57
|
+
await stopBot();
|
|
58
|
+
await session.close('SIGINT').catch(() => undefined);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
};
|
|
61
|
+
const onSignal = () => void shutdown();
|
|
62
|
+
process.once('SIGINT', onSignal);
|
|
63
|
+
process.once('SIGTERM', onSignal);
|
|
64
|
+
try {
|
|
65
|
+
let paired = false;
|
|
66
|
+
for (let attempt = 0; attempt < MAX_CODE_ATTEMPTS && !paired; attempt++) {
|
|
67
|
+
const code = await text({
|
|
68
|
+
message: 'Paste the pairing code from the bot\'s DM reply',
|
|
69
|
+
placeholder: '6-digit code',
|
|
70
|
+
validate: (v) => (!v || !v.trim() ? 'required' : undefined),
|
|
71
|
+
});
|
|
72
|
+
if (isCancel(code)) {
|
|
73
|
+
log.info('Pairing cancelled.');
|
|
74
|
+
await stopBot();
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const result = await channel.confirmPairingCode(String(code));
|
|
78
|
+
if (result.ok) {
|
|
79
|
+
paired = true;
|
|
80
|
+
log.success(`Paired ✓ — Discord user ${result.userId} is authorized.`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
log.warn(result.message);
|
|
84
|
+
}
|
|
85
|
+
if (!paired) {
|
|
86
|
+
log.error('Too many failed attempts — pairing aborted. Run `moxxy discord pair` to retry.');
|
|
87
|
+
await stopBot();
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (exitAfterPairRequested(ctx)) {
|
|
91
|
+
// Orchestrated pairing (`moxxy onboard`): hand control back — the
|
|
92
|
+
// caller starts the bot under its own service afterwards.
|
|
93
|
+
await stopBot();
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
const spin = spinner();
|
|
97
|
+
spin.start('Bot is running. Press Ctrl+C to stop.');
|
|
98
|
+
// Only reached if the bot stops on its own (a signal path exits via
|
|
99
|
+
// shutdown()).
|
|
100
|
+
await handle.running;
|
|
101
|
+
spin.stop('bot stopped.');
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
process.removeListener('SIGINT', onSignal);
|
|
106
|
+
process.removeListener('SIGTERM', onSignal);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=pair-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pair-flow.js","sourceRoot":"","sources":["../src/pair-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type HostCodeState } from '@moxxy/channel-kit';
|
|
2
|
+
/**
|
|
3
|
+
* Discord pairing — the kit's host-code machine, driven over DM in the
|
|
4
|
+
* "bot replies with a code, operator pastes it in the terminal" direction.
|
|
5
|
+
*
|
|
6
|
+
* Flow (the wizard/`moxxy discord pair` arms it):
|
|
7
|
+
* 1. The channel starts with a pairing window ARMED (no code yet).
|
|
8
|
+
* 2. An unauthorized user DMs the bot → we mint a one-time code bound to
|
|
9
|
+
* THAT user ({@link mintCodeForPeer} = `openHostCodeWindow` with the
|
|
10
|
+
* DM-ing user recorded as the pending peer) and DM the code back.
|
|
11
|
+
* 3. The operator pastes the code into the terminal wizard →
|
|
12
|
+
* {@link confirmPendingCode} (`submitPeerCode` against the pending peer).
|
|
13
|
+
* On match the DM-ing user becomes the authorized principal.
|
|
14
|
+
*
|
|
15
|
+
* Security property: the code only ever lives in the candidate's DM, so
|
|
16
|
+
* pasting it in the terminal proves the operator controls (or trusts) that
|
|
17
|
+
* Discord account. A different user DM-ing while armed re-mints the window
|
|
18
|
+
* for themselves — which STALES the earlier code, so the operator's paste of
|
|
19
|
+
* the legitimate code then mismatches and pairing simply retries; an attacker
|
|
20
|
+
* can never be authorized unless the operator pastes the ATTACKER's code
|
|
21
|
+
* (which only exists in the attacker's DM).
|
|
22
|
+
*
|
|
23
|
+
* The transitions live in `@moxxy/channel-kit` (generic over the peer id —
|
|
24
|
+
* a Discord user id string here); this module keeps the Discord-shaped state
|
|
25
|
+
* (armed flag + pending peer) and maps the kit's semantic action kinds to
|
|
26
|
+
* Discord-worded messages.
|
|
27
|
+
*/
|
|
28
|
+
export type DiscordPairingPhase = 'idle' | 'armed' | 'awaiting-host-code' | 'paired' | 'expired';
|
|
29
|
+
export interface DiscordPairingState {
|
|
30
|
+
/** Kit machine state; peer = Discord user id. */
|
|
31
|
+
readonly kit: HostCodeState<string>;
|
|
32
|
+
/** A pairing window is armed (codes will be minted for DM-ing users). */
|
|
33
|
+
readonly armed: boolean;
|
|
34
|
+
/** The user the CURRENT code was minted for (null when none outstanding). */
|
|
35
|
+
readonly pendingUserId: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface DiscordPairingDecision {
|
|
38
|
+
readonly state: DiscordPairingState;
|
|
39
|
+
readonly action: {
|
|
40
|
+
kind: 'code-minted';
|
|
41
|
+
userId: string;
|
|
42
|
+
code: string;
|
|
43
|
+
} | {
|
|
44
|
+
kind: 'paired';
|
|
45
|
+
userId: string;
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'still-paired';
|
|
48
|
+
userId: string;
|
|
49
|
+
} | {
|
|
50
|
+
kind: 'reject';
|
|
51
|
+
message: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'mismatch';
|
|
54
|
+
message: string;
|
|
55
|
+
} | {
|
|
56
|
+
kind: 'not-pending';
|
|
57
|
+
message: string;
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'expired';
|
|
60
|
+
message: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export declare function createDiscordPairingState(opts?: {
|
|
64
|
+
authorizedUserId?: string | null;
|
|
65
|
+
}): DiscordPairingState;
|
|
66
|
+
export declare function pairingPhase(state: DiscordPairingState): DiscordPairingPhase;
|
|
67
|
+
/** Arm the pairing window: DM-ing users will now be issued one-time codes. */
|
|
68
|
+
export declare function armPairing(state: DiscordPairingState): DiscordPairingState;
|
|
69
|
+
export declare function isUserAuthorized(state: DiscordPairingState, userId: string): boolean;
|
|
70
|
+
export declare function clearDiscordPairing(state: DiscordPairingState): DiscordPairingState;
|
|
71
|
+
/**
|
|
72
|
+
* An unauthorized user DMed the bot while a window may be armed. Mints a
|
|
73
|
+
* one-time code bound to that user (re-arming replaces any earlier pending
|
|
74
|
+
* code — see the module doc for why that fails safe) or rejects with the
|
|
75
|
+
* appropriate wording.
|
|
76
|
+
*/
|
|
77
|
+
export declare function mintCodeForPeer(state: DiscordPairingState, userId: string, code?: string): DiscordPairingDecision;
|
|
78
|
+
/**
|
|
79
|
+
* The operator pasted a code into the terminal wizard. Compared against the
|
|
80
|
+
* code minted for the CURRENT pending user; on match that user is authorized.
|
|
81
|
+
*/
|
|
82
|
+
export declare function confirmPendingCode(state: DiscordPairingState, rawCode: string, now?: number): DiscordPairingDecision;
|
|
83
|
+
//# sourceMappingURL=pairing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing.d.ts","sourceRoot":"","sources":["../src/pairing.ts"],"names":[],"mappings":"AACA,OAAO,EAQL,KAAK,aAAa,EACnB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,OAAO,GAAG,oBAAoB,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEjG,MAAM,WAAW,mBAAmB;IAClC,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC;IACpC,QAAQ,CAAC,MAAM,EACX;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GACrD;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAClC;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GACxC;QAAE,IAAI,EAAE,QAAQ,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACnC;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACrC;QAAE,IAAI,EAAE,aAAa,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GACxC;QAAE,IAAI,EAAE,SAAS,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAUD,wBAAgB,yBAAyB,CACvC,IAAI,GAAE;IAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAC9C,mBAAmB,CAMrB;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAK5E;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAE1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAEpF;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,mBAAmB,GAAG,mBAAmB,CAMnF;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,mBAAmB,EAC1B,MAAM,EAAE,MAAM,EACd,IAAI,GAAE,MAAsB,GAC3B,sBAAsB,CAgBxB;AAiCD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,mBAAmB,EAC1B,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAmB,GACvB,sBAAsB,CAMxB"}
|