@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/dist/index.d.cts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
|
|
3
|
+
/** Mirrors `external_product_body_schema` in the owni.chat backend. */
|
|
4
|
+
type OwniProductAttribute = {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
type OwniProductInput = {
|
|
9
|
+
/** The shop's own product id. Required for bulk upserts; it is what add-to-cart snippets interpolate. */
|
|
10
|
+
external_id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
price?: string | number | null;
|
|
14
|
+
old_price?: string | number | null;
|
|
15
|
+
currency?: string | null;
|
|
16
|
+
image_url?: string | null;
|
|
17
|
+
/** Required by the API — the product page the widget links to. */
|
|
18
|
+
url: string;
|
|
19
|
+
availability?: string | null;
|
|
20
|
+
category?: string | null;
|
|
21
|
+
brand?: string | null;
|
|
22
|
+
attributes?: OwniProductAttribute[];
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
type OwniProduct = {
|
|
26
|
+
id: string;
|
|
27
|
+
external_id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string | null;
|
|
30
|
+
price: string | null;
|
|
31
|
+
old_price: string | null;
|
|
32
|
+
currency: string | null;
|
|
33
|
+
image_url: string | null;
|
|
34
|
+
url: string;
|
|
35
|
+
availability: string | null;
|
|
36
|
+
category: string | null;
|
|
37
|
+
brand: string | null;
|
|
38
|
+
source_id: string;
|
|
39
|
+
created_at: string;
|
|
40
|
+
updated_at: string;
|
|
41
|
+
};
|
|
42
|
+
type OwniProductPage = {
|
|
43
|
+
items: OwniProduct[];
|
|
44
|
+
total: number;
|
|
45
|
+
page: number;
|
|
46
|
+
page_size: number;
|
|
47
|
+
};
|
|
48
|
+
type OwniBulkUpsertResult = {
|
|
49
|
+
upserted: number;
|
|
50
|
+
created: number;
|
|
51
|
+
updated: number;
|
|
52
|
+
items: Array<{
|
|
53
|
+
external_id: string;
|
|
54
|
+
created: boolean;
|
|
55
|
+
id: string;
|
|
56
|
+
}>;
|
|
57
|
+
};
|
|
58
|
+
type OwniIdentity = {
|
|
59
|
+
project: {
|
|
60
|
+
id: string;
|
|
61
|
+
workspace_id: string;
|
|
62
|
+
/** Public key for the widget script tag (`data-project-key`). */
|
|
63
|
+
project_key: string;
|
|
64
|
+
name: string;
|
|
65
|
+
domain: string;
|
|
66
|
+
chat_mode: string;
|
|
67
|
+
is_active: boolean;
|
|
68
|
+
install_status: string;
|
|
69
|
+
};
|
|
70
|
+
api_key: {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
scopes: string[];
|
|
74
|
+
expires_at: string | null;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
type OwniKnowledgeSourceType = 'text' | 'url' | 'faq' | 'product_feed' | 'website';
|
|
78
|
+
type OwniKnowledgeSourceInput = {
|
|
79
|
+
type: OwniKnowledgeSourceType;
|
|
80
|
+
name: string;
|
|
81
|
+
/** Required for `url`, `product_feed` and `website`; rejected for the others. */
|
|
82
|
+
source_url?: string;
|
|
83
|
+
/** Required for `text` and `faq`; rejected for the others. */
|
|
84
|
+
raw_text?: string;
|
|
85
|
+
metadata?: {
|
|
86
|
+
feed_format?: 'auto' | 'yandex' | 'google';
|
|
87
|
+
sync_enabled?: boolean;
|
|
88
|
+
sync_cron?: string;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
type OwniKnowledgeSource = {
|
|
92
|
+
id: string;
|
|
93
|
+
type: string;
|
|
94
|
+
name: string;
|
|
95
|
+
status: string;
|
|
96
|
+
created_at: string;
|
|
97
|
+
};
|
|
98
|
+
type OwniEmbeddingJob = {
|
|
99
|
+
id: string;
|
|
100
|
+
status: string;
|
|
101
|
+
created_at: string;
|
|
102
|
+
};
|
|
103
|
+
type OwniDomain = {
|
|
104
|
+
id: string;
|
|
105
|
+
domain: string;
|
|
106
|
+
source: 'manual' | 'plugin';
|
|
107
|
+
created_at: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
declare const DEFAULT_BASE_URL = "https://app.owni.chat";
|
|
111
|
+
/** The API rejects bulk payloads larger than this, so the client chunks for you. */
|
|
112
|
+
declare const BULK_CHUNK_SIZE = 100;
|
|
113
|
+
type OwniClientOptions = {
|
|
114
|
+
/** Project API key, `ak_live_…`. Server-side only — never ship it to the browser. */
|
|
115
|
+
apiKey: string;
|
|
116
|
+
baseUrl?: string;
|
|
117
|
+
/** Optional: skips one `me()` round trip on the first catalog call. */
|
|
118
|
+
projectId?: string;
|
|
119
|
+
fetch?: FetchLike;
|
|
120
|
+
maxRetries?: number;
|
|
121
|
+
timeoutMs?: number;
|
|
122
|
+
};
|
|
123
|
+
declare class OwniClient {
|
|
124
|
+
private readonly http;
|
|
125
|
+
private projectIdPromise;
|
|
126
|
+
readonly products: ProductsResource;
|
|
127
|
+
readonly knowledge: KnowledgeResource;
|
|
128
|
+
readonly domains: DomainsResource;
|
|
129
|
+
constructor(options: OwniClientOptions);
|
|
130
|
+
/**
|
|
131
|
+
* Everything the key is bound to, including the `project_key` a plugin needs for the
|
|
132
|
+
* widget script tag. This is what makes "paste one API key" installs possible.
|
|
133
|
+
*/
|
|
134
|
+
me(): Promise<OwniIdentity>;
|
|
135
|
+
/** Resolved once per client and cached; concurrent callers share the same request. */
|
|
136
|
+
projectId(): Promise<string>;
|
|
137
|
+
/** @internal */
|
|
138
|
+
projectRequest<T>(options: {
|
|
139
|
+
method?: string;
|
|
140
|
+
path: (projectId: string) => string;
|
|
141
|
+
query?: Record<string, string | number | undefined>;
|
|
142
|
+
json?: unknown;
|
|
143
|
+
body?: BodyInit;
|
|
144
|
+
headers?: Record<string, string>;
|
|
145
|
+
}): Promise<T>;
|
|
146
|
+
}
|
|
147
|
+
declare class ProductsResource {
|
|
148
|
+
private readonly client;
|
|
149
|
+
constructor(client: OwniClient);
|
|
150
|
+
list(input?: {
|
|
151
|
+
page?: number;
|
|
152
|
+
pageSize?: number;
|
|
153
|
+
}): Promise<OwniProductPage>;
|
|
154
|
+
get(externalId: string): Promise<OwniProduct>;
|
|
155
|
+
upsert(product: OwniProductInput): Promise<{
|
|
156
|
+
product: OwniProduct;
|
|
157
|
+
created: boolean;
|
|
158
|
+
}>;
|
|
159
|
+
delete(externalId: string): Promise<OwniProduct>;
|
|
160
|
+
/**
|
|
161
|
+
* Sends products in chunks of {@link BULK_CHUNK_SIZE} and merges the results, so a
|
|
162
|
+
* full catalog can be passed in one call. Chunks are sequential on purpose: the API
|
|
163
|
+
* re-indexes embeddings after each batch and parallel writes just trip the rate limit.
|
|
164
|
+
*/
|
|
165
|
+
bulkUpsert(products: OwniProductInput[]): Promise<OwniBulkUpsertResult>;
|
|
166
|
+
}
|
|
167
|
+
declare class KnowledgeResource {
|
|
168
|
+
private readonly client;
|
|
169
|
+
constructor(client: OwniClient);
|
|
170
|
+
createSource(input: OwniKnowledgeSourceInput): Promise<OwniKnowledgeSource>;
|
|
171
|
+
/** `file` is a Blob/File; in Node 18+ use `new Blob([buffer])` or `fs.openAsBlob`. */
|
|
172
|
+
uploadFile(input: {
|
|
173
|
+
file: Blob;
|
|
174
|
+
filename: string;
|
|
175
|
+
name?: string;
|
|
176
|
+
}): Promise<OwniKnowledgeSource>;
|
|
177
|
+
reindex(sourceId: string): Promise<OwniEmbeddingJob>;
|
|
178
|
+
}
|
|
179
|
+
declare class DomainsResource {
|
|
180
|
+
private readonly client;
|
|
181
|
+
constructor(client: OwniClient);
|
|
182
|
+
list(): Promise<OwniDomain[]>;
|
|
183
|
+
/**
|
|
184
|
+
* Registers an origin the widget is allowed to load on. Idempotent — re-sending an
|
|
185
|
+
* existing domain returns `created: false` instead of failing, so plugins can call
|
|
186
|
+
* this on every settings save.
|
|
187
|
+
*/
|
|
188
|
+
add(domain: string): Promise<{
|
|
189
|
+
domain: OwniDomain;
|
|
190
|
+
created: boolean;
|
|
191
|
+
}>;
|
|
192
|
+
remove(domainId: string): Promise<{
|
|
193
|
+
status: string;
|
|
194
|
+
}>;
|
|
195
|
+
}
|
|
196
|
+
declare const createOwniClient: (options: OwniClientOptions) => OwniClient;
|
|
197
|
+
|
|
198
|
+
declare class OwniApiError extends Error {
|
|
199
|
+
readonly status: number;
|
|
200
|
+
readonly body: unknown;
|
|
201
|
+
constructor(status: number, message: string, body?: unknown);
|
|
202
|
+
/** 401/403 — a wrong, revoked, expired or under-scoped API key. Retrying never helps. */
|
|
203
|
+
get isAuthError(): boolean;
|
|
204
|
+
/** 429 — the external API allows 60 requests per minute per key. */
|
|
205
|
+
get isRateLimited(): boolean;
|
|
206
|
+
}
|
|
207
|
+
/** Raised when a request exceeds the client timeout or the connection fails outright. */
|
|
208
|
+
declare class OwniConnectionError extends Error {
|
|
209
|
+
readonly cause: unknown;
|
|
210
|
+
constructor(message: string, cause?: unknown);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { BULK_CHUNK_SIZE, DEFAULT_BASE_URL, type FetchLike, OwniApiError, type OwniBulkUpsertResult, OwniClient, type OwniClientOptions, OwniConnectionError, type OwniDomain, type OwniEmbeddingJob, type OwniIdentity, type OwniKnowledgeSource, type OwniKnowledgeSourceInput, type OwniKnowledgeSourceType, type OwniProduct, type OwniProductAttribute, type OwniProductInput, type OwniProductPage, createOwniClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
2
|
+
|
|
3
|
+
/** Mirrors `external_product_body_schema` in the owni.chat backend. */
|
|
4
|
+
type OwniProductAttribute = {
|
|
5
|
+
name: string;
|
|
6
|
+
value: string;
|
|
7
|
+
};
|
|
8
|
+
type OwniProductInput = {
|
|
9
|
+
/** The shop's own product id. Required for bulk upserts; it is what add-to-cart snippets interpolate. */
|
|
10
|
+
external_id: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string | null;
|
|
13
|
+
price?: string | number | null;
|
|
14
|
+
old_price?: string | number | null;
|
|
15
|
+
currency?: string | null;
|
|
16
|
+
image_url?: string | null;
|
|
17
|
+
/** Required by the API — the product page the widget links to. */
|
|
18
|
+
url: string;
|
|
19
|
+
availability?: string | null;
|
|
20
|
+
category?: string | null;
|
|
21
|
+
brand?: string | null;
|
|
22
|
+
attributes?: OwniProductAttribute[];
|
|
23
|
+
metadata?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
type OwniProduct = {
|
|
26
|
+
id: string;
|
|
27
|
+
external_id: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string | null;
|
|
30
|
+
price: string | null;
|
|
31
|
+
old_price: string | null;
|
|
32
|
+
currency: string | null;
|
|
33
|
+
image_url: string | null;
|
|
34
|
+
url: string;
|
|
35
|
+
availability: string | null;
|
|
36
|
+
category: string | null;
|
|
37
|
+
brand: string | null;
|
|
38
|
+
source_id: string;
|
|
39
|
+
created_at: string;
|
|
40
|
+
updated_at: string;
|
|
41
|
+
};
|
|
42
|
+
type OwniProductPage = {
|
|
43
|
+
items: OwniProduct[];
|
|
44
|
+
total: number;
|
|
45
|
+
page: number;
|
|
46
|
+
page_size: number;
|
|
47
|
+
};
|
|
48
|
+
type OwniBulkUpsertResult = {
|
|
49
|
+
upserted: number;
|
|
50
|
+
created: number;
|
|
51
|
+
updated: number;
|
|
52
|
+
items: Array<{
|
|
53
|
+
external_id: string;
|
|
54
|
+
created: boolean;
|
|
55
|
+
id: string;
|
|
56
|
+
}>;
|
|
57
|
+
};
|
|
58
|
+
type OwniIdentity = {
|
|
59
|
+
project: {
|
|
60
|
+
id: string;
|
|
61
|
+
workspace_id: string;
|
|
62
|
+
/** Public key for the widget script tag (`data-project-key`). */
|
|
63
|
+
project_key: string;
|
|
64
|
+
name: string;
|
|
65
|
+
domain: string;
|
|
66
|
+
chat_mode: string;
|
|
67
|
+
is_active: boolean;
|
|
68
|
+
install_status: string;
|
|
69
|
+
};
|
|
70
|
+
api_key: {
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
scopes: string[];
|
|
74
|
+
expires_at: string | null;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
type OwniKnowledgeSourceType = 'text' | 'url' | 'faq' | 'product_feed' | 'website';
|
|
78
|
+
type OwniKnowledgeSourceInput = {
|
|
79
|
+
type: OwniKnowledgeSourceType;
|
|
80
|
+
name: string;
|
|
81
|
+
/** Required for `url`, `product_feed` and `website`; rejected for the others. */
|
|
82
|
+
source_url?: string;
|
|
83
|
+
/** Required for `text` and `faq`; rejected for the others. */
|
|
84
|
+
raw_text?: string;
|
|
85
|
+
metadata?: {
|
|
86
|
+
feed_format?: 'auto' | 'yandex' | 'google';
|
|
87
|
+
sync_enabled?: boolean;
|
|
88
|
+
sync_cron?: string;
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
type OwniKnowledgeSource = {
|
|
92
|
+
id: string;
|
|
93
|
+
type: string;
|
|
94
|
+
name: string;
|
|
95
|
+
status: string;
|
|
96
|
+
created_at: string;
|
|
97
|
+
};
|
|
98
|
+
type OwniEmbeddingJob = {
|
|
99
|
+
id: string;
|
|
100
|
+
status: string;
|
|
101
|
+
created_at: string;
|
|
102
|
+
};
|
|
103
|
+
type OwniDomain = {
|
|
104
|
+
id: string;
|
|
105
|
+
domain: string;
|
|
106
|
+
source: 'manual' | 'plugin';
|
|
107
|
+
created_at: string;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
declare const DEFAULT_BASE_URL = "https://app.owni.chat";
|
|
111
|
+
/** The API rejects bulk payloads larger than this, so the client chunks for you. */
|
|
112
|
+
declare const BULK_CHUNK_SIZE = 100;
|
|
113
|
+
type OwniClientOptions = {
|
|
114
|
+
/** Project API key, `ak_live_…`. Server-side only — never ship it to the browser. */
|
|
115
|
+
apiKey: string;
|
|
116
|
+
baseUrl?: string;
|
|
117
|
+
/** Optional: skips one `me()` round trip on the first catalog call. */
|
|
118
|
+
projectId?: string;
|
|
119
|
+
fetch?: FetchLike;
|
|
120
|
+
maxRetries?: number;
|
|
121
|
+
timeoutMs?: number;
|
|
122
|
+
};
|
|
123
|
+
declare class OwniClient {
|
|
124
|
+
private readonly http;
|
|
125
|
+
private projectIdPromise;
|
|
126
|
+
readonly products: ProductsResource;
|
|
127
|
+
readonly knowledge: KnowledgeResource;
|
|
128
|
+
readonly domains: DomainsResource;
|
|
129
|
+
constructor(options: OwniClientOptions);
|
|
130
|
+
/**
|
|
131
|
+
* Everything the key is bound to, including the `project_key` a plugin needs for the
|
|
132
|
+
* widget script tag. This is what makes "paste one API key" installs possible.
|
|
133
|
+
*/
|
|
134
|
+
me(): Promise<OwniIdentity>;
|
|
135
|
+
/** Resolved once per client and cached; concurrent callers share the same request. */
|
|
136
|
+
projectId(): Promise<string>;
|
|
137
|
+
/** @internal */
|
|
138
|
+
projectRequest<T>(options: {
|
|
139
|
+
method?: string;
|
|
140
|
+
path: (projectId: string) => string;
|
|
141
|
+
query?: Record<string, string | number | undefined>;
|
|
142
|
+
json?: unknown;
|
|
143
|
+
body?: BodyInit;
|
|
144
|
+
headers?: Record<string, string>;
|
|
145
|
+
}): Promise<T>;
|
|
146
|
+
}
|
|
147
|
+
declare class ProductsResource {
|
|
148
|
+
private readonly client;
|
|
149
|
+
constructor(client: OwniClient);
|
|
150
|
+
list(input?: {
|
|
151
|
+
page?: number;
|
|
152
|
+
pageSize?: number;
|
|
153
|
+
}): Promise<OwniProductPage>;
|
|
154
|
+
get(externalId: string): Promise<OwniProduct>;
|
|
155
|
+
upsert(product: OwniProductInput): Promise<{
|
|
156
|
+
product: OwniProduct;
|
|
157
|
+
created: boolean;
|
|
158
|
+
}>;
|
|
159
|
+
delete(externalId: string): Promise<OwniProduct>;
|
|
160
|
+
/**
|
|
161
|
+
* Sends products in chunks of {@link BULK_CHUNK_SIZE} and merges the results, so a
|
|
162
|
+
* full catalog can be passed in one call. Chunks are sequential on purpose: the API
|
|
163
|
+
* re-indexes embeddings after each batch and parallel writes just trip the rate limit.
|
|
164
|
+
*/
|
|
165
|
+
bulkUpsert(products: OwniProductInput[]): Promise<OwniBulkUpsertResult>;
|
|
166
|
+
}
|
|
167
|
+
declare class KnowledgeResource {
|
|
168
|
+
private readonly client;
|
|
169
|
+
constructor(client: OwniClient);
|
|
170
|
+
createSource(input: OwniKnowledgeSourceInput): Promise<OwniKnowledgeSource>;
|
|
171
|
+
/** `file` is a Blob/File; in Node 18+ use `new Blob([buffer])` or `fs.openAsBlob`. */
|
|
172
|
+
uploadFile(input: {
|
|
173
|
+
file: Blob;
|
|
174
|
+
filename: string;
|
|
175
|
+
name?: string;
|
|
176
|
+
}): Promise<OwniKnowledgeSource>;
|
|
177
|
+
reindex(sourceId: string): Promise<OwniEmbeddingJob>;
|
|
178
|
+
}
|
|
179
|
+
declare class DomainsResource {
|
|
180
|
+
private readonly client;
|
|
181
|
+
constructor(client: OwniClient);
|
|
182
|
+
list(): Promise<OwniDomain[]>;
|
|
183
|
+
/**
|
|
184
|
+
* Registers an origin the widget is allowed to load on. Idempotent — re-sending an
|
|
185
|
+
* existing domain returns `created: false` instead of failing, so plugins can call
|
|
186
|
+
* this on every settings save.
|
|
187
|
+
*/
|
|
188
|
+
add(domain: string): Promise<{
|
|
189
|
+
domain: OwniDomain;
|
|
190
|
+
created: boolean;
|
|
191
|
+
}>;
|
|
192
|
+
remove(domainId: string): Promise<{
|
|
193
|
+
status: string;
|
|
194
|
+
}>;
|
|
195
|
+
}
|
|
196
|
+
declare const createOwniClient: (options: OwniClientOptions) => OwniClient;
|
|
197
|
+
|
|
198
|
+
declare class OwniApiError extends Error {
|
|
199
|
+
readonly status: number;
|
|
200
|
+
readonly body: unknown;
|
|
201
|
+
constructor(status: number, message: string, body?: unknown);
|
|
202
|
+
/** 401/403 — a wrong, revoked, expired or under-scoped API key. Retrying never helps. */
|
|
203
|
+
get isAuthError(): boolean;
|
|
204
|
+
/** 429 — the external API allows 60 requests per minute per key. */
|
|
205
|
+
get isRateLimited(): boolean;
|
|
206
|
+
}
|
|
207
|
+
/** Raised when a request exceeds the client timeout or the connection fails outright. */
|
|
208
|
+
declare class OwniConnectionError extends Error {
|
|
209
|
+
readonly cause: unknown;
|
|
210
|
+
constructor(message: string, cause?: unknown);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { BULK_CHUNK_SIZE, DEFAULT_BASE_URL, type FetchLike, OwniApiError, type OwniBulkUpsertResult, OwniClient, type OwniClientOptions, OwniConnectionError, type OwniDomain, type OwniEmbeddingJob, type OwniIdentity, type OwniKnowledgeSource, type OwniKnowledgeSourceInput, type OwniKnowledgeSourceType, type OwniProduct, type OwniProductAttribute, type OwniProductInput, type OwniProductPage, createOwniClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { OwniConnectionError, OwniApiError } from './chunk-2OWJTVJ3.js';
|
|
2
|
+
export { OwniApiError, OwniConnectionError } from './chunk-2OWJTVJ3.js';
|
|
3
|
+
|
|
4
|
+
// src/http.ts
|
|
5
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
6
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
var retryDelayMs = (attempt, retryAfterHeader) => {
|
|
9
|
+
if (retryAfterHeader) {
|
|
10
|
+
const seconds = Number(retryAfterHeader);
|
|
11
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
12
|
+
return Math.min(seconds * 1e3, 6e4);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const base = Math.min(2 ** attempt * 500, 16e3);
|
|
16
|
+
return base + Math.random() * 250;
|
|
17
|
+
};
|
|
18
|
+
var isRetryableStatus = (status) => status === 429 || status >= 500;
|
|
19
|
+
var HttpClient = class {
|
|
20
|
+
baseUrl;
|
|
21
|
+
apiKey;
|
|
22
|
+
fetchImpl;
|
|
23
|
+
maxRetries;
|
|
24
|
+
timeoutMs;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
if (!options.apiKey) {
|
|
27
|
+
throw new Error("An owni.chat API key is required (ak_live_\u2026).");
|
|
28
|
+
}
|
|
29
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
30
|
+
this.apiKey = options.apiKey;
|
|
31
|
+
this.maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
32
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
33
|
+
const resolvedFetch = options.fetch ?? globalThis.fetch;
|
|
34
|
+
if (!resolvedFetch) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"No fetch implementation found. Use Node 18+ or pass `fetch` in the client options."
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
this.fetchImpl = resolvedFetch;
|
|
40
|
+
}
|
|
41
|
+
async request(options) {
|
|
42
|
+
const url = this.buildUrl(options.path, options.query);
|
|
43
|
+
const headers = {
|
|
44
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
45
|
+
Accept: "application/json",
|
|
46
|
+
...options.headers
|
|
47
|
+
};
|
|
48
|
+
let body = options.body;
|
|
49
|
+
if (options.json !== void 0) {
|
|
50
|
+
headers["Content-Type"] = "application/json";
|
|
51
|
+
body = JSON.stringify(options.json);
|
|
52
|
+
}
|
|
53
|
+
let lastError;
|
|
54
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt += 1) {
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = await this.fetchWithTimeout(url, {
|
|
58
|
+
method: options.method ?? "GET",
|
|
59
|
+
headers,
|
|
60
|
+
body
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
lastError = new OwniConnectionError(`Request to ${url} failed`, error);
|
|
64
|
+
if (attempt === this.maxRetries) throw lastError;
|
|
65
|
+
await sleep(retryDelayMs(attempt, null));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (response.ok) {
|
|
69
|
+
return await this.parseJson(response);
|
|
70
|
+
}
|
|
71
|
+
if (isRetryableStatus(response.status) && attempt < this.maxRetries) {
|
|
72
|
+
await sleep(retryDelayMs(attempt, response.headers.get("retry-after")));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
throw await this.toApiError(response);
|
|
76
|
+
}
|
|
77
|
+
throw lastError ?? new OwniConnectionError(`Request to ${url} failed`);
|
|
78
|
+
}
|
|
79
|
+
buildUrl(path, query) {
|
|
80
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
81
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
82
|
+
if (value !== void 0) {
|
|
83
|
+
url.searchParams.set(key, String(value));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return url.toString();
|
|
87
|
+
}
|
|
88
|
+
async fetchWithTimeout(url, init) {
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
91
|
+
try {
|
|
92
|
+
return await this.fetchImpl(url, { ...init, signal: controller.signal });
|
|
93
|
+
} finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async parseJson(response) {
|
|
98
|
+
const text = await response.text();
|
|
99
|
+
if (!text) return null;
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(text);
|
|
102
|
+
} catch {
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async toApiError(response) {
|
|
107
|
+
const body = await this.parseJson(response);
|
|
108
|
+
const message = body && typeof body === "object" && "message" in body && typeof body.message === "string" ? body.message : `owni.chat API responded with status ${response.status}`;
|
|
109
|
+
return new OwniApiError(response.status, message, body);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// src/client.ts
|
|
114
|
+
var DEFAULT_BASE_URL = "https://app.owni.chat";
|
|
115
|
+
var BULK_CHUNK_SIZE = 100;
|
|
116
|
+
var OwniClient = class {
|
|
117
|
+
http;
|
|
118
|
+
projectIdPromise = null;
|
|
119
|
+
products;
|
|
120
|
+
knowledge;
|
|
121
|
+
domains;
|
|
122
|
+
constructor(options) {
|
|
123
|
+
this.http = new HttpClient({
|
|
124
|
+
baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
|
|
125
|
+
apiKey: options.apiKey,
|
|
126
|
+
fetch: options.fetch,
|
|
127
|
+
maxRetries: options.maxRetries,
|
|
128
|
+
timeoutMs: options.timeoutMs
|
|
129
|
+
});
|
|
130
|
+
if (options.projectId) {
|
|
131
|
+
this.projectIdPromise = Promise.resolve(options.projectId);
|
|
132
|
+
}
|
|
133
|
+
this.products = new ProductsResource(this);
|
|
134
|
+
this.knowledge = new KnowledgeResource(this);
|
|
135
|
+
this.domains = new DomainsResource(this);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Everything the key is bound to, including the `project_key` a plugin needs for the
|
|
139
|
+
* widget script tag. This is what makes "paste one API key" installs possible.
|
|
140
|
+
*/
|
|
141
|
+
me() {
|
|
142
|
+
return this.http.request({ path: "/api/external/v1/me" });
|
|
143
|
+
}
|
|
144
|
+
/** Resolved once per client and cached; concurrent callers share the same request. */
|
|
145
|
+
projectId() {
|
|
146
|
+
if (!this.projectIdPromise) {
|
|
147
|
+
this.projectIdPromise = this.me().then((identity) => identity.project.id).catch((error) => {
|
|
148
|
+
this.projectIdPromise = null;
|
|
149
|
+
throw error;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return this.projectIdPromise;
|
|
153
|
+
}
|
|
154
|
+
/** @internal */
|
|
155
|
+
async projectRequest(options) {
|
|
156
|
+
const projectId = await this.projectId();
|
|
157
|
+
return this.http.request({
|
|
158
|
+
method: options.method,
|
|
159
|
+
path: options.path(projectId),
|
|
160
|
+
query: options.query,
|
|
161
|
+
json: options.json,
|
|
162
|
+
body: options.body,
|
|
163
|
+
headers: options.headers
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
var ProductsResource = class {
|
|
168
|
+
constructor(client) {
|
|
169
|
+
this.client = client;
|
|
170
|
+
}
|
|
171
|
+
client;
|
|
172
|
+
list(input = {}) {
|
|
173
|
+
return this.client.projectRequest({
|
|
174
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products`,
|
|
175
|
+
query: { page: input.page, page_size: input.pageSize }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async get(externalId) {
|
|
179
|
+
const data = await this.client.projectRequest({
|
|
180
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/${encodeURIComponent(externalId)}`
|
|
181
|
+
});
|
|
182
|
+
return data.product;
|
|
183
|
+
}
|
|
184
|
+
async upsert(product) {
|
|
185
|
+
return this.client.projectRequest({
|
|
186
|
+
method: "PUT",
|
|
187
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/${encodeURIComponent(product.external_id)}`,
|
|
188
|
+
json: product
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async delete(externalId) {
|
|
192
|
+
const data = await this.client.projectRequest({
|
|
193
|
+
method: "DELETE",
|
|
194
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/${encodeURIComponent(externalId)}`
|
|
195
|
+
});
|
|
196
|
+
return data.product;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Sends products in chunks of {@link BULK_CHUNK_SIZE} and merges the results, so a
|
|
200
|
+
* full catalog can be passed in one call. Chunks are sequential on purpose: the API
|
|
201
|
+
* re-indexes embeddings after each batch and parallel writes just trip the rate limit.
|
|
202
|
+
*/
|
|
203
|
+
async bulkUpsert(products) {
|
|
204
|
+
const merged = { upserted: 0, created: 0, updated: 0, items: [] };
|
|
205
|
+
if (products.length === 0) return merged;
|
|
206
|
+
for (let index = 0; index < products.length; index += BULK_CHUNK_SIZE) {
|
|
207
|
+
const chunk = products.slice(index, index + BULK_CHUNK_SIZE);
|
|
208
|
+
const result = await this.client.projectRequest({
|
|
209
|
+
method: "POST",
|
|
210
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/products/bulk`,
|
|
211
|
+
json: { products: chunk }
|
|
212
|
+
});
|
|
213
|
+
merged.upserted += result.upserted;
|
|
214
|
+
merged.created += result.created;
|
|
215
|
+
merged.updated += result.updated;
|
|
216
|
+
merged.items.push(...result.items);
|
|
217
|
+
}
|
|
218
|
+
return merged;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
var KnowledgeResource = class {
|
|
222
|
+
constructor(client) {
|
|
223
|
+
this.client = client;
|
|
224
|
+
}
|
|
225
|
+
client;
|
|
226
|
+
async createSource(input) {
|
|
227
|
+
const data = await this.client.projectRequest({
|
|
228
|
+
method: "POST",
|
|
229
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/knowledge-sources`,
|
|
230
|
+
json: input
|
|
231
|
+
});
|
|
232
|
+
return data.knowledge_source;
|
|
233
|
+
}
|
|
234
|
+
/** `file` is a Blob/File; in Node 18+ use `new Blob([buffer])` or `fs.openAsBlob`. */
|
|
235
|
+
async uploadFile(input) {
|
|
236
|
+
const form = new FormData();
|
|
237
|
+
form.append("file", input.file, input.filename);
|
|
238
|
+
if (input.name) form.append("name", input.name);
|
|
239
|
+
const data = await this.client.projectRequest({
|
|
240
|
+
method: "POST",
|
|
241
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/knowledge-sources/upload`,
|
|
242
|
+
body: form
|
|
243
|
+
});
|
|
244
|
+
return data.knowledge_source;
|
|
245
|
+
}
|
|
246
|
+
async reindex(sourceId) {
|
|
247
|
+
const data = await this.client.projectRequest({
|
|
248
|
+
method: "POST",
|
|
249
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/knowledge-sources/${encodeURIComponent(sourceId)}/reindex`
|
|
250
|
+
});
|
|
251
|
+
return data.embedding_job;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var DomainsResource = class {
|
|
255
|
+
constructor(client) {
|
|
256
|
+
this.client = client;
|
|
257
|
+
}
|
|
258
|
+
client;
|
|
259
|
+
async list() {
|
|
260
|
+
const data = await this.client.projectRequest({
|
|
261
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/domains`
|
|
262
|
+
});
|
|
263
|
+
return data.domains;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Registers an origin the widget is allowed to load on. Idempotent — re-sending an
|
|
267
|
+
* existing domain returns `created: false` instead of failing, so plugins can call
|
|
268
|
+
* this on every settings save.
|
|
269
|
+
*/
|
|
270
|
+
add(domain) {
|
|
271
|
+
return this.client.projectRequest({
|
|
272
|
+
method: "POST",
|
|
273
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/domains`,
|
|
274
|
+
json: { domain }
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
remove(domainId) {
|
|
278
|
+
return this.client.projectRequest({
|
|
279
|
+
method: "DELETE",
|
|
280
|
+
path: (projectId) => `/api/external/v1/projects/${projectId}/domains/${encodeURIComponent(domainId)}`
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
var createOwniClient = (options) => new OwniClient(options);
|
|
285
|
+
|
|
286
|
+
export { BULK_CHUNK_SIZE, DEFAULT_BASE_URL, OwniClient, createOwniClient };
|