@findlane/server 0.10.0 → 0.11.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/README.md +2 -0
- package/dist/index.cjs +46 -1
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +46 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,8 @@ const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent:
|
|
|
57
57
|
| `getSettings()`, `replaceSettings(settings)`, `waitForSettings()` | Read and change searchable fields, facets, filters, sorts, custom ranking, synonyms, languages, typo tolerance, and text normalization. |
|
|
58
58
|
| `getIndexingStatus()`, `waitUntilIndexed(revision)` | Writes return an accepted revision and are searchable when accepted, so `waitUntilIndexed` returns at once. |
|
|
59
59
|
| `getPublicSearchAccess()`, `setPublicSearchPolicy(policy)`, `usePublicIndexDefaults()` | Restrict what browser search may read and query. |
|
|
60
|
+
| `events.send(events)`, `events.importOrders(orders)`, `events.cancelOrder(orderID)` | Orders and other events for the index's Events page and Bought together; the key needs the `events` or `write` scope. |
|
|
61
|
+
| `events.proxy(request)`, `events.relay(body, visitor)` | A route on your own site for the browser SDK's events (its `eventsUrl`), which ad blockers let through. See [Events](https://findlane.dev/docs/events#through-your-own-domain). |
|
|
60
62
|
|
|
61
63
|
### Settings
|
|
62
64
|
|
package/dist/index.cjs
CHANGED
|
@@ -69,6 +69,18 @@ class StagedReplacementError extends Error {
|
|
|
69
69
|
}
|
|
70
70
|
var eventsPerRequest = 200;
|
|
71
71
|
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
72
|
+
var relayedBytes = 256 * 1024;
|
|
73
|
+
function requestVisitor(request) {
|
|
74
|
+
const headers = request.headers;
|
|
75
|
+
const cf = request.cf;
|
|
76
|
+
return {
|
|
77
|
+
ip: headers.get("cf-connecting-ip") ?? headers.get("x-real-ip") ?? headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
|
|
78
|
+
userAgent: headers.get("user-agent") ?? undefined,
|
|
79
|
+
origin: headers.get("origin") ?? new URL(request.url).origin,
|
|
80
|
+
country: (typeof cf?.country == "string" ? cf.country : null) ?? headers.get("cf-ipcountry") ?? headers.get("x-vercel-ip-country") ?? undefined
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var jsonResponse = (body, status, headers = {}) => new Response(body, { status, headers: { "content-type": "application/json", "cache-control": "no-store", ...headers } });
|
|
72
84
|
var visitorHeaders = (visitor) => visitor?.ip ? {
|
|
73
85
|
"findlane-visitor-ip": visitor.ip.trim(),
|
|
74
86
|
...visitor.userAgent ? { "findlane-visitor-agent": visitor.userAgent.replace(/[^\x20-\x7e]/g, "").slice(0, 512) } : {}
|
|
@@ -164,6 +176,24 @@ class SearchServerClient {
|
|
|
164
176
|
await pause(wait, signal);
|
|
165
177
|
}
|
|
166
178
|
};
|
|
179
|
+
const relayEvents = async (body, visitor, signal) => {
|
|
180
|
+
if (!visitor.ip?.trim())
|
|
181
|
+
throw new Error("Relaying events needs the visitor's IP address");
|
|
182
|
+
const response = await this.fetcher(this.baseUrl + path + "/events", {
|
|
183
|
+
method: "POST",
|
|
184
|
+
signal,
|
|
185
|
+
body,
|
|
186
|
+
headers: {
|
|
187
|
+
authorization: `Bearer ${this.key}`,
|
|
188
|
+
"content-type": "application/json",
|
|
189
|
+
...visitorHeaders(visitor),
|
|
190
|
+
...visitor.origin ? { "findlane-visitor-origin": visitor.origin } : {},
|
|
191
|
+
...visitor.country ? { "findlane-visitor-country": visitor.country } : {}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const retryAfter = response.headers.get("retry-after");
|
|
195
|
+
return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
|
|
196
|
+
};
|
|
167
197
|
const sendEvents = async (events, signal) => {
|
|
168
198
|
const result = { accepted: 0, rejected: [] };
|
|
169
199
|
let batch = [];
|
|
@@ -219,7 +249,22 @@ class SearchServerClient {
|
|
|
219
249
|
...timestamp === undefined ? {} : { timestamp: typeof timestamp === "number" ? timestamp : new Date(timestamp).getTime() }
|
|
220
250
|
};
|
|
221
251
|
}(), signal),
|
|
222
|
-
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], signal)
|
|
252
|
+
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], signal),
|
|
253
|
+
proxy: async (request2, visitor = {}) => {
|
|
254
|
+
if (request2.method !== "POST")
|
|
255
|
+
return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
|
|
256
|
+
const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
|
|
257
|
+
if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
|
|
258
|
+
return tooLarge();
|
|
259
|
+
const body = await request2.text();
|
|
260
|
+
if (new TextEncoder().encode(body).length > relayedBytes)
|
|
261
|
+
return tooLarge();
|
|
262
|
+
const found = { ...requestVisitor(request2), ...visitor };
|
|
263
|
+
if (!found.ip)
|
|
264
|
+
throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
|
|
265
|
+
return relayEvents(body, { ...found, ip: found.ip });
|
|
266
|
+
},
|
|
267
|
+
relay: (body, visitor, signal) => relayEvents(body, visitor, signal)
|
|
223
268
|
},
|
|
224
269
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
225
270
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
package/dist/index.d.cts
CHANGED
|
@@ -263,6 +263,13 @@ export type RequestOptions = {
|
|
|
263
263
|
signal?: AbortSignal;
|
|
264
264
|
visitor?: Visitor;
|
|
265
265
|
};
|
|
266
|
+
/** A visitor whose events a server relays from its own domain (`events.relay`). */
|
|
267
|
+
export type RelayedVisitor = Visitor & {
|
|
268
|
+
/** The page's origin, such as `https://shop.example`: the site the Events page shows. */
|
|
269
|
+
origin?: string;
|
|
270
|
+
/** The visitor's country as two letters, such as `IS`, when the host knows it. */
|
|
271
|
+
country?: string;
|
|
272
|
+
};
|
|
266
273
|
/** A product in a cart or an order. */
|
|
267
274
|
export type EventItem = {
|
|
268
275
|
objectID: string;
|
|
@@ -357,6 +364,15 @@ export declare class SearchServerClient {
|
|
|
357
364
|
importOrders: (orders: Iterable<Order> | AsyncIterable<Order>, signal?: AbortSignal) => Promise<EventsResult>;
|
|
358
365
|
/** Takes a cancelled or refunded order out of Bought together. */
|
|
359
366
|
cancelOrder: (orderID: string, signal?: AbortSignal) => Promise<EventsResult>;
|
|
367
|
+
/** A route on your own domain for the browser SDK's events (its `eventsUrl` option): ad blockers
|
|
368
|
+
* block beacons to other sites, not to the page's own. Give it the browser's POST request (Next.js
|
|
369
|
+
* route handlers, Workers, Hono, Bun, Deno); it finds the visitor's address in the headers
|
|
370
|
+
* Cloudflare, Vercel and most proxies set. Where a visitor reaches your server directly, they can
|
|
371
|
+
* set those too, so pass `{ ip }`. The API key needs the `events` or `write` scope. */
|
|
372
|
+
proxy: (request: Request, visitor?: Partial<RelayedVisitor>) => Promise<Response>;
|
|
373
|
+
/** `proxy` for servers without Fetch API requests, such as Express: the request's body as text,
|
|
374
|
+
* and its visitor. Answers with the API's response. */
|
|
375
|
+
relay: (body: string, visitor: RelayedVisitor, signal?: AbortSignal) => Promise<Response>;
|
|
360
376
|
};
|
|
361
377
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
362
378
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
package/dist/index.d.ts
CHANGED
|
@@ -263,6 +263,13 @@ export type RequestOptions = {
|
|
|
263
263
|
signal?: AbortSignal;
|
|
264
264
|
visitor?: Visitor;
|
|
265
265
|
};
|
|
266
|
+
/** A visitor whose events a server relays from its own domain (`events.relay`). */
|
|
267
|
+
export type RelayedVisitor = Visitor & {
|
|
268
|
+
/** The page's origin, such as `https://shop.example`: the site the Events page shows. */
|
|
269
|
+
origin?: string;
|
|
270
|
+
/** The visitor's country as two letters, such as `IS`, when the host knows it. */
|
|
271
|
+
country?: string;
|
|
272
|
+
};
|
|
266
273
|
/** A product in a cart or an order. */
|
|
267
274
|
export type EventItem = {
|
|
268
275
|
objectID: string;
|
|
@@ -357,6 +364,15 @@ export declare class SearchServerClient {
|
|
|
357
364
|
importOrders: (orders: Iterable<Order> | AsyncIterable<Order>, signal?: AbortSignal) => Promise<EventsResult>;
|
|
358
365
|
/** Takes a cancelled or refunded order out of Bought together. */
|
|
359
366
|
cancelOrder: (orderID: string, signal?: AbortSignal) => Promise<EventsResult>;
|
|
367
|
+
/** A route on your own domain for the browser SDK's events (its `eventsUrl` option): ad blockers
|
|
368
|
+
* block beacons to other sites, not to the page's own. Give it the browser's POST request (Next.js
|
|
369
|
+
* route handlers, Workers, Hono, Bun, Deno); it finds the visitor's address in the headers
|
|
370
|
+
* Cloudflare, Vercel and most proxies set. Where a visitor reaches your server directly, they can
|
|
371
|
+
* set those too, so pass `{ ip }`. The API key needs the `events` or `write` scope. */
|
|
372
|
+
proxy: (request: Request, visitor?: Partial<RelayedVisitor>) => Promise<Response>;
|
|
373
|
+
/** `proxy` for servers without Fetch API requests, such as Express: the request's body as text,
|
|
374
|
+
* and its visitor. Answers with the API's response. */
|
|
375
|
+
relay: (body: string, visitor: RelayedVisitor, signal?: AbortSignal) => Promise<Response>;
|
|
360
376
|
};
|
|
361
377
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
362
378
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,18 @@ class StagedReplacementError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
var eventsPerRequest = 200;
|
|
25
25
|
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
26
|
+
var relayedBytes = 256 * 1024;
|
|
27
|
+
function requestVisitor(request) {
|
|
28
|
+
const headers = request.headers;
|
|
29
|
+
const cf = request.cf;
|
|
30
|
+
return {
|
|
31
|
+
ip: headers.get("cf-connecting-ip") ?? headers.get("x-real-ip") ?? headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
|
|
32
|
+
userAgent: headers.get("user-agent") ?? undefined,
|
|
33
|
+
origin: headers.get("origin") ?? new URL(request.url).origin,
|
|
34
|
+
country: (typeof cf?.country == "string" ? cf.country : null) ?? headers.get("cf-ipcountry") ?? headers.get("x-vercel-ip-country") ?? undefined
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var jsonResponse = (body, status, headers = {}) => new Response(body, { status, headers: { "content-type": "application/json", "cache-control": "no-store", ...headers } });
|
|
26
38
|
var visitorHeaders = (visitor) => visitor?.ip ? {
|
|
27
39
|
"findlane-visitor-ip": visitor.ip.trim(),
|
|
28
40
|
...visitor.userAgent ? { "findlane-visitor-agent": visitor.userAgent.replace(/[^\x20-\x7e]/g, "").slice(0, 512) } : {}
|
|
@@ -118,6 +130,24 @@ class SearchServerClient {
|
|
|
118
130
|
await pause(wait, signal);
|
|
119
131
|
}
|
|
120
132
|
};
|
|
133
|
+
const relayEvents = async (body, visitor, signal) => {
|
|
134
|
+
if (!visitor.ip?.trim())
|
|
135
|
+
throw new Error("Relaying events needs the visitor's IP address");
|
|
136
|
+
const response = await this.fetcher(this.baseUrl + path + "/events", {
|
|
137
|
+
method: "POST",
|
|
138
|
+
signal,
|
|
139
|
+
body,
|
|
140
|
+
headers: {
|
|
141
|
+
authorization: `Bearer ${this.key}`,
|
|
142
|
+
"content-type": "application/json",
|
|
143
|
+
...visitorHeaders(visitor),
|
|
144
|
+
...visitor.origin ? { "findlane-visitor-origin": visitor.origin } : {},
|
|
145
|
+
...visitor.country ? { "findlane-visitor-country": visitor.country } : {}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const retryAfter = response.headers.get("retry-after");
|
|
149
|
+
return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
|
|
150
|
+
};
|
|
121
151
|
const sendEvents = async (events, signal) => {
|
|
122
152
|
const result = { accepted: 0, rejected: [] };
|
|
123
153
|
let batch = [];
|
|
@@ -173,7 +203,22 @@ class SearchServerClient {
|
|
|
173
203
|
...timestamp === undefined ? {} : { timestamp: typeof timestamp === "number" ? timestamp : new Date(timestamp).getTime() }
|
|
174
204
|
};
|
|
175
205
|
}(), signal),
|
|
176
|
-
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], signal)
|
|
206
|
+
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], signal),
|
|
207
|
+
proxy: async (request2, visitor = {}) => {
|
|
208
|
+
if (request2.method !== "POST")
|
|
209
|
+
return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
|
|
210
|
+
const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
|
|
211
|
+
if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
|
|
212
|
+
return tooLarge();
|
|
213
|
+
const body = await request2.text();
|
|
214
|
+
if (new TextEncoder().encode(body).length > relayedBytes)
|
|
215
|
+
return tooLarge();
|
|
216
|
+
const found = { ...requestVisitor(request2), ...visitor };
|
|
217
|
+
if (!found.ip)
|
|
218
|
+
throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
|
|
219
|
+
return relayEvents(body, { ...found, ip: found.ip });
|
|
220
|
+
},
|
|
221
|
+
relay: (body, visitor, signal) => relayEvents(body, visitor, signal)
|
|
177
222
|
},
|
|
178
223
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
179
224
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.",
|
|
5
5
|
"keywords": ["findlane", "search", "full-text search", "typo tolerance", "ecommerce search", "search api", "sdk"],
|
|
6
6
|
"license": "MIT",
|