@ontrove/sdk 0.1.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/dist/define.d.ts +49 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +57 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/manifest.d.ts +52 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +144 -0
- package/dist/runtime.d.ts +70 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +132 -0
- package/dist/types.d.ts +250 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/package.json +52 -0
- package/src/define.ts +70 -0
- package/src/index.ts +57 -0
- package/src/manifest.ts +173 -0
- package/src/runtime.ts +195 -0
- package/src/types.ts +252 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hollyburn Analytics Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# @ontrove/sdk
|
|
2
|
+
|
|
3
|
+
The thin standard library for authoring **Trove connectors**. You write a
|
|
4
|
+
`sync(ctx)` that fetches from a source and returns documents; the SDK owns the
|
|
5
|
+
document shape, the typed `ctx` capability object, the watermark/cursor model,
|
|
6
|
+
the local-run harness the CLI drives, and manifest validation.
|
|
7
|
+
|
|
8
|
+
It is symmetric with the hosted-MCP SDK
|
|
9
|
+
([`@ontrove/mcp`](https://www.npmjs.com/package/@ontrove/mcp)) `defineMcpServer`
|
|
10
|
+
contract: a connector returns documents to be _stored_ (batch `sync`); an MCP
|
|
11
|
+
server returns tool results to be _read live_.
|
|
12
|
+
|
|
13
|
+
See the full [SDK Reference](https://docs.ontrove.sh/connectors/sdk-reference/),
|
|
14
|
+
the [manifest.json reference](https://docs.ontrove.sh/connectors/manifest/), and
|
|
15
|
+
the [cursors & sources model](https://docs.ontrove.sh/connectors/cursors-and-sources/)
|
|
16
|
+
in the docs.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @ontrove/sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
A connector's `index.ts` must `export default defineConnector(...)`. Your `sync`
|
|
27
|
+
fetches from the source and returns documents — each field maps 1:1 onto the
|
|
28
|
+
`IngestDocumentInput` the Mac app pushes via `ingestDocuments`.
|
|
29
|
+
|
|
30
|
+
### Hacker News front page
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { defineConnector } from '@ontrove/sdk';
|
|
34
|
+
|
|
35
|
+
export default defineConnector({
|
|
36
|
+
async sync(ctx) {
|
|
37
|
+
const res = await ctx.fetch('https://hn.algolia.com/api/v1/search?tags=front_page');
|
|
38
|
+
const { hits } = await res.json();
|
|
39
|
+
ctx.log(`fetched ${hits.length} front-page stories`);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
documents: hits.map((hit) => ({
|
|
43
|
+
id: hit.objectID, // → externalId; the dedup key
|
|
44
|
+
title: hit.title,
|
|
45
|
+
text: hit.story_text ?? hit.title,
|
|
46
|
+
url: hit.url,
|
|
47
|
+
author: hit.author,
|
|
48
|
+
date: new Date(hit.created_at_i * 1000).toISOString(),
|
|
49
|
+
contentType: 'bookmark',
|
|
50
|
+
})),
|
|
51
|
+
// Advance the cursor to the newest story's timestamp.
|
|
52
|
+
cursor: { type: 'date', value: new Date(hits[0].created_at_i * 1000).toISOString() },
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### RSS feed (incremental, date watermark)
|
|
59
|
+
|
|
60
|
+
> `parseRss` / `stripHtml` below are **your own helpers** — the SDK ships no XML
|
|
61
|
+
> parser. Bring your own (e.g. [`fast-xml-parser`](https://www.npmjs.com/package/fast-xml-parser)).
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { defineConnector } from '@ontrove/sdk';
|
|
65
|
+
// import { parseRss, stripHtml } from './rss-helpers'; // your own — see note above
|
|
66
|
+
|
|
67
|
+
export default defineConnector({
|
|
68
|
+
async sync(ctx) {
|
|
69
|
+
const feedUrl = ctx.config.feedUrl as string;
|
|
70
|
+
const since = ctx.cursor.type === 'date' ? new Date(ctx.cursor.value) : new Date(0);
|
|
71
|
+
|
|
72
|
+
const res = await ctx.fetch(feedUrl);
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
// Throw to fail the run; the Mac app retries next tick. Already-pushed docs persist.
|
|
75
|
+
throw new Error(`feed returned ${res.status} ${res.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const items = parseRss(await res.text()).filter((i) => new Date(i.pubDate) > since);
|
|
79
|
+
const documents = items.map((item) => ({
|
|
80
|
+
id: item.guid,
|
|
81
|
+
title: item.title,
|
|
82
|
+
text: stripHtml(item.contentEncoded ?? item.description),
|
|
83
|
+
url: item.link,
|
|
84
|
+
author: item.creator ?? feedUrl,
|
|
85
|
+
date: item.pubDate,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
const newest = items[0]?.pubDate;
|
|
89
|
+
return newest
|
|
90
|
+
? { documents, cursor: { type: 'date', value: newest } }
|
|
91
|
+
: { documents };
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
A bare array is accepted too — `return documents;` is shorthand for
|
|
97
|
+
`{ documents }` with no cursor change.
|
|
98
|
+
|
|
99
|
+
## The `ctx` object
|
|
100
|
+
|
|
101
|
+
| Member | Type | Description |
|
|
102
|
+
| ------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
|
|
103
|
+
| `ctx.config` | `C` (typed preferences) | The user's setup preferences. **Never credentials.** |
|
|
104
|
+
| `ctx.cursor` | `Watermark` (read-only) | The source's current watermark; `{ type: 'none' }` on first sync. |
|
|
105
|
+
| `ctx.fetch` | `(url, init?) => Promise<Response>` | Standard `fetch` — routes through the Mac app's networking. |
|
|
106
|
+
| `ctx.log` | `(...args) => void` | Structured log entry, surfaced in connector logs. |
|
|
107
|
+
| `ctx.now` | `() => Date` | Injected clock (deterministic under test). |
|
|
108
|
+
|
|
109
|
+
`ctx.credentials` (Keychain-resolved auth) and `ctx.browser` (Playwright) are
|
|
110
|
+
**proposed**, not part of the shipped contract, and intentionally absent.
|
|
111
|
+
|
|
112
|
+
## The document shape → `IngestDocumentInput`
|
|
113
|
+
|
|
114
|
+
`ConnectorDocument` maps 1:1 onto the GraphQL `IngestDocumentInput` wire type:
|
|
115
|
+
|
|
116
|
+
| `ConnectorDocument` | `IngestDocumentInput` | Required |
|
|
117
|
+
| ------------------- | --------------------- | ------------------------------ |
|
|
118
|
+
| `id` | `externalId` | **Yes** |
|
|
119
|
+
| `title` | `title` | No |
|
|
120
|
+
| `text` | `text` | One of `text` / `audioUrl` |
|
|
121
|
+
| `audioUrl` | `audioUrl` | One of `text` / `audioUrl` |
|
|
122
|
+
| `url` | `url` | No |
|
|
123
|
+
| `author` | `author` | No |
|
|
124
|
+
| `date` | `date` (ISO 8601) | No |
|
|
125
|
+
| `tags` | `tags` | No |
|
|
126
|
+
| `metadata` | `metadata` (JSON) | No |
|
|
127
|
+
| `contentType` | `contentType` | No (default `text`) |
|
|
128
|
+
|
|
129
|
+
Dedup is keyed on `(source, id)`, so `sync` is safe to retry — re-returning the
|
|
130
|
+
same `id` is skipped.
|
|
131
|
+
|
|
132
|
+
## Watermarks
|
|
133
|
+
|
|
134
|
+
A `Watermark` describes how a source resumes between syncs:
|
|
135
|
+
|
|
136
|
+
- `{ type: 'date', value }` — time-ordered sources with "since <date>" filtering (RSS, most APIs).
|
|
137
|
+
- `{ type: 'idSet', values, max? }` — monotonic ids, no reliable date filter.
|
|
138
|
+
- `{ type: 'none' }` — re-fetch everything; rely on dedup. Always correct, less efficient.
|
|
139
|
+
|
|
140
|
+
## Local run harness
|
|
141
|
+
|
|
142
|
+
`runConnector` is what `trove connector dev` / `trove connector test` call. It
|
|
143
|
+
builds a `ctx`, runs `sync`, validates and dedups the documents:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { runConnector } from '@ontrove/sdk';
|
|
147
|
+
import connector from './index.js';
|
|
148
|
+
|
|
149
|
+
const { documents, cursor, duplicatesSkipped } = await runConnector(connector, {
|
|
150
|
+
config: { feedUrl: 'https://example.com/feed.xml' },
|
|
151
|
+
cursor: { type: 'date', value: '2026-06-01T00:00:00Z' },
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Manifest validation
|
|
156
|
+
|
|
157
|
+
`validateConnectorManifest` backs `trove connector validate`. It checks required
|
|
158
|
+
fields and the `id`/`version` patterns, and lints `config` for credential-shaped
|
|
159
|
+
keys — the same spirit as the cloud's `validateConfig` (config holds preferences
|
|
160
|
+
only; credentials live in the macOS Keychain).
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { validateConnectorManifest } from '@ontrove/sdk';
|
|
164
|
+
|
|
165
|
+
const { valid, errors } = validateConnectorManifest(manifest);
|
|
166
|
+
if (!valid) throw new Error(errors.join('\n'));
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Support
|
|
170
|
+
|
|
171
|
+
Guides and the full reference live at [docs.ontrove.sh](https://docs.ontrove.sh).
|
|
172
|
+
|
|
173
|
+
Report bugs or security issues to <matt@hollyburnanalytics.com>.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
Released under the [MIT License](./LICENSE). © 2026 Hollyburn Analytics Inc.
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineConnector` — the single entry point for authoring a Trove connector
|
|
3
|
+
* (connectors/sdk-reference). It is the symmetric sibling of `defineMcpServer`
|
|
4
|
+
* in `@ontrove/mcp`.
|
|
5
|
+
*
|
|
6
|
+
* It is an identity function with light, eager validation: it confirms the value
|
|
7
|
+
* is a connector object with a `sync` method, so a typo (`snyc`, a missing
|
|
8
|
+
* `sync`, a non-function) fails at authoring/deploy time with a clear message
|
|
9
|
+
* rather than at the first scheduled run. It does **not** execute `sync` — that is
|
|
10
|
+
* {@link runConnector}'s job.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { ConnectorContext, ConnectorDocument, ConnectorSyncResult, TroveConnector } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Validate and return a connector definition unchanged.
|
|
17
|
+
*
|
|
18
|
+
* Using `defineConnector(...)` (rather than a bare `satisfies TroveConnector`)
|
|
19
|
+
* gives an eager runtime check: the CLI calls this when loading a connector
|
|
20
|
+
* module so a malformed export is rejected before any sync runs. The returned
|
|
21
|
+
* value is the same object, with full inferred types preserved.
|
|
22
|
+
*
|
|
23
|
+
* @typeParam C - The shape of the connector's typed `ctx.config` preferences.
|
|
24
|
+
* @param connector - The connector object to validate.
|
|
25
|
+
* @returns The same connector object.
|
|
26
|
+
* @throws {Error} If `connector` is not an object with a `sync` function.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* export default defineConnector({
|
|
31
|
+
* async sync(ctx) {
|
|
32
|
+
* const res = await ctx.fetch(ctx.config.feedUrl as string);
|
|
33
|
+
* return parseRss(await res.text());
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function defineConnector<C = Record<string, unknown>>(connector: TroveConnector<C>): TroveConnector<C>;
|
|
39
|
+
/**
|
|
40
|
+
* A connector authored inline as a single `sync` function, for the common case
|
|
41
|
+
* where the connector has no other members. Equivalent to
|
|
42
|
+
* `defineConnector({ sync })`.
|
|
43
|
+
*
|
|
44
|
+
* @typeParam C - The shape of the connector's typed `ctx.config` preferences.
|
|
45
|
+
* @param sync - The connector's `sync(ctx)` implementation.
|
|
46
|
+
* @returns A validated {@link TroveConnector} wrapping `sync`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function defineSync<C = Record<string, unknown>>(sync: (ctx: ConnectorContext<C>) => Promise<ConnectorSyncResult | ConnectorDocument[]>): TroveConnector<C>;
|
|
49
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,eAAe,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzD,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,GAC3B,cAAc,CAAC,CAAC,CAAC,CAQnB;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpD,IAAI,EAAE,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,mBAAmB,GAAG,iBAAiB,EAAE,CAAC,GACrF,cAAc,CAAC,CAAC,CAAC,CAEnB"}
|
package/dist/define.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineConnector` — the single entry point for authoring a Trove connector
|
|
3
|
+
* (connectors/sdk-reference). It is the symmetric sibling of `defineMcpServer`
|
|
4
|
+
* in `@ontrove/mcp`.
|
|
5
|
+
*
|
|
6
|
+
* It is an identity function with light, eager validation: it confirms the value
|
|
7
|
+
* is a connector object with a `sync` method, so a typo (`snyc`, a missing
|
|
8
|
+
* `sync`, a non-function) fails at authoring/deploy time with a clear message
|
|
9
|
+
* rather than at the first scheduled run. It does **not** execute `sync` — that is
|
|
10
|
+
* {@link runConnector}'s job.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Validate and return a connector definition unchanged.
|
|
16
|
+
*
|
|
17
|
+
* Using `defineConnector(...)` (rather than a bare `satisfies TroveConnector`)
|
|
18
|
+
* gives an eager runtime check: the CLI calls this when loading a connector
|
|
19
|
+
* module so a malformed export is rejected before any sync runs. The returned
|
|
20
|
+
* value is the same object, with full inferred types preserved.
|
|
21
|
+
*
|
|
22
|
+
* @typeParam C - The shape of the connector's typed `ctx.config` preferences.
|
|
23
|
+
* @param connector - The connector object to validate.
|
|
24
|
+
* @returns The same connector object.
|
|
25
|
+
* @throws {Error} If `connector` is not an object with a `sync` function.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* export default defineConnector({
|
|
30
|
+
* async sync(ctx) {
|
|
31
|
+
* const res = await ctx.fetch(ctx.config.feedUrl as string);
|
|
32
|
+
* return parseRss(await res.text());
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function defineConnector(connector) {
|
|
38
|
+
if (connector === null || typeof connector !== 'object') {
|
|
39
|
+
throw new Error('defineConnector: expected a connector object with a `sync` method');
|
|
40
|
+
}
|
|
41
|
+
if (typeof connector.sync !== 'function') {
|
|
42
|
+
throw new Error('defineConnector: connector must have a `sync(ctx)` function');
|
|
43
|
+
}
|
|
44
|
+
return connector;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A connector authored inline as a single `sync` function, for the common case
|
|
48
|
+
* where the connector has no other members. Equivalent to
|
|
49
|
+
* `defineConnector({ sync })`.
|
|
50
|
+
*
|
|
51
|
+
* @typeParam C - The shape of the connector's typed `ctx.config` preferences.
|
|
52
|
+
* @param sync - The connector's `sync(ctx)` implementation.
|
|
53
|
+
* @returns A validated {@link TroveConnector} wrapping `sync`.
|
|
54
|
+
*/
|
|
55
|
+
export function defineSync(sync) {
|
|
56
|
+
return defineConnector({ sync });
|
|
57
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ontrove/sdk` — the thin standard library for authoring **Trove connectors**.
|
|
3
|
+
* You write a `sync(ctx)` that fetches from a source and returns documents; the
|
|
4
|
+
* SDK owns the document shape (which maps 1:1 onto `IngestDocumentInput`), the
|
|
5
|
+
* typed `ctx` capability object, the watermark/cursor model, the local-run
|
|
6
|
+
* harness the CLI drives, and manifest validation.
|
|
7
|
+
*
|
|
8
|
+
* It is the symmetric sibling of `@ontrove/mcp`: a connector returns documents to
|
|
9
|
+
* be _stored_ (`defineConnector` + `sync`); an MCP server returns tool results to
|
|
10
|
+
* be _read live_ (`defineMcpServer`).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { defineConnector } from '@ontrove/sdk';
|
|
15
|
+
*
|
|
16
|
+
* export default defineConnector({
|
|
17
|
+
* async sync(ctx) {
|
|
18
|
+
* const res = await ctx.fetch('https://hn.algolia.com/api/v1/search?tags=front_page');
|
|
19
|
+
* const { hits } = await res.json();
|
|
20
|
+
* return hits.map((hit) => ({
|
|
21
|
+
* id: hit.objectID,
|
|
22
|
+
* title: hit.title,
|
|
23
|
+
* text: hit.story_text ?? hit.title,
|
|
24
|
+
* url: hit.url,
|
|
25
|
+
* author: hit.author,
|
|
26
|
+
* date: new Date(hit.created_at_i * 1000).toISOString(),
|
|
27
|
+
* contentType: 'bookmark',
|
|
28
|
+
* }));
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
export { defineConnector, defineSync } from './define.js';
|
|
36
|
+
export { isCredentialConfigKey, type ManifestValidationResult, validateConnectorManifest, } from './manifest.js';
|
|
37
|
+
export { type RunOptions, type RunResult, runConnector, } from './runtime.js';
|
|
38
|
+
export type { ConnectorContentType, ConnectorContext, ConnectorDocument, ConnectorManifest, ConnectorSyncResult, FetchLike, ManifestConfigField, TroveConnector, Watermark, } from './types.js';
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EACL,qBAAqB,EACrB,KAAK,wBAAwB,EAC7B,yBAAyB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,YAAY,GACb,MAAM,cAAc,CAAC;AACtB,YAAY,EACV,oBAAoB,EACpB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EACjB,mBAAmB,EACnB,SAAS,EACT,mBAAmB,EACnB,cAAc,EACd,SAAS,GACV,MAAM,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ontrove/sdk` — the thin standard library for authoring **Trove connectors**.
|
|
3
|
+
* You write a `sync(ctx)` that fetches from a source and returns documents; the
|
|
4
|
+
* SDK owns the document shape (which maps 1:1 onto `IngestDocumentInput`), the
|
|
5
|
+
* typed `ctx` capability object, the watermark/cursor model, the local-run
|
|
6
|
+
* harness the CLI drives, and manifest validation.
|
|
7
|
+
*
|
|
8
|
+
* It is the symmetric sibling of `@ontrove/mcp`: a connector returns documents to
|
|
9
|
+
* be _stored_ (`defineConnector` + `sync`); an MCP server returns tool results to
|
|
10
|
+
* be _read live_ (`defineMcpServer`).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { defineConnector } from '@ontrove/sdk';
|
|
15
|
+
*
|
|
16
|
+
* export default defineConnector({
|
|
17
|
+
* async sync(ctx) {
|
|
18
|
+
* const res = await ctx.fetch('https://hn.algolia.com/api/v1/search?tags=front_page');
|
|
19
|
+
* const { hits } = await res.json();
|
|
20
|
+
* return hits.map((hit) => ({
|
|
21
|
+
* id: hit.objectID,
|
|
22
|
+
* title: hit.title,
|
|
23
|
+
* text: hit.story_text ?? hit.title,
|
|
24
|
+
* url: hit.url,
|
|
25
|
+
* author: hit.author,
|
|
26
|
+
* date: new Date(hit.created_at_i * 1000).toISOString(),
|
|
27
|
+
* contentType: 'bookmark',
|
|
28
|
+
* }));
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
export { defineConnector, defineSync } from './define.js';
|
|
36
|
+
export { isCredentialConfigKey, validateConnectorManifest, } from './manifest.js';
|
|
37
|
+
export { runConnector, } from './runtime.js';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connector manifest validation for `trove connector validate`
|
|
3
|
+
* (see the connectors manifest reference).
|
|
4
|
+
*
|
|
5
|
+
* Two checks live here:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Shape validation** — required fields (`id`, `name`, `version`), the
|
|
8
|
+
* `id` pattern (`^[a-z0-9-]+$`), and well-formedness of `config`.
|
|
9
|
+
* 2. **Credential-key lint** — the same spirit as the server-side
|
|
10
|
+
* `validateConfig` (`src/graphql/validate-config.ts`, invariant #6): a
|
|
11
|
+
* connector's `config` holds **user preferences only, never credentials**.
|
|
12
|
+
* The cloud rejects writes whose config contains credential-shaped keys, so
|
|
13
|
+
* the SDK lints the manifest locally to catch the mistake before deploy. Auth
|
|
14
|
+
* material belongs in the (PROPOSED) `auth` block — surfaced from the macOS
|
|
15
|
+
* Keychain — not in `config`.
|
|
16
|
+
*
|
|
17
|
+
* The deny-list below is kept deliberately identical in spirit to the
|
|
18
|
+
* authoritative cloud list so the two cannot meaningfully drift.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Whether a single config key looks like a credential and must be rejected.
|
|
24
|
+
* Mirrors the cloud's `isSensitiveConfigKey`.
|
|
25
|
+
*
|
|
26
|
+
* @param key - The config key to test.
|
|
27
|
+
* @returns True if the key matches a credential pattern.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isCredentialConfigKey(key: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* The outcome of {@link validateConnectorManifest}.
|
|
32
|
+
*/
|
|
33
|
+
export interface ManifestValidationResult {
|
|
34
|
+
/** True when no errors were found. */
|
|
35
|
+
valid: boolean;
|
|
36
|
+
/** Human-readable error messages; empty when `valid` is true. */
|
|
37
|
+
errors: string[];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate a connector `manifest.json` for `trove connector validate`.
|
|
41
|
+
*
|
|
42
|
+
* Checks required fields and the `id`/`version` patterns, validates the `config`
|
|
43
|
+
* descriptors, and runs the credential-key lint (rejecting credential-shaped
|
|
44
|
+
* `config` keys — same spirit as the server-side `validateConfig`). Returns a
|
|
45
|
+
* structured result rather than throwing, so the CLI can print all errors at
|
|
46
|
+
* once.
|
|
47
|
+
*
|
|
48
|
+
* @param manifest - The parsed manifest object to validate.
|
|
49
|
+
* @returns `{ valid, errors }` — `valid: true` with an empty `errors` array on success.
|
|
50
|
+
*/
|
|
51
|
+
export declare function validateConnectorManifest(manifest: unknown): ManifestValidationResult;
|
|
52
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA+CH;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG1D;AAuCD;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,sCAAsC;IACtC,KAAK,EAAE,OAAO,CAAC;IACf,iEAAiE;IACjE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,OAAO,GAAG,wBAAwB,CAkCrF"}
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connector manifest validation for `trove connector validate`
|
|
3
|
+
* (see the connectors manifest reference).
|
|
4
|
+
*
|
|
5
|
+
* Two checks live here:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Shape validation** — required fields (`id`, `name`, `version`), the
|
|
8
|
+
* `id` pattern (`^[a-z0-9-]+$`), and well-formedness of `config`.
|
|
9
|
+
* 2. **Credential-key lint** — the same spirit as the server-side
|
|
10
|
+
* `validateConfig` (`src/graphql/validate-config.ts`, invariant #6): a
|
|
11
|
+
* connector's `config` holds **user preferences only, never credentials**.
|
|
12
|
+
* The cloud rejects writes whose config contains credential-shaped keys, so
|
|
13
|
+
* the SDK lints the manifest locally to catch the mistake before deploy. Auth
|
|
14
|
+
* material belongs in the (PROPOSED) `auth` block — surfaced from the macOS
|
|
15
|
+
* Keychain — not in `config`.
|
|
16
|
+
*
|
|
17
|
+
* The deny-list below is kept deliberately identical in spirit to the
|
|
18
|
+
* authoritative cloud list so the two cannot meaningfully drift.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Credential-shaped key patterns that may NOT appear in a connector's `config`
|
|
24
|
+
* (invariant #6). Mirrors the authoritative cloud deny-list in
|
|
25
|
+
* `src/graphql/validate-config.ts`. Matched case-insensitively and
|
|
26
|
+
* separator-insensitively as substrings, so `apiKey`, `api_key`, `access_token`,
|
|
27
|
+
* and `refreshToken` all match.
|
|
28
|
+
*/
|
|
29
|
+
const CREDENTIAL_KEY_PATTERNS = [
|
|
30
|
+
'session',
|
|
31
|
+
'cookie',
|
|
32
|
+
'token',
|
|
33
|
+
'api_key',
|
|
34
|
+
'apikey',
|
|
35
|
+
'password',
|
|
36
|
+
'secret',
|
|
37
|
+
'auth',
|
|
38
|
+
'credential',
|
|
39
|
+
'bearer',
|
|
40
|
+
'access_key',
|
|
41
|
+
'private_key',
|
|
42
|
+
];
|
|
43
|
+
/** The `id` pattern: lowercase letters, digits, and hyphens. */
|
|
44
|
+
const ID_RE = /^[a-z0-9-]+$/;
|
|
45
|
+
/** A permissive semver check — `MAJOR.MINOR.PATCH` with optional pre-release/build. */
|
|
46
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+].+)?$/;
|
|
47
|
+
/**
|
|
48
|
+
* Normalize a config key for credential-pattern matching: lowercase and strip
|
|
49
|
+
* `_`/`-` separators so `api_key`, `apiKey`, and `api-key` collapse to one form.
|
|
50
|
+
* Identical to the cloud's `normalizeKey`.
|
|
51
|
+
*
|
|
52
|
+
* @param key - The config key to normalize.
|
|
53
|
+
* @returns The normalized comparable form.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeKey(key) {
|
|
56
|
+
return key.toLowerCase().replaceAll('_', '').replaceAll('-', '');
|
|
57
|
+
}
|
|
58
|
+
/** Pre-normalized credential patterns, computed once. */
|
|
59
|
+
const NORMALIZED_CREDENTIAL_PATTERNS = CREDENTIAL_KEY_PATTERNS.map(normalizeKey);
|
|
60
|
+
/**
|
|
61
|
+
* Whether a single config key looks like a credential and must be rejected.
|
|
62
|
+
* Mirrors the cloud's `isSensitiveConfigKey`.
|
|
63
|
+
*
|
|
64
|
+
* @param key - The config key to test.
|
|
65
|
+
* @returns True if the key matches a credential pattern.
|
|
66
|
+
*/
|
|
67
|
+
export function isCredentialConfigKey(key) {
|
|
68
|
+
const normalized = normalizeKey(key);
|
|
69
|
+
return NORMALIZED_CREDENTIAL_PATTERNS.some((pattern) => normalized.includes(pattern));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Push an error if a required string field is missing or empty.
|
|
73
|
+
*
|
|
74
|
+
* @param value - The candidate field value.
|
|
75
|
+
* @param field - The field name, for the message.
|
|
76
|
+
* @param errors - The accumulator to append to.
|
|
77
|
+
*/
|
|
78
|
+
function requireString(value, field, errors) {
|
|
79
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
80
|
+
errors.push(`manifest.${field} is required and must be a non-empty string`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Validate the `config` block: each value must be an object descriptor, and no
|
|
85
|
+
* key may be credential-shaped.
|
|
86
|
+
*
|
|
87
|
+
* @param config - The manifest `config` object.
|
|
88
|
+
* @param errors - The accumulator to append to.
|
|
89
|
+
*/
|
|
90
|
+
function validateConfigBlock(config, errors) {
|
|
91
|
+
const offending = [];
|
|
92
|
+
for (const [key, descriptor] of Object.entries(config)) {
|
|
93
|
+
if (isCredentialConfigKey(key))
|
|
94
|
+
offending.push(key);
|
|
95
|
+
if (descriptor === null || typeof descriptor !== 'object' || Array.isArray(descriptor)) {
|
|
96
|
+
errors.push(`manifest.config.${key} must be a field descriptor object`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (offending.length > 0) {
|
|
100
|
+
const unique = [...new Set(offending)];
|
|
101
|
+
errors.push(`manifest.config must not contain credential-shaped key(s): ${unique.join(', ')}. ` +
|
|
102
|
+
'Connector credentials belong in the macOS Keychain (declared as `auth`), not in `config`.');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Validate a connector `manifest.json` for `trove connector validate`.
|
|
107
|
+
*
|
|
108
|
+
* Checks required fields and the `id`/`version` patterns, validates the `config`
|
|
109
|
+
* descriptors, and runs the credential-key lint (rejecting credential-shaped
|
|
110
|
+
* `config` keys — same spirit as the server-side `validateConfig`). Returns a
|
|
111
|
+
* structured result rather than throwing, so the CLI can print all errors at
|
|
112
|
+
* once.
|
|
113
|
+
*
|
|
114
|
+
* @param manifest - The parsed manifest object to validate.
|
|
115
|
+
* @returns `{ valid, errors }` — `valid: true` with an empty `errors` array on success.
|
|
116
|
+
*/
|
|
117
|
+
export function validateConnectorManifest(manifest) {
|
|
118
|
+
const errors = [];
|
|
119
|
+
if (manifest === null || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
120
|
+
return { valid: false, errors: ['manifest must be a JSON object'] };
|
|
121
|
+
}
|
|
122
|
+
const m = manifest;
|
|
123
|
+
requireString(m.id, 'id', errors);
|
|
124
|
+
if (typeof m.id === 'string' && m.id.length > 0 && !ID_RE.test(m.id)) {
|
|
125
|
+
errors.push('manifest.id must match ^[a-z0-9-]+$ (lowercase letters, digits, hyphens)');
|
|
126
|
+
}
|
|
127
|
+
requireString(m.name, 'name', errors);
|
|
128
|
+
requireString(m.version, 'version', errors);
|
|
129
|
+
if (typeof m.version === 'string' && m.version.length > 0 && !SEMVER_RE.test(m.version)) {
|
|
130
|
+
errors.push('manifest.version must be a semver string (e.g. "1.0.0")');
|
|
131
|
+
}
|
|
132
|
+
if (m.config !== undefined) {
|
|
133
|
+
if (m.config === null || typeof m.config !== 'object' || Array.isArray(m.config)) {
|
|
134
|
+
errors.push('manifest.config must be an object');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
validateConfigBlock(m.config, errors);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (m.needs_browser !== undefined && typeof m.needs_browser !== 'boolean') {
|
|
141
|
+
errors.push('manifest.needs_browser must be a boolean');
|
|
142
|
+
}
|
|
143
|
+
return { valid: errors.length === 0, errors };
|
|
144
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local-run harness — what makes `trove connector dev` / `trove connector test`
|
|
3
|
+
* work (connectors/sdk-reference; connectors/cursors-and-sources).
|
|
4
|
+
*
|
|
5
|
+
* {@link runConnector} builds a {@link ConnectorContext} from a `{ config, cursor }`
|
|
6
|
+
* spec, invokes `sync(ctx)`, then normalizes and validates the result: it accepts
|
|
7
|
+
* either a `ConnectorSyncResult` or a bare `ConnectorDocument[]`, deduplicates by
|
|
8
|
+
* `id` (first occurrence wins, matching server-side `(source, id)` dedup), and
|
|
9
|
+
* validates every document's required fields, surfacing problems clearly instead
|
|
10
|
+
* of pushing a malformed payload to the cloud.
|
|
11
|
+
*
|
|
12
|
+
* It is the symmetric sibling of `@ontrove/mcp`'s `dispatch`/`toFetchHandler`: that
|
|
13
|
+
* runs a tool call end-to-end without the hosted runtime; this runs a connector
|
|
14
|
+
* sync end-to-end without the Mac app sync engine.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import type { ConnectorDocument, FetchLike, TroveConnector, Watermark } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* The options {@link runConnector} builds a `ctx` from. The CLI passes the
|
|
21
|
+
* source's stored config and current cursor; tests inject `fetch`, `log`, and
|
|
22
|
+
* `now` for determinism.
|
|
23
|
+
*
|
|
24
|
+
* @typeParam C - The shape of the connector's typed `ctx.config` preferences.
|
|
25
|
+
*/
|
|
26
|
+
export interface RunOptions<C = Record<string, unknown>> {
|
|
27
|
+
/** The source's preference values (no credentials). Defaults to `{}`. */
|
|
28
|
+
config?: C;
|
|
29
|
+
/** The source's current watermark. Defaults to `{ type: 'none' }`. */
|
|
30
|
+
cursor?: Watermark;
|
|
31
|
+
/** The fetch implementation to expose as `ctx.fetch`. Defaults to global `fetch`. */
|
|
32
|
+
fetchImpl?: FetchLike;
|
|
33
|
+
/** A sink for `ctx.log(...)` calls. Defaults to collecting into the returned `logs`. */
|
|
34
|
+
logSink?: (args: unknown[]) => void;
|
|
35
|
+
/** The clock backing `ctx.now()`. Defaults to `() => new Date()`. */
|
|
36
|
+
now?: () => Date;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The outcome of {@link runConnector}: the validated, deduped documents, the
|
|
40
|
+
* resolved cursor, and the captured log lines. Mirrors enough of what the cloud
|
|
41
|
+
* ingest reports for `trove connector test` to print a useful summary.
|
|
42
|
+
*/
|
|
43
|
+
export interface RunResult {
|
|
44
|
+
/** The validated, deduped documents `sync` returned. */
|
|
45
|
+
documents: ConnectorDocument[];
|
|
46
|
+
/** The cursor `sync` returned, or `{ type: 'none' }` if it returned none. */
|
|
47
|
+
cursor: Watermark;
|
|
48
|
+
/** Captured `ctx.log(...)` lines (when no custom `logSink` was supplied). */
|
|
49
|
+
logs: unknown[][];
|
|
50
|
+
/** Count of documents dropped as duplicates of an earlier `id`. */
|
|
51
|
+
duplicatesSkipped: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run a connector's `sync` against a built `ctx` and collect/validate the result.
|
|
55
|
+
*
|
|
56
|
+
* This is the harness the CLI drives for `trove connector dev` / `trove connector
|
|
57
|
+
* test`: it builds the {@link ConnectorContext} from `{ config, cursor }`, runs
|
|
58
|
+
* `sync(ctx)`, normalizes a bare-array return, validates every document's
|
|
59
|
+
* required fields (clear, indexed errors), and dedups by `id`. Errors thrown by
|
|
60
|
+
* `sync` itself propagate unchanged so the CLI can report a failed run exactly as
|
|
61
|
+
* the Mac app would.
|
|
62
|
+
*
|
|
63
|
+
* @typeParam C - The shape of the connector's typed `ctx.config` preferences.
|
|
64
|
+
* @param connector - The connector whose `sync` to run.
|
|
65
|
+
* @param options - The `{ config, cursor }` spec plus test injection points.
|
|
66
|
+
* @returns The validated, deduped {@link RunResult}.
|
|
67
|
+
* @throws {Error} If `sync` throws, returns a malformed value, or a document fails validation.
|
|
68
|
+
*/
|
|
69
|
+
export declare function runConnector<C = Record<string, unknown>>(connector: TroveConnector<C>, options?: RunOptions<C>): Promise<RunResult>;
|
|
70
|
+
//# sourceMappingURL=runtime.d.ts.map
|