@lobu/cli 6.0.1 → 7.0.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/README.md +20 -27
- package/dist/bundled-skills/lobu/SKILL.md +11 -11
- package/dist/commands/_lib/apply/apply-cmd.d.ts +38 -0
- package/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +574 -40
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +180 -1
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/client.js +308 -28
- package/dist/commands/_lib/apply/client.js.map +1 -1
- package/dist/commands/_lib/apply/desired-state.d.ts +134 -3
- package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +703 -89
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/_lib/apply/diff.d.ts +61 -3
- package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
- package/dist/commands/_lib/apply/diff.js +382 -92
- package/dist/commands/_lib/apply/diff.js.map +1 -1
- package/dist/commands/_lib/apply/prompt.d.ts +6 -0
- package/dist/commands/_lib/apply/prompt.d.ts.map +1 -1
- package/dist/commands/_lib/apply/prompt.js +16 -0
- package/dist/commands/_lib/apply/prompt.js.map +1 -1
- package/dist/commands/_lib/apply/render.d.ts +9 -0
- package/dist/commands/_lib/apply/render.d.ts.map +1 -1
- package/dist/commands/_lib/apply/render.js +80 -3
- package/dist/commands/_lib/apply/render.js.map +1 -1
- package/dist/commands/agent.d.ts +7 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +65 -1
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts +12 -9
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +125 -57
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/dev.d.ts +23 -7
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +197 -49
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +136 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts +8 -0
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +72 -6
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts +22 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +355 -182
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/link.d.ts +11 -0
- package/dist/commands/link.d.ts.map +1 -0
- package/dist/commands/link.js +28 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +14 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/browser-auth-cmd.js +3 -3
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/mcp.d.ts +2 -2
- package/dist/commands/memory/_lib/mcp.d.ts.map +1 -1
- package/dist/commands/memory/_lib/mcp.js +24 -12
- package/dist/commands/memory/_lib/mcp.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.d.ts +1 -0
- package/dist/commands/memory/_lib/openclaw-auth.d.ts.map +1 -1
- package/dist/commands/memory/_lib/openclaw-auth.js +14 -3
- package/dist/commands/memory/_lib/openclaw-auth.js.map +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js +1 -1
- package/dist/commands/memory/_lib/openclaw-cmd.js.map +1 -1
- package/dist/commands/memory/_lib/schema.d.ts +29 -2
- package/dist/commands/memory/_lib/schema.d.ts.map +1 -1
- package/dist/commands/memory/_lib/schema.js +121 -5
- package/dist/commands/memory/_lib/schema.js.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.d.ts.map +1 -1
- package/dist/commands/memory/_lib/seed-cmd.js +46 -24
- package/dist/commands/memory/_lib/seed-cmd.js.map +1 -1
- package/dist/commands/memory/run.d.ts.map +1 -1
- package/dist/commands/memory/run.js +2 -2
- package/dist/commands/memory/run.js.map +1 -1
- package/dist/commands/org.d.ts +4 -0
- package/dist/commands/org.d.ts.map +1 -1
- package/dist/commands/org.js +10 -0
- package/dist/commands/org.js.map +1 -1
- package/dist/commands/platforms/platform-prompts.d.ts +0 -1
- package/dist/commands/platforms/platform-prompts.d.ts.map +1 -1
- package/dist/commands/platforms/platform-prompts.js +54 -8
- package/dist/commands/platforms/platform-prompts.js.map +1 -1
- package/dist/commands/telemetry.d.ts +10 -0
- package/dist/commands/telemetry.d.ts.map +1 -0
- package/dist/commands/telemetry.js +68 -0
- package/dist/commands/telemetry.js.map +1 -0
- package/dist/commands/token.d.ts +9 -0
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +54 -0
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connectors/README.md +534 -0
- package/dist/connectors/__tests__/browser-scraper-utils.test.ts +186 -0
- package/dist/connectors/apple_health.ts +138 -0
- package/dist/connectors/apple_screen_time.ts +82 -0
- package/dist/connectors/browser-scraper-utils.ts +246 -0
- package/dist/connectors/capterra.ts +277 -0
- package/dist/connectors/g2.ts +290 -0
- package/dist/connectors/github.ts +1530 -0
- package/dist/connectors/glassdoor.ts +295 -0
- package/dist/connectors/gmaps.ts +197 -0
- package/dist/connectors/google_calendar.ts +641 -0
- package/dist/connectors/google_gmail.ts +754 -0
- package/dist/connectors/google_photos.ts +776 -0
- package/dist/connectors/google_play.ts +349 -0
- package/dist/connectors/hackernews.ts +471 -0
- package/dist/connectors/index.ts +28 -0
- package/dist/connectors/ios_appstore.ts +226 -0
- package/dist/connectors/linkedin.ts +494 -0
- package/dist/connectors/local_directory.ts +91 -0
- package/dist/connectors/microsoft_outlook.ts +410 -0
- package/dist/connectors/producthunt.ts +471 -0
- package/dist/connectors/reddit.ts +600 -0
- package/dist/connectors/revolut.ts +572 -0
- package/dist/connectors/rss.ts +448 -0
- package/dist/connectors/spotify.ts +590 -0
- package/dist/connectors/trustpilot.ts +203 -0
- package/dist/connectors/website.ts +629 -0
- package/dist/connectors/whatsapp.ts +1081 -0
- package/dist/connectors/whatsapp_local.ts +125 -0
- package/dist/connectors/x.ts +536 -0
- package/dist/connectors/youtube.ts +666 -0
- package/dist/db/migrations/00000000000000_baseline.sql +4867 -0
- package/dist/db/migrations/20260405193000_add_mcp_sessions.sql +33 -0
- package/dist/db/migrations/20260408120000_remove_system_connectors.sql +48 -0
- package/dist/db/migrations/20260408120001_optional_compiled_code.sql +6 -0
- package/dist/db/migrations/20260409110000_add_active_watcher_run_index.sql +9 -0
- package/dist/db/migrations/20260409130000_connector_default_config.sql +5 -0
- package/dist/db/migrations/20260410120000_add_agent_secrets.sql +25 -0
- package/dist/db/migrations/20260413170000_add_watcher_group_id.sql +67 -0
- package/dist/db/migrations/20260416120000_add_entity_wa_jid_index.sql +14 -0
- package/dist/db/migrations/20260417100000_add_entity_identities.sql +77 -0
- package/dist/db/migrations/20260418100000_add_auth_runs.sql +83 -0
- package/dist/db/migrations/20260418110000_add_runs_created_by_user.sql +18 -0
- package/dist/db/migrations/20260419120000_add_event_identity_indexes.sql +56 -0
- package/dist/db/migrations/20260420120000_extend_reserved_org_slugs.sql +56 -0
- package/dist/db/migrations/20260424030000_add_watcher_run_correlation.sql +52 -0
- package/dist/db/migrations/20260424130000_relax_events_client_id_fk.sql +47 -0
- package/dist/db/migrations/20260425100000_normalize_watcher_feedback.sql +91 -0
- package/dist/db/migrations/20260425120000_add_run_diagnostics.sql +20 -0
- package/dist/db/migrations/20260425130000_add_repair_agent_plumbing.sql +46 -0
- package/dist/db/migrations/20260426120000_entities_entity_type_fk.sql +101 -0
- package/dist/db/migrations/20260426130000_db_integrity_cleanup.sql +104 -0
- package/dist/db/migrations/20260426130001_db_integrity_cleanup_concurrent.sql +187 -0
- package/dist/db/migrations/20260427133000_events_created_by_nullable.sql +74 -0
- package/dist/db/migrations/20260427140000_identity_engine_indexes.sql +140 -0
- package/dist/db/migrations/20260427150000_drop_events_source_id.sql +177 -0
- package/dist/db/migrations/20260427160000_drop_dead_schema.sql +76 -0
- package/dist/db/migrations/20260427170000_market_founder_to_member.sql +364 -0
- package/dist/db/migrations/20260428040000_cascade_events_watchers_org_fk.sql +66 -0
- package/dist/db/migrations/20260428050000_add_runs_approved_input.sql +9 -0
- package/dist/db/migrations/20260429010000_auth_profile_tenant_scoped_fk.sql +79 -0
- package/dist/db/migrations/20260429060000_extend_runs_for_lobu_queue.sql +108 -0
- package/dist/db/migrations/20260429120000_agent_changed_notify.sql +97 -0
- package/dist/db/migrations/20260429120100_user_auth_profiles_and_model_prefs.sql +36 -0
- package/dist/db/migrations/20260429120200_fix_notify_old_keys.sql +130 -0
- package/dist/db/migrations/20260429130000_oauth_states_cli_sessions_rate_limits.sql +83 -0
- package/dist/db/migrations/20260429140000_phase8_grants_chat_connections_mcp_sessions.sql +84 -0
- package/dist/db/migrations/20260429140100_runs_priority_expires_at_retry_delay.sql +44 -0
- package/dist/db/migrations/20260429180000_drop_invalidatable_cache_triggers.sql +25 -0
- package/dist/db/migrations/20260430005614_agents_apply_fields.sql +21 -0
- package/dist/db/migrations/20260430022231_fix_connection_config_encryption.sql +69 -0
- package/dist/db/migrations/20260430151215_add_task_run_type.sql +77 -0
- package/dist/db/migrations/20260501000000_drop_cli_sessions.sql +27 -0
- package/dist/db/migrations/20260501133000_lobu_memory_mcp_id.sql +117 -0
- package/dist/db/migrations/20260502000000_drop_chat_connections.sql +60 -0
- package/dist/db/migrations/20260503000000_agent_secrets_org_scope.sql +56 -0
- package/dist/db/migrations/20260504000000_flatten_agents_drop_sandbox_model.sql +48 -0
- package/dist/db/migrations/20260510220000_connector_required_capability.sql +47 -0
- package/dist/db/migrations/20260512000000_device_worker_connection_binding.sql +113 -0
- package/dist/db/migrations/20260512131703_connections_slug.sql +131 -0
- package/dist/db/migrations/20260513000000_chat_user_identities.sql +24 -0
- package/dist/db/migrations/20260513120000_auth_profiles_device_binding.sql +50 -0
- package/dist/db/migrations/20260513150000_auth_profiles_cdp_url.sql +43 -0
- package/dist/db/migrations/20260513200000_notifications_as_events.sql +86 -0
- package/dist/db/migrations/20260514000000_scheduled_jobs.sql +97 -0
- package/dist/db/migrations/20260514120000_auth_profiles_connector_key_nullable.sql +42 -0
- package/dist/eval/types.d.ts +2 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +210 -132
- package/dist/index.js.map +1 -1
- package/dist/internal/api-client.d.ts +4 -8
- package/dist/internal/api-client.d.ts.map +1 -1
- package/dist/internal/api-client.js +1 -1
- package/dist/internal/api-client.js.map +1 -1
- package/dist/internal/context.js +2 -2
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +6 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/gateway-url.d.ts +14 -0
- package/dist/internal/gateway-url.d.ts.map +1 -1
- package/dist/internal/gateway-url.js +19 -0
- package/dist/internal/gateway-url.js.map +1 -1
- package/dist/internal/index.d.ts +3 -4
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +3 -3
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/oauth.d.ts +6 -5
- package/dist/internal/oauth.d.ts.map +1 -1
- package/dist/internal/oauth.js +2 -2
- package/dist/internal/project-link.d.ts +10 -0
- package/dist/internal/project-link.d.ts.map +1 -0
- package/dist/internal/project-link.js +48 -0
- package/dist/internal/project-link.js.map +1 -0
- package/dist/providers.json +2 -2
- package/dist/server.bundle.mjs +31654 -30866
- package/dist/start-local.bundle.mjs +74409 -0
- package/dist/templates/README.md.tmpl +10 -11
- package/dist/templates/TESTING.md.tmpl +9 -9
- package/package.json +15 -13
- package/dist/__tests__/chat.integration.test.d.ts +0 -2
- package/dist/__tests__/chat.integration.test.d.ts.map +0 -1
- package/dist/__tests__/chat.integration.test.js +0 -337
- package/dist/__tests__/chat.integration.test.js.map +0 -1
- package/dist/__tests__/dev.test.d.ts +0 -2
- package/dist/__tests__/dev.test.d.ts.map +0 -1
- package/dist/__tests__/dev.test.js +0 -25
- package/dist/__tests__/dev.test.js.map +0 -1
- package/dist/__tests__/init-memory.test.d.ts +0 -2
- package/dist/__tests__/init-memory.test.d.ts.map +0 -1
- package/dist/__tests__/init-memory.test.js +0 -45
- package/dist/__tests__/init-memory.test.js.map +0 -1
- package/dist/__tests__/token.test.d.ts +0 -2
- package/dist/__tests__/token.test.d.ts.map +0 -1
- package/dist/__tests__/token.test.js +0 -52
- package/dist/__tests__/token.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/client.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/client.test.js +0 -23
- package/dist/commands/_lib/apply/__tests__/client.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/desired-state.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js +0 -140
- package/dist/commands/_lib/apply/__tests__/desired-state.test.js.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts +0 -2
- package/dist/commands/_lib/apply/__tests__/diff.test.d.ts.map +0 -1
- package/dist/commands/_lib/apply/__tests__/diff.test.js +0 -378
- package/dist/commands/_lib/apply/__tests__/diff.test.js.map +0 -1
- package/dist/commands/apply.d.ts +0 -3
- package/dist/commands/apply.d.ts.map +0 -1
- package/dist/commands/apply.js +0 -5
- package/dist/commands/apply.js.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts +0 -2
- package/dist/commands/memory/_lib/openclaw-auth.test.d.ts.map +0 -1
- package/dist/commands/memory/_lib/openclaw-auth.test.js +0 -9
- package/dist/commands/memory/_lib/openclaw-auth.test.js.map +0 -1
- package/dist/internal/__tests__/api-client.test.d.ts +0 -2
- package/dist/internal/__tests__/api-client.test.d.ts.map +0 -1
- package/dist/internal/__tests__/api-client.test.js +0 -95
- package/dist/internal/__tests__/api-client.test.js.map +0 -1
- package/dist/internal/__tests__/context.test.d.ts +0 -2
- package/dist/internal/__tests__/context.test.d.ts.map +0 -1
- package/dist/internal/__tests__/context.test.js +0 -77
- package/dist/internal/__tests__/context.test.js.map +0 -1
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revolut Connector (V1 runtime)
|
|
3
|
+
*
|
|
4
|
+
* Revolut has no public personal-banking API, so this connector drives the
|
|
5
|
+
* Revolut web app and captures the JSON it fetches from
|
|
6
|
+
* `app.revolut.com/api/retail/user/current/transactions/last?...` while
|
|
7
|
+
* paginating the transaction list (the `to=<ms>` param walks back in time).
|
|
8
|
+
*
|
|
9
|
+
* Auth: CDP only. Revolut's `app.revolut.com` access token (`credentials`
|
|
10
|
+
* cookie) is bound to the browser that minted it (a per-request `x-device-id`
|
|
11
|
+
* header + Cloudflare/TLS fingerprint), so exported cookies replayed in a fresh
|
|
12
|
+
* headless browser get a 401 on `/api/retail/...` and bounce to
|
|
13
|
+
* `sso.revolut.com/passcode`. The connector therefore connects over CDP to a
|
|
14
|
+
* Chrome that already holds the live session — see the auth-schema notes.
|
|
15
|
+
*
|
|
16
|
+
* The emitted event shape matches the original file-import Revolut connector
|
|
17
|
+
* (`semantic_type: "transaction"`, metadata `{ date, description, amount,
|
|
18
|
+
* direction, balance, currency }`) so historical imports stay uniform.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
type ActionContext,
|
|
23
|
+
type ActionResult,
|
|
24
|
+
browserNetworkSync,
|
|
25
|
+
type ConnectorDefinition,
|
|
26
|
+
ConnectorRuntime,
|
|
27
|
+
type EventEnvelope,
|
|
28
|
+
type SyncContext,
|
|
29
|
+
type SyncResult,
|
|
30
|
+
} from "@lobu/connector-sdk";
|
|
31
|
+
import {
|
|
32
|
+
getBrowserCdpUrl,
|
|
33
|
+
getBrowserCookies,
|
|
34
|
+
getBrowserUserDataDir,
|
|
35
|
+
validateCookieNotExpired,
|
|
36
|
+
} from "./browser-scraper-utils";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Types
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
export interface RevolutCheckpoint {
|
|
43
|
+
last_transaction_id?: string;
|
|
44
|
+
last_timestamp?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RevolutTransaction {
|
|
48
|
+
id: string;
|
|
49
|
+
description: string;
|
|
50
|
+
/** Absolute value in major currency units (e.g. 20.0 for £20.00). */
|
|
51
|
+
amount: number;
|
|
52
|
+
direction: "in" | "out";
|
|
53
|
+
/** Account balance after the transaction, in major units (may be absent). */
|
|
54
|
+
balance?: number;
|
|
55
|
+
currency: string;
|
|
56
|
+
/** ISO calendar date (YYYY-MM-DD) the transaction settled / started. */
|
|
57
|
+
date: string;
|
|
58
|
+
/** Full settlement timestamp. */
|
|
59
|
+
occurredAt: Date;
|
|
60
|
+
/** Revolut transaction type, e.g. CARD_PAYMENT, TRANSFER, TOPUP. */
|
|
61
|
+
type?: string;
|
|
62
|
+
/** Revolut state, e.g. COMPLETED, PENDING. */
|
|
63
|
+
state?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Currencies with zero minor units — Revolut returns these amounts unscaled.
|
|
67
|
+
const ZERO_DECIMAL_CURRENCIES = new Set([
|
|
68
|
+
"JPY",
|
|
69
|
+
"KRW",
|
|
70
|
+
"VND",
|
|
71
|
+
"CLP",
|
|
72
|
+
"ISK",
|
|
73
|
+
"XAF",
|
|
74
|
+
"XOF",
|
|
75
|
+
"BIF",
|
|
76
|
+
"DJF",
|
|
77
|
+
"GNF",
|
|
78
|
+
"KMF",
|
|
79
|
+
"MGA",
|
|
80
|
+
"PYG",
|
|
81
|
+
"RWF",
|
|
82
|
+
"UGX",
|
|
83
|
+
"VUV",
|
|
84
|
+
"XPF",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
// Transaction states worth keeping. DECLINED/FAILED/REVERTED never settled.
|
|
88
|
+
const KEPT_STATES = new Set(["COMPLETED", "PENDING", "CONFIRMED", "SETTLED"]);
|
|
89
|
+
|
|
90
|
+
// Fields the web app uses for the transaction timestamp, best first.
|
|
91
|
+
const TIMESTAMP_FIELDS = [
|
|
92
|
+
"completedDate",
|
|
93
|
+
"completed_date",
|
|
94
|
+
"completedAt",
|
|
95
|
+
"bookingDate",
|
|
96
|
+
"booking_date",
|
|
97
|
+
"valueDate",
|
|
98
|
+
"value_date",
|
|
99
|
+
"startedDate",
|
|
100
|
+
"started_date",
|
|
101
|
+
"createdDate",
|
|
102
|
+
"created_date",
|
|
103
|
+
"createdAt",
|
|
104
|
+
"date",
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Parsing
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
function minorUnitsToMajor(raw: number, currency: string): number {
|
|
112
|
+
const exponent = ZERO_DECIMAL_CURRENCIES.has(currency.toUpperCase()) ? 0 : 2;
|
|
113
|
+
return raw / 10 ** exponent;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function coerceTimestamp(value: unknown): Date | null {
|
|
117
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
118
|
+
// Revolut uses ms-epoch; treat 10-digit values as seconds defensively.
|
|
119
|
+
const ms = value < 1e12 ? value * 1000 : value;
|
|
120
|
+
const d = new Date(ms);
|
|
121
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
122
|
+
}
|
|
123
|
+
if (typeof value === "string" && value.trim()) {
|
|
124
|
+
const d = new Date(value);
|
|
125
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function extractAmountAndCurrency(
|
|
131
|
+
record: Record<string, unknown>,
|
|
132
|
+
): { amount: number; currency: string } | null {
|
|
133
|
+
// Flat shape: { amount: -2000, currency: "GBP" }
|
|
134
|
+
if (
|
|
135
|
+
typeof record.amount === "number" &&
|
|
136
|
+
typeof record.currency === "string"
|
|
137
|
+
) {
|
|
138
|
+
return { amount: record.amount, currency: record.currency };
|
|
139
|
+
}
|
|
140
|
+
// Nested money shape: { amount: { value: -2000, currency: "GBP" } } or
|
|
141
|
+
// { amount: { amount: -20.0, currency: "GBP" } }.
|
|
142
|
+
const amt = record.amount;
|
|
143
|
+
if (amt && typeof amt === "object") {
|
|
144
|
+
const obj = amt as Record<string, unknown>;
|
|
145
|
+
const value =
|
|
146
|
+
typeof obj.value === "number"
|
|
147
|
+
? obj.value
|
|
148
|
+
: typeof obj.amount === "number"
|
|
149
|
+
? obj.amount
|
|
150
|
+
: null;
|
|
151
|
+
const currency = typeof obj.currency === "string" ? obj.currency : null;
|
|
152
|
+
if (value !== null && currency) return { amount: value, currency };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function nameOf(node: unknown): string | null {
|
|
158
|
+
if (!node || typeof node !== "object") return null;
|
|
159
|
+
const obj = node as Record<string, unknown>;
|
|
160
|
+
for (const key of ["name", "legalName", "username", "displayName"]) {
|
|
161
|
+
const v = obj[key];
|
|
162
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function describeTransaction(record: Record<string, unknown>): string {
|
|
168
|
+
// Card payments carry a clean `merchant.name` ("OpenAI") alongside a noisy
|
|
169
|
+
// raw descriptor ("Openai *chatgpt Subscr") — prefer the merchant name, which
|
|
170
|
+
// is also what the Revolut UI shows and what the legacy import used. Transfers
|
|
171
|
+
// and top-ups have no merchant, so fall back to the human description.
|
|
172
|
+
const merchant = nameOf(record.merchant);
|
|
173
|
+
if (merchant) return merchant;
|
|
174
|
+
for (const key of [
|
|
175
|
+
"description",
|
|
176
|
+
"localisedDescription",
|
|
177
|
+
"reference",
|
|
178
|
+
"comment",
|
|
179
|
+
]) {
|
|
180
|
+
const v = record[key];
|
|
181
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
182
|
+
}
|
|
183
|
+
for (const key of [
|
|
184
|
+
"counterpart",
|
|
185
|
+
"counterparty",
|
|
186
|
+
"recipient",
|
|
187
|
+
"sender",
|
|
188
|
+
"beneficiary",
|
|
189
|
+
]) {
|
|
190
|
+
const v = nameOf(record[key]);
|
|
191
|
+
if (v) return v;
|
|
192
|
+
}
|
|
193
|
+
const type = record.type;
|
|
194
|
+
return typeof type === "string" && type.trim()
|
|
195
|
+
? type.replace(/_/g, " ")
|
|
196
|
+
: "Transaction";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractBalance(
|
|
200
|
+
record: Record<string, unknown>,
|
|
201
|
+
currency: string,
|
|
202
|
+
): number | undefined {
|
|
203
|
+
const raw =
|
|
204
|
+
typeof record.balance === "number"
|
|
205
|
+
? record.balance
|
|
206
|
+
: record.balance && typeof record.balance === "object"
|
|
207
|
+
? ((record.balance as Record<string, unknown>).value ??
|
|
208
|
+
(record.balance as Record<string, unknown>).amount)
|
|
209
|
+
: undefined;
|
|
210
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return undefined;
|
|
211
|
+
return Number.isInteger(raw) ? minorUnitsToMajor(raw, currency) : raw;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseTransactionRecord(
|
|
215
|
+
record: Record<string, unknown>,
|
|
216
|
+
): RevolutTransaction | null {
|
|
217
|
+
const money = extractAmountAndCurrency(record);
|
|
218
|
+
if (!money) return null;
|
|
219
|
+
|
|
220
|
+
const id = record.id ?? record.legId ?? record.transactionId ?? record.code;
|
|
221
|
+
if (typeof id !== "string" && typeof id !== "number") return null;
|
|
222
|
+
|
|
223
|
+
let occurredAt: Date | null = null;
|
|
224
|
+
for (const field of TIMESTAMP_FIELDS) {
|
|
225
|
+
occurredAt = coerceTimestamp(record[field]);
|
|
226
|
+
if (occurredAt) break;
|
|
227
|
+
}
|
|
228
|
+
if (!occurredAt) return null;
|
|
229
|
+
|
|
230
|
+
const state =
|
|
231
|
+
typeof record.state === "string" ? record.state.toUpperCase() : undefined;
|
|
232
|
+
if (state && !KEPT_STATES.has(state)) return null;
|
|
233
|
+
|
|
234
|
+
const currency = money.currency.toUpperCase();
|
|
235
|
+
// Revolut's retail API returns integer minor units; some endpoints return a
|
|
236
|
+
// decimal already in major units — fractional values mean "already major".
|
|
237
|
+
const value = Number.isInteger(money.amount)
|
|
238
|
+
? minorUnitsToMajor(money.amount, currency)
|
|
239
|
+
: money.amount;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
id: String(id),
|
|
243
|
+
description: describeTransaction(record),
|
|
244
|
+
amount: Math.abs(value),
|
|
245
|
+
direction: value < 0 ? "out" : "in",
|
|
246
|
+
balance: extractBalance(record, currency),
|
|
247
|
+
currency,
|
|
248
|
+
date: occurredAt.toISOString().slice(0, 10),
|
|
249
|
+
occurredAt,
|
|
250
|
+
type: typeof record.type === "string" ? record.type : undefined,
|
|
251
|
+
state,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Walk an arbitrary JSON value and pull out anything that looks like a Revolut
|
|
257
|
+
* transaction. The web app's responses vary (bare arrays, `{ items: [...] }`,
|
|
258
|
+
* paginated envelopes, single-transaction detail endpoints), so we recurse
|
|
259
|
+
* rather than assume one shape. A record only counts as a transaction if it
|
|
260
|
+
* carries both an amount/currency and a timestamp, which keeps merchant/budget
|
|
261
|
+
* objects out.
|
|
262
|
+
*/
|
|
263
|
+
export function extractTransactionsFromResponse(
|
|
264
|
+
json: unknown,
|
|
265
|
+
): RevolutTransaction[] {
|
|
266
|
+
const found: RevolutTransaction[] = [];
|
|
267
|
+
const seen = new Set<object>();
|
|
268
|
+
|
|
269
|
+
const visit = (node: unknown): void => {
|
|
270
|
+
if (!node || typeof node !== "object") return;
|
|
271
|
+
if (seen.has(node as object)) return;
|
|
272
|
+
seen.add(node as object);
|
|
273
|
+
|
|
274
|
+
if (Array.isArray(node)) {
|
|
275
|
+
for (const item of node) {
|
|
276
|
+
const parsed =
|
|
277
|
+
item && typeof item === "object" && !Array.isArray(item)
|
|
278
|
+
? parseTransactionRecord(item as Record<string, unknown>)
|
|
279
|
+
: null;
|
|
280
|
+
if (parsed) found.push(parsed);
|
|
281
|
+
else visit(item);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const record = node as Record<string, unknown>;
|
|
287
|
+
const asTxn = parseTransactionRecord(record);
|
|
288
|
+
if (asTxn) {
|
|
289
|
+
found.push(asTxn);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
for (const value of Object.values(record)) visit(value);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
visit(json);
|
|
296
|
+
return found;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// Checkpoint filtering
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
export function filterTransactionsSinceCheckpoint(
|
|
304
|
+
transactions: RevolutTransaction[],
|
|
305
|
+
checkpoint: RevolutCheckpoint | null | undefined,
|
|
306
|
+
): RevolutTransaction[] {
|
|
307
|
+
const lastTs = checkpoint?.last_timestamp
|
|
308
|
+
? new Date(checkpoint.last_timestamp).getTime()
|
|
309
|
+
: null;
|
|
310
|
+
const lastId = checkpoint?.last_transaction_id;
|
|
311
|
+
const seen = new Set<string>();
|
|
312
|
+
return transactions.filter((t) => {
|
|
313
|
+
if (seen.has(t.id)) return false;
|
|
314
|
+
seen.add(t.id);
|
|
315
|
+
if (lastId && t.id === lastId) return false;
|
|
316
|
+
if (
|
|
317
|
+
lastTs !== null &&
|
|
318
|
+
Number.isFinite(lastTs) &&
|
|
319
|
+
t.occurredAt.getTime() <= lastTs
|
|
320
|
+
) {
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Event mapping (matches the original file-import Revolut connector)
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
function currencySymbol(currency: string): string {
|
|
332
|
+
switch (currency.toUpperCase()) {
|
|
333
|
+
case "GBP":
|
|
334
|
+
return "£";
|
|
335
|
+
case "USD":
|
|
336
|
+
return "$";
|
|
337
|
+
case "EUR":
|
|
338
|
+
return "€";
|
|
339
|
+
default:
|
|
340
|
+
return `${currency} `;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function transactionToEvent(t: RevolutTransaction): EventEnvelope {
|
|
345
|
+
const sign = t.direction === "out" ? "-" : "+";
|
|
346
|
+
return {
|
|
347
|
+
origin_id: `revolut-${t.id}`,
|
|
348
|
+
payload_text: `${t.description} ${sign}${currencySymbol(t.currency)}${t.amount} on ${t.date}`,
|
|
349
|
+
occurred_at: t.occurredAt,
|
|
350
|
+
semantic_type: "transaction",
|
|
351
|
+
metadata: {
|
|
352
|
+
date: t.date,
|
|
353
|
+
description: t.description,
|
|
354
|
+
amount: t.amount,
|
|
355
|
+
direction: t.direction,
|
|
356
|
+
...(t.balance !== undefined ? { balance: t.balance } : {}),
|
|
357
|
+
currency: t.currency,
|
|
358
|
+
...(t.type ? { transaction_type: t.type } : {}),
|
|
359
|
+
...(t.state ? { state: t.state } : {}),
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
// Sync
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
|
|
368
|
+
// The Revolut web app fetches account history from
|
|
369
|
+
// `app.revolut.com/api/retail/user/current/transactions/last?count=N&to=<ms>&internalPocketId=<uuid>`
|
|
370
|
+
// (the `to` param walks back in time as you scroll). These patterns also cover
|
|
371
|
+
// plausible alternates without catching unrelated `/api/retail/...` calls.
|
|
372
|
+
const TRANSACTION_API_PATTERNS: RegExp[] = [
|
|
373
|
+
/\/api\/retail\/.*transactions?(?:\/|\b)/i,
|
|
374
|
+
/\/api\/.*\/transactions(?:\b|\?|\/|$)/i,
|
|
375
|
+
/transactions?[./](?:last|recent|history|search)/i,
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
const REVOLUT_AUTH_DOMAINS = ["app.revolut.com", ".revolut.com"];
|
|
379
|
+
// `/transactions` shows the full, infinitely-scrollable history for the default
|
|
380
|
+
// account; `/home` only shows the latest ~10. Per-pocket history lives at
|
|
381
|
+
// `/transactions?accountType=pocket&walletId=<uuid>&pocketId=<uuid>` — point a
|
|
382
|
+
// second feed's `start_url` there to sync a non-default currency pocket.
|
|
383
|
+
const DEFAULT_START_URL = "https://app.revolut.com/transactions";
|
|
384
|
+
|
|
385
|
+
function isLoggedIn(url: string): boolean {
|
|
386
|
+
let host: string;
|
|
387
|
+
try {
|
|
388
|
+
host = new URL(url).hostname;
|
|
389
|
+
} catch {
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
// An unauthenticated session is bounced to sso.revolut.com/passcode.
|
|
393
|
+
if (host !== "app.revolut.com") return false;
|
|
394
|
+
return !/\/(?:start|signin|login|verify|onboarding)\b/i.test(url);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const configSchema = {
|
|
398
|
+
type: "object",
|
|
399
|
+
properties: {
|
|
400
|
+
start_url: {
|
|
401
|
+
type: "string",
|
|
402
|
+
default: DEFAULT_START_URL,
|
|
403
|
+
description:
|
|
404
|
+
"Revolut web app URL to open. Defaults to the full transactions view for the primary account; set it to a per-pocket /transactions?...pocketId=<uuid> URL to sync a different currency pocket.",
|
|
405
|
+
},
|
|
406
|
+
currency_filter: {
|
|
407
|
+
type: "string",
|
|
408
|
+
description:
|
|
409
|
+
'If set, keep only transactions in this ISO 4217 currency (e.g. "GBP").',
|
|
410
|
+
},
|
|
411
|
+
max_scrolls: {
|
|
412
|
+
type: "integer",
|
|
413
|
+
minimum: 1,
|
|
414
|
+
maximum: 100,
|
|
415
|
+
default: 20,
|
|
416
|
+
description:
|
|
417
|
+
"Maximum scroll iterations to paginate older transactions (default: 20).",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const transactionMetadataSchema = {
|
|
423
|
+
type: "object",
|
|
424
|
+
properties: {
|
|
425
|
+
date: { type: "string", format: "date" },
|
|
426
|
+
description: { type: "string" },
|
|
427
|
+
amount: { type: "number" },
|
|
428
|
+
direction: { type: "string", enum: ["in", "out"] },
|
|
429
|
+
balance: { type: "number" },
|
|
430
|
+
currency: { type: "string" },
|
|
431
|
+
transaction_type: { type: "string" },
|
|
432
|
+
state: { type: "string" },
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
export default class RevolutConnector extends ConnectorRuntime {
|
|
437
|
+
readonly definition: ConnectorDefinition = {
|
|
438
|
+
key: "revolut",
|
|
439
|
+
name: "Revolut",
|
|
440
|
+
description:
|
|
441
|
+
"Syncs Revolut account transactions from the Revolut web app (no public API). Requires a Chrome instance, with remote debugging enabled, that stays logged in to app.revolut.com.",
|
|
442
|
+
version: "2.0.0",
|
|
443
|
+
faviconDomain: "app.revolut.com",
|
|
444
|
+
authSchema: {
|
|
445
|
+
methods: [
|
|
446
|
+
// CDP only — *not* `cli` cookie capture. Revolut's `app.revolut.com`
|
|
447
|
+
// access token (the `credentials` cookie) is bound to the browser that
|
|
448
|
+
// minted it (device-id header + Cloudflare/TLS fingerprint), so cookies
|
|
449
|
+
// exported from Chrome and replayed in a fresh headless browser get a 401
|
|
450
|
+
// on /api/retail/... and bounce to sso.revolut.com/passcode. The only
|
|
451
|
+
// path that authenticates is connecting over CDP to the *same* Chrome
|
|
452
|
+
// that holds the live session — keep one logged in and reachable.
|
|
453
|
+
{
|
|
454
|
+
type: "browser",
|
|
455
|
+
capture: "cdp",
|
|
456
|
+
defaultCdpUrl: "http://127.0.0.1:9222",
|
|
457
|
+
requiredDomains: REVOLUT_AUTH_DOMAINS,
|
|
458
|
+
description:
|
|
459
|
+
"Connect over CDP to a Chrome logged in to app.revolut.com: lobu memory browser-auth --connector revolut --launch-cdp (log in there, re-enter the passcode whenever Revolut expires the session).",
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
},
|
|
463
|
+
feeds: {
|
|
464
|
+
transactions: {
|
|
465
|
+
key: "transactions",
|
|
466
|
+
name: "Transactions",
|
|
467
|
+
description: "Account transactions pulled from the Revolut web app.",
|
|
468
|
+
configSchema,
|
|
469
|
+
eventKinds: {
|
|
470
|
+
transaction: {
|
|
471
|
+
description: "A bank transaction",
|
|
472
|
+
metadataSchema: transactionMetadataSchema,
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
optionsSchema: configSchema,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
async sync(ctx: SyncContext): Promise<SyncResult> {
|
|
481
|
+
const config = (ctx.config ?? {}) as Record<string, unknown>;
|
|
482
|
+
const checkpoint = (ctx.checkpoint ?? {}) as RevolutCheckpoint;
|
|
483
|
+
|
|
484
|
+
const startUrl =
|
|
485
|
+
typeof config.start_url === "string" && config.start_url.trim()
|
|
486
|
+
? config.start_url.trim()
|
|
487
|
+
: DEFAULT_START_URL;
|
|
488
|
+
const currencyFilter =
|
|
489
|
+
typeof config.currency_filter === "string" &&
|
|
490
|
+
config.currency_filter.trim()
|
|
491
|
+
? config.currency_filter.trim().toUpperCase()
|
|
492
|
+
: null;
|
|
493
|
+
const maxScrolls = Math.max(
|
|
494
|
+
1,
|
|
495
|
+
Math.min(100, Number(config.max_scrolls ?? 20) || 20),
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Primary auth is CDP (connect to the Chrome that holds the live Revolut
|
|
499
|
+
// session). Stored cookies are only a best-effort fallback for the
|
|
500
|
+
// Playwright path — see the auth-schema comment on why they rarely suffice
|
|
501
|
+
// for Revolut. Don't fail the sync just because there are none.
|
|
502
|
+
const userDataDir = getBrowserUserDataDir(ctx.sessionState);
|
|
503
|
+
const cdpUrl = getBrowserCdpUrl(ctx.sessionState) ?? "auto";
|
|
504
|
+
let cookies: ReturnType<typeof getBrowserCookies> = [];
|
|
505
|
+
if (!userDataDir) {
|
|
506
|
+
try {
|
|
507
|
+
cookies = getBrowserCookies(
|
|
508
|
+
ctx.checkpoint as Record<string, unknown> | null,
|
|
509
|
+
ctx.sessionState,
|
|
510
|
+
"revolut",
|
|
511
|
+
);
|
|
512
|
+
validateCookieNotExpired(cookies, "credentials", "revolut");
|
|
513
|
+
} catch {
|
|
514
|
+
cookies = [];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const result = await browserNetworkSync<RevolutTransaction>({
|
|
519
|
+
config: {
|
|
520
|
+
interceptPatterns: TRANSACTION_API_PATTERNS,
|
|
521
|
+
authDomains: REVOLUT_AUTH_DOMAINS,
|
|
522
|
+
maxScrolls,
|
|
523
|
+
scrollDelayMs: 2500,
|
|
524
|
+
responseTimeoutMs: 8000,
|
|
525
|
+
navigationTimeoutMs: 20000,
|
|
526
|
+
stealth: true,
|
|
527
|
+
},
|
|
528
|
+
url: startUrl,
|
|
529
|
+
cdpUrl,
|
|
530
|
+
cookies,
|
|
531
|
+
userDataDir,
|
|
532
|
+
parseResponse: (_url, json) => extractTransactionsFromResponse(json),
|
|
533
|
+
checkAuth: async (page) => isLoggedIn(page.url()),
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
let transactions = filterTransactionsSinceCheckpoint(
|
|
537
|
+
result.items,
|
|
538
|
+
checkpoint,
|
|
539
|
+
);
|
|
540
|
+
if (currencyFilter) {
|
|
541
|
+
transactions = transactions.filter((t) => t.currency === currencyFilter);
|
|
542
|
+
}
|
|
543
|
+
transactions.sort(
|
|
544
|
+
(a, b) => b.occurredAt.getTime() - a.occurredAt.getTime(),
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
const events: EventEnvelope[] = transactions.map(transactionToEvent);
|
|
548
|
+
const newest = transactions[0];
|
|
549
|
+
const newCheckpoint: RevolutCheckpoint = newest
|
|
550
|
+
? {
|
|
551
|
+
last_transaction_id: newest.id,
|
|
552
|
+
last_timestamp: newest.occurredAt.toISOString(),
|
|
553
|
+
}
|
|
554
|
+
: checkpoint;
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
events,
|
|
558
|
+
checkpoint: newCheckpoint as unknown as Record<string, unknown>,
|
|
559
|
+
auth_update: { cookies: result.cookies },
|
|
560
|
+
metadata: {
|
|
561
|
+
items_found: events.length,
|
|
562
|
+
api_calls: result.apiCallCount,
|
|
563
|
+
backend: result.backend,
|
|
564
|
+
...(currencyFilter ? { currency_filter: currencyFilter } : {}),
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async execute(_ctx: ActionContext): Promise<ActionResult> {
|
|
570
|
+
return { success: false, error: "Actions not supported" };
|
|
571
|
+
}
|
|
572
|
+
}
|