@lunora/bindings 0.0.0 → 1.0.0-alpha.10

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.
Files changed (41) hide show
  1. package/LICENSE.md +111 -0
  2. package/README.md +39 -1
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/analytics/index.d.mts +148 -0
  5. package/dist/analytics/index.d.ts +148 -0
  6. package/dist/analytics/index.mjs +2 -0
  7. package/dist/images/index.d.mts +338 -0
  8. package/dist/images/index.d.ts +338 -0
  9. package/dist/images/index.mjs +3 -0
  10. package/dist/kv/index.d.mts +271 -0
  11. package/dist/kv/index.d.ts +271 -0
  12. package/dist/kv/index.mjs +2 -0
  13. package/dist/packem_shared/AnalyticsSqlError-C2nz3jpH.mjs +41 -0
  14. package/dist/packem_shared/R2SqlError-drPKSCZ3.mjs +65 -0
  15. package/dist/packem_shared/SelectBuilder-BOqJQHEv.mjs +168 -0
  16. package/dist/packem_shared/SetOperation-DmPgUL8W.mjs +81 -0
  17. package/dist/packem_shared/Sql-B3zq2YGx.mjs +74 -0
  18. package/dist/packem_shared/WindowExpression-BT_uA6g1.mjs +44 -0
  19. package/dist/packem_shared/WindowFunction-DrnuZUF6.mjs +82 -0
  20. package/dist/packem_shared/asc-DZbQCxh1.mjs +16 -0
  21. package/dist/packem_shared/buildImageDeliveryUrl-qZ7XbqTL.mjs +35 -0
  22. package/dist/packem_shared/buildSignedImageUrl-DNUFfyGP.mjs +130 -0
  23. package/dist/packem_shared/concurrent-CkCEVwqP.mjs +39 -0
  24. package/dist/packem_shared/createAnalytics-CEEI69o9.mjs +57 -0
  25. package/dist/packem_shared/createContextVectors-DwZtnPeC.mjs +140 -0
  26. package/dist/packem_shared/createImages-BzRnsz3H.mjs +85 -0
  27. package/dist/packem_shared/createKv-C8Iyu5hD.mjs +145 -0
  28. package/dist/packem_shared/createKvIntrospector-Byk4GfsY.mjs +77 -0
  29. package/dist/packem_shared/createPipelines-CfyJ6VGu.mjs +10 -0
  30. package/dist/packem_shared/createVectorAdminIntrospector-DuSvcBa5.mjs +53 -0
  31. package/dist/packem_shared/createVectors-CTSrctiK.mjs +95 -0
  32. package/dist/pipelines/index.d.mts +41 -0
  33. package/dist/pipelines/index.d.ts +41 -0
  34. package/dist/pipelines/index.mjs +1 -0
  35. package/dist/r2sql/index.d.mts +383 -0
  36. package/dist/r2sql/index.d.ts +383 -0
  37. package/dist/r2sql/index.mjs +7 -0
  38. package/dist/vectors/index.d.mts +285 -0
  39. package/dist/vectors/index.d.ts +285 -0
  40. package/dist/vectors/index.mjs +3 -0
  41. package/package.json +57 -4
