@aivisibilityskill/sdk 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +116 -2
- package/dist/index.d.ts +1308 -0
- package/dist/index.js +327 -0
- package/dist/schema.d.ts +7320 -0
- package/dist/schema.js +1 -0
- package/dist/webhooks.d.ts +24 -0
- package/dist/webhooks.js +91 -0
- package/package.json +49 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { verifyWebhook, WebhookVerificationError } from "./webhooks.js";
|
|
2
|
+
export { verifyWebhook, WebhookVerificationError };
|
|
3
|
+
export class AivError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
detail;
|
|
7
|
+
requestId;
|
|
8
|
+
body;
|
|
9
|
+
constructor(status, code, detail, requestId,
|
|
10
|
+
/** Extra fields from the problem response (credits, balance, retry hints…). */
|
|
11
|
+
body) {
|
|
12
|
+
super(`${status} ${code}${detail ? `: ${detail}` : ""}`);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.detail = detail;
|
|
16
|
+
this.requestId = requestId;
|
|
17
|
+
this.body = body;
|
|
18
|
+
this.name = "AivError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const RETRYABLE = new Set([429, 502, 503, 504]);
|
|
22
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
23
|
+
const enc = encodeURIComponent;
|
|
24
|
+
export const VERSION = "1.0.0";
|
|
25
|
+
/** A cursor-paged list: `for await (const job of aiv.jobs.iterate())`. */
|
|
26
|
+
async function* pages(fetchPage, items) {
|
|
27
|
+
let before;
|
|
28
|
+
for (;;) {
|
|
29
|
+
const page = await fetchPage(before);
|
|
30
|
+
for (const item of items(page))
|
|
31
|
+
yield item;
|
|
32
|
+
if (!page.next)
|
|
33
|
+
return;
|
|
34
|
+
before = page.next;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class Aiv {
|
|
38
|
+
#key;
|
|
39
|
+
#base;
|
|
40
|
+
#fetch;
|
|
41
|
+
#retries;
|
|
42
|
+
#timeout;
|
|
43
|
+
constructor(opts) {
|
|
44
|
+
if (!opts || typeof opts.apiKey !== "string" || !/^aiv_(live|test)_/.test(opts.apiKey))
|
|
45
|
+
throw new Error("apiKey must start with aiv_live_ or aiv_test_");
|
|
46
|
+
this.#key = opts.apiKey;
|
|
47
|
+
this.#base = (opts.baseUrl ?? "https://api.aivisibilityskill.com").replace(/\/+$/, "");
|
|
48
|
+
this.#fetch = opts.fetch ?? ((req) => fetch(req));
|
|
49
|
+
this.#retries = Math.max(0, opts.maxRetries ?? 2);
|
|
50
|
+
this.#timeout = opts.timeoutMs ?? 120_000;
|
|
51
|
+
}
|
|
52
|
+
/** Any endpoint, for ones this version doesn't wrap yet. Retries GETs and keyed writes. */
|
|
53
|
+
async request(method, path, opts = {}) {
|
|
54
|
+
const url = new URL(this.#base + path);
|
|
55
|
+
for (const [k, v] of Object.entries(opts.query ?? {})) {
|
|
56
|
+
if (v === undefined || v === null)
|
|
57
|
+
continue;
|
|
58
|
+
url.searchParams.set(k, Array.isArray(v) ? v.join(",") : String(v));
|
|
59
|
+
}
|
|
60
|
+
// GETs are safe to retry; writes only when an idempotency key protects them.
|
|
61
|
+
const retryable = method === "GET" || opts.idempotencyKey !== undefined;
|
|
62
|
+
for (let attempt = 0;; attempt++) {
|
|
63
|
+
const headers = {
|
|
64
|
+
Authorization: `Bearer ${this.#key}`,
|
|
65
|
+
Accept: opts.accept === "text" ? "text/csv" : "application/json",
|
|
66
|
+
};
|
|
67
|
+
if (opts.body !== undefined)
|
|
68
|
+
headers["Content-Type"] = "application/json";
|
|
69
|
+
if (opts.idempotencyKey)
|
|
70
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
71
|
+
let res;
|
|
72
|
+
try {
|
|
73
|
+
res = await this.#fetch(new Request(url, {
|
|
74
|
+
method,
|
|
75
|
+
headers,
|
|
76
|
+
body: opts.body === undefined ? null : JSON.stringify(opts.body),
|
|
77
|
+
signal: AbortSignal.timeout(this.#timeout),
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (retryable && attempt < this.#retries) {
|
|
82
|
+
await sleep(500 * 2 ** attempt);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
if (res.ok) {
|
|
88
|
+
if (res.status === 204)
|
|
89
|
+
return undefined;
|
|
90
|
+
return (opts.accept === "text" ? await res.text() : await res.json());
|
|
91
|
+
}
|
|
92
|
+
if (retryable && RETRYABLE.has(res.status) && attempt < this.#retries) {
|
|
93
|
+
await res.body?.cancel();
|
|
94
|
+
const after = Number(res.headers.get("Retry-After"));
|
|
95
|
+
await sleep(Number.isFinite(after) && after > 0 ? Math.min(after, 60) * 1000 : 1000 * 2 ** attempt);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const parsed = await res.json().catch(() => null);
|
|
99
|
+
const body = parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
100
|
+
? parsed
|
|
101
|
+
: {};
|
|
102
|
+
throw new AivError(res.status, typeof body.code === "string" ? body.code : "http_error", typeof body.detail === "string" ? body.detail : undefined, typeof body.request_id === "string"
|
|
103
|
+
? body.request_id
|
|
104
|
+
: (res.headers.get("X-Request-Id") ?? undefined), body);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Public price list: credits per engine and mode, plans. */
|
|
108
|
+
pricing() {
|
|
109
|
+
return this.request("GET", "/v1/pricing");
|
|
110
|
+
}
|
|
111
|
+
/** What a check would cost, and whether this key may run it. Free. */
|
|
112
|
+
estimate(body) {
|
|
113
|
+
return this.request("POST", "/v1/estimate", { body });
|
|
114
|
+
}
|
|
115
|
+
account = {
|
|
116
|
+
get: () => this.request("GET", "/v1/account"),
|
|
117
|
+
update: (body) => this.request("PATCH", "/v1/account", { body }),
|
|
118
|
+
};
|
|
119
|
+
locales = {
|
|
120
|
+
list: () => this.request("GET", "/v1/locales"),
|
|
121
|
+
get: (locale) => this.request("GET", `/v1/locales/${enc(locale)}`),
|
|
122
|
+
};
|
|
123
|
+
checks = {
|
|
124
|
+
/** Starts a check. A retry with the same idempotencyKey never charges twice. */
|
|
125
|
+
create: (body, opts = {}) => this.request("POST", "/v1/checks", {
|
|
126
|
+
body,
|
|
127
|
+
idempotencyKey: opts.idempotencyKey ?? crypto.randomUUID(),
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
130
|
+
batches = {
|
|
131
|
+
/**
|
|
132
|
+
* Thousands of answers in one request; they run as checks of up to 200 as your plan's
|
|
133
|
+
* concurrency allows. Retrying with the same idempotencyKey never starts a second batch.
|
|
134
|
+
*/
|
|
135
|
+
create: (body, opts = {}) => this.request("POST", "/v1/batches", {
|
|
136
|
+
body,
|
|
137
|
+
idempotencyKey: opts.idempotencyKey ?? crypto.randomUUID(),
|
|
138
|
+
}),
|
|
139
|
+
get: (id) => this.request("GET", `/v1/batches/${enc(id)}`),
|
|
140
|
+
list: (q = {}) => this.request("GET", "/v1/batches", { query: q }),
|
|
141
|
+
iterate: (q = {}) => pages((before) => this.batches.list({ ...q, ...(before ? { before } : {}) }), (p) => p.batches),
|
|
142
|
+
/** Stops starting new checks; running ones finish and nothing more is charged. */
|
|
143
|
+
cancel: (id) => this.request("POST", `/v1/batches/${enc(id)}/cancel`),
|
|
144
|
+
/** Polls until the batch completes or stops (default timeout: 2 hours). */
|
|
145
|
+
wait: async (id, opts = {}) => {
|
|
146
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 2 * 3600_000);
|
|
147
|
+
for (;;) {
|
|
148
|
+
const b = await this.batches.get(id);
|
|
149
|
+
if (b.poll_after_seconds === null || Date.now() >= deadline)
|
|
150
|
+
return b;
|
|
151
|
+
await sleep(Math.min(b.poll_after_seconds * 1000, Math.max(0, deadline - Date.now())));
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
jobs = {
|
|
156
|
+
get: (id, opts = {}) => this.request("GET", `/v1/jobs/${enc(id)}`, {
|
|
157
|
+
query: { text: opts.text === false ? "false" : undefined },
|
|
158
|
+
}),
|
|
159
|
+
list: (q = {}) => this.request("GET", "/v1/jobs", {
|
|
160
|
+
query: { limit: q.limit, before: q.before, project_id: q.projectId, customer: q.customer },
|
|
161
|
+
}),
|
|
162
|
+
/** Every job, newest first, across pages. */
|
|
163
|
+
iterate: (q = {}) => pages((before) => this.jobs.list({ ...q, ...(before ? { before } : {}) }), (p) => p.jobs),
|
|
164
|
+
/** Polls (at the pace the API suggests) until the job finishes or the timeout passes. */
|
|
165
|
+
wait: async (id, opts = {}) => {
|
|
166
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60_000);
|
|
167
|
+
for (;;) {
|
|
168
|
+
const job = await this.jobs.get(id);
|
|
169
|
+
if (job.finished_at || job.poll_after_seconds === null)
|
|
170
|
+
return job;
|
|
171
|
+
if (Date.now() >= deadline)
|
|
172
|
+
return job;
|
|
173
|
+
await sleep(Math.min(job.poll_after_seconds * 1000, Math.max(0, deadline - Date.now())));
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
answers = {
|
|
178
|
+
get: (id) => this.request("GET", `/v1/answers/${enc(id)}`),
|
|
179
|
+
/** The engine's raw response, as stored when the answer was collected. */
|
|
180
|
+
receipt: (id) => this.request("GET", `/v1/answers/${enc(id)}/receipt`),
|
|
181
|
+
};
|
|
182
|
+
projects = {
|
|
183
|
+
create: (body, opts = {}) => this.request("POST", "/v1/projects", {
|
|
184
|
+
body,
|
|
185
|
+
idempotencyKey: opts.idempotencyKey ?? crypto.randomUUID(),
|
|
186
|
+
}),
|
|
187
|
+
list: (q = {}) => this.request("GET", "/v1/projects", { query: q }),
|
|
188
|
+
get: (id) => this.request("GET", `/v1/projects/${enc(id)}`),
|
|
189
|
+
update: (id, body) => this.request("PATCH", `/v1/projects/${enc(id)}`, { body }),
|
|
190
|
+
archive: (id) => this.request("DELETE", `/v1/projects/${enc(id)}`),
|
|
191
|
+
visibility: (id, q = {}) => this.request("GET", `/v1/projects/${enc(id)}/visibility`, {
|
|
192
|
+
query: analyticsQuery(q),
|
|
193
|
+
}),
|
|
194
|
+
competitors: (id, q = {}) => this.request("GET", `/v1/projects/${enc(id)}/competitors`, {
|
|
195
|
+
query: analyticsQuery(q),
|
|
196
|
+
}),
|
|
197
|
+
sources: (id, q = {}) => this.request("GET", `/v1/projects/${enc(id)}/sources`, {
|
|
198
|
+
query: analyticsQuery(q),
|
|
199
|
+
}),
|
|
200
|
+
facts: (id, q = {}) => this.request("GET", `/v1/projects/${enc(id)}/facts`, {
|
|
201
|
+
query: analyticsQuery(q),
|
|
202
|
+
}),
|
|
203
|
+
history: (id, q = {}) => this.request("GET", `/v1/projects/${enc(id)}/history`, {
|
|
204
|
+
query: { ...analyticsQuery(q), bucket: q.bucket },
|
|
205
|
+
}),
|
|
206
|
+
/** Paid action: robots.txt, llms.txt, sitemaps and key pages, as AI crawlers see them. */
|
|
207
|
+
siteCheck: (id, body = {}) => this.request("POST", `/v1/projects/${enc(id)}/site-check`, { body }),
|
|
208
|
+
/** Paid action: what ranks on Google for a question and what those pages say. */
|
|
209
|
+
research: (id, body) => this.request("POST", `/v1/projects/${enc(id)}/research`, { body }),
|
|
210
|
+
/** Paid action: ranks what to fix first from the project's answers and checks. */
|
|
211
|
+
findOpportunities: (id, body = {}) => this.request("POST", `/v1/projects/${enc(id)}/opportunities`, { body }),
|
|
212
|
+
opportunities: (id, q = {}) => this.request("GET", `/v1/projects/${enc(id)}/opportunities`, { query: q }),
|
|
213
|
+
};
|
|
214
|
+
opportunities = {
|
|
215
|
+
get: (id) => this.request("GET", `/v1/opportunities/${enc(id)}`),
|
|
216
|
+
update: (id, body) => this.request("PATCH", `/v1/opportunities/${enc(id)}`, { body }),
|
|
217
|
+
/** Paid action: a step-by-step fix brief for one opportunity. */
|
|
218
|
+
brief: (id, body = {}) => this.request("POST", `/v1/opportunities/${enc(id)}/brief`, { body }),
|
|
219
|
+
/** Records a fix you shipped; it's verified on the site and later in AI answers. */
|
|
220
|
+
recordFix: (id, body) => this.request("POST", `/v1/opportunities/${enc(id)}/fixes`, { body }),
|
|
221
|
+
};
|
|
222
|
+
fixes = {
|
|
223
|
+
get: (id) => this.request("GET", `/v1/fixes/${enc(id)}`),
|
|
224
|
+
verify: (id, body = {}) => this.request("POST", `/v1/fixes/${enc(id)}/verify`, {
|
|
225
|
+
body,
|
|
226
|
+
}),
|
|
227
|
+
};
|
|
228
|
+
actions = {
|
|
229
|
+
get: (id) => this.request("GET", `/v1/actions/${enc(id)}`),
|
|
230
|
+
/** Polls a paid action (site check, research, brief…) until it's done. */
|
|
231
|
+
wait: async (id, opts = {}) => {
|
|
232
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 10 * 60_000);
|
|
233
|
+
for (;;) {
|
|
234
|
+
const action = await this.actions.get(id);
|
|
235
|
+
if (action.poll_after_seconds === null || Date.now() >= deadline)
|
|
236
|
+
return action;
|
|
237
|
+
await sleep(Math.min(action.poll_after_seconds * 1000, Math.max(0, deadline - Date.now())));
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
keys = {
|
|
242
|
+
list: (q = {}) => this.request("GET", "/v1/keys", { query: q }),
|
|
243
|
+
iterate: (q = {}) => pages((before) => this.keys.list({ ...q, ...(before ? { before } : {}) }), (p) => p.keys),
|
|
244
|
+
/** The response's `secret` is shown once. */
|
|
245
|
+
create: (body) => this.request("POST", "/v1/keys", { body }),
|
|
246
|
+
update: (id, body) => this.request("PATCH", `/v1/keys/${enc(id)}`, { body }),
|
|
247
|
+
revoke: (id) => this.request("DELETE", `/v1/keys/${enc(id)}`),
|
|
248
|
+
};
|
|
249
|
+
usage = {
|
|
250
|
+
get: (q = {}) => this.request("GET", "/v1/usage", { query: usageQuery(q) }),
|
|
251
|
+
/** The same report as CSV text, ready for a spreadsheet or an invoice. */
|
|
252
|
+
csv: (q = {}) => this.request("GET", "/v1/usage", {
|
|
253
|
+
query: { ...usageQuery(q), format: "csv" },
|
|
254
|
+
accept: "text",
|
|
255
|
+
}),
|
|
256
|
+
};
|
|
257
|
+
webhooks = {
|
|
258
|
+
list: () => this.request("GET", "/v1/webhooks"),
|
|
259
|
+
/** The response's `secret` (whsec_…) is shown once: store it to verify deliveries. */
|
|
260
|
+
create: (body) => this.request("POST", "/v1/webhooks", { body }),
|
|
261
|
+
get: (id) => this.request("GET", `/v1/webhooks/${enc(id)}`),
|
|
262
|
+
update: (id, body) => this.request("PATCH", `/v1/webhooks/${enc(id)}`, { body }),
|
|
263
|
+
delete: (id) => this.request("DELETE", `/v1/webhooks/${enc(id)}`),
|
|
264
|
+
/** A new secret; the old one keeps working for 24 hours. */
|
|
265
|
+
rotateSecret: (id, body = {}) => this.request("POST", `/v1/webhooks/${enc(id)}/rotate-secret`, { body }),
|
|
266
|
+
/** Sends a `ping` event to this endpoint only. */
|
|
267
|
+
test: (id, body = {}) => this.request("POST", `/v1/webhooks/${enc(id)}/test`, {
|
|
268
|
+
body,
|
|
269
|
+
}),
|
|
270
|
+
/** Re-sends everything the endpoint missed since a time (ISO date), and re-enables it. */
|
|
271
|
+
recover: (id, body) => this.request("POST", `/v1/webhooks/${enc(id)}/recover`, { body }),
|
|
272
|
+
deliveries: {
|
|
273
|
+
list: (id, q = {}) => this.request("GET", `/v1/webhooks/${enc(id)}/deliveries`, { query: q }),
|
|
274
|
+
get: (id, deliveryId) => this.request("GET", `/v1/webhooks/${enc(id)}/deliveries/${enc(deliveryId)}`),
|
|
275
|
+
resend: (id, deliveryId) => this.request("POST", `/v1/webhooks/${enc(id)}/deliveries/${enc(deliveryId)}/resend`),
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
events = {
|
|
279
|
+
list: (q = {}) => this.request("GET", "/v1/events", {
|
|
280
|
+
query: q,
|
|
281
|
+
}),
|
|
282
|
+
get: (id) => this.request("GET", `/v1/events/${enc(id)}`),
|
|
283
|
+
/** Every event, newest first (the last 30 days). */
|
|
284
|
+
iterate: (q = {}) => pages((before) => this.events.list({ ...q, ...(before ? { before } : {}) }), (p) => p.events),
|
|
285
|
+
};
|
|
286
|
+
auditLog = {
|
|
287
|
+
list: (q = {}) => this.request("GET", "/v1/audit-log", {
|
|
288
|
+
query: q,
|
|
289
|
+
}),
|
|
290
|
+
iterate: (q = {}) => pages((before) => this.auditLog.list({ ...q, ...(before !== undefined ? { before } : {}) }), (p) => p.entries),
|
|
291
|
+
};
|
|
292
|
+
customers = {
|
|
293
|
+
/**
|
|
294
|
+
* Deletes everything stored for one of your customers (irreversible). Runs in the
|
|
295
|
+
* background: poll `dataDeletions.get(id)` or `dataDeletions.wait(id)`.
|
|
296
|
+
*/
|
|
297
|
+
deleteData: (customer) => this.request("POST", `/v1/customers/${enc(customer)}/delete-data`, {
|
|
298
|
+
body: { confirm: true },
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
dataDeletions = {
|
|
302
|
+
get: (id) => this.request("GET", `/v1/data-deletions/${enc(id)}`),
|
|
303
|
+
wait: async (id, opts = {}) => {
|
|
304
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 30 * 60_000);
|
|
305
|
+
for (;;) {
|
|
306
|
+
const d = await this.dataDeletions.get(id);
|
|
307
|
+
if (d.status === "completed" || d.status === "failed" || Date.now() >= deadline)
|
|
308
|
+
return d;
|
|
309
|
+
await sleep(Math.min(opts.intervalMs ?? 30_000, Math.max(0, deadline - Date.now())));
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
/** Checks a webhook delivery's signature and returns the parsed event. Throws if it's not ours. */
|
|
314
|
+
static verifyWebhook = verifyWebhook;
|
|
315
|
+
}
|
|
316
|
+
function analyticsQuery(q) {
|
|
317
|
+
return { question_id: q.questionId, engines: q.engines, days: q.days };
|
|
318
|
+
}
|
|
319
|
+
function usageQuery(q) {
|
|
320
|
+
return {
|
|
321
|
+
from: q.from,
|
|
322
|
+
to: q.to,
|
|
323
|
+
group_by: q.groupBy,
|
|
324
|
+
customer: q.customer,
|
|
325
|
+
project_id: q.projectId,
|
|
326
|
+
};
|
|
327
|
+
}
|