@lanes-sh/link 0.3.2 → 0.4.1
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/README.md +11 -3
- package/instructions/agents/lanes-link-scout.md +2 -2
- package/instructions/skills/lanes-link/SKILL.md +135 -11
- package/package.json +3 -1
- package/src/cli/argv.ts +52 -0
- package/src/cli/commands/connect/authorise.ts +5 -0
- package/src/cli/commands/connect/custom/ask.ts +167 -0
- package/src/cli/commands/connect/custom/credential.ts +143 -0
- package/src/cli/commands/connect/custom/derive.ts +229 -0
- package/src/cli/commands/connect/custom/index.ts +285 -0
- package/src/cli/commands/connect/custom/prompts.ts +160 -0
- package/src/cli/commands/connect/custom/spec.ts +293 -0
- package/src/cli/commands/connect/custom/values.ts +53 -0
- package/src/cli/commands/connect/custom/write.ts +166 -0
- package/src/cli/commands/connect/grant.ts +27 -0
- package/src/cli/commands/connect/index.ts +24 -27
- package/src/cli/commands/connect/outcome.ts +3 -1
- package/src/cli/commands/connect/requirements.ts +11 -1
- package/src/cli/commands/connect/settle.ts +17 -0
- package/src/cli/commands/connect/setup.ts +9 -1
- package/src/cli/commands/connect/strategy.ts +87 -0
- package/src/cli/commands/connect/unknown.ts +41 -0
- package/src/cli/commands/mcp/list.ts +123 -29
- package/src/cli/identity.ts +29 -5
- package/src/cli/main.ts +42 -14
- package/src/cli/oauth.ts +89 -36
- package/src/cli/runtime/open.ts +13 -1
- package/src/cli/runtime/registry.ts +12 -0
- package/src/cli/selection.ts +12 -0
- package/src/cli/usage.ts +9 -1
- package/src/connectivity/auth/README.md +8 -1
- package/src/connectivity/auth/strategy/index.ts +128 -4
- package/src/connectivity/connector.ts +11 -0
- package/src/connectivity/index.ts +11 -1
- package/src/connectivity/manifest/auth.ts +19 -0
- package/src/connectivity/manifest/connector.ts +21 -0
- package/src/connectivity/manifest/primitives.ts +5 -1
- package/src/connectivity/manifest/provider.ts +30 -12
- package/src/connectivity/provider.ts +55 -0
- package/src/connectivity/transports/factory.ts +1 -0
- package/src/connectivity/transports/http/index.ts +73 -2
- package/src/dispatch/dispatch.ts +44 -5
- package/src/providers/bunq/hints.ts +45 -0
- package/src/providers/bunq/index.ts +87 -0
- package/src/providers/bunq/redact.ts +75 -0
- package/src/providers/bunq/specs/bunq.v1.json +883 -0
- package/src/providers/bunq/specs/vendor.ts +396 -0
- package/src/providers/bunq/strategy/handshake.ts +211 -0
- package/src/providers/bunq/strategy/index.ts +298 -0
- package/src/providers/bunq/strategy/keys.ts +72 -0
- package/src/providers/custom/index.ts +1 -6
- package/src/providers/custom/load.ts +56 -14
- package/src/providers/custom/template.ts +1 -1
- package/src/providers/discord/hints.ts +195 -0
- package/src/providers/discord/index.ts +121 -0
- package/src/providers/discord/redact.ts +99 -0
- package/src/providers/discord/specs/discord.v10.json +2333 -0
- package/src/providers/discord/specs/vendor.ts +164 -0
- package/src/providers/google/specs/vendor.ts +32 -317
- package/src/providers/index.ts +9 -0
- package/src/providers/reddit/index.ts +113 -0
- package/src/providers/reddit/oauth.ts +77 -0
- package/src/providers/reddit/redact.ts +33 -0
- package/src/providers/reddit/scopes.ts +27 -0
- package/src/providers/reddit/specs/reddit.v1.json +700 -0
- package/src/providers/scopes.ts +2 -0
- package/src/providers/shared/openapi.ts +155 -0
- package/src/providers/shared/vendor-operations.ts +179 -0
- package/src/providers/shared/vendor-spec.ts +309 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The documentation, because Discord's spec has none.
|
|
3
|
+
*
|
|
4
|
+
* Their OpenAPI document is machine-generated and carries a `summary` or
|
|
5
|
+
* `description` on 17 of its 242 operations — none of them ours. So
|
|
6
|
+
* `mcp-from-openapi` synthesises `POST /channels/{channel_id}/messages` and that
|
|
7
|
+
* is the *entire* description an agent would otherwise read. A hint is appended
|
|
8
|
+
* to it, which here means a hint is the whole of it.
|
|
9
|
+
*
|
|
10
|
+
* Every operation therefore gets one, including the ones whose names look
|
|
11
|
+
* self-explanatory. Two reasons. The names are self-explanatory to someone who
|
|
12
|
+
* already knows Discord's model, and the model has sharp edges — a channel id is
|
|
13
|
+
* not a channel name, `list_messages` pages backwards, an announcement is only
|
|
14
|
+
* publishable from one kind of channel. And `cli/tools.test.ts` looks each hint
|
|
15
|
+
* up by capability name, so a full set is also the check that all twenty tools
|
|
16
|
+
* generated: a hint keyed to a tool that silently failed to generate fails the
|
|
17
|
+
* suite, which is the one failure the size and shape tests cannot see.
|
|
18
|
+
*
|
|
19
|
+
* Prose costs 3.4 KB of a 192 KB surface budget. It is not the constraint.
|
|
20
|
+
*/
|
|
21
|
+
export const DISCORD_HINTS: Record<string, string> = {
|
|
22
|
+
get_my_user: [
|
|
23
|
+
'Returns the bot application this token belongs to — its id, username, and discriminator.',
|
|
24
|
+
'Costs nothing and needs no permissions, so it is the call to make first when a connection',
|
|
25
|
+
'is misbehaving: a 401 here means the stored token is wrong or is missing its `Bot ` prefix,',
|
|
26
|
+
'and everything else will fail the same way.',
|
|
27
|
+
].join(' '),
|
|
28
|
+
|
|
29
|
+
list_my_guilds: [
|
|
30
|
+
'The servers this bot has been added to — not the servers you are a member of.',
|
|
31
|
+
'A server the bot was never invited to is invisible here and unreachable by every other',
|
|
32
|
+
'tool, so an empty list means the invite step was missed rather than that you have no servers.',
|
|
33
|
+
'Returns id, name, and your permissions in each. Start here to turn a server name into the',
|
|
34
|
+
'id every other call wants.',
|
|
35
|
+
].join(' '),
|
|
36
|
+
|
|
37
|
+
get_guild: [
|
|
38
|
+
'One server by id: name, description, icon, owner, and feature flags.',
|
|
39
|
+
'Pass `with_counts: true` to also get approximate member and presence counts.',
|
|
40
|
+
'Use this to confirm you have the right server before posting to it.',
|
|
41
|
+
].join(' '),
|
|
42
|
+
|
|
43
|
+
list_guild_channels: [
|
|
44
|
+
'Every channel in a server, which is how a channel *name* becomes the `channel_id`',
|
|
45
|
+
'the posting and reading tools require. Check the `type` field before posting:',
|
|
46
|
+
'0 is a normal text channel, 5 is an announcement channel (the only kind',
|
|
47
|
+
'`crosspost_message` works on), 15 is a forum, and 2 and 13 are voice and stage,',
|
|
48
|
+
'which cannot take messages. `parent_id` is the category a channel sits under.',
|
|
49
|
+
].join(' '),
|
|
50
|
+
|
|
51
|
+
get_channel: [
|
|
52
|
+
'One channel by id — its name, type, topic, and which category it belongs to.',
|
|
53
|
+
'Worth calling before a first post to a channel an agent has not written to before,',
|
|
54
|
+
'to confirm it is the one intended and that it takes messages at all.',
|
|
55
|
+
].join(' '),
|
|
56
|
+
|
|
57
|
+
list_messages: [
|
|
58
|
+
'Read a channel\'s recent messages. This is the triage read, and the only one:',
|
|
59
|
+
'Discord exposes no message search to bot applications, so finding something means',
|
|
60
|
+
'paging this and filtering yourself.',
|
|
61
|
+
'Newest first by default, up to `limit: 100` per call. Page *backwards* through history',
|
|
62
|
+
'with `before` set to the oldest id you have seen; page forward for new arrivals with',
|
|
63
|
+
'`after` set to the newest. `around` centres on one message.',
|
|
64
|
+
'If every `content` comes back empty, the Message Content intent is switched off for',
|
|
65
|
+
'this application — that is a toggle in the developer portal, not a permission on the',
|
|
66
|
+
'invite, and nothing else reports it.',
|
|
67
|
+
].join(' '),
|
|
68
|
+
|
|
69
|
+
get_message: [
|
|
70
|
+
'One message by id, with its author, timestamp, attachments, reactions, and any embeds.',
|
|
71
|
+
'Use it to re-read a single message in full after `list_messages` has narrowed things down,',
|
|
72
|
+
'or to check whether an edit or a reaction landed.',
|
|
73
|
+
].join(' '),
|
|
74
|
+
|
|
75
|
+
list_pins: [
|
|
76
|
+
'The pinned messages in a channel. Pinning is how this integration marks something as',
|
|
77
|
+
'handled or important where a human will see it, so this is the read that shows what',
|
|
78
|
+
'has been marked. Note the path is the current one — Discord also serves a deprecated',
|
|
79
|
+
'pins endpoint that is not exposed here.',
|
|
80
|
+
].join(' '),
|
|
81
|
+
|
|
82
|
+
list_message_reactions_by_emoji: [
|
|
83
|
+
'Who reacted to a message with one specific emoji. Pass a Unicode emoji directly',
|
|
84
|
+
'(`emoji_name: "✅"`); a custom server emoji is `name:id`.',
|
|
85
|
+
'Useful for reading a lightweight vote or acknowledgement off a message rather than',
|
|
86
|
+
'asking people to reply.',
|
|
87
|
+
].join(' '),
|
|
88
|
+
|
|
89
|
+
get_active_guild_threads: [
|
|
90
|
+
'Every thread in a server that is not archived, across all channels at once.',
|
|
91
|
+
'Cheaper than walking channels one at a time when the question is "what conversations',
|
|
92
|
+
'are open right now", which is the usual first step of a triage pass.',
|
|
93
|
+
].join(' '),
|
|
94
|
+
|
|
95
|
+
create_message: [
|
|
96
|
+
'Post a message to a channel, as the bot application — it will carry an APP badge',
|
|
97
|
+
'and the application\'s own name and avatar. To post under your own name instead,',
|
|
98
|
+
'use `execute_webhook`.',
|
|
99
|
+
'For a plain message, pass `content` (up to 2000 characters, Discord-flavoured markdown).',
|
|
100
|
+
'For an announcement, prefer `embeds`: an embed gives you a title, a coloured left border,',
|
|
101
|
+
'a description, and up to 25 name/value `fields`, and it is what makes a post read as',
|
|
102
|
+
'deliberate rather than typed. `color` is a decimal integer, not a hex string —',
|
|
103
|
+
'0x5865F2 is 5793266.',
|
|
104
|
+
'Set `allowed_mentions` explicitly on anything that could ping a room:',
|
|
105
|
+
'`{"parse": []}` suppresses every mention even if the text contains @everyone.',
|
|
106
|
+
'Reply to something by passing `message_reference` with the message id.',
|
|
107
|
+
'Attachments are not available here — files need a multipart request, which Discord accepts',
|
|
108
|
+
'and this connector cannot encode.',
|
|
109
|
+
].join(' '),
|
|
110
|
+
|
|
111
|
+
update_message: [
|
|
112
|
+
'Edit a message this application posted. It cannot edit anyone else\'s, including one',
|
|
113
|
+
'posted through a webhook — that needs the webhook\'s own edit endpoint.',
|
|
114
|
+
'This is the typo fix for an announcement already out. Only the fields you pass are',
|
|
115
|
+
'changed; passing `content` alone leaves existing embeds in place, and clearing an',
|
|
116
|
+
'embed means passing `embeds: []` explicitly.',
|
|
117
|
+
'Discord shows an "edited" marker afterwards, which cannot be suppressed.',
|
|
118
|
+
].join(' '),
|
|
119
|
+
|
|
120
|
+
delete_message: [
|
|
121
|
+
'Delete a message — the retraction, for an announcement that should not have gone out.',
|
|
122
|
+
'Irreversible, and it leaves no trace in the channel for anyone who had not already read it.',
|
|
123
|
+
'The application can always delete its own messages; deleting somebody else\'s needs',
|
|
124
|
+
'Manage Messages in that channel.',
|
|
125
|
+
'There is deliberately no bulk delete here: this removes one message named by id, so',
|
|
126
|
+
'clearing a channel is not something an agent can do in one call.',
|
|
127
|
+
].join(' '),
|
|
128
|
+
|
|
129
|
+
crosspost_message: [
|
|
130
|
+
'Publish a message that was posted in an *announcement* channel out to every server',
|
|
131
|
+
'that follows it. This is what makes an announcement channel worth using: subscribers',
|
|
132
|
+
'see the post in their own server without joining yours.',
|
|
133
|
+
'Only works on channel `type: 5`, and only on a message already posted there — so the',
|
|
134
|
+
'sequence is `create_message` then this, with the id it returned.',
|
|
135
|
+
'Rate limited far more tightly than posting, and it cannot be undone.',
|
|
136
|
+
].join(' '),
|
|
137
|
+
|
|
138
|
+
add_my_message_reaction: [
|
|
139
|
+
'React to a message as this application. The cheapest way to mark a message as seen,',
|
|
140
|
+
'triaged, or categorised — a ✅ or 👀 costs nothing, notifies nobody, and is visible',
|
|
141
|
+
'to everyone reading the channel.',
|
|
142
|
+
'Pass a Unicode emoji directly (`emoji_name: "✅"`); a custom server emoji is `name:id`.',
|
|
143
|
+
'Remove one with the matching delete tool, which is not exposed here.',
|
|
144
|
+
].join(' '),
|
|
145
|
+
|
|
146
|
+
create_pin: [
|
|
147
|
+
'Pin a message to its channel, where it stays visible in the channel\'s pinned list.',
|
|
148
|
+
'The heavier alternative to a reaction for marking something important: a pin is',
|
|
149
|
+
'channel-wide and appears in the header, so it is the right weight for "this is the',
|
|
150
|
+
'announcement that matters" and the wrong weight for routine triage.',
|
|
151
|
+
'Needs Manage Messages. A channel holds at most 50 pins.',
|
|
152
|
+
].join(' '),
|
|
153
|
+
|
|
154
|
+
create_thread_from_message: [
|
|
155
|
+
'Start a thread hanging off an existing message, which is how a post becomes a',
|
|
156
|
+
'discussion without cluttering the channel. `name` is the thread title and is required.',
|
|
157
|
+
'`auto_archive_duration` is in minutes and accepts only 60, 1440, 4320, or 10080.',
|
|
158
|
+
'Use this to route a triaged message somewhere people can talk about it, rather than',
|
|
159
|
+
'replying inline where it scrolls away.',
|
|
160
|
+
].join(' '),
|
|
161
|
+
|
|
162
|
+
list_channel_webhooks: [
|
|
163
|
+
'The webhooks already configured on a channel, with their ids and tokens.',
|
|
164
|
+
'Check here before calling `create_webhook`: a channel accumulates a webhook per call',
|
|
165
|
+
'and there is a limit of 15, so reusing the one you made last time is the difference',
|
|
166
|
+
'between a working integration and a channel full of clutter.',
|
|
167
|
+
'Needs Manage Webhooks. The response includes each webhook\'s token, which is a',
|
|
168
|
+
'credential — see the note on `execute_webhook`.',
|
|
169
|
+
].join(' '),
|
|
170
|
+
|
|
171
|
+
create_webhook: [
|
|
172
|
+
'Create a webhook on a channel, which is what makes posting under your own name',
|
|
173
|
+
'possible. Do this once per channel and keep the id and token; call',
|
|
174
|
+
'`list_channel_webhooks` first to find an existing one rather than making another.',
|
|
175
|
+
'`name` is a fallback label only — the name and avatar that actually appear are the',
|
|
176
|
+
'ones passed per message to `execute_webhook`. `avatar` takes a base64 data URI,',
|
|
177
|
+
'not a URL, and is optional.',
|
|
178
|
+
'Needs Manage Webhooks. The returned `token` is a standalone credential: anybody',
|
|
179
|
+
'holding it can post to this channel without any other authentication.',
|
|
180
|
+
].join(' '),
|
|
181
|
+
|
|
182
|
+
execute_webhook: [
|
|
183
|
+
'Post through a webhook, setting the name and avatar on the message itself.',
|
|
184
|
+
'This is how an announcement appears under your own name rather than the',
|
|
185
|
+
'application\'s: pass `username` and `avatar_url` and the message wears them.',
|
|
186
|
+
'It still carries an APP badge — Discord has no way to remove that, and no API for',
|
|
187
|
+
'posting as a real user account.',
|
|
188
|
+
'Takes the same `content` and `embeds` as `create_message`. Pass `wait: true` to get',
|
|
189
|
+
'the created message back, which is the only way to learn its id.',
|
|
190
|
+
'`thread_id` posts into an existing thread in the webhook\'s channel.',
|
|
191
|
+
'Note what this cannot do: a webhook only posts. It cannot read the channel, and the',
|
|
192
|
+
'message it creates cannot be edited by `update_message` — a webhook message is',
|
|
193
|
+
'editable only through the webhook that sent it.',
|
|
194
|
+
].join(' '),
|
|
195
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import { DISCORD_HINTS } from './hints.ts';
|
|
3
|
+
import { DISCORD_REDACT } from './redact.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Discord asks callers to identify themselves and blocks the ones that do not.
|
|
7
|
+
*
|
|
8
|
+
* Their documented format is `DiscordBot ($url, $version)`, and a request with a
|
|
9
|
+
* generic or absent agent is refused at the edge rather than by the API — so it
|
|
10
|
+
* presents as a network failure with no JSON to read. `connector.headers` on an
|
|
11
|
+
* `http` connector is what carries it (ADR-045 added the field for the same
|
|
12
|
+
* problem at Reddit).
|
|
13
|
+
*
|
|
14
|
+
* No handle or address in it: this file is public, and `architecture.test.ts`
|
|
15
|
+
* refuses a real identifier anywhere a reader can see. A name and a URL are what
|
|
16
|
+
* the check actually looks for.
|
|
17
|
+
*/
|
|
18
|
+
const DISCORD_USER_AGENT = 'DiscordBot (https://lanes.sh/link, 0.3)';
|
|
19
|
+
|
|
20
|
+
/** The vendored spec sits beside this file. See `specs/vendor.ts`. */
|
|
21
|
+
function specPath(name: string): string {
|
|
22
|
+
return new URL(`./specs/${name}`, import.meta.url).pathname;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Discord's v10 HTTP API, reached with a bot token the operator pasted.
|
|
27
|
+
*
|
|
28
|
+
* There is no user-account path and it is not for want of looking. Discord
|
|
29
|
+
* publishes no API for acting as yourself: automating a user token is
|
|
30
|
+
* self-botting, which their terms forbid and which gets accounts terminated, and
|
|
31
|
+
* the OAuth2 user scopes cover neither posting to a channel nor reading its
|
|
32
|
+
* history. Everything an integration can legitimately do, it does as an
|
|
33
|
+
* application. So a message will always carry an APP badge — that part is not
|
|
34
|
+
* negotiable — but the *name and avatar* on it are, per message, through a
|
|
35
|
+
* webhook. `execute_webhook` is why the three webhook operations are vendored:
|
|
36
|
+
* an announcement can read as the operator while an ordinary reply reads as the
|
|
37
|
+
* integration, from the one token.
|
|
38
|
+
*
|
|
39
|
+
* Not OAuth, and for a reason of its own rather than the one ADR-033 gave. That
|
|
40
|
+
* reason — a vendor matching its callback URL exactly, against a port the kernel
|
|
41
|
+
* picks — was withdrawn by ADR-045, which added `auth.redirect_uri`; so it is no
|
|
42
|
+
* longer an argument for a pasted token anywhere. What holds here is narrower and
|
|
43
|
+
* not fixable on our side: a Discord OAuth2 flow with the `bot` scope installs an
|
|
44
|
+
* application into a server, but the credential that then calls the API is the
|
|
45
|
+
* *application's* bot token, a property of the app rather than anything the
|
|
46
|
+
* exchange returns — so a browser flow would end with the operator still copying
|
|
47
|
+
* a token out of the developer portal. The `webhook.incoming` scope does return a
|
|
48
|
+
* usable credential, and it is write-only to a single channel: it cannot read, so
|
|
49
|
+
* it cannot serve the half of this that is triage.
|
|
50
|
+
*
|
|
51
|
+
* `auth.kind` is `header` rather than `bearer` because Discord's scheme word is
|
|
52
|
+
* `Bot`, not `Bearer`, and a bearer credential is assembled as `Bearer <token>`
|
|
53
|
+
* with no way to say otherwise. `header` writes the stored value into
|
|
54
|
+
* `Authorization` verbatim, so the value the operator pastes carries its own
|
|
55
|
+
* scheme — `Bot MTIz…`. That keeps the whole provider inside this folder at the
|
|
56
|
+
* cost of a prefix somebody can forget, which is why the prompt label spells it
|
|
57
|
+
* out and `troubleshooting` names it as the first thing to check.
|
|
58
|
+
*
|
|
59
|
+
* No `identity` block, and its absence is deliberate rather than an omission.
|
|
60
|
+
* The probe sends `Authorization: Bearer <stored value>`, which for this
|
|
61
|
+
* provider would be `Bearer Bot MTIz…` — a 401 and a null, after a network round
|
|
62
|
+
* trip, every single connect. Naming the connection falls to the operator
|
|
63
|
+
* instead. Re-running `connect` with the *same* label repairs the existing
|
|
64
|
+
* connection rather than adding a second one, because `settleIdentity` matches
|
|
65
|
+
* on the label; a scripted run passes `--display-name`.
|
|
66
|
+
*/
|
|
67
|
+
export const discord = defineProvider({
|
|
68
|
+
id: 'discord',
|
|
69
|
+
name: 'Discord',
|
|
70
|
+
description:
|
|
71
|
+
'Post announcements, read channels, and triage messages in the servers your bot has been added to, via the Discord v10 HTTP API.',
|
|
72
|
+
connector: {
|
|
73
|
+
kind: 'http',
|
|
74
|
+
base_url: 'https://discord.com/api/v10',
|
|
75
|
+
// Vendored, not fetched, and doubly so here: upstream is a public preview
|
|
76
|
+
// Discord says may break without notice, and `connect` grants everything a
|
|
77
|
+
// provider discovers. See `specs/vendor.ts`.
|
|
78
|
+
openapi: specPath('discord.v10.json'),
|
|
79
|
+
headers: { 'User-Agent': DISCORD_USER_AGENT },
|
|
80
|
+
},
|
|
81
|
+
auth: { kind: 'header', header: 'Authorization' },
|
|
82
|
+
redact: DISCORD_REDACT,
|
|
83
|
+
hints: DISCORD_HINTS,
|
|
84
|
+
setup: {
|
|
85
|
+
summary:
|
|
86
|
+
'Discord authenticates an integration as an application, not as you. You create one in ' +
|
|
87
|
+
'their developer portal, copy its bot token, and invite it to the servers you want ' +
|
|
88
|
+
'reachable. Posts will carry an APP badge; the name and avatar on them are yours to set. ' +
|
|
89
|
+
'You are asked for the token once.',
|
|
90
|
+
docs: 'docs/detailed/setup/discord.md',
|
|
91
|
+
docs_url: 'https://discord.com/developers/applications',
|
|
92
|
+
steps: [
|
|
93
|
+
'Open https://discord.com/developers/applications and choose "New Application". Name it whatever you want the posts to read as — this is the name people will see.',
|
|
94
|
+
'On the General Information page, set the icon. That is the avatar posts will carry.',
|
|
95
|
+
'Open the Bot tab. Set the username, then under "Privileged Gateway Intents" switch on MESSAGE CONTENT — without it every message you read comes back with an empty body, and nothing else will tell you why. An app in fewer than 10,000 servers can just toggle it; there is no review.',
|
|
96
|
+
'Still on the Bot tab, choose "Reset Token" and copy what it shows you. Discord shows it once. Paste it below prefixed with the word Bot and a space, exactly as: Bot MTIzNDU2Nzg5.abcdef',
|
|
97
|
+
'Turn off "Public Bot" on the same page unless you want strangers adding it to their servers.',
|
|
98
|
+
'Now invite it. Take the Application ID from General Information and open: https://discord.com/oauth2/authorize?client_id=<application-id>&scope=bot&permissions=309774593088',
|
|
99
|
+
'Pick a server you own and authorise. Those permission bits are exactly what the vendored operations need: view channels, send messages, manage messages (for pinning), read message history, add reactions, create public threads, send in threads, and manage webhooks. Repeat per server.',
|
|
100
|
+
'A private channel needs the bot added to it as well — server-wide permissions do not reach a channel it cannot see.',
|
|
101
|
+
'To rotate: "Reset Token" in the portal, then run: lanes link connect discord --replace',
|
|
102
|
+
],
|
|
103
|
+
troubleshooting:
|
|
104
|
+
'Discord refused the token. Check the prefix first — the stored value must begin with ' +
|
|
105
|
+
'"Bot " and a token pasted bare is the usual cause of a 401 that names nothing. ' +
|
|
106
|
+
'After that: a token invalidated by a later "Reset Token", or an application that was ' +
|
|
107
|
+
'deleted. If calls authenticate but a channel 403s, the bot is in the server but not ' +
|
|
108
|
+
'that channel, or the invite was authorised without the permissions above. If reads ' +
|
|
109
|
+
'succeed and every message body is empty, the MESSAGE CONTENT intent is off. ' +
|
|
110
|
+
'Reset the token at https://discord.com/developers/applications and re-run: ' +
|
|
111
|
+
'lanes link connect discord --replace',
|
|
112
|
+
prompts: [
|
|
113
|
+
{
|
|
114
|
+
key: 'token',
|
|
115
|
+
label: 'Discord bot token, prefixed — Bot <token>',
|
|
116
|
+
secret: true,
|
|
117
|
+
scope: 'connection' as const,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What survives into the audit log when Discord is read or posted to.
|
|
3
|
+
*
|
|
4
|
+
* The default withholds every value, which for a write log means recording that
|
|
5
|
+
* something was posted somewhere without recording where — so every operation
|
|
6
|
+
* here names its identifiers explicitly. Snowflake ids are kept throughout:
|
|
7
|
+
* which guild, which channel, which message, which webhook. Pagination cursors
|
|
8
|
+
* and limits are kept too, because "read 50 messages after this one" is the
|
|
9
|
+
* whole shape of a triage pass and none of it is anybody's content.
|
|
10
|
+
*
|
|
11
|
+
* Message bodies are withheld. `content`, `embeds`, `components`, `attachments`,
|
|
12
|
+
* `poll`, and a thread's `name` are the operator's own words, and a log that
|
|
13
|
+
* reproduces them is a second copy of the channel.
|
|
14
|
+
*
|
|
15
|
+
* Two departures worth stating.
|
|
16
|
+
*
|
|
17
|
+
* `allowed_mentions` is **kept** on every operation that posts. It is not
|
|
18
|
+
* content, it is the blast radius: whether a post was allowed to ping
|
|
19
|
+
* `@everyone` is exactly the fact an operator wants in the log, and it is
|
|
20
|
+
* unrecoverable from anywhere else once the message is edited or deleted.
|
|
21
|
+
*
|
|
22
|
+
* `username` is **kept** on `execute_webhook` and `webhook_token` is not.
|
|
23
|
+
* Keeping the username is what makes the log legible — a webhook post is
|
|
24
|
+
* deliberately wearing a name that is not the app's, and "posted as Ops in
|
|
25
|
+
* channel 123" answers the question the entry exists for. The token is a
|
|
26
|
+
* standalone credential for posting to that channel, so it is withheld by
|
|
27
|
+
* omission and appears as a type marker; `avatar_url` goes the same way for
|
|
28
|
+
* being cosmetic rather than a fact about what happened.
|
|
29
|
+
*/
|
|
30
|
+
export const DISCORD_REDACT: Record<string, string[]> = {
|
|
31
|
+
// Reads. Everything an argument can be here is an id, a cursor, or a count.
|
|
32
|
+
get_my_user: [],
|
|
33
|
+
list_my_guilds: ['before', 'after', 'limit', 'with_counts'],
|
|
34
|
+
get_guild: ['guild_id', 'with_counts'],
|
|
35
|
+
list_guild_channels: ['guild_id'],
|
|
36
|
+
get_channel: ['channel_id'],
|
|
37
|
+
list_messages: ['channel_id', 'around', 'before', 'after', 'limit'],
|
|
38
|
+
get_message: ['channel_id', 'message_id'],
|
|
39
|
+
list_pins: ['channel_id', 'before', 'limit'],
|
|
40
|
+
list_message_reactions_by_emoji: [
|
|
41
|
+
'channel_id',
|
|
42
|
+
'message_id',
|
|
43
|
+
'emoji_name',
|
|
44
|
+
'after',
|
|
45
|
+
'limit',
|
|
46
|
+
'type',
|
|
47
|
+
],
|
|
48
|
+
get_active_guild_threads: ['guild_id'],
|
|
49
|
+
|
|
50
|
+
// Posting. `content`, `embeds`, `components`, `attachments`, `poll`, and
|
|
51
|
+
// `shared_client_theme` are withheld by omission.
|
|
52
|
+
create_message: [
|
|
53
|
+
'channel_id',
|
|
54
|
+
'allowed_mentions',
|
|
55
|
+
'message_reference',
|
|
56
|
+
'sticker_ids',
|
|
57
|
+
'flags',
|
|
58
|
+
'tts',
|
|
59
|
+
'nonce',
|
|
60
|
+
'enforce_nonce',
|
|
61
|
+
],
|
|
62
|
+
update_message: [
|
|
63
|
+
'channel_id',
|
|
64
|
+
'message_id',
|
|
65
|
+
'allowed_mentions',
|
|
66
|
+
'sticker_ids',
|
|
67
|
+
'flags',
|
|
68
|
+
],
|
|
69
|
+
delete_message: ['channel_id', 'message_id'],
|
|
70
|
+
crosspost_message: ['channel_id', 'message_id'],
|
|
71
|
+
|
|
72
|
+
// Triage marking. An emoji is a reaction, not private content, and which one
|
|
73
|
+
// was used is the entire meaning of the action.
|
|
74
|
+
add_my_message_reaction: ['channel_id', 'message_id', 'emoji_name'],
|
|
75
|
+
create_pin: ['channel_id', 'message_id'],
|
|
76
|
+
// `name` withheld: a thread title is the operator's words.
|
|
77
|
+
create_thread_from_message: [
|
|
78
|
+
'channel_id',
|
|
79
|
+
'message_id',
|
|
80
|
+
'auto_archive_duration',
|
|
81
|
+
'rate_limit_per_user',
|
|
82
|
+
],
|
|
83
|
+
|
|
84
|
+
// Webhooks. `avatar` on creation is base64 image bytes; withholding it is the
|
|
85
|
+
// same call `gmail.send_message` makes about attachments.
|
|
86
|
+
list_channel_webhooks: ['channel_id'],
|
|
87
|
+
create_webhook: ['channel_id', 'name'],
|
|
88
|
+
execute_webhook: [
|
|
89
|
+
'webhook_id',
|
|
90
|
+
'username',
|
|
91
|
+
'thread_id',
|
|
92
|
+
'allowed_mentions',
|
|
93
|
+
'applied_tags',
|
|
94
|
+
'with_components',
|
|
95
|
+
'wait',
|
|
96
|
+
'flags',
|
|
97
|
+
'tts',
|
|
98
|
+
],
|
|
99
|
+
};
|