@lunora/bindings 1.0.0-alpha.8 → 1.0.0-alpha.81
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/dist/analytics/index.d.mts +81 -85
- package/dist/analytics/index.d.ts +81 -85
- package/dist/analytics/index.mjs +1 -2
- package/dist/images/index.d.mts +159 -174
- package/dist/images/index.d.ts +159 -174
- package/dist/images/index.mjs +1 -3
- package/dist/kv/index.d.mts +74 -151
- package/dist/kv/index.d.ts +74 -151
- package/dist/kv/index.mjs +1 -2
- package/dist/packem_shared/AnalyticsSqlError-CeQ3A5Eb.mjs +1 -0
- package/dist/packem_shared/R2SqlError-ZHgOWClB.mjs +1 -0
- package/dist/packem_shared/SelectBuilder-DJXNSdqC.mjs +1 -0
- package/dist/packem_shared/SetOperation-kiI5Wnlm.mjs +1 -0
- package/dist/packem_shared/Sql-BfnxRway.mjs +1 -0
- package/dist/packem_shared/WindowExpression-VX7EEV3h.mjs +1 -0
- package/dist/packem_shared/WindowFunction-CL4jYy2l.mjs +1 -0
- package/dist/packem_shared/asc-DP_WFiAE.mjs +1 -0
- package/dist/packem_shared/buildImageDeliveryUrl-Brqs-dcZ.mjs +1 -0
- package/dist/packem_shared/buildSignedImageUrl-BV-iSJKA.mjs +4 -0
- package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
- package/dist/packem_shared/concurrent-vRmSvRpF.mjs +1 -0
- package/dist/packem_shared/createAnalytics-BXTNc57d.mjs +1 -0
- package/dist/packem_shared/createContextVectors-DiyO3pZU.mjs +6 -0
- package/dist/packem_shared/createImages-D7JExfqF.mjs +1 -0
- package/dist/packem_shared/createKv-mAHanD5g.mjs +1 -0
- package/dist/packem_shared/createKvIntrospector-BpRiFRFQ.mjs +1 -0
- package/dist/packem_shared/createPipelines-CIvqrc7E.mjs +1 -0
- package/dist/packem_shared/createVectorAdminIntrospector-Ct8v6PxJ.mjs +1 -0
- package/dist/packem_shared/createVectors-Dzv0ilKE.mjs +1 -0
- package/dist/pipelines/index.d.mts +24 -24
- package/dist/pipelines/index.d.ts +24 -24
- package/dist/pipelines/index.mjs +1 -1
- package/dist/r2sql/index.d.mts +169 -125
- package/dist/r2sql/index.d.ts +169 -125
- package/dist/r2sql/index.mjs +1 -7
- package/dist/vectors/index.d.mts +299 -134
- package/dist/vectors/index.d.ts +299 -134
- package/dist/vectors/index.mjs +1 -3
- package/package.json +3 -2
- package/dist/packem_shared/AnalyticsSqlError-C2nz3jpH.mjs +0 -41
- package/dist/packem_shared/R2SqlError-drPKSCZ3.mjs +0 -65
- package/dist/packem_shared/SelectBuilder-BOqJQHEv.mjs +0 -168
- package/dist/packem_shared/SetOperation-DmPgUL8W.mjs +0 -81
- package/dist/packem_shared/Sql-B3zq2YGx.mjs +0 -74
- package/dist/packem_shared/WindowExpression-BT_uA6g1.mjs +0 -44
- package/dist/packem_shared/WindowFunction-DrnuZUF6.mjs +0 -82
- package/dist/packem_shared/asc-DZbQCxh1.mjs +0 -16
- package/dist/packem_shared/buildImageDeliveryUrl-qZ7XbqTL.mjs +0 -35
- package/dist/packem_shared/buildSignedImageUrl-DNUFfyGP.mjs +0 -130
- package/dist/packem_shared/concurrent-CkCEVwqP.mjs +0 -39
- package/dist/packem_shared/createAnalytics-CEEI69o9.mjs +0 -57
- package/dist/packem_shared/createContextVectors-DwZtnPeC.mjs +0 -140
- package/dist/packem_shared/createImages-BzRnsz3H.mjs +0 -85
- package/dist/packem_shared/createKv-C8Iyu5hD.mjs +0 -145
- package/dist/packem_shared/createKvIntrospector-Byk4GfsY.mjs +0 -77
- package/dist/packem_shared/createPipelines-CfyJ6VGu.mjs +0 -10
- package/dist/packem_shared/createVectorAdminIntrospector-DuSvcBa5.mjs +0 -53
- package/dist/packem_shared/createVectors-CTSrctiK.mjs +0 -95
|
@@ -1,130 +0,0 @@
|
|
|
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 };
|
|
@@ -1,39 +0,0 @@
|
|
|
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 };
|
|
@@ -1,57 +0,0 @@
|
|
|
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 };
|
|
@@ -1,140 +0,0 @@
|
|
|
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 };
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const ALLOWED_OUTPUT_FORMATS = /* @__PURE__ */ new Set(["image/avif", "image/gif", "image/jpeg", "image/png", "image/webp"]);
|
|
4
|
-
const DEFAULT_MAX_DIMENSION = 1e4;
|
|
5
|
-
const DEFAULT_OUTPUT_FORMAT = "image/webp";
|
|
6
|
-
const isR2ObjectBody = (input) => {
|
|
7
|
-
if (typeof input !== "object") {
|
|
8
|
-
return false;
|
|
9
|
-
}
|
|
10
|
-
const object = input;
|
|
11
|
-
return "body" in object && !(input instanceof ArrayBuffer) && !(input instanceof Uint8Array);
|
|
12
|
-
};
|
|
13
|
-
const toStream = (input) => {
|
|
14
|
-
if (input instanceof ReadableStream) {
|
|
15
|
-
return input;
|
|
16
|
-
}
|
|
17
|
-
if (isR2ObjectBody(input)) {
|
|
18
|
-
if (input.body === null) {
|
|
19
|
-
throw new LunoraError("INTERNAL", "@lunora/bindings/images: R2 object body is null (object missing or already consumed)");
|
|
20
|
-
}
|
|
21
|
-
return input.body;
|
|
22
|
-
}
|
|
23
|
-
if (input instanceof Blob) {
|
|
24
|
-
return input.stream();
|
|
25
|
-
}
|
|
26
|
-
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
27
|
-
return new ReadableStream({
|
|
28
|
-
start(controller) {
|
|
29
|
-
controller.enqueue(bytes);
|
|
30
|
-
controller.close();
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
const sanitizeTransform = (transform, maxDimension) => {
|
|
35
|
-
if (transform === void 0) {
|
|
36
|
-
return {};
|
|
37
|
-
}
|
|
38
|
-
const clampDimension = (value) => {
|
|
39
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
40
|
-
throw new TypeError("@lunora/bindings/images: width/height must be a positive finite number");
|
|
41
|
-
}
|
|
42
|
-
return Math.min(Math.floor(value), maxDimension);
|
|
43
|
-
};
|
|
44
|
-
const rest = { ...transform };
|
|
45
|
-
delete rest.draw;
|
|
46
|
-
return {
|
|
47
|
-
...rest,
|
|
48
|
-
...transform.width === void 0 ? {} : { width: clampDimension(transform.width) },
|
|
49
|
-
...transform.height === void 0 ? {} : { height: clampDimension(transform.height) }
|
|
50
|
-
};
|
|
51
|
-
};
|
|
52
|
-
const splitOverlay = (overlay) => {
|
|
53
|
-
const { image, transform, ...drawOptions } = overlay;
|
|
54
|
-
return { drawOptions, image, transform };
|
|
55
|
-
};
|
|
56
|
-
const resolveOutput = (output) => {
|
|
57
|
-
const format = output?.format ?? DEFAULT_OUTPUT_FORMAT;
|
|
58
|
-
if (!ALLOWED_OUTPUT_FORMATS.has(format)) {
|
|
59
|
-
throw new LunoraError(
|
|
60
|
-
"INTERNAL",
|
|
61
|
-
`@lunora/bindings/images: unsupported output format "${format}" (allowed: ${[...ALLOWED_OUTPUT_FORMATS].join(", ")})`
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
return { ...output, format };
|
|
65
|
-
};
|
|
66
|
-
const createImages = (options) => {
|
|
67
|
-
const { binding } = options;
|
|
68
|
-
const maxDimension = options.maxDimension ?? DEFAULT_MAX_DIMENSION;
|
|
69
|
-
return {
|
|
70
|
-
info: async (input) => binding.info(toStream(input)),
|
|
71
|
-
transform: async (input, transform, output, overlays) => {
|
|
72
|
-
const safeTransform = sanitizeTransform(transform, maxDimension);
|
|
73
|
-
const outputOptions = resolveOutput(output);
|
|
74
|
-
let transformer = binding.input(toStream(input)).transform(safeTransform);
|
|
75
|
-
for (const overlay of overlays ?? []) {
|
|
76
|
-
const { drawOptions, image, transform: overlayTransform } = splitOverlay(overlay);
|
|
77
|
-
const overlayImage = overlayTransform === void 0 ? toStream(image) : binding.input(toStream(image)).transform(sanitizeTransform(overlayTransform, maxDimension));
|
|
78
|
-
transformer = transformer.draw(overlayImage, drawOptions);
|
|
79
|
-
}
|
|
80
|
-
return transformer.output(outputOptions);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export { createImages };
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { LunoraError } from '@lunora/errors';
|
|
2
|
-
|
|
3
|
-
const MAX_KEY_LENGTH = 512;
|
|
4
|
-
const MAX_LIST_LIMIT = 1e3;
|
|
5
|
-
const TEXT_ENCODER = new TextEncoder();
|
|
6
|
-
const byteLength = (value) => TEXT_ENCODER.encode(value).length;
|
|
7
|
-
const validateKey = (key) => {
|
|
8
|
-
if (typeof key !== "string" || key.length === 0) {
|
|
9
|
-
throw new TypeError("@lunora/bindings/kv: key must be a non-empty string");
|
|
10
|
-
}
|
|
11
|
-
if (byteLength(key) > MAX_KEY_LENGTH) {
|
|
12
|
-
throw new LunoraError("INTERNAL", `@lunora/bindings/kv: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
13
|
-
}
|
|
14
|
-
if (key.includes("\0")) {
|
|
15
|
-
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: key contains NUL byte");
|
|
16
|
-
}
|
|
17
|
-
const segments = key.split("/");
|
|
18
|
-
for (const segment of segments) {
|
|
19
|
-
if (segment === "." || segment === "..") {
|
|
20
|
-
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: key contains a `.`/`..` path component");
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
const validatePrefix = (prefix) => {
|
|
25
|
-
if (prefix.length === 0) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
if (byteLength(prefix) > MAX_KEY_LENGTH) {
|
|
29
|
-
throw new LunoraError("INTERNAL", `@lunora/bindings/kv: prefix exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
30
|
-
}
|
|
31
|
-
if (prefix.includes("\0")) {
|
|
32
|
-
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: prefix contains NUL byte");
|
|
33
|
-
}
|
|
34
|
-
const segments = prefix.split("/");
|
|
35
|
-
for (const segment of segments) {
|
|
36
|
-
if (segment === "." || segment === "..") {
|
|
37
|
-
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: prefix contains a `.`/`..` path component");
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
const scopeKey = (prefix, key) => {
|
|
42
|
-
validateKey(prefix);
|
|
43
|
-
validateKey(key);
|
|
44
|
-
const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
45
|
-
const composed = `${trimmedPrefix}/${key}`;
|
|
46
|
-
if (byteLength(composed) > MAX_KEY_LENGTH) {
|
|
47
|
-
throw new LunoraError("INTERNAL", `@lunora/bindings/kv: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
|
|
48
|
-
}
|
|
49
|
-
return composed;
|
|
50
|
-
};
|
|
51
|
-
const toPutOptions = (options) => {
|
|
52
|
-
const out = {};
|
|
53
|
-
if (options.expiration !== void 0 && options.expirationTtl !== void 0) {
|
|
54
|
-
throw new LunoraError("INTERNAL", "@lunora/bindings/kv: `expiration` and `expirationTtl` are mutually exclusive");
|
|
55
|
-
}
|
|
56
|
-
if (options.expiration !== void 0) {
|
|
57
|
-
out.expiration = options.expiration;
|
|
58
|
-
}
|
|
59
|
-
if (options.expirationTtl !== void 0) {
|
|
60
|
-
out.expirationTtl = options.expirationTtl;
|
|
61
|
-
}
|
|
62
|
-
if (options.metadata !== void 0) {
|
|
63
|
-
out.metadata = options.metadata;
|
|
64
|
-
}
|
|
65
|
-
return Object.keys(out).length > 0 ? out : void 0;
|
|
66
|
-
};
|
|
67
|
-
const createKv = (options) => {
|
|
68
|
-
if (!options.namespace) {
|
|
69
|
-
throw new TypeError("@lunora/bindings/kv: `namespace` is required");
|
|
70
|
-
}
|
|
71
|
-
const { keyPrefix, namespace } = options;
|
|
72
|
-
if (keyPrefix !== void 0) {
|
|
73
|
-
validateKey(keyPrefix);
|
|
74
|
-
}
|
|
75
|
-
const resolve = (key) => {
|
|
76
|
-
if (keyPrefix === void 0) {
|
|
77
|
-
validateKey(key);
|
|
78
|
-
return key;
|
|
79
|
-
}
|
|
80
|
-
return scopeKey(keyPrefix, key);
|
|
81
|
-
};
|
|
82
|
-
const stripPrefix = (name) => {
|
|
83
|
-
if (keyPrefix === void 0) {
|
|
84
|
-
return name;
|
|
85
|
-
}
|
|
86
|
-
const trimmed = keyPrefix.endsWith("/") ? keyPrefix : `${keyPrefix}/`;
|
|
87
|
-
return name.startsWith(trimmed) ? name.slice(trimmed.length) : name;
|
|
88
|
-
};
|
|
89
|
-
const get = async (key, getOptions = {}) => {
|
|
90
|
-
const value = await namespace.get(resolve(key), { cacheTtl: getOptions.cacheTtl, type: "json" });
|
|
91
|
-
return value ?? null;
|
|
92
|
-
};
|
|
93
|
-
const getRaw = async (key, getOptions = {}) => {
|
|
94
|
-
const value = await namespace.get(resolve(key), { cacheTtl: getOptions.cacheTtl, type: getOptions.type ?? "text" });
|
|
95
|
-
return value ?? null;
|
|
96
|
-
};
|
|
97
|
-
const getWithMetadata = async (key, getOptions = {}) => {
|
|
98
|
-
const result = await namespace.getWithMetadata(resolve(key), { cacheTtl: getOptions.cacheTtl, type: "json" });
|
|
99
|
-
return { metadata: result.metadata ?? null, value: result.value ?? null };
|
|
100
|
-
};
|
|
101
|
-
const put = async (key, value, putOptions = {}) => {
|
|
102
|
-
const body = putOptions.raw ? value : JSON.stringify(value);
|
|
103
|
-
await namespace.put(resolve(key), body, toPutOptions(putOptions));
|
|
104
|
-
};
|
|
105
|
-
const deleteKey = async (key) => {
|
|
106
|
-
await namespace.delete(resolve(key));
|
|
107
|
-
};
|
|
108
|
-
const list = async (listOptions = {}) => {
|
|
109
|
-
if (listOptions.limit !== void 0 && (!Number.isInteger(listOptions.limit) || listOptions.limit <= 0)) {
|
|
110
|
-
throw new TypeError("@lunora/bindings/kv: `limit` must be a positive integer");
|
|
111
|
-
}
|
|
112
|
-
let { prefix } = listOptions;
|
|
113
|
-
if (prefix !== void 0) {
|
|
114
|
-
validatePrefix(prefix);
|
|
115
|
-
}
|
|
116
|
-
if (keyPrefix !== void 0) {
|
|
117
|
-
const base = keyPrefix.endsWith("/") ? keyPrefix : `${keyPrefix}/`;
|
|
118
|
-
prefix = listOptions.prefix === void 0 ? base : `${base}${listOptions.prefix}`;
|
|
119
|
-
validatePrefix(prefix);
|
|
120
|
-
}
|
|
121
|
-
const limit = listOptions.limit === void 0 ? void 0 : Math.min(listOptions.limit, MAX_LIST_LIMIT);
|
|
122
|
-
const result = await namespace.list({ cursor: listOptions.cursor, limit, prefix });
|
|
123
|
-
const keys = result.keys.map((entry) => {
|
|
124
|
-
return {
|
|
125
|
-
...entry,
|
|
126
|
-
name: stripPrefix(entry.name)
|
|
127
|
-
};
|
|
128
|
-
});
|
|
129
|
-
return {
|
|
130
|
-
cursor: result.list_complete ? void 0 : result.cursor,
|
|
131
|
-
keys,
|
|
132
|
-
listComplete: result.list_complete
|
|
133
|
-
};
|
|
134
|
-
};
|
|
135
|
-
return {
|
|
136
|
-
delete: deleteKey,
|
|
137
|
-
get,
|
|
138
|
-
getRaw,
|
|
139
|
-
getWithMetadata,
|
|
140
|
-
list,
|
|
141
|
-
put
|
|
142
|
-
};
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
export { createKv, scopeKey };
|
|
@@ -1,77 +0,0 @@
|
|
|
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 };
|