@ownichat/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 +21 -0
- package/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/chunk-2OWJTVJ3.js +29 -0
- package/dist/index.cjs +318 -0
- package/dist/index.d.cts +213 -0
- package/dist/index.d.ts +213 -0
- package/dist/index.js +286 -0
- package/dist/webhooks.cjs +60 -0
- package/dist/webhooks.d.cts +51 -0
- package/dist/webhooks.d.ts +51 -0
- package/dist/webhooks.js +53 -0
- package/dist/widget.cjs +102 -0
- package/dist/widget.d.cts +82 -0
- package/dist/widget.d.ts +82 -0
- package/dist/widget.js +81 -0
- package/package.json +92 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@ownichat/sdk` are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/): breaking changes to the owni.chat API surface
|
|
5
|
+
(a removed field, a changed response shape) ship as a major release.
|
|
6
|
+
|
|
7
|
+
## 0.1.0
|
|
8
|
+
|
|
9
|
+
First public release.
|
|
10
|
+
|
|
11
|
+
- `createOwniClient()` — typed client for the owni.chat external API
|
|
12
|
+
- `me()` — resolve the project, workspace and widget key a server key is bound to
|
|
13
|
+
- `products` — `list`, `get`, `upsert`, `delete`, and `bulkUpsert` with automatic
|
|
14
|
+
splitting into the API's 100-item batches
|
|
15
|
+
- `knowledge` — create URL, text and FAQ sources, upload files, trigger reindexing
|
|
16
|
+
- `domains` — register, list and remove the origins the widget may load on
|
|
17
|
+
- `@ownichat/sdk/webhooks` — `verifyWebhookSignature` and `parseWebhookRequest` for
|
|
18
|
+
HMAC-SHA256 signed deliveries, with replay protection
|
|
19
|
+
- `@ownichat/sdk/widget` — browser client for the public widget API
|
|
20
|
+
- Automatic retry with exponential backoff on 429 and 5xx, honouring `Retry-After`
|
|
21
|
+
- Typed errors: `OwniApiError`, `OwniConnectionError`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 owni.chat
|
|
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,201 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @ownichat/sdk
|
|
4
|
+
|
|
5
|
+
**Official API client for [owni.chat](https://owni.chat)** — the AI chat widget that answers
|
|
6
|
+
from your real catalog.
|
|
7
|
+
|
|
8
|
+
[](https://www.npmjs.com/package/@ownichat/sdk)
|
|
9
|
+
[](https://bundlephobia.com/package/@ownichat/sdk)
|
|
10
|
+
[](https://www.npmjs.com/package/@ownichat/sdk)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install @ownichat/sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
| | |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| 🛍️ **Catalog sync** | Push products, prices and stock — batching handled for you |
|
|
22
|
+
| 📚 **Knowledge** | Create URL, text and FAQ sources, upload files, trigger reindexing |
|
|
23
|
+
| 🌐 **Domains** | Register the origins the widget may load on |
|
|
24
|
+
| 🔐 **Webhooks** | Verify HMAC-signed deliveries, with replay protection |
|
|
25
|
+
| 💬 **Widget API** | Talk to the public chat API when building your own UI |
|
|
26
|
+
|
|
27
|
+
**Zero runtime dependencies.** Node 18+ (uses the global `fetch`), and works in Deno, Bun,
|
|
28
|
+
Cloudflare Workers and other edge runtimes. Ships ESM and CJS with types for both.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
Create an API key in the owni.chat dashboard under **Settings → API keys**. It looks like
|
|
33
|
+
`ak_live_…` and must stay server-side.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { createOwniClient } from '@ownichat/sdk';
|
|
37
|
+
|
|
38
|
+
const owni = createOwniClient({ apiKey: process.env.OWNI_API_KEY! });
|
|
39
|
+
|
|
40
|
+
// Everything the key is bound to — including the public key for the widget script tag
|
|
41
|
+
const { project } = await owni.me();
|
|
42
|
+
console.log(project.project_key); // pk_…
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
That single call is what makes one-field installs possible: the merchant pastes the API key,
|
|
46
|
+
and your integration discovers the project id and the widget key on its own.
|
|
47
|
+
|
|
48
|
+
## Sync a product catalog
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
await owni.products.bulkUpsert([
|
|
52
|
+
{
|
|
53
|
+
external_id: '1042', // your shop's product id
|
|
54
|
+
title: 'Merino wool scarf',
|
|
55
|
+
description: 'Lightweight, 100% merino.',
|
|
56
|
+
url: 'https://shop.example.com/p/1042', // required
|
|
57
|
+
price: '49.00',
|
|
58
|
+
old_price: '69.00',
|
|
59
|
+
currency: 'EUR',
|
|
60
|
+
image_url: 'https://shop.example.com/i/1042.jpg',
|
|
61
|
+
availability: 'in_stock',
|
|
62
|
+
category: 'Accessories',
|
|
63
|
+
brand: 'Example',
|
|
64
|
+
attributes: [{ name: 'Colour', value: 'Charcoal' }],
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`bulkUpsert` accepts any number of products and splits them into the API's 100-item batches
|
|
70
|
+
for you, sequentially, so a full catalog sync stays within the rate limit.
|
|
71
|
+
|
|
72
|
+
Other catalog calls:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
await owni.products.list({ page: 1, pageSize: 50 });
|
|
76
|
+
await owni.products.get('1042');
|
|
77
|
+
await owni.products.upsert({ external_id: '1042', title: '…', url: '…' });
|
|
78
|
+
await owni.products.delete('1042');
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Keep `external_id` equal to your shop's own product id — it is what the widget's
|
|
82
|
+
"add to cart" snippet interpolates when a visitor buys from a product card.
|
|
83
|
+
|
|
84
|
+
## Allowed domains
|
|
85
|
+
|
|
86
|
+
The widget only loads on origins registered for the project. Register the storefront you're
|
|
87
|
+
installed on so staging sites and secondary domains work without dashboard edits:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
await owni.domains.add('staging.shop.example.com'); // idempotent
|
|
91
|
+
await owni.domains.list();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Knowledge sources
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
await owni.knowledge.createSource({ type: 'url', name: 'Shipping policy', source_url: 'https://shop.example.com/shipping' });
|
|
98
|
+
await owni.knowledge.createSource({ type: 'faq', name: 'FAQ', raw_text: 'Q: …\nA: …' });
|
|
99
|
+
|
|
100
|
+
const file = new Blob([await readFile('./returns.pdf')]);
|
|
101
|
+
await owni.knowledge.uploadFile({ file, filename: 'returns.pdf' });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Webhooks
|
|
105
|
+
|
|
106
|
+
owni.chat signs outgoing webhooks with `X-Owni-Signature` (HMAC-SHA256 over
|
|
107
|
+
`<timestamp>.<raw body>`) and `X-Owni-Timestamp`. Verify both — the timestamp is what stops
|
|
108
|
+
a captured delivery from being replayed later.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { parseWebhookRequest } from '@ownichat/sdk/webhooks';
|
|
112
|
+
|
|
113
|
+
export async function POST(request: Request) {
|
|
114
|
+
const body = await request.text(); // raw body, not request.json()
|
|
115
|
+
|
|
116
|
+
const result = parseWebhookRequest({
|
|
117
|
+
body,
|
|
118
|
+
headers: request.headers,
|
|
119
|
+
secret: process.env.OWNI_WEBHOOK_SECRET!,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
if (!result.valid) {
|
|
123
|
+
return new Response(result.reason, { status: 401 });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (result.event.event === 'lead_captured') {
|
|
127
|
+
// …
|
|
128
|
+
}
|
|
129
|
+
return new Response('ok');
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`verifyWebhookSignature` is available separately if you already parse the body yourself. Both
|
|
134
|
+
work with a `Headers` object and with a plain Node header record.
|
|
135
|
+
|
|
136
|
+
The signing secret is shown once when you connect the webhook integration, and can be
|
|
137
|
+
regenerated from **Integrations → Webhooks → Rotate secret**. Integrations created before
|
|
138
|
+
signing existed keep receiving unsigned deliveries until you rotate.
|
|
139
|
+
|
|
140
|
+
## Widget API (browser)
|
|
141
|
+
|
|
142
|
+
Only needed when building a custom chat UI — the standard embed script already does this.
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { createWidgetClient } from '@ownichat/sdk/widget';
|
|
146
|
+
|
|
147
|
+
const widget = createWidgetClient({ projectKey: 'pk_…' });
|
|
148
|
+
const config = await widget.getConfig();
|
|
149
|
+
const session = await widget.createSession({ source_page_url: location.href });
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Requests are origin-checked server-side, so the page must run on the project domain or one of
|
|
153
|
+
its registered domains.
|
|
154
|
+
|
|
155
|
+
## Errors and retries
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
import { OwniApiError, OwniConnectionError } from '@ownichat/sdk';
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
await owni.products.bulkUpsert(products);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof OwniApiError && error.isAuthError) {
|
|
164
|
+
// wrong, revoked, expired or under-scoped key — retrying will not help
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
429 and 5xx responses are retried automatically with exponential backoff, honouring
|
|
170
|
+
`Retry-After`. Configure with `maxRetries` (default 3) and `timeoutMs` (default 30 000).
|
|
171
|
+
|
|
172
|
+
## Options
|
|
173
|
+
|
|
174
|
+
| Option | Default | Notes |
|
|
175
|
+
| --- | --- | --- |
|
|
176
|
+
| `apiKey` | — | Required. `ak_live_…`, server-side only. |
|
|
177
|
+
| `baseUrl` | `https://app.owni.chat` | Point at your own instance if self-hosting. |
|
|
178
|
+
| `projectId` | resolved via `me()` | Supply it to skip one round trip. |
|
|
179
|
+
| `fetch` | global `fetch` | Inject your own for tests or proxies. |
|
|
180
|
+
| `maxRetries` | `3` | Applies to 429 and 5xx only. |
|
|
181
|
+
| `timeoutMs` | `30000` | Per request, including retries individually. |
|
|
182
|
+
|
|
183
|
+
## Related packages
|
|
184
|
+
|
|
185
|
+
| Package | For |
|
|
186
|
+
| --- | --- |
|
|
187
|
+
| [`@ownichat/react`](https://www.npmjs.com/package/@ownichat/react) | Widget provider, hooks and inline chat panel |
|
|
188
|
+
| [`@ownichat/next`](https://www.npmjs.com/package/@ownichat/next) | Next.js server component, client hooks and webhook route |
|
|
189
|
+
|
|
190
|
+
Running a ready-made platform? owni.chat ships official plugins for WordPress/WooCommerce,
|
|
191
|
+
Shopify and OpenCart — see [owni.chat/integrations](https://owni.chat/integrations).
|
|
192
|
+
|
|
193
|
+
## Notes
|
|
194
|
+
|
|
195
|
+
The widget bundle shipped by owni.chat has its own copy of the browser API types; the two are
|
|
196
|
+
kept in sync by hand for now. If you spot a drift, the backend's Joi schemas are the source of
|
|
197
|
+
truth.
|
|
198
|
+
|
|
199
|
+
## License
|
|
200
|
+
|
|
201
|
+
MIT © [owni.chat](https://owni.chat)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var OwniApiError = class extends Error {
|
|
3
|
+
status;
|
|
4
|
+
body;
|
|
5
|
+
constructor(status, message, body) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "OwniApiError";
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.body = body;
|
|
10
|
+
}
|
|
11
|
+
/** 401/403 — a wrong, revoked, expired or under-scoped API key. Retrying never helps. */
|
|
12
|
+
get isAuthError() {
|
|
13
|
+
return this.status === 401 || this.status === 403;
|
|
14
|
+
}
|
|
15
|
+
/** 429 — the external API allows 60 requests per minute per key. */
|
|
16
|
+
get isRateLimited() {
|
|
17
|
+
return this.status === 429;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var OwniConnectionError = class extends Error {
|
|
21
|
+
cause;
|
|
22
|
+
constructor(message, cause) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = "OwniConnectionError";
|
|
25
|
+
this.cause = cause;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export { OwniApiError, OwniConnectionError };
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var OwniApiError = class extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, message, body) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "OwniApiError";
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.body = body;
|
|
12
|
+
}
|
|
13
|
+
/** 401/403 — a wrong, revoked, expired or under-scoped API key. Retrying never helps. */
|
|
14
|
+
get isAuthError() {
|
|
15
|
+
return this.status === 401 || this.status === 403;
|
|
16
|
+
}
|
|
17
|
+
/** 429 — the external API allows 60 requests per minute per key. */
|
|
18
|
+
get isRateLimited() {
|
|
19
|
+
return this.status === 429;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var OwniConnectionError = class extends Error {
|
|
23
|
+
cause;
|
|
24
|
+
constructor(message, cause) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "OwniConnectionError";
|
|
27
|
+
this.cause = cause;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/http.ts
|
|
32
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
33
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
34
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
35
|
+
var retryDelayMs = (attempt, retryAfterHeader) => {
|
|
36
|
+
if (retryAfterHeader) {
|
|
37
|
+
const seconds = Number(retryAfterHeader);
|
|
38
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
39
|
+
return Math.min(seconds * 1e3, 6e4);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const base = Math.min(2 ** attempt * 500, 16e3);
|
|
43
|
+
return base + Math.random() * 250;
|
|
44
|
+
};
|
|
45
|
+
var isRetryableStatus = (status) => status === 429 || status >= 500;
|
|
46
|
+
var HttpClient = class {
|
|
47
|
+
baseUrl;
|
|
48
|
+
apiKey;
|
|
49
|
+
fetchImpl;
|
|
50
|
+
maxRetries;
|
|
51
|
+
timeoutMs;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
if (!options.apiKey) {
|
|
54
|
+
throw new Error("An owni.chat API key is required (ak_live_\u2026).");
|
|
55
|
+
}
|
|
56
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
57
|
+
this.apiKey = options.apiKey;
|
|
58
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
59
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
60
|
+
const resolvedFetch = options.fetch ?? globalThis.fetch;
|
|
61
|
+
if (!resolvedFetch) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"No fetch implementation found. Use Node 18+ or pass `fetch` in the client options."
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
this.fetchImpl = resolvedFetch;
|
|
67
|
+
}
|
|
68
|
+
async request(options) {
|
|
69
|
+
const url = this.buildUrl(options.path, options.query);
|
|
70
|
+
const headers = {
|
|
71
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
72
|
+
Accept: "application/json",
|
|
73
|
+
...options.headers
|
|
74
|
+
};
|
|
75
|
+
let body = options.body;
|
|
76
|
+
if (options.json !== void 0) {
|
|
77
|
+
headers["Content-Type"] = "application/json";
|
|
78
|
+
body = JSON.stringify(options.json);
|
|
79
|
+
}
|
|
80
|
+
let lastError;
|
|
81
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
82
|
+
let response;
|
|
83
|
+
try {
|
|
84
|
+
response = await this.fetchWithTimeout(url, {
|
|
85
|
+
method: options.method ?? "GET",
|
|
86
|
+
headers,
|
|
87
|
+
body
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
lastError = new OwniConnectionError(`Request to ${url} failed`, error);
|
|
91
|
+
if (attempt === this.maxRetries) throw lastError;
|
|
92
|
+
await sleep(retryDelayMs(attempt, null));
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (response.ok) {
|
|
96
|
+
return await this.parseJson(response);
|
|
97
|
+
}
|
|
98
|
+
if (isRetryableStatus(response.status) && attempt < this.maxRetries) {
|
|
99
|
+
await sleep(retryDelayMs(attempt, response.headers.get("retry-after")));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
throw await this.toApiError(response);
|
|
103
|
+
}
|
|
104
|
+
throw lastError ?? new OwniConnectionError(`Request to ${url} failed`);
|
|
105
|
+
}
|
|
106
|
+
buildUrl(path, query) {
|
|
107
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
108
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
109
|
+
if (value !== void 0) {
|
|
110
|
+
url.searchParams.set(key, String(value));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return url.toString();
|
|
114
|
+
}
|
|
115
|
+
async fetchWithTimeout(url, init) {
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
118
|
+
try {
|
|
119
|
+
return await this.fetchImpl(url, { ...init, signal: controller.signal });
|
|
120
|
+
} finally {
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async parseJson(response) {
|
|
125
|
+
const text = await response.text();
|
|
126
|
+
if (!text) return null;
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(text);
|
|
129
|
+
} catch {
|
|
130
|
+
return text;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async toApiError(response) {
|
|
134
|
+
const body = await this.parseJson(response);
|
|
135
|
+
const message = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : `owni.chat API responded with status ${response.status}`;
|
|
136
|
+
return new OwniApiError(response.status, message, body);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/client.ts
|
|
141
|
+
var DEFAULT_BASE_URL = "https://app.owni.chat";
|
|
142
|
+
var BULK_CHUNK_SIZE = 100;
|
|
143
|
+
var OwniClient = class {
|
|
144
|
+
http;
|
|
145
|
+
projectIdPromise = null;
|
|
146
|
+
products;
|
|
147
|
+
knowledge;
|
|
148
|
+
domains;
|
|
149
|
+
constructor(options) {
|
|
150
|
+
this.http = new HttpClient({
|
|
151
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
152
|
+
apiKey: options.apiKey,
|
|
153
|
+
fetch: options.fetch,
|
|
154
|
+
maxRetries: options.maxRetries,
|
|
155
|
+
timeoutMs: options.timeoutMs
|
|
156
|
+
});
|
|
157
|
+
if (options.projectId) {
|
|
158
|
+
this.projectIdPromise = Promise.resolve(options.projectId);
|
|
159
|
+
}
|
|
160
|
+
this.products = new ProductsResource(this);
|
|
161
|
+
this.knowledge = new KnowledgeResource(this);
|
|
162
|
+
this.domains = new DomainsResource(this);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Everything the key is bound to, including the `project_key` a plugin needs for the
|
|
166
|
+
* widget script tag. This is what makes "paste one API key" installs possible.
|
|
167
|
+
*/
|
|
168
|
+
me() {
|
|
169
|
+
return this.http.request({ path: "/api/external/v1/me" });
|
|
170
|
+
}
|
|
171
|
+
/** Resolved once per client and cached; concurrent callers share the same request. */
|
|
172
|
+
projectId() {
|
|
173
|
+
if (!this.projectIdPromise) {
|
|
174
|
+
this.projectIdPromise = this.me().then((identity) => identity.project.id).catch((error) => {
|
|
175
|
+
this.projectIdPromise = null;
|
|
176
|
+
throw error;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return this.projectIdPromise;
|
|
180
|
+
}
|
|
181
|
+
/** @internal */
|
|
182
|
+
async projectRequest(options) {
|
|
183
|
+
const projectId = await this.projectId();
|
|
184
|
+
return this.http.request({
|
|
185
|
+
method: options.method,
|
|
186
|
+
path: options.path(projectId),
|
|
187
|
+
query: options.query,
|
|
188
|
+
json: options.json,
|
|
189
|
+
body: options.body,
|
|
190
|
+
headers: options.headers
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
var ProductsResource = class {
|
|
195
|
+
constructor(client) {
|
|
196
|
+
this.client = client;
|
|
197
|
+
}
|
|
198
|
+
client;
|
|
199
|
+
list(input = {}) {
|
|
200
|
+
return this.client.projectRequest({
|
|
201
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products`,
|
|
202
|
+
query: { page: input.page, page_size: input.pageSize }
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async get(externalId) {
|
|
206
|
+
const data = await this.client.projectRequest({
|
|
207
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/${encodeURIComponent(externalId)}`
|
|
208
|
+
});
|
|
209
|
+
return data.product;
|
|
210
|
+
}
|
|
211
|
+
async upsert(product) {
|
|
212
|
+
return this.client.projectRequest({
|
|
213
|
+
method: "PUT",
|
|
214
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/${encodeURIComponent(product.external_id)}`,
|
|
215
|
+
json: product
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
async delete(externalId) {
|
|
219
|
+
const data = await this.client.projectRequest({
|
|
220
|
+
method: "DELETE",
|
|
221
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/${encodeURIComponent(externalId)}`
|
|
222
|
+
});
|
|
223
|
+
return data.product;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Sends products in chunks of {@link BULK_CHUNK_SIZE} and merges the results, so a
|
|
227
|
+
* full catalog can be passed in one call. Chunks are sequential on purpose: the API
|
|
228
|
+
* re-indexes embeddings after each batch and parallel writes just trip the rate limit.
|
|
229
|
+
*/
|
|
230
|
+
async bulkUpsert(products) {
|
|
231
|
+
const merged = { upserted: 0, created: 0, updated: 0, items: [] };
|
|
232
|
+
if (products.length === 0) return merged;
|
|
233
|
+
for (let index = 0; index < products.length; index += BULK_CHUNK_SIZE) {
|
|
234
|
+
const chunk = products.slice(index, index + BULK_CHUNK_SIZE);
|
|
235
|
+
const result = await this.client.projectRequest({
|
|
236
|
+
method: "POST",
|
|
237
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/bulk`,
|
|
238
|
+
json: { products: chunk }
|
|
239
|
+
});
|
|
240
|
+
merged.upserted += result.upserted;
|
|
241
|
+
merged.created += result.created;
|
|
242
|
+
merged.updated += result.updated;
|
|
243
|
+
merged.items.push(...result.items);
|
|
244
|
+
}
|
|
245
|
+
return merged;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
var KnowledgeResource = class {
|
|
249
|
+
constructor(client) {
|
|
250
|
+
this.client = client;
|
|
251
|
+
}
|
|
252
|
+
client;
|
|
253
|
+
async createSource(input) {
|
|
254
|
+
const data = await this.client.projectRequest({
|
|
255
|
+
method: "POST",
|
|
256
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/knowledge-sources`,
|
|
257
|
+
json: input
|
|
258
|
+
});
|
|
259
|
+
return data.knowledge_source;
|
|
260
|
+
}
|
|
261
|
+
/** `file` is a Blob/File; in Node 18+ use `new Blob([buffer])` or `fs.openAsBlob`. */
|
|
262
|
+
async uploadFile(input) {
|
|
263
|
+
const form = new FormData();
|
|
264
|
+
form.append("file", input.file, input.filename);
|
|
265
|
+
if (input.name) form.append("name", input.name);
|
|
266
|
+
const data = await this.client.projectRequest({
|
|
267
|
+
method: "POST",
|
|
268
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/knowledge-sources/upload`,
|
|
269
|
+
body: form
|
|
270
|
+
});
|
|
271
|
+
return data.knowledge_source;
|
|
272
|
+
}
|
|
273
|
+
async reindex(sourceId) {
|
|
274
|
+
const data = await this.client.projectRequest({
|
|
275
|
+
method: "POST",
|
|
276
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/knowledge-sources/${encodeURIComponent(sourceId)}/reindex`
|
|
277
|
+
});
|
|
278
|
+
return data.embedding_job;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var DomainsResource = class {
|
|
282
|
+
constructor(client) {
|
|
283
|
+
this.client = client;
|
|
284
|
+
}
|
|
285
|
+
client;
|
|
286
|
+
async list() {
|
|
287
|
+
const data = await this.client.projectRequest({
|
|
288
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/domains`
|
|
289
|
+
});
|
|
290
|
+
return data.domains;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Registers an origin the widget is allowed to load on. Idempotent — re-sending an
|
|
294
|
+
* existing domain returns `created: false` instead of failing, so plugins can call
|
|
295
|
+
* this on every settings save.
|
|
296
|
+
*/
|
|
297
|
+
add(domain) {
|
|
298
|
+
return this.client.projectRequest({
|
|
299
|
+
method: "POST",
|
|
300
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/domains`,
|
|
301
|
+
json: { domain }
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
remove(domainId) {
|
|
305
|
+
return this.client.projectRequest({
|
|
306
|
+
method: "DELETE",
|
|
307
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/domains/${encodeURIComponent(domainId)}`
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
var createOwniClient = (options) => new OwniClient(options);
|
|
312
|
+
|
|
313
|
+
exports.BULK_CHUNK_SIZE = BULK_CHUNK_SIZE;
|
|
314
|
+
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
315
|
+
exports.OwniApiError = OwniApiError;
|
|
316
|
+
exports.OwniClient = OwniClient;
|
|
317
|
+
exports.OwniConnectionError = OwniConnectionError;
|
|
318
|
+
exports.createOwniClient = createOwniClient;
|