@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
package/src/dispatch/dispatch.ts
CHANGED
|
@@ -6,8 +6,14 @@ import type { RuntimeState } from '#stores/state';
|
|
|
6
6
|
import type { PolicyDocument } from '#policy';
|
|
7
7
|
import { RateLimiter, evaluate } from '#policy';
|
|
8
8
|
import type { BlobStore } from '#stores/blobs';
|
|
9
|
-
import type {
|
|
10
|
-
|
|
9
|
+
import type {
|
|
10
|
+
AnyConnector,
|
|
11
|
+
AuthStrategyContext,
|
|
12
|
+
CapabilityResult,
|
|
13
|
+
ConnectorContext,
|
|
14
|
+
Logger,
|
|
15
|
+
} from '#connectivity';
|
|
16
|
+
import { isToolResult, strategyContextFrom, strategyFor } from '#connectivity';
|
|
11
17
|
import type { Config } from '#profile';
|
|
12
18
|
import { buildProviderContext, createProviderLogger } from './context.ts';
|
|
13
19
|
import { stageAttachment, type StagedAttachment, type StageRequest } from './staging.ts';
|
|
@@ -252,13 +258,38 @@ export class Dispatcher {
|
|
|
252
258
|
},
|
|
253
259
|
};
|
|
254
260
|
|
|
261
|
+
// A provider whose authentication is code rather than a manifest field.
|
|
262
|
+
// The strategy stands in for the credential resolver completely: it holds
|
|
263
|
+
// whatever session the vendor issues, signs the outbound request, and
|
|
264
|
+
// checks the reply. Looked up once, because both halves of the connector
|
|
265
|
+
// context come from the same one.
|
|
266
|
+
const strategy =
|
|
267
|
+
entry.manifest.auth.kind === 'strategy'
|
|
268
|
+
? strategyFor(entry.manifest, this.#deps.registry)
|
|
269
|
+
: undefined;
|
|
270
|
+
|
|
271
|
+
// Derived from the provider context — which is handed the closure below,
|
|
272
|
+
// so it cannot exist yet. Both are only ever *called* after this block
|
|
273
|
+
// finishes, and memoising keeps one context per invocation rather than
|
|
274
|
+
// one per outbound request.
|
|
275
|
+
let strategyContext: AuthStrategyContext | undefined;
|
|
276
|
+
const forStrategy = (): AuthStrategyContext =>
|
|
277
|
+
(strategyContext ??= strategyContextFrom({
|
|
278
|
+
source: providerContext,
|
|
279
|
+
manifest: entry.manifest,
|
|
280
|
+
connectionId: declared.id,
|
|
281
|
+
profile: this.#deps.config.instance.profile,
|
|
282
|
+
}));
|
|
283
|
+
|
|
255
284
|
// One closure, handed to both contexts. A provider that authors a
|
|
256
285
|
// capability its transport cannot express still calls the vendor through
|
|
257
286
|
// the same authorizer the transport would have used.
|
|
258
287
|
const authorize = (outbound: Request): Promise<Request> =>
|
|
259
|
-
|
|
260
|
-
?
|
|
261
|
-
:
|
|
288
|
+
strategy
|
|
289
|
+
? strategy.authorize(outbound, forStrategy())
|
|
290
|
+
: this.#deps.authorizeRequest
|
|
291
|
+
? this.#deps.authorizeRequest(providerId, declared.id, outbound)
|
|
292
|
+
: Promise.resolve(outbound);
|
|
262
293
|
|
|
263
294
|
const providerContext = buildProviderContext({
|
|
264
295
|
manifest: entry.manifest,
|
|
@@ -288,10 +319,18 @@ export class Dispatcher {
|
|
|
288
319
|
});
|
|
289
320
|
}
|
|
290
321
|
|
|
322
|
+
// Only where the strategy asks for it. A vendor that signs its replies
|
|
323
|
+
// expects them verified, and the transport clones the response so the
|
|
324
|
+
// check costs the caller nothing.
|
|
325
|
+
const verifyResponse = strategy?.verify?.bind(strategy);
|
|
326
|
+
|
|
291
327
|
const connectorContext: ConnectorContext = {
|
|
292
328
|
manifest: entry.manifest,
|
|
293
329
|
provider: providerContext,
|
|
294
330
|
authorize,
|
|
331
|
+
...(verifyResponse
|
|
332
|
+
? { verify: (response: Response) => verifyResponse(response, forStrategy()) }
|
|
333
|
+
: {}),
|
|
295
334
|
};
|
|
296
335
|
|
|
297
336
|
// The connector owns argument validation: a local provider validates
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What bunq's own descriptions leave out, and an agent has to know.
|
|
3
|
+
*
|
|
4
|
+
* Appended to the generated description at discovery. Three things earn a line
|
|
5
|
+
* here: the distinction that decides whether money moves, the shape of an
|
|
6
|
+
* amount, and the identifier an agent cannot guess.
|
|
7
|
+
*/
|
|
8
|
+
export const BUNQ_HINTS: Record<string, string> = {
|
|
9
|
+
List_all_User:
|
|
10
|
+
'Call this first. Every other bunq tool is addressed under a userID, and this is the only thing that reports it.',
|
|
11
|
+
|
|
12
|
+
List_all_MonetaryAccount_for_User:
|
|
13
|
+
'Returns every account: current, savings, joint, and closed ones. Each is wrapped in a type key ' +
|
|
14
|
+
'(MonetaryAccountBank, MonetaryAccountSavings, …) and the id inside it is the `monetary-accountID` ' +
|
|
15
|
+
'the payment tools take. Check `status` is ACTIVE before paying from one.',
|
|
16
|
+
|
|
17
|
+
CREATE_Payment_for_User_MonetaryAccount:
|
|
18
|
+
'This executes immediately and is not reversible. The money leaves the account as soon as bunq accepts the call — ' +
|
|
19
|
+
'there is no confirmation step, in the app or anywhere else. Use draft-payment instead when a human should see it first. ' +
|
|
20
|
+
'amount is { "value": "10.00", "currency": "EUR" } — a decimal string, not a number, and never cents. ' +
|
|
21
|
+
'counterparty_alias is { "type": "IBAN", "value": "<iban>", "name": "<account holder>" }, and for IBAN the name must match ' +
|
|
22
|
+
'the one on the receiving account. type may also be EMAIL or PHONE_NUMBER for another bunq user.',
|
|
23
|
+
|
|
24
|
+
CREATE_DraftPayment_for_User_MonetaryAccount:
|
|
25
|
+
'Prepares a payment without sending it: it waits in the bunq app until a human approves it, which is the checkpoint ' +
|
|
26
|
+
'a direct payment does not have. Takes `entries`, an array of { amount, counterparty_alias, description } shaped exactly ' +
|
|
27
|
+
'like a direct payment, plus number_of_required_accepts (1 for a personal account).',
|
|
28
|
+
|
|
29
|
+
UPDATE_DraftPayment_for_User_MonetaryAccount:
|
|
30
|
+
'Changes a draft that is still pending — status ACCEPTED sends it, REJECTED cancels it. ' +
|
|
31
|
+
'previous_updated_timestamp is required and comes from reading the draft first; it is what stops two ' +
|
|
32
|
+
'callers acting on the same draft. Those two fields are the entire call — bunq refuses entries and ' +
|
|
33
|
+
'number_of_required_accepts here as superfluous — so changing what a draft pays means rejecting it and ' +
|
|
34
|
+
'creating another.',
|
|
35
|
+
|
|
36
|
+
CREATE_PaymentBatch_for_User_MonetaryAccount:
|
|
37
|
+
'Up to 350 payments in one call. Executes immediately, like a direct payment, and is all-or-nothing: bunq rejects ' +
|
|
38
|
+
'the whole batch if any entry is invalid. `payments` must be an ARRAY of { amount, counterparty_alias, description } ' +
|
|
39
|
+
'objects, each shaped exactly like a direct payment — bunq\'s specification declares the field as an object rather ' +
|
|
40
|
+
'than an array, which is wrong, so the schema here cannot tell you that and this line has to.',
|
|
41
|
+
|
|
42
|
+
List_all_Payment_for_User_MonetaryAccount:
|
|
43
|
+
'Newest first, one page at a time. bunq pages with `count`, `older_id` and `newer_id` rather than an offset, ' +
|
|
44
|
+
'but its specification does not declare them, so this tool returns the most recent page and no more.',
|
|
45
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defineProvider, defineProviderWithStrategy } from '#connectivity';
|
|
2
|
+
import { BUNQ_HINTS } from './hints.ts';
|
|
3
|
+
import { BUNQ_REDACT } from './redact.ts';
|
|
4
|
+
import { createBunqStrategy } from './strategy/index.ts';
|
|
5
|
+
|
|
6
|
+
const specPath = (name: string): string => new URL(`./specs/${name}`, import.meta.url).pathname;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* bunq — accounts, transaction history, and payments.
|
|
10
|
+
*
|
|
11
|
+
* The one provider here whose authentication is code. Everything else about it
|
|
12
|
+
* is declared like any other manifest, and the eleven operations it can reach
|
|
13
|
+
* come from a vendored OpenAPI document rather than from anything written by
|
|
14
|
+
* hand. What earns the exception is the handshake: bunq generates nothing for
|
|
15
|
+
* you, it wants a keypair you made, an installation, a registered device, and a
|
|
16
|
+
* session, and then it wants every request body signed. No manifest field
|
|
17
|
+
* describes that. See ADR-008 and `./strategy/`.
|
|
18
|
+
*
|
|
19
|
+
* `base_url` carries the version in the **path**, like Sheets and unlike Drive.
|
|
20
|
+
* `https://api.bunq.com` alone 404s on every call.
|
|
21
|
+
*
|
|
22
|
+
* Sandbox is not a second provider and not a flag either: the strategy reads
|
|
23
|
+
* its host from `base_url`, so a manifest in `providers.d/` naming
|
|
24
|
+
* `public-api.sandbox.bunq.com` handshakes and pays there, borrowing this
|
|
25
|
+
* strategy through `strategyFor`. That is where anything touching this should
|
|
26
|
+
* be proven before it is pointed at a real account.
|
|
27
|
+
*
|
|
28
|
+
* No `identity` block, and that is not an omission. The generic HTTP identity
|
|
29
|
+
* probe sends `Authorization: Bearer <token>`, which bunq does not accept and
|
|
30
|
+
* would fail in a way that reads as a bad credential rather than as an
|
|
31
|
+
* inapplicable probe. `connect` asks what to call the connection instead.
|
|
32
|
+
*/
|
|
33
|
+
const manifest = defineProvider({
|
|
34
|
+
id: 'bunq',
|
|
35
|
+
name: 'bunq',
|
|
36
|
+
description:
|
|
37
|
+
'Bank accounts, balances, transaction history, and payments — including batches and drafts that wait for approval in the bunq app.',
|
|
38
|
+
connector: {
|
|
39
|
+
kind: 'http',
|
|
40
|
+
base_url: 'https://api.bunq.com/v1',
|
|
41
|
+
openapi: specPath('bunq.v1.json'),
|
|
42
|
+
},
|
|
43
|
+
auth: {
|
|
44
|
+
kind: 'strategy',
|
|
45
|
+
strategy: 'bunq',
|
|
46
|
+
// No `credential_ref`, so it derives per connection — `bunq/<id>`. bunq
|
|
47
|
+
// issues one API key per account, and a declared ref would mean every
|
|
48
|
+
// connection sharing one, which is the opposite of true here.
|
|
49
|
+
},
|
|
50
|
+
redact: BUNQ_REDACT,
|
|
51
|
+
hints: BUNQ_HINTS,
|
|
52
|
+
setup: {
|
|
53
|
+
summary:
|
|
54
|
+
'bunq issues an API key from inside the app, not from a web console. The key is bound to the IP addresses ' +
|
|
55
|
+
'it is used from unless you mark it as a wildcard key — which you must do in the app if this endpoint will ' +
|
|
56
|
+
'ever run anywhere but this machine.',
|
|
57
|
+
docs: 'docs/detailed/setup/bunq.md',
|
|
58
|
+
docs_url: 'https://doc.bunq.com/basics/authentication/api-keys',
|
|
59
|
+
steps: [
|
|
60
|
+
'In the bunq app: Profile → Security & Settings → Developers → API keys → Add API key.',
|
|
61
|
+
'Name it "Lanes Link", so you can revoke this one later without touching your others.',
|
|
62
|
+
'Set a spending limit on the key while you are there. It is the only bound that does not depend on this software being correct — policy rules and tool lists are ours to get wrong, and a limit at the bank is not.',
|
|
63
|
+
'If this endpoint will run deployed rather than on this machine, mark the key as a wildcard key in the same screen. bunq refuses to set that over the API, deliberately, so it cannot be done for you.',
|
|
64
|
+
'Copy the key and paste it below. Nothing is sent anywhere until you do — the handshake that registers this device runs immediately afterwards.',
|
|
65
|
+
'You will be asked what to call this connection. bunq has no endpoint that reports whose account a key belongs to, so the label is yours to choose.',
|
|
66
|
+
'To try this without a real account first, use bunq\'s sandbox: https://public-api.sandbox.bunq.com issues a test key and needs no bank account at all. Put a manifest of your own in providers.d/ naming that base_url — see docs/detailed/setup/bunq.md.',
|
|
67
|
+
],
|
|
68
|
+
troubleshooting:
|
|
69
|
+
'bunq refused the key. The usual causes are a key that was revoked in the app, a request from an address the key ' +
|
|
70
|
+
'does not permit (mark it as a wildcard key if this runs deployed), or a session that ended because the account\'s ' +
|
|
71
|
+
'auto-logout elapsed — that last one recovers by itself on the next call. Generate a new key in the app and re-run: ' +
|
|
72
|
+
'lanes link connect bunq --replace.',
|
|
73
|
+
prompts: [
|
|
74
|
+
{
|
|
75
|
+
key: 'api_key',
|
|
76
|
+
label: 'bunq API key',
|
|
77
|
+
secret: true,
|
|
78
|
+
scope: 'connection' as const,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
export const bunq = defineProviderWithStrategy({
|
|
85
|
+
manifest,
|
|
86
|
+
strategy: createBunqStrategy(),
|
|
87
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A payment log that can answer "what moved, and to whom".
|
|
3
|
+
*
|
|
4
|
+
* The default is wrong here, and actively so. It withholds every value, which
|
|
5
|
+
* for mail is the right instinct — the body is the private part — and for a
|
|
6
|
+
* bank produces an audit log recording that a payment happened without
|
|
7
|
+
* recording its amount or its recipient. That is not a redaction, it is an
|
|
8
|
+
* erasure of the only facts anyone would ever go looking for.
|
|
9
|
+
*
|
|
10
|
+
* So the judgement runs the other way round from every other provider here: a
|
|
11
|
+
* payment argument is a *fact about a transaction*, not a piece of the user's
|
|
12
|
+
* content, and every one of them is kept. There is no equivalent of a message
|
|
13
|
+
* body in this provider's write surface — the nearest thing is `description`,
|
|
14
|
+
* which is the reference that appears on the recipient's statement and is
|
|
15
|
+
* therefore already shared with a third party.
|
|
16
|
+
*
|
|
17
|
+
* The reads are listed for the same reason Sheets lists its own: they carry
|
|
18
|
+
* identifiers and a page size, no query, and nothing worth protecting. Leaving
|
|
19
|
+
* them out would mean a log that cannot distinguish reading one payment from
|
|
20
|
+
* enumerating the account.
|
|
21
|
+
*
|
|
22
|
+
* Names are the generated argument names, which the OpenAPI generator prefixes
|
|
23
|
+
* where a body field and a path parameter would otherwise collide.
|
|
24
|
+
*/
|
|
25
|
+
export const BUNQ_REDACT: Record<string, string[]> = {
|
|
26
|
+
// Reads.
|
|
27
|
+
List_all_User: [],
|
|
28
|
+
List_all_MonetaryAccount_for_User: ['userID'],
|
|
29
|
+
List_all_Payment_for_User_MonetaryAccount: ['userID', 'monetary-accountID'],
|
|
30
|
+
READ_Payment_for_User_MonetaryAccount: ['userID', 'monetary-accountID', 'itemId'],
|
|
31
|
+
List_all_DraftPayment_for_User_MonetaryAccount: ['userID', 'monetary-accountID'],
|
|
32
|
+
READ_DraftPayment_for_User_MonetaryAccount: ['userID', 'monetary-accountID', 'itemId'],
|
|
33
|
+
List_all_PaymentBatch_for_User_MonetaryAccount: ['userID', 'monetary-accountID'],
|
|
34
|
+
|
|
35
|
+
// Writes. Everything, because everything is a fact worth reconstructing: the
|
|
36
|
+
// account it left, the amount, who received it, and what the reference said.
|
|
37
|
+
CREATE_Payment_for_User_MonetaryAccount: [
|
|
38
|
+
'userID',
|
|
39
|
+
'monetary-accountID',
|
|
40
|
+
'amount',
|
|
41
|
+
'counterparty_alias',
|
|
42
|
+
'description',
|
|
43
|
+
'merchant_reference',
|
|
44
|
+
'allow_bunqto',
|
|
45
|
+
],
|
|
46
|
+
CREATE_DraftPayment_for_User_MonetaryAccount: [
|
|
47
|
+
'userID',
|
|
48
|
+
'monetary-accountID',
|
|
49
|
+
'entries',
|
|
50
|
+
'number_of_required_accepts',
|
|
51
|
+
'status',
|
|
52
|
+
'schedule',
|
|
53
|
+
],
|
|
54
|
+
// Five, not seven: `entries` and `number_of_required_accepts` are no longer
|
|
55
|
+
// arguments at all. This is the one write here whose event cannot name an
|
|
56
|
+
// amount or a counterparty, because the call does not carry them — it says
|
|
57
|
+
// which draft was accepted and against which version of it, and that is
|
|
58
|
+
// everything there is to keep.
|
|
59
|
+
//
|
|
60
|
+
// Not everything a reader wants, though, and worth being honest that the gap
|
|
61
|
+
// is real rather than closed. The entries are on the event that *created* the
|
|
62
|
+
// draft, and nothing joins the two: an `AuditEvent` records arguments only,
|
|
63
|
+
// and bunq returns the draft id in the create's response. Reconstructing what
|
|
64
|
+
// an ACCEPTED draft paid means matching by hand. `context.audit.annotate`,
|
|
65
|
+
// which `gmail.send_message` uses to record resolved facts, is the shape of a
|
|
66
|
+
// fix and is a change to dispatch rather than to this list.
|
|
67
|
+
UPDATE_DraftPayment_for_User_MonetaryAccount: [
|
|
68
|
+
'userID',
|
|
69
|
+
'monetary-accountID',
|
|
70
|
+
'itemId',
|
|
71
|
+
'status',
|
|
72
|
+
'previous_updated_timestamp',
|
|
73
|
+
],
|
|
74
|
+
CREATE_PaymentBatch_for_User_MonetaryAccount: ['userID', 'monetary-accountID', 'payments'],
|
|
75
|
+
};
|