@mybase-dev/sdk 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +147 -34
- package/dist/analytics-script.d.ts +3 -0
- package/dist/better-auth.d.ts +123 -0
- package/dist/better-auth.js +81 -0
- package/dist/captcha-widget.d.ts +1 -0
- package/dist/captcha.d.ts +54 -0
- package/dist/captcha.js +239 -0
- package/dist/captcha.min.js +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/search.d.ts +120 -0
- package/dist/search.js +315 -0
- package/dist/secrets.d.ts +138 -0
- package/dist/secrets.js +274 -0
- package/dist/vector.d.ts +234 -0
- package/dist/vector.js +331 -0
- package/package.json +33 -3
- package/src/better-auth.ts +189 -0
- package/src/captcha.ts +110 -0
- package/src/config.ts +10 -3
- package/src/search.ts +250 -0
- package/src/secrets.ts +234 -0
- package/src/vector.ts +356 -0
package/dist/secrets.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
function defineConfig(config) {
|
|
3
|
+
return config;
|
|
4
|
+
}
|
|
5
|
+
var DEFAULT_CRON_PATH = "/api/cron";
|
|
6
|
+
|
|
7
|
+
// src/cron.ts
|
|
8
|
+
var SIGNATURE_HEADER = "X-Mybase-Signature";
|
|
9
|
+
var JOB_HEADER = "X-Mybase-Job";
|
|
10
|
+
var RUN_HEADER = "X-Mybase-Run";
|
|
11
|
+
|
|
12
|
+
class CronVerificationError extends Error {
|
|
13
|
+
name = "CronVerificationError";
|
|
14
|
+
}
|
|
15
|
+
var encoder = new TextEncoder;
|
|
16
|
+
async function hmacHex(secret, message) {
|
|
17
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
18
|
+
const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, encoder.encode(message)));
|
|
19
|
+
return Array.from(mac, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
20
|
+
}
|
|
21
|
+
function safeEqual(a, b) {
|
|
22
|
+
if (a.length !== b.length)
|
|
23
|
+
return false;
|
|
24
|
+
let diff = 0;
|
|
25
|
+
for (let i = 0;i < a.length; i++)
|
|
26
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
27
|
+
return diff === 0;
|
|
28
|
+
}
|
|
29
|
+
async function signCronRequest(secret, body, t = Math.floor(Date.now() / 1000)) {
|
|
30
|
+
return `t=${t},v1=${await hmacHex(secret, `${t}.${body}`)}`;
|
|
31
|
+
}
|
|
32
|
+
async function verifySigned(signature, secret, toleranceSec, message) {
|
|
33
|
+
if (!signature)
|
|
34
|
+
throw new CronVerificationError(`missing ${SIGNATURE_HEADER} header`);
|
|
35
|
+
let t;
|
|
36
|
+
const sigs = [];
|
|
37
|
+
for (const part of signature.split(",")) {
|
|
38
|
+
const [k, v] = part.trim().split("=", 2);
|
|
39
|
+
if (k === "t" && v && /^\d+$/.test(v))
|
|
40
|
+
t = Number(v);
|
|
41
|
+
else if (k === "v1" && v)
|
|
42
|
+
sigs.push(v);
|
|
43
|
+
}
|
|
44
|
+
if (t === undefined || sigs.length === 0)
|
|
45
|
+
throw new CronVerificationError(`malformed ${SIGNATURE_HEADER} header`);
|
|
46
|
+
if (Math.abs(Date.now() / 1000 - t) > toleranceSec)
|
|
47
|
+
throw new CronVerificationError("signature timestamp outside tolerance");
|
|
48
|
+
const secrets = typeof secret === "string" ? [secret] : secret;
|
|
49
|
+
if (secrets.length === 0 || secrets.some((s) => !s))
|
|
50
|
+
throw new CronVerificationError("no cron secret configured");
|
|
51
|
+
for (const s of secrets) {
|
|
52
|
+
const expected = await hmacHex(s, message(t));
|
|
53
|
+
if (sigs.some((sig) => safeEqual(sig, expected)))
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
throw new CronVerificationError("signature does not match");
|
|
57
|
+
}
|
|
58
|
+
async function verifyCronSignature({ body, signature, secret, toleranceSec = 300 }) {
|
|
59
|
+
await verifySigned(signature, secret, toleranceSec, (t) => `${t}.${body}`);
|
|
60
|
+
}
|
|
61
|
+
async function verifyCronRequest(req, secret, options = {}) {
|
|
62
|
+
const body = await req.text();
|
|
63
|
+
await verifyCronSignature({ body, signature: req.headers.get(SIGNATURE_HEADER), secret, ...options });
|
|
64
|
+
let run;
|
|
65
|
+
try {
|
|
66
|
+
run = JSON.parse(body);
|
|
67
|
+
} catch {
|
|
68
|
+
throw new CronVerificationError("body is not JSON");
|
|
69
|
+
}
|
|
70
|
+
if (typeof run?.job !== "string" || typeof run.runId !== "string")
|
|
71
|
+
throw new CronVerificationError("body is not a cron run");
|
|
72
|
+
return run;
|
|
73
|
+
}
|
|
74
|
+
var reply = (body, status) => new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
|
|
75
|
+
function cronHandler(options) {
|
|
76
|
+
const jobs = options.jobs;
|
|
77
|
+
return async (req) => {
|
|
78
|
+
if (req.method !== "POST")
|
|
79
|
+
return reply({ error: "method not allowed" }, 405);
|
|
80
|
+
let run;
|
|
81
|
+
try {
|
|
82
|
+
run = await verifyCronRequest(req, options.secret, { toleranceSec: options.toleranceSec });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err instanceof CronVerificationError)
|
|
85
|
+
return reply({ error: err.message }, 401);
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
const job = Object.hasOwn(jobs, run.job) ? jobs[run.job] : undefined;
|
|
89
|
+
if (!job)
|
|
90
|
+
return reply({ error: `no handler for job "${run.job}"` }, 404);
|
|
91
|
+
try {
|
|
92
|
+
await job(run);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (options.onError)
|
|
95
|
+
options.onError(err, run);
|
|
96
|
+
else
|
|
97
|
+
console.error(`mybase cron job "${run.job}" failed`, err);
|
|
98
|
+
return reply({ error: "job failed" }, 500);
|
|
99
|
+
}
|
|
100
|
+
return reply({ ok: true }, 200);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
class MybaseApiError extends Error {
|
|
105
|
+
status;
|
|
106
|
+
name = "MybaseApiError";
|
|
107
|
+
constructor(message, status) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.status = status;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
var UNITS = { ms: 1, s: 1000, m: 60000, h: 3600000, d: 86400000 };
|
|
113
|
+
function parseDelay(value) {
|
|
114
|
+
if (typeof value === "number") {
|
|
115
|
+
if (!Number.isFinite(value) || value < 0)
|
|
116
|
+
throw new RangeError("delay must be a non-negative number of ms");
|
|
117
|
+
return Math.round(value);
|
|
118
|
+
}
|
|
119
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)\s*$/.exec(value);
|
|
120
|
+
if (!m)
|
|
121
|
+
throw new RangeError(`invalid delay "${value}". Use ms, or a number with ms, s, m, h, or d (e.g. "15m")`);
|
|
122
|
+
return Math.round(Number(m[1]) * UNITS[m[2]]);
|
|
123
|
+
}
|
|
124
|
+
function createCronClient(options) {
|
|
125
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
126
|
+
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
127
|
+
const base = `/cron/${encodeURIComponent(options.project)}/tasks`;
|
|
128
|
+
async function call(method, path, body) {
|
|
129
|
+
const raw = body === undefined ? "" : JSON.stringify(body);
|
|
130
|
+
const res = await doFetch(`${apiUrl}${path}`, {
|
|
131
|
+
method,
|
|
132
|
+
body: body === undefined ? undefined : raw,
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
Authorization: `Bearer ${options.apiKey}`
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
if (res.status === 404 && method !== "POST")
|
|
139
|
+
return null;
|
|
140
|
+
const data = await res.json().catch(() => ({}));
|
|
141
|
+
if (!res.ok)
|
|
142
|
+
throw new MybaseApiError(data.error ?? `mybase API answered ${res.status}`, res.status);
|
|
143
|
+
return data;
|
|
144
|
+
}
|
|
145
|
+
const byId = (id) => `${base}/${encodeURIComponent(id)}`;
|
|
146
|
+
const byKey = (key) => `${base}/by-key/${encodeURIComponent(key)}`;
|
|
147
|
+
return {
|
|
148
|
+
async schedule(job, opts = {}) {
|
|
149
|
+
if (opts.at !== undefined && opts.in !== undefined)
|
|
150
|
+
throw new TypeError("give `at` or `in`, not both");
|
|
151
|
+
let url = opts.url;
|
|
152
|
+
if (!url) {
|
|
153
|
+
if (!options.baseUrl)
|
|
154
|
+
throw new TypeError("set `baseUrl` on the client, or `url` on the task");
|
|
155
|
+
url = `${options.baseUrl.replace(/\/+$/, "")}/${(opts.path ?? DEFAULT_CRON_PATH).replace(/^\/+/, "")}`;
|
|
156
|
+
}
|
|
157
|
+
const at = opts.at === undefined ? undefined : new Date(opts.at);
|
|
158
|
+
if (at && Number.isNaN(at.getTime()))
|
|
159
|
+
throw new RangeError(`invalid date "${String(opts.at)}"`);
|
|
160
|
+
const req = {
|
|
161
|
+
job,
|
|
162
|
+
url,
|
|
163
|
+
runAt: at?.toISOString(),
|
|
164
|
+
delayMs: opts.in === undefined ? undefined : parseDelay(opts.in),
|
|
165
|
+
key: opts.key,
|
|
166
|
+
payload: opts.payload,
|
|
167
|
+
retries: opts.retries,
|
|
168
|
+
timeoutMs: opts.timeoutMs
|
|
169
|
+
};
|
|
170
|
+
return await call("POST", base, req);
|
|
171
|
+
},
|
|
172
|
+
get: (id) => call("GET", byId(id)),
|
|
173
|
+
getByKey: (key) => call("GET", byKey(key)),
|
|
174
|
+
cancel: (id) => call("DELETE", byId(id)),
|
|
175
|
+
cancelByKey: (key) => call("DELETE", byKey(key))
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/secrets.ts
|
|
180
|
+
var SECRETS_ENV_PATTERN = /^[a-z][a-z0-9_-]{0,39}$/;
|
|
181
|
+
var SECRETS_ENV_RULES = "1–40 characters: lowercase letters, digits, _ or -, starting with a letter";
|
|
182
|
+
var SECRET_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
183
|
+
var SECRET_KEY_RULES = "1–128 characters: letters, digits, or _, not starting with a digit";
|
|
184
|
+
var SECRETS_LIMITS = {
|
|
185
|
+
valueBytes: 64 * 1024,
|
|
186
|
+
keysPerEnv: 1000,
|
|
187
|
+
envsPerProject: 50,
|
|
188
|
+
revisionsKept: 200,
|
|
189
|
+
messageLength: 200
|
|
190
|
+
};
|
|
191
|
+
var DEFAULT_SECRETS_ENVS = ["dev", "staging", "prod"];
|
|
192
|
+
var MANAGED_SECRET_KEYS = [
|
|
193
|
+
"DATABASE_URL",
|
|
194
|
+
"S3_ENDPOINT",
|
|
195
|
+
"S3_REGION",
|
|
196
|
+
"S3_BUCKET",
|
|
197
|
+
"S3_ACCESS_KEY_ID",
|
|
198
|
+
"S3_SECRET_ACCESS_KEY",
|
|
199
|
+
"REDIS_URL",
|
|
200
|
+
"MYBASE_URL",
|
|
201
|
+
"MYBASE_PROJECT"
|
|
202
|
+
];
|
|
203
|
+
function createSecretsClient(options) {
|
|
204
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
205
|
+
const url = `${options.apiUrl.replace(/\/+$/, "")}/secrets/${encodeURIComponent(options.project)}/${encodeURIComponent(options.env)}`;
|
|
206
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
207
|
+
const maxAgeMs = options.maxAgeMs ?? 0;
|
|
208
|
+
const headers = { Authorization: `Bearer ${options.apiKey}` };
|
|
209
|
+
let cached;
|
|
210
|
+
async function fail(res) {
|
|
211
|
+
const data = await res.json().catch(() => ({}));
|
|
212
|
+
throw new MybaseApiError(data.error ?? `mybase API answered ${res.status}`, res.status);
|
|
213
|
+
}
|
|
214
|
+
async function all() {
|
|
215
|
+
if (cached && Date.now() - cached.at < maxAgeMs)
|
|
216
|
+
return { ...cached.secrets };
|
|
217
|
+
const res = await doFetch(url, {
|
|
218
|
+
headers: cached?.etag ? { ...headers, "If-None-Match": cached.etag } : headers,
|
|
219
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
220
|
+
});
|
|
221
|
+
if (res.status === 304 && cached) {
|
|
222
|
+
cached.at = Date.now();
|
|
223
|
+
return { ...cached.secrets };
|
|
224
|
+
}
|
|
225
|
+
if (!res.ok)
|
|
226
|
+
return fail(res);
|
|
227
|
+
const data = await res.json();
|
|
228
|
+
cached = { etag: res.headers.get("ETag"), secrets: data.secrets, at: Date.now() };
|
|
229
|
+
return { ...data.secrets };
|
|
230
|
+
}
|
|
231
|
+
async function update(body) {
|
|
232
|
+
const res = await doFetch(url, {
|
|
233
|
+
method: "PATCH",
|
|
234
|
+
body: JSON.stringify(body),
|
|
235
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
236
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
237
|
+
});
|
|
238
|
+
if (!res.ok)
|
|
239
|
+
return fail(res);
|
|
240
|
+
cached = undefined;
|
|
241
|
+
return await res.json();
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
all,
|
|
245
|
+
get: async (key) => (await all())[key],
|
|
246
|
+
async load({ override = false } = {}) {
|
|
247
|
+
const secrets = await all();
|
|
248
|
+
const env = globalThis.process?.env;
|
|
249
|
+
if (!env)
|
|
250
|
+
throw new Error("load() needs process.env (Node, Bun, Deno); use all() elsewhere");
|
|
251
|
+
const set = [];
|
|
252
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
253
|
+
if (!override && env[key] !== undefined)
|
|
254
|
+
continue;
|
|
255
|
+
env[key] = value;
|
|
256
|
+
set.push(key);
|
|
257
|
+
}
|
|
258
|
+
return set;
|
|
259
|
+
},
|
|
260
|
+
set: (values, opts = {}) => update({ set: values, message: opts.message }),
|
|
261
|
+
unset: (keys, opts = {}) => update({ unset: [keys].flat(), message: opts.message })
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
export {
|
|
265
|
+
DEFAULT_SECRETS_ENVS,
|
|
266
|
+
MANAGED_SECRET_KEYS,
|
|
267
|
+
MybaseApiError,
|
|
268
|
+
SECRETS_ENV_PATTERN,
|
|
269
|
+
SECRETS_ENV_RULES,
|
|
270
|
+
SECRETS_LIMITS,
|
|
271
|
+
SECRET_KEY_PATTERN,
|
|
272
|
+
SECRET_KEY_RULES,
|
|
273
|
+
createSecretsClient
|
|
274
|
+
};
|
package/dist/vector.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { MybaseApiError } from "./cron";
|
|
2
|
+
import type { ToolInputSchema } from "./search";
|
|
3
|
+
export { MybaseApiError };
|
|
4
|
+
export type { ToolInputSchema };
|
|
5
|
+
/** Index names: they become table names, so lowercase letters, digits, and `_`. */
|
|
6
|
+
export declare const VECTOR_INDEX_PATTERN: RegExp;
|
|
7
|
+
export declare const VECTOR_INDEX_RULES = "Use 1\u201348 lowercase letters, digits, or `_`, starting with a letter.";
|
|
8
|
+
/** How vectors are compared. `cosine` suits most embedding models. */
|
|
9
|
+
export declare const VECTOR_METRICS: readonly ["cosine", "l2", "ip"];
|
|
10
|
+
export type VectorMetric = (typeof VECTOR_METRICS)[number];
|
|
11
|
+
/** Postgres text search configurations: stemming and stop words. `simple` does neither. */
|
|
12
|
+
export declare const VECTOR_LANGUAGES: readonly ["simple", "arabic", "armenian", "basque", "catalan", "danish", "dutch", "english", "finnish", "french", "german", "greek", "hindi", "hungarian", "indonesian", "irish", "italian", "lithuanian", "nepali", "norwegian", "portuguese", "romanian", "russian", "serbian", "spanish", "swedish", "tamil", "turkish", "yiddish"];
|
|
13
|
+
export type VectorLanguage = (typeof VECTOR_LANGUAGES)[number];
|
|
14
|
+
export declare const VECTOR_QUERY_MODES: readonly ["text", "vector", "hybrid"];
|
|
15
|
+
export type VectorQueryMode = (typeof VECTOR_QUERY_MODES)[number];
|
|
16
|
+
export declare const VECTOR_LIMITS: {
|
|
17
|
+
/** Documents per upsert, get, or delete. */
|
|
18
|
+
readonly batch: 100;
|
|
19
|
+
/** One upsert's JSON body. */
|
|
20
|
+
readonly batchBytes: number;
|
|
21
|
+
readonly idLength: 256;
|
|
22
|
+
readonly titleLength: 1000;
|
|
23
|
+
readonly textLength: 100000;
|
|
24
|
+
/** A document's metadata, as JSON. */
|
|
25
|
+
readonly metadataBytes: number;
|
|
26
|
+
/** Vector dimensions (pgvector's HNSW index takes up to 2000). */
|
|
27
|
+
readonly dimensions: {
|
|
28
|
+
readonly min: 1;
|
|
29
|
+
readonly max: 2000;
|
|
30
|
+
};
|
|
31
|
+
readonly topK: {
|
|
32
|
+
readonly min: 1;
|
|
33
|
+
readonly max: 100;
|
|
34
|
+
readonly default: 10;
|
|
35
|
+
};
|
|
36
|
+
/** Query text. */
|
|
37
|
+
readonly queryLength: 2000;
|
|
38
|
+
readonly indexesPerProject: 50;
|
|
39
|
+
};
|
|
40
|
+
/** Where an index is defined: dashboard/CLI, mybase.config.ts (`mybase vector sync`), or app code. */
|
|
41
|
+
export type VectorIndexSource = "dashboard" | "code" | "api";
|
|
42
|
+
export type VectorMetadata = Record<string, unknown>;
|
|
43
|
+
/** An index's settings. Dimensions, metric, and language are fixed at creation. */
|
|
44
|
+
export interface VectorIndexSettings {
|
|
45
|
+
/** Vector length; null = a text-only index (no vectors). */
|
|
46
|
+
dimensions: number | null;
|
|
47
|
+
metric: VectorMetric;
|
|
48
|
+
language: VectorLanguage;
|
|
49
|
+
/** mybase embeds `text` with the project's embedding provider when a document or query has no vector. */
|
|
50
|
+
embed: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface VectorIndexInput {
|
|
53
|
+
/** Omit with `embed: true` to use the provider's dimensions. */
|
|
54
|
+
dimensions?: number | null;
|
|
55
|
+
/** Default `cosine`. */
|
|
56
|
+
metric?: VectorMetric;
|
|
57
|
+
/** Default `english`. */
|
|
58
|
+
language?: VectorLanguage;
|
|
59
|
+
/** Default false. Needs an embedding provider in the project's vector settings. */
|
|
60
|
+
embed?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface VectorIndex extends VectorIndexSettings {
|
|
63
|
+
name: string;
|
|
64
|
+
source: VectorIndexSource;
|
|
65
|
+
/** Approximate document count (Postgres statistics, exact for small tables). */
|
|
66
|
+
documents: number;
|
|
67
|
+
createdAt: string;
|
|
68
|
+
}
|
|
69
|
+
/** A document to upsert. Documents with the same id are replaced. */
|
|
70
|
+
export interface VectorDocument<M extends VectorMetadata = VectorMetadata> {
|
|
71
|
+
id: string;
|
|
72
|
+
/** Searched by keyword, weighted above `text`. */
|
|
73
|
+
title?: string | null;
|
|
74
|
+
/** Searched by keyword, and embedded when the index has `embed` on and no `vector` is given. */
|
|
75
|
+
text?: string;
|
|
76
|
+
/** Free-form JSON, matched by `filter`. */
|
|
77
|
+
metadata?: M;
|
|
78
|
+
vector?: number[];
|
|
79
|
+
}
|
|
80
|
+
export interface StoredDocument<M extends VectorMetadata = VectorMetadata> {
|
|
81
|
+
id: string;
|
|
82
|
+
title: string | null;
|
|
83
|
+
text: string;
|
|
84
|
+
metadata: M;
|
|
85
|
+
/** Only when asked for (`includeVectors`). */
|
|
86
|
+
vector?: number[] | null;
|
|
87
|
+
updatedAt: string;
|
|
88
|
+
}
|
|
89
|
+
export interface UpsertDocumentsRequest {
|
|
90
|
+
documents: VectorDocument[];
|
|
91
|
+
}
|
|
92
|
+
export interface UpsertDocumentsResponse {
|
|
93
|
+
upserted: number;
|
|
94
|
+
/** How many of them mybase embedded. */
|
|
95
|
+
embedded: number;
|
|
96
|
+
}
|
|
97
|
+
export interface DeleteDocumentsRequest {
|
|
98
|
+
ids?: string[];
|
|
99
|
+
/** Deletes every document whose metadata contains this object. */
|
|
100
|
+
filter?: VectorMetadata;
|
|
101
|
+
}
|
|
102
|
+
export interface DeleteDocumentsResponse {
|
|
103
|
+
deleted: number;
|
|
104
|
+
}
|
|
105
|
+
export interface ListDocumentsResponse<M extends VectorMetadata = VectorMetadata> {
|
|
106
|
+
documents: StoredDocument<M>[];
|
|
107
|
+
/** Pass as `after` for the next page; null at the end. */
|
|
108
|
+
next: string | null;
|
|
109
|
+
}
|
|
110
|
+
export interface VectorQueryRequest {
|
|
111
|
+
/** Keywords for text search, and embedded for vector search when no `vector` is given. */
|
|
112
|
+
text?: string;
|
|
113
|
+
vector?: number[];
|
|
114
|
+
/** Default: `hybrid` when both a vector (given or embedded) and text are available, else whichever is. */
|
|
115
|
+
mode?: VectorQueryMode;
|
|
116
|
+
/** 1–100, default 10. */
|
|
117
|
+
topK?: number;
|
|
118
|
+
/** Only documents whose metadata contains this object, e.g. `{ lang: "en" }`. */
|
|
119
|
+
filter?: VectorMetadata;
|
|
120
|
+
/** Hybrid only: weight of the vector ranking, 0–1 (text gets the rest). Default 0.5. */
|
|
121
|
+
alpha?: number;
|
|
122
|
+
/** Return each match's text. Default true. */
|
|
123
|
+
includeText?: boolean;
|
|
124
|
+
includeVectors?: boolean;
|
|
125
|
+
/** Text and hybrid: a snippet with matched words wrapped in `<b>…</b>`. */
|
|
126
|
+
highlight?: boolean;
|
|
127
|
+
}
|
|
128
|
+
export interface VectorMatch<M extends VectorMetadata = VectorMetadata> {
|
|
129
|
+
id: string;
|
|
130
|
+
/**
|
|
131
|
+
* Higher is better. text: ts_rank_cd. vector: cosine similarity (cosine), negative distance (l2),
|
|
132
|
+
* or inner product (ip). hybrid: reciprocal rank fusion.
|
|
133
|
+
*/
|
|
134
|
+
score: number;
|
|
135
|
+
title: string | null;
|
|
136
|
+
text?: string;
|
|
137
|
+
metadata: M;
|
|
138
|
+
vector?: number[] | null;
|
|
139
|
+
highlight?: string;
|
|
140
|
+
}
|
|
141
|
+
export interface VectorQueryResponse<M extends VectorMetadata = VectorMetadata> {
|
|
142
|
+
mode: VectorQueryMode;
|
|
143
|
+
matches: VectorMatch<M>[];
|
|
144
|
+
}
|
|
145
|
+
/** The project's embedding provider: any OpenAI-compatible `POST <url>/embeddings`. */
|
|
146
|
+
export interface EmbeddingSettings {
|
|
147
|
+
/** e.g. `https://api.openai.com/v1` or `http://ollama.lan:11434/v1`. */
|
|
148
|
+
url: string;
|
|
149
|
+
model: string;
|
|
150
|
+
/** Vector length the model returns. */
|
|
151
|
+
dimensions: number;
|
|
152
|
+
/** Whether an API key is stored (it's never returned). */
|
|
153
|
+
hasKey: boolean;
|
|
154
|
+
}
|
|
155
|
+
export interface VectorServiceSettings {
|
|
156
|
+
/** The API's public URL, for `createVectorClient({ apiUrl })`. */
|
|
157
|
+
apiUrl: string;
|
|
158
|
+
embedding: EmbeddingSettings | null;
|
|
159
|
+
enabledAt: string;
|
|
160
|
+
}
|
|
161
|
+
export interface VectorClientOptions {
|
|
162
|
+
/** Where the mybase API is reachable from your app, e.g. `https://mybase.example.com`. */
|
|
163
|
+
apiUrl: string;
|
|
164
|
+
project: string;
|
|
165
|
+
/** A mybase API key (`mb_…`) with `vector:write` for this project (`vector:read` is enough to query). Server-side only. */
|
|
166
|
+
apiKey: string;
|
|
167
|
+
/** How long to wait for mybase, in ms. Default 30000 (embedding can take a while). */
|
|
168
|
+
timeoutMs?: number;
|
|
169
|
+
/** Default `globalThis.fetch`. */
|
|
170
|
+
fetch?: typeof fetch;
|
|
171
|
+
}
|
|
172
|
+
export type QueryOptions = Omit<VectorQueryRequest, "text">;
|
|
173
|
+
export interface VectorIndexClient<M extends VectorMetadata = VectorMetadata> {
|
|
174
|
+
readonly name: string;
|
|
175
|
+
/** Inserts or replaces documents by id, up to 100 per call. */
|
|
176
|
+
upsert(documents: VectorDocument<M> | VectorDocument<M>[]): Promise<UpsertDocumentsResponse>;
|
|
177
|
+
/** Documents by id; missing ones are left out. */
|
|
178
|
+
get(ids: string | string[], options?: {
|
|
179
|
+
includeVectors?: boolean;
|
|
180
|
+
}): Promise<StoredDocument<M>[]>;
|
|
181
|
+
/** By ids, or by `{ filter }`. */
|
|
182
|
+
delete(target: string | string[] | {
|
|
183
|
+
filter: Partial<M> & VectorMetadata;
|
|
184
|
+
}): Promise<DeleteDocumentsResponse>;
|
|
185
|
+
/** Search by text (`query("…")`), or pass a full request (`query({ vector, filter })`). */
|
|
186
|
+
query(query: string | VectorQueryRequest, options?: QueryOptions): Promise<VectorQueryResponse<M>>;
|
|
187
|
+
/** Pages through documents in id order. */
|
|
188
|
+
list(options?: {
|
|
189
|
+
limit?: number;
|
|
190
|
+
after?: string;
|
|
191
|
+
}): Promise<ListDocumentsResponse<M>>;
|
|
192
|
+
info(): Promise<VectorIndex>;
|
|
193
|
+
}
|
|
194
|
+
export interface VectorClient {
|
|
195
|
+
index<M extends VectorMetadata = VectorMetadata>(name: string): VectorIndexClient<M>;
|
|
196
|
+
listIndexes(): Promise<VectorIndex[]>;
|
|
197
|
+
getIndex(name: string): Promise<VectorIndex>;
|
|
198
|
+
/** Idempotent: an existing index with the same settings is returned as is. */
|
|
199
|
+
createIndex(name: string, settings?: VectorIndexInput): Promise<VectorIndex>;
|
|
200
|
+
/** Only `embed` can change. */
|
|
201
|
+
updateIndex(name: string, settings: {
|
|
202
|
+
embed: boolean;
|
|
203
|
+
}): Promise<VectorIndex>;
|
|
204
|
+
/** Deletes the index and all its documents. */
|
|
205
|
+
deleteIndex(name: string): Promise<void>;
|
|
206
|
+
}
|
|
207
|
+
export declare function createVectorClient(options: VectorClientOptions): VectorClient;
|
|
208
|
+
/**
|
|
209
|
+
* A tool for an LLM agent, independent of the provider. Map it to yours:
|
|
210
|
+
* Anthropic `{ name, description, input_schema: inputSchema }`,
|
|
211
|
+
* OpenAI `{ type: "function", function: { name, description, parameters: inputSchema } }`.
|
|
212
|
+
*/
|
|
213
|
+
export interface RetrievalTool {
|
|
214
|
+
name: string;
|
|
215
|
+
description: string;
|
|
216
|
+
inputSchema: ToolInputSchema;
|
|
217
|
+
/** Runs the search and returns text for the tool result. Fixable problems come back as `Error: …`. */
|
|
218
|
+
run(input: unknown): Promise<string>;
|
|
219
|
+
}
|
|
220
|
+
export interface RetrievalToolOptions {
|
|
221
|
+
/** Default `search_documents`. */
|
|
222
|
+
name?: string;
|
|
223
|
+
/** What the index holds, for the model, e.g. "the product documentation". */
|
|
224
|
+
about?: string;
|
|
225
|
+
/** Matches returned. Default 5. */
|
|
226
|
+
topK?: number;
|
|
227
|
+
/** Applied to every search (e.g. the signed-in user's tenant). */
|
|
228
|
+
filter?: VectorMetadata;
|
|
229
|
+
mode?: VectorQueryMode;
|
|
230
|
+
/** Most characters of each match's text. Default 2000. */
|
|
231
|
+
maxChars?: number;
|
|
232
|
+
}
|
|
233
|
+
/** A retrieval tool over one index, ready for an LLM tool-use loop (RAG). */
|
|
234
|
+
export declare function retrievalTool(index: VectorIndexClient, options?: RetrievalToolOptions): RetrievalTool;
|