@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.
Files changed (58) hide show
  1. package/dist/analytics/index.d.mts +81 -85
  2. package/dist/analytics/index.d.ts +81 -85
  3. package/dist/analytics/index.mjs +1 -2
  4. package/dist/images/index.d.mts +159 -174
  5. package/dist/images/index.d.ts +159 -174
  6. package/dist/images/index.mjs +1 -3
  7. package/dist/kv/index.d.mts +74 -151
  8. package/dist/kv/index.d.ts +74 -151
  9. package/dist/kv/index.mjs +1 -2
  10. package/dist/packem_shared/AnalyticsSqlError-CeQ3A5Eb.mjs +1 -0
  11. package/dist/packem_shared/R2SqlError-ZHgOWClB.mjs +1 -0
  12. package/dist/packem_shared/SelectBuilder-DJXNSdqC.mjs +1 -0
  13. package/dist/packem_shared/SetOperation-kiI5Wnlm.mjs +1 -0
  14. package/dist/packem_shared/Sql-BfnxRway.mjs +1 -0
  15. package/dist/packem_shared/WindowExpression-VX7EEV3h.mjs +1 -0
  16. package/dist/packem_shared/WindowFunction-CL4jYy2l.mjs +1 -0
  17. package/dist/packem_shared/asc-DP_WFiAE.mjs +1 -0
  18. package/dist/packem_shared/buildImageDeliveryUrl-Brqs-dcZ.mjs +1 -0
  19. package/dist/packem_shared/buildSignedImageUrl-BV-iSJKA.mjs +4 -0
  20. package/dist/packem_shared/cap-error-body-YBKO32BF.mjs +1 -0
  21. package/dist/packem_shared/concurrent-vRmSvRpF.mjs +1 -0
  22. package/dist/packem_shared/createAnalytics-BXTNc57d.mjs +1 -0
  23. package/dist/packem_shared/createContextVectors-DiyO3pZU.mjs +6 -0
  24. package/dist/packem_shared/createImages-D7JExfqF.mjs +1 -0
  25. package/dist/packem_shared/createKv-mAHanD5g.mjs +1 -0
  26. package/dist/packem_shared/createKvIntrospector-BpRiFRFQ.mjs +1 -0
  27. package/dist/packem_shared/createPipelines-CIvqrc7E.mjs +1 -0
  28. package/dist/packem_shared/createVectorAdminIntrospector-Ct8v6PxJ.mjs +1 -0
  29. package/dist/packem_shared/createVectors-Dzv0ilKE.mjs +1 -0
  30. package/dist/pipelines/index.d.mts +24 -24
  31. package/dist/pipelines/index.d.ts +24 -24
  32. package/dist/pipelines/index.mjs +1 -1
  33. package/dist/r2sql/index.d.mts +169 -125
  34. package/dist/r2sql/index.d.ts +169 -125
  35. package/dist/r2sql/index.mjs +1 -7
  36. package/dist/vectors/index.d.mts +299 -134
  37. package/dist/vectors/index.d.ts +299 -134
  38. package/dist/vectors/index.mjs +1 -3
  39. package/package.json +3 -2
  40. package/dist/packem_shared/AnalyticsSqlError-C2nz3jpH.mjs +0 -41
  41. package/dist/packem_shared/R2SqlError-drPKSCZ3.mjs +0 -65
  42. package/dist/packem_shared/SelectBuilder-BOqJQHEv.mjs +0 -168
  43. package/dist/packem_shared/SetOperation-DmPgUL8W.mjs +0 -81
  44. package/dist/packem_shared/Sql-B3zq2YGx.mjs +0 -74
  45. package/dist/packem_shared/WindowExpression-BT_uA6g1.mjs +0 -44
  46. package/dist/packem_shared/WindowFunction-DrnuZUF6.mjs +0 -82
  47. package/dist/packem_shared/asc-DZbQCxh1.mjs +0 -16
  48. package/dist/packem_shared/buildImageDeliveryUrl-qZ7XbqTL.mjs +0 -35
  49. package/dist/packem_shared/buildSignedImageUrl-DNUFfyGP.mjs +0 -130
  50. package/dist/packem_shared/concurrent-CkCEVwqP.mjs +0 -39
  51. package/dist/packem_shared/createAnalytics-CEEI69o9.mjs +0 -57
  52. package/dist/packem_shared/createContextVectors-DwZtnPeC.mjs +0 -140
  53. package/dist/packem_shared/createImages-BzRnsz3H.mjs +0 -85
  54. package/dist/packem_shared/createKv-C8Iyu5hD.mjs +0 -145
  55. package/dist/packem_shared/createKvIntrospector-Byk4GfsY.mjs +0 -77
  56. package/dist/packem_shared/createPipelines-CfyJ6VGu.mjs +0 -10
  57. package/dist/packem_shared/createVectorAdminIntrospector-DuSvcBa5.mjs +0 -53
  58. 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 };
@@ -1,10 +0,0 @@
1
- const createPipelines = (options) => {
2
- const { binding } = options;
3
- return {
4
- send: async (records) => {
5
- await binding.send(Array.isArray(records) ? records : [records]);
6
- }
7
- };
8
- };
9
-
10
- export { createPipelines };