@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,113 @@
|
|
|
1
|
+
import { defineProvider } from '#connectivity';
|
|
2
|
+
import {
|
|
3
|
+
REDDIT_APP,
|
|
4
|
+
REDDIT_REDIRECT_URI,
|
|
5
|
+
REDDIT_SCOPES,
|
|
6
|
+
REDDIT_USER_AGENT,
|
|
7
|
+
specPath,
|
|
8
|
+
} from './oauth.ts';
|
|
9
|
+
import { REDDIT_REDACT } from './redact.ts';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reddit, through its own REST API.
|
|
13
|
+
*
|
|
14
|
+
* No MCP server exists to proxy and no OpenAPI document is published, so the
|
|
15
|
+
* spec beside this file is hand-authored — fifteen operations chosen rather
|
|
16
|
+
* than a whole API filtered down. That is a real cost and it buys the thing
|
|
17
|
+
* ADR-008 is about: the operations still become capabilities mechanically, and
|
|
18
|
+
* there is no per-endpoint translation code anywhere in this folder.
|
|
19
|
+
*
|
|
20
|
+
* Reddit was the first provider to need three things the transports did not
|
|
21
|
+
* have — a form-encoded request body, a `User-Agent`, and an OAuth redirect on
|
|
22
|
+
* a port fixed in advance. All three are now general, because each was a gap
|
|
23
|
+
* rather than a Reddit quirk: the last one is the exact reason `../github`
|
|
24
|
+
* gives for using a pasted token instead of OAuth.
|
|
25
|
+
*/
|
|
26
|
+
export const reddit = defineProvider({
|
|
27
|
+
id: 'reddit',
|
|
28
|
+
name: 'Reddit',
|
|
29
|
+
description:
|
|
30
|
+
'Read subreddits, posts, comments, and search, and post, comment, vote, and edit as your account, via the Reddit API.',
|
|
31
|
+
connector: {
|
|
32
|
+
kind: 'http',
|
|
33
|
+
// Host only. Reddit puts no version in the path, and this must equal the
|
|
34
|
+
// spec's `servers[0].url` — `cli/tools.test.ts` checks that they agree.
|
|
35
|
+
base_url: 'https://oauth.reddit.com',
|
|
36
|
+
openapi: specPath('reddit.v1.json'),
|
|
37
|
+
headers: { 'User-Agent': REDDIT_USER_AGENT },
|
|
38
|
+
},
|
|
39
|
+
auth: {
|
|
40
|
+
kind: 'oauth',
|
|
41
|
+
registration: 'manual',
|
|
42
|
+
app: REDDIT_APP,
|
|
43
|
+
/**
|
|
44
|
+
* `www.reddit.com` for the grant, `oauth.reddit.com` for the API. Mixing
|
|
45
|
+
* the two 404s, and the 404 says nothing about which half was wrong.
|
|
46
|
+
*/
|
|
47
|
+
authorize_url: 'https://www.reddit.com/api/v1/authorize',
|
|
48
|
+
token_url: 'https://www.reddit.com/api/v1/access_token',
|
|
49
|
+
redirect_uri: REDDIT_REDIRECT_URI,
|
|
50
|
+
scopes: [...REDDIT_SCOPES],
|
|
51
|
+
/**
|
|
52
|
+
* Without `duration=permanent` Reddit returns an access token and no
|
|
53
|
+
* refresh token. The connection then works for exactly one hour and stops,
|
|
54
|
+
* which is a miserable thing to debug an hour after a successful connect —
|
|
55
|
+
* the same failure `authorize_params` was added for, where Google needs
|
|
56
|
+
* `access_type=offline`.
|
|
57
|
+
*/
|
|
58
|
+
authorize_params: { duration: 'permanent' },
|
|
59
|
+
revoke_url: 'https://www.reddit.com/api/v1/revoke_token',
|
|
60
|
+
},
|
|
61
|
+
identity: { kind: 'http', url: 'https://oauth.reddit.com/api/v1/me', field: 'name' },
|
|
62
|
+
redact: REDDIT_REDACT,
|
|
63
|
+
/**
|
|
64
|
+
* Said once here rather than left for the tool descriptions to imply.
|
|
65
|
+
*
|
|
66
|
+
* Both are things an agent gets wrong on the first attempt and cannot learn
|
|
67
|
+
* from the error: Reddit answers a bare id with a generic failure, and a
|
|
68
|
+
* subreddit that requires a flair rejects the submission without saying that
|
|
69
|
+
* a flair was what was missing.
|
|
70
|
+
*/
|
|
71
|
+
hints: {
|
|
72
|
+
add_comment:
|
|
73
|
+
'thing_id must be a fullname — t3_<id> for a post, t1_<id> for a comment — not the bare id that appears in a URL.',
|
|
74
|
+
submit_post:
|
|
75
|
+
'Many subreddits reject a submission with no flair. Call list_flairs first and pass flair_id, and read get_rules when posting somewhere unfamiliar.',
|
|
76
|
+
},
|
|
77
|
+
setup: {
|
|
78
|
+
summary:
|
|
79
|
+
'Reddit needs an app of your own, which takes a couple of minutes in a browser. There is no shared ' +
|
|
80
|
+
'client for this one on purpose: Reddit rate-limits per client id, so a client everyone shared would ' +
|
|
81
|
+
'mean strangers using up your hundred requests a minute. Your own app gets its own budget.',
|
|
82
|
+
docs: 'docs/detailed/setup/reddit.md',
|
|
83
|
+
docs_url: 'https://www.reddit.com/prefs/apps',
|
|
84
|
+
steps: [
|
|
85
|
+
'Open https://www.reddit.com/prefs/apps and choose "create another app...".',
|
|
86
|
+
'Name it something you will recognise later — the name is how you revoke this one without touching your others.',
|
|
87
|
+
'Choose "web app". "script" only ever reaches your own account, and "installed app" has no secret for this to hold.',
|
|
88
|
+
`Set the redirect uri to exactly ${REDDIT_REDIRECT_URI} — Reddit matches it character for character, and this is the address this command listens on.`,
|
|
89
|
+
'Create the app. The client id is the string under the app name; the secret is the field labelled "secret".',
|
|
90
|
+
'Reddit also asks you to register for API access separately, from the link on that page. Creating the app is not the same as being allowed to call the API.',
|
|
91
|
+
'If you regenerate the secret later, run: lanes link connect reddit --replace.',
|
|
92
|
+
],
|
|
93
|
+
troubleshooting:
|
|
94
|
+
'Reddit refused the connection. If the browser said "invalid redirect_uri", the value registered on the app ' +
|
|
95
|
+
`is not exactly ${REDDIT_REDIRECT_URI}. If the call failed after connecting, the app exists but API access ` +
|
|
96
|
+
'has not been granted — that is a separate registration, linked from https://www.reddit.com/prefs/apps. ' +
|
|
97
|
+
'A 429 means the hundred-per-minute limit for this client id was reached.',
|
|
98
|
+
prompts: [
|
|
99
|
+
{
|
|
100
|
+
key: 'client_id',
|
|
101
|
+
label: 'Reddit client ID',
|
|
102
|
+
secret: false,
|
|
103
|
+
credential_ref: `${REDDIT_APP}/client_id`,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: 'client_secret',
|
|
107
|
+
label: 'Reddit client secret',
|
|
108
|
+
secret: true,
|
|
109
|
+
credential_ref: `${REDDIT_APP}/client_secret`,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Reddit client, and why it is the operator's rather than ours.
|
|
3
|
+
*
|
|
4
|
+
* Every other pre-registered provider here reaches for the broker ADR-028
|
|
5
|
+
* built: one client Lanes operates, so nobody visits a console. That is the
|
|
6
|
+
* right default and it is the wrong answer for Reddit, for a reason particular
|
|
7
|
+
* to how Reddit meters access.
|
|
8
|
+
*
|
|
9
|
+
* Reddit's rate limit is a hundred queries a minute *per OAuth client id* —
|
|
10
|
+
* not per user, not per token. A shared client would pool every install of this
|
|
11
|
+
* program into one bucket, so the limit would be reached by strangers and the
|
|
12
|
+
* failure would arrive as somebody else's traffic. An operator registering
|
|
13
|
+
* their own gets their own hundred, which is the whole budget for one person
|
|
14
|
+
* and nowhere near it for the next.
|
|
15
|
+
*
|
|
16
|
+
* The cost is a console visit, which is what the broker exists to avoid. It is
|
|
17
|
+
* worth paying once here because the alternative degrades with every new user,
|
|
18
|
+
* and a shared limit is not a thing a later change could unpick.
|
|
19
|
+
*/
|
|
20
|
+
export const REDDIT_APP = 'reddit';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How this program names itself to Reddit.
|
|
24
|
+
*
|
|
25
|
+
* Reddit asks callers to identify themselves and throttles the default agent
|
|
26
|
+
* hardest — a client that does not set one is treated as a scraper, which
|
|
27
|
+
* surfaces as sporadic 429s rather than as a refusal anybody can read. Nothing
|
|
28
|
+
* else in this repository sets a `User-Agent`, so `connector.headers` is what
|
|
29
|
+
* carries it.
|
|
30
|
+
*
|
|
31
|
+
* Reddit's documented format ends with the author's handle. That is omitted
|
|
32
|
+
* deliberately: this file is public, a handle is a real identifier, and
|
|
33
|
+
* `architecture.test.ts` refuses one anywhere a reader can see. A descriptive
|
|
34
|
+
* name and a URL satisfy what the throttle actually looks for.
|
|
35
|
+
*/
|
|
36
|
+
export const REDDIT_USER_AGENT = 'lanes-link/0.3 (+https://lanes.sh/link)';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Where the browser comes back to, named to Reddit exactly as written.
|
|
40
|
+
*
|
|
41
|
+
* Reddit matches `redirect_uri` byte for byte, and `connect` normally listens
|
|
42
|
+
* on whatever port the kernel hands out — so a URL registered in the console
|
|
43
|
+
* months earlier could never match. Declaring it pins the listener to this
|
|
44
|
+
* port instead. See `cli/oauth.ts` and ADR-045.
|
|
45
|
+
*
|
|
46
|
+
* The port is high and otherwise unused, which is the only property it needs:
|
|
47
|
+
* nothing else in this program listens here, and the operator types the same
|
|
48
|
+
* string into Reddit's form.
|
|
49
|
+
*/
|
|
50
|
+
export const REDDIT_REDIRECT_URI = 'http://127.0.0.1:8765/callback';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What the browser grant asks for.
|
|
54
|
+
*
|
|
55
|
+
* `identity` and `read` carry the reads; `submit` posts and comments; `edit`,
|
|
56
|
+
* `vote`, `save`, and `flair` cover what an author does to their own content
|
|
57
|
+
* afterwards. `mysubreddits` is what lists the subreddits this account follows.
|
|
58
|
+
*
|
|
59
|
+
* Absent on purpose: `privatemessages`, `history`, and every `mod*` scope.
|
|
60
|
+
* Nothing vendored needs them, and a scope on the consent screen that no tool
|
|
61
|
+
* can use is a grant asked for and never spent.
|
|
62
|
+
*/
|
|
63
|
+
export const REDDIT_SCOPES = [
|
|
64
|
+
'identity',
|
|
65
|
+
'read',
|
|
66
|
+
'submit',
|
|
67
|
+
'edit',
|
|
68
|
+
'vote',
|
|
69
|
+
'save',
|
|
70
|
+
'flair',
|
|
71
|
+
'mysubreddits',
|
|
72
|
+
] as const;
|
|
73
|
+
|
|
74
|
+
/** Resolve a vendored spec beside this folder. */
|
|
75
|
+
export function specPath(name: string): string {
|
|
76
|
+
return new URL(`./specs/${name}`, import.meta.url).pathname;
|
|
77
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What survives into the audit log when Reddit is written to.
|
|
3
|
+
*
|
|
4
|
+
* The line is Gmail's and Slack's, because it is the same object: something a
|
|
5
|
+
* person wrote for other people to read. Where it went is recorded, what it
|
|
6
|
+
* said is not. A log that quoted the body would be a second copy of everything
|
|
7
|
+
* this account has ever posted, held somewhere nobody expects one.
|
|
8
|
+
*
|
|
9
|
+
* `title` is withheld along with the body, which is worth saying because it
|
|
10
|
+
* looks like metadata and is not — a Reddit title is usually the whole of the
|
|
11
|
+
* post, and the body is often empty. Keeping it would defeat the rest.
|
|
12
|
+
*
|
|
13
|
+
* Keys are the *shortened* names. `shortenName` strips a redundant provider
|
|
14
|
+
* prefix, so these must match what the capability is finally called — keying
|
|
15
|
+
* this block on anything else matches nothing and withholds everything,
|
|
16
|
+
* silently and with the log still looking correct.
|
|
17
|
+
*/
|
|
18
|
+
export const REDDIT_REDACT: Record<string, string[]> = {
|
|
19
|
+
// Everything that says where the post went and how it was marked, and nothing
|
|
20
|
+
// that says what it said. `url` is withheld with the body: for kind="link"
|
|
21
|
+
// the URL *is* the submission.
|
|
22
|
+
submit_post: ['sr', 'kind', 'flair_id', 'nsfw', 'spoiler', 'sendreplies', 'api_type'],
|
|
23
|
+
add_comment: ['thing_id', 'api_type'],
|
|
24
|
+
edit_text: ['thing_id', 'api_type'],
|
|
25
|
+
// Kept whole. Every argument is an identifier or a value from a fixed
|
|
26
|
+
// vocabulary, and an entry recording that something was voted on without
|
|
27
|
+
// recording which way records nothing anyone would look for — the reading
|
|
28
|
+
// that lets Slack keep an emoji name.
|
|
29
|
+
vote: ['id', 'dir'],
|
|
30
|
+
delete_thing: ['id'],
|
|
31
|
+
save_thing: ['id', 'category'],
|
|
32
|
+
set_flair: ['sr', 'link', 'flair_template_id', 'api_type'],
|
|
33
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ScopeMeaning } from '../scopes.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reddit's scopes, in Reddit's own words.
|
|
5
|
+
*
|
|
6
|
+
* Taken verbatim from `https://www.reddit.com/api/v1/scopes`, which publishes a
|
|
7
|
+
* description per scope, rather than paraphrased. A paraphrase is where a
|
|
8
|
+
* consent screen quietly starts understating what it asks for, and the vendor's
|
|
9
|
+
* own sentence is the one that can be checked against the vendor.
|
|
10
|
+
*
|
|
11
|
+
* Three are marked broad, and all three for the same reason: they act publicly
|
|
12
|
+
* as the person. A post, a comment, a vote, and an edit are visible under their
|
|
13
|
+
* username to everyone who reads the subreddit, and unlike a private write they
|
|
14
|
+
* cannot be quietly undone — `delete` leaves the fact of the deletion behind,
|
|
15
|
+
* and anything cached or quoted stays. `read` is not marked: it reaches only
|
|
16
|
+
* what the account can already see, and Reddit's readable surface is public.
|
|
17
|
+
*/
|
|
18
|
+
export const REDDIT_SCOPE_MEANINGS: Record<string, ScopeMeaning> = {
|
|
19
|
+
identity: { meaning: 'read your username and signup date' },
|
|
20
|
+
read: { meaning: 'read posts and comments' },
|
|
21
|
+
submit: { meaning: 'post and comment publicly as you', broad: true },
|
|
22
|
+
edit: { meaning: 'edit and delete your posts and comments', broad: true },
|
|
23
|
+
vote: { meaning: 'vote on posts and comments as you', broad: true },
|
|
24
|
+
save: { meaning: 'save and unsave posts and comments' },
|
|
25
|
+
flair: { meaning: 'set the flair on your posts' },
|
|
26
|
+
mysubreddits: { meaning: 'list the subreddits you belong to' },
|
|
27
|
+
};
|