@lanes-sh/link 0.3.1 → 0.4.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.
Files changed (70) hide show
  1. package/README.md +11 -3
  2. package/instructions/agents/lanes-link-scout.md +2 -2
  3. package/instructions/skills/lanes-link/SKILL.md +135 -11
  4. package/package.json +3 -1
  5. package/src/cli/argv.ts +52 -0
  6. package/src/cli/commands/connect/authorise.ts +5 -0
  7. package/src/cli/commands/connect/custom/ask.ts +167 -0
  8. package/src/cli/commands/connect/custom/credential.ts +143 -0
  9. package/src/cli/commands/connect/custom/derive.ts +229 -0
  10. package/src/cli/commands/connect/custom/index.ts +285 -0
  11. package/src/cli/commands/connect/custom/prompts.ts +160 -0
  12. package/src/cli/commands/connect/custom/spec.ts +293 -0
  13. package/src/cli/commands/connect/custom/values.ts +53 -0
  14. package/src/cli/commands/connect/custom/write.ts +166 -0
  15. package/src/cli/commands/connect/grant.ts +27 -0
  16. package/src/cli/commands/connect/index.ts +24 -27
  17. package/src/cli/commands/connect/outcome.ts +3 -1
  18. package/src/cli/commands/connect/requirements.ts +11 -1
  19. package/src/cli/commands/connect/settle.ts +17 -0
  20. package/src/cli/commands/connect/setup.ts +9 -1
  21. package/src/cli/commands/connect/strategy.ts +87 -0
  22. package/src/cli/commands/connect/unknown.ts +41 -0
  23. package/src/cli/identity.ts +29 -5
  24. package/src/cli/main.ts +37 -13
  25. package/src/cli/oauth.ts +89 -36
  26. package/src/cli/runtime/open.ts +13 -1
  27. package/src/cli/runtime/registry.ts +12 -0
  28. package/src/cli/selection.ts +12 -0
  29. package/src/cli/usage.ts +9 -1
  30. package/src/connectivity/auth/README.md +8 -1
  31. package/src/connectivity/auth/strategy/index.ts +128 -4
  32. package/src/connectivity/connector.ts +11 -0
  33. package/src/connectivity/index.ts +11 -1
  34. package/src/connectivity/manifest/auth.ts +19 -0
  35. package/src/connectivity/manifest/connector.ts +21 -0
  36. package/src/connectivity/manifest/primitives.ts +5 -1
  37. package/src/connectivity/manifest/provider.ts +30 -12
  38. package/src/connectivity/provider.ts +55 -0
  39. package/src/connectivity/transports/factory.ts +1 -0
  40. package/src/connectivity/transports/http/index.ts +73 -2
  41. package/src/deployments/sync-apply.ts +62 -8
  42. package/src/deployments/sync.ts +34 -6
  43. package/src/dispatch/dispatch.ts +44 -5
  44. package/src/providers/bunq/hints.ts +43 -0
  45. package/src/providers/bunq/index.ts +87 -0
  46. package/src/providers/bunq/redact.ts +64 -0
  47. package/src/providers/bunq/specs/bunq.v1.json +864 -0
  48. package/src/providers/bunq/specs/vendor.ts +338 -0
  49. package/src/providers/bunq/strategy/handshake.ts +211 -0
  50. package/src/providers/bunq/strategy/index.ts +298 -0
  51. package/src/providers/bunq/strategy/keys.ts +72 -0
  52. package/src/providers/custom/index.ts +1 -6
  53. package/src/providers/custom/load.ts +56 -14
  54. package/src/providers/custom/template.ts +1 -1
  55. package/src/providers/discord/hints.ts +195 -0
  56. package/src/providers/discord/index.ts +121 -0
  57. package/src/providers/discord/redact.ts +99 -0
  58. package/src/providers/discord/specs/discord.v10.json +2333 -0
  59. package/src/providers/discord/specs/vendor.ts +164 -0
  60. package/src/providers/google/specs/vendor.ts +32 -317
  61. package/src/providers/index.ts +9 -0
  62. package/src/providers/reddit/index.ts +113 -0
  63. package/src/providers/reddit/oauth.ts +77 -0
  64. package/src/providers/reddit/redact.ts +33 -0
  65. package/src/providers/reddit/scopes.ts +27 -0
  66. package/src/providers/reddit/specs/reddit.v1.json +700 -0
  67. package/src/providers/scopes.ts +2 -0
  68. package/src/providers/shared/openapi.ts +155 -0
  69. package/src/providers/shared/vendor-operations.ts +98 -0
  70. package/src/providers/shared/vendor-spec.ts +309 -0
@@ -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 { AnyConnector, CapabilityResult, ConnectorContext, Logger } from '#connectivity';
10
- import { isToolResult } from '#connectivity';
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
- this.#deps.authorizeRequest
260
- ? this.#deps.authorizeRequest(providerId, declared.id, outbound)
261
- : Promise.resolve(outbound);
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,43 @@
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.',
33
+
34
+ CREATE_PaymentBatch_for_User_MonetaryAccount:
35
+ 'Up to 350 payments in one call. Executes immediately, like a direct payment, and is all-or-nothing: bunq rejects ' +
36
+ 'the whole batch if any entry is invalid. `payments` must be an ARRAY of { amount, counterparty_alias, description } ' +
37
+ 'objects, each shaped exactly like a direct payment — bunq\'s specification declares the field as an object rather ' +
38
+ 'than an array, which is wrong, so the schema here cannot tell you that and this line has to.',
39
+
40
+ List_all_Payment_for_User_MonetaryAccount:
41
+ 'Newest first, one page at a time. bunq pages with `count`, `older_id` and `newer_id` rather than an offset, ' +
42
+ 'but its specification does not declare them, so this tool returns the most recent page and no more.',
43
+ };
@@ -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,64 @@
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
+ UPDATE_DraftPayment_for_User_MonetaryAccount: [
55
+ 'userID',
56
+ 'monetary-accountID',
57
+ 'itemId',
58
+ 'status',
59
+ 'entries',
60
+ 'previous_updated_timestamp',
61
+ 'number_of_required_accepts',
62
+ ],
63
+ CREATE_PaymentBatch_for_User_MonetaryAccount: ['userID', 'monetary-accountID', 'payments'],
64
+ };