@lunora/bindings 1.0.0-alpha.1 → 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.
- package/LICENSE.md +6 -0
- package/dist/analytics/index.d.mts +73 -73
- package/dist/analytics/index.d.ts +73 -73
- package/dist/analytics/index.mjs +1 -1
- package/dist/images/index.d.mts +147 -147
- package/dist/images/index.d.ts +147 -147
- package/dist/images/index.mjs +3 -3
- package/dist/kv/index.d.mts +146 -52
- package/dist/kv/index.d.ts +146 -52
- package/dist/kv/index.mjs +2 -1
- package/dist/packem_shared/{AnalyticsSqlError-CGTdsi4H.mjs → AnalyticsSqlError-C2nz3jpH.mjs} +4 -5
- package/dist/packem_shared/{R2SqlError-DkQZ4Omg.mjs → R2SqlError-drPKSCZ3.mjs} +9 -9
- package/dist/packem_shared/{SelectBuilder-BGXfCF0J.mjs → SelectBuilder-BOqJQHEv.mjs} +6 -5
- package/dist/packem_shared/{SetOperation-CGRu681M.mjs → SetOperation-DmPgUL8W.mjs} +3 -2
- package/dist/packem_shared/{Sql-CkDyJ_Sc.mjs → Sql-B3zq2YGx.mjs} +21 -1
- package/dist/packem_shared/{WindowExpression-C2vj7oNX.mjs → WindowExpression-BT_uA6g1.mjs} +1 -1
- package/dist/packem_shared/{WindowFunction-CuKHfZX3.mjs → WindowFunction-DrnuZUF6.mjs} +3 -3
- package/dist/packem_shared/{asc-C6Jbaa6R.mjs → asc-DZbQCxh1.mjs} +1 -1
- package/dist/packem_shared/{buildImageDeliveryUrl-D1sVfIOP.mjs → buildImageDeliveryUrl-qZ7XbqTL.mjs} +9 -4
- package/dist/packem_shared/{buildSignedImageUrl-Otdgc_jO.mjs → buildSignedImageUrl-DNUFfyGP.mjs} +47 -30
- package/dist/packem_shared/{concurrent-Dj5sOibv.mjs → concurrent-CkCEVwqP.mjs} +17 -1
- package/dist/packem_shared/{createContextVectors-BSizpmu5.mjs → createContextVectors-DwZtnPeC.mjs} +1 -1
- package/dist/packem_shared/{createImages-CJrvqX0u.mjs → createImages-BzRnsz3H.mjs} +8 -3
- package/dist/packem_shared/{createKv-DTiSt216.mjs → createKv-C8Iyu5hD.mjs} +18 -14
- package/dist/packem_shared/createKvIntrospector-Byk4GfsY.mjs +77 -0
- package/dist/packem_shared/{createVectorAdminIntrospector-BJUOM6VW.mjs → createVectorAdminIntrospector-DuSvcBa5.mjs} +6 -4
- package/dist/packem_shared/{createVectors-LSpGoKCd.mjs → createVectors-CTSrctiK.mjs} +10 -6
- package/dist/pipelines/index.d.mts +24 -24
- package/dist/pipelines/index.d.ts +24 -24
- package/dist/r2sql/index.d.mts +122 -122
- package/dist/r2sql/index.d.ts +122 -122
- package/dist/r2sql/index.mjs +7 -7
- package/dist/vectors/index.d.mts +83 -83
- package/dist/vectors/index.d.ts +83 -83
- package/dist/vectors/index.mjs +3 -3
- package/package.json +4 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
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;
|
|
2
5
|
class Sql {
|
|
3
6
|
text;
|
|
4
7
|
constructor(text) {
|
|
@@ -11,6 +14,18 @@ class Sql {
|
|
|
11
14
|
const isSql = (value) => value instanceof Sql;
|
|
12
15
|
const raw = (text) => new Sql(text);
|
|
13
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
|
+
};
|
|
14
29
|
const lit = (value) => {
|
|
15
30
|
if (value === null || value === void 0) {
|
|
16
31
|
return "NULL";
|
|
@@ -50,5 +65,10 @@ const sql = (strings, ...values) => {
|
|
|
50
65
|
return new Sql(out);
|
|
51
66
|
};
|
|
52
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
|
+
};
|
|
53
73
|
|
|
54
|
-
export { Sql, isSql, joinSql, lit, raw, sql, toText };
|
|
74
|
+
export { Sql, assertLimit, ident, isSql, joinSql, lit, raw, sql, tableRef, toText };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { renderOrderTerm } from './asc-
|
|
2
|
-
import { lit, toText } from './Sql-
|
|
3
|
-
import WindowExpression from './WindowExpression-
|
|
1
|
+
import { renderOrderTerm } from './asc-DZbQCxh1.mjs';
|
|
2
|
+
import { lit, toText } from './Sql-B3zq2YGx.mjs';
|
|
3
|
+
import WindowExpression from './WindowExpression-BT_uA6g1.mjs';
|
|
4
4
|
|
|
5
5
|
const toArray = (value) => {
|
|
6
6
|
if (value === void 0) {
|
package/dist/packem_shared/{buildImageDeliveryUrl-D1sVfIOP.mjs → buildImageDeliveryUrl-qZ7XbqTL.mjs}
RENAMED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const ABSOLUTE_URL_RE = /^[a-z][a-z\d+\-.]*:\/\//i;
|
|
4
|
+
const FORBIDDEN_VALUE_CHARS = [",", "=", "#", "?", "/"];
|
|
2
5
|
const stripTrailingSlash = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
|
|
3
6
|
const stripLeadingSlash = (value) => value.startsWith("/") ? value.slice(1) : value;
|
|
4
7
|
const serializeTransform = (transform) => Object.entries(transform).filter(([, value]) => value !== void 0 && (typeof value === "string" || typeof value === "number")).map(([key, value]) => {
|
|
5
8
|
const serialized = String(value);
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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)\`.`
|
|
9
14
|
);
|
|
10
15
|
}
|
|
11
16
|
return `${key}=${serialized}`;
|
|
@@ -18,7 +23,7 @@ const buildImageDeliveryUrl = (options) => {
|
|
|
18
23
|
return `${base}/${encodeURIComponent(options.imageId)}/${encodeURIComponent(variant)}`;
|
|
19
24
|
}
|
|
20
25
|
if (options.key === void 0) {
|
|
21
|
-
throw new
|
|
26
|
+
throw new LunoraError("INTERNAL", "@lunora/bindings/images: buildImageDeliveryUrl requires either `imageId` or `key`");
|
|
22
27
|
}
|
|
23
28
|
const optionString = options.transform === void 0 ? "" : serializeTransform(options.transform);
|
|
24
29
|
const isAbsolute = ABSOLUTE_URL_RE.test(options.key);
|
package/dist/packem_shared/{buildSignedImageUrl-Otdgc_jO.mjs → buildSignedImageUrl-DNUFfyGP.mjs}
RENAMED
|
@@ -1,7 +1,16 @@
|
|
|
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
|
+
|
|
1
11
|
const textEncoder = new TextEncoder();
|
|
2
|
-
const
|
|
3
|
-
const SCHEME_PREFIX_RE = /^[a-z][a-
|
|
4
|
-
const LEADING_SLASH_RE = /^\//;
|
|
12
|
+
const MAX_SIGNED_URL_TTL_SECONDS = 7 * 24 * 60 * 60;
|
|
13
|
+
const SCHEME_PREFIX_RE = /^[a-z][a-z0-9+\-.]*:\/\//i;
|
|
5
14
|
const toBase64Url = (bytes) => {
|
|
6
15
|
const binary = String.fromCodePoint(...bytes);
|
|
7
16
|
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
@@ -22,16 +31,30 @@ const importHmacKey = async (secret) => {
|
|
|
22
31
|
if (cached) {
|
|
23
32
|
return cached;
|
|
24
33
|
}
|
|
25
|
-
|
|
26
|
-
const oldest = keyCache.keys().next().value;
|
|
27
|
-
if (oldest !== void 0) {
|
|
28
|
-
keyCache.delete(oldest);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
34
|
+
evictOldestEntry(keyCache, KEY_CACHE_MAX);
|
|
31
35
|
const keyPromise = crypto.subtle.importKey("raw", textEncoder.encode(secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign", "verify"]);
|
|
32
36
|
keyCache.set(secret, keyPromise);
|
|
33
37
|
return keyPromise;
|
|
34
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 = /^\//;
|
|
35
58
|
const serializeTransform = (transform) => {
|
|
36
59
|
if (transform === void 0) {
|
|
37
60
|
return "";
|
|
@@ -42,30 +65,30 @@ const canonicalize = (host, key, exp, transform) => `${host.toLowerCase()}
|
|
|
42
65
|
${key}
|
|
43
66
|
${String(exp)}
|
|
44
67
|
${transform}`;
|
|
45
|
-
const extractHost = (input) => {
|
|
46
|
-
try {
|
|
47
|
-
return new URL(input).host;
|
|
48
|
-
} catch {
|
|
49
|
-
const noScheme = input.replace(SCHEME_PREFIX_RE, "");
|
|
50
|
-
return noScheme.split("/")[0] ?? "";
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
68
|
const encodeKey = (key) => key.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
54
69
|
const buildSignedImageUrl = async (options) => {
|
|
55
70
|
const expiresInSeconds = options.expiresInSeconds ?? 60 * 60;
|
|
56
71
|
if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
|
|
57
|
-
throw new
|
|
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)`);
|
|
58
76
|
}
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
);
|
|
61
86
|
}
|
|
62
87
|
const exp = Math.floor(Date.now() / 1e3) + expiresInSeconds;
|
|
63
88
|
const host = extractHost(options.baseUrl);
|
|
64
89
|
const transform = serializeTransform(options.transform);
|
|
65
90
|
const normalizedKey = options.key.replace(LEADING_SLASH_RE, "");
|
|
66
|
-
const
|
|
67
|
-
const signature = await crypto.subtle.sign("HMAC", cryptoKey, textEncoder.encode(canonicalize(host, normalizedKey, exp, transform)));
|
|
68
|
-
const sig = toBase64Url(new Uint8Array(signature));
|
|
91
|
+
const sig = await signCanonical(options.secret, canonicalize(host, normalizedKey, exp, transform));
|
|
69
92
|
const base = options.baseUrl.endsWith("/") ? options.baseUrl.slice(0, -1) : options.baseUrl;
|
|
70
93
|
const safeKey = encodeKey(normalizedKey);
|
|
71
94
|
const tParameter = transform === "" ? "" : `&t=${encodeURIComponent(transform)}`;
|
|
@@ -97,13 +120,7 @@ const verifySignedImageUrl = async (input, secret, options) => {
|
|
|
97
120
|
return { reason: "malformed", valid: false };
|
|
98
121
|
}
|
|
99
122
|
const host = options?.expectedHost === void 0 ? url.host : extractHost(options.expectedHost);
|
|
100
|
-
const
|
|
101
|
-
const valid = await crypto.subtle.verify(
|
|
102
|
-
"HMAC",
|
|
103
|
-
cryptoKey,
|
|
104
|
-
sigBytes,
|
|
105
|
-
textEncoder.encode(canonicalize(host, key, exp, transform))
|
|
106
|
-
);
|
|
123
|
+
const valid = await verifyCanonical(secret, canonicalize(host, key, exp, transform), sigBytes);
|
|
107
124
|
if (!valid) {
|
|
108
125
|
return { reason: "bad_signature", valid: false };
|
|
109
126
|
}
|
|
@@ -6,17 +6,33 @@ const concurrentMap = async (items, limit, function_) => {
|
|
|
6
6
|
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
7
7
|
const results = Array.from({ length: items.length });
|
|
8
8
|
let cursor = 0;
|
|
9
|
+
let failed = false;
|
|
10
|
+
let firstError;
|
|
9
11
|
const workers = Array.from({ length: effectiveLimit }, async () => {
|
|
10
12
|
for (; ; ) {
|
|
13
|
+
if (failed) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
11
16
|
const index = cursor;
|
|
12
17
|
cursor += 1;
|
|
13
18
|
if (index >= items.length) {
|
|
14
19
|
return;
|
|
15
20
|
}
|
|
16
|
-
|
|
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
|
+
}
|
|
17
30
|
}
|
|
18
31
|
});
|
|
19
32
|
await Promise.all(workers);
|
|
33
|
+
if (failed) {
|
|
34
|
+
throw firstError;
|
|
35
|
+
}
|
|
20
36
|
return results;
|
|
21
37
|
};
|
|
22
38
|
|
package/dist/packem_shared/{createContextVectors-BSizpmu5.mjs → createContextVectors-DwZtnPeC.mjs}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as concurrentMap, U as UPSERT_EMBED_CONCURRENCY } from './concurrent-
|
|
1
|
+
import { c as concurrentMap, U as UPSERT_EMBED_CONCURRENCY } from './concurrent-CkCEVwqP.mjs';
|
|
2
2
|
|
|
3
3
|
const createContextVectors = (lunora) => {
|
|
4
4
|
const upsert = async (indexName, input) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const ALLOWED_OUTPUT_FORMATS = /* @__PURE__ */ new Set(["image/avif", "image/gif", "image/jpeg", "image/png", "image/webp"]);
|
|
2
4
|
const DEFAULT_MAX_DIMENSION = 1e4;
|
|
3
5
|
const DEFAULT_OUTPUT_FORMAT = "image/webp";
|
|
@@ -14,7 +16,7 @@ const toStream = (input) => {
|
|
|
14
16
|
}
|
|
15
17
|
if (isR2ObjectBody(input)) {
|
|
16
18
|
if (input.body === null) {
|
|
17
|
-
throw new
|
|
19
|
+
throw new LunoraError("INTERNAL", "@lunora/bindings/images: R2 object body is null (object missing or already consumed)");
|
|
18
20
|
}
|
|
19
21
|
return input.body;
|
|
20
22
|
}
|
|
@@ -35,7 +37,7 @@ const sanitizeTransform = (transform, maxDimension) => {
|
|
|
35
37
|
}
|
|
36
38
|
const clampDimension = (value) => {
|
|
37
39
|
if (!Number.isFinite(value) || value <= 0) {
|
|
38
|
-
throw new
|
|
40
|
+
throw new TypeError("@lunora/bindings/images: width/height must be a positive finite number");
|
|
39
41
|
}
|
|
40
42
|
return Math.min(Math.floor(value), maxDimension);
|
|
41
43
|
};
|
|
@@ -54,7 +56,10 @@ const splitOverlay = (overlay) => {
|
|
|
54
56
|
const resolveOutput = (output) => {
|
|
55
57
|
const format = output?.format ?? DEFAULT_OUTPUT_FORMAT;
|
|
56
58
|
if (!ALLOWED_OUTPUT_FORMATS.has(format)) {
|
|
57
|
-
throw new
|
|
59
|
+
throw new LunoraError(
|
|
60
|
+
"INTERNAL",
|
|
61
|
+
`@lunora/bindings/images: unsupported output format "${format}" (allowed: ${[...ALLOWED_OUTPUT_FORMATS].join(", ")})`
|
|
62
|
+
);
|
|
58
63
|
}
|
|
59
64
|
return { ...output, format };
|
|
60
65
|
};
|
|
@@ -1,19 +1,23 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const MAX_KEY_LENGTH = 512;
|
|
2
4
|
const MAX_LIST_LIMIT = 1e3;
|
|
5
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
6
|
+
const byteLength = (value) => TEXT_ENCODER.encode(value).length;
|
|
3
7
|
const validateKey = (key) => {
|
|
4
8
|
if (typeof key !== "string" || key.length === 0) {
|
|
5
|
-
throw new
|
|
9
|
+
throw new TypeError("@lunora/bindings/kv: key must be a non-empty string");
|
|
6
10
|
}
|
|
7
|
-
if (key
|
|
8
|
-
throw new
|
|
11
|
+
if (byteLength(key) > MAX_KEY_LENGTH) {
|
|
12
|
+
throw new LunoraError("INTERNAL", `@lunora/bindings/kv: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
9
13
|
}
|
|
10
14
|
if (key.includes("\0")) {
|
|
11
|
-
throw new
|
|
15
|
+
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: key contains NUL byte");
|
|
12
16
|
}
|
|
13
17
|
const segments = key.split("/");
|
|
14
18
|
for (const segment of segments) {
|
|
15
19
|
if (segment === "." || segment === "..") {
|
|
16
|
-
throw new
|
|
20
|
+
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: key contains a `.`/`..` path component");
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
};
|
|
@@ -21,16 +25,16 @@ const validatePrefix = (prefix) => {
|
|
|
21
25
|
if (prefix.length === 0) {
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
24
|
-
if (prefix
|
|
25
|
-
throw new
|
|
28
|
+
if (byteLength(prefix) > MAX_KEY_LENGTH) {
|
|
29
|
+
throw new LunoraError("INTERNAL", `@lunora/bindings/kv: prefix exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
26
30
|
}
|
|
27
31
|
if (prefix.includes("\0")) {
|
|
28
|
-
throw new
|
|
32
|
+
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: prefix contains NUL byte");
|
|
29
33
|
}
|
|
30
34
|
const segments = prefix.split("/");
|
|
31
35
|
for (const segment of segments) {
|
|
32
36
|
if (segment === "." || segment === "..") {
|
|
33
|
-
throw new
|
|
37
|
+
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: prefix contains a `.`/`..` path component");
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
};
|
|
@@ -39,15 +43,15 @@ const scopeKey = (prefix, key) => {
|
|
|
39
43
|
validateKey(key);
|
|
40
44
|
const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
41
45
|
const composed = `${trimmedPrefix}/${key}`;
|
|
42
|
-
if (composed
|
|
43
|
-
throw new
|
|
46
|
+
if (byteLength(composed) > MAX_KEY_LENGTH) {
|
|
47
|
+
throw new LunoraError("INTERNAL", `@lunora/bindings/kv: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
44
48
|
}
|
|
45
49
|
return composed;
|
|
46
50
|
};
|
|
47
51
|
const toPutOptions = (options) => {
|
|
48
52
|
const out = {};
|
|
49
53
|
if (options.expiration !== void 0 && options.expirationTtl !== void 0) {
|
|
50
|
-
throw new
|
|
54
|
+
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: `expiration` and `expirationTtl` are mutually exclusive");
|
|
51
55
|
}
|
|
52
56
|
if (options.expiration !== void 0) {
|
|
53
57
|
out.expiration = options.expiration;
|
|
@@ -62,7 +66,7 @@ const toPutOptions = (options) => {
|
|
|
62
66
|
};
|
|
63
67
|
const createKv = (options) => {
|
|
64
68
|
if (!options.namespace) {
|
|
65
|
-
throw new
|
|
69
|
+
throw new TypeError("@lunora/bindings/kv: `namespace` is required");
|
|
66
70
|
}
|
|
67
71
|
const { keyPrefix, namespace } = options;
|
|
68
72
|
if (keyPrefix !== void 0) {
|
|
@@ -103,7 +107,7 @@ const createKv = (options) => {
|
|
|
103
107
|
};
|
|
104
108
|
const list = async (listOptions = {}) => {
|
|
105
109
|
if (listOptions.limit !== void 0 && (!Number.isInteger(listOptions.limit) || listOptions.limit <= 0)) {
|
|
106
|
-
throw new
|
|
110
|
+
throw new TypeError("@lunora/bindings/kv: `limit` must be a positive integer");
|
|
107
111
|
}
|
|
108
112
|
let { prefix } = listOptions;
|
|
109
113
|
if (prefix !== void 0) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
3
|
+
const createKvIntrospector = (options) => {
|
|
4
|
+
const { namespaces } = options;
|
|
5
|
+
const resolveNamespace = (binding) => {
|
|
6
|
+
const ns = namespaces[binding];
|
|
7
|
+
if (!Object.hasOwn(namespaces, binding) || ns === void 0) {
|
|
8
|
+
throw new LunoraError("BAD_REQUEST", `@lunora/bindings/kv: no namespace registered under binding "${binding}"`);
|
|
9
|
+
}
|
|
10
|
+
return ns;
|
|
11
|
+
};
|
|
12
|
+
const listNamespaces = () => Promise.resolve(
|
|
13
|
+
Object.keys(namespaces).map((binding) => {
|
|
14
|
+
return { binding };
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
const listKeys = async (listOptions) => {
|
|
18
|
+
const ns = resolveNamespace(listOptions.namespace);
|
|
19
|
+
const result = await ns.list({ cursor: listOptions.cursor, limit: listOptions.limit, prefix: listOptions.prefix });
|
|
20
|
+
const keys = result.keys.map((entry) => {
|
|
21
|
+
return {
|
|
22
|
+
expiration: entry.expiration,
|
|
23
|
+
metadata: entry.metadata,
|
|
24
|
+
name: entry.name
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
cursor: result.list_complete ? void 0 : result.cursor,
|
|
29
|
+
keys,
|
|
30
|
+
listComplete: result.list_complete
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const getValue = async (getOptions) => {
|
|
34
|
+
const ns = resolveNamespace(getOptions.namespace);
|
|
35
|
+
const result = await ns.getWithMetadata(getOptions.key, "text");
|
|
36
|
+
return { metadata: result.metadata ?? null, value: result.value ?? null };
|
|
37
|
+
};
|
|
38
|
+
const putValue = async (putOptions) => {
|
|
39
|
+
const ns = resolveNamespace(putOptions.namespace);
|
|
40
|
+
await ns.put(putOptions.key, putOptions.value, {
|
|
41
|
+
expiration: putOptions.expiration,
|
|
42
|
+
expirationTtl: putOptions.expirationTtl,
|
|
43
|
+
metadata: putOptions.metadata
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const deleteKey = async (deleteOptions) => {
|
|
47
|
+
const ns = resolveNamespace(deleteOptions.namespace);
|
|
48
|
+
await ns.delete(deleteOptions.key);
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
deleteKey,
|
|
52
|
+
getValue,
|
|
53
|
+
listKeys,
|
|
54
|
+
listNamespaces,
|
|
55
|
+
putValue
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
const isKvNamespace = (value) => {
|
|
59
|
+
if (typeof value !== "object" || value === null) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
const candidate = value;
|
|
63
|
+
return typeof candidate.getWithMetadata === "function" && typeof candidate.list === "function" && typeof candidate.put === "function" && typeof candidate.delete === "function";
|
|
64
|
+
};
|
|
65
|
+
const createKvIntrospectorFromEnv = (env) => {
|
|
66
|
+
const namespaces = {};
|
|
67
|
+
if (typeof env === "object" && env !== null) {
|
|
68
|
+
for (const [binding, value] of Object.entries(env)) {
|
|
69
|
+
if (isKvNamespace(value)) {
|
|
70
|
+
namespaces[binding] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return createKvIntrospector({ namespaces });
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export { createKvIntrospector, createKvIntrospectorFromEnv };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const MAX_TOP_K = 20;
|
|
2
4
|
const DEFAULT_TOP_K = 10;
|
|
3
5
|
const createVectorAdminIntrospector = (options) => {
|
|
@@ -27,12 +29,12 @@ const createVectorAdminIntrospector = (options) => {
|
|
|
27
29
|
}
|
|
28
30
|
const queryIndex = async ({ name, text, topK }) => {
|
|
29
31
|
const binding = indexes[name];
|
|
30
|
-
if (binding === void 0) {
|
|
31
|
-
throw new
|
|
32
|
+
if (!Object.hasOwn(indexes, name) || binding === void 0) {
|
|
33
|
+
throw new LunoraError("INTERNAL", `@lunora/bindings/vectors: no Vectorize binding registered for index "${name}"`);
|
|
32
34
|
}
|
|
33
35
|
const embed = embedders[name];
|
|
34
|
-
if (embed === void 0) {
|
|
35
|
-
throw new
|
|
36
|
+
if (!Object.hasOwn(embedders, name) || embed === void 0) {
|
|
37
|
+
throw new LunoraError("INTERNAL", `@lunora/bindings/vectors: no embedder registered for index "${name}" — it lists read-only`);
|
|
36
38
|
}
|
|
37
39
|
const vector = await embed(text);
|
|
38
40
|
const result = await binding.query(vector, {
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
import { c as concurrentMap, U as UPSERT_EMBED_CONCURRENCY } from './concurrent-CkCEVwqP.mjs';
|
|
2
3
|
|
|
3
4
|
const resolveIndex = (indexes, name) => {
|
|
4
5
|
const index = indexes[name];
|
|
5
|
-
if (!index) {
|
|
6
|
-
throw new
|
|
6
|
+
if (!Object.hasOwn(indexes, name) || index === void 0) {
|
|
7
|
+
throw new LunoraError(
|
|
8
|
+
"INTERNAL",
|
|
9
|
+
`@lunora/bindings/vectors: no index registered for "${name}". Known indexes: ${Object.keys(indexes).join(", ") || "(none)"}`
|
|
10
|
+
);
|
|
7
11
|
}
|
|
8
12
|
return index;
|
|
9
13
|
};
|
|
@@ -22,7 +26,7 @@ const MAX_ID_BATCH = 1e3;
|
|
|
22
26
|
const MAX_UPSERT_BATCH = 1e3;
|
|
23
27
|
const createVectors = (options) => {
|
|
24
28
|
if (Object.keys(options.indexes).length === 0) {
|
|
25
|
-
throw new
|
|
29
|
+
throw new TypeError("@lunora/bindings/vectors: at least one index binding is required");
|
|
26
30
|
}
|
|
27
31
|
const upsert = async (indexName, input) => {
|
|
28
32
|
const index = resolveIndex(options.indexes, indexName);
|
|
@@ -52,7 +56,7 @@ const createVectors = (options) => {
|
|
|
52
56
|
values = input.vector;
|
|
53
57
|
} else {
|
|
54
58
|
if (!input.embed || input.input === void 0) {
|
|
55
|
-
throw new
|
|
59
|
+
throw new TypeError("@lunora/bindings/vectors: query requires either `vector` or both `input` and `embed`");
|
|
56
60
|
}
|
|
57
61
|
values = await input.embed(input.input);
|
|
58
62
|
}
|
|
@@ -81,7 +85,7 @@ const createVectors = (options) => {
|
|
|
81
85
|
const describe = async (indexName) => {
|
|
82
86
|
const index = resolveIndex(options.indexes, indexName);
|
|
83
87
|
if (!index.describe) {
|
|
84
|
-
throw new
|
|
88
|
+
throw new LunoraError("INTERNAL", `@lunora/bindings/vectors: binding for "${indexName}" does not implement describe()`);
|
|
85
89
|
}
|
|
86
90
|
return index.describe();
|
|
87
91
|
};
|
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structural types for the Cloudflare Pipelines write path (R2-backed streaming
|
|
3
|
-
* ingestion). Pipelines is the other "emit data to a sink" surface alongside
|
|
4
|
-
* Analytics Engine — telemetry/events out, no in-handler read-back. The binding
|
|
5
|
-
* is mirrored structurally (`*Like`) so a plain-object fake satisfies it in unit
|
|
6
|
-
* tests, like `@lunora/bindings/analytics`'s `AnalyticsEngineDatasetLike`.
|
|
7
|
-
*/
|
|
2
|
+
* Structural types for the Cloudflare Pipelines write path (R2-backed streaming
|
|
3
|
+
* ingestion). Pipelines is the other "emit data to a sink" surface alongside
|
|
4
|
+
* Analytics Engine — telemetry/events out, no in-handler read-back. The binding
|
|
5
|
+
* is mirrored structurally (`*Like`) so a plain-object fake satisfies it in unit
|
|
6
|
+
* tests, like `@lunora/bindings/analytics`'s `AnalyticsEngineDatasetLike`.
|
|
7
|
+
*/
|
|
8
8
|
/** One Pipelines record — a JSON object matching the stream's schema. */
|
|
9
9
|
type PipelineRecord = Record<string, unknown>;
|
|
10
10
|
/**
|
|
11
|
-
* Minimal structural projection of workers-types' `Pipeline<T>` binding. The
|
|
12
|
-
* real binding's `send` takes an array of records and resolves once accepted.
|
|
13
|
-
*/
|
|
11
|
+
* Minimal structural projection of workers-types' `Pipeline<T>` binding. The
|
|
12
|
+
* real binding's `send` takes an array of records and resolves once accepted.
|
|
13
|
+
*/
|
|
14
14
|
interface PipelineBindingLike<T extends PipelineRecord = PipelineRecord> {
|
|
15
15
|
send: (records: T[]) => Promise<void>;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* The write-side client bound to `ctx.pipelines` (the generated context imports
|
|
19
|
-
* this exact type as `import("@lunora/bindings/pipelines").PipelineClient`).
|
|
20
|
-
* Ingestion is durable, batched, and fire-and-forget — never read a record back
|
|
21
|
-
* in-handler.
|
|
22
|
-
*/
|
|
18
|
+
* The write-side client bound to `ctx.pipelines` (the generated context imports
|
|
19
|
+
* this exact type as `import("@lunora/bindings/pipelines").PipelineClient`).
|
|
20
|
+
* Ingestion is durable, batched, and fire-and-forget — never read a record back
|
|
21
|
+
* in-handler.
|
|
22
|
+
*/
|
|
23
23
|
interface PipelineClient<T extends PipelineRecord = PipelineRecord> {
|
|
24
24
|
/** Ingest one record or an array of records into the R2-backed sink. */
|
|
25
25
|
send: (records: T | T[]) => Promise<void>;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Wrap a Cloudflare Pipelines binding in the write-side {@link PipelineClient}
|
|
29
|
-
* bound to `ctx.pipelines`. The binding is `env.PIPELINES` (the `pipelines`
|
|
30
|
-
* binding the config layer recognizes; the remote pipeline name is minted with
|
|
31
|
-
* `wrangler pipelines create`).
|
|
32
|
-
*
|
|
33
|
-
* Ingestion is durable and batched: `send` accepts one record or an array and
|
|
34
|
-
* resolves once Cloudflare has accepted them for delivery to the R2-backed sink.
|
|
35
|
-
* There is no in-handler read-back — this is a fire-and-forget egress path, so
|
|
36
|
-
* it belongs on ActionCtx only (external I/O), mirroring `ctx.images`.
|
|
37
|
-
*/
|
|
28
|
+
* Wrap a Cloudflare Pipelines binding in the write-side {@link PipelineClient}
|
|
29
|
+
* bound to `ctx.pipelines`. The binding is `env.PIPELINES` (the `pipelines`
|
|
30
|
+
* binding the config layer recognizes; the remote pipeline name is minted with
|
|
31
|
+
* `wrangler pipelines create`).
|
|
32
|
+
*
|
|
33
|
+
* Ingestion is durable and batched: `send` accepts one record or an array and
|
|
34
|
+
* resolves once Cloudflare has accepted them for delivery to the R2-backed sink.
|
|
35
|
+
* There is no in-handler read-back — this is a fire-and-forget egress path, so
|
|
36
|
+
* it belongs on ActionCtx only (external I/O), mirroring `ctx.images`.
|
|
37
|
+
*/
|
|
38
38
|
declare const createPipelines: <T extends PipelineRecord = PipelineRecord>(options: {
|
|
39
39
|
binding: PipelineBindingLike<T>;
|
|
40
40
|
}) => PipelineClient<T>;
|
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structural types for the Cloudflare Pipelines write path (R2-backed streaming
|
|
3
|
-
* ingestion). Pipelines is the other "emit data to a sink" surface alongside
|
|
4
|
-
* Analytics Engine — telemetry/events out, no in-handler read-back. The binding
|
|
5
|
-
* is mirrored structurally (`*Like`) so a plain-object fake satisfies it in unit
|
|
6
|
-
* tests, like `@lunora/bindings/analytics`'s `AnalyticsEngineDatasetLike`.
|
|
7
|
-
*/
|
|
2
|
+
* Structural types for the Cloudflare Pipelines write path (R2-backed streaming
|
|
3
|
+
* ingestion). Pipelines is the other "emit data to a sink" surface alongside
|
|
4
|
+
* Analytics Engine — telemetry/events out, no in-handler read-back. The binding
|
|
5
|
+
* is mirrored structurally (`*Like`) so a plain-object fake satisfies it in unit
|
|
6
|
+
* tests, like `@lunora/bindings/analytics`'s `AnalyticsEngineDatasetLike`.
|
|
7
|
+
*/
|
|
8
8
|
/** One Pipelines record — a JSON object matching the stream's schema. */
|
|
9
9
|
type PipelineRecord = Record<string, unknown>;
|
|
10
10
|
/**
|
|
11
|
-
* Minimal structural projection of workers-types' `Pipeline<T>` binding. The
|
|
12
|
-
* real binding's `send` takes an array of records and resolves once accepted.
|
|
13
|
-
*/
|
|
11
|
+
* Minimal structural projection of workers-types' `Pipeline<T>` binding. The
|
|
12
|
+
* real binding's `send` takes an array of records and resolves once accepted.
|
|
13
|
+
*/
|
|
14
14
|
interface PipelineBindingLike<T extends PipelineRecord = PipelineRecord> {
|
|
15
15
|
send: (records: T[]) => Promise<void>;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* The write-side client bound to `ctx.pipelines` (the generated context imports
|
|
19
|
-
* this exact type as `import("@lunora/bindings/pipelines").PipelineClient`).
|
|
20
|
-
* Ingestion is durable, batched, and fire-and-forget — never read a record back
|
|
21
|
-
* in-handler.
|
|
22
|
-
*/
|
|
18
|
+
* The write-side client bound to `ctx.pipelines` (the generated context imports
|
|
19
|
+
* this exact type as `import("@lunora/bindings/pipelines").PipelineClient`).
|
|
20
|
+
* Ingestion is durable, batched, and fire-and-forget — never read a record back
|
|
21
|
+
* in-handler.
|
|
22
|
+
*/
|
|
23
23
|
interface PipelineClient<T extends PipelineRecord = PipelineRecord> {
|
|
24
24
|
/** Ingest one record or an array of records into the R2-backed sink. */
|
|
25
25
|
send: (records: T | T[]) => Promise<void>;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Wrap a Cloudflare Pipelines binding in the write-side {@link PipelineClient}
|
|
29
|
-
* bound to `ctx.pipelines`. The binding is `env.PIPELINES` (the `pipelines`
|
|
30
|
-
* binding the config layer recognizes; the remote pipeline name is minted with
|
|
31
|
-
* `wrangler pipelines create`).
|
|
32
|
-
*
|
|
33
|
-
* Ingestion is durable and batched: `send` accepts one record or an array and
|
|
34
|
-
* resolves once Cloudflare has accepted them for delivery to the R2-backed sink.
|
|
35
|
-
* There is no in-handler read-back — this is a fire-and-forget egress path, so
|
|
36
|
-
* it belongs on ActionCtx only (external I/O), mirroring `ctx.images`.
|
|
37
|
-
*/
|
|
28
|
+
* Wrap a Cloudflare Pipelines binding in the write-side {@link PipelineClient}
|
|
29
|
+
* bound to `ctx.pipelines`. The binding is `env.PIPELINES` (the `pipelines`
|
|
30
|
+
* binding the config layer recognizes; the remote pipeline name is minted with
|
|
31
|
+
* `wrangler pipelines create`).
|
|
32
|
+
*
|
|
33
|
+
* Ingestion is durable and batched: `send` accepts one record or an array and
|
|
34
|
+
* resolves once Cloudflare has accepted them for delivery to the R2-backed sink.
|
|
35
|
+
* There is no in-handler read-back — this is a fire-and-forget egress path, so
|
|
36
|
+
* it belongs on ActionCtx only (external I/O), mirroring `ctx.images`.
|
|
37
|
+
*/
|
|
38
38
|
declare const createPipelines: <T extends PipelineRecord = PipelineRecord>(options: {
|
|
39
39
|
binding: PipelineBindingLike<T>;
|
|
40
40
|
}) => PipelineClient<T>;
|