@lobu/cli 7.0.0 → 7.2.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/dist/commands/_lib/apply/apply-cmd.d.ts.map +1 -1
- package/dist/commands/_lib/apply/apply-cmd.js +160 -12
- package/dist/commands/_lib/apply/apply-cmd.js.map +1 -1
- package/dist/commands/_lib/apply/client.d.ts +106 -0
- package/dist/commands/_lib/apply/client.d.ts.map +1 -1
- package/dist/commands/_lib/apply/client.js +163 -2
- package/dist/commands/_lib/apply/client.js.map +1 -1
- package/dist/commands/_lib/apply/desired-state.d.ts +53 -0
- package/dist/commands/_lib/apply/desired-state.d.ts.map +1 -1
- package/dist/commands/_lib/apply/desired-state.js +182 -5
- package/dist/commands/_lib/apply/desired-state.js.map +1 -1
- package/dist/commands/_lib/apply/diff.d.ts +12 -1
- package/dist/commands/_lib/apply/diff.d.ts.map +1 -1
- package/dist/commands/_lib/apply/diff.js +106 -7
- package/dist/commands/_lib/apply/diff.js.map +1 -1
- package/dist/commands/_lib/connector-loader.d.ts +3 -0
- package/dist/commands/_lib/connector-loader.d.ts.map +1 -0
- package/dist/commands/_lib/connector-loader.js +129 -0
- package/dist/commands/_lib/connector-loader.js.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts +35 -0
- package/dist/commands/_lib/connector-run-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/connector-run-cmd.js +351 -0
- package/dist/commands/_lib/connector-run-cmd.js.map +1 -0
- package/dist/commands/_lib/export/export-cmd.d.ts +35 -0
- package/dist/commands/_lib/export/export-cmd.d.ts.map +1 -0
- package/dist/commands/_lib/export/export-cmd.js +329 -0
- package/dist/commands/_lib/export/export-cmd.js.map +1 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +11 -14
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +19 -5
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/connector.d.ts +3 -0
- package/dist/commands/connector.d.ts.map +1 -0
- package/dist/commands/connector.js +5 -0
- package/dist/commands/connector.js.map +1 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.d.ts.map +1 -1
- package/dist/commands/context.js +19 -2
- package/dist/commands/context.js.map +1 -1
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +156 -4
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +12 -13
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +5 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +22 -16
- 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 +15 -144
- package/dist/commands/memory/_lib/browser-auth-cmd.js.map +1 -1
- package/dist/commands/token.d.ts.map +1 -1
- package/dist/commands/token.js +1 -4
- package/dist/commands/token.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +4 -13
- package/dist/commands/validate.js.map +1 -1
- package/dist/config/loader.js +2 -2
- package/dist/config/loader.js.map +1 -1
- package/dist/connectors/README.md +0 -1
- package/dist/connectors/apple_photos.ts +178 -0
- package/dist/connectors/browser-scraper-utils.ts +76 -0
- package/dist/connectors/chrome.ts +351 -0
- package/dist/connectors/chrome_bookmarks.ts +79 -0
- package/dist/connectors/chrome_downloads.ts +80 -0
- package/dist/connectors/chrome_history.ts +80 -0
- package/dist/connectors/github.ts +1 -0
- package/dist/connectors/google_calendar.ts +14 -2
- package/dist/connectors/google_play.ts +22 -2
- package/dist/connectors/hackernews.ts +37 -2
- package/dist/connectors/index.ts +15 -1
- package/dist/connectors/reddit.ts +1 -0
- package/dist/connectors/revolut.ts +10 -13
- package/dist/connectors/rss.ts +33 -8
- package/dist/connectors/trustpilot.ts +31 -20
- package/dist/connectors/website.ts +7 -68
- package/dist/connectors/whatsapp.ts +12 -21
- package/dist/db/migrations/20260514130000_connection_action_modes.sql +103 -0
- package/dist/db/migrations/20260514160000_auth_profiles_mirror_mode.sql +32 -0
- package/dist/db/migrations/20260515120000_agents_per_org_pk.sql +66 -0
- package/dist/db/migrations/20260515150000_geo_enrichment.sql +208 -0
- package/dist/db/migrations/20260515160000_drop_agents_org_id_unique.sql +24 -0
- package/dist/db/migrations/20260515170000_auth_profiles_default_for_connector.sql +23 -0
- package/dist/db/migrations/20260516120000_agents_per_org_pk_swap.sql +125 -0
- package/dist/db/migrations/20260516200000_events_search_tsv.sql +134 -0
- package/dist/db/migrations/20260516200100_events_lifecycle_changes_index.sql +25 -0
- package/dist/db/migrations/20260517010000_drop_unused_indexes.sql +49 -0
- package/dist/db/migrations/20260517020000_softdelete_orphan_feeds.sql +56 -0
- package/dist/db/migrations/20260517030000_pat_worker_id_binding.sql +27 -0
- package/dist/db/migrations/20260517040000_archive_orphan_watchers.sql +30 -0
- package/dist/db/migrations/20260517050000_watcher_agent_id_not_null.sql +34 -0
- package/dist/db/migrations/20260517060000_watcher_schema_additions.sql +78 -0
- package/dist/db/migrations/20260517150000_goals_primitive.sql +55 -0
- package/dist/db/migrations/20260517160000_drop_goals_primitive.sql +45 -0
- package/dist/db/migrations/20260518000000_pending_interactions.sql +49 -0
- package/dist/db/migrations/20260518010000_runs_heartbeat_reaper_index.sql +22 -0
- package/dist/db/migrations/20260518020000_runs_heartbeat_inflight_narrow.sql +36 -0
- package/dist/db/migrations/20260518040000_agent_transcript_snapshot.sql +54 -0
- package/dist/db/migrations/20260518050000_runs_denormalize_agent_conversation.sql +36 -0
- package/dist/db/migrations/20260518060000_revert_runs_denormalize.sql +29 -0
- package/dist/db/migrations/20260518070000_runs_heartbeat_inflight_widen.sql +33 -0
- package/dist/eval/client.d.ts.map +1 -1
- package/dist/eval/client.js +11 -0
- package/dist/eval/client.js.map +1 -1
- package/dist/eval/grader.js +2 -1
- package/dist/eval/grader.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +84 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/context.d.ts +13 -1
- package/dist/internal/context.d.ts.map +1 -1
- package/dist/internal/context.js +83 -8
- package/dist/internal/context.js.map +1 -1
- package/dist/internal/credentials.d.ts +5 -0
- package/dist/internal/credentials.d.ts.map +1 -1
- package/dist/internal/credentials.js +75 -1
- package/dist/internal/credentials.js.map +1 -1
- package/dist/internal/index.d.ts +2 -2
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +2 -2
- package/dist/internal/index.js.map +1 -1
- package/dist/internal/local-env.d.ts.map +1 -1
- package/dist/internal/local-env.js +9 -2
- package/dist/internal/local-env.js.map +1 -1
- package/dist/server.bundle.mjs +7085 -2832
- package/dist/start-local.bundle.mjs +8269 -3656
- package/package.json +7 -5
- package/dist/connectors/google_photos.ts +0 -776
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome Bookmarks Connector — Owletto for Chrome only.
|
|
3
|
+
*
|
|
4
|
+
* Opt-in ambient feed. The Chrome extension advertises capability
|
|
5
|
+
* `browser.bookmarks` when the user grants the `bookmarks` Chrome
|
|
6
|
+
* permission via the sidepanel Permissions panel; the gateway then
|
|
7
|
+
* auto-wires this connector.
|
|
8
|
+
*
|
|
9
|
+
* Emits one event per bookmark (with its folder path). Backfills the
|
|
10
|
+
* full tree on first sync, then streams `bookmarks.onCreated/onRemoved/
|
|
11
|
+
* onChanged/onMoved` thereafter.
|
|
12
|
+
*
|
|
13
|
+
* Cloud-side `sync()` / `execute()` throw — actual work happens in
|
|
14
|
+
* apps/chrome/feeds-bookmarks.js in the extension.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type ActionResult,
|
|
19
|
+
type ConnectorDefinition,
|
|
20
|
+
ConnectorRuntime,
|
|
21
|
+
type SyncContext,
|
|
22
|
+
type SyncResult,
|
|
23
|
+
} from '@lobu/connector-sdk';
|
|
24
|
+
|
|
25
|
+
const BRIDGE_ONLY =
|
|
26
|
+
'chrome.bookmarks runs only on a worker advertising capability "browser.bookmarks" (Owletto for Chrome with bookmarks permission granted).';
|
|
27
|
+
|
|
28
|
+
export default class ChromeBookmarksConnector extends ConnectorRuntime {
|
|
29
|
+
readonly definition: ConnectorDefinition = {
|
|
30
|
+
key: 'chrome.bookmarks',
|
|
31
|
+
name: 'Chrome bookmarks',
|
|
32
|
+
description:
|
|
33
|
+
"Bookmarks (and folder structure) from the paired Chrome profile. Opt-in — requires the user to grant the extension's optional `bookmarks` permission.",
|
|
34
|
+
version: '0.1.0',
|
|
35
|
+
faviconDomain: 'google.com',
|
|
36
|
+
requiredCapability: 'browser.bookmarks',
|
|
37
|
+
runtime: { platforms: ['chrome-extension'] as unknown as ['macos'] },
|
|
38
|
+
authSchema: { methods: [{ type: 'none' }] },
|
|
39
|
+
feeds: {
|
|
40
|
+
bookmarks: {
|
|
41
|
+
key: 'bookmarks',
|
|
42
|
+
name: 'Bookmarks',
|
|
43
|
+
description:
|
|
44
|
+
'One event per bookmark. Backfills the full tree on first sync via chrome.bookmarks.getTree, then streams onCreated / onRemoved / onChanged / onMoved.',
|
|
45
|
+
configSchema: { type: 'object', properties: {} },
|
|
46
|
+
eventKinds: {
|
|
47
|
+
bookmark: {
|
|
48
|
+
description: 'One row per bookmark add/remove/edit.',
|
|
49
|
+
metadataSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
required: ['source', 'origin_id', 'event_type'],
|
|
52
|
+
properties: {
|
|
53
|
+
source: { type: 'string', const: 'chrome_bookmarks' },
|
|
54
|
+
origin_id: { type: 'string' },
|
|
55
|
+
event_type: {
|
|
56
|
+
enum: ['created', 'removed', 'changed', 'moved'],
|
|
57
|
+
},
|
|
58
|
+
bookmark_id: { type: 'string' },
|
|
59
|
+
title: { type: 'string' },
|
|
60
|
+
url: { type: 'string' },
|
|
61
|
+
parent_folder_id: { type: 'string' },
|
|
62
|
+
parent_folder_path: { type: 'string' },
|
|
63
|
+
date_added: { type: 'string', format: 'date-time' },
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
async sync(_ctx: SyncContext): Promise<SyncResult> {
|
|
73
|
+
throw new Error(BRIDGE_ONLY);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async execute(): Promise<ActionResult> {
|
|
77
|
+
throw new Error(BRIDGE_ONLY);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome Downloads Connector — Owletto for Chrome only.
|
|
3
|
+
*
|
|
4
|
+
* Opt-in ambient feed. The Chrome extension advertises capability
|
|
5
|
+
* `browser.downloads` when the user grants the `downloads` Chrome
|
|
6
|
+
* permission via the sidepanel Permissions panel; the gateway then
|
|
7
|
+
* auto-wires this connector.
|
|
8
|
+
*
|
|
9
|
+
* Emits one event per download (filename, source URL, mime type, size,
|
|
10
|
+
* finish time). Backfills recent downloads on first sync via
|
|
11
|
+
* chrome.downloads.search({}), then streams chrome.downloads.onCreated /
|
|
12
|
+
* onChanged.
|
|
13
|
+
*
|
|
14
|
+
* Cloud-side `sync()` / `execute()` throw — actual work happens in
|
|
15
|
+
* apps/chrome/feeds-downloads.js in the extension.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
type ActionResult,
|
|
20
|
+
type ConnectorDefinition,
|
|
21
|
+
ConnectorRuntime,
|
|
22
|
+
type SyncContext,
|
|
23
|
+
type SyncResult,
|
|
24
|
+
} from '@lobu/connector-sdk';
|
|
25
|
+
|
|
26
|
+
const BRIDGE_ONLY =
|
|
27
|
+
'chrome.downloads runs only on a worker advertising capability "browser.downloads" (Owletto for Chrome with downloads permission granted).';
|
|
28
|
+
|
|
29
|
+
export default class ChromeDownloadsConnector extends ConnectorRuntime {
|
|
30
|
+
readonly definition: ConnectorDefinition = {
|
|
31
|
+
key: 'chrome.downloads',
|
|
32
|
+
name: 'Chrome downloads',
|
|
33
|
+
description:
|
|
34
|
+
"Files downloaded in the paired Chrome profile, with their source URLs. Opt-in — requires the user to grant the extension's optional `downloads` permission.",
|
|
35
|
+
version: '0.1.0',
|
|
36
|
+
faviconDomain: 'google.com',
|
|
37
|
+
requiredCapability: 'browser.downloads',
|
|
38
|
+
runtime: { platforms: ['chrome-extension'] as unknown as ['macos'] },
|
|
39
|
+
authSchema: { methods: [{ type: 'none' }] },
|
|
40
|
+
feeds: {
|
|
41
|
+
downloads: {
|
|
42
|
+
key: 'downloads',
|
|
43
|
+
name: 'Downloads',
|
|
44
|
+
description:
|
|
45
|
+
'One event per download. Backfills via chrome.downloads.search({}), then streams onCreated / onChanged (state=complete).',
|
|
46
|
+
configSchema: { type: 'object', properties: {} },
|
|
47
|
+
eventKinds: {
|
|
48
|
+
download: {
|
|
49
|
+
description: 'One row per file the user downloaded.',
|
|
50
|
+
metadataSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
required: ['source', 'origin_id'],
|
|
53
|
+
properties: {
|
|
54
|
+
source: { type: 'string', const: 'chrome_downloads' },
|
|
55
|
+
origin_id: { type: 'string' },
|
|
56
|
+
download_id: { type: 'integer' },
|
|
57
|
+
filename: { type: 'string' },
|
|
58
|
+
source_url: { type: 'string', format: 'uri' },
|
|
59
|
+
referrer: { type: 'string' },
|
|
60
|
+
mime: { type: 'string' },
|
|
61
|
+
bytes: { type: 'integer' },
|
|
62
|
+
started_at: { type: 'string', format: 'date-time' },
|
|
63
|
+
finished_at: { type: 'string', format: 'date-time' },
|
|
64
|
+
state: { type: 'string' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
async sync(_ctx: SyncContext): Promise<SyncResult> {
|
|
74
|
+
throw new Error(BRIDGE_ONLY);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async execute(): Promise<ActionResult> {
|
|
78
|
+
throw new Error(BRIDGE_ONLY);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chrome History Connector — Owletto for Chrome only.
|
|
3
|
+
*
|
|
4
|
+
* Opt-in ambient feed. The Chrome extension advertises capability
|
|
5
|
+
* `browser.history` when the user grants the `history` Chrome permission
|
|
6
|
+
* via the sidepanel Permissions panel; the gateway then auto-wires this
|
|
7
|
+
* connector to the paired chrome-extension device.
|
|
8
|
+
*
|
|
9
|
+
* Emits one event per page load (URL, title, visit time, transition type).
|
|
10
|
+
* The backfill feed seeds with up to 90 days of history on first sync; the
|
|
11
|
+
* live feed streams `history.onVisited` thereafter.
|
|
12
|
+
*
|
|
13
|
+
* Cloud-side `sync()` / `execute()` throw — actual work happens in
|
|
14
|
+
* apps/chrome/feeds-history.js in the extension.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type ActionResult,
|
|
19
|
+
type ConnectorDefinition,
|
|
20
|
+
ConnectorRuntime,
|
|
21
|
+
type SyncContext,
|
|
22
|
+
type SyncResult,
|
|
23
|
+
} from '@lobu/connector-sdk';
|
|
24
|
+
|
|
25
|
+
const BRIDGE_ONLY =
|
|
26
|
+
'chrome.history runs only on a worker advertising capability "browser.history" (Owletto for Chrome with history permission granted).';
|
|
27
|
+
|
|
28
|
+
export default class ChromeHistoryConnector extends ConnectorRuntime {
|
|
29
|
+
readonly definition: ConnectorDefinition = {
|
|
30
|
+
key: 'chrome.history',
|
|
31
|
+
name: 'Chrome history',
|
|
32
|
+
description:
|
|
33
|
+
"Every page the user visits in their paired Chrome profile, with visit time + transition type. Opt-in — requires the user to grant the extension's optional `history` permission.",
|
|
34
|
+
version: '0.1.0',
|
|
35
|
+
faviconDomain: 'google.com',
|
|
36
|
+
requiredCapability: 'browser.history',
|
|
37
|
+
runtime: { platforms: ['chrome-extension'] as unknown as ['macos'] },
|
|
38
|
+
authSchema: { methods: [{ type: 'none' }] },
|
|
39
|
+
feeds: {
|
|
40
|
+
visits: {
|
|
41
|
+
key: 'visits',
|
|
42
|
+
name: 'Visits',
|
|
43
|
+
description:
|
|
44
|
+
'One event per page visit. Backfills ~90 days on first sync (chrome.history.search), then streams new visits via the chrome.history.onVisited listener.',
|
|
45
|
+
configSchema: { type: 'object', properties: {} },
|
|
46
|
+
eventKinds: {
|
|
47
|
+
page_visit: {
|
|
48
|
+
description: 'One row per visit observed.',
|
|
49
|
+
metadataSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
required: ['source', 'origin_id', 'url', 'visit_time'],
|
|
52
|
+
properties: {
|
|
53
|
+
source: { type: 'string', const: 'chrome_history' },
|
|
54
|
+
origin_id: { type: 'string' },
|
|
55
|
+
url: { type: 'string', format: 'uri' },
|
|
56
|
+
title: { type: 'string' },
|
|
57
|
+
visit_time: { type: 'string', format: 'date-time' },
|
|
58
|
+
transition_type: {
|
|
59
|
+
description:
|
|
60
|
+
'How the user got to the page: link, typed, auto_bookmark, auto_subframe, manual_subframe, generated, start_page, form_submit, reload, keyword, keyword_generated.',
|
|
61
|
+
type: 'string',
|
|
62
|
+
},
|
|
63
|
+
visit_id: { type: 'integer' },
|
|
64
|
+
visit_count: { type: 'integer' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
async sync(_ctx: SyncContext): Promise<SyncResult> {
|
|
74
|
+
throw new Error(BRIDGE_ONLY);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async execute(): Promise<ActionResult> {
|
|
78
|
+
throw new Error(BRIDGE_ONLY);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -208,6 +208,7 @@ export default class GitHubConnector extends ConnectorRuntime {
|
|
|
208
208
|
name: 'GitHub',
|
|
209
209
|
description: 'Collects GitHub issues/discussions and executes repo actions.',
|
|
210
210
|
version: '1.2.0',
|
|
211
|
+
faviconDomain: 'github.com',
|
|
211
212
|
authSchema: {
|
|
212
213
|
methods: [
|
|
213
214
|
{
|
|
@@ -255,7 +255,14 @@ export default class GoogleCalendarConnector extends ConnectorRuntime {
|
|
|
255
255
|
let pageToken: string | undefined;
|
|
256
256
|
let nextSyncToken: string | undefined;
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
// Safety bound — at 250 events/page, 200 pages = 50k events, more than
|
|
259
|
+
// any reasonable calendar window. Stops a runaway loop if the upstream
|
|
260
|
+
// ever returns a self-referential page token.
|
|
261
|
+
const MAX_PAGES = 200;
|
|
262
|
+
let pages = 0;
|
|
263
|
+
|
|
264
|
+
while (pages < MAX_PAGES) {
|
|
265
|
+
pages++;
|
|
259
266
|
// Always request a full page — `maxResults` is a soft cap on *stored*
|
|
260
267
|
// events, not a reason to shrink the request size (shrinking to 1 once the
|
|
261
268
|
// cap is hit would crawl a busy calendar one event per round-trip).
|
|
@@ -350,7 +357,12 @@ export default class GoogleCalendarConnector extends ConnectorRuntime {
|
|
|
350
357
|
let pageToken: string | undefined;
|
|
351
358
|
let nextSyncToken: string | undefined;
|
|
352
359
|
|
|
353
|
-
|
|
360
|
+
// Same hard ceiling as the full-sync path — defensive only.
|
|
361
|
+
const MAX_PAGES = 200;
|
|
362
|
+
let pages = 0;
|
|
363
|
+
|
|
364
|
+
while (pages < MAX_PAGES) {
|
|
365
|
+
pages++;
|
|
354
366
|
const params = new URLSearchParams({
|
|
355
367
|
maxResults: String(Math.max(1, Math.min(250, maxResults - events.length))),
|
|
356
368
|
syncToken,
|
|
@@ -136,6 +136,12 @@ async function fetchReviewsPage(
|
|
|
136
136
|
|
|
137
137
|
if (!res.ok) {
|
|
138
138
|
if (res.status === 404) throw new Error('App not found (404)');
|
|
139
|
+
if (res.status === 429) {
|
|
140
|
+
const retryAfter = res.headers.get('Retry-After');
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Google Play rate limit (429). Retry after ${retryAfter ?? 'unknown'} seconds.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
139
145
|
throw new Error(`Google Play request failed: ${res.status} ${res.statusText}`);
|
|
140
146
|
}
|
|
141
147
|
|
|
@@ -143,14 +149,28 @@ async function fetchReviewsPage(
|
|
|
143
149
|
|
|
144
150
|
// Response starts with ")]}'" (security prefix), then a newline, then JSON.
|
|
145
151
|
// The library skips the first 5 characters.
|
|
146
|
-
|
|
152
|
+
// Wrap parse in try/catch — Google sometimes returns an HTML interstitial
|
|
153
|
+
// (captcha / geo-block / maintenance) with status 200, which would bubble up
|
|
154
|
+
// as an unhelpful SyntaxError otherwise.
|
|
155
|
+
let outer: any;
|
|
156
|
+
try {
|
|
157
|
+
outer = JSON.parse(text.substring(5));
|
|
158
|
+
} catch {
|
|
159
|
+
const preview = text.substring(0, 120).replace(/\s+/g, ' ');
|
|
160
|
+
throw new Error(`Google Play returned non-JSON response: ${preview}`);
|
|
161
|
+
}
|
|
147
162
|
const innerJson: string | null = outer?.[0]?.[2];
|
|
148
163
|
|
|
149
164
|
if (innerJson === null || innerJson === undefined) {
|
|
150
165
|
return { reviews: [], nextToken: null };
|
|
151
166
|
}
|
|
152
167
|
|
|
153
|
-
|
|
168
|
+
let data: any;
|
|
169
|
+
try {
|
|
170
|
+
data = JSON.parse(innerJson);
|
|
171
|
+
} catch {
|
|
172
|
+
throw new Error('Google Play returned malformed inner JSON payload');
|
|
173
|
+
}
|
|
154
174
|
return {
|
|
155
175
|
reviews: extractReviews(data, appId),
|
|
156
176
|
nextToken: extractPaginationToken(data),
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type SyncContext,
|
|
17
17
|
type SyncResult,
|
|
18
18
|
} from '@lobu/connector-sdk';
|
|
19
|
+
import { validatePublicUrl } from './browser-scraper-utils.ts';
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
21
22
|
// Algolia HN API types
|
|
@@ -261,11 +262,36 @@ export default class HackerNewsConnector extends ConnectorRuntime {
|
|
|
261
262
|
`&numericFilters=${encodeURIComponent(`created_at_i>${lookbackTimestamp}`)}`;
|
|
262
263
|
|
|
263
264
|
const response = await fetch(url);
|
|
265
|
+
|
|
266
|
+
// Honor Algolia's rate-limit response so we don't hammer them and turn
|
|
267
|
+
// a transient 429 into "Unexpected token < in JSON" when the next call
|
|
268
|
+
// returns an HTML error page.
|
|
269
|
+
if (response.status === 429) {
|
|
270
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
271
|
+
const waitMs = retryAfter ? Math.min(60_000, Math.max(1, Number(retryAfter)) * 1000) : 5000;
|
|
272
|
+
await this.sleep(Number.isFinite(waitMs) ? waitMs : 5000);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
264
276
|
if (!response.ok) {
|
|
265
|
-
|
|
277
|
+
const text = await response.text().catch(() => '');
|
|
278
|
+
throw new Error(`Algolia API error (${response.status}): ${text}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Algolia normally returns JSON, but proxies/captive portals occasionally
|
|
282
|
+
// return HTML. Surface a useful error instead of a bare SyntaxError that
|
|
283
|
+
// makes the connector look broken when the upstream is at fault.
|
|
284
|
+
let data: AlgoliaResponse;
|
|
285
|
+
try {
|
|
286
|
+
data = (await response.json()) as AlgoliaResponse;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
289
|
+
throw new Error(`Algolia API returned non-JSON response: ${message}`);
|
|
266
290
|
}
|
|
267
291
|
|
|
268
|
-
|
|
292
|
+
if (!data || !Array.isArray(data.hits)) {
|
|
293
|
+
throw new Error('Algolia API returned an unexpected response shape');
|
|
294
|
+
}
|
|
269
295
|
|
|
270
296
|
for (const hit of data.hits) {
|
|
271
297
|
if (contentType === 'comment') {
|
|
@@ -404,6 +430,15 @@ export default class HackerNewsConnector extends ConnectorRuntime {
|
|
|
404
430
|
|
|
405
431
|
private async fetchExternalContent(url: string): Promise<string | null> {
|
|
406
432
|
try {
|
|
433
|
+
// SSRF guard — `url` is supplied by whoever submitted the HN story and
|
|
434
|
+
// is therefore attacker-controllable. Refuse to fetch private/internal
|
|
435
|
+
// addresses (loopback, 169.254.169.254 cloud metadata, RFC1918, etc.).
|
|
436
|
+
try {
|
|
437
|
+
validatePublicUrl(url);
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
407
442
|
const controller = new AbortController();
|
|
408
443
|
const timeoutId = setTimeout(() => controller.abort(), this.CONTENT_FETCH_TIMEOUT);
|
|
409
444
|
|
package/dist/connectors/index.ts
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
export * from './apple_health.ts';
|
|
2
|
+
export * from './apple_photos.ts';
|
|
2
3
|
export * from './apple_screen_time.ts';
|
|
3
4
|
export * from './local_directory.ts';
|
|
4
5
|
export * from './browser-scraper-utils.ts';
|
|
5
6
|
export * from './capterra.ts';
|
|
7
|
+
// Chrome — paired Chrome profile via the Owletto for Chrome extension.
|
|
8
|
+
// One connector exposing feeds.open_tabs (auto-wired tab snapshot) +
|
|
9
|
+
// actions.{navigate, get_accessibility_tree, click_ref, type_ref,
|
|
10
|
+
// wait_for_selector, screenshot, evaluate} (one-shot tools the extension
|
|
11
|
+
// dispatcher executes via chrome.debugger + a custom DOM accessibility
|
|
12
|
+
// snapshot). Replaces the four prior standalone connectors
|
|
13
|
+
// (chrome.tabs / browser.evaluate / browser.page_text / browser.fill_form).
|
|
14
|
+
// chrome.history / chrome.bookmarks / chrome.downloads are opt-in
|
|
15
|
+
// ambient feeds that auto-wire when the user grants the corresponding
|
|
16
|
+
// optional permission in the extension's Permissions panel.
|
|
17
|
+
export * from './chrome.ts';
|
|
18
|
+
export * from './chrome_history.ts';
|
|
19
|
+
export * from './chrome_bookmarks.ts';
|
|
20
|
+
export * from './chrome_downloads.ts';
|
|
6
21
|
export * from './g2.ts';
|
|
7
22
|
export * from './github.ts';
|
|
8
23
|
export * from './glassdoor.ts';
|
|
9
24
|
export * from './gmaps.ts';
|
|
10
25
|
export * from './google_calendar.ts';
|
|
11
26
|
export * from './google_gmail.ts';
|
|
12
|
-
export * from './google_photos.ts';
|
|
13
27
|
export * from './google_play.ts';
|
|
14
28
|
export * from './hackernews.ts';
|
|
15
29
|
export * from './ios_appstore.ts';
|
|
@@ -142,12 +142,9 @@ function extractAmountAndCurrency(
|
|
|
142
142
|
const amt = record.amount;
|
|
143
143
|
if (amt && typeof amt === "object") {
|
|
144
144
|
const obj = amt as Record<string, unknown>;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: typeof obj.amount === "number"
|
|
149
|
-
? obj.amount
|
|
150
|
-
: null;
|
|
145
|
+
let value: number | null = null;
|
|
146
|
+
if (typeof obj.value === "number") value = obj.value;
|
|
147
|
+
else if (typeof obj.amount === "number") value = obj.amount;
|
|
151
148
|
const currency = typeof obj.currency === "string" ? obj.currency : null;
|
|
152
149
|
if (value !== null && currency) return { amount: value, currency };
|
|
153
150
|
}
|
|
@@ -200,13 +197,13 @@ function extractBalance(
|
|
|
200
197
|
record: Record<string, unknown>,
|
|
201
198
|
currency: string,
|
|
202
199
|
): number | undefined {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
200
|
+
let raw: unknown;
|
|
201
|
+
if (typeof record.balance === "number") {
|
|
202
|
+
raw = record.balance;
|
|
203
|
+
} else if (record.balance && typeof record.balance === "object") {
|
|
204
|
+
const obj = record.balance as Record<string, unknown>;
|
|
205
|
+
raw = obj.value ?? obj.amount;
|
|
206
|
+
}
|
|
210
207
|
if (typeof raw !== "number" || !Number.isFinite(raw)) return undefined;
|
|
211
208
|
return Number.isInteger(raw) ? minorUnitsToMajor(raw, currency) : raw;
|
|
212
209
|
}
|
package/dist/connectors/rss.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type SyncContext,
|
|
16
16
|
type SyncResult,
|
|
17
17
|
} from '@lobu/connector-sdk';
|
|
18
|
+
import { validatePublicUrl } from './browser-scraper-utils.ts';
|
|
18
19
|
|
|
19
20
|
// ---------------------------------------------------------------------------
|
|
20
21
|
// Types
|
|
@@ -211,6 +212,11 @@ export default class RSSConnector extends ConnectorRuntime {
|
|
|
211
212
|
// -------------------------------------------------------------------------
|
|
212
213
|
|
|
213
214
|
private async fetchAndParseFeed(feedUrl: string, maxItems: number): Promise<RSSFeedItem[]> {
|
|
215
|
+
// SSRF guard at the trust boundary. `feed_urls` is operator/user supplied
|
|
216
|
+
// via connector config and must not be allowed to target loopback, RFC1918,
|
|
217
|
+
// or cloud-metadata IPs from the gateway process.
|
|
218
|
+
validatePublicUrl(feedUrl);
|
|
219
|
+
|
|
214
220
|
const controller = new AbortController();
|
|
215
221
|
const timeoutId = setTimeout(() => controller.abort(), this.FETCH_TIMEOUT_MS);
|
|
216
222
|
|
|
@@ -222,18 +228,13 @@ export default class RSSConnector extends ConnectorRuntime {
|
|
|
222
228
|
Accept: 'application/rss+xml, application/atom+xml, application/xml, text/xml, */*',
|
|
223
229
|
},
|
|
224
230
|
});
|
|
225
|
-
|
|
226
|
-
clearTimeout(timeoutId);
|
|
227
|
-
|
|
228
231
|
if (!response.ok) {
|
|
229
232
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
230
233
|
}
|
|
231
|
-
|
|
232
234
|
const xml = await response.text();
|
|
233
235
|
return this.parseXml(xml, feedUrl, maxItems);
|
|
234
|
-
}
|
|
236
|
+
} finally {
|
|
235
237
|
clearTimeout(timeoutId);
|
|
236
|
-
throw err;
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
|
|
@@ -413,8 +414,32 @@ export default class RSSConnector extends ConnectorRuntime {
|
|
|
413
414
|
case '#39':
|
|
414
415
|
return "'";
|
|
415
416
|
default:
|
|
416
|
-
|
|
417
|
-
|
|
417
|
+
// Use fromCodePoint, not fromCharCode — astral-plane characters
|
|
418
|
+
// (emoji, CJK extension B+, etc.) have code points > 0xFFFF which
|
|
419
|
+
// fromCharCode silently truncates, producing mojibake in feed
|
|
420
|
+
// titles. Guard the range so a malformed entity doesn't throw.
|
|
421
|
+
if (hex) {
|
|
422
|
+
const cp = parseInt(hex, 16);
|
|
423
|
+
if (Number.isFinite(cp) && cp >= 0 && cp <= 0x10ffff) {
|
|
424
|
+
try {
|
|
425
|
+
return String.fromCodePoint(cp);
|
|
426
|
+
} catch {
|
|
427
|
+
return _match;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return _match;
|
|
431
|
+
}
|
|
432
|
+
if (decimal) {
|
|
433
|
+
const cp = parseInt(decimal, 10);
|
|
434
|
+
if (Number.isFinite(cp) && cp >= 0 && cp <= 0x10ffff) {
|
|
435
|
+
try {
|
|
436
|
+
return String.fromCodePoint(cp);
|
|
437
|
+
} catch {
|
|
438
|
+
return _match;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return _match;
|
|
442
|
+
}
|
|
418
443
|
return _match;
|
|
419
444
|
}
|
|
420
445
|
}
|
|
@@ -98,7 +98,11 @@ export default class TrustpilotConnector extends ConnectorRuntime {
|
|
|
98
98
|
throw new Error('Either business_url or business_name is required');
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
// encodeURIComponent the user-supplied businessName so a value like
|
|
102
|
+
// "../search?foo=bar" can't escape the /review/ path on trustpilot.com.
|
|
103
|
+
const baseUrl =
|
|
104
|
+
businessUrl ||
|
|
105
|
+
`https://www.trustpilot.com/review/${encodeURIComponent(businessName ?? '')}`;
|
|
102
106
|
validateUrlDomain(baseUrl, 'trustpilot.com');
|
|
103
107
|
|
|
104
108
|
const userDataDir = getBrowserUserDataDir(ctx.sessionState);
|
|
@@ -161,27 +165,34 @@ export default class TrustpilotConnector extends ConnectorRuntime {
|
|
|
161
165
|
// Filter reviews with meaningful content (more than 10 chars)
|
|
162
166
|
const reviews: TrustpilotReview[] = rawReviews.filter((r) => r.text && r.text.length > 10);
|
|
163
167
|
|
|
164
|
-
// Transform to EventEnvelope format
|
|
165
|
-
|
|
168
|
+
// Transform to EventEnvelope format. Drop rows whose `date` attribute
|
|
169
|
+
// was missing/invalid in the DOM — `new Date("")` yields an Invalid
|
|
170
|
+
// Date, which downstream sorting/checkpointing then can't compare, and
|
|
171
|
+
// an empty `date` made `origin_id` collide on `-<author>` across rows.
|
|
172
|
+
const events: EventEnvelope[] = reviews.flatMap((review) => {
|
|
166
173
|
const content = review.title ? `${review.title}\n\n${review.text}` : review.text;
|
|
167
|
-
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
174
|
+
const parsedDate = review.date ? new Date(review.date) : null;
|
|
175
|
+
if (!parsedDate || Number.isNaN(parsedDate.getTime())) return [];
|
|
176
|
+
|
|
177
|
+
return [
|
|
178
|
+
{
|
|
179
|
+
origin_id: `${review.date}-${review.author}`,
|
|
180
|
+
payload_text: content,
|
|
181
|
+
author_name: review.author,
|
|
182
|
+
occurred_at: parsedDate,
|
|
183
|
+
origin_type: 'review',
|
|
184
|
+
score: calculateEngagementScore('trustpilot', {
|
|
185
|
+
rating: review.rating,
|
|
186
|
+
helpful_count: 0,
|
|
187
|
+
}),
|
|
188
|
+
source_url: baseUrl,
|
|
189
|
+
metadata: {
|
|
190
|
+
rating: review.rating,
|
|
191
|
+
helpful_count: 0,
|
|
192
|
+
title: review.title,
|
|
193
|
+
},
|
|
183
194
|
},
|
|
184
|
-
|
|
195
|
+
];
|
|
185
196
|
});
|
|
186
197
|
|
|
187
198
|
return {
|