@@ -0,0 +1,74 @@
1
+ const quoteString = (value) => `'${value.replaceAll("'", "''")}'`;
2
+ const IDENTIFIER_RE = /^\w+(?:\.\w+)*$/;
3
+ const TABLE_REF_RE = /^\w+(?:\.\w+)*(?:\s+(?:as\s+)?\w+)?$/i;
4
+ const MAX_LIMIT = 1e4;
5
+ class Sql {
6
+ text;
7
+ constructor(text) {
8
+ this.text = text;
9
+ }
10
+ toString() {
11
+ return this.text;
12
+ }
13
+ }
14
+ const isSql = (value) => value instanceof Sql;
15
+ const raw = (text) => new Sql(text);
16
+ const toText = (value) => isSql(value) ? value.text : value;
17
+ const ident = (name) => {
18
+ if (typeof name !== "string" || !IDENTIFIER_RE.test(name)) {
19
+ throw new TypeError(`r2sql: invalid identifier ${JSON.stringify(name)} — expected dotted [A-Za-z0-9_] segments (e.g. "namespace.table").`);
20
+ }
21
+ return name;
22
+ };
23
+ const tableRef = (ref) => {
24
+ if (typeof ref !== "string" || !TABLE_REF_RE.test(ref)) {
25
+ throw new TypeError(`r2sql: invalid table reference ${JSON.stringify(ref)} — expected "namespace.table" with an optional "[AS] alias".`);
26
+ }
27
+ return ref;
28
+ };
29
+ const lit = (value) => {
30
+ if (value === null || value === void 0) {
31
+ return "NULL";
32
+ }
33
+ if (typeof value === "boolean") {
34
+ return value ? "true" : "false";
35
+ }
36
+ if (typeof value === "bigint") {
37
+ return value.toString();
38
+ }
39
+ if (typeof value === "number") {
40
+ if (!Number.isFinite(value)) {
41
+ throw new TypeError(`r2sql: cannot inline a non-finite number (${String(value)}) as a SQL literal.`);
42
+ }
43
+ return String(value);
44
+ }
45
+ if (typeof value === "string") {
46
+ return quoteString(value);
47
+ }
48
+ if (value instanceof Date) {
49
+ return quoteString(value.toISOString());
50
+ }
51
+ if (Array.isArray(value)) {
52
+ if (value.length === 0) {
53
+ throw new TypeError("r2sql: cannot inline an empty array — `IN ()` is not valid SQL. Guard the empty case before building the query.");
54
+ }
55
+ return `(${value.map((element) => lit(element)).join(", ")})`;
56
+ }
57
+ throw new TypeError(`r2sql: cannot inline a value of type ${typeof value} as a SQL literal. Wrap trusted SQL with raw(), or pass a primitive/Date/array.`);
58
+ };
59
+ const sql = (strings, ...values) => {
60
+ let out = strings[0] ?? "";
61
+ for (const [index, value] of values.entries()) {
62
+ out += isSql(value) ? value.text : lit(value);
63
+ out += strings[index + 1] ?? "";
64
+ }
65
+ return new Sql(out);
66
+ };
67
+ const joinSql = (parts, separator) => new Sql(parts.map((part) => toText(part)).join(separator));
68
+ const assertLimit = (n) => {
69
+ if (!Number.isInteger(n) || n < 1 || n > MAX_LIMIT) {
70
+ throw new RangeError(`r2sql: limit must be an integer between 1 and ${String(MAX_LIMIT)} (got ${String(n)}).`);
71
+ }
72
+ };
73
+
74
+ export { Sql, assertLimit, ident, isSql, joinSql, lit, raw, sql, tableRef, toText };
@@ -0,0 +1,44 @@
1
+ import { Sql, lit } from './Sql-B3zq2YGx.mjs';
2
+
3
+ const IDENTIFIER = /^[A-Z_][\w$]*$/i;
4
+ const assertIdent = (name) => {
5
+ if (!IDENTIFIER.test(name)) {
6
+ throw new TypeError(`r2sql: invalid identifier ${JSON.stringify(name)} — expected a simple SQL name (letters, digits, underscore).`);
7
+ }
8
+ return name;
9
+ };
10
+ class WindowExpression extends Sql {
11
+ /** Alias the expression — `... AS alias` — for use in a `SELECT` list. */
12
+ as(alias) {
13
+ return new Sql(`${this.text} AS ${assertIdent(alias)}`);
14
+ }
15
+ /** `expr BETWEEN low AND high`. */
16
+ between(low, high) {
17
+ return new Sql(`${this.text} BETWEEN ${lit(low)} AND ${lit(high)}`);
18
+ }
19
+ /** `expr = value`. */
20
+ eq(value) {
21
+ return this.compare("=", value);
22
+ }
23
+ /** `expr > value`. */
24
+ gt(value) {
25
+ return this.compare(">", value);
26
+ }
27
+ /** `expr >= value`. */
28
+ gte(value) {
29
+ return this.compare(">=", value);
30
+ }
31
+ /** `expr &lt; value`. */
32
+ lt(value) {
33
+ return this.compare("<", value);
34
+ }
35
+ /** `expr &lt;= value`. */
36
+ lte(value) {
37
+ return this.compare("<=", value);
38
+ }
39
+ compare(operator, value) {
40
+ return new Sql(`${this.text} ${operator} ${lit(value)}`);
41
+ }
42
+ }
43
+
44
+ export { WindowExpression as default };
@@ -0,0 +1,82 @@
1
+ import { renderOrderTerm } from './asc-DZbQCxh1.mjs';
2
+ import { lit, toText } from './Sql-B3zq2YGx.mjs';
3
+ import WindowExpression from './WindowExpression-BT_uA6g1.mjs';
4
+
5
+ const toArray = (value) => {
6
+ if (value === void 0) {
7
+ return [];
8
+ }
9
+ return Array.isArray(value) ? value : [value];
10
+ };
11
+ const renderOver = (spec) => {
12
+ const clauses = [];
13
+ const partitions = toArray(spec.partitionBy).map((part) => toText(part));
14
+ if (partitions.length > 0) {
15
+ clauses.push(`PARTITION BY ${partitions.join(", ")}`);
16
+ }
17
+ const orders = toArray(spec.orderBy).map((order) => renderOrderTerm(order));
18
+ if (orders.length > 0) {
19
+ clauses.push(`ORDER BY ${orders.join(", ")}`);
20
+ }
21
+ if (spec.frame !== void 0 && spec.frame.length > 0) {
22
+ clauses.push(spec.frame);
23
+ }
24
+ return `OVER (${clauses.join(" ")})`;
25
+ };
26
+ const offsetArguments = (column, offset, fallback) => {
27
+ const parts = [toText(column)];
28
+ if (offset !== void 0) {
29
+ parts.push(lit(offset));
30
+ }
31
+ if (fallback !== void 0) {
32
+ parts.push(lit(fallback));
33
+ }
34
+ return parts.join(", ");
35
+ };
36
+ class WindowFunction {
37
+ callText;
38
+ constructor(callText) {
39
+ this.callText = callText;
40
+ }
41
+ /** Attach the window frame, yielding a {@link WindowExpression}. */
42
+ over(spec = {}) {
43
+ return new WindowExpression(`${this.callText} ${renderOver(spec)}`);
44
+ }
45
+ }
46
+ const windowFunction = (callText) => new WindowFunction(callText);
47
+ const fn = {
48
+ /** `AVG(column) OVER (...)`. */
49
+ avg: (column) => windowFunction(`AVG(${toText(column)})`),
50
+ /** `COUNT(column) OVER (...)` — omit the column for `COUNT(*)`. */
51
+ count: (column) => windowFunction(`COUNT(${column === void 0 ? "*" : toText(column)})`),
52
+ /** `CUME_DIST() OVER (...)`. */
53
+ cumeDist: () => windowFunction("CUME_DIST()"),
54
+ /** `DENSE_RANK() OVER (...)`. */
55
+ denseRank: () => windowFunction("DENSE_RANK()"),
56
+ /** `FIRST_VALUE(column) OVER (...)`. */
57
+ firstValue: (column) => windowFunction(`FIRST_VALUE(${toText(column)})`),
58
+ /** `LAG(column[, offset[, default]]) OVER (...)`. */
59
+ lag: (column, offset, fallback) => windowFunction(`LAG(${offsetArguments(column, offset, fallback)})`),
60
+ /** `LAST_VALUE(column) OVER (...)`. */
61
+ lastValue: (column) => windowFunction(`LAST_VALUE(${toText(column)})`),
62
+ /** `LEAD(column[, offset[, default]]) OVER (...)`. */
63
+ lead: (column, offset, fallback) => windowFunction(`LEAD(${offsetArguments(column, offset, fallback)})`),
64
+ /** `MAX(column) OVER (...)`. */
65
+ max: (column) => windowFunction(`MAX(${toText(column)})`),
66
+ /** `MIN(column) OVER (...)`. */
67
+ min: (column) => windowFunction(`MIN(${toText(column)})`),
68
+ /** `NTH_VALUE(column, n) OVER (...)`. */
69
+ nthValue: (column, n) => windowFunction(`NTH_VALUE(${toText(column)}, ${lit(n)})`),
70
+ /** `NTILE(buckets) OVER (...)`. */
71
+ ntile: (buckets) => windowFunction(`NTILE(${lit(buckets)})`),
72
+ /** `PERCENT_RANK() OVER (...)`. */
73
+ percentRank: () => windowFunction("PERCENT_RANK()"),
74
+ /** `RANK() OVER (...)`. */
75
+ rank: () => windowFunction("RANK()"),
76
+ /** `ROW_NUMBER() OVER (...)`. */
77
+ rowNumber: () => windowFunction("ROW_NUMBER()"),
78
+ /** `SUM(column) OVER (...)`. */
79
+ sum: (column) => windowFunction(`SUM(${toText(column)})`)
80
+ };
81
+
82
+ export { WindowFunction, fn };
@@ -0,0 +1,16 @@
1
+ import { Sql, toText } from './Sql-B3zq2YGx.mjs';
2
+
3
+ const asc = (expr) => {
4
+ return { dir: "ASC", expr };
5
+ };
6
+ const desc = (expr) => {
7
+ return { dir: "DESC", expr };
8
+ };
9
+ const renderOrderTerm = (term) => {
10
+ if (typeof term === "string" || term instanceof Sql) {
11
+ return toText(term);
12
+ }
13
+ return `${toText(term.expr)} ${term.dir}`;
14
+ };
15
+
16
+ export { asc, desc, renderOrderTerm };
@@ -0,0 +1,35 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
3
+ const ABSOLUTE_URL_RE = /^[a-z][a-z\d+\-.]*:\/\//i;
4
+ const FORBIDDEN_VALUE_CHARS = [",", "=", "#", "?", "/"];
5
+ const stripTrailingSlash = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
6
+ const stripLeadingSlash = (value) => value.startsWith("/") ? value.slice(1) : value;
7
+ const serializeTransform = (transform) => Object.entries(transform).filter(([, value]) => value !== void 0 && (typeof value === "string" || typeof value === "number")).map(([key, value]) => {
8
+ const serialized = String(value);
9
+ const offendingChar = FORBIDDEN_VALUE_CHARS.find((char) => serialized.includes(char));
10
+ if (offendingChar !== void 0) {
11
+ throw new LunoraError(
12
+ "INTERNAL",
13
+ `@lunora/bindings/images: transform option \`${key}\` value \`${serialized}\` contains a \`${offendingChar}\`, which the /cdn-cgi/image/ option path cannot represent (\`,\`/\`=\` are the option/key-value separators; \`#\`/\`?\`/\`/\` are URL-structural). Percent-encode the value — for colors use \`%23RRGGBB\`, not \`#RRGGBB\` or \`rgb(r,g,b)\`.`
14
+ );
15
+ }
16
+ return `${key}=${serialized}`;
17
+ }).join(",");
18
+ const encodeKey = (key) => key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
19
+ const buildImageDeliveryUrl = (options) => {
20
+ const base = stripTrailingSlash(options.baseUrl);
21
+ if (options.imageId !== void 0) {
22
+ const variant = options.variant ?? "public";
23
+ return `${base}/${encodeURIComponent(options.imageId)}/${encodeURIComponent(variant)}`;
24
+ }
25
+ if (options.key === void 0) {
26
+ throw new LunoraError("INTERNAL", "@lunora/bindings/images: buildImageDeliveryUrl requires either `imageId` or `key`");
27
+ }
28
+ const optionString = options.transform === void 0 ? "" : serializeTransform(options.transform);
29
+ const isAbsolute = ABSOLUTE_URL_RE.test(options.key);
30
+ const source = isAbsolute ? options.key : `/${encodeKey(stripLeadingSlash(options.key))}`;
31
+ const prefix = optionString === "" ? "/cdn-cgi/image" : `/cdn-cgi/image/${optionString}`;
32
+ return `${base}${prefix}${source.startsWith("/") ? source : `/${source}`}`;
33
+ };
34
+
35
+ export { buildImageDeliveryUrl };
@@ -0,0 +1,130 @@
1
+ const evictOldestEntry = (map, capacity) => {
2
+ if (map.size < capacity) {
3
+ return;
4
+ }
5
+ const oldest = map.keys().next().value;
6
+ if (oldest !== void 0) {
7
+ map.delete(oldest);
8
+ }
9
+ };
10
+
11
+ const textEncoder = new TextEncoder();
12
+ const MAX_SIGNED_URL_TTL_SECONDS = 7 * 24 * 60 * 60;
13
+ const SCHEME_PREFIX_RE = /^[a-z][a-z0-9+\-.]*:\/\//i;
14
+ const toBase64Url = (bytes) => {
15
+ const binary = String.fromCodePoint(...bytes);
16
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
17
+ };
18
+ const fromBase64Url = (input) => {
19
+ const padded = input.replaceAll("-", "+").replaceAll("_", "/") + "===".slice((input.length + 3) % 4);
20
+ const binary = atob(padded);
21
+ const bytes = new Uint8Array(binary.length);
22
+ for (let index = 0; index < binary.length; index += 1) {
23
+ bytes[index] = binary.codePointAt(index) ?? 0;
24
+ }
25
+ return bytes;
26
+ };
27
+ const KEY_CACHE_MAX = 64;
28
+ const keyCache = /* @__PURE__ */ new Map();
29
+ const importHmacKey = async (secret) => {
30
+ const cached = keyCache.get(secret);
31
+ if (cached) {
32
+ return cached;
33
+ }
34
+ evictOldestEntry(keyCache, KEY_CACHE_MAX);
35
+ const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
36
+ keyCache.set(secret, keyPromise);
37
+ return keyPromise;
38
+ };
39
+ const extractHost = (input) => {
40
+ try {
41
+ return new URL(input).host;
42
+ } catch {
43
+ const noScheme = input.replace(SCHEME_PREFIX_RE, "");
44
+ return noScheme.split("/")[0] ?? "";
45
+ }
46
+ };
47
+ const signCanonical = async (secret, canonical) => {
48
+ const cryptoKey = await importHmacKey(secret);
49
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonical));
50
+ return toBase64Url(new Uint8Array(signature));
51
+ };
52
+ const verifyCanonical = async (secret, canonical, sigBytes) => {
53
+ const cryptoKey = await importHmacKey(secret);
54
+ return crypto.subtle.verify("HMAC", cryptoKey, sigBytes, textEncoder.encode(canonical));
55
+ };
56
+
57
+ const LEADING_SLASH_RE = /^\//;
58
+ const serializeTransform = (transform) => {
59
+ if (transform === void 0) {
60
+ return "";
61
+ }
62
+ return Object.entries(transform).filter(([, value]) => value !== void 0).toSorted(([a], [b]) => (a > b ? 1 : 0) - (a < b ? 1 : 0)).map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : String(value)}`).join("&");
63
+ };
64
+ const canonicalize = (host, key, exp, transform) => `${host.toLowerCase()}
65
+ ${key}
66
+ ${String(exp)}
67
+ ${transform}`;
68
+ const encodeKey = (key) => key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
69
+ const buildSignedImageUrl = async (options) => {
70
+ const expiresInSeconds = options.expiresInSeconds ?? 60 * 60;
71
+ if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
72
+ throw new TypeError("@lunora/bindings/images: expiresInSeconds must be a positive finite number");
73
+ }
74
+ if (expiresInSeconds > MAX_SIGNED_URL_TTL_SECONDS) {
75
+ throw new TypeError(`@lunora/bindings/images: expiresInSeconds must not exceed ${String(MAX_SIGNED_URL_TTL_SECONDS)} (7 days)`);
76
+ }
77
+ let basePath = "";
78
+ try {
79
+ basePath = new URL(options.baseUrl).pathname;
80
+ } catch {
81
+ }
82
+ if (basePath !== "" && basePath !== "/") {
83
+ throw new TypeError(
84
+ `@lunora/bindings/images: baseUrl must not carry a path ("${basePath}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`
85
+ );
86
+ }
87
+ const exp = Math.floor(Date.now() / 1e3) + expiresInSeconds;
88
+ const host = extractHost(options.baseUrl);
89
+ const transform = serializeTransform(options.transform);
90
+ const normalizedKey = options.key.replace(LEADING_SLASH_RE, "");
91
+ const sig = await signCanonical(options.secret, canonicalize(host, normalizedKey, exp, transform));
92
+ const base = options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl;
93
+ const safeKey = encodeKey(normalizedKey);
94
+ const tParameter = transform === "" ? "" : `&t=${encodeURIComponent(transform)}`;
95
+ return `${base}/${safeKey}?exp=${String(exp)}&sig=${sig}${tParameter}`;
96
+ };
97
+ const verifySignedImageUrl = async (input, secret, options) => {
98
+ let url;
99
+ try {
100
+ url = input instanceof URL ? input : new URL(input);
101
+ } catch {
102
+ return { reason: "malformed", valid: false };
103
+ }
104
+ const expRaw = url.searchParams.get("exp");
105
+ const exp = expRaw === null ? Number.NaN : Number(expRaw);
106
+ const sig = url.searchParams.get("sig");
107
+ const transform = url.searchParams.get("t") ?? "";
108
+ if (!sig || !Number.isInteger(exp)) {
109
+ return { reason: "malformed", valid: false };
110
+ }
111
+ if (exp < Math.floor(Date.now() / 1e3)) {
112
+ return { reason: "expired", valid: false };
113
+ }
114
+ let key;
115
+ let sigBytes;
116
+ try {
117
+ key = url.pathname.replace(LEADING_SLASH_RE, "").split("/").map((segment) => decodeURIComponent(segment)).join("/");
118
+ sigBytes = fromBase64Url(sig);
119
+ } catch {
120
+ return { reason: "malformed", valid: false };
121
+ }
122
+ const host = options?.expectedHost === void 0 ? url.host : extractHost(options.expectedHost);
123
+ const valid = await verifyCanonical(secret, canonicalize(host, key, exp, transform), sigBytes);
124
+ if (!valid) {
125
+ return { reason: "bad_signature", valid: false };
126
+ }
127
+ return { key, transform: transform === "" ? void 0 : transform, valid: true };
128
+ };
129
+
130
+ export { buildSignedImageUrl, verifySignedImageUrl };
@@ -0,0 +1,39 @@
1
+ const UPSERT_EMBED_CONCURRENCY = 8;
2
+ const concurrentMap = async (items, limit, function_) => {
3
+ if (items.length === 0) {
4
+ return [];
5
+ }
6
+ const effectiveLimit = Math.max(1, Math.min(limit, items.length));
7
+ const results = Array.from({ length: items.length });
8
+ let cursor = 0;
9
+ let failed = false;
10
+ let firstError;
11
+ const workers = Array.from({ length: effectiveLimit }, async () => {
12
+ for (; ; ) {
13
+ if (failed) {
14
+ return;
15
+ }
16
+ const index = cursor;
17
+ cursor += 1;
18
+ if (index >= items.length) {
19
+ return;
20
+ }
21
+ try {
22
+ results[index] = await function_(items[index], index);
23
+ } catch (error) {
24
+ if (!failed) {
25
+ failed = true;
26
+ firstError = error;
27
+ }
28
+ return;
29
+ }
30
+ }
31
+ });
32
+ await Promise.all(workers);
33
+ if (failed) {
34
+ throw firstError;
35
+ }
36
+ return results;
37
+ };
38
+
39
+ export { UPSERT_EMBED_CONCURRENCY as U, concurrentMap as c };
@@ -0,0 +1,57 @@
1
+ const MAX_BLOBS = 20;
2
+ const MAX_DOUBLES = 20;
3
+ const MAX_INDEXES = 1;
4
+ const MAX_BLOB_BYTES = 16 * 1024;
5
+ const MAX_INDEX_BYTES = 96;
6
+ const TEXT_ENCODER = new TextEncoder();
7
+ const byteLengthOf = (value) => {
8
+ if (value === null) {
9
+ return 0;
10
+ }
11
+ if (typeof value === "string") {
12
+ return TEXT_ENCODER.encode(value).length;
13
+ }
14
+ return value.byteLength;
15
+ };
16
+ const assertWithin = (kind, length, max) => {
17
+ if (length > max) {
18
+ throw new RangeError(`@lunora/bindings/analytics: a data point may carry at most ${String(max)} ${kind} (got ${String(length)}).`);
19
+ }
20
+ };
21
+ const assertByteBudget = (kind, bytes, max) => {
22
+ if (bytes > max) {
23
+ throw new RangeError(`@lunora/bindings/analytics: a data point's ${kind} may total at most ${String(max)} bytes (got ${String(bytes)}).`);
24
+ }
25
+ };
26
+ const createAnalytics = (binding) => {
27
+ const writeDataPoint = (event) => {
28
+ assertWithin("blobs", event.blobs?.length ?? 0, MAX_BLOBS);
29
+ assertWithin("doubles", event.doubles?.length ?? 0, MAX_DOUBLES);
30
+ assertWithin("indexes", event.indexes?.length ?? 0, MAX_INDEXES);
31
+ const blobBytes = (event.blobs ?? []).reduce((total, blob) => total + byteLengthOf(blob), 0);
32
+ assertByteBudget("blobs", blobBytes, MAX_BLOB_BYTES);
33
+ for (const index of event.indexes ?? []) {
34
+ assertByteBudget("index", byteLengthOf(index), MAX_INDEX_BYTES);
35
+ }
36
+ binding.writeDataPoint(event);
37
+ };
38
+ const track = (name, event = {}) => {
39
+ const dimensionEntries = Object.entries(event.dimensions ?? {});
40
+ const metricEntries = Object.entries(event.metrics ?? {});
41
+ const blobs = [name, ...dimensionEntries.map(([, value]) => value)];
42
+ const doubles = metricEntries.map(([, value]) => value);
43
+ const indexes = event.index === void 0 ? [] : [event.index];
44
+ writeDataPoint({ blobs, doubles, indexes });
45
+ const dimensions = dimensionEntries.map(([field], offset) => {
46
+ return { column: `blob${String(offset + 2)}`, field };
47
+ });
48
+ const metrics = metricEntries.map(([field], offset) => {
49
+ return { column: `double${String(offset + 1)}`, field };
50
+ });
51
+ const index = event.index === void 0 ? null : { column: "index1", field: "index" };
52
+ return { dimensions, index, metrics, name };
53
+ };
54
+ return { track, writeDataPoint };
55
+ };
56
+
57
+ export { createAnalytics };
@@ -0,0 +1,140 @@
1
+ import { c as concurrentMap, U as UPSERT_EMBED_CONCURRENCY } from './concurrent-CkCEVwqP.mjs';
2
+
3
+ const createContextVectors = (lunora) => {
4
+ const upsert = async (indexName, input) => {
5
+ await lunora.upsert(indexName, {
6
+ embed: input.embed,
7
+ id: input.id,
8
+ input: input.input,
9
+ metadata: input.metadata,
10
+ namespace: input.namespace
11
+ });
12
+ };
13
+ return {
14
+ deleteByIds: async (indexName, ids) => {
15
+ await lunora.deleteByIds(indexName, ids);
16
+ },
17
+ getByIds: async (indexName, ids) => {
18
+ const records = await lunora.getByIds(indexName, ids);
19
+ return records.map((record) => {
20
+ return { id: record.id, metadata: record.metadata, values: record.values };
21
+ });
22
+ },
23
+ query: async (indexName, input) => {
24
+ const result = await lunora.query(indexName, {
25
+ embed: input.embed,
26
+ filter: input.filter,
27
+ input: input.input,
28
+ namespace: input.namespace,
29
+ // Default to "indexed" rather than "all": returning every
30
+ // metadata field by default leaks whatever was stored on the
31
+ // vector (potentially cross-tenant if namespaces aren't wired).
32
+ // Callers that need full metadata opt in explicitly via input.
33
+ returnMetadata: input.returnMetadata ?? "indexed",
34
+ topK: input.topK,
35
+ vector: input.vector
36
+ });
37
+ return {
38
+ count: result.count,
39
+ matches: result.matches.map((match) => {
40
+ return { id: match.id, metadata: match.metadata, score: match.score };
41
+ })
42
+ };
43
+ },
44
+ upsert,
45
+ upsertNow: upsert
46
+ };
47
+ };
48
+ const sharedNamespaceWarned = /* @__PURE__ */ new Set();
49
+ const warnSharedNamespace = (indexName) => {
50
+ if (sharedNamespaceWarned.has(indexName)) {
51
+ return;
52
+ }
53
+ sharedNamespaceWarned.add(indexName);
54
+ console.warn(
55
+ `[@lunora/bindings/vectors] index "${indexName}" syncs vectors without a namespace — in a
56
+ multi-tenant/sharded app this exposes one tenant's vectors (and any captured
57
+ metadata) to every other tenant, since Vectorize indexes are account-global.
58
+ Pass \`namespace\` (the shard/tenant key) on both write and query — query-side
59
+ namespace filtering is mandatory for multi-tenant apps. Single-tenant apps that
60
+ legitimately have no tenant key suppress this via { allowSharedNamespace: true }.`
61
+ );
62
+ };
63
+ const pickMetadata = (row, fields) => {
64
+ const result = {};
65
+ for (const field of fields) {
66
+ if (field in row) {
67
+ result[field] = row[field];
68
+ }
69
+ }
70
+ return result;
71
+ };
72
+ const createVectorSyncHook = (options) => {
73
+ const { allowSharedNamespace, namespace, schema, vectors } = options;
74
+ return async (event) => {
75
+ const tableDefinition = schema.tables[event.table];
76
+ const inlineIndexes = tableDefinition?.vectorIndexes ?? [];
77
+ const standaloneIndexes = Object.entries(schema.vectorIndexes).filter(([, definition]) => definition.table === event.table);
78
+ if (inlineIndexes.length === 0 && standaloneIndexes.length === 0) {
79
+ return;
80
+ }
81
+ const allIndexNames = [...inlineIndexes.map((index) => index.name), ...standaloneIndexes.map(([name]) => name)];
82
+ if (event.op === "delete") {
83
+ await Promise.all(allIndexNames.map((name) => vectors.deleteByIds(name, [event.id])));
84
+ return;
85
+ }
86
+ const row = event.doc;
87
+ if (!row) {
88
+ return;
89
+ }
90
+ const inlineWithValue = inlineIndexes.map((index) => {
91
+ return { index, value: row[index.field] };
92
+ });
93
+ const inlineToUpsert = inlineWithValue.filter((entry) => entry.value !== void 0 && entry.value !== null);
94
+ const inlineToClear = inlineWithValue.filter((entry) => entry.value === void 0 || entry.value === null);
95
+ for (const { index, value } of inlineToUpsert) {
96
+ if (typeof value !== "string") {
97
+ throw new TypeError(
98
+ `@lunora/bindings/vectors: inline index "${index.name}" expects a string source at "${index.field}" on table "${event.table}" (got ${typeof value}); use a standalone defineVectorIndex with a select() to derive text from non-string columns`
99
+ );
100
+ }
101
+ }
102
+ const operations = [
103
+ ...inlineToClear.map((entry) => async () => {
104
+ await vectors.deleteByIds(entry.index.name, [event.id]);
105
+ }),
106
+ ...inlineToUpsert.map((entry) => async () => {
107
+ if (!allowSharedNamespace && namespace === void 0) {
108
+ warnSharedNamespace(entry.index.name);
109
+ }
110
+ await vectors.upsert(entry.index.name, {
111
+ embed: entry.index.embed,
112
+ id: event.id,
113
+ input: entry.value,
114
+ metadata: entry.index.metadata ? pickMetadata(row, entry.index.metadata) : void 0,
115
+ namespace
116
+ });
117
+ }),
118
+ ...standaloneIndexes.map(([name, definition]) => async () => {
119
+ if (!allowSharedNamespace && namespace === void 0) {
120
+ warnSharedNamespace(name);
121
+ }
122
+ await vectors.upsert(name, {
123
+ embed: definition.embed,
124
+ id: event.id,
125
+ input: definition.select(row),
126
+ metadata: definition.metadata?.(row),
127
+ namespace
128
+ });
129
+ })
130
+ ];
131
+ try {
132
+ await concurrentMap(operations, UPSERT_EMBED_CONCURRENCY, async (operation) => operation());
133
+ } catch (error) {
134
+ await Promise.allSettled(allIndexNames.map((name) => vectors.deleteByIds(name, [event.id])));
135
+ throw error;
136
+ }
137
+ };
138
+ };
139
+
140
+ export { createContextVectors, createVectorSyncHook